dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x

This commit is contained in:
hermes committed 2026-10-01 19:21:19 +00:00
commit e6dcef9bf5
302 files changed
+75178

No files matched your search

@@ -0,0 +1,151 @@
import { request as httpRequest } from 'node:http'
// A Bootstrap handoff stops DSH while the updater that initiated it is still
// recorded as building the candidate. This state is consulted only after the
// DSH upstream becomes unavailable, so ordinary candidate builds stay visible.
const SWITCHING_STATES = new Set(['building-candidate', 'snapshotting-data', 'switching', 'probation'])
const RECOVERING_STATES = new Set(['restoring-data'])
const COPY = Object.freeze({
en: Object.freeze({
starting: 'DeepSeek Harness is starting',
stopping: 'DeepSeek Harness is stopping',
stopped: 'DeepSeek Harness is stopped',
restarting: 'DeepSeek Harness is restarting',
switching: 'Switching the DeepSeek Harness runtime',
'runtime-recovering': 'Restoring the DeepSeek Harness runtime',
recovering: 'DeepSeek Harness stopped unexpectedly and recovery is in progress',
failed: 'DeepSeek Harness could not start',
'plugin-failed': 'DeepSeek Harness plugins repeatedly failed to load',
unavailable: 'DeepSeek Harness is temporarily unavailable and recovery is in progress',
}),
zh: Object.freeze({
starting: 'DeepSeek Harness 正在启动',
stopping: 'DeepSeek Harness 正在停止',
stopped: 'DeepSeek Harness 已停止',
restarting: 'DeepSeek Harness 正在重新启动',
switching: '正在切换 DeepSeek Harness 运行版本',
'runtime-recovering': '正在恢复 DeepSeek Harness 运行版本',
recovering: 'DeepSeek Harness 意外停止,正在尝试恢复',
failed: 'DeepSeek Harness 启动失败',
'plugin-failed': 'DeepSeek Harness 插件持续加载失败',
unavailable: 'DeepSeek Harness 暂时不可用,正在尝试恢复',
}),
})
const MANAGEMENT_LINK = Object.freeze({
en: 'Open DSH Management Console for diagnostics and recovery',
zh: '打开 DSH 管理中心进行检查和恢复',
})
const REFRESH_LINK = Object.freeze({ en: 'Reload page', zh: '刷新页面' })
export class DshAvailability {
constructor({ now = () => Date.now(), failures = 3, failureWindowMs = 1_500 } = {}) {
this.now = now
this.failures = failures
this.failureWindowMs = failureWindowMs
this.everReady = false
this.consecutiveFailures = 0
this.firstFailureAt = null
}
observe(ready) {
if (ready) {
this.everReady = true
this.consecutiveFailures = 0
this.firstFailureAt = null
return
}
this.consecutiveFailures += 1
this.firstFailureAt ??= this.now()
}
classify(platform = {}) {
if (platform.operation === 'restart-failed') return 'failed'
if (platform.operation === 'restarting') return 'restarting'
if (platform.operation === 'recovering') return 'runtime-recovering'
if (platform.operation === 'switching') return 'switching'
const updateState = platform.update?.status
if (RECOVERING_STATES.has(updateState)) return 'runtime-recovering'
if (SWITCHING_STATES.has(updateState)) return 'switching'
const lifecycle = platform.dshLifecycle ?? {}
if (['starting', 'stopping', 'stopped', 'restarting', 'recovering', 'failed'].includes(lifecycle.state)) {
return lifecycle.state
}
if (platform.recoveryMode !== null && platform.recoveryMode !== undefined) return 'failed'
if (!this.everReady) return 'starting'
if (
this.consecutiveFailures >= this.failures
&& this.firstFailureAt !== null
&& this.now() - this.firstFailureAt >= this.failureWindowMs
) return 'unavailable'
return 'unknown'
}
}
export function probeDsh({ host, port, timeoutMs = 750 }) {
return new Promise(resolve => {
const request = httpRequest({ hostname: host, port, path: '/', method: 'GET' }, response => {
response.resume()
resolve((response.statusCode ?? 500) < 500)
})
request.setTimeout(timeoutMs, () => request.destroy())
request.once('error', () => resolve(false))
request.end()
})
}
function cookieLocale(cookie = '') {
for (const part of cookie.split(';')) {
const [name, value] = part.trim().split('=', 2)
if (name === 'dsh_locale' && (value === 'zh' || value === 'en')) return value
}
return undefined
}
export function language(headers) {
const saved = cookieLocale(headers.cookie)
if (saved !== undefined) return saved
for (const item of (headers['accept-language'] ?? '').split(',')) {
const primary = item.trim().split(';', 1)[0].split('-', 1)[0].toLowerCase()
if (primary === 'zh' || primary === 'en') return primary
}
return 'en'
}
export function stateMessage(state, headers = {}, lifecycle = {}) {
const locale = language(headers)
const message = COPY[locale][state] ?? COPY.en.unavailable
if (state !== 'recovering' || !(lifecycle.attempt > 0) || !(lifecycle.maxAttempts > 0)) return message
return locale === 'zh'
? `${message}(第 ${String(lifecycle.attempt)} / ${String(lifecycle.maxAttempts)} 次)`
: `${message} (attempt ${String(lifecycle.attempt)} of ${String(lifecycle.maxAttempts)})`
}
export function availabilityPage(state, headers = {}, { lifecycle = {}, returnPath = null, poll = true, allowRefresh = false, managementHref = '/_dsh_platform/console/' } = {}) {
const locale = language(headers)
const message = stateMessage(state, headers, lifecycle)
const managementLink = MANAGEMENT_LINK[locale]
const refreshLink = REFRESH_LINK[locale]
const target = JSON.stringify(returnPath).replaceAll('<', '\\u003c')
return `<!doctype html>
<html lang="${locale === 'zh' ? 'zh-CN' : 'en'}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>DeepSeek Harness</title>
<style>
html{height:100%}body{min-height:100%;margin:0;display:grid;place-items:center;background:#151517;color:#f3f3f4;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.boot{display:flex;flex-direction:column;align-items:center;gap:16px;padding:24px;text-align:center}.wordmark{font-size:16px;line-height:24px;font-weight:600;letter-spacing:0}.status{max-width:520px;font-size:13px;line-height:20px;color:#a7a7ad}.actions{display:flex;flex-wrap:wrap;justify-content:center;gap:10px}.management{padding:7px 12px;border:1px solid #46464b;border-radius:6px;color:#d7d7db;font-size:13px;line-height:20px;text-decoration:none}.management:hover{background:#232326}.management:focus-visible{outline:2px solid #8ca8ff;outline-offset:2px}@media(prefers-color-scheme:light){body{background:#f9fafb;color:#0f1115}.status{color:#6d7178}.management{border-color:#d6d8dc;color:#34373d}.management:hover{background:#f0f1f3}}
</style>
</head>
<body>
<main class="boot"><div class="wordmark">HARNESS</div><div class="status" role="status">${message}</div><div class="actions"><a class="management" href="${String(managementHref).replaceAll('"', '&quot;')}">${managementLink}</a>${allowRefresh ? `<a class="management refresh" href="">${refreshLink}</a>` : ''}</div></main>
${poll ? `<script>
const returnPath=${target};const status=document.querySelector('.status');const management=document.querySelector('.management:not(.refresh)');
async function check(){try{const response=await fetch('/_dsh_gateway/readiness',{cache:'no-store'});const value=await response.json();if(typeof value.managementHref==='string')management.href=value.managementHref;if(value.ready){if(returnPath===null)location.reload();else location.replace(returnPath);return}if(typeof value.message==='string')status.textContent=value.message}catch{}setTimeout(check,1000)}
setTimeout(check,600);
</script>` : ''}
</body>
</html>`
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,75 @@
import { UsageError } from './errors.mjs'
export function parseBoolean(name, value, fallback) {
const resolved = value ?? String(fallback)
if (resolved === 'true') return true
if (resolved === 'false') return false
throw new UsageError(`${name} must be true or false`)
}
function parseUrlAuthority(value) {
try {
return new URL(`http://${value}`)
} catch {
return undefined
}
}
function canonicalAuthority(value, parsed) {
const port = parsed.port !== '' ? parsed.port : new URL(`https://${value}`).port
return port === '' ? parsed.hostname : `${parsed.hostname}:${port}`
}
export function parseTrustedAuthority(value) {
const parsed = parseUrlAuthority(value)
if (
parsed === undefined
|| parsed.hostname.includes('*')
|| canonicalAuthority(value, parsed) !== value.toLowerCase()
) {
throw new UsageError(
`DSH_TRUSTED_HOSTS entry ${JSON.stringify(value)} must be a canonical host or host:port authority`,
)
}
const authority = canonicalAuthority(value, parsed)
return Object.freeze({
hostname: parsed.hostname,
authority,
anyPort: authority === parsed.hostname,
matchAuthority: parsed.host,
})
}
export function parseTrustedHosts(environment = process.env) {
const raw = environment.DSH_TRUSTED_HOSTS?.trim() ?? ''
if (raw === '') return Object.freeze({ wildcard: false, authorities: Object.freeze([]) })
const entries = raw.split(',').map(entry => entry.trim())
if (entries.some(entry => entry === '')) {
throw new UsageError('DSH_TRUSTED_HOSTS cannot contain an empty entry')
}
if (entries.includes('*')) {
if (entries.length !== 1) throw new UsageError('DSH_TRUSTED_HOSTS wildcard must be used alone')
return Object.freeze({ wildcard: true, authorities: Object.freeze([]) })
}
const unique = new Map()
for (const entry of entries) {
const parsed = parseTrustedAuthority(entry)
unique.set(`${parsed.authority}|${String(parsed.anyPort)}`, parsed)
}
return Object.freeze({ wildcard: false, authorities: Object.freeze([...unique.values()]) })
}
export async function loadConfig(environment = process.env) {
const managementPort = environment.DSH_MANAGEMENT_PORT === undefined || environment.DSH_MANAGEMENT_PORT.trim() === ''
? 3081 : Number(environment.DSH_MANAGEMENT_PORT)
if (managementPort !== null && (!Number.isInteger(managementPort) || managementPort < 1 || managementPort > 65535)) {
throw new UsageError('DSH_MANAGEMENT_PORT must be an integer between 1 and 65535')
}
return Object.freeze({
trustedHosts: parseTrustedHosts(environment),
polyfill: parseBoolean('DSH_PROXY_POLYFILL', environment.DSH_PROXY_POLYFILL, true),
managementPort,
})
}
@@ -0,0 +1,119 @@
import { request } from 'node:http'
function lifecycleReadiness(socketPath) {
const body = Buffer.from('{}')
return new Promise((resolve, reject) => {
const outgoing = request({
socketPath,
path: '/v1/runtime/readiness',
method: 'POST',
headers: {
'content-length': body.byteLength,
'content-type': 'application/json',
},
}, response => {
const chunks = []
response.on('data', chunk => chunks.push(chunk))
response.once('end', () => {
if (response.statusCode !== 200) {
reject(new Error(`DSH lifecycle readiness returned HTTP ${String(response.statusCode)}`))
return
}
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')))
} catch (error) {
reject(error)
}
})
})
outgoing.setTimeout(750, () => outgoing.destroy(new Error('DSH lifecycle readiness timed out')))
outgoing.once('error', reject)
outgoing.end(body)
})
}
function exchange(host, port, readyUrl) {
const url = new URL(readyUrl)
if (url.protocol !== 'http:' || url.hostname !== host || Number(url.port) !== port
|| url.pathname !== '/' || url.username !== '' || url.password !== '' || url.hash !== ''
|| url.searchParams.getAll('token').length !== 1) {
throw new Error('DSH Web readiness URL is invalid')
}
return new Promise((resolve, reject) => {
const outgoing = request({
hostname: host,
port,
path: `${url.pathname}${url.search}`,
method: 'GET',
headers: { 'cache-control': 'no-store' },
}, response => {
response.resume()
response.once('end', () => {
const cookie = response.headers['set-cookie']?.[0]?.split(';', 1)[0]
if (response.statusCode !== 303 || response.headers.location !== '/'
|| typeof cookie !== 'string' || cookie === '') {
reject(new Error('DSH Web authentication exchange failed'))
} else resolve(cookie)
})
})
outgoing.setTimeout(750, () => outgoing.destroy(new Error('DSH Web authentication exchange timed out')))
outgoing.once('error', reject)
outgoing.end()
})
}
export class DshUpstreamAuthentication {
constructor({
socketPath = '/run/dsh-platform/dsh-lifecycle.sock',
host = '127.0.0.1',
port = 3079,
now = () => Date.now(),
legacyProbeIntervalMs = 250,
} = {}) {
this.socketPath = socketPath
this.host = host
this.port = port
this.now = now
this.legacyProbeIntervalMs = legacyProbeIntervalMs
this.cachedCookie = undefined
this.cachedGeneration = null
this.nextProbeAt = 0
this.pending = null
}
async publicReady() {
const readiness = await lifecycleReadiness(this.socketPath)
const ready = readiness?.publicReady === true
|| (readiness?.publicReady === undefined && readiness?.ready === true)
if (ready) this.nextProbeAt = 0
return ready
}
async cookie() {
if (this.pending !== null) return this.pending
if (this.now() < this.nextProbeAt) return null
this.pending = this.refresh().finally(() => { this.pending = null })
return this.pending
}
async refresh() {
const readiness = await lifecycleReadiness(this.socketPath)
const publicReady = readiness?.publicReady === true
|| (readiness?.publicReady === undefined && readiness?.ready === true)
if (!publicReady || readiness?.ready !== true || typeof readiness.readyUrl !== 'string') {
if (!publicReady) {
this.cachedCookie = undefined
this.cachedGeneration = null
}
this.nextProbeAt = this.now() + this.legacyProbeIntervalMs
return null
}
if (this.cachedCookie !== undefined && this.cachedGeneration === (readiness.generation ?? null)) {
return this.cachedCookie
}
const cookie = await exchange(this.host, this.port, readiness.readyUrl)
this.cachedCookie = cookie
this.cachedGeneration = readiness.generation ?? null
return cookie
}
}
@@ -0,0 +1,7 @@
export class UsageError extends Error {
constructor(message) {
super(message)
this.name = 'UsageError'
this.exitCode = 64
}
}
@@ -0,0 +1,157 @@
import { createGatewayServer, closeGatewayServer } from './proxy.mjs'
import { createBrowserAuthentication } from './browser-auth.mjs'
import { LocalApiClient } from '../../../modules/updater/lib/client.mjs'
import { safeReturnPath } from './proxy.mjs'
import { DshUpstreamAuthentication } from './dsh-upstream-auth.mjs'
export const EXTERNAL_HOST = '0.0.0.0'
export const EXTERNAL_PORT = 3080
function listen(server, port, host) {
return new Promise((resolve, reject) => {
const onError = (error) => {
server.off('listening', onListening)
reject(error)
}
const onListening = () => {
server.off('error', onError)
resolve()
}
server.once('error', onError)
server.once('listening', onListening)
server.listen(port, host)
})
}
export async function runGateway(config, {
signalSource = process,
gatewayFactory = createGatewayServer,
externalHost = EXTERNAL_HOST,
externalPort = EXTERNAL_PORT,
managementPort = null,
managementHost = externalHost,
managementSocketPath = process.env.DSH_PLATFORM_MANAGEMENT_SOCKET ?? '/run/dsh-platform/management.sock',
managementCliSocketPath = process.env.DSH_PLATFORM_MANAGEMENT_CLI_SOCKET ?? '/run/dsh-platform/management-cli.sock',
maintenanceSocketPath = process.env.DSH_PLATFORM_MAINTENANCE_SOCKET ?? '/run/dsh-platform/maintenance.sock',
lifecycleSocketPath = process.env.DSH_PLATFORM_LIFECYCLE_SOCKET ?? '/run/dsh-platform/dsh-lifecycle.sock',
accessSocketPath = process.env.DSH_PLATFORM_ACCESS_SOCKET ?? '/run/dsh-platform/access/access.sock',
accessClient,
managementClient,
platformStatusPollMs = 250,
platformStatusCacheMs = 10_000,
now = () => Date.now(),
report = async () => {},
} = {}) {
const record = (message, fields = {}) => Promise.resolve().then(() => report(message, fields)).catch(() => {})
const management = managementClient ?? new LocalApiClient(managementCliSocketPath)
const access = accessClient ?? new LocalApiClient(accessSocketPath)
const accessRequest = access.request.bind(access)
const resilientAccess = accessClient === undefined
? { request: async (method, path, body, headers) => {
for (let attempt = 0; ; attempt++) {
try {
return await accessRequest(method, path, body, headers)
} catch (error) {
const retryable = ['ENOENT', 'ECONNREFUSED', 'ECONNRESET', 'EPIPE'].includes(error?.code)
if (!retryable || attempt >= 20) throw error
await new Promise(resolve => setTimeout(resolve, Math.min(250, 25 * (attempt + 1))))
}
}
} } : access
const browserAuthentication = createBrowserAuthentication({ access: resilientAccess, safeReturnPath, report: record })
const dshUpstreamAuthentication = new DshUpstreamAuthentication({ socketPath: lifecycleSocketPath })
const isolatedBrowserAuthentication = createBrowserAuthentication({
access: resilientAccess,
safeReturnPath,
report: record,
paths: { authPrefix: '/auth/', accessPrefix: '/access/', transitionPrefix: '/transition/', consolePath: '/' },
})
let lastPlatformStatus
let lastPlatformStatusAt = 0
const refreshPlatformStatus = async () => {
const status = await management.request('GET', '/_dsh_platform/api/v1/status')
lastPlatformStatus = status
lastPlatformStatusAt = now()
return status
}
const platformStatus = async () => {
try {
return await refreshPlatformStatus()
} catch (error) {
if (lastPlatformStatus !== undefined && now() - lastPlatformStatusAt <= platformStatusCacheMs) {
return lastPlatformStatus
}
throw error
}
}
const server = gatewayFactory({
polyfill: config.polyfill,
trustedHosts: config.trustedHosts,
managementSocketPath,
maintenanceSocketPath,
browserAuthentication,
dshUpstreamAuthentication,
platformStatus,
report: record,
surface: 'compat',
})
const managementServer = managementPort === null || managementPort === undefined
? null
: gatewayFactory({
polyfill: false,
trustedHosts: config.trustedHosts,
managementSocketPath,
maintenanceSocketPath,
browserAuthentication: isolatedBrowserAuthentication,
dshUpstreamAuthentication,
platformStatus,
report: record,
surface: 'management',
})
const platformStatusTimer = setInterval(() => {
void refreshPlatformStatus().catch(() => {})
}, platformStatusPollMs)
platformStatusTimer.unref?.()
void refreshPlatformStatus().catch(() => {})
let resolveSignal
const receivedSignal = new Promise(resolve => { resolveSignal = resolve })
const onSignal = signal => resolveSignal(signal)
const onSigint = () => onSignal('SIGINT')
const onSigterm = () => onSignal('SIGTERM')
signalSource.once('SIGINT', onSigint)
signalSource.once('SIGTERM', onSigterm)
try {
await record('gateway.starting', {
externalHost, externalPort,
...(managementServer === null ? {} : { managementHost, managementPort }),
})
await listen(server, externalPort, externalHost)
if (managementServer !== null) await listen(managementServer, managementPort, managementHost)
await record('gateway.ready', {
externalHost, externalPort,
...(managementServer === null ? {} : { managementHost, managementPort }),
})
const serverFailed = new Promise((_, reject) => server.once('error', reject))
const signal = await Promise.race([receivedSignal, serverFailed])
await record('gateway.stopping', { signal })
await closeGatewayServer(server)
if (managementServer !== null) await closeGatewayServer(managementServer)
await record('gateway.stopped')
return 0
} catch (error) {
await record('gateway.fatal', { error })
if (server.listening || managementServer?.listening) {
try {
if (server.listening) await closeGatewayServer(server)
if (managementServer?.listening) await closeGatewayServer(managementServer)
} catch (closeError) {
await record('gateway.stop.failed', { error: closeError, cause: error instanceof Error ? error.message : String(error) })
}
}
throw error
} finally {
clearInterval(platformStatusTimer)
signalSource.off('SIGINT', onSigint)
signalSource.off('SIGTERM', onSigterm)
}
}
@@ -0,0 +1,60 @@
export const RANDOM_UUID_POLYFILL = '<script>(function(){try{var c=globalThis.crypto;if(c&&typeof c.randomUUID!=="function"&&typeof c.getRandomValues==="function"){c.randomUUID=function(){var b=c.getRandomValues(new Uint8Array(16));b[6]=(b[6]&15)|64;b[8]=(b[8]&63)|128;var h="";for(var i=0;i<16;i++){h+=b[i].toString(16).padStart(2,"0")}return h.slice(0,8)+"-"+h.slice(8,12)+"-"+h.slice(12,16)+"-"+h.slice(16,20)+"-"+h.slice(20)}}}catch(e){}})();</script>'
export const LIFECYCLE_TRANSITION_GUARD = `<script>(function(){
try{
var api="/_dsh_platform/plugin-api/v1",wait="/_dsh_gateway/wait",navigating=false;
function transition(value){return ["restarting","switching","recovering","restart-failed"].includes(value.operation)||["snapshotting-data","switching","probation","restoring-data"].includes(value.update&&value.update.status)||["starting","stopping","stopped","restarting","recovering","failed"].includes(value.dshLifecycle&&value.dshLifecycle.state)}
function navigate(){if(navigating||location.pathname===wait)return;navigating=true;var back=location.pathname+location.search+location.hash;location.replace(wait+"?return="+encodeURIComponent(back))}
async function inspect(){try{var response=await fetch(api+"/status",{cache:"no-store"}),value=await response.json();if(transition(value))navigate()}catch(e){}}
var events=new EventSource(api+"/events");events.addEventListener("state",inspect);inspect();
}catch(e){}
})();</script>`
export const SESSION_VALIDITY_GUARD = `<script>(function(){
try{
var checking=false;
async function inspect(){if(checking)return;checking=true;try{var response=await fetch("/_dsh_platform/auth/session-context",{cache:"no-store",credentials:"same-origin"});if(response.status===401)location.reload()}catch(e){}finally{checking=false}}
setInterval(inspect,5000);addEventListener("focus",inspect);document.addEventListener("visibilitychange",function(){if(document.visibilityState==="visible")inspect()});
}catch(e){}
})();</script>`
export const PLUGIN_RECOVERY_GUARD = `<script>(function(){
try{
var nativeFetch=globalThis.fetch.bind(globalThis),markerKey="dsh-platform:plugin-load-recovery",waitPath="/_dsh_gateway/wait",failurePath="/_dsh_gateway/plugin-failure",navigating=false;
function plugin(url){var match=url.pathname.match(/^\\/plugins\\/(.+)\\/client\\.js$/);if(!match)return null;try{return decodeURIComponent(match[1])}catch(e){return match[1]}}
function target(input){try{return new URL(typeof input==="string"?input:input.url,location.href)}catch(e){return null}}
function marker(){try{var value=JSON.parse(sessionStorage.getItem(markerKey));return value&&Date.now()-value.createdAt<120000?value:null}catch(e){return null}}
function save(value){try{sessionStorage.setItem(markerKey,JSON.stringify(value))}catch(e){}}
function report(event,fields){nativeFetch("/_dsh_gateway/client-event",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(Object.assign({event:event,pathname:location.pathname},fields)),keepalive:true}).catch(function(){})}
async function json(path){try{var response=await nativeFetch(path,{cache:"no-store"});return await response.json()}catch(e){return {}}}
function transition(value){return ["restarting","switching","recovering","restart-failed"].includes(value.operation)||["snapshotting-data","switching","probation","restoring-data"].includes(value.update&&value.update.status)||["starting","stopping","stopped","restarting","recovering","failed"].includes(value.dshLifecycle&&value.dshLifecycle.state)}
async function recover(url,pluginId,reason){
var values=await Promise.all([json("/_dsh_gateway/readiness"),json("/_dsh_platform/plugin-api/v1/status")]),readiness=values[0],status=values[1],lifecycle=status.dshLifecycle||{},updated=Date.parse(lifecycle.updatedAt),recent=typeof lifecycle.taskId==="string"&&Number.isFinite(updated)&&Date.now()-updated<30000,eligible=transition(status)||recent||readiness.pluginRecoveryEligible===true;
var identity=typeof lifecycle.taskId==="string"?lifecycle.taskId:(lifecycle.updatedAt||readiness.state||"unknown"),previous=marker(),attempt=previous&&previous.identity===identity?previous.attempt+1:1,fields={pluginId:pluginId,revision:url.searchParams.get("rev"),lifecycleState:lifecycle.state||readiness.state||null,lifecycleTaskId:lifecycle.taskId||null,recoveryAttempt:attempt,reason:reason};
if(attempt>=3){report("browser.plugin-load.recovery.failed",Object.assign({level:"error"},fields));save({identity:identity,taskId:lifecycle.taskId||null,url:url.pathname+url.search,pluginId:pluginId,attempt:attempt,createdAt:Date.now(),completed:false});if(!navigating){navigating=true;location.replace(failurePath)}return true}
if(!eligible){report("browser.plugin-load.failed",Object.assign({level:"error"},fields));return false}
report("browser.plugin-load.failed",Object.assign({level:"warning"},fields));
save({identity:identity,taskId:lifecycle.taskId||null,url:url.pathname+url.search,pluginId:pluginId,attempt:attempt,createdAt:Date.now(),completed:false});
report("browser.plugin-load.recovery.started",Object.assign({level:"warning"},fields));
if(!navigating){navigating=true;var back=location.pathname+location.search+location.hash;location.replace(waitPath+"?return="+encodeURIComponent(back))}return true
}
function completed(url,pluginId){var value=marker();if(!value||value.completed||value.url!==url.pathname+url.search)return;value.completed=true;save(value);report("browser.plugin-load.recovery.completed",{level:"info",pluginId:pluginId,revision:url.searchParams.get("rev"),lifecycleTaskId:value.taskId||null,recoveryAttempt:value.attempt})}
globalThis.fetch=function(input,init){var url=target(input),pluginId=url&&plugin(url),pending=nativeFetch(input,init);if(!pluginId)return pending;return pending.then(function(response){if(response.ok){completed(url,pluginId);return response}if(response.status!==502&&response.status!==503)return response;return recover(url,pluginId,"HTTP "+response.status).then(function(active){return active?new Promise(function(){}):response})},function(error){return recover(url,pluginId,error&&error.name||"network error").then(function(active){if(active)return new Promise(function(){});throw error})})}
function errorUrl(value){
var candidates=[value&&value.filename,value&&value.error&&value.error.stack,value&&value.error&&value.error.message,value&&value.reason&&value.reason.stack,value&&value.reason&&value.reason.message];
for(var i=0;i<candidates.length;i++){if(typeof candidates[i]!=="string")continue;var match=candidates[i].match(/(?:https?:\\/\\/[^\\s)]+)?(\\/plugins\\/(?:@[^/\\s]+\\/)?[^/\\s]+\\/client\\.js(?:\\?[^\\s)]+)?)/);if(!match)continue;var url=target(match[1]);if(url)return url}
return null
}
function handleError(value,reason){var url=errorUrl(value);if(!url)return;var pluginId=plugin(url);if(!pluginId)return;recover(url,pluginId,reason).catch(function(){})}
addEventListener("error",function(event){handleError(event,"plugin module error")},true);
addEventListener("unhandledrejection",function(event){handleError({reason:event.reason},"plugin module rejection")},true);
}catch(e){}
})();</script>`
export function injectRandomUuidPolyfill(html) {
const match = /<head(?:\s[^>]*)?>/i.exec(html)
const injected = `${RANDOM_UUID_POLYFILL}${LIFECYCLE_TRANSITION_GUARD}${SESSION_VALIDITY_GUARD}${PLUGIN_RECOVERY_GUARD}`
if (match === null) return `${injected}${html}`
const end = match.index + match[0].length
return `${html.slice(0, end)}${injected}${html.slice(end)}`
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,56 @@
function parseRequestAuthority(value) {
if (
value === ''
|| /[\s/@?#]/.test(value)
|| value.endsWith(':')
) return undefined
try {
const parsed = new URL(`http://${value}`)
return parsed.hostname === '' ? undefined : parsed
} catch {
return undefined
}
}
export function isLoopbackHostname(hostname) {
if (hostname === 'localhost' || hostname === '[::1]') return true
const parts = hostname.split('.')
return parts.length === 4
&& parts[0] === '127'
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
}
function matchesTrustedAuthority(requestUrl, trustedHosts) {
if (trustedHosts.wildcard) return true
return trustedHosts.authorities.some((entry) => (
entry.anyPort ? entry.hostname === requestUrl.hostname : entry.matchAuthority === requestUrl.host
))
}
export function inspectExternalRequest(headers, trustedHosts, {
allowCrossOrigin = false,
allowUntrustedAuthority = false,
} = {}) {
const host = headers.host
if (typeof host !== 'string') return Object.freeze({ accepted: false, reason: 'missing-host' })
const hostUrl = parseRequestAuthority(host)
if (hostUrl === undefined) return Object.freeze({ accepted: false, reason: 'invalid-host' })
if (!allowUntrustedAuthority && !isLoopbackHostname(hostUrl.hostname) && !matchesTrustedAuthority(hostUrl, trustedHosts)) {
return Object.freeze({ accepted: false, reason: 'untrusted-host' })
}
if (!allowCrossOrigin && headers['sec-fetch-site'] === 'cross-site') {
return Object.freeze({ accepted: false, reason: 'cross-site' })
}
const origin = headers.origin
if (!allowCrossOrigin && origin !== undefined) {
if (typeof origin !== 'string') return Object.freeze({ accepted: false, reason: 'invalid-origin' })
try {
if (new URL(origin).host !== hostUrl.host) {
return Object.freeze({ accepted: false, reason: 'origin-mismatch' })
}
} catch {
return Object.freeze({ accepted: false, reason: 'invalid-origin' })
}
}
return Object.freeze({ accepted: true, externalAuthority: hostUrl.host })
}