mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(desktop): drop legacy session token for the local spawned backend
The desktop's local backend binds to loopback, where the gateway now enforces no identity token (REST via Phase 2, WS via Phase 4 — the peer-IP + Host/Origin guard is the boundary). So the desktop's local token machinery is dead weight and is removed: - stop generating HERMES_DASHBOARD_SESSION_TOKEN + passing it to the two local-spawn child envs - fetchJson omits X-Hermes-Session-Token when the token is falsy - the local connection uses token:null + a credential-free WS URL (new buildGatewayWsUrlNoAuth helper, electron-free + unit-tested) - delete dashboard-token.cjs (+ its test): it existed solely to reconcile the served __HERMES_SESSION_TOKEN__ drift for the local backend, which the server now ignores on loopback The REMOTE auth modes are untouched: 'token' (user-saved token for a remote loopback/--insecure gateway, still sent as X-Hermes-Session-Token + ?token=) and 'oauth' (cookie + ?ticket=) both work exactly as before. Co-authored-by: Hermes subagent <noreply@nousresearch.com> Note: windows-child-process.test.cjs has one pre-existing failure on origin/main (a stale source-scan needle 'execFileSync(pyExe'); unrelated to this change and left as-is.
This commit is contained in:
parent
25da2472ac
commit
6cf12eef4e
6 changed files with 59 additions and 266 deletions
|
|
@ -78,6 +78,24 @@ function buildGatewayWsUrlWithTicket(baseUrl, ticket) {
|
|||
return `${wsScheme}://${parsed.host}${prefix}/api/ws?ticket=${encodeURIComponent(ticket)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a credential-free WS URL for the LOCAL spawned backend. The desktop's
|
||||
* own dashboard binds to loopback (127.0.0.1), where the gateway gates the WS
|
||||
* upgrade purely on the peer-IP + Host/Origin guard and IGNORES any token. So
|
||||
* the local renderer connects to a bare `ws(s)://host/prefix/api/ws` with no
|
||||
* `?token=` — there is no credential to send.
|
||||
*
|
||||
* This is distinct from buildGatewayWsUrl (the REMOTE `token` auth mode, which
|
||||
* still appends `?token=` for a user-saved remote-gateway token).
|
||||
*/
|
||||
function buildGatewayWsUrlNoAuth(baseUrl) {
|
||||
const parsed = new URL(baseUrl)
|
||||
const wsScheme = parsed.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const prefix = parsed.pathname.replace(/\/+$/, '')
|
||||
|
||||
return `${wsScheme}://${parsed.host}${prefix}/api/ws`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the WS URL the renderer would connect with, so the connection test can
|
||||
* exercise the same transport the app actually uses.
|
||||
|
|
@ -274,6 +292,7 @@ module.exports = {
|
|||
RT_COOKIE_VARIANTS,
|
||||
authModeFromStatus,
|
||||
buildGatewayWsUrl,
|
||||
buildGatewayWsUrlNoAuth,
|
||||
buildGatewayWsUrlWithTicket,
|
||||
connectionScopeKey,
|
||||
cookiesHaveSession,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const {
|
|||
RT_COOKIE_VARIANTS,
|
||||
authModeFromStatus,
|
||||
buildGatewayWsUrl,
|
||||
buildGatewayWsUrlNoAuth,
|
||||
buildGatewayWsUrlWithTicket,
|
||||
connectionScopeKey,
|
||||
cookiesHaveSession,
|
||||
|
|
@ -201,6 +202,24 @@ test('buildGatewayWsUrl url-encodes the token', () => {
|
|||
assert.equal(buildGatewayWsUrl('https://host', 'a/b c+d'), 'wss://host/api/ws?token=a%2Fb%20c%2Bd')
|
||||
})
|
||||
|
||||
// --- buildGatewayWsUrlNoAuth (local loopback, credential-free) ---
|
||||
|
||||
test('buildGatewayWsUrlNoAuth builds a bare ws URL with no token param', () => {
|
||||
assert.equal(buildGatewayWsUrlNoAuth('http://127.0.0.1:9119'), 'ws://127.0.0.1:9119/api/ws')
|
||||
})
|
||||
|
||||
test('buildGatewayWsUrlNoAuth uses wss for https', () => {
|
||||
assert.equal(buildGatewayWsUrlNoAuth('https://gw.example.com'), 'wss://gw.example.com/api/ws')
|
||||
})
|
||||
|
||||
test('buildGatewayWsUrlNoAuth honors a path prefix and never adds a credential', () => {
|
||||
const url = buildGatewayWsUrlNoAuth('http://127.0.0.1:9119/hermes/')
|
||||
assert.equal(url, 'ws://127.0.0.1:9119/hermes/api/ws')
|
||||
assert.ok(!url.includes('token='))
|
||||
assert.ok(!url.includes('ticket='))
|
||||
assert.ok(!url.includes('?'))
|
||||
})
|
||||
|
||||
// --- buildGatewayWsUrlWithTicket (oauth) ---
|
||||
|
||||
test('buildGatewayWsUrlWithTicket uses ?ticket= not ?token=', () => {
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
/**
|
||||
* Helpers for local dashboard session-token discovery.
|
||||
*
|
||||
* The desktop main process can pass HERMES_DASHBOARD_SESSION_TOKEN when it
|
||||
* spawns the local dashboard, but the dashboard is the source of truth for the
|
||||
* token it actually serves to the renderer. If those drift, HTTP readiness
|
||||
* probes still pass while /api/ws rejects the renderer's token.
|
||||
*/
|
||||
|
||||
const DEFAULT_TOKEN_FETCH_TIMEOUT_MS = 3_000
|
||||
|
||||
async function fetchPublicText(url, options = {}) {
|
||||
const { protocol } = new URL(url)
|
||||
if (protocol !== 'http:' && protocol !== 'https:') {
|
||||
throw new Error(`Unsupported Hermes backend URL protocol: ${protocol}`)
|
||||
}
|
||||
|
||||
const timeoutMs = options.timeoutMs ?? DEFAULT_TOKEN_FETCH_TIMEOUT_MS
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }).catch(error => {
|
||||
if (error.name === 'TimeoutError') {
|
||||
throw new Error(`Timed out connecting to Hermes backend after ${timeoutMs}ms`)
|
||||
}
|
||||
throw error
|
||||
})
|
||||
const text = await res.text()
|
||||
|
||||
if (!res.ok) throw new Error(`${res.status}: ${text || res.statusText}`)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
function extractInjectedDashboardToken(html) {
|
||||
const match = /window\.__HERMES_SESSION_TOKEN__\s*=\s*("(?:\\.|[^"\\])*")/.exec(String(html || ''))
|
||||
if (!match) return null
|
||||
try {
|
||||
return JSON.parse(match[1])
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function dashboardIndexUrl(baseUrl) {
|
||||
return `${String(baseUrl || '').replace(/\/+$/, '')}/`
|
||||
}
|
||||
|
||||
async function resolveServedDashboardToken(baseUrl, fallbackToken, options = {}) {
|
||||
const fetchText = options.fetchText || fetchPublicText
|
||||
const html = await fetchText(dashboardIndexUrl(baseUrl), {
|
||||
timeoutMs: options.timeoutMs ?? DEFAULT_TOKEN_FETCH_TIMEOUT_MS
|
||||
})
|
||||
const servedToken = extractInjectedDashboardToken(html)
|
||||
|
||||
if (servedToken && servedToken !== fallbackToken && typeof options.rememberLog === 'function') {
|
||||
options.rememberLog('[boot] dashboard served a different session token; using served token for WebSocket auth')
|
||||
}
|
||||
|
||||
return servedToken || fallbackToken
|
||||
}
|
||||
|
||||
/**
|
||||
* A served token that differs from our spawn token while our child is DEAD
|
||||
* came from a process we did not spawn (orphan/port squatter that satisfied
|
||||
* the public /api/status readiness probe). With a live child the mismatch is
|
||||
* benign: our own backend regenerated the token because the env pin did not
|
||||
* survive the spawn.
|
||||
*/
|
||||
function isForeignBackendToken({ servedToken, spawnToken, childAlive }) {
|
||||
return Boolean(servedToken) && servedToken !== spawnToken && !childAlive
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the token the backend actually serves, adopting benign drift and
|
||||
* failing loudly on a foreign backend. `childAlive` is a thunk so liveness is
|
||||
* sampled after the fetch, not before.
|
||||
*/
|
||||
async function adoptServedDashboardToken(baseUrl, spawnToken, { childAlive, label = 'Hermes backend', ...options }) {
|
||||
const servedToken = await resolveServedDashboardToken(baseUrl, spawnToken, options).catch(error => {
|
||||
options.rememberLog?.(`[boot] could not read served dashboard token (${label}): ${error.message}`)
|
||||
return spawnToken
|
||||
})
|
||||
|
||||
if (isForeignBackendToken({ servedToken, spawnToken, childAlive: childAlive() })) {
|
||||
throw new Error(
|
||||
`${label} exited and ${dashboardIndexUrl(baseUrl)} is served by a process we did not spawn; refusing its session token.`
|
||||
)
|
||||
}
|
||||
|
||||
return servedToken
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_TOKEN_FETCH_TIMEOUT_MS,
|
||||
adoptServedDashboardToken,
|
||||
dashboardIndexUrl,
|
||||
extractInjectedDashboardToken,
|
||||
fetchPublicText,
|
||||
isForeignBackendToken,
|
||||
resolveServedDashboardToken
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
/**
|
||||
* Tests for electron/dashboard-token.cjs.
|
||||
*
|
||||
* Run with: node --test electron/dashboard-token.test.cjs
|
||||
* (Wired into npm test:desktop:platforms in package.json.)
|
||||
*/
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const {
|
||||
adoptServedDashboardToken,
|
||||
dashboardIndexUrl,
|
||||
extractInjectedDashboardToken,
|
||||
fetchPublicText,
|
||||
isForeignBackendToken,
|
||||
resolveServedDashboardToken
|
||||
} = require('./dashboard-token.cjs')
|
||||
|
||||
test('extractInjectedDashboardToken reads the JSON-encoded dashboard token', () => {
|
||||
const html = '<script>window.__HERMES_SESSION_TOKEN__="served-token";window.__HERMES_BASE_PATH__=""</script>'
|
||||
assert.equal(extractInjectedDashboardToken(html), 'served-token')
|
||||
})
|
||||
|
||||
test('extractInjectedDashboardToken handles escaped token strings', () => {
|
||||
const html = '<script>window.__HERMES_SESSION_TOKEN__="served\\\\token\\"quoted";</script>'
|
||||
assert.equal(extractInjectedDashboardToken(html), 'served\\token"quoted')
|
||||
})
|
||||
|
||||
test('extractInjectedDashboardToken returns null for missing or malformed values', () => {
|
||||
assert.equal(extractInjectedDashboardToken('<html></html>'), null)
|
||||
assert.equal(extractInjectedDashboardToken('<script>window.__HERMES_SESSION_TOKEN__={bad}</script>'), null)
|
||||
})
|
||||
|
||||
test('dashboardIndexUrl preserves dashboard path prefixes', () => {
|
||||
assert.equal(dashboardIndexUrl('http://127.0.0.1:9120'), 'http://127.0.0.1:9120/')
|
||||
assert.equal(dashboardIndexUrl('https://host.example/hermes/'), 'https://host.example/hermes/')
|
||||
})
|
||||
|
||||
test('resolveServedDashboardToken uses the served token and logs when it differs', async () => {
|
||||
const logs = []
|
||||
const token = await resolveServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
|
||||
fetchText: async url => {
|
||||
assert.equal(url, 'http://127.0.0.1:9120/')
|
||||
return '<script>window.__HERMES_SESSION_TOKEN__="served-token";</script>'
|
||||
},
|
||||
rememberLog: line => logs.push(line)
|
||||
})
|
||||
|
||||
assert.equal(token, 'served-token')
|
||||
assert.equal(logs.length, 1)
|
||||
assert.match(logs[0], /served a different session token/)
|
||||
})
|
||||
|
||||
test('resolveServedDashboardToken falls back when the served HTML has no token', async () => {
|
||||
const token = await resolveServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
|
||||
fetchText: async () => '<html></html>',
|
||||
rememberLog: () => {
|
||||
throw new Error('should not log when no served token is present')
|
||||
}
|
||||
})
|
||||
|
||||
assert.equal(token, 'spawn-token')
|
||||
})
|
||||
|
||||
test('resolveServedDashboardToken does not log when served token matches fallback', async () => {
|
||||
const token = await resolveServedDashboardToken('http://127.0.0.1:9120', 'same-token', {
|
||||
fetchText: async () => '<script>window.__HERMES_SESSION_TOKEN__="same-token";</script>',
|
||||
rememberLog: () => {
|
||||
throw new Error('should not log when token already matches')
|
||||
}
|
||||
})
|
||||
|
||||
assert.equal(token, 'same-token')
|
||||
})
|
||||
|
||||
test('resolveServedDashboardToken propagates fetch errors so callers can fall back explicitly', async () => {
|
||||
await assert.rejects(
|
||||
() =>
|
||||
resolveServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
|
||||
fetchText: async () => {
|
||||
throw new Error('boom')
|
||||
}
|
||||
}),
|
||||
/boom/
|
||||
)
|
||||
})
|
||||
|
||||
test('fetchPublicText rejects unsupported protocols', async () => {
|
||||
await assert.rejects(() => fetchPublicText('file:///tmp/index.html'), /Unsupported Hermes backend URL protocol/)
|
||||
})
|
||||
|
||||
test('isForeignBackendToken only flags a mismatched token from a dead child', () => {
|
||||
const cases = [
|
||||
[{ servedToken: 'other', spawnToken: 'mine', childAlive: false }, true],
|
||||
// Live child + drift = our backend regenerated the token (env pin lost).
|
||||
[{ servedToken: 'other', spawnToken: 'mine', childAlive: true }, false],
|
||||
[{ servedToken: 'mine', spawnToken: 'mine', childAlive: false }, false],
|
||||
[{ servedToken: 'mine', spawnToken: 'mine', childAlive: true }, false],
|
||||
[{ servedToken: null, spawnToken: 'mine', childAlive: false }, false],
|
||||
[{ servedToken: '', spawnToken: 'mine', childAlive: false }, false]
|
||||
]
|
||||
for (const [input, expected] of cases) {
|
||||
assert.equal(isForeignBackendToken(input), expected, JSON.stringify(input))
|
||||
}
|
||||
})
|
||||
|
||||
test('adoptServedDashboardToken adopts drift from a live child', async () => {
|
||||
const token = await adoptServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
|
||||
childAlive: () => true,
|
||||
fetchText: async () => '<script>window.__HERMES_SESSION_TOKEN__="served-token";</script>'
|
||||
})
|
||||
|
||||
assert.equal(token, 'served-token')
|
||||
})
|
||||
|
||||
test('adoptServedDashboardToken refuses a foreign token when our child is dead', async () => {
|
||||
await assert.rejects(
|
||||
() =>
|
||||
adoptServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
|
||||
childAlive: () => false,
|
||||
fetchText: async () => '<script>window.__HERMES_SESSION_TOKEN__="squatter-token";</script>',
|
||||
label: 'Hermes backend for profile "work"'
|
||||
}),
|
||||
/profile "work".*process we did not spawn/
|
||||
)
|
||||
})
|
||||
|
||||
test('adoptServedDashboardToken falls back to the spawn token when the fetch fails', async () => {
|
||||
const logs = []
|
||||
const token = await adoptServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
|
||||
childAlive: () => true,
|
||||
fetchText: async () => {
|
||||
throw new Error('boom')
|
||||
},
|
||||
rememberLog: line => logs.push(line)
|
||||
})
|
||||
|
||||
assert.equal(token, 'spawn-token')
|
||||
assert.equal(logs.length, 1)
|
||||
assert.match(logs[0], /could not read served dashboard token \(Hermes backend\): boom/)
|
||||
})
|
||||
|
|
@ -34,7 +34,6 @@ const {
|
|||
} = require('./session-windows.cjs')
|
||||
const { canImportHermesCli, verifyHermesCli } = require('./backend-probes.cjs')
|
||||
const { probeGatewayWebSocket } = require('./gateway-ws-probe.cjs')
|
||||
const { adoptServedDashboardToken } = require('./dashboard-token.cjs')
|
||||
const { waitForDashboardPort } = require('./backend-ready.cjs')
|
||||
const { serializeJsonBody, setJsonRequestHeaders } = require('./oauth-net-request.cjs')
|
||||
const { fetchMarketplaceThemes, searchMarketplaceThemes } = require('./vscode-marketplace.cjs')
|
||||
|
|
@ -57,6 +56,7 @@ const { isPackagedInstallPath: isPackagedInstallPathUnderRoots } = require('./wo
|
|||
const {
|
||||
authModeFromStatus,
|
||||
buildGatewayWsUrl,
|
||||
buildGatewayWsUrlNoAuth,
|
||||
buildGatewayWsUrlWithTicket,
|
||||
connectionScopeKey,
|
||||
cookiesHaveSession,
|
||||
|
|
@ -2638,7 +2638,10 @@ function fetchJson(url, token, options = {}) {
|
|||
method: options.method || 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Hermes-Session-Token': token,
|
||||
// The LOCAL loopback backend needs no credential — the server ignores
|
||||
// any identity token there — so a null/empty token omits the header
|
||||
// entirely. The REMOTE 'token' auth mode still sends its token.
|
||||
...(token ? { 'X-Hermes-Session-Token': token } : {}),
|
||||
...(body ? { 'Content-Length': String(body.length) } : {})
|
||||
}
|
||||
},
|
||||
|
|
@ -3268,6 +3271,9 @@ function closePreviewWatchers() {
|
|||
}
|
||||
}
|
||||
|
||||
// Poll /api/status until the backend answers. `token` is optional: the LOCAL
|
||||
// loopback backend sends no credential (the server ignores it there), so it's
|
||||
// omitted; the REMOTE 'token' auth mode still passes its user-saved token.
|
||||
async function waitForHermes(baseUrl, token) {
|
||||
const deadline = Date.now() + 45_000
|
||||
let lastError = null
|
||||
|
|
@ -4696,7 +4702,6 @@ async function spawnPoolBackend(profile, entry) {
|
|||
}
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(32).toString('base64url')
|
||||
// --profile wins over the inherited HERMES_HOME env (see _apply_profile_override
|
||||
// step 3 in hermes_cli/main.py), so the child re-homes to this profile.
|
||||
// --port 0: the OS assigns an ephemeral port; the child announces it on stdout.
|
||||
|
|
@ -4720,7 +4725,6 @@ async function spawnPoolBackend(profile, entry) {
|
|||
// the child process. Inherited TERMINAL_CWD (or a stale config bridge)
|
||||
// can still point at the install dir even when spawn cwd is home.
|
||||
TERMINAL_CWD: hermesCwd,
|
||||
HERMES_DASHBOARD_SESSION_TOKEN: token,
|
||||
// Marks this dashboard backend as desktop-spawned so it runs the cron
|
||||
// scheduler tick loop (the gateway isn't running under the app).
|
||||
HERMES_DESKTOP: '1',
|
||||
|
|
@ -4731,7 +4735,6 @@ async function spawnPoolBackend(profile, entry) {
|
|||
})
|
||||
)
|
||||
entry.process = child
|
||||
entry.token = token
|
||||
|
||||
child.stdout.on('data', rememberLog)
|
||||
child.stderr.on('data', rememberLog)
|
||||
|
|
@ -4761,23 +4764,20 @@ async function spawnPoolBackend(profile, entry) {
|
|||
entry.port = port
|
||||
|
||||
const baseUrl = `http://127.0.0.1:${port}`
|
||||
await Promise.race([waitForHermes(baseUrl, token), startFailed])
|
||||
await Promise.race([waitForHermes(baseUrl), startFailed])
|
||||
ready = true
|
||||
const authToken = await adoptServedDashboardToken(baseUrl, token, {
|
||||
childAlive: () => child.exitCode === null && !child.killed,
|
||||
label: `Hermes backend for profile "${profile}"`,
|
||||
rememberLog
|
||||
})
|
||||
entry.token = authToken
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
mode: 'local',
|
||||
source: 'local',
|
||||
// The local backend binds to loopback, where the gateway ignores any
|
||||
// identity token (peer-IP + Host/Origin guard is the boundary). No
|
||||
// credential is sent: REST omits X-Hermes-Session-Token, WS omits ?token=.
|
||||
authMode: 'token',
|
||||
token: authToken,
|
||||
token: null,
|
||||
profile,
|
||||
wsUrl: `ws://127.0.0.1:${port}/api/ws?token=${encodeURIComponent(authToken)}`,
|
||||
wsUrl: buildGatewayWsUrlNoAuth(baseUrl),
|
||||
logs: hermesLog.slice(-80),
|
||||
...getWindowState()
|
||||
}
|
||||
|
|
@ -4899,7 +4899,6 @@ async function startHermes() {
|
|||
}
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(32).toString('base64url')
|
||||
// --port 0: the OS assigns an ephemeral port; the child announces it on stdout.
|
||||
const dashboardArgs = ['dashboard', '--no-open', '--host', '127.0.0.1', '--port', '0']
|
||||
// Pin the desktop's chosen profile via the global --profile flag. This is
|
||||
|
|
@ -4937,7 +4936,6 @@ async function startHermes() {
|
|||
HERMES_HOME,
|
||||
...backend.env,
|
||||
TERMINAL_CWD: hermesCwd,
|
||||
HERMES_DASHBOARD_SESSION_TOKEN: token,
|
||||
// Marks this dashboard backend as desktop-spawned so it runs the cron
|
||||
// scheduler tick loop (the gateway isn't running under the app).
|
||||
HERMES_DESKTOP: '1',
|
||||
|
|
@ -5001,13 +4999,8 @@ async function startHermes() {
|
|||
|
||||
const baseUrl = `http://127.0.0.1:${port}`
|
||||
await advanceBootProgress('backend.wait', 'Waiting for Hermes backend to become ready', 90)
|
||||
await Promise.race([waitForHermes(baseUrl, token), backendStartFailed])
|
||||
await Promise.race([waitForHermes(baseUrl), backendStartFailed])
|
||||
backendReady = true
|
||||
const authToken = await adoptServedDashboardToken(baseUrl, token, {
|
||||
// The exit/error handlers null hermesProcess when the child dies.
|
||||
childAlive: () => hermesProcess !== null && hermesProcess.exitCode === null && !hermesProcess.killed,
|
||||
rememberLog
|
||||
})
|
||||
updateBootProgress({
|
||||
phase: 'backend.ready',
|
||||
message: 'Hermes backend is ready. Finalizing desktop startup',
|
||||
|
|
@ -5020,9 +5013,12 @@ async function startHermes() {
|
|||
baseUrl,
|
||||
mode: 'local',
|
||||
source: 'local',
|
||||
// The local backend binds to loopback, where the gateway ignores any
|
||||
// identity token (peer-IP + Host/Origin guard is the boundary). No
|
||||
// credential is sent: REST omits X-Hermes-Session-Token, WS omits ?token=.
|
||||
authMode: 'token',
|
||||
token: authToken,
|
||||
wsUrl: `ws://127.0.0.1:${port}/api/ws?token=${encodeURIComponent(authToken)}`,
|
||||
token: null,
|
||||
wsUrl: buildGatewayWsUrlNoAuth(baseUrl),
|
||||
logs: hermesLog.slice(-80),
|
||||
...getWindowState()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
"test:desktop:nsis": "node scripts/test-desktop.mjs nsis",
|
||||
"test:desktop:existing": "node scripts/test-desktop.mjs existing",
|
||||
"test:desktop:fresh": "node scripts/test-desktop.mjs fresh",
|
||||
"test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/windows-user-env.test.cjs",
|
||||
"test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/windows-user-env.test.cjs",
|
||||
"typecheck": "tsc -p . --noEmit",
|
||||
"lint": "eslint src/ electron/",
|
||||
"lint:fix": "eslint src/ electron/ --fix",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue