Files
zhu1090093659 76e92e540c chore(repo): rename product from dsh-web-ui to dsh-web
Display and source layers rename to dsh-web: GitHub slug, docs and prose,
aggregate package dir packages/dsh-web-all with npm name
@linxin666/dsh-web-all, settings package dir packages/dsh-web-settings,
private shared package dsh-web-shared, repo-local skill dirs, and the
docs banner asset.

Runtime, wire, and storage identifiers are frozen byte-identical so
installed profiles keep resolving with zero migration: web-ui-* bundle
ids, the dsh-web-ui-market settings section id, /api/dsh-web-ui-settings
and its proxy-token header, and the dsh-web-ui-telemetry-* storage keys.
Frozen history (docs/archive, docs/release-notes, archived notes), the
JAVA-LW fork reference, and local filesystem paths keep the old name.

npm migration: the next tag release dual-publishes @linxin666/dsh-web-all
alongside the final @linxin666/dsh-web-ui-all version, then the old name
is deprecated with a pointer; dual-publish lasts two releases.

Decision record: .agents/notes/implemented/architecture/2026-08-24-product-rename-dsh-web.md
2026-08-24 14:22:14 +08:00

63 lines
2.3 KiB
TypeScript

/**
* Loopback trust fence shared by the host route families: socket address,
* Host header, and browser same-origin markers. Packages receive this file as
* a generated copy via scripts/sync-shared.mjs; edit the shared source and
* re-run the sync instead of editing a copy.
*
* Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
* remote-web-ui gate), localhost hostnames, plus the browser same-origin
* markers (sec-fetch-site and Origin) for the request-level fence.
* @module dsh-web-shared/host/loopback
*/
import type { IncomingMessage } from 'node:http'
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
export function isIPv4Loopback(v4: string): boolean {
const parts = v4.split('.')
return parts.length === 4
&& parts[0] === '127'
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
}
/** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
export function isLoopbackAddress(address: string | undefined): boolean {
if (address === undefined) return false
const normalized = address.toLowerCase()
if (normalized === '::1') return true
if (normalized.startsWith('::ffff:')) return isIPv4Loopback(normalized.slice('::ffff:'.length))
return isIPv4Loopback(normalized)
}
/** Whether a normalized URL hostname names the loopback authority (localhost, [::1], 127/8). */
export function isLoopbackHostname(hostname: string): boolean {
if (hostname === 'localhost' || hostname === '[::1]') return true
return isIPv4Loopback(hostname)
}
/**
* Request-level trust fence: a loopback socket address AND a loopback Host
* header, plus browser same-origin markers. The socket address is
* authoritative; X-Forwarded-For is never trusted.
*/
export function isLoopbackRequest(request: IncomingMessage): boolean {
if (!isLoopbackAddress(request.socket.remoteAddress)) return false
const host = request.headers.host
if (typeof host !== 'string') return false
let hostUrl: URL
try {
hostUrl = new URL('http://' + host)
} catch {
return false
}
if (!isLoopbackHostname(hostUrl.hostname)) return false
if (request.headers['sec-fetch-site'] === 'cross-site') return false
const origin = request.headers.origin
if (origin === undefined) return true
try {
return new URL(origin).host === hostUrl.host
} catch {
return false
}
}