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>
64 lines
2.5 KiB
TypeScript
64 lines
2.5 KiB
TypeScript
/**
|
|
* gateway/src/server.ts — Express app wiring.
|
|
*
|
|
* Public: GET /healthz, GET /, POST /signup (IP rate-limited).
|
|
* Bearer: GET /account, POST /account/withdraw,
|
|
* POST /openai/v1/chat/completions, GET /openai/v1/models,
|
|
* POST /openai/v1/chat/sessions[/:id/messages|/close].
|
|
*/
|
|
|
|
import express, { type Express, type Request, type Response } from "express";
|
|
import type { GatewayDeps } from "./deps.js";
|
|
import { makeApiKeyAuth } from "./middleware/apiKeyAuth.js";
|
|
import { ipRateLimit, keyRateLimit } from "./middleware/rateLimit.js";
|
|
import { sendError } from "./middleware/http.js";
|
|
import { notFound } from "./openai/errors.js";
|
|
import { makeChatCompletionsHandler } from "./openai/chatCompletions.js";
|
|
import { makeModelsHandler } from "./openai/models.js";
|
|
import {
|
|
makeOpenSessionHandler,
|
|
makeSessionMessageHandler,
|
|
makeCloseSessionHandler,
|
|
} from "./openai/sessions.js";
|
|
import { makeSignupHandler, makeAccountHandler, makeWithdrawHandler } from "./account/routes.js";
|
|
import { INDEX_HTML } from "./ui/page.js";
|
|
|
|
export function createApp(deps: GatewayDeps): Express {
|
|
const app = express();
|
|
// Behind traefik/Cloudflare — trust the proxy chain so req.ip is the client.
|
|
app.set("trust proxy", true);
|
|
app.use(express.json({ limit: "1mb" }));
|
|
|
|
app.get("/healthz", (_req: Request, res: Response) => {
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
app.get("/", (_req: Request, res: Response) => {
|
|
res.status(200).type("html").send(INDEX_HTML);
|
|
});
|
|
|
|
// Public signup, rate-limited per IP.
|
|
app.post("/signup", ipRateLimit(deps.config.signupRate), makeSignupHandler(deps));
|
|
|
|
// Bearer-gated routes: auth → per-key rate limit → handler.
|
|
const auth = makeApiKeyAuth(deps.store);
|
|
const krl = keyRateLimit(deps.config.keyRate);
|
|
const gated = [auth, krl] as const;
|
|
|
|
app.get("/account", auth, makeAccountHandler(deps));
|
|
app.post("/account/withdraw", auth, makeWithdrawHandler(deps));
|
|
|
|
app.post("/openai/v1/chat/completions", ...gated, makeChatCompletionsHandler(deps));
|
|
app.get("/openai/v1/models", auth, makeModelsHandler(deps));
|
|
app.post("/openai/v1/chat/sessions", ...gated, makeOpenSessionHandler(deps));
|
|
app.post("/openai/v1/chat/sessions/:id/messages", ...gated, makeSessionMessageHandler(deps));
|
|
app.post("/openai/v1/chat/sessions/:id/close", ...gated, makeCloseSessionHandler(deps));
|
|
|
|
// OpenAI-shaped 404 for anything else.
|
|
app.use((_req: Request, res: Response) => {
|
|
sendError(res, notFound("not_found", "unknown route"));
|
|
});
|
|
|
|
return app;
|
|
}
|