oclaw/uds-auth/test/remediation-0310.test.js
oliver df97c5e042
Some checks are pending
ci / test (push) Waiting to run
ci / test-postgresql (push) Waiting to run
fix(uds-auth): emergency login workspaces, Desktop key prompts, persistent sessions
Emergency (fallback / sealed-box) admins lost the workspace sidebar and saw
"sign-in failed / add API key" on DSH Desktop because the remote.mux opens
anonymously at boot and every early call was rejected terminally.

- gateway: anonymous workspace/follow reaches dsh-acl's empty baseline;
  anonymous streams are parked until the carrier aborts instead of failing;
  anonymous workspace/initializeDefault answers "nothing created"
- request-auth: a dead UDS_SESSION cookie no longer shadows a valid WS ticket
  or bridge (sync + async variants used by all callers)
- dsh-acl: /api/* waits (bounded 15s) for the gateway ACL during Host startup
  instead of 403, so the Desktop welcome read no longer fails
- desktop-bootstrap: key/account projection is best-effort per reference
- client: emit connection/reset after login so boot-time caches (settings
  describe mirror -> Settings > Models) re-read under the real principal
- sessions: absolute lifetime fixed at 7 days (was min(ttl, 7d) = 30 min);
  bridge expiry slides with its session; MemoryStore persisted to
  sessions.json (bearer hash only, userData AES-256-GCM sealed)
- README: session lifetime/persistence and DSH compatibility notes

Also includes previously uncommitted uds-auth 0.3.x work in this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:11:09 +08:00

353 lines
22 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { mkdtemp, readFile, writeFile, mkdir, rm, readdir, lstat } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { RolesStore, ROLES } from '../lib/roles.js'
import { hashPasswordAsync, derivePasswordKey } from '../lib/utils/password-kdf.js'
import { sealLocalAdminBox, openLocalAdminBoxAsync } from '../lib/local-admin.js'
import { QrChallengeStore, qrLoginCredentials } from '../lib/qr-challenge.js'
import { TaskCapabilityStore } from '../lib/task-capability.js'
import { createTaskEnvironment } from '../lib/task-environment.js'
import { SessionBridgeStore } from '../lib/session-bridge.js'
import { extractSessionBearer } from '../lib/session/request-auth.js'
import { migrateState } from '../lib/state-migration.js'
import { atomicWriteFile } from '../lib/utils/atomic-write.js'
import { checkRequestOrigin } from '../lib/utils/origin-guard.js'
import { protectProfileDirectory } from '../lib/profile-security.js'
import { runInNewContext } from 'node:vm'
import { installGatewayPolicy } from '../lib/gateway-policy.js'
import { installGatewayEvents } from '../lib/gateway-events.js'
test('Gateway guards new namespaces and lookup owners before invocation, including live streams', async () => {
let principal = { empNo: 'u' }, active = true, viewAll = false, prepared = 0
const descriptors = {
global: { namespace: 'newPlugin', method: 'global', parameters: [], invocation: { kind: 'direct' } },
read: { namespace: 'newPlugin', method: 'read', parameters: [{ name: 'workspaceFileScope', source: 'lookup', wire: 'workspaceFileScopeId' }], invocation: { kind: 'direct' } },
stream: { namespace: 'newPlugin', method: 'stream', parameters: [{ name: 'sessionId', wire: 'sessionId' }], invocation: { kind: 'direct' }, mode: 'stream' },
}
const gateway = {
async *openRemoteEvents() {}, receiveRemoteEventResult() {},
resolveDescriptor(ns, method) { return descriptors[method] },
async prepareInvocation(request) {
prepared++
return { method: request.method === 'stream' ? async function* () { yield 'first'; yield 'second' } : () => 'ok' }
},
}
installGatewayPolicy(gateway, { identity: () => principal,
assertPrincipal: p => { if (!p || !active) throw new Error('login_required') },
permissions: () => ({ canAccessSettings: viewAll }),
assertSession: sid => { if (sid !== 'own') throw new Error('session_forbidden') },
deny: code => { throw new Error(code) } })
await assert.rejects(gateway.prepareInvocation({ namespace: 'newPlugin', method: 'global', args: {} }), /forbidden_settings/)
await assert.rejects(gateway.prepareInvocation({ namespace: 'newPlugin', method: 'read', args: { workspaceFileScopeId: 'foreign' } }), /session_forbidden/)
assert.equal(prepared, 0, 'authorization precedes providers and lookups')
const invocation = await gateway.prepareInvocation({ namespace: 'newPlugin', method: 'read', args: { workspaceFileScopeId: 'own' } })
assert.equal(invocation.method(), 'ok')
const stream = (await gateway.prepareInvocation({ namespace: 'newPlugin', method: 'stream', args: { sessionId: 'own' } })).method()
assert.equal((await stream.next()).value, 'first')
active = false; await assert.rejects(stream.next(), /login_required/)
principal = undefined
assert.equal((await gateway.prepareInvocation({ namespace: 'newPlugin', method: 'global', args: {} })).method(), 'ok')
principal = null; await assert.rejects(gateway.prepareInvocation({ namespace: 'newPlugin', method: 'global', args: {} }), /login_required/)
assert.throws(() => installGatewayPolicy({}, {}), /unsupported_gateway_acl_contract/)
})
test('Gateway event stream hides foreign events and binds responses to the issuing local session', async () => {
let principal = { empNo: 'u', sessionId: 'login1' }, active = true
let finish
const gate = new Promise(resolve => { finish = resolve })
const gateway = { pendingRemoteEvents: new Map([['event-own', { frame: { agentId: 'own' } }]]),
async *openRemoteEvents() {
yield { type: 'ready', clientId: 'client1', host: { home: 'private-home' } }
yield { type: 'emit', event: 'api-session/added', args: [{ id: 'foreign', title: 'secret' }] }
yield { type: 'emit', event: 'api-session/status', args: ['own', true] }
yield { type: 'emit', event: 'credentials/record-updated', args: ['global-secret'] }
yield { type: 'waterfall', eventId: 'event-foreign', agentId: 'foreign', request: { secret: 'hidden' } }
yield { type: 'waterfall', eventId: 'event-own', agentId: 'own', request: {} }
await gate
yield { type: 'emit', event: 'api-session/status', args: ['own', false] }
}, receiveRemoteEventResult() { return 'accepted' } }
installGatewayEvents(gateway, { identity: () => principal,
assertPrincipal: p => { if (!p || !active) throw new Error('login_required') },
permissions: () => ({ canAccessSettings: false }), assertSession: sid => { if (sid !== 'own') throw new Error('session_forbidden') },
deny: code => { throw new Error(code) } })
const stream = gateway.openRemoteEvents()
assert.deepEqual((await stream.next()).value.host, { home: '' })
assert.equal((await stream.next()).value.event, 'api-session/status')
assert.equal((await stream.next()).value.eventId, 'event-own')
const result = { clientId: 'client1', eventId: 'event-own' }
assert.equal(gateway.receiveRemoteEventResult({}, result), 'accepted')
principal = { empNo: 'u', sessionId: 'login2' }
assert.throws(() => gateway.receiveRemoteEventResult({}, result), /event_client_forbidden/)
active = false; finish(); await assert.rejects(stream.next(), /login_required/)
})
test('background shell capability survives foreground completion and ends with its own job', () => {
const caps = new TaskCapabilityStore(), roles = new RolesStore({}), controller = new AbortController()
const env = createTaskEnvironment({ getOwner: () => 'u', rolesStore: roles, capabilities: caps, getWebServer: () => ({ port: 12345 }) })
const execution = { token: {}, agent: { session: { header: { id: 'own' } } }, signal: controller.signal }
const values = env.contributor.resolve(execution)
let listener, unsubscribed = false
const jobs = { list: () => [{ id: 'job1', status: 'running' }], events: { subscribe(scope, fn) { assert.deepEqual(scope, { owner: 'own' }); listener = fn; return () => { unsubscribed = true } } } }
env.finish(execution, { value: { kind: 'background', jobId: 'job1' } }, jobs)
controller.abort()
assert.ok(caps.verify(values.DSH_UDS_TASK_CAPABILITY))
listener({ type: 'settled', job: { id: 'other' } }); assert.ok(caps.verify(values.DSH_UDS_TASK_CAPABILITY))
listener({ type: 'settled', job: { id: 'job1' } }); assert.equal(caps.verify(values.DSH_UDS_TASK_CAPABILITY), null)
assert.equal(unsubscribed, true)
})
async function temporary(t) {
const dir = await mkdtemp(join(tmpdir(), 'uds-auth-0310-'))
t.after(() => rm(dir, { recursive: true, force: true }))
return dir
}
test('password transactions queue set/set/clear and persist a restart-safe generation', async t => {
const dir = await temporary(t), file = join(dir, 'roles.json')
const store = new RolesStore({ rolesFile: file })
await store.init()
await Promise.all([
store.setFallbackPassword('password-first-0310', ROLES.SUPER_ADMIN),
store.setFallbackPassword('password-second-0310', ROLES.SUPER_ADMIN),
store.clearFallbackPassword(ROLES.SUPER_ADMIN),
])
assert.equal(store.isFallbackEnabled(), false)
assert.equal(store.getFallbackCredVersion(), 3)
const reopened = new RolesStore({ rolesFile: file }); await reopened.init()
assert.equal(reopened.isFallbackEnabled(), false)
assert.equal(reopened.getFallbackCredVersion(), 3)
})
test('queued admin writes recheck the actor after a preceding demotion commits', async t => {
const store = new RolesStore({ rolesFile: join(await temporary(t), 'roles.json') }); await store.init()
await store.ensureUser('actor', ROLES.SUPER_ADMIN); await store.setRole('actor', ROLES.ADMIN, ROLES.SUPER_ADMIN)
await store.ensureUser('target', ROLES.SUPER_ADMIN)
const flush = store.flush.bind(store)
let start, finish
const started = new Promise(resolve => { start = resolve }), gate = new Promise(resolve => { finish = resolve })
store.flush = async () => { start(); await gate; return flush() }
const demotion = store.setRole('actor', ROLES.USER, ROLES.SUPER_ADMIN)
await started
const stale = store.setRole('target', ROLES.ADMIN, () => store.getRole('actor'))
const denied = assert.rejects(stale, { code: 'forbidden_set_role' })
finish(); await demotion; await denied
assert.equal(store.getRole('target'), ROLES.USER)
})
test('failed credential write cannot resurrect an uncommitted peer; next queued write commits', async t => {
const file = join(await temporary(t), 'roles.json')
const store = new RolesStore({ rolesFile: file }); await store.init()
await store.setFallbackPassword('original-password-0310', ROLES.SUPER_ADMIN)
const originalFlush = store.flush.bind(store)
let start, finish
const started = new Promise(r => start = r), gate = new Promise(r => finish = r)
let count = 0
store.flush = async () => {
if (++count === 1) { start(); await gate; throw new Error('injected-write-failure') }
await originalFlush()
}
const first = store.setFallbackPassword('uncommitted-password-0310', ROLES.SUPER_ADMIN)
const failure = assert.rejects(first, /injected-write-failure/)
await started
const second = store.setFallbackPassword('committed-second-0310', ROLES.SUPER_ADMIN)
assert.equal(store.getFallbackCredVersion(), 1, 'readers see only the durable generation')
finish(); await failure; await second
assert.equal(await store.verifyFallbackAsync('uncommitted-password-0310', 'ip1'), false)
assert.equal(await store.verifyFallbackAsync('committed-second-0310', 'ip2'), true)
const again = new RolesStore({ rolesFile: file }); await again.init()
assert.equal(again.getFallbackCredVersion(), 2)
assert.equal(await again.verifyFallbackAsync('committed-second-0310', 'ip3'), true)
})
test('staged role promotion is invisible before commit; failed preferences restore durable state', async t => {
const store = new RolesStore({ rolesFile: join(await temporary(t), 'roles.json') }); await store.init()
await store.ensureUser('u', ROLES.SUPER_ADMIN); await store.ensureUser('boss', ROLES.SUPER_ADMIN)
await store.setRole('boss', ROLES.SUPER_ADMIN, ROLES.SUPER_ADMIN)
let start, finish
const started = new Promise(r => start = r), gate = new Promise(r => finish = r)
store.flush = async () => { start(); await gate; throw new Error('disk-offline') }
const promotion = store.setRole('u', ROLES.ADMIN, ROLES.SUPER_ADMIN)
const failed = assert.rejects(promotion, /disk-offline/)
await started
assert.equal(store.resolvePermissions('u').canAccessSettings, false)
finish(); await failed
assert.equal(store.getRole('u'), ROLES.USER)
await assert.rejects(store.setViewAllSessionsDurable('boss', false), /disk-offline/)
assert.equal(store.resolvePermissions('boss').canViewAllSessions, true)
})
test('runtime KDF leaves the event loop responsive and rejects queue overload', async () => {
let heartbeat = false
const timer = setTimeout(() => { heartbeat = true }, 0)
await hashPasswordAsync('responsive-password-0310')
clearTimeout(timer)
assert.equal(heartbeat, true)
const results = await Promise.allSettled(Array.from({ length: 12 }, () => derivePasswordKey('load-password', Buffer.alloc(16))))
assert.equal(results.filter(r => r.status === 'fulfilled').length, 10)
assert.equal(results.filter(r => r.status === 'rejected' && r.reason.code === 'kdf_busy').length, 2)
})
test('async sealed-box unlock uses the existing wire format and rejects wrong passphrase', async () => {
const box = sealLocalAdminBox('offline-passphrase-0310')
assert.equal((await openLocalAdminBoxAsync(box, 'offline-passphrase-0310')).ok, true)
assert.equal((await openLocalAdminBoxAsync(box, 'wrong-passphrase-0310')).ok, false)
})
test('QR pending survives; public scan coordinates cannot poll or consume on another browser', () => {
const store = new QrChallengeStore(), c = store.create({ loginSystemCode: 'sys', originSystemCode: '' })
assert.equal(c.qrCodeStr.includes(c.browserBinding), false)
assert.equal(store.peek(c.qrCodeKey, c.qrCodeValue, '').reason, 'qr_binding_required')
assert.equal(store.peek(c.qrCodeKey, c.qrCodeValue, 'f'.repeat(64)).ok, false)
assert.equal(store.peek(c.qrCodeKey, c.qrCodeValue, c.browserBinding).ok, true)
assert.equal(store.peek(c.qrCodeKey, c.qrCodeValue, c.browserBinding).ok, true)
const results = Array.from({ length: 20 }, () => store.consume(c.qrCodeKey, c.qrCodeValue, c.browserBinding))
assert.equal(results.filter(r => r.ok).length, 1)
assert.equal(results.filter(r => r.reason === 'qr_replay').length, 19)
})
test('QR expiry and missing-credential success fail closed', () => {
const store = new QrChallengeStore(), c = store.create({ loginSystemCode: 'sys', originSystemCode: '' })
store.rows.get(c.qrCodeKey).expiresAt = Date.now() - 1
assert.equal(store.consume(c.qrCodeKey, c.qrCodeValue, c.browserBinding).reason, 'qr_expired')
assert.equal(qrLoginCredentials({ code: { code: '0000' }, bo: { code: '0000' } }), null)
assert.equal(qrLoginCredentials({ code: '0000', bo: { code: '4002' }, other: { account: 'u', token: 't' } }), null)
assert.deepEqual(qrLoginCredentials({ code: '0000', bo: { code: '0000' }, other: { account: 'u', token: 't' } }), { empNo: 'u', token: 't' })
})
test('shell/cron environment derives the owner and revokes on tool result/abort/disposal', () => {
const roles = new RolesStore({}), caps = new TaskCapabilityStore()
const owners = new Map([['s1', 'u']])
const env = createTaskEnvironment({ getOwner: sid => owners.get(sid), rolesStore: roles,
capabilities: caps, getWebServer: () => ({ port: 43210 }) })
const controller = new AbortController()
const execution = { token: {}, agent: { session: { header: { id: 's1' } } }, signal: controller.signal,
arguments: { empNo: 'other', env: { UDS_TASK_CAPABILITY: 'forged' } } }
let values = env.contributor.resolve(execution)
let cap = caps.verify(values.DSH_UDS_TASK_CAPABILITY, { dshSessionId: 's1' })
assert.equal(cap.empNo, 'u'); assert.deepEqual(cap.scopes, ['outbound'])
assert.equal(values.DSH_UDS_AUTH_BASE, 'http://127.0.0.1:43210/uds-auth')
assert.equal(caps.verify(values.DSH_UDS_TASK_CAPABILITY, { dshSessionId: 's2' }), null)
env.release({ token: execution.token }); assert.equal(caps.verify(values.DSH_UDS_TASK_CAPABILITY), null)
values = env.contributor.resolve(execution); controller.abort()
assert.equal(caps.verify(values.DSH_UDS_TASK_CAPABILITY), null)
const next = { token: {}, agent: execution.agent, signal: new AbortController().signal }
values = env.contributor.resolve(next); env.dispose()
assert.equal(caps.verify(values.DSH_UDS_TASK_CAPABILITY), null)
assert.equal(env.contributor.resolve({ token: {}, agent: { session: { header: { id: 'missing' } } } }).DSH_UDS_TASK_CAPABILITY, '')
})
test('WS tickets authorize only GET with a real upgrade on the Gateway mux entry', () => {
const bridge = new SessionBridgeStore({}), cap = bridge.mintWsTicket({ empNo: 'u', bearer: 'b'.repeat(32) })
const headers = { upgrade: 'websocket', connection: 'Upgrade' }
const good = { method: 'GET', url: '/api/remote.mux?udsWsTicket=' + cap.ticket, headers }
// Wrong path / method must not consume. Missing Connection is allowed (0.3.9 Desktop).
for (const bad of [
{ ...good, url: '/foreign?udsWsTicket=' + cap.ticket },
{ ...good, method: 'POST' },
{ ...good, headers: { connection: 'Upgrade' } },
]) {
assert.equal(extractSessionBearer(bad, { sessionBridge: bridge }), null)
}
assert.equal(
extractSessionBearer(
{ ...good, headers: { upgrade: 'websocket' } },
{ sessionBridge: bridge },
)?.via,
'ws_ticket',
)
const cap2 = bridge.mintWsTicket({ empNo: 'u', bearer: 'b'.repeat(32) })
const good2 = { method: 'GET', url: '/api/remote.mux?udsWsTicket=' + cap2.ticket, headers }
assert.equal(extractSessionBearer(good2, { sessionBridge: bridge })?.via, 'ws_ticket')
assert.equal(extractSessionBearer(good2, { sessionBridge: bridge }), null)
})
test('Host/CSRF/upgrade matrix rejects hostile authority, Origin and browser cross-site signals', () => {
const req = { method: 'POST', headers: { host: 'localhost:3000', origin: 'http://localhost:3000' }, socket: { remoteAddress: '127.0.0.1' } }
assert.equal(checkRequestOrigin(req).ok, true)
for (const headers of [{ ...req.headers, host: 'evil.invalid:3000' }, { ...req.headers, origin: 'http://evil.invalid:3000' },
{ ...req.headers, origin: 'file://localhost:3000' }, { ...req.headers, origin: 'http://localhost:3000/path' },
{ ...req.headers, host: 'localhost:3000/evil' }, { ...req.headers, 'sec-fetch-site': 'cross-site' }]) {
assert.equal(checkRequestOrigin({ ...req, method: 'GET', headers }, { upgrade: true }).ok, false)
}
assert.equal(checkRequestOrigin({ ...req, headers: { host: '[::1]:3000', origin: 'null' }, socket: { remoteAddress: '::1' } }).ok, true)
assert.equal(checkRequestOrigin({ ...req, headers: { host: 'localhost:3000', origin: 'dsh-app://app' } }).ok, true)
assert.equal(checkRequestOrigin({ ...req, headers: { host: 'localhost:3000', origin: 'dsh-app://foreign' } }).ok, false)
})
test('legacy migration rolls back partial writes, resumes identical files and never overwrites conflicts', async t => {
const root = await temporary(t), src = join(root, 'src'), dst = join(root, 'dst')
await mkdir(src); await mkdir(dst)
await writeFile(join(src, 'roles.json'), '{"roles":{"boss":"super_admin"}}')
await writeFile(join(src, 'session-owners.json'), '{"owners":{"s":"boss"}}')
let calls = 0
const { copyFile } = await import('node:fs/promises')
await assert.rejects(migrateState(dst, src, { error() {} }, { copyFile: async (...args) => {
if (++calls === 2) throw Object.assign(new Error('disk error'), { code: 'EIO' })
return copyFile(...args)
} }), /disk error/)
assert.deepEqual(await readdir(dst), [])
await copyFile(join(src, 'roles.json'), join(dst, 'roles.json'))
assert.equal((await migrateState(dst, src, { info() {} })).migrated, true)
assert.equal((await migrateState(dst, src)).reason, 'already')
const conflict = join(root, 'conflict'); await mkdir(conflict)
await writeFile(join(conflict, 'roles.json'), '{"roles":{}}')
await assert.rejects(migrateState(conflict, src), { code: 'legacy_migrate_conflict' })
assert.deepEqual(await readdir(conflict), ['roles.json'])
})
test('failed atomic rename cleans temporary secret buffers from disk', async t => {
const dir = await temporary(t), dest = join(dir, 'directory'); await mkdir(dest)
await assert.rejects(atomicWriteFile(dest, 'secret'))
assert.deepEqual(await readdir(dir), ['directory'])
})
test('profile applies and verifies actual platform permissions on a disposable directory', async t => {
const dir = await temporary(t); await writeFile(join(dir, 'roles.json'), '{}')
const result = await protectProfileDirectory(dir)
if (process.platform === 'win32') {
assert.equal(result.protected, true); assert.deepEqual(result.principals, ['current-user', 'SYSTEM'])
} else {
assert.equal((await lstat(dir)).mode & 0o777, 0o700)
assert.equal((await lstat(join(dir, 'roles.json'))).mode & 0o777, 0o600)
}
})
test('actual Desktop fetch/WS wrapper preserves headers and only attaches credentials to the owned authority', async () => {
const src = await readFile(new URL('../lib/client.js', import.meta.url), 'utf8')
const calls = [], sockets = [], bridge = { empNo: 'administrator', token: 'b'.repeat(32), kind: 'sealed_box' }
const window = { fetch: async (input, opts) => { calls.push({ input, opts }); return {} },
WebSocket: function(url) { sockets.push(url) }, localStorage: { getItem: () => JSON.stringify(bridge) },
__ModuleLoader__: { load({ factory }) { factory(() => ({ createElement() {} })) } } }
const location = { protocol: 'dsh-app:', hostname: 'app', port: '', href: 'dsh-app://app/' }
const ticketOpens = []
const context = { window, location, URL, Headers, console, XMLHttpRequest: function() {
this.open = (method, url) => { ticketOpens.push(String(url)) }
this.setRequestHeader = () => {}; this.send = () => {}
this.status = 200; this.responseText = '{"ticket":"once"}'
} }
runInNewContext(src, context)
// 0.3.9: without streamBaseUrl, only loopback is trusted (not arbitrary hosts).
await window.fetch('http://example.com/api/remote')
assert.equal(calls.at(-1).opts, undefined, 'boot not ready must not trust non-loopback')
await window.fetch('http://127.0.0.1:1111/api/remote')
assert.equal(calls.at(-1).opts.headers['X-UDS-Bridge-Token'], bridge.token, 'loopback Gateway trusted before transport ready')
context.__DSH_TRANSPORT__ = { streamBaseUrl: 'http://127.0.0.1:43210/' }
await window.fetch('dsh-app://foreign/api/remote'); assert.equal(calls.at(-1).opts, undefined)
await window.fetch('http://127.0.0.1:43211/api/remote'); assert.equal(calls.at(-1).opts, undefined)
await window.fetch({ url: 'http://127.0.0.1:43210/api/remote', headers: new Headers({ 'X-Original': 'keep' }) })
assert.equal(calls.at(-1).opts.headers['X-UDS-Bridge-Token'], bridge.token)
assert.equal(calls.at(-1).opts.headers['x-original'], 'keep')
assert.equal(calls.at(-1).opts.redirect, 'error')
new window.WebSocket('ws://127.0.0.1:43210/api/remote.mux')
assert.equal(new URL(sockets.at(-1)).searchParams.get('udsWsTicket'), 'once')
assert.equal(sockets.at(-1).includes(bridge.token), false)
assert.ok(
ticketOpens[0] === '/uds-auth/api/ws-ticket'
|| ticketOpens.some((u) => u.startsWith('http://127.0.0.1:43210/uds-auth/api/ws-ticket')),
'Desktop ticket mint must hit relative /uds-auth or Host streamBaseUrl',
)
new window.WebSocket('ws://127.0.0.1:43210/foreign')
assert.equal(new URL(sockets.at(-1)).search, '')
})