Files
agents-marketplace/gateway/test/http.test.ts
david 6de62b8cf0 feat(gateway): OpenAI-compatible custodial gateway
New multi-tenant gateway package: external developers point any OpenAI SDK at
the marketplace. Each API key is its own custodial buyer wallet (AES-256-GCM
sealed under GATEWAY_MASTER_KEY); every request posts an on-chain escrow from
that user's wallet.

- POST /openai/v1/chat/completions (one escrow/call, llm.text.generate.v1,
  buffered + pseudo-stream), chat sessions (one escrow/session, llm.chat.v1,
  real SSE), GET /openai/v1/models, /signup, /account, /account/withdraw.
- Settles by resolving the Submitted ref via the indexer + awaited Accept;
  state-aware reclaim sweeper + per-wallet auto-consolidation for collateral.
- Reuses the buyer Marketplace SDK via new buyer exports ./sdk; new shared
  buildWithdrawTx (custodial exit). Runs via tsx; Dockerfile + mainnet compose.

Design in docs/gateway.md. Typecheck passes for all packages; 20 vitest tests
pass. Not yet exercised against a live chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:22:58 +02:00

106 lines
4.4 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { tmpdir } from "os";
import { join } from "path";
import { randomUUID } from "crypto";
import request from "supertest";
import { createApp } from "../src/server.js";
import { GatewayStore } from "../src/db/store.js";
import { SdkRegistry } from "../src/sdk/registry.js";
import type { GatewayConfig } from "../src/config.js";
import type { GatewayDeps } from "../src/deps.js";
const SUPPLIERS = [
{ utxo_ref: "aa".repeat(32) + "#0", supplier_pkh: "s1", capability_id: "llm.text.generate.v1", model: "qwen", max_output_tokens: 256, price_lovelace: "1000000", supplier_bond_lovelace: "1000000", buyer_bond_lovelace: "1000000", endpoint_url: "http://sup", advert_status: "Active", status: "free" },
];
function makeDeps(fetchFn?: typeof globalThis.fetch): GatewayDeps {
const dbDir = join(tmpdir(), `gw-test-${randomUUID()}`);
const store = new GatewayStore(dbDir);
const config: GatewayConfig = {
masterKeyHex: "ab".repeat(32),
indexerUrl: "http://ix",
ogmiosUrl: "http://og",
networkId: 0,
liveChain: true,
port: 0,
dbDir,
signupRate: { max: 1000, windowMs: 60_000 },
keyRate: { max: 1000, windowMs: 60_000 },
sweeperIntervalMs: 60_000,
walletHealthIntervalMs: 600_000,
sdkRegistryMax: 100,
};
const chain = { queryUtxosByAddress: async () => [] } as unknown as GatewayDeps["chain"];
const registry = new SdkRegistry({
chain,
indexerUrl: config.indexerUrl,
networkId: config.networkId,
masterKeyHex: config.masterKeyHex,
max: config.sdkRegistryMax,
});
const defaultFetch = (async () => new Response("[]", { status: 200 })) as unknown as typeof globalThis.fetch;
return { config, store, chain, registry, fetchFn: fetchFn ?? defaultFetch };
}
describe("gateway HTTP", () => {
it("GET /healthz", async () => {
const res = await request(createApp(makeDeps())).get("/healthz");
expect(res.status).toBe(200);
expect(res.body).toEqual({ ok: true });
});
it("GET / serves the UI", async () => {
const res = await request(createApp(makeDeps())).get("/");
expect(res.status).toBe(200);
expect(res.text).toContain("OpenAI-compatible Gateway");
});
it("rejects unauthenticated chat completions with OpenAI error shape", async () => {
const res = await request(createApp(makeDeps()))
.post("/openai/v1/chat/completions")
.send({ model: "qwen", messages: [{ role: "user", content: "hi" }] });
expect(res.status).toBe(401);
expect(res.body.error.type).toBe("authentication_error");
expect(res.body.error.code).toBe("invalid_api_key");
});
it("signup → account roundtrip", async () => {
const app = createApp(makeDeps());
const signup = await request(app).post("/signup").send({ label: "test" });
expect(signup.status).toBe(201);
expect(signup.body.api_key).toMatch(/^vk_test_[0-9a-f]{48}$/);
expect(signup.body.deposit_address).toMatch(/^addr_test1/);
const key = signup.body.api_key as string;
const acct = await request(app).get("/account").set("authorization", `Bearer ${key}`);
expect(acct.status).toBe(200);
expect(acct.body.balance.available_lovelace).toBe("0");
expect(acct.body.collateral_ok).toBe(false);
expect(acct.body.spend.request_count).toBe(0);
});
it("GET /openai/v1/models lists distinct models", async () => {
const fetchFn = (async (url: string) =>
new Response(JSON.stringify(String(url).includes("/suppliers") ? SUPPLIERS : []), { status: 200 })) as unknown as typeof globalThis.fetch;
const app = createApp(makeDeps(fetchFn));
const signup = await request(app).post("/signup").send({});
const key = signup.body.api_key as string;
const res = await request(app).get("/openai/v1/models").set("authorization", `Bearer ${key}`);
expect(res.status).toBe(200);
expect(res.body.object).toBe("list");
expect(res.body.data.map((m: { id: string }) => m.id)).toContain("qwen");
});
it("rejects tools/functions with 400 before any chain work", async () => {
const app = createApp(makeDeps());
const signup = await request(app).post("/signup").send({});
const key = signup.body.api_key as string;
const res = await request(app)
.post("/openai/v1/chat/completions")
.set("authorization", `Bearer ${key}`)
.send({ model: "qwen", messages: [{ role: "user", content: "hi" }], tools: [{ type: "function" }] });
expect(res.status).toBe(400);
expect(res.body.error.code).toBe("unsupported_parameter");
});
});