123 lines
4.5 KiB
TypeScript
123 lines
4.5 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// Real signed assertions through the template's request adapter, across separate requests.
|
|
import { generateKeyPairSync, randomUUID, sign } from "node:crypto"
|
|
import { createServer } from "node:http"
|
|
import type { AddressInfo } from "node:net"
|
|
import { buildRegistry, Container } from "@conusai-kit/core"
|
|
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
|
|
const mocks = vi.hoisted(() => ({ runtime: vi.fn() }))
|
|
vi.mock("../apps/web/src/server/app", () => ({ webRuntime: mocks.runtime }))
|
|
vi.mock("@conusai-kit/tenancy", async (original) => ({
|
|
...(await original<Record<string, unknown>>()),
|
|
createTenancyHooks: () => ({}),
|
|
}))
|
|
|
|
const { supportOboCaller } = await import("../apps/web/src/server/support-obo")
|
|
const issuer = "https://platform.example.test"
|
|
const audience = "https://app.example.test"
|
|
const keys = generateKeyPairSync("ed25519")
|
|
const jwk = { ...keys.publicKey.export({ format: "jwk" }), kid: "synthetic", alg: "EdDSA", use: "sig" }
|
|
const registry = buildRegistry([])
|
|
const httpFetch = globalThis.fetch
|
|
|
|
function configure() {
|
|
mocks.runtime.mockReturnValue({
|
|
data: {},
|
|
registry,
|
|
env: {
|
|
deployment: {
|
|
CONUSAI_API_URL: `${issuer}/api`,
|
|
CONUSAI_PUBLIC_URL: audience,
|
|
CONUSAI_ORG_ID: "3",
|
|
CONUSAI_PROJECT_ID: "4",
|
|
CONUSAI_ENVIRONMENT_ID: "5",
|
|
},
|
|
},
|
|
})
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => Response.json({ keys: [jwk] })),
|
|
)
|
|
}
|
|
|
|
function assertion(extra: Record<string, unknown> = {}) {
|
|
const now = Math.floor(Date.now() / 1000)
|
|
const encode = (value: object) => Buffer.from(JSON.stringify(value)).toString("base64url")
|
|
const body = `${encode({ alg: "EdDSA", typ: "conusai-obo+jwt", kid: "synthetic" })}.${encode({
|
|
iss: issuer,
|
|
aud: audience,
|
|
sub: "synthetic-user",
|
|
sub_ns: "app",
|
|
app_tenant: "tenant-a",
|
|
conv: "synthetic-conversation",
|
|
channel: "sc_synthetic",
|
|
org: "3",
|
|
project: "4",
|
|
env: "5",
|
|
jti: randomUUID(),
|
|
iat: now,
|
|
exp: now + 60,
|
|
...extra,
|
|
})}`
|
|
return `${body}.${sign(null, Buffer.from(body), keys.privateKey).toString("base64url")}`
|
|
}
|
|
|
|
function request(token?: string, pathname = "/api/mcp") {
|
|
return new Request(`${audience}${pathname}`, {
|
|
headers: token ? { "Conusai-On-Behalf-Of": token } : {},
|
|
})
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe("the Support OBO request adapter", () => {
|
|
it("accepts a signed assertion once and refuses its replay on a separate request", async () => {
|
|
configure()
|
|
const token = assertion()
|
|
expect(await supportOboCaller(request(token), new Container())).not.toBeNull()
|
|
await expect(supportOboCaller(request(token), new Container())).rejects.toMatchObject({ status: 401 })
|
|
})
|
|
|
|
it("refuses another environment and permits a new assertion", async () => {
|
|
configure()
|
|
await expect(supportOboCaller(request(assertion({ env: "6" })), new Container())).rejects.toMatchObject({
|
|
status: 401,
|
|
})
|
|
expect(await supportOboCaller(request(assertion()), new Container())).not.toBeNull()
|
|
})
|
|
|
|
it("keeps the replay boundary across real HTTP requests", async () => {
|
|
configure()
|
|
const server = createServer(async (incoming, response) => {
|
|
try {
|
|
const token = incoming.headers["conusai-on-behalf-of"]
|
|
const caller = await supportOboCaller(request(typeof token === "string" ? token : undefined), new Container())
|
|
response.writeHead(caller ? 200 : 204).end()
|
|
} catch (error) {
|
|
response.writeHead((error as { status?: number }).status ?? 500).end()
|
|
}
|
|
})
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
|
|
const origin = `http://127.0.0.1:${(server.address() as AddressInfo).port}`
|
|
const token = assertion()
|
|
try {
|
|
const headers = { "Conusai-On-Behalf-Of": token }
|
|
expect((await httpFetch(`${origin}/api/mcp`, { headers })).status).toBe(200)
|
|
expect((await httpFetch(`${origin}/api/mcp`, { headers })).status).toBe(401)
|
|
expect((await httpFetch(`${origin}/api/mcp`)).status).toBe(204)
|
|
} finally {
|
|
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())))
|
|
}
|
|
})
|
|
|
|
it("leaves ordinary requests and non-MCP routes on session authentication", async () => {
|
|
expect(await supportOboCaller(request(), new Container())).toBeNull()
|
|
expect(await supportOboCaller(request("ignored", "/api/ready"), new Container())).toBeNull()
|
|
expect(mocks.runtime).not.toHaveBeenCalled()
|
|
})
|
|
})
|