diff --git a/api-reference/endpoints.mdx b/api-reference/endpoints.mdx index 4c3c1fa..c0c19cb 100644 --- a/api-reference/endpoints.mdx +++ b/api-reference/endpoints.mdx @@ -5,6 +5,8 @@ description: "Full HTTP API reference for the managed platform" The TEE server exposes a REST API. The SDK's `Wraith` and `WraithAgent` classes wrap these endpoints — most developers don't call them directly. +The TypeScript examples on this page build on each other. Run them in order, starting with the client in the Authentication section. + ## Authentication All requests require an API key: @@ -20,6 +22,68 @@ X-AI-Provider: openai X-AI-Key: sk-... ``` +### Example Client + +The examples share one `fetch` wrapper. It sends the API key as a bearer token, adds the BYOM headers when you pass a model key, and turns error responses into a `WraithApiError`: + +```typescript +const WRAITH_API_URL = "https://api.wraith.dev"; + +type HttpMethod = "GET" | "POST" | "DELETE"; + +interface WraithClientOptions { + apiKey: string; + baseUrl?: string; + // Optional BYOM: run agent chat on your own model provider key. + ai?: { provider: "gemini" | "openai" | "claude"; apiKey: string }; +} + +class WraithApiError extends Error { + readonly statusCode: number; + + constructor(statusCode: number, message: string) { + super(message); + this.name = "WraithApiError"; + this.statusCode = statusCode; + } +} + +function createWraithClient(options: WraithClientOptions) { + const baseUrl = (options.baseUrl ?? WRAITH_API_URL).replace(/\/+$/, ""); + const headers: Record = { + Authorization: `Bearer ${options.apiKey}`, + "Content-Type": "application/json", + }; + if (options.ai) { + headers["X-AI-Provider"] = options.ai.provider; + headers["X-AI-Key"] = options.ai.apiKey; + } + + return async function request(method: HttpMethod, path: string, body?: unknown): Promise { + const res = await fetch(`${baseUrl}${path}`, { + method, + headers, + body: body === undefined ? undefined : JSON.stringify(body), + }); + + if (!res.ok) { + // Error bodies use the ApiError shape from Error Responses below. + const error = (await res.json().catch(() => null)) as Partial | null; + throw new WraithApiError(res.status, error?.message ?? res.statusText); + } + + return (await res.json()) as T; + }; +} + +const apiKey = process.env.WRAITH_API_KEY; +if (!apiKey) { + throw new Error("Set WRAITH_API_KEY to your Wraith API key"); +} + +const wraith = createWraithClient({ apiKey }); +``` + --- ## Agent Endpoints @@ -34,8 +98,8 @@ Create a new agent with a `.wraith` name. **Request:** -```typescript no-check -{ +```typescript +interface CreateAgentRequest { name: string; // "alice" (becomes alice.wraith) chain: string; // "horizen" | "stellar" | "ethereum" | ... wallet: string; // owner wallet address @@ -44,10 +108,10 @@ Create a new agent with a `.wraith` name. } ``` -**Response:** +**Response:** `AgentInfo` ```typescript -{ +interface AgentInfo { id: string; // UUID name: string; // "alice" chain: string; // "horizen" @@ -56,6 +120,28 @@ Create a new agent with a `.wraith` name. } ``` +**Example:** + +```typescript +import { Keypair } from "@stellar/stellar-sdk"; +import { Chain } from "@wraith-protocol/sdk"; + +// The owner wallet signs a message to prove it owns the new agent. +// Keypair.random() keeps the example self-contained. Sign with the real owner wallet. +const owner = Keypair.random(); +const ownershipMessage = "Sign to create Wraith agent"; + +const agent = await wraith("POST", "/agent/create", { + name: "alice", + chain: Chain.Stellar, + wallet: owner.publicKey(), + signature: owner.sign(Buffer.from(ownershipMessage)).toString("hex"), + message: ownershipMessage, +} satisfies CreateAgentRequest); + +console.log(`${agent.name}.wraith on ${agent.chain}: ${agent.metaAddress}`); +``` + ### List Agents ``` @@ -105,7 +191,7 @@ Returns balance, pending invoices, and other stats. **Response:** ```typescript -{ +interface AgentStatus { balance: string; tokens: Record; pendingInvoices: number; @@ -126,7 +212,7 @@ Export the agent's private key. Requires a fresh wallet signature. **Request:** ```typescript -{ +interface ExportKeyRequest { signature: string; // fresh signature from owner wallet message: string; // signed message } @@ -135,7 +221,7 @@ Export the agent's private key. Requires a fresh wallet signature. **Response:** ```typescript -{ +interface ExportKeyResponse { secret: string; // "0x..." — the agent's private key } ``` @@ -154,8 +240,8 @@ Send a natural language message to the AI agent. **Request:** -```typescript no-check -{ +```typescript +interface ChatRequest { message: string; conversationId?: string; // continue existing conversation } @@ -163,8 +249,8 @@ Send a natural language message to the AI agent. **Response:** -```typescript no-check -{ +```typescript +interface ChatResponse { response: string; // agent's text reply toolCalls?: ToolCall[]; // tools the agent executed conversationId: string; // conversation ID for continuity @@ -179,6 +265,24 @@ interface ToolCall { } ``` +**Example:** + +```typescript +const reply = await wraith("POST", `/agent/${agent.id}/chat`, { + message: "What is my balance?", +} satisfies ChatRequest); + +// Pass conversationId to keep the next message in the same conversation. +const followUp = await wraith("POST", `/agent/${agent.id}/chat`, { + message: "Scan for incoming stealth payments", + conversationId: reply.conversationId, +} satisfies ChatRequest); + +for (const call of followUp.toolCalls ?? []) { + console.log(`${call.name}: ${call.status}`); +} +``` + --- ## Conversations @@ -214,14 +318,14 @@ POST /agent/:id/conversations GET /agent/:id/conversations/:convId/messages ``` -**Response:** +**Response:** `Message[]` ```typescript -Array<{ +interface Message { role: "user" | "agent" | "tool" | "system"; text: string; createdAt: string; -}> +} ``` ### Delete Conversation @@ -243,7 +347,7 @@ GET /invoice/:id **Response:** ```typescript -{ +interface Invoice { id: string; agentName: string; amount: string; @@ -276,8 +380,8 @@ GET /agent/:id/notifications **Response:** -```typescript no-check -{ +```typescript +interface NotificationList { notifications: Notification[]; unreadCount: number; } @@ -319,7 +423,7 @@ GET /health **Response:** ```typescript -{ +interface HealthResponse { status: "ok"; } ``` @@ -342,12 +446,36 @@ Returns cryptographic proof that the TEE is running authentic code and the agent --- +## Pagination + +List endpoints such as `GET /agents`, `GET /agent/:id/conversations`, and `GET /agent/:id/notifications` return the whole collection in one response. They take no page or cursor parameters, so page through results on the client. This example loads messages only for the conversations on the first page: + +```typescript +const PAGE_SIZE = 20; + +function getPage(items: readonly T[], pageIndex: number, pageSize = PAGE_SIZE): T[] { + return items.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); +} + +const conversations = await wraith("GET", `/agent/${agent.id}/conversations`); + +for (const conversation of getPage(conversations, 0)) { + const messages = await wraith( + "GET", + `/agent/${agent.id}/conversations/${conversation.id}/messages`, + ); + console.log(`${conversation.title}: ${messages.length} messages`); +} +``` + +--- + ## Error Responses All errors return JSON with a `message` field: ```typescript -{ +interface ApiError { message: string; statusCode: number; } @@ -363,22 +491,43 @@ All errors return JSON with a `message` field: ### Example -```typescript no-check +```typescript // 400 Bad Request -{ - "message": "Name must be 3-32 characters, lowercase alphanumeric and hyphens only", - "statusCode": 400 -} +const invalidName: ApiError = { + message: "Name must be 3-32 characters, lowercase alphanumeric and hyphens only", + statusCode: 400, +}; // 401 Unauthorized -{ - "message": "Invalid API key", - "statusCode": 401 -} +const invalidApiKey: ApiError = { + message: "Invalid API key", + statusCode: 401, +}; // 409 Conflict -{ - "message": "Name 'alice' is already registered", - "statusCode": 409 +const nameTaken: ApiError = { + message: "Name 'alice' is already registered", + statusCode: 409, +}; +``` + +### Handling Errors + +`WraithApiError` carries the HTTP status code, so branch on `statusCode`. A 404 from the name lookup means no agent is registered under that name: + +```typescript +async function isNameAvailable(name: string): Promise { + try { + await wraith("GET", `/agent/info/${encodeURIComponent(name)}`); + return false; // an agent already uses this name + } catch (err) { + if (err instanceof WraithApiError && err.statusCode === 404) { + return true; + } + throw err; // 401 invalid API key, 500 server error, ... + } } + +console.log("alice available:", await isNameAvailable("alice")); +console.log("bob available:", await isNameAvailable("bob")); ``` diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 0c8ad2d..7a32990 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -27,6 +27,23 @@ Use `no-check` only for intentionally illustrative pseudocode: Prefer making snippets compile over opting them out. +### API endpoint examples + +`api-reference/endpoints.mdx` has a stricter gate, which `check:snippets` also runs: + +```bash +npm run check:api-examples +``` + +The checker (`scripts/check-api-examples.ts`) compiles the page's TypeScript fences +together, in page order, as one `strict` module, then runs them offline against the +typed response fixtures in `scripts/fixtures/api-endpoint-responses.json`. Each +request must hit a route documented on the page, and each fixture is compiled with +`satisfies` against the response type it names. + +When an example calls an endpoint, add one fixture per request it makes. `no-check` +is not allowed on that page. + ## Navigation coverage Every `.mdx` page in the shipped taxonomy (root pages plus `architecture/`, diff --git a/package.json b/package.json index 12daa6c..6e19237 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "type": "module", "scripts": { "check:placeholders": "tsx scripts/check-placeholders.ts", - "check:snippets": "tsx scripts/check-snippets.ts", + "check:snippets": "tsx scripts/check-snippets.ts && tsx scripts/check-api-examples.ts", + "check:api-examples": "tsx scripts/check-api-examples.ts", "check:snippets-extended": "tsx scripts/check-snippets-extended.ts", "check:nav-coverage": "node scripts/check-nav-coverage.mjs", "check:contract-registry": "node scripts/check-contract-registry.mjs", diff --git a/scripts/check-api-examples.ts b/scripts/check-api-examples.ts new file mode 100644 index 0000000..3ac68c5 --- /dev/null +++ b/scripts/check-api-examples.ts @@ -0,0 +1,427 @@ +/** + * check-api-examples.ts + * + * Type-checks and runs the TypeScript examples in api-reference/endpoints.mdx. + * + * check-snippets.ts renders every fence with `// @ts-nocheck`, so it only catches + * syntax errors. The endpoints page documents copy-paste client calls, so it gets + * a stricter gate: + * + * 1. Every ts/typescript fence on the page is extracted, in page order, into one + * module (the examples build on each other) and compiled with `strict: true` + * against the installed SDK, Stellar SDK, and Node types. There is no + * `@ts-nocheck`, no ambient `any` prelude, and no `no-check` fence allowed on + * the page. + * 2. Network responses come from typed fixtures in + * scripts/fixtures/api-endpoint-responses.json. Each fixture names a response + * type declared on the page, and its body is compiled with `satisfies `, + * so a fixture that drifts from the documented shape fails the check. + * 3. The module then runs offline against a fetch stub that serves those + * fixtures. Every request must target a route documented on the page, send + * the `Authorization: Bearer` header, and match the next unused fixture for + * that method and path. Every fixture must be consumed. + * + * Usage + * pnpm run check:api-examples + */ + +import { spawn } from "node:child_process"; +import { mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import process from "node:process"; +import { pathToFileURL } from "node:url"; + +const repoRoot = process.cwd(); +const pageFile = "api-reference/endpoints.mdx"; +const fixturesFile = "scripts/fixtures/api-endpoint-responses.json"; +const moduleFile = "endpoints.ts"; +const apiOrigin = "https://api.wraith.dev"; +const testApiKey = "wraith_test_docs_examples"; + +const typescriptLanguages = new Set(["ts", "typescript"]); +const routePattern = /^(GET|POST|PUT|PATCH|DELETE)\s+(\/\S*)$/; +const fixtureTypePattern = /^[A-Za-z_][A-Za-z0-9_]*(\[\])?$/; + +type Fence = { + lang: string; + attrs: string; + code: string; + /** 1-based MDX line of the first line inside the fence. */ + line: number; +}; + +type Route = { + method: string; + path: string; + regex: RegExp; +}; + +type Fixture = { + request: string; + status: number; + type: string; + body: unknown; +}; + +type Segment = { + /** First generated line (1-based) covered by this segment. */ + start: number; + /** Last generated line (1-based) covered by this segment. */ + end: number; + describe: (generatedLine: number, column: string) => string; +}; + +type GeneratedModule = { + source: string; + segments: Segment[]; +}; + +async function main() { + const markdown = await readFile(path.join(repoRoot, pageFile), "utf8"); + const fences = parseFences(markdown); + const examples = fences.filter((fence) => typescriptLanguages.has(fence.lang)); + const routes = fences.flatMap((fence) => parseRoute(fence) ?? []); + const fixtures = parseFixtures(await readFile(path.join(repoRoot, fixturesFile), "utf8")); + const errors: string[] = []; + + for (const fence of fences) { + if (/\bno-check\b/.test(fence.attrs)) { + errors.push( + `${pageFile}:${fence.line - 1}: remove \`no-check\`. Every fence on this page is ` + + "type-checked; use a fence without a language for prose fragments.", + ); + } + } + if (examples.length === 0) { + errors.push(`${pageFile}: no TypeScript fences found.`); + } + if (routes.length === 0) { + errors.push(`${pageFile}: no documented routes (\`METHOD /path\` fences) found.`); + } + for (const fixture of fixtures) { + const [method, requestPath] = fixture.request.split(" "); + if (!routes.some((route) => route.method === method && route.regex.test(requestPath))) { + errors.push( + `${fixturesFile}: "${fixture.request}" does not match a route documented on ${pageFile}.`, + ); + } + } + if (errors.length > 0) fail(errors); + + const generated = renderModule(examples, fixtures); + const tmp = await mkdtemp(path.join(tmpdir(), "wraith-api-examples-")); + let outcome: { errors: string[]; output: string[]; requests: string[] }; + + try { + await writeFile(path.join(tmp, "package.json"), JSON.stringify({ type: "module" }), "utf8"); + await symlink(path.join(repoRoot, "node_modules"), path.join(tmp, "node_modules"), "dir"); + await writeFile(path.join(tmp, moduleFile), generated.source, "utf8"); + await writeFile( + path.join(tmp, "tsconfig.json"), + JSON.stringify(createTsConfig(), null, 2), + "utf8", + ); + + const tsc = path.join(repoRoot, "node_modules", "typescript", "bin", "tsc"); + const typecheck = await run( + process.execPath, + [tsc, "--project", "tsconfig.json", "--pretty", "false"], + tmp, + ); + + if (typecheck.exitCode !== 0) { + const report = mapLocations(typecheck.output.trim(), generated.segments); + outcome = { errors: [`Type check failed:\n\n${report}`], output: [], requests: [] }; + } else { + const result = await runExamples(path.join(tmp, moduleFile), routes, fixtures); + outcome = { + ...result, + errors: result.errors.map((error) => mapLocations(error, generated.segments)), + }; + } + } finally { + await rm(tmp, { force: true, recursive: true }); + } + + if (outcome.errors.length > 0) fail(outcome.errors, outcome.output); + + console.log( + [ + `Page: ${pageFile}`, + `Documented routes: ${routes.length}`, + `TypeScript fences compiled (strict): ${examples.length}`, + `Typed response fixtures: ${fixtures.length}`, + `Requests served: ${outcome.requests.length}`, + "", + "Example output:", + ...outcome.output.map((line) => ` ${line}`), + "", + "API example check passed.", + ].join("\n"), + ); +} + +/** Collects fenced code blocks, including fences without a language. */ +function parseFences(markdown: string): Fence[] { + const fences: Fence[] = []; + const lines = markdown.split("\n"); + let open: { lang: string; attrs: string; line: number; body: string[] } | null = null; + + lines.forEach((line, index) => { + if (open === null) { + const match = /^```([A-Za-z0-9_-]*)(.*)$/.exec(line); + if (match) { + open = { lang: match[1].toLowerCase(), attrs: match[2].trim(), line: index + 2, body: [] }; + } + return; + } + if (line.trim() === "```") { + fences.push({ + lang: open.lang, + attrs: open.attrs, + code: open.body.join("\n"), + line: open.line, + }); + open = null; + return; + } + open.body.push(line); + }); + + return fences; +} + +/** Reads a route fence such as `GET /agent/:id/status`. */ +function parseRoute(fence: Fence): Route | null { + if (fence.lang !== "") return null; + const match = routePattern.exec(fence.code.trim()); + if (!match) return null; + + const [, method, routePath] = match; + const source = routePath + .split("/") + .map((part) => (part.startsWith(":") ? "[^/]+" : part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))) + .join("/"); + return { method, path: routePath, regex: new RegExp(`^${source}$`) }; +} + +function parseFixtures(json: string): Fixture[] { + const parsed = JSON.parse(json) as { responses?: unknown }; + if (!Array.isArray(parsed.responses)) { + fail([`${fixturesFile}: expected a "responses" array.`]); + } + + return parsed.responses.map((entry, index) => { + const fixture = entry as Partial; + const where = `${fixturesFile} responses[${index}]`; + if (typeof fixture.request !== "string" || !/^[A-Z]+ \/\S*$/.test(fixture.request)) { + fail([`${where}: "request" must look like "GET /path".`]); + } + if (typeof fixture.status !== "number" || !Number.isInteger(fixture.status)) { + fail([`${where}: "status" must be an integer HTTP status.`]); + } + if (typeof fixture.type !== "string" || !fixtureTypePattern.test(fixture.type)) { + fail([`${where}: "type" must name a page type, e.g. "AgentInfo" or "Message[]".`]); + } + if (!("body" in fixture)) { + fail([`${where}: "body" is required.`]); + } + return fixture as Fixture; + }); +} + +/** + * Renders the page's TypeScript fences into one module, followed by a + * `satisfies` assertion per fixture, and records where each line came from. + */ +function renderModule(examples: Fence[], fixtures: Fixture[]): GeneratedModule { + const lines = [ + `// Generated by scripts/check-api-examples.ts from ${pageFile} and ${fixturesFile}.`, + ]; + const segments: Segment[] = []; + + for (const fence of examples) { + lines.push(`// ${pageFile}:${fence.line}`); + const start = lines.length + 1; + lines.push(...fence.code.split("\n")); + segments.push({ + start, + end: lines.length, + describe: (generatedLine, column) => + `${pageFile}:${fence.line + generatedLine - start}:${column}`, + }); + } + + lines.push("", "export {};", "", `// Typed network fixtures from ${fixturesFile}.`); + fixtures.forEach((fixture, index) => { + const start = lines.length + 1; + const body = JSON.stringify(fixture.body, null, 2).split("\n"); + body[0] = `const fixture${index} = ${body[0]}`; + body[body.length - 1] = `${body[body.length - 1]} satisfies ${fixture.type};`; + lines.push(...body); + segments.push({ + start, + end: lines.length, + describe: () => `${fixturesFile} responses[${index}] (${fixture.request} as ${fixture.type})`, + }); + }); + + return { source: `${lines.join("\n")}\n`, segments }; +} + +function createTsConfig() { + return { + compilerOptions: { + target: "ES2022", + module: "NodeNext", + moduleResolution: "NodeNext", + lib: ["ES2022"], + types: ["node"], + strict: true, + skipLibCheck: true, + esModuleInterop: true, + noEmit: true, + }, + files: [moduleFile], + }; +} + +async function runExamples(modulePath: string, routes: Route[], fixtures: Fixture[]) { + const errors: string[] = []; + const output: string[] = []; + const requests: string[] = []; + const used = new Set(); + + const fixtureFetch = async ( + input: string | URL | Request, + init: RequestInit = {}, + ): Promise => { + const url = new URL(input instanceof Request ? input.url : input.toString()); + const method = (init.method ?? "GET").toUpperCase(); + const request = `${method} ${url.pathname}`; + requests.push(request); + + if (url.origin !== apiOrigin) { + throw new Error(`${request}: sent to ${url.origin}, expected ${apiOrigin}.`); + } + if (url.search !== "") { + throw new Error(`${request}: unexpected query string ${url.search}.`); + } + if (!routes.some((route) => route.method === method && route.regex.test(url.pathname))) { + throw new Error(`${request}: not a route documented on ${pageFile}.`); + } + + const headers = new Headers(init.headers); + if (headers.get("authorization") !== `Bearer ${testApiKey}`) { + throw new Error(`${request}: missing the "Authorization: Bearer " header.`); + } + if (init.body !== undefined && init.body !== null) { + if (headers.get("content-type") !== "application/json") { + throw new Error(`${request}: JSON bodies need "Content-Type: application/json".`); + } + try { + JSON.parse(String(init.body)); + } catch { + throw new Error(`${request}: request body is not valid JSON.`); + } + } + + const index = fixtures.findIndex( + (fixture, candidate) => !used.has(candidate) && fixture.request === request, + ); + if (index === -1) { + throw new Error(`${request}: no unused fixture in ${fixturesFile}.`); + } + used.add(index); + + const fixture = fixtures[index]; + return new Response(JSON.stringify(fixture.body), { + status: fixture.status, + headers: { "content-type": "application/json" }, + }); + }; + + const originalFetch = globalThis.fetch; + const originalLog = console.log; + const originalApiKey = process.env.WRAITH_API_KEY; + + globalThis.fetch = fixtureFetch as typeof fetch; + console.log = (...args: unknown[]) => { + output.push(args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ")); + }; + process.env.WRAITH_API_KEY = testApiKey; + + try { + await import(pathToFileURL(modulePath).href); + } catch (error) { + const detail = error instanceof Error ? (error.stack ?? error.message) : String(error); + errors.push(`Running the examples failed:\n\n${detail}`); + } finally { + globalThis.fetch = originalFetch; + console.log = originalLog; + if (originalApiKey === undefined) { + delete process.env.WRAITH_API_KEY; + } else { + process.env.WRAITH_API_KEY = originalApiKey; + } + } + + if (errors.length === 0) { + fixtures.forEach((fixture, index) => { + if (!used.has(index)) { + errors.push( + `${fixturesFile} responses[${index}] (${fixture.request}) was never requested.`, + ); + } + }); + } + + return { errors, output, requests }; +} + +/** Rewrites `endpoints.ts(12,5)` and `endpoints.ts:12:5` to their MDX or fixture source. */ +function mapLocations(text: string, segments: Segment[]) { + const describe = (line: number, column: string) => { + const segment = segments.find((candidate) => line >= candidate.start && line <= candidate.end); + return segment ? segment.describe(line, column) : `${moduleFile}:${line}:${column}`; + }; + + return text + .replace(/(?:[^\s(]*[\\/])?endpoints\.ts\((\d+),(\d+)\)/g, (_, line, column) => + describe(Number(line), column), + ) + .replace(/(?:file:\/\/)?(?:[^\s(]*[\\/])?endpoints\.ts:(\d+):(\d+)/g, (_, line, column) => + describe(Number(line), column), + ); +} + +function run(command: string, args: string[], cwd: string) { + return new Promise<{ exitCode: number; output: string }>((resolve) => { + const child = spawn(command, args, { cwd, env: process.env, shell: false }); + let output = ""; + child.stdout.on("data", (chunk) => { + output += chunk.toString(); + }); + child.stderr.on("data", (chunk) => { + output += chunk.toString(); + }); + child.on("close", (exitCode) => { + resolve({ exitCode: exitCode ?? 1, output }); + }); + }); +} + +function fail(errors: string[], output: string[] = []): never { + const details = + output.length > 0 + ? `\n\nExample output before the failure:\n${output.map((line) => ` ${line}`).join("\n")}` + : ""; + console.error(`API example check failed:\n\n${errors.join("\n\n")}${details}`); + process.exit(1); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/fixtures/api-endpoint-responses.json b/scripts/fixtures/api-endpoint-responses.json new file mode 100644 index 0000000..cf25078 --- /dev/null +++ b/scripts/fixtures/api-endpoint-responses.json @@ -0,0 +1,131 @@ +{ + "$comment": "Network responses served to the api-reference/endpoints.mdx examples by scripts/check-api-examples.ts. Each entry is consumed once, in order, by the request it names. `type` is a response type declared on that page; the checker compiles every body with `satisfies `, so a fixture that drifts from the documented shape fails the check.", + "responses": [ + { + "request": "POST /agent/create", + "status": 201, + "type": "AgentInfo", + "body": { + "id": "6f1c2a9e-4b7d-4e2a-9c3f-1d8b5e7a2c40", + "name": "alice", + "chain": "stellar", + "address": "GCQFQ5LKASGUNZDGOTMHTOXSVXWRVATA7KZHX3XVZNE4STBS5SVJGPLI", + "metaAddress": "st:xlm:eb8452e938d04e9a56ef69c47dacd8224464b030b5ca569d5b4e4399f8d0fb5529a8dd877a3803289ab3a62ac39cce4a99021a3cd0fac6ad982e051c8fa769dc" + } + }, + { + "request": "POST /agent/6f1c2a9e-4b7d-4e2a-9c3f-1d8b5e7a2c40/chat", + "status": 201, + "type": "ChatResponse", + "body": { + "response": "Your balance is 10,000 XLM.", + "conversationId": "0d9e4c1b-7a52-4f0e-8b3d-2c6a9f1e5b77" + } + }, + { + "request": "POST /agent/6f1c2a9e-4b7d-4e2a-9c3f-1d8b5e7a2c40/chat", + "status": 201, + "type": "ChatResponse", + "body": { + "response": "No new stealth payments since your last scan.", + "toolCalls": [ + { + "name": "scan_payments", + "status": "success", + "detail": "[]" + } + ], + "conversationId": "0d9e4c1b-7a52-4f0e-8b3d-2c6a9f1e5b77" + } + }, + { + "request": "GET /agent/6f1c2a9e-4b7d-4e2a-9c3f-1d8b5e7a2c40/conversations", + "status": 200, + "type": "Conversation[]", + "body": [ + { + "id": "0d9e4c1b-7a52-4f0e-8b3d-2c6a9f1e5b77", + "title": "What is my balance?", + "createdAt": "2026-10-17T09:00:00.000Z", + "updatedAt": "2026-10-17T09:01:30.000Z" + }, + { + "id": "a3f7b2d8-5c1e-4d9a-b6f0-8e2c4a7d1b93", + "title": "Pay bob.wraith 25 XLM", + "createdAt": "2026-10-16T14:20:00.000Z", + "updatedAt": "2026-10-16T14:21:05.000Z" + } + ] + }, + { + "request": "GET /agent/6f1c2a9e-4b7d-4e2a-9c3f-1d8b5e7a2c40/conversations/0d9e4c1b-7a52-4f0e-8b3d-2c6a9f1e5b77/messages", + "status": 200, + "type": "Message[]", + "body": [ + { + "role": "user", + "text": "What is my balance?", + "createdAt": "2026-10-17T09:00:00.000Z" + }, + { + "role": "agent", + "text": "Your balance is 10,000 XLM.", + "createdAt": "2026-10-17T09:00:02.000Z" + }, + { + "role": "user", + "text": "Scan for incoming stealth payments", + "createdAt": "2026-10-17T09:01:20.000Z" + }, + { + "role": "tool", + "text": "scan_payments\n[]", + "createdAt": "2026-10-17T09:01:29.000Z" + }, + { + "role": "agent", + "text": "No new stealth payments since your last scan.", + "createdAt": "2026-10-17T09:01:30.000Z" + } + ] + }, + { + "request": "GET /agent/6f1c2a9e-4b7d-4e2a-9c3f-1d8b5e7a2c40/conversations/a3f7b2d8-5c1e-4d9a-b6f0-8e2c4a7d1b93/messages", + "status": 200, + "type": "Message[]", + "body": [ + { + "role": "user", + "text": "Pay bob.wraith 25 XLM", + "createdAt": "2026-10-16T14:20:00.000Z" + }, + { + "role": "agent", + "text": "Sent 25 XLM to bob.wraith through a fresh stealth address.", + "createdAt": "2026-10-16T14:21:05.000Z" + } + ] + }, + { + "request": "GET /agent/info/alice", + "status": 200, + "type": "AgentInfo", + "body": { + "id": "6f1c2a9e-4b7d-4e2a-9c3f-1d8b5e7a2c40", + "name": "alice", + "chain": "stellar", + "address": "GCQFQ5LKASGUNZDGOTMHTOXSVXWRVATA7KZHX3XVZNE4STBS5SVJGPLI", + "metaAddress": "st:xlm:eb8452e938d04e9a56ef69c47dacd8224464b030b5ca569d5b4e4399f8d0fb5529a8dd877a3803289ab3a62ac39cce4a99021a3cd0fac6ad982e051c8fa769dc" + } + }, + { + "request": "GET /agent/info/bob", + "status": 404, + "type": "ApiError", + "body": { + "message": "Agent not found", + "statusCode": 404 + } + } + ] +}