@riceawa/dsh-lan-gateway 0.6.2 → 0.7.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/README.md +14 -12
- package/lib/client.js +83 -143
- package/lib/client.js.map +1 -1
- package/lib/index.js +52 -2
- package/package.json +21 -23
- package/src/client/index.ts +37 -26
- package/src/client/lan-gateway-card.tsx +173 -197
- package/src/gateway.ts +31 -5
- package/src/request-policy.ts +36 -0
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["useState"],"sources":["../src/config-fields.ts","../src/client/lan-gateway-card.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * The `lan-gateway` configuration field table: which settings keys exist, how\n * each one is rendered, and how a draft string becomes a config value or a\n * clear.\n *\n * The host and the browser card share this module. The host derives the key set\n * its config route accepts and the set an empty value clears from it; the card\n * renders its controls from it. A field therefore cannot be editable on one\n * side and unknown on the other, which is what let the card silently rewrite\n * keys it never showed.\n *\n * It also carries `MIN_PASSWORD_LENGTH`: the login password is deliberately not\n * a field here (it is a secret in `state.json`, not a settings key), but its\n * length bound is a contract the card's form and the host's password route\n * must not disagree about.\n *\n * Because both halves import it, this module must stay dependency-free and\n * side-effect-free: no schemastery (the host schema remains the validating\n * authority, not this table), no node built-ins, no DOM, no I/O.\n *\n * @module @riceawa/dsh-lan-gateway/config-fields\n */\n\n/**\n * The wire shape of the `lan-gateway` config section, as the Settings card sees\n * it. Deliberately narrower than the host's `Config`: `cookieName` and the\n * retired `authRequired` are not card-editable. The config route applies a\n * patch, so a key absent here is left untouched rather than reset.\n */\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/** A settings key this table knows how to edit. */\nexport type ConfigFieldKey = keyof LanGatewaySettings\n\n/**\n * Minimum login-password length, in characters. The credential is not a config\n * field (it lives in `state.json`, never in the schema), but the bound belongs\n * here all the same: the card's password form and the host's password route\n * both read it, so a draft the card accepts is never answered with a 400.\n */\nexport const MIN_PASSWORD_LENGTH = 8\n\n/** How a field is rendered and parsed. */\nexport type FieldKind = 'boolean' | 'number' | 'text' | 'cidrs' | 'select' | 'tristate'\n\nexport interface FieldDef {\n readonly field: ConfigFieldKey\n readonly kind: FieldKind\n /** An empty draft clears the key back to the composition layer. */\n readonly optional?: boolean\n /** The accepted values of a `select` field. */\n readonly options?: readonly string[]\n}\n\n/** The three states of a tri-state field, in display order. */\nexport const TRISTATE_OPTIONS = ['auto', 'true', 'false'] as const\n\n/**\n * The editable settings, in display order. Adding a config key means adding it\n * here (the host whitelist and the card's controls both follow), to the\n * `Config` schema in `index.ts`, and to `listenerKey` when it changes listener\n * behavior.\n */\nexport const 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\n/** Every settings key the config route accepts; anything else is ignored. */\nexport const CONFIG_FIELD_KEYS: ReadonlySet<string> = new Set(FIELDS.map(def => def.field as string))\n\n/** Keys an empty submitted value clears back to the composition layer. */\nexport const OPTIONAL_CONFIG_KEYS: ReadonlySet<string> = new Set(\n FIELDS.filter(def => def.optional === true).map(def => def.field as string),\n)\n\n/** Render a stored value as draft text. */\nexport function 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\n/** One field's contribution to a config patch. */\nexport type Write = { kind: 'set'; value: unknown } | { kind: 'clear' }\n\n/** Parse draft text into a value for the POST body; undefined blocks saving. */\nexport function 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 * The lan-gateway settings card, rendered by the official DSH Plugins page\n * through its `plugins.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 only platform service it needs is\n * the `slots` service 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// Type-only. The Plugins page owns the `plugins.item` contract, and its own\n// doc says a registrant merges that contract with `import type` instead of\n// importing the package at runtime. Taking the contract from its owner is also\n// what turns the next upstream rename of this slot into a compile error here,\n// rather than a card that quietly stops rendering.\nimport type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'\nimport {\n FIELDS,\n MIN_PASSWORD_LENGTH,\n TRISTATE_OPTIONS,\n formatValue,\n parseValue,\n type FieldDef,\n type LanGatewaySettings,\n} from '../config-fields.ts'\n\n/**\n * Props the renderer binds for this card. The Plugins page asks for either the\n * card's one-liner (`summary`) or the body of its own page (`page`), and draws\n * the page's title, icon, and crumb itself. The card needs no injected face —\n * it fetches its own route.\n */\nexport type LanGatewayCardProps = PropsRuntime<'plugins.item'>\n\n/**\n * The card's field table and value codecs live in `config-fields.ts`, shared\n * with the host: the host's config route decides which submitted keys are\n * editable and which empty value means \"clear\", and a table duplicated here\n * would let the two disagree about a field the card can render but the route\n * would refuse. Re-exported so the existing tests keep their import path.\n */\nexport { FIELDS, TRISTATE_OPTIONS, formatValue, parseValue }\nexport type { FieldDef, LanGatewaySettings }\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 /** Whether a login credential exists. Never the credential itself. */\n passwordSet?: boolean\n}\n\n/** Why a password draft cannot be submitted yet. */\nexport type PasswordProblem = 'tooShort' | 'mismatch'\n\n/**\n * The card's password badge state.\n *\n * Three states, not two: a host that predates the password route does not\n * report `passwordSet` at all, and `undefined` must read as \"unknown\" rather\n * than \"not set\" — a client refreshed against a running old host would\n * otherwise announce that a gateway which is demonstrably running (it cannot\n * start without a credential) has no password.\n */\nexport type PasswordStatus = 'set' | 'unset' | 'unknown'\n\n/**\n * Classify the host's `passwordSet` for the badge.\n * @param passwordSet - the snapshot's flag, or `undefined` when absent.\n * @returns `set` / `unset` / `unknown` (absent field).\n */\nexport function passwordStatus(passwordSet: boolean | undefined): PasswordStatus {\n if (passwordSet === undefined) return 'unknown'\n return passwordSet ? 'set' : 'unset'\n}\n\n/**\n * Judge a password draft the way the host's password route will, so a draft the\n * card enables is never answered with a 400. The length bound is the shared\n * {@link MIN_PASSWORD_LENGTH}; the confirmation is a UI concern and is checked\n * here rather than server-side (the host is told one password, and storing only\n * what was typed twice is the browser's job).\n * @param password - the new password draft.\n * @param confirm - the confirmation draft.\n * @returns the reason it cannot be submitted, or `null` when it can.\n */\nexport function passwordProblem(password: string, confirm: string): PasswordProblem | null {\n if (password.length < MIN_PASSWORD_LENGTH) return 'tooShort'\n if (password !== confirm) return 'mismatch'\n return 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 readOnly: string\n saveFailed: string\n loadFailed: string\n emptyMeansClear: string\n running: string\n stopped: string\n tls: string\n lastError: string\n passwordSection: string\n passwordHint: string\n passwordNew: string\n passwordConfirm: string\n passwordSet: string\n passwordUnset: string\n passwordUnknown: string\n passwordRequired: string\n passwordHostStale: string\n passwordChange: string\n passwordChanged: string\n passwordFailed: string\n passwordStaleHost: string\n passwordTooShort: string\n passwordMismatch: 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 readOnly: '网关设置只能在宿主机本机打开 dsh web 时修改:配置路由仅监听回环地址,经网关远程访问的浏览器会被拒绝。远程请改用 lan_gateway 工具。',\n saveFailed: '保存未生效,请检查输入后重试。',\n loadFailed: '无法读取网关配置',\n emptyMeansClear: '留空 = 使用默认',\n running: '运行中',\n stopped: '已停止',\n tls: 'TLS',\n lastError: '上次错误',\n passwordSection: '登录密码',\n passwordHint: '当前密码不会显示(也读不出来)。输入两次新密码后直接覆盖原密码;改密会递增会话代次,所有已登录会话与已建立的 WebSocket 立即失效。仅在从本机 loopback 打开 dsh web 时可改。',\n passwordNew: '新密码(至少 8 位)',\n passwordConfirm: '再次输入新密码',\n passwordSet: '已设置',\n passwordUnset: '未设置',\n passwordUnknown: '状态未知(宿主端较旧)',\n passwordRequired: '未设置密码时网关拒绝启动。',\n passwordHostStale: '宿主端没有报告密码状态,说明 dsh web 还在运行旧版本:重启后这里才会显示「已设置 / 未设置」,这个改密表单也才会生效。',\n passwordChange: '修改密码',\n passwordChanged: '密码已更新:旧密码立即失效,所有已登录会话已作废。',\n passwordFailed: '修改密码失败,请重试。',\n passwordStaleHost: '宿主端没有响应这个接口:可能还没重启 dsh web 加载新版本。重启后再试;重启前仍可用 lan_gateway 工具改密。',\n passwordTooShort: '密码至少 8 位。',\n passwordMismatch: '两次输入不一致。',\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)。留空 = 未声明。注意:登录限流以 TCP 源地址为键,代理之后所有浏览器共用一个额度(5 次/分钟)',\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 readOnly: 'Gateway settings can only be changed where dsh web runs locally: the config route listens on loopback only, so a browser reaching dsh through the gateway is refused. Use the lan_gateway tool remotely.',\n saveFailed: 'The save did not land — check the inputs and retry.',\n loadFailed: 'Cannot read the gateway configuration',\n emptyMeansClear: 'Empty = default',\n running: 'Running',\n stopped: 'Stopped',\n tls: 'TLS',\n lastError: 'Last error',\n passwordSection: 'Login password',\n passwordHint: 'The current password is never shown (it cannot be read back). Enter a new one twice to overwrite it; changing it advances the session epoch, so every signed-in session and live WebSocket is invalidated at once. Only changeable where dsh web runs on loopback.',\n passwordNew: 'New password (min 8 chars)',\n passwordConfirm: 'Repeat new password',\n passwordSet: 'Set',\n passwordUnset: 'Not set',\n passwordUnknown: 'Unknown (older host)',\n passwordRequired: 'The gateway refuses to start without a password.',\n passwordHostStale: 'The host did not report a password state, so dsh web is still running the previous build: restart it to get Set / Not set here and to make this form take effect.',\n passwordChange: 'Change password',\n passwordChanged: 'Password updated: the old one no longer works and every signed-in session was revoked.',\n passwordFailed: 'The password change failed — retry.',\n passwordStaleHost: 'The host did not answer this endpoint — it may still be running the previous build. Restart dsh web and retry; the lan_gateway tool can change the password meanwhile.',\n passwordTooShort: 'At least 8 characters.',\n passwordMismatch: 'The two entries do not match.',\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. Note: login rate limiting keys on the TCP source address, so behind a proxy every browser shares one budget (5/min)',\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. Applies to the next issuance only: an existing certificate is reused until it expires',\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). Applies to the next issuance only: an existing certificate is reused until it expires',\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 * The card's title in the browser's language, for the Plugins page's list\n * entry. A thunk so the label follows the page's locale without re-registering.\n * @returns the localized card title.\n */\nexport function cardTitle(): string {\n return labels().title\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 *\n * `view` swaps between the card's one-liner and its page body, so the branch\n * sits after the hooks: the Plugins page re-renders one contribution under the\n * other view when the card is opened.\n * @param props - the view the Plugins page is asking for.\n * @returns the one-liner, 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 // The password section is its own submit path: a password is not a settings\n // key, so it must not ride the config patch (nor light up the card's \"未保存\"\n // badge for every keystroke in a password box).\n const [password, setPasswordDraft] = useState('')\n const [passwordConfirm, setPasswordConfirmDraft] = useState('')\n const [passwordBusy, setPasswordBusy] = useState(false)\n const [passwordError, setPasswordError] = useState<string | null>(null)\n const [passwordNotice, setPasswordNotice] = 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 // The Plugins page lists this plugin as one card and opens its own page on\n // demand: `summary` is the one-liner the list shows, `page` the body. The\n // hooks above run for both views, because the same contribution flips\n // between them.\n if (props.view === 'summary') return t.description\n\n // A remote browser reaches this card through the gateway, which answers 403\n // for the plugin's own prefix by design, so the route is unreachable exactly\n // where a user is most likely to go looking for the setting. Rendering\n // nothing left them with a blank entry and no way to tell a missing card from\n // a broken one; say what is wrong and where the card does work instead.\n if (loadFailed) {\n return (\n <div style={styles.card}>\n <div style={styles.header}>\n <span style={styles.headerTop}>\n <span style={styles.name}>{t.title}</span>\n </span>\n <span style={styles.description}>{t.loadFailed}</span>\n </div>\n <div style={styles.body}>\n <p style={styles.hint}>{t.readOnly}</p>\n </div>\n </div>\n )\n }\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 const passwordDraftProblem = passwordProblem(password, passwordConfirm)\n const passwordTyping = password !== '' || passwordConfirm !== ''\n const passwordBadge = passwordStatus(route.passwordSet)\n\n /** Adopt the host's post-write snapshot, keeping the last known values. */\n const adopt = (body: Partial<RouteState>): void => {\n if (body.config === undefined) return\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 // Absent is a state of its own (an older host does not report the field),\n // so it must not be coerced into `false`: that is what made a running,\n // password-protected gateway render as \"not set\".\n ...(body.passwordSet !== undefined ? { passwordSet: body.passwordSet } : {}),\n })\n }\n\n /**\n * Overwrite the login password. Deliberately writes only to\n * `/lan-gateway/password`: the credential is not part of the config patch, so\n * a change here can never disturb an unsaved settings draft, and the old\n * password is neither sent nor requested.\n */\n const changePassword = async (): Promise<void> => {\n if (passwordBusy || passwordProblem(password, passwordConfirm) !== null) return\n setPasswordBusy(true)\n setPasswordError(null)\n setPasswordNotice(null)\n try {\n const response = await fetch('/lan-gateway/password', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ password }),\n })\n // A host that predates this route answers the SPA fallback (HTML, 200),\n // not a 404 — so \"not our JSON\" is the signal, and it must not read as a\n // wrong password or a network failure.\n const body = await response.json().catch(() => null) as (Partial<RouteState> & { error?: string }) | null\n if (!response.ok) {\n setPasswordError(body?.error ?? (response.status === 404 ? t.passwordStaleHost : `HTTP ${response.status}`))\n return\n }\n if (body === null) {\n setPasswordError(t.passwordStaleHost)\n return\n }\n // Clear the boxes the moment the write lands: the new credential is now\n // the stored one, and leaving it on screen is the one thing a password\n // field should not do.\n setPasswordDraft('')\n setPasswordConfirmDraft('')\n setPasswordNotice(t.passwordChanged)\n adopt(body)\n } catch {\n setPasswordError(t.passwordFailed)\n } finally {\n setPasswordBusy(false)\n }\n }\n\n const save = async (): Promise<void> => {\n if (!dirty || saving || invalid()) return\n setSaving(true)\n setFailed(null)\n try {\n // A patch of the edited fields only, never the whole config: the card\n // cannot express every key the section may hold (a custom `cookieName`,\n // say), and posting a synthesized full config made the route treat those\n // keys as submitted — resetting each one to its schema default.\n const patch: Record<string, unknown> = {}\n for (const [field, text] of Object.entries(drafts)) {\n const def = FIELDS.find(f => f.field === field)\n if (def === undefined) continue\n const write = parseValue(def, text ?? '')\n if (write === undefined) continue\n patch[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(patch),\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 adopt(body)\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 <div 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 <div style={styles.section}>\n <div style={styles.sectionHead}>\n <span style={styles.label}>{t.passwordSection}</span>\n <span style={passwordBadge === 'unset' ? styles.badgeAlert : styles.badge}>\n {passwordBadge === 'set'\n ? t.passwordSet\n : passwordBadge === 'unset' ? t.passwordUnset : t.passwordUnknown}\n </span>\n </div>\n <p style={styles.hint}>{t.passwordHint}</p>\n {passwordBadge === 'unset' ? <p style={styles.error}>{t.passwordRequired}</p> : null}\n {passwordBadge === 'unknown' ? <p style={styles.hint}>{t.passwordHostStale}</p> : null}\n <div style={styles.passwordRow}>\n <input\n id=\"lan-gw-password\"\n type=\"password\"\n autoComplete=\"new-password\"\n aria-label={t.passwordNew}\n style={passwordInput}\n placeholder={t.passwordNew}\n value={password}\n disabled={passwordBusy}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n setPasswordDraft(e.target.value)\n setPasswordError(null)\n setPasswordNotice(null)\n }}\n />\n <input\n id=\"lan-gw-password-confirm\"\n type=\"password\"\n autoComplete=\"new-password\"\n aria-label={t.passwordConfirm}\n style={passwordInput}\n placeholder={t.passwordConfirm}\n value={passwordConfirm}\n disabled={passwordBusy}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n setPasswordConfirmDraft(e.target.value)\n setPasswordError(null)\n setPasswordNotice(null)\n }}\n />\n </div>\n <div style={styles.passwordFoot}>\n {passwordError !== null\n ? <p style={styles.error} role=\"alert\">{passwordError}</p>\n : passwordNotice !== null\n ? <p style={styles.notice} role=\"status\">{passwordNotice}</p>\n : passwordTyping && passwordDraftProblem !== null\n ? (\n <p style={styles.error} role=\"status\">\n {passwordDraftProblem === 'tooShort' ? t.passwordTooShort : t.passwordMismatch}\n </p>\n )\n : null}\n <button\n type=\"button\"\n style={styles.save}\n disabled={passwordBusy || passwordDraftProblem !== null}\n onClick={() => { void changePassword() }}\n >\n {passwordBusy ? t.saving : t.passwordChange}\n </button>\n </div>\n </div>\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 </div>\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 // The password block sits inside the card body, ahead of the config fields:\n // it is the setting an operator comes here for, and it owns its own submit\n // button (a password is not part of the settings patch), so it is separated\n // by a rule rather than merged into the field list.\n section: {\n display: 'flex',\n flexDirection: 'column',\n gap: '8px',\n padding: '14px 0',\n borderBottom: `1px solid ${L.border}`,\n },\n sectionHead: { display: 'flex', alignItems: 'center', gap: '8px' },\n badge: {\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 badgeAlert: {\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.error,\n },\n notice: { flex: 1, minWidth: 0, margin: 0, fontSize: '12px', lineHeight: 1.5, color: L.labelSecondary },\n passwordRow: { display: 'flex', flexWrap: 'wrap', gap: '8px' },\n passwordFoot: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-end',\n gap: '8px',\n marginTop: '2px',\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/** The shared input style plus the growth two side-by-side password boxes need. */\nconst passwordInput: React.CSSProperties = { ...styles.input, flex: '1 1 160px' }\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 Plugins\n * page (`plugins.item` slot) so port, CIDRs, auth, and TLS stay adjustable\n * 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'\n// Type-only, and never bundled: the Plugins page's slot contract (`plugins.item`)\n// and the settings domain's `configForms` service both resolve from the web\n// shell's frozen module table. Importing them is what subjects the registration\n// below to the platform's own contract instead of a local copy that drifts the\n// next time upstream renames a slot.\nimport type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { LanGatewayCard, cardTitle } from './lan-gateway-card.tsx'\n\nexport const name = 'dsh-lan-gateway'\n\n/**\n * The profile entry id this plugin's bundle patch composes it under, and the\n * settings namespace dsh ≥ 0.1.7 addresses every write by.\n */\nconst ENTRY_ID = 'dsh-lan-gateway'\n\n/** The slots service the card rides, and the settings mirror the gate reads. */\nexport const inject = ['slots', 'configForms']\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 page. Like ModLens it registers with no\n // inject face and fetches its own loopback config route, so it depends on no\n // settings, locale, or connection service.\n //\n // dsh 0.1.7 replaced the namespace-keyed `settings.plugin.item` slot with the\n // list slot `plugins.item`, which is where a host-plane plugin's own\n // configuration page belongs (\"one companion package per host-plane\n // namespace\"); the page renders the contribution as the card's one-liner and,\n // once opened, as the body of the plugin's own page. Registering into the\n // retired slot left the card invisible on 0.1.7.\n //\n // `whileServed` keeps the entry off the page until the Host's settings mirror\n // serves this plugin's entry. That is the one gate worth having: without a\n // Loader entry there is nothing to write to, and the card would appear only\n // to fail every save with the route's 409.\n ctx.effect(() => ctx.configForms.whileServed([ENTRY_ID], () =>\n ctx.slots.inject('plugins.item', () => ctx.slots.register({\n name: 'plugins.item',\n id: ENTRY_ID,\n order: 30,\n label: () => cardTitle(),\n }, LanGatewayCard))))\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["useState"],"sources":["../src/config-fields.ts","../src/client/lan-gateway-card.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * The `lan-gateway` configuration field table: which settings keys exist, how\n * each one is rendered, and how a draft string becomes a config value or a\n * clear.\n *\n * The host and the browser card share this module. The host derives the key set\n * its config route accepts and the set an empty value clears from it; the card\n * renders its controls from it. A field therefore cannot be editable on one\n * side and unknown on the other, which is what let the card silently rewrite\n * keys it never showed.\n *\n * It also carries `MIN_PASSWORD_LENGTH`: the login password is deliberately not\n * a field here (it is a secret in `state.json`, not a settings key), but its\n * length bound is a contract the card's form and the host's password route\n * must not disagree about.\n *\n * Because both halves import it, this module must stay dependency-free and\n * side-effect-free: no schemastery (the host schema remains the validating\n * authority, not this table), no node built-ins, no DOM, no I/O.\n *\n * @module @riceawa/dsh-lan-gateway/config-fields\n */\n\n/**\n * The wire shape of the `lan-gateway` config section, as the Settings card sees\n * it. Deliberately narrower than the host's `Config`: `cookieName` and the\n * retired `authRequired` are not card-editable. The config route applies a\n * patch, so a key absent here is left untouched rather than reset.\n */\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/** A settings key this table knows how to edit. */\nexport type ConfigFieldKey = keyof LanGatewaySettings\n\n/**\n * Minimum login-password length, in characters. The credential is not a config\n * field (it lives in `state.json`, never in the schema), but the bound belongs\n * here all the same: the card's password form and the host's password route\n * both read it, so a draft the card accepts is never answered with a 400.\n */\nexport const MIN_PASSWORD_LENGTH = 8\n\n/** How a field is rendered and parsed. */\nexport type FieldKind = 'boolean' | 'number' | 'text' | 'cidrs' | 'select' | 'tristate'\n\nexport interface FieldDef {\n readonly field: ConfigFieldKey\n readonly kind: FieldKind\n /** An empty draft clears the key back to the composition layer. */\n readonly optional?: boolean\n /** The accepted values of a `select` field. */\n readonly options?: readonly string[]\n}\n\n/** The three states of a tri-state field, in display order. */\nexport const TRISTATE_OPTIONS = ['auto', 'true', 'false'] as const\n\n/**\n * The editable settings, in display order. Adding a config key means adding it\n * here (the host whitelist and the card's controls both follow), to the\n * `Config` schema in `index.ts`, and to `listenerKey` when it changes listener\n * behavior.\n */\nexport const 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\n/** Every settings key the config route accepts; anything else is ignored. */\nexport const CONFIG_FIELD_KEYS: ReadonlySet<string> = new Set(FIELDS.map(def => def.field as string))\n\n/** Keys an empty submitted value clears back to the composition layer. */\nexport const OPTIONAL_CONFIG_KEYS: ReadonlySet<string> = new Set(\n FIELDS.filter(def => def.optional === true).map(def => def.field as string),\n)\n\n/** Render a stored value as draft text. */\nexport function 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\n/** One field's contribution to a config patch. */\nexport type Write = { kind: 'set'; value: unknown } | { kind: 'clear' }\n\n/** Parse draft text into a value for the POST body; undefined blocks saving. */\nexport function 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 * The lan-gateway configuration card, rendered by the official DSH Plugins page\n * on this bundle's own page through its `plugins.bundle.config` 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 only platform service it needs is\n * the `slots` service every plugin already has.\n *\n * The page — not the card — draws the plugin's title, icon, and crumb, and the\n * card is the page body: `view: 'summary'` renders nothing (bundle\n * configuration is `page`-only).\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// Type-only. The Plugins page owns the `plugins.bundle.config` contract, and its\n// own doc says a registrant merges that contract with `import type` instead of\n// importing the package at runtime. Taking the contract from its owner is also\n// what turns the next upstream rename of this slot into a compile error here,\n// rather than a card that quietly stops rendering.\nimport type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'\nimport {\n FIELDS,\n MIN_PASSWORD_LENGTH,\n TRISTATE_OPTIONS,\n formatValue,\n parseValue,\n type FieldDef,\n type LanGatewaySettings,\n} from '../config-fields.ts'\n\n/**\n * Props the renderer binds for this card. The Plugins page asks a bundle's\n * configuration entry only for the body of its own page (`page`); the shared\n * contract still carries `summary`, which bundle configuration never renders.\n * The card needs no injected face — it fetches its own route.\n */\nexport type LanGatewayCardProps = PropsRuntime<'plugins.bundle.config'>\n\n/**\n * The card's field table and value codecs live in `config-fields.ts`, shared\n * with the host: the host's config route decides which submitted keys are\n * editable and which empty value means \"clear\", and a table duplicated here\n * would let the two disagree about a field the card can render but the route\n * would refuse. Re-exported so the existing tests keep their import path.\n */\nexport { FIELDS, TRISTATE_OPTIONS, formatValue, parseValue }\nexport type { FieldDef, LanGatewaySettings }\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 /** Whether a login credential exists. Never the credential itself. */\n passwordSet?: boolean\n}\n\n/** Why a password draft cannot be submitted yet. */\nexport type PasswordProblem = 'tooShort' | 'mismatch'\n\n/**\n * The card's password badge state.\n *\n * Three states, not two: a host that predates the password route does not\n * report `passwordSet` at all, and `undefined` must read as \"unknown\" rather\n * than \"not set\" — a client refreshed against a running old host would\n * otherwise announce that a gateway which is demonstrably running (it cannot\n * start without a credential) has no password.\n */\nexport type PasswordStatus = 'set' | 'unset' | 'unknown'\n\n/**\n * Classify the host's `passwordSet` for the badge.\n * @param passwordSet - the snapshot's flag, or `undefined` when absent.\n * @returns `set` / `unset` / `unknown` (absent field).\n */\nexport function passwordStatus(passwordSet: boolean | undefined): PasswordStatus {\n if (passwordSet === undefined) return 'unknown'\n return passwordSet ? 'set' : 'unset'\n}\n\n/**\n * Judge a password draft the way the host's password route will, so a draft the\n * card enables is never answered with a 400. The length bound is the shared\n * {@link MIN_PASSWORD_LENGTH}; the confirmation is a UI concern and is checked\n * here rather than server-side (the host is told one password, and storing only\n * what was typed twice is the browser's job).\n * @param password - the new password draft.\n * @param confirm - the confirmation draft.\n * @returns the reason it cannot be submitted, or `null` when it can.\n */\nexport function passwordProblem(password: string, confirm: string): PasswordProblem | null {\n if (password.length < MIN_PASSWORD_LENGTH) return 'tooShort'\n if (password !== confirm) return 'mismatch'\n return null\n}\n\n/**\n * Whether the config route's refusal came from the gateway rather than from dsh\n * itself.\n *\n * The gateway owns the `/lan-gateway*` prefix and answers it with a bare 403\n * `forbidden` whenever the client is not on the host; dsh, when it answers the\n * route at all, does not produce that pair. Telling the two apart is what lets\n * the card say \"you are not on the host loopback\" instead of the generic\n * \"cannot read the configuration\".\n * @param status - the HTTP status of the failed read.\n * @param body - that response's body.\n * @returns true when the gateway itself refused the read.\n */\nexport function refusedByGateway(status: number, body: string): boolean {\n return status === 403 && body.trim() === 'forbidden'\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 readOnly: string\n readOnlyGateway: string\n saveFailed: string\n loadFailed: string\n emptyMeansClear: string\n running: string\n stopped: string\n tls: string\n lastError: string\n passwordSection: string\n passwordHint: string\n passwordNew: string\n passwordConfirm: string\n passwordSet: string\n passwordUnset: string\n passwordUnknown: string\n passwordRequired: string\n passwordHostStale: string\n passwordChange: string\n passwordChanged: string\n passwordFailed: string\n passwordStaleHost: string\n passwordTooShort: string\n passwordMismatch: 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 readOnly: '读不到网关配置:这个接口只在宿主机本机应答——Host 必须是回环地址且同源。请在宿主机上打开 dsh web 时修改,远程请改用 lan_gateway 工具。',\n readOnlyGateway: '网关拒绝了这次读取:/lan-gateway/* 管理面由网关独占,只放行「TCP 来源为回环 且 地址写的是 127.0.0.1 / localhost」的浏览器。你现在不是从宿主机回环地址访问的——请在宿主机上用 127.0.0.1 或 localhost 打开本页(局域网 IP、域名都不算),或远程改用 lan_gateway 工具。',\n saveFailed: '保存未生效,请检查输入后重试。',\n loadFailed: '无法读取网关配置',\n emptyMeansClear: '留空 = 使用默认',\n running: '运行中',\n stopped: '已停止',\n tls: 'TLS',\n lastError: '上次错误',\n passwordSection: '登录密码',\n passwordHint: '当前密码不会显示(也读不出来)。输入两次新密码后直接覆盖原密码;改密会递增会话代次,所有已登录会话与已建立的 WebSocket 立即失效。仅在从本机 loopback 打开 dsh web 时可改。',\n passwordNew: '新密码(至少 8 位)',\n passwordConfirm: '再次输入新密码',\n passwordSet: '已设置',\n passwordUnset: '未设置',\n passwordUnknown: '状态未知(宿主端较旧)',\n passwordRequired: '未设置密码时网关拒绝启动。',\n passwordHostStale: '宿主端没有报告密码状态,说明 dsh web 还在运行旧版本:重启后这里才会显示「已设置 / 未设置」,这个改密表单也才会生效。',\n passwordChange: '修改密码',\n passwordChanged: '密码已更新:旧密码立即失效,所有已登录会话已作废。',\n passwordFailed: '修改密码失败,请重试。',\n passwordStaleHost: '宿主端没有响应这个接口:可能还没重启 dsh web 加载新版本。重启后再试;重启前仍可用 lan_gateway 工具改密。',\n passwordTooShort: '密码至少 8 位。',\n passwordMismatch: '两次输入不一致。',\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)。留空 = 未声明。注意:登录限流以 TCP 源地址为键,代理之后所有浏览器共用一个额度(5 次/分钟)',\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 readOnly: 'Cannot read the configuration: the route answers on the host only — the Host must be a loopback address and same-origin. Change the settings where dsh web runs on the host, or use the lan_gateway tool remotely.',\n readOnlyGateway: 'The gateway refused this read: it owns the /lan-gateway/* management plane and admits only browsers that are both loopback-sourced and using a 127.0.0.1 / localhost address. You are not on the host loopback — open this page on the host via 127.0.0.1 or localhost (a LAN IP or a hostname does not qualify), or use the lan_gateway tool remotely.',\n saveFailed: 'The save did not land — check the inputs and retry.',\n loadFailed: 'Cannot read the gateway configuration',\n emptyMeansClear: 'Empty = default',\n running: 'Running',\n stopped: 'Stopped',\n tls: 'TLS',\n lastError: 'Last error',\n passwordSection: 'Login password',\n passwordHint: 'The current password is never shown (it cannot be read back). Enter a new one twice to overwrite it; changing it advances the session epoch, so every signed-in session and live WebSocket is invalidated at once. Only changeable where dsh web runs on loopback.',\n passwordNew: 'New password (min 8 chars)',\n passwordConfirm: 'Repeat new password',\n passwordSet: 'Set',\n passwordUnset: 'Not set',\n passwordUnknown: 'Unknown (older host)',\n passwordRequired: 'The gateway refuses to start without a password.',\n passwordHostStale: 'The host did not report a password state, so dsh web is still running the previous build: restart it to get Set / Not set here and to make this form take effect.',\n passwordChange: 'Change password',\n passwordChanged: 'Password updated: the old one no longer works and every signed-in session was revoked.',\n passwordFailed: 'The password change failed — retry.',\n passwordStaleHost: 'The host did not answer this endpoint — it may still be running the previous build. Restart dsh web and retry; the lan_gateway tool can change the password meanwhile.',\n passwordTooShort: 'At least 8 characters.',\n passwordMismatch: 'The two entries do not match.',\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. Note: login rate limiting keys on the TCP source address, so behind a proxy every browser shares one budget (5/min)',\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. Applies to the next issuance only: an existing certificate is reused until it expires',\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). Applies to the next issuance only: an existing certificate is reused until it expires',\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/* Card */\n/* ------------------------------------------------------------------ */\n\n/**\n * Render the LAN gateway configuration card. Self-loading: fetches the config\n * route on mount, posts the edited config on save.\n *\n * `summary` renders nothing: dsh asks a bundle's own configuration for its page\n * body only, and the card's former one-liner (a list entry on the official\n * plugin card) went away with `plugins.item`.\n * @param props - the view the Plugins page is asking for.\n * @returns the page body, or nothing while the view is `summary`.\n */\nexport function LanGatewayCard(props: LanGatewayCardProps): ReactNode {\n const t = labels()\n const [route, setRoute] = useState<RouteState | null>(null)\n const [failure, setFailure] = useState<'gateway' | 'other' | null>(null)\n const [drafts, setDrafts] = useState<Partial<Record<string, string>>>({})\n const [saving, setSaving] = useState(false)\n const [failed, setFailed] = useState<string | null>(null)\n // The password section is its own submit path: a password is not a settings\n // key, so it must not ride the config patch (nor light up the card's \"未保存\"\n // badge for every keystroke in a password box).\n const [password, setPasswordDraft] = useState('')\n const [passwordConfirm, setPasswordConfirmDraft] = useState('')\n const [passwordBusy, setPasswordBusy] = useState(false)\n const [passwordError, setPasswordError] = useState<string | null>(null)\n const [passwordNotice, setPasswordNotice] = 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) {\n // The body is only read to tell the gateway's own bare refusal from\n // any other answer; it is never rendered.\n const body = await response.text().catch(() => '')\n if (!cancelled) setFailure(refusedByGateway(response.status, body) ? 'gateway' : 'other')\n return\n }\n setRoute(await response.json() as RouteState)\n })\n .catch(() => {\n if (!cancelled) setFailure('other')\n })\n return () => { cancelled = true }\n }, [])\n\n // dsh asks a bundle for its own configuration under `view: 'page'` only, and\n // the page draws the title, icon, and crumb itself — so the card is the page\n // body and has no one-liner. Returning null keeps the contribution honest for\n // a host that ever asks for the other view.\n if (props.view === 'summary') return null\n\n // A browser that is not on the host reaches this card through the gateway,\n // which answers 403 for the plugin's own prefix by design, so the route is\n // unreachable exactly where a user is most likely to go looking for the\n // setting. Rendering nothing left them with a blank entry and no way to tell a\n // missing card from a broken one; say what is wrong — and whether the gateway\n // or the host refused — and where the card does work instead.\n if (failure !== null) {\n return (\n <div style={styles.card}>\n <p style={styles.status}>{t.loadFailed}</p>\n <p style={styles.hint}>{failure === 'gateway' ? t.readOnlyGateway : t.readOnly}</p>\n </div>\n )\n }\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 const passwordDraftProblem = passwordProblem(password, passwordConfirm)\n const passwordTyping = password !== '' || passwordConfirm !== ''\n const passwordBadge = passwordStatus(route.passwordSet)\n\n /** Adopt the host's post-write snapshot, keeping the last known values. */\n const adopt = (body: Partial<RouteState>): void => {\n if (body.config === undefined) return\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 // Absent is a state of its own (an older host does not report the field),\n // so it must not be coerced into `false`: that is what made a running,\n // password-protected gateway render as \"not set\".\n ...(body.passwordSet !== undefined ? { passwordSet: body.passwordSet } : {}),\n })\n }\n\n /**\n * Overwrite the login password. Deliberately writes only to\n * `/lan-gateway/password`: the credential is not part of the config patch, so\n * a change here can never disturb an unsaved settings draft, and the old\n * password is neither sent nor requested.\n */\n const changePassword = async (): Promise<void> => {\n if (passwordBusy || passwordProblem(password, passwordConfirm) !== null) return\n setPasswordBusy(true)\n setPasswordError(null)\n setPasswordNotice(null)\n try {\n const response = await fetch('/lan-gateway/password', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ password }),\n })\n // A host that predates this route answers the SPA fallback (HTML, 200),\n // not a 404 — so \"not our JSON\" is the signal, and it must not read as a\n // wrong password or a network failure.\n const body = await response.json().catch(() => null) as (Partial<RouteState> & { error?: string }) | null\n if (!response.ok) {\n setPasswordError(body?.error ?? (response.status === 404 ? t.passwordStaleHost : `HTTP ${response.status}`))\n return\n }\n if (body === null) {\n setPasswordError(t.passwordStaleHost)\n return\n }\n // Clear the boxes the moment the write lands: the new credential is now\n // the stored one, and leaving it on screen is the one thing a password\n // field should not do.\n setPasswordDraft('')\n setPasswordConfirmDraft('')\n setPasswordNotice(t.passwordChanged)\n adopt(body)\n } catch {\n setPasswordError(t.passwordFailed)\n } finally {\n setPasswordBusy(false)\n }\n }\n\n const save = async (): Promise<void> => {\n if (!dirty || saving || invalid()) return\n setSaving(true)\n setFailed(null)\n try {\n // A patch of the edited fields only, never the whole config: the card\n // cannot express every key the section may hold (a custom `cookieName`,\n // say), and posting a synthesized full config made the route treat those\n // keys as submitted — resetting each one to its schema default.\n const patch: Record<string, unknown> = {}\n for (const [field, text] of Object.entries(drafts)) {\n const def = FIELDS.find(f => f.field === field)\n if (def === undefined) continue\n const write = parseValue(def, text ?? '')\n if (write === undefined) continue\n patch[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(patch),\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 adopt(body)\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 <div style={styles.card}>\n {/* The page above draws the plugin's title itself, so the card opens with\n the live listener status and the unsaved marker — the two facts an\n operator wants before touching a field. */}\n <div style={styles.statusRow}>\n <span style={styles.status} title={statusLine}>{statusLine}</span>\n {dirty ? <span style={styles.pending}>{t.unsaved}</span> : null}\n </div>\n <div style={styles.body}>\n {route.lastError ? <p style={styles.error} role=\"status\">{t.lastError}: {route.lastError}</p> : null}\n <div style={styles.section}>\n <div style={styles.sectionHead}>\n <span style={styles.label}>{t.passwordSection}</span>\n <span style={passwordBadge === 'unset' ? styles.badgeAlert : styles.badge}>\n {passwordBadge === 'set'\n ? t.passwordSet\n : passwordBadge === 'unset' ? t.passwordUnset : t.passwordUnknown}\n </span>\n </div>\n <p style={styles.hint}>{t.passwordHint}</p>\n {passwordBadge === 'unset' ? <p style={styles.error}>{t.passwordRequired}</p> : null}\n {passwordBadge === 'unknown' ? <p style={styles.hint}>{t.passwordHostStale}</p> : null}\n <div style={styles.passwordRow}>\n <input\n id=\"lan-gw-password\"\n type=\"password\"\n autoComplete=\"new-password\"\n aria-label={t.passwordNew}\n style={passwordInput}\n placeholder={t.passwordNew}\n value={password}\n disabled={passwordBusy}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n setPasswordDraft(e.target.value)\n setPasswordError(null)\n setPasswordNotice(null)\n }}\n />\n <input\n id=\"lan-gw-password-confirm\"\n type=\"password\"\n autoComplete=\"new-password\"\n aria-label={t.passwordConfirm}\n style={passwordInput}\n placeholder={t.passwordConfirm}\n value={passwordConfirm}\n disabled={passwordBusy}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n setPasswordConfirmDraft(e.target.value)\n setPasswordError(null)\n setPasswordNotice(null)\n }}\n />\n </div>\n <div style={styles.passwordFoot}>\n {passwordError !== null\n ? <p style={styles.error} role=\"alert\">{passwordError}</p>\n : passwordNotice !== null\n ? <p style={styles.notice} role=\"status\">{passwordNotice}</p>\n : passwordTyping && passwordDraftProblem !== null\n ? (\n <p style={styles.error} role=\"status\">\n {passwordDraftProblem === 'tooShort' ? t.passwordTooShort : t.passwordMismatch}\n </p>\n )\n : null}\n <button\n type=\"button\"\n style={styles.save}\n disabled={passwordBusy || passwordDraftProblem !== null}\n onClick={() => { void changePassword() }}\n >\n {passwordBusy ? t.saving : t.passwordChange}\n </button>\n </div>\n </div>\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 </div>\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 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 error: tk('--dsw-alias-label-error', '#d1242f'),\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 display: 'flex',\n flexDirection: 'column',\n overflow: 'hidden',\n },\n // The page draws the plugin's title above this body, so the card leads with\n // the live listener status and the unsaved marker instead.\n statusRow: {\n display: 'flex',\n alignItems: 'center',\n gap: '12px',\n padding: '12px 16px',\n borderBottom: `1px solid ${L.border}`,\n },\n // The status carries a verbose TLS cert summary; cap it and ellipsize so it\n // can never swallow the row.\n status: {\n flex: '0 1 auto',\n minWidth: 0,\n maxWidth: '80%',\n fontSize: '11px',\n lineHeight: 1.4,\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 body: {\n padding: '4px 16px 8px',\n display: 'flex',\n flexDirection: 'column',\n },\n // The password block sits inside the card body, ahead of the config fields:\n // it is the setting an operator comes here for, and it owns its own submit\n // button (a password is not part of the settings patch), so it is separated\n // by a rule rather than merged into the field list.\n section: {\n display: 'flex',\n flexDirection: 'column',\n gap: '8px',\n padding: '14px 0',\n borderBottom: `1px solid ${L.border}`,\n },\n sectionHead: { display: 'flex', alignItems: 'center', gap: '8px' },\n badge: {\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 badgeAlert: {\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.error,\n },\n notice: { flex: 1, minWidth: 0, margin: 0, fontSize: '12px', lineHeight: 1.5, color: L.labelSecondary },\n passwordRow: { display: 'flex', flexWrap: 'wrap', gap: '8px' },\n passwordFoot: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-end',\n gap: '8px',\n marginTop: '2px',\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/** The shared input style plus the growth two side-by-side password boxes need. */\nconst passwordInput: React.CSSProperties = { ...styles.input, flex: '1 1 160px' }\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 Plugins\n * page as THIS bundle's own configuration (`plugins.bundle.config`, keyed by\n * the package name), so port, CIDRs, auth, and TLS stay adjustable from the\n * GUI on the plugin's own page. `plugins.item` is deliberately not used: dsh\n * reserves it for the official settings pages, one companion package per\n * host-plane namespace, and a bundle's configuration belongs in its own\n * config slot instead.\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 { Context } from '@deepseek-ai/cordis'\n// Type-only, and never bundled: the `slots` service declaration (owned by the\n// renderer, which installs the registry), the Plugins page's slot contract\n// (`plugins.bundle.config`), and the settings domain's `configForms` service all\n// resolve from the web shell's frozen module table. Importing them is what\n// subjects the registration below to the platform's own contract instead of a\n// local copy that drifts the next time upstream renames a slot or a service.\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { LanGatewayCard } from './lan-gateway-card.tsx'\n\nexport const name = 'dsh-lan-gateway'\n\n/**\n * The package name dsh keys a bundle's own configuration by in\n * `plugins.bundle.config`, and the name this profile's bundle row resolves to.\n * It is the package's npm name — not the locale namespace or the row id.\n */\nconst PACKAGE_NAME = '@riceawa/dsh-lan-gateway'\n\n/**\n * The profile entry id this plugin's bundle patch composes it under. dsh >=\n * 0.1.7 addresses both settings writes and the serve gate by entry id.\n */\nconst ENTRY_ID = 'dsh-lan-gateway'\n\n/** The slots service the card rides, and the settings mirror the gate reads. */\nexport const inject = ['slots', 'configForms']\n\n/**\n * Mount the settings card and the UUID shim.\n * @param ctx - the browser plugin context.\n */\nexport function apply(ctx: Context): void {\n installRandomUuidShim()\n\n // The card is this bundle's own configuration page. Like ModLens it registers\n // with no inject face and fetches its own loopback config route, so it depends\n // on no settings, locale, or connection service.\n //\n // It rides `plugins.bundle.config`, keyed by the bundle's package name, which\n // is the slot dsh declares for exactly this: dsh 0.1.7+ lists a bundle's own\n // configuration on the bundle's page, and it reserves `plugins.item` for the\n // official settings pages (\"one companion package per host-plane namespace\").\n // Registering there made the card pose as an official plugin; the dsh 0.2\n // Plugins page still declares that slot, so nothing failed loudly.\n //\n // `whileServed` keeps the entry off the page until the Host's settings mirror\n // serves this plugin's entry. That is the one gate worth having: without a\n // Loader entry there is nothing to write to, and the card would appear only\n // to fail every save with the route's 409.\n ctx.effect(() => ctx.configForms.whileServed([ENTRY_ID], () =>\n ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({\n name: 'plugins.bundle.config',\n key: PACKAGE_NAME,\n }, LanGatewayCard))))\n}\n"],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -677,6 +677,36 @@ function isLoopbackHost(hostname) {
|
|
|
677
677
|
const parts = hostname.split(".");
|
|
678
678
|
return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
679
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Whether a `Host` header names a loopback authority (127/8, `localhost`, ::1).
|
|
682
|
+
*
|
|
683
|
+
* This is the second half of the gateway's local-management exemption, and it
|
|
684
|
+
* answers a different question from {@link isLoopbackHost}'s own callers: not
|
|
685
|
+
* "is this authority loopback" but "did the browser itself use a loopback
|
|
686
|
+
* address". The two tests are combined on purpose. The socket source is what a
|
|
687
|
+
* remote client cannot forge; the Host is what a deployment cannot blur — behind
|
|
688
|
+
* a trusted TLS terminator every socket source is the terminator's loopback
|
|
689
|
+
* address, so the source test alone would readmit every remote browser, while a
|
|
690
|
+
* remote browser names the public host it dialed and stays refused.
|
|
691
|
+
*
|
|
692
|
+
* A client that can set an arbitrary Host (curl, not a browser) must still pass
|
|
693
|
+
* the socket-source test and hold a gateway session to reach anything, and the
|
|
694
|
+
* route behind the prefix is the one the native loopback listener already
|
|
695
|
+
* answers with no credential at all.
|
|
696
|
+
* @param host - the `Host` header value, or undefined.
|
|
697
|
+
* @returns true only when it parses and names a loopback authority.
|
|
698
|
+
*/
|
|
699
|
+
function isLoopbackAuthority(host) {
|
|
700
|
+
if (host === void 0 || host === "") return false;
|
|
701
|
+
let url;
|
|
702
|
+
try {
|
|
703
|
+
url = new URL(`http://${host}`);
|
|
704
|
+
} catch {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
if (url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") return false;
|
|
708
|
+
return isLoopbackHost(url.hostname);
|
|
709
|
+
}
|
|
680
710
|
/** Whether this source must present a gateway session (default: everyone). */
|
|
681
711
|
function requiresLogin(source, lanPasswordless) {
|
|
682
712
|
return !(lanPasswordless && source !== "internet");
|
|
@@ -1149,6 +1179,23 @@ var LanGateway = class {
|
|
|
1149
1179
|
return this.config.classifySource !== void 0 ? this.config.classifySource(req) : classifySource(req.socket.remoteAddress, this.config.lanCidrs);
|
|
1150
1180
|
}
|
|
1151
1181
|
/**
|
|
1182
|
+
* Whether a request for the gateway-owned management prefix comes from the
|
|
1183
|
+
* host itself, and may therefore be relayed to the loopback-only config and
|
|
1184
|
+
* password routes.
|
|
1185
|
+
*
|
|
1186
|
+
* Both tests are required and neither is sufficient alone. The socket source
|
|
1187
|
+
* is what a remote client cannot forge; the authority the client named is what
|
|
1188
|
+
* a trusted TLS terminator deployment cannot blur, because there every socket
|
|
1189
|
+
* source is the terminator's own loopback address while the browser still
|
|
1190
|
+
* names the public host it dialed. A local browser that used `127.0.0.1` (or
|
|
1191
|
+
* `localhost`) is the same operator the native route already trusts, and it
|
|
1192
|
+
* still has to clear the session gate and the same-site fence below before
|
|
1193
|
+
* anything is forwarded.
|
|
1194
|
+
*/
|
|
1195
|
+
localManagementRequest(req, source) {
|
|
1196
|
+
return source === "loopback" && isLoopbackAuthority(req.headers.host);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1152
1199
|
* The session a request carries, or undefined when it presents none, presents
|
|
1153
1200
|
* one that no longer verifies under the current epoch, or presents one whose
|
|
1154
1201
|
* id has been signed out.
|
|
@@ -1193,7 +1240,10 @@ var LanGateway = class {
|
|
|
1193
1240
|
const attributes = `Path=/; HttpOnly; SameSite=Strict; Max-Age=${maxAgeSeconds}`;
|
|
1194
1241
|
return `${this.config.cookieName}=${value}; ${attributes}${this.config.secureCookies ? "; Secure" : ""}`;
|
|
1195
1242
|
}
|
|
1196
|
-
/**
|
|
1243
|
+
/**
|
|
1244
|
+
* Handle one HTTP request: login surface → owned-path gate → session gate →
|
|
1245
|
+
* same-site gate → relay.
|
|
1246
|
+
*/
|
|
1197
1247
|
async handleHttp(req, res) {
|
|
1198
1248
|
const url = req.url ?? "/";
|
|
1199
1249
|
const pathname = pathOf(url);
|
|
@@ -1206,7 +1256,7 @@ var LanGateway = class {
|
|
|
1206
1256
|
this.handleLogout(req, res);
|
|
1207
1257
|
return;
|
|
1208
1258
|
}
|
|
1209
|
-
if (isOwnedPath(pathname)) {
|
|
1259
|
+
if (isOwnedPath(pathname) && !this.localManagementRequest(req, source)) {
|
|
1210
1260
|
res.writeHead(403, this.securityHeaders());
|
|
1211
1261
|
res.end("forbidden");
|
|
1212
1262
|
return;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riceawa/dsh-lan-gateway",
|
|
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
|
|
4
|
-
"version": "0.
|
|
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 configuration page on the dsh Plugins page (the bundle's own `plugins.bundle.config` slot) for live adjustment of port, CIDRs, auth, TLS, and the login password (the card overwrites the stored credential without ever displaying the old one; the password stays a scrypt hash in state.json, out of the config schema). 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.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -34,9 +34,6 @@
|
|
|
34
34
|
"patch": "./cordis.patch.yml"
|
|
35
35
|
},
|
|
36
36
|
"client": {
|
|
37
|
-
"inject": [
|
|
38
|
-
"@deepseek-ai/dsh-client-runtime"
|
|
39
|
-
],
|
|
40
37
|
"platform": "web",
|
|
41
38
|
"immediately": true
|
|
42
39
|
}
|
|
@@ -51,24 +48,25 @@
|
|
|
51
48
|
"@deepseek-ai/cordis": "4.0.4",
|
|
52
49
|
"@deepseek-ai/cordis-plugin-loader": "1.0.5",
|
|
53
50
|
"@deepseek-ai/cosmokit": "1.8.5",
|
|
54
|
-
"@deepseek-ai/dsh-brand": "0.
|
|
55
|
-
"@deepseek-ai/dsh-agent": "0.
|
|
56
|
-
"@deepseek-ai/dsh-invariants": "0.
|
|
57
|
-
"@deepseek-ai/dsh-ptc-runtime": "0.
|
|
58
|
-
"@deepseek-ai/dsh-sandbox": "0.
|
|
59
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.
|
|
60
|
-
"@deepseek-ai/dsh-session": "0.
|
|
61
|
-
"@deepseek-ai/dsh-system-prompt": "0.
|
|
62
|
-
"@deepseek-ai/dsh-user-approval": "0.
|
|
63
|
-
"@deepseek-ai/dsh-client-
|
|
64
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-
|
|
67
|
-
"@deepseek-ai/dsh-
|
|
68
|
-
"@deepseek-ai/dsh-
|
|
69
|
-
"@deepseek-ai/dsh-
|
|
70
|
-
"@deepseek-ai/dsh-
|
|
71
|
-
"@deepseek-ai/dsh-
|
|
51
|
+
"@deepseek-ai/dsh-brand": "0.2.0-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.2.0-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-session": "0.2.0-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-cordis-client-runner": "0.2.0-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.0-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-scope": "0.2.0-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-settings": "0.2.0-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-util-values": "0.2.0-rc.2",
|
|
72
70
|
"@deepseek-ai/schemastery": "3.18.4",
|
|
73
71
|
"@types/node": "^22.0.0",
|
|
74
72
|
"@types/react": "~18.3.1",
|
package/src/client/index.ts
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
* module scope. With TLS enabled the origin is secure and the shim is a
|
|
9
9
|
* no-op.
|
|
10
10
|
* 2. Settings card: registers the LAN gateway card into the official Plugins
|
|
11
|
-
* page
|
|
12
|
-
* from the
|
|
11
|
+
* page as THIS bundle's own configuration (`plugins.bundle.config`, keyed by
|
|
12
|
+
* the package name), so port, CIDRs, auth, and TLS stay adjustable from the
|
|
13
|
+
* GUI on the plugin's own page. `plugins.item` is deliberately not used: dsh
|
|
14
|
+
* reserves it for the official settings pages, one companion package per
|
|
15
|
+
* host-plane namespace, and a bundle's configuration belongs in its own
|
|
16
|
+
* config slot instead.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
/** RFC 4122 v4 UUID from crypto.getRandomValues (available on insecure origins). */
|
|
@@ -60,21 +64,30 @@ export function installRandomUuidShim(): boolean {
|
|
|
60
64
|
// any RPC/session code runs. apply() re-runs it as a belt-and-braces re-check.
|
|
61
65
|
installRandomUuidShim()
|
|
62
66
|
|
|
63
|
-
import type {
|
|
64
|
-
// Type-only, and never bundled: the
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
67
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
68
|
+
// Type-only, and never bundled: the `slots` service declaration (owned by the
|
|
69
|
+
// renderer, which installs the registry), the Plugins page's slot contract
|
|
70
|
+
// (`plugins.bundle.config`), and the settings domain's `configForms` service all
|
|
71
|
+
// resolve from the web shell's frozen module table. Importing them is what
|
|
72
|
+
// subjects the registration below to the platform's own contract instead of a
|
|
73
|
+
// local copy that drifts the next time upstream renames a slot or a service.
|
|
74
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
69
75
|
import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'
|
|
70
76
|
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
71
|
-
import { LanGatewayCard
|
|
77
|
+
import { LanGatewayCard } from './lan-gateway-card.tsx'
|
|
72
78
|
|
|
73
79
|
export const name = 'dsh-lan-gateway'
|
|
74
80
|
|
|
75
81
|
/**
|
|
76
|
-
* The
|
|
77
|
-
*
|
|
82
|
+
* The package name dsh keys a bundle's own configuration by in
|
|
83
|
+
* `plugins.bundle.config`, and the name this profile's bundle row resolves to.
|
|
84
|
+
* It is the package's npm name — not the locale namespace or the row id.
|
|
85
|
+
*/
|
|
86
|
+
const PACKAGE_NAME = '@riceawa/dsh-lan-gateway'
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The profile entry id this plugin's bundle patch composes it under. dsh >=
|
|
90
|
+
* 0.1.7 addresses both settings writes and the serve gate by entry id.
|
|
78
91
|
*/
|
|
79
92
|
const ENTRY_ID = 'dsh-lan-gateway'
|
|
80
93
|
|
|
@@ -85,29 +98,27 @@ export const inject = ['slots', 'configForms']
|
|
|
85
98
|
* Mount the settings card and the UUID shim.
|
|
86
99
|
* @param ctx - the browser plugin context.
|
|
87
100
|
*/
|
|
88
|
-
export function apply(ctx:
|
|
101
|
+
export function apply(ctx: Context): void {
|
|
89
102
|
installRandomUuidShim()
|
|
90
103
|
|
|
91
|
-
// The card
|
|
92
|
-
// inject face and fetches its own loopback config route, so it depends
|
|
93
|
-
// settings, locale, or connection service.
|
|
104
|
+
// The card is this bundle's own configuration page. Like ModLens it registers
|
|
105
|
+
// with no inject face and fetches its own loopback config route, so it depends
|
|
106
|
+
// on no settings, locale, or connection service.
|
|
94
107
|
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// configuration page
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
108
|
+
// It rides `plugins.bundle.config`, keyed by the bundle's package name, which
|
|
109
|
+
// is the slot dsh declares for exactly this: dsh 0.1.7+ lists a bundle's own
|
|
110
|
+
// configuration on the bundle's page, and it reserves `plugins.item` for the
|
|
111
|
+
// official settings pages ("one companion package per host-plane namespace").
|
|
112
|
+
// Registering there made the card pose as an official plugin; the dsh 0.2
|
|
113
|
+
// Plugins page still declares that slot, so nothing failed loudly.
|
|
101
114
|
//
|
|
102
115
|
// `whileServed` keeps the entry off the page until the Host's settings mirror
|
|
103
116
|
// serves this plugin's entry. That is the one gate worth having: without a
|
|
104
117
|
// Loader entry there is nothing to write to, and the card would appear only
|
|
105
118
|
// to fail every save with the route's 409.
|
|
106
119
|
ctx.effect(() => ctx.configForms.whileServed([ENTRY_ID], () =>
|
|
107
|
-
ctx.slots.inject('plugins.
|
|
108
|
-
name: 'plugins.
|
|
109
|
-
|
|
110
|
-
order: 30,
|
|
111
|
-
label: () => cardTitle(),
|
|
120
|
+
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
121
|
+
name: 'plugins.bundle.config',
|
|
122
|
+
key: PACKAGE_NAME,
|
|
112
123
|
}, LanGatewayCard))))
|
|
113
124
|
}
|