ci / check (push) Waiting to run
The first public release of LLeMbas CLI: a terminal coding agent and project manager for any LLM API, with permission modes, git snapshots, memory and skills, knowledge bases, MCP, voice, and a link to a LLeMbas instance whose web UI can work its sessions too. Signed Linux binaries for x64 and arm64. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.9 KiB
TypeScript
84 lines
3.9 KiB
TypeScript
// OAuth against a real authorisation server: the SDK's own example server with --oauth (discovery,
|
|
// dynamic client registration, PKCE, tokens), which approves every sign-in at once — so fetching
|
|
// the authorisation page and following its redirect plays the browser.
|
|
import { afterAll, beforeAll, expect, test } from "bun:test"
|
|
import { readFileSync, statSync } from "node:fs"
|
|
import { join } from "node:path"
|
|
import { paths } from "../src/config/paths.ts"
|
|
import { McpManager } from "../src/mcp/index.ts"
|
|
import { codeFrom } from "../src/mcp/oauth.ts"
|
|
|
|
const node = Bun.which("node")
|
|
const example = join(import.meta.dir, "..", "node_modules/@modelcontextprotocol/sdk/dist/esm/examples/server/simpleStreamableHttp.js")
|
|
const free = () => {
|
|
const s = Bun.serve({ port: 0, fetch: () => new Response() })
|
|
const p = s.port
|
|
s.stop(true)
|
|
return p
|
|
}
|
|
const [MCP, AUTH, CB] = [free(), free(), free()]
|
|
let server: ReturnType<typeof Bun.spawn> | undefined
|
|
|
|
beforeAll(async () => {
|
|
if (!node) return
|
|
server = Bun.spawn([node, example, "--oauth"], { env: { ...process.env, MCP_PORT: String(MCP), MCP_AUTH_PORT: String(AUTH) }, stdout: "pipe", stderr: "pipe" })
|
|
const until = Date.now() + 15_000
|
|
while (Date.now() < until) {
|
|
if (await fetch(`http://localhost:${MCP}/mcp`, { method: "POST" }).then(() => true, () => false)) break
|
|
await Bun.sleep(100)
|
|
}
|
|
})
|
|
afterAll(() => server?.kill())
|
|
|
|
test.skipIf(!node)("sign in: needs_auth first, then the redirect is caught, tokens stored (0600) and reused", async () => {
|
|
const cfg = { demo: { url: `http://localhost:${MCP}/mcp`, source: "global" as const, oauth: { callback_port: CB } } }
|
|
const m = new McpManager(cfg, { root: "/tmp", version: "t" })
|
|
await m.start()
|
|
expect(m.servers.get("demo")!.status).toBe("needs_auth")
|
|
let shown = ""
|
|
const s = await m.auth("demo", async (url) => {
|
|
shown = url
|
|
const r = await fetch(url, { redirect: "manual" })
|
|
await fetch(r.headers.get("location")!)
|
|
})
|
|
expect(new URL(shown).searchParams.get("code_challenge_method")).toBe("S256")
|
|
expect(s.status).toBe("connected")
|
|
expect((await m.tools().find((t) => t.name === "mcp__demo__greet")!.run({ name: "Jaro" }, { signal: new AbortController().signal } as any)).output).toBe("Hello, Jaro!")
|
|
await m.close()
|
|
const file = join(paths.data, "mcp-auth.json")
|
|
expect(statSync(file).mode & 0o777).toBe(0o600)
|
|
// Stored under the name and the URL together (a project's "demo" elsewhere cannot replace it).
|
|
const stored = JSON.parse(readFileSync(file, "utf8")) as Record<string, { url: string; tokens?: { access_token?: string } }>
|
|
const key = Object.keys(stored).find((k) => k.startsWith("demo "))!
|
|
expect(stored[key]!.tokens?.access_token).toBeTruthy()
|
|
const again = new McpManager(cfg, { root: "/tmp", version: "t" })
|
|
await again.start()
|
|
expect(again.servers.get("demo")!.status).toBe("connected")
|
|
again.logout("demo")
|
|
expect(JSON.parse(readFileSync(file, "utf8")).demo).toBeUndefined()
|
|
await again.close()
|
|
})
|
|
|
|
test.skipIf(!node)("a pasted redirect address works when the browser cannot reach this machine", async () => {
|
|
const cfg = { paste: { url: `http://localhost:${MCP}/mcp`, source: "global" as const, oauth: { callback_port: free() } } }
|
|
const m = new McpManager(cfg, { root: "/tmp", version: "t" })
|
|
let give: (s: string) => void = () => {}
|
|
const pasted = new Promise<string>((r) => (give = r))
|
|
const s = await m.auth(
|
|
"paste",
|
|
async (url) => {
|
|
// the browser ends on the redirect address; it never reaches our listener
|
|
const r = await fetch(url, { redirect: "manual" })
|
|
give(r.headers.get("location")!)
|
|
},
|
|
pasted,
|
|
)
|
|
expect(s.status).toBe("connected")
|
|
await m.close()
|
|
})
|
|
|
|
test("the code from a pasted address or a bare code", () => {
|
|
expect(codeFrom("http://127.0.0.1:19876/mcp/oauth/callback?code=abc&state=xyz")).toEqual({ code: "abc", state: "xyz", fromUrl: true })
|
|
expect(codeFrom(" abc ")).toEqual({ code: "abc" })
|
|
})
|