LLeMbas CLI 1.5.2: a login again leaves one device; the link says why it cannot connect
ci / check (push) Canceled after 0s

A second `lembas login` to the same instance overwrote the old token without
signing it out, leaving an offline twin of the device holding its agent chats.
The old token is now signed out over the TLS that just worked, naming the new
one, so an instance that knows `replaced_by` moves the chats; a failed sign-out
is said.

`login --ca` kept the path as typed, so a service started in another directory
dialled with no CA. It is kept in full now, the link trusts what discovery got
through with (the system's bundle included), and "could not reach the instance"
says why: a CA file that is gone, or what discovery met.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-10-25 19:48:50 +00:00
1 parent 82918ba5a8
commit a03d46cf74
7 files changed
+120 -13

No files matched your search

+32
View File
@@ -22,6 +22,8 @@ interface Fake {
stop(): void
polls: number
revoked: string[]
/** What each DELETE /api/v1/token said it was replaced by. */
replacedBy: (string | undefined)[]
models: unknown[]
approveAfter: number
deny?: boolean
@@ -52,6 +54,7 @@ function lembas(o: Partial<Pick<Fake, "approveAfter" | "deny" | "protocol" | "mo
stop: () => {},
polls: 0,
revoked: [],
replacedBy: [],
approveAfter: o.approveAfter ?? 1,
deny: o.deny,
protocol: o.protocol ?? PROTOCOL,
@@ -98,6 +101,8 @@ function lembas(o: Partial<Pick<Fake, "approveAfter" | "deny" | "protocol" | "mo
}
if (u.pathname === "/api/v1/token" && req.method === "DELETE") {
state.revoked.push(auth ?? "")
const text = await req.text()
state.replacedBy.push(text ? (JSON.parse(text) as { replaced_by?: string }).replaced_by : undefined)
return new Response(null, { status: 204 })
}
if (!ours) return new Response("no", { status: 401 })
@@ -340,6 +345,33 @@ test("login again reads the models now", async () => {
expect(JSON.parse(readFileSync(cacheFile("example"), "utf8")).models[0].id).toBe("llama")
})
test("login again signs the old token out, naming the new one, so its chats follow", async () => {
fake = lembas()
await login(fake.url, { ...quick, onCode: () => {} })
fake.token = "lmb_newer"
fake.polls = 0
const r = await login(fake.url, { ...quick, onCode: () => {} })
expect(fake.revoked).toEqual(["Bearer lmb_secret"])
expect(fake.replacedBy).toEqual(["lmb_newer"])
expect(readFileSync(keyFile(r.connection), "utf8").trim()).toBe("lmb_newer")
expect(r.changes.join("\n")).not.toContain("could not be signed out")
})
test("a CA given as a relative path is kept as a full one", async () => {
fake = lembas()
const dir = join(paths.config, "certs")
mkdirSync(dir, { recursive: true })
writeFileSync(join(dir, "ca.crt"), "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n")
const cwd = process.cwd()
process.chdir(paths.config)
try {
const r = await login(fake.url, { ...quick, ca: "certs/ca.crt", onCode: () => {} })
expect(instances()[r.connection]!.ca).toBe(join(dir, "ca.crt"))
} finally {
process.chdir(cwd)
}
})
test("logout revokes the token and removes exactly what login wrote", async () => {
writeFileSync(join(paths.config, "connections.yaml"), "connections:\n local:\n dialect: openai-chat\n base_url: http://x/v1\n models: { m: {} }\n", { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "voice:\n stt:\n provider: openai\n base_url: http://stt.local/v1\n")