Hub stage 1: apps across servers, read-only
hub/apps/matrix reads apps/list and each app's config on every server (the read token first, the admin token once if a server refuses) and returns per app and server the version, available update and a config hash, plus whether versions and configs match and how many JSON leaf paths differ. Config texts never leave the hub. In hub mode the Apps section has the server selector: All servers shows the matrix with filters for differing versions, differing configs and updates; one server shows its list without write actions. The dev mock takes --variant a|b with differing apps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ef2d53a65
commit
3d8df08fa0
14 files changed
+1082
-37
No files matched your search
+5
-5
@@ -41,14 +41,14 @@ export class Upstream {
|
||||
}
|
||||
|
||||
/** The `response` of the server's envelope, or an `UpstreamError`. */
|
||||
async call(server: HubServer, path: string, params: Params = {}): Promise<unknown> {
|
||||
async call(server: HubServer, path: string, params: Params = {}, token: string = server.token): Promise<unknown> {
|
||||
const sorted = Object.entries(params).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
const key = `${server.id}\n${path}\n${new URLSearchParams(sorted).toString()}`
|
||||
const hit = this.#cache.get(key)
|
||||
if (hit !== undefined && hit.until > this.#now()) return hit.response
|
||||
|
||||
const query = new URLSearchParams(sorted)
|
||||
query.set('token', server.token)
|
||||
query.set('token', token)
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), this.#config.timeoutMs)
|
||||
|
||||
@@ -60,7 +60,7 @@ export class Upstream {
|
||||
})
|
||||
envelope = (await res.json()) as typeof envelope
|
||||
} catch (e) {
|
||||
throw new UpstreamError('unreachable', this.#describe(e, server))
|
||||
throw new UpstreamError('unreachable', this.#describe(e, [server.token, token]))
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
@@ -88,10 +88,10 @@ export class Upstream {
|
||||
}
|
||||
|
||||
/* A short reason without anything that could carry the token. */
|
||||
#describe(e: unknown, server: HubServer): string {
|
||||
#describe(e: unknown, secrets: string[]): string {
|
||||
if (e instanceof Error && e.name === 'AbortError') return `timed out after ${this.#config.timeoutMs} ms`
|
||||
const cause = e instanceof Error ? (e.cause as { code?: unknown } | undefined) : undefined
|
||||
const text = typeof cause?.code === 'string' ? cause.code : e instanceof Error ? e.message : 'request failed'
|
||||
return text.split(server.token).join('***')
|
||||
return secrets.reduce((out, secret) => (secret === '' ? out : out.split(secret).join('***')), text)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user