Test and deploy / test-and-deploy (push) Successful in 56s
One system: post-roast observations (weights, colour, DTR, cup notes, 'one change next batch', disproof) now live on the uploaded roast (actual_roasts.after, migration 011), edited on the roast detail view with weights/DTR prefilled from the .alog itself. The planner's screen section is removed (the print worksheet keeps its hand-fill copy), and the lot 'last refine' suggestion reads the note from both the new home and legacy plans, newest wins. Every roast now downloads two ways: the untouched original .alog, and an updated supplemental copy with the app's data written back as valid Artisan fields (weight, beans, roastingnotes incl. the LLM review, cuppingnotes incl. linked cupping score/flavors) — serialized as a Python literal so Artisan's ast.literal_eval re-imports it. Co-Authored-By: Claude Fable 5 <[email protected]>
236 lines
21 KiB
JavaScript
236 lines
21 KiB
JavaScript
// Hand-authored OpenAPI 3.0 spec for every API surface (including admin), served at
|
|
// /api/openapi.json and rendered by the self-hosted Swagger UI at /api-docs. Kept in one
|
|
// place, built with small helpers so route coverage stays readable — update this file
|
|
// whenever a route is added or its shape changes.
|
|
|
|
const ok = (description, schema) => ({
|
|
description,
|
|
content: schema ? { "application/json": { schema } } : undefined,
|
|
});
|
|
const errorResponse = {
|
|
type: "object",
|
|
properties: { ok: { type: "boolean", example: false }, code: { type: "string" } },
|
|
};
|
|
const err = (description) => ok(description, errorResponse);
|
|
const jsonBody = (schema, required = true) => ({
|
|
required,
|
|
content: { "application/json": { schema } },
|
|
});
|
|
const idParam = {
|
|
name: "id",
|
|
in: "path",
|
|
required: true,
|
|
schema: { type: "string", format: "uuid" },
|
|
};
|
|
const obj = (properties, required) => ({ type: "object", properties, ...(required ? { required } : {}) });
|
|
const str = { type: "string" };
|
|
const num = { type: "number", nullable: true };
|
|
const bool = { type: "boolean" };
|
|
const arr = (items) => ({ type: "array", items });
|
|
|
|
const bean = obj({
|
|
id: str, name: str, roaster: str, origin: str, process: str, variety: str,
|
|
roastLevel: str, roastDate: { ...str, nullable: true }, initialWeightG: num,
|
|
remainingWeightG: num, url: str, tastingNotes: str, notes: str,
|
|
roastPlanId: { ...str, nullable: true }, archived: bool,
|
|
});
|
|
const beanBody = obj({
|
|
name: str, roaster: str, origin: str, process: str, variety: str, roastLevel: str,
|
|
roastDate: { ...str, description: "YYYY-MM-DD", nullable: true }, initialWeightG: num,
|
|
url: str, tastingNotes: str, notes: str, roastPlanId: { ...str, nullable: true },
|
|
archived: bool,
|
|
});
|
|
const brew = obj({
|
|
id: str, beanId: { ...str, nullable: true }, beanName: { ...str, nullable: true },
|
|
method: str, doseG: num, waterG: num, yieldG: num, grinder: str, grindSetting: str,
|
|
waterTempC: num, brewTimeS: { type: "integer", nullable: true },
|
|
bloomTimeS: { type: "integer", nullable: true }, rating: num, recipe: str,
|
|
tastingNotes: str, notes: str, brewedAt: str,
|
|
});
|
|
const brewBody = obj({
|
|
beanId: { ...str, nullable: true },
|
|
method: { ...str, description: "One of the brew-method keys from GET /api/brew-methods" },
|
|
doseG: num, waterG: num, yieldG: num, grinder: str, grindSetting: str, waterTempC: num,
|
|
brewTimeS: { type: "integer", nullable: true }, bloomTimeS: { type: "integer", nullable: true },
|
|
rating: { ...num, description: "0-10" },
|
|
recipe: { ...str, description: "Free-text steps (bloom, pours, timings)" },
|
|
tastingNotes: str, notes: str,
|
|
});
|
|
const lot = obj({
|
|
id: str, origin: str, variety: str, process: str, producer: str,
|
|
purchaseDate: { ...str, nullable: true }, initialWeightG: { type: "number" },
|
|
remainingWeightG: { type: "number" }, costTotal: num, moisturePct: num, densityGL: num,
|
|
notes: str, archived: bool,
|
|
});
|
|
const roast = obj({
|
|
id: str, roastPlanId: { ...str, nullable: true }, planTitle: { ...str, nullable: true },
|
|
filename: str, roast: { type: "object", nullable: true }, derived: { type: "object", nullable: true },
|
|
evaluationStatus: { ...str, enum: ["pending", "done", "failed"] },
|
|
evaluationError: { ...str, nullable: true }, evaluationGrade: { ...str, nullable: true },
|
|
evaluationSummary: { ...str, nullable: true },
|
|
});
|
|
|
|
/** @param {{origin?: string}} options */
|
|
export function buildOpenApiSpec({ origin = "" } = {}) {
|
|
const security = [{ cookieAuth: [] }, { bearerAuth: [] }];
|
|
const paths = {
|
|
// ── Auth ──
|
|
"/api/auth/signup-enabled": { get: { tags: ["auth"], summary: "Whether self-signup is enabled", security: [], responses: { 200: ok("Flag", obj({ ok: bool, enabled: bool })) } } },
|
|
"/api/auth/signup": { post: { tags: ["auth"], summary: "Create an account (when enabled)", security: [], requestBody: jsonBody(obj({ email: str, password: { ...str, minLength: 12 } }, ["email", "password"])), responses: { 201: ok("Signed up; session cookie set"), 400: err("Invalid credentials"), 403: err("Signups disabled"), 409: err("Email exists") } } },
|
|
"/api/auth/login": { post: { tags: ["auth"], summary: "Log in", security: [], requestBody: jsonBody(obj({ email: str, password: str }, ["email", "password"])), responses: { 200: ok("Logged in; session cookie set"), 401: err("Invalid credentials"), 429: err("Too many attempts") } } },
|
|
"/api/auth/logout": { post: { tags: ["auth"], summary: "Log out the current session", responses: { 200: ok("Logged out") } } },
|
|
"/api/auth/me": { get: { tags: ["auth"], summary: "Current user", responses: { 200: ok("User", obj({ ok: bool, user: obj({ id: str, email: str, role: str }) })), 401: err("Unauthorized") } } },
|
|
"/api/auth/forgot": { post: { tags: ["auth"], summary: "Request a password reset", security: [], requestBody: jsonBody(obj({ email: str }, ["email"])), responses: { 200: ok("Always ok") } } },
|
|
"/api/auth/reset": { post: { tags: ["auth"], summary: "Reset password with a token", security: [], requestBody: jsonBody(obj({ token: str, password: str }, ["token", "password"])), responses: { 200: ok("Password reset"), 400: err("Invalid token/password") } } },
|
|
|
|
// ── API tokens ──
|
|
"/api/tokens": {
|
|
get: { tags: ["tokens"], summary: "List your API tokens", responses: { 200: ok("Tokens", obj({ ok: bool, tokens: arr(obj({ id: str, name: str, createdAt: str, lastUsedAt: { ...str, nullable: true } })) })) } },
|
|
post: { tags: ["tokens"], summary: "Create an API token (raw value returned exactly once)", requestBody: jsonBody(obj({ name: str }), false), responses: { 201: ok("Token created", obj({ ok: bool, token: { ...str, description: "rpt_… bearer token — store it now, it is never shown again" }, id: str, name: str })), 409: err("Too many tokens") } },
|
|
},
|
|
"/api/tokens/{id}": { delete: { tags: ["tokens"], summary: "Revoke an API token", parameters: [{ ...idParam, schema: str }], responses: { 200: ok("Revoked"), 404: err("Not found") } } },
|
|
|
|
// ── Roast plans ──
|
|
"/api/plans": {
|
|
get: { tags: ["roast plans"], summary: "List your roast plans", responses: { 200: ok("Plans", obj({ ok: bool, plans: arr({ type: "object" }) })) } },
|
|
post: { tags: ["roast plans"], summary: "Create a roast plan", requestBody: jsonBody(obj({ plan: { type: "object" } }, ["plan"])), responses: { 201: ok("Created") } },
|
|
},
|
|
"/api/plans/{id}": {
|
|
put: { tags: ["roast plans"], summary: "Update a roast plan", parameters: [idParam], requestBody: jsonBody(obj({ plan: { type: "object" } }, ["plan"])), responses: { 200: ok("Updated"), 404: err("Not found") } },
|
|
delete: { tags: ["roast plans"], summary: "Delete a roast plan", parameters: [idParam], responses: { 200: ok("Deleted"), 404: err("Not found") } },
|
|
},
|
|
"/api/machine-profile": { get: { tags: ["roast plans"], summary: "Learned per-user machine profile", responses: { 200: ok("Profile", obj({ ok: bool, profile: { type: "object" } })) } } },
|
|
|
|
// ── Actual roasts ──
|
|
"/api/roasts": {
|
|
get: { tags: ["actual roasts"], summary: "List uploaded finished roasts", parameters: [{ name: "plan", in: "query", schema: { type: "string", format: "uuid" } }], responses: { 200: ok("Roasts", obj({ ok: bool, roasts: arr(roast) })) } },
|
|
post: { tags: ["actual roasts"], summary: "Upload a finished Artisan .alog (starts async LLM review)", requestBody: jsonBody(obj({ roastPlanId: { ...str, nullable: true }, filename: str, content: { ...str, description: "raw .alog file text" } }, ["content"])), responses: { 201: ok("Stored", obj({ ok: bool, roast })), 422: err("Unparseable .alog") } },
|
|
},
|
|
"/api/roasts/{id}": {
|
|
get: { tags: ["actual roasts"], summary: "Roast detail incl. curve, LLM review, linked plan", parameters: [idParam], responses: { 200: ok("Detail"), 404: err("Not found") } },
|
|
put: { tags: ["actual roasts"], summary: "Update the roast's machine and/or after-the-roast observations", parameters: [idParam], requestBody: jsonBody(obj({ roasterId: { ...str, nullable: true }, after: { type: "object", description: "greenIn/out/weightLossPct/vsTarget/actualDtrPct/colour/restedDays/brewRatio/method/cupNotes/oneChange/disproof (strings)" } })), responses: { 200: ok("Updated"), 404: err("Not found") } },
|
|
delete: { tags: ["actual roasts"], summary: "Delete an uploaded roast", parameters: [idParam], responses: { 200: ok("Deleted"), 404: err("Not found") } },
|
|
},
|
|
"/api/roasts/{id}/download": { get: { tags: ["actual roasts"], summary: "Download the .alog — original, or ?variant=updated for a supplemental copy with the app's data (weights, notes, cupping) written back as Artisan fields, importable into Artisan", parameters: [idParam, { name: "variant", in: "query", schema: { type: "string", enum: ["updated"] } }], responses: { 200: { description: "File as attachment" }, 404: err("Not found") } } },
|
|
"/api/roasts/{id}/evaluate": { post: { tags: ["actual roasts"], summary: "Queue a re-review", parameters: [idParam], responses: { 200: ok("Queued"), 404: err("Not found") } } },
|
|
|
|
// ── Green inventory ──
|
|
"/api/inventory": {
|
|
get: { tags: ["green inventory"], summary: "List green bean lots", responses: { 200: ok("Lots", obj({ ok: bool, lots: arr(lot) })) } },
|
|
post: { tags: ["green inventory"], summary: "Add a lot", requestBody: jsonBody(obj({ origin: str, initialWeightG: { type: "number" } }, ["origin", "initialWeightG"])), responses: { 201: ok("Created") } },
|
|
},
|
|
"/api/inventory/{id}": {
|
|
get: { tags: ["green inventory"], summary: "Lot detail + consumption log", parameters: [idParam], responses: { 200: ok("Lot"), 404: err("Not found") } },
|
|
put: { tags: ["green inventory"], summary: "Update a lot", parameters: [idParam], responses: { 200: ok("Updated"), 404: err("Not found") } },
|
|
delete: { tags: ["green inventory"], summary: "Delete a lot", parameters: [idParam], responses: { 200: ok("Deleted"), 404: err("Not found") } },
|
|
},
|
|
"/api/inventory/{id}/consume": { post: { tags: ["green inventory"], summary: "Draw weight from a lot (idempotent per plan)", parameters: [idParam], requestBody: jsonBody(obj({ weightG: { type: "number" }, roastPlanId: { ...str, nullable: true } }, ["weightG"])), responses: { 201: ok("Drawn"), 409: err("Already consumed for that plan") } } },
|
|
"/api/inventory/{id}/last-refine": { get: { tags: ["green inventory"], summary: "Most recent 'one change next batch' note for the lot", parameters: [idParam], responses: { 200: ok("Refine suggestion"), 404: err("Not found") } } },
|
|
|
|
// ── Cupping ──
|
|
"/api/cupping": {
|
|
get: { tags: ["cupping"], summary: "List cupping sessions", parameters: [{ name: "plan", in: "query", schema: { type: "string", format: "uuid" } }], responses: { 200: ok("Sessions") } },
|
|
post: { tags: ["cupping"], summary: "Create a session", requestBody: jsonBody(obj({ roastPlanId: { ...str, nullable: true }, cupCount: { type: "integer" } }), false), responses: { 201: ok("Created") } },
|
|
},
|
|
"/api/cupping/{id}": {
|
|
get: { tags: ["cupping"], summary: "Session detail", parameters: [idParam], responses: { 200: ok("Session"), 404: err("Not found") } },
|
|
put: { tags: ["cupping"], summary: "Update a session", parameters: [idParam], requestBody: jsonBody(obj({ data: { type: "object" } }, ["data"])), responses: { 200: ok("Updated"), 404: err("Not found") } },
|
|
delete: { tags: ["cupping"], summary: "Delete a session", parameters: [idParam], responses: { 200: ok("Deleted"), 404: err("Not found") } },
|
|
},
|
|
|
|
// ── Roasted beans (brewing) ──
|
|
"/api/beans": {
|
|
get: { tags: ["beans"], summary: "List roasted/purchased beans", responses: { 200: ok("Beans", obj({ ok: bool, beans: arr(bean) })) } },
|
|
post: { tags: ["beans"], summary: "Add a bean", requestBody: jsonBody(beanBody), responses: { 201: ok("Created", obj({ ok: bool, bean })), 400: err("Invalid") } },
|
|
},
|
|
"/api/beans/{id}": {
|
|
put: { tags: ["beans"], summary: "Update a bean", parameters: [idParam], requestBody: jsonBody(beanBody), responses: { 200: ok("Updated"), 404: err("Not found") } },
|
|
delete: { tags: ["beans"], summary: "Delete a bean", parameters: [idParam], responses: { 200: ok("Deleted"), 404: err("Not found") } },
|
|
},
|
|
"/api/gear": {
|
|
get: { tags: ["brews"], summary: "Owned equipment (brewers + grinders)", responses: { 200: ok("Gear", obj({ ok: bool, gear: obj({ brewers: arr(str), grinders: arr(str) }) })) } },
|
|
put: { tags: ["brews"], summary: "Set owned equipment", requestBody: jsonBody(obj({ brewers: arr({ ...str, description: "brew-method keys" }), grinders: arr(str) }, ["brewers", "grinders"])), responses: { 200: ok("Saved"), 400: err("Unknown brewer key") } },
|
|
},
|
|
"/api/brew-methods": { get: { tags: ["brews"], summary: "Brew-method taxonomy (categories + methods)", responses: { 200: ok("Methods", obj({ ok: bool, categories: arr(obj({ key: str, name: str })), methods: arr(obj({ key: str, name: str, category: str })) })) } } },
|
|
|
|
// ── Brews ──
|
|
"/api/brews": {
|
|
get: { tags: ["brews"], summary: "List brews (newest first)", parameters: [{ name: "bean", in: "query", schema: { type: "string", format: "uuid" } }], responses: { 200: ok("Brews", obj({ ok: bool, brews: arr(brew) })) } },
|
|
post: { tags: ["brews"], summary: "Log a brew", requestBody: jsonBody(brewBody), responses: { 201: ok("Created", obj({ ok: bool, brew })), 400: err("Invalid") } },
|
|
},
|
|
"/api/brews/{id}": {
|
|
put: { tags: ["brews"], summary: "Update a brew", parameters: [idParam], requestBody: jsonBody(brewBody), responses: { 200: ok("Updated"), 404: err("Not found") } },
|
|
delete: { tags: ["brews"], summary: "Delete a brew", parameters: [idParam], responses: { 200: ok("Deleted"), 404: err("Not found") } },
|
|
},
|
|
|
|
// ── LLM helpers ──
|
|
"/api/plan-chat": { post: { tags: ["llm"], summary: "Chat with the LLM about a roast plan (stateless; send the full visible conversation)", requestBody: jsonBody(obj({ plan: { type: "object" }, messages: arr(obj({ role: { ...str, enum: ["user", "assistant"] }, content: str }, ["role", "content"])) }, ["plan", "messages"])), responses: { 200: ok("Reply", obj({ ok: bool, reply: str })), 400: err("Bad plan/messages"), 503: err("No LLM model configured") } } },
|
|
"/api/roaster-profile": { get: { tags: ["llm"], summary: "Learned behavior for a machine (?roaster=id; default roaster otherwise), with user overrides applied", parameters: [{ name: "roaster", in: "query", schema: { type: "string", format: "uuid" } }], responses: { 200: ok("Profile", obj({ ok: bool, profile: { type: "object" } })) } } },
|
|
"/api/roasters": {
|
|
get: { tags: ["roasters"], summary: "List your roasting machines", responses: { 200: ok("Roasters") } },
|
|
post: { tags: ["roasters"], summary: "Add a machine (first one becomes default)", requestBody: jsonBody(obj({ name: str, model: str, notes: str, isDefault: bool }, ["name"])), responses: { 201: ok("Created") } },
|
|
},
|
|
"/api/roasters/{id}": {
|
|
put: { tags: ["roasters"], summary: "Update a machine (name/model/notes/default/override tweaks)", parameters: [idParam], requestBody: jsonBody(obj({ name: str, model: str, notes: str, isDefault: bool, overrides: { type: "object", description: "chargeTempC/turningPointS/turningPointTempC/yellowTempC/firstCrackTempC/dropTempC/paceFactor" } }), false), responses: { 200: ok("Updated"), 400: err("Bad override") } },
|
|
delete: { tags: ["roasters"], summary: "Delete a machine (its roasts detach)", parameters: [idParam], responses: { 200: ok("Deleted") } },
|
|
},
|
|
"/api/prefill": { post: { tags: ["llm"], summary: "Extract coffee facts from a product URL (used by roast planner and bean form)", requestBody: jsonBody(obj({ url: str }, ["url"])), responses: { 200: ok("Extraction + derived worksheet fields"), 422: err("Extraction failed"), 503: err("No LLM model configured") } } },
|
|
"/api/alog": { post: { tags: ["llm"], summary: "Parse an Artisan .alog for the reference-curve overlay (no storage)", requestBody: jsonBody(obj({ filename: str, content: str }, ["content"])), responses: { 200: ok("Parsed"), 422: err("Unparseable") } } },
|
|
|
|
// ── Account ──
|
|
"/api/account/email": { put: { tags: ["account"], summary: "Change email", requestBody: jsonBody(obj({ email: str, password: str }, ["email", "password"])), responses: { 200: ok("Changed") } } },
|
|
"/api/account/password": { put: { tags: ["account"], summary: "Change password", requestBody: jsonBody(obj({ currentPassword: str, newPassword: str }, ["currentPassword", "newPassword"])), responses: { 200: ok("Changed") } } },
|
|
"/api/account/sessions": { get: { tags: ["account"], summary: "List sessions", responses: { 200: ok("Sessions") } } },
|
|
"/api/account/sessions/{id}": { delete: { tags: ["account"], summary: "Revoke a session", parameters: [{ ...idParam, schema: str }], responses: { 200: ok("Revoked") } } },
|
|
"/api/account/sessions/revoke-others": { post: { tags: ["account"], summary: "Revoke all other sessions", responses: { 200: ok("Revoked") } } },
|
|
"/api/account/export": { get: { tags: ["account"], summary: "Download all of your own data as JSON", responses: { 200: ok("Personal data export") } } },
|
|
"/api/account/avatar": {
|
|
get: { tags: ["account"], summary: "Your profile picture (image response)", responses: { 200: { description: "Image" }, 404: err("No picture set") } },
|
|
put: { tags: ["account"], summary: "Set your profile picture (small data URL)", requestBody: jsonBody(obj({ dataUrl: { ...str, description: "data:image/png|jpeg|webp;base64,… (≤300KB)" } }, ["dataUrl"])), responses: { 200: ok("Saved"), 400: err("Bad image") } },
|
|
delete: { tags: ["account"], summary: "Remove your profile picture", responses: { 200: ok("Removed") } },
|
|
},
|
|
"/api/account": { delete: { tags: ["account"], summary: "Delete your account", requestBody: jsonBody(obj({ password: str }, ["password"])), responses: { 200: ok("Deleted") } } },
|
|
|
|
// ── Admin ──
|
|
"/api/admin/users": { get: { tags: ["admin"], summary: "List users", responses: { 200: ok("Users"), 403: err("Forbidden") } } },
|
|
"/api/admin/metrics": { get: { tags: ["admin"], summary: "Instance metrics", responses: { 200: ok("Metrics") } } },
|
|
"/api/admin/plans": { get: { tags: ["admin"], summary: "All plans (optional ?user=)", parameters: [{ name: "user", in: "query", schema: { type: "string", format: "uuid" } }], responses: { 200: ok("Plans") } } },
|
|
"/api/admin/audit": { get: { tags: ["admin"], summary: "Recent audit events", responses: { 200: ok("Events") } } },
|
|
"/api/admin/password-resets": { get: { tags: ["admin"], summary: "Pending reset links (no-SMTP deployments)", responses: { 200: ok("Links") } } },
|
|
"/api/admin/signup-enabled": { put: { tags: ["admin"], summary: "Toggle signups", requestBody: jsonBody(obj({ enabled: bool }, ["enabled"])), responses: { 200: ok("Toggled") } } },
|
|
"/api/admin/llm": {
|
|
get: { tags: ["admin"], summary: "Configured LLM models + current choice", responses: { 200: ok("Models", obj({ ok: bool, current: str, models: arr(obj({ key: str, name: str, provider: str })), modelsError: { ...str, nullable: true } })) } },
|
|
put: { tags: ["admin"], summary: "Choose the LLM model ('' = auto)", requestBody: jsonBody(obj({ model: str }, ["model"])), responses: { 200: ok("Saved"), 400: err("Unknown model"), 503: err("Model listing unavailable") } },
|
|
},
|
|
"/api/admin/users/{id}/role": { put: { tags: ["admin"], summary: "Change a user's role", parameters: [idParam], requestBody: jsonBody(obj({ role: { ...str, enum: ["user", "admin"] } }, ["role"])), responses: { 200: ok("Changed") } } },
|
|
"/api/admin/users/{id}/disabled": { put: { tags: ["admin"], summary: "Disable/enable a user", parameters: [idParam], requestBody: jsonBody(obj({ disabled: bool }, ["disabled"])), responses: { 200: ok("Changed") } } },
|
|
"/api/admin/users/{id}": { delete: { tags: ["admin"], summary: "Delete a user and their data", parameters: [idParam], responses: { 200: ok("Deleted") } } },
|
|
"/api/admin/backup": { get: { tags: ["admin"], summary: "Export the entire database as JSON", responses: { 200: ok("Backup file (attachment)") } } },
|
|
"/api/admin/backup/import": { post: { tags: ["admin"], summary: "REPLACE the entire database from a backup export", requestBody: jsonBody(obj({ format: { ...str, example: "roast-planner-backup" }, version: { type: "integer", example: 1 }, tables: { type: "object" } }, ["format", "version", "tables"])), responses: { 200: ok("Imported", obj({ ok: bool, counts: { type: "object" }, sessionKept: bool })), 400: err("Bad backup / no admin in backup") } } },
|
|
};
|
|
|
|
return {
|
|
openapi: "3.0.3",
|
|
info: {
|
|
title: "Roast Planner API",
|
|
version: "1.0.0",
|
|
description:
|
|
"Every capability of the app — roast planning, finished-roast uploads with LLM review, green inventory, cupping, roasted-bean management, brew logging, account, and admin — over JSON. Authenticate with the browser session cookie or an API token (`Authorization: Bearer rpt_…`, generated on the Account page). Bearer requests skip CSRF; cookie-based write requests must send the `x-csrf-token` header.",
|
|
},
|
|
servers: [{ url: origin || "/" }],
|
|
tags: [
|
|
{ name: "auth" }, { name: "tokens" }, { name: "roast plans" }, { name: "actual roasts" }, { name: "roasters" },
|
|
{ name: "green inventory" }, { name: "cupping" }, { name: "beans" }, { name: "brews" },
|
|
{ name: "llm" }, { name: "account" }, { name: "admin" },
|
|
],
|
|
components: {
|
|
securitySchemes: {
|
|
cookieAuth: { type: "apiKey", in: "cookie", name: "rp_session" },
|
|
bearerAuth: { type: "http", scheme: "bearer", description: "API token from the Account page (rpt_…)" },
|
|
},
|
|
},
|
|
security,
|
|
paths,
|
|
};
|
|
}
|