mirror of
https://github.com/dsh-market/dsh-market.git
synced 2026-09-28 05:03:07 +08:00
The read itself was right; it landed too early. `HostConnectionService` is constructed behind `await BrowserAuth.create(...)`, so the `connection` service does not exist yet when the market mounts on `inject(['webServer', 'loader'])` -- both of those are ready first. The one read the market took therefore saw `undefined`, and `Array.isArray(connection?.trustedHosts) ? … : []` turned "not asked yet" into "none declared": that empty list was handed to setTrustedHostsSource for the life of the process. So 2b67f66's own subject is still true on a web host -- a deployment reached by a name answers 403 `untrusted origin` to every mutation while reads and loopback keep working, which is #729. Measured on a real host (market 1.65.3): the mount-time read sees `undefined`, and the same ctx read while handling a request sees `['<name>']`. The fix is the idiom `agentsLookupOf` already uses two functions up -- resolve at request time -- plus a one-time warn when the service is absent, because the silent fallback is what made "we could not ask" and "nothing is declared" the same fence and hid this for a release. `useTrustedHosts` is exported for the spec that covers the wiring; the fence it feeds stays covered through `sameOrigin` in tests/http.spec.ts, and the flows case sets the source directly, so the two of them together could not see when the market reads it. Verified: 1821 unit tests over 75 files green, including the new tests/trusted-hosts.spec.ts -- red against a mount-time read, green here; typecheck (src, client, tests) clean; and end to end on a real DSH web host, where a declared name reaches the handler (400 from the route's own body validation), an undeclared name is still refused (403), loopback still works, and a real mutation from the declared name answers {"ok":true,...}.
67 lines
3.2 KiB
TypeScript
67 lines
3.2 KiB
TypeScript
/**
|
|
* The wiring that feeds the origin fence (#729).
|
|
*
|
|
* `tests/http.spec.ts` covers the fence itself and `tests/flows.spec.ts` drives
|
|
* real routes through it -- but both hand it its authorities with
|
|
* `setTrustedHostsSource(...)` directly, so neither can see WHEN the market
|
|
* reads them from the host. That is the whole bug: the read was correct and
|
|
* landed too early.
|
|
*
|
|
* The host provides `connection` behind its own async init, so the market's
|
|
* mount (`inject(['webServer', 'loader'])`) wins the race and the one read it
|
|
* took saw `undefined`. The [] fallback then made "not asked yet" and "none
|
|
* declared" the same fence, and every mutating route answered 403 on a
|
|
* deployment reached by a name for the life of the process.
|
|
*/
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { sameOrigin } from '../src/http.ts'
|
|
import { useTrustedHosts } from '../src/index.ts'
|
|
|
|
const request = (host: string) => ({ headers: { host, origin: `https://${host}` } }) as never
|
|
const installed: Array<() => void> = []
|
|
afterEach(() => { while (installed.length > 0) installed.pop()!() })
|
|
|
|
/** A host context whose services arrive when the host's init finishes. */
|
|
function contextWith(services: Record<string, unknown>) {
|
|
return { get: (name: string) => services[name] } as never
|
|
}
|
|
|
|
describe('the host authorities are read per request', () => {
|
|
it('picks the service up when it arrives after the market mounted', () => {
|
|
const services: Record<string, unknown> = {}
|
|
installed.push(useTrustedHosts(contextWith(services)))
|
|
|
|
// Mount time: the host has not published the service yet, so a name is not
|
|
// yet ours and the rebinding defence stays where it was.
|
|
expect(sameOrigin(request('dsh.example.org'))).toBe(false)
|
|
|
|
// The host's init lands. Nothing tells the market; the next request decides.
|
|
services.connection = { trustedHosts: ['dsh.example.org'] }
|
|
expect(sameOrigin(request('dsh.example.org'))).toBe(true)
|
|
expect(sameOrigin(request('still-undeclared.example.org'))).toBe(false)
|
|
|
|
// And it keeps listening: a declaration that changes later is honoured too.
|
|
services.connection = { trustedHosts: ['dsh.example.org:8443'] }
|
|
expect(sameOrigin(request('dsh.example.org:8443'))).toBe(true)
|
|
expect(sameOrigin(request('dsh.example.org'))).toBe(false)
|
|
})
|
|
|
|
it('falls back to loopback-only while the host has no such service', () => {
|
|
const services: Record<string, unknown> = {}
|
|
installed.push(useTrustedHosts(contextWith(services)))
|
|
expect(sameOrigin(request('127.0.0.1:3081'))).toBe(true)
|
|
expect(sameOrigin(request('dsh.example.org'))).toBe(false)
|
|
// A host service that exists but carries nothing valid is the same answer.
|
|
services.connection = { trustedHosts: 'dsh.example.org' }
|
|
expect(sameOrigin(request('dsh.example.org'))).toBe(false)
|
|
})
|
|
|
|
it('goes back to the previous source when its effect is disposed', () => {
|
|
const services: Record<string, unknown> = { connection: { trustedHosts: ['dsh.example.org'] } }
|
|
const restore = useTrustedHosts(contextWith(services))
|
|
expect(sameOrigin(request('dsh.example.org'))).toBe(true)
|
|
restore()
|
|
expect(sameOrigin(request('dsh.example.org'))).toBe(false)
|
|
})
|
|
})
|