@civitai/app-sdk
v0.6.0
Published
OAuth + PKCE, encrypted-cookie sessions, scopes, and orchestrator helpers for building third-party Civitai apps.
Downloads
371
Readme
@civitai/app-sdk
Shared OAuth + orchestrator glue for building third-party Civitai apps. Used internally by every starter in civitai/civitai-app-starters, and publishable for direct use in your own app.
This package is runtime-agnostic — Node 20+ APIs only. There is no Next.js, SvelteKit, or Express coupling. Each starter writes a ~30-line framework adapter that calls into these primitives.
Install
pnpm add @civitai/app-sdkWhat it does
| Surface | Why it exists |
|---|---|
| oauth/* — generatePkce, buildAuthorizeUrl, exchangeCode, refreshToken, revokeToken, fetchMe | The Civitai OAuth flow (Authorization Code + PKCE S256), as a set of stateless functions you call from your server-side handlers. |
| scopes/* — TokenScope, TokenScopePresets, bitmaskFromScopes, scopesFromBitmask, hasScope, getScopeLabel | Civitai scopes are stored as bitmasks. These helpers let you compose scope sets from named flags rather than magic numbers. |
| cookies/* — sealCookie, unsealCookie, buildSetCookieHeader, readCookie | AES-256-CTR encrypted cookie crypto. Use to seal a session blob (refresh token, expiry, scope) into an httpOnly cookie with zero external session store. |
| orchestrator/* — createOrchestratorClient, estimateWorkflow, submitWorkflow, getWorkflow, pollWorkflow, buildTextToImageBody, buildImageGenBody, buildWorkflowBody, WORKFLOW_STEP_TYPES, IMAGE_GEN_ENGINES, isTerminal, extractImageUrls, OrchestratorError, WorkflowSnapshot, GenerateInput, ImageGenInput, WorkflowStepType, ImageGenEngine, DEFAULT_MODEL_AIR | Orchestrator workflow glue — types, body builders, raw HTTP, and long-poll helper. Client + server safe (fetch-only). estimateWorkflow calls ?whatif=true to preview Buzz cost without spending. pollWorkflow long-polls to terminal status. WORKFLOW_STEP_TYPES is the catalog of every step $type the orchestrator accepts. |
| blocks/* — defineBlock, BlockManifestError, BLOCK_SCOPES, BLOCK_SCOPE_PATTERN, isMessage, types (BlockManifestV1, BlockContext, BlockToken, BlockSettings, ViewerInfo, ThemeInfo, BlockWorkflowSnapshot, BlockInitPayload, ParentToBlockMessage, BlockToParentMessage, …) | Framework-agnostic contract for Civitai App Blocks. defineBlock(config) validates a BlockManifestV1 at startup so authoring mistakes surface in pnpm dev instead of at civitai deploy. Ships the JSON Schema (draft-07) at the ./schemas/app-block/v1.json subpath for offline validation. Runtime-agnostic — no React or DOM types. Hooks and the iframe transport live in a separate package. |
Subpath imports
import { defineBlock, BLOCK_SCOPES } from '@civitai/app-sdk/blocks';
// JSON Schema for the manifest, e.g. for IDE validation:
import manifestSchema from '@civitai/app-sdk/schemas/app-block/v1.json' with { type: 'json' };Minimal usage example
import {
buildAuthorizeUrl, generatePkce, generateState,
exchangeCode, fetchMe,
TokenScope, bitmaskFromScopes,
sealCookie, unsealCookie, buildSetCookieHeader,
createOrchestratorClient, buildTextToImageBody,
estimateWorkflow, submitWorkflow, pollWorkflow,
} from '@civitai/app-sdk';
// 1. Kick off OAuth login from your server-side handler
const { verifier, challenge } = generatePkce();
const state = generateState();
const scope = bitmaskFromScopes(['AIServicesWrite', 'BuzzRead', 'UserRead']);
// Persist { verifier, state, scope } against the user's session — e.g. in an
// encrypted cookie sealed with `sealCookie`. Then redirect to:
const authorizeUrl = buildAuthorizeUrl({
clientId: process.env.CIVITAI_CLIENT_ID!,
redirectUri: 'https://your-app.com/api/auth/callback/civitai',
scope,
state,
codeChallenge: challenge,
});
// 2. In your callback handler, exchange the code for tokens
const tokens = await exchangeCode({
clientId: process.env.CIVITAI_CLIENT_ID!,
clientSecret: process.env.CIVITAI_CLIENT_SECRET, // omit for public clients
redirectUri: 'https://your-app.com/api/auth/callback/civitai',
code: codeFromQuery,
codeVerifier: verifierFromSealedCookie,
});
// 3. Store tokens in an encrypted httpOnly cookie
const sealed = sealCookie(JSON.stringify(tokens), process.env.SESSION_SECRET!);
const setCookie = buildSetCookieHeader('civ_session', sealed, { maxAge: 3600 });
// 4. Use the token to make orchestrator calls
const client = createOrchestratorClient({ accessToken: tokens.access_token });
const me = await fetchMe({ accessToken: tokens.access_token });
console.log(`Hi ${me.username}, you have ${me.balance} Buzz`);
// 5. Estimate cost, then submit
const body = buildTextToImageBody({ prompt: 'a fox' }, { tags: ['my-app'] });
const estimate = await estimateWorkflow(client, body);
console.log(`This will cost ${estimate.cost?.total ?? 0} Buzz`);
// ...show to user, get confirmation...
const submitted = await submitWorkflow(client, body);
const finished = await pollWorkflow(client, submitted.id, { timeoutMs: 30_000 });The starters in civitai/civitai-app-starters wire this into framework-specific route handlers (Next.js App Router, SvelteKit +server.ts, Hono inside a Vite-built PWA). Read those for end-to-end reference implementations.
Choosing a workflow step type
The orchestrator is a workflow API: each request submits a list of typed steps. WORKFLOW_STEP_TYPES is the in-code catalog of every step $type it accepts, with a one-line description for each — textToImage, imageGen, videoGen, comfy, textToSpeech, aceStepAudio, transcription, imageUpscaler, and ~25 more.
Find the step you want, then pick a builder:
| Step type | Builder | When |
|---|---|---|
| textToImage | buildTextToImageBody | Diffusion checkpoints (SDXL / Flux.1 / Pony / SD1.5) via AIR URN |
| imageGen | buildImageGenBody | Closed-source image-gen APIs — Nano Banana, Gemini, GPT-Image, Flux.1 Kontext, Flux.2, Seedream, Grok, fal. IMAGE_GEN_ENGINES lists the engines. |
| Any other (videoGen, comfy, textToSpeech, transcription, …) | buildWorkflowBody | Generic single-step envelope — pass { $type, input }, the SDK adds name/timeout defaults. |
For multi-step workflows, hand-build { tags?, steps: [step1, step2, ...] } — no special envelope work beyond a JSON array.
Reference-image gen (the Nano Banana / Gemini / Kontext use case):
import { buildImageGenBody, estimateWorkflow, submitWorkflow } from '@civitai/app-sdk/orchestrator';
const body = buildImageGenBody({
engine: 'google',
model: 'nano-banana-2',
prompt: 'turn this person into a cartoon sticker',
images: ['data:image/png;base64,...', 'https://example.com/style-ref.jpg'],
aspectRatio: '1:1',
numImages: 1,
resolution: '1K',
}, { tags: ['my-app'] });
const estimate = await estimateWorkflow(client, body);
console.log(`This will cost ${estimate.cost?.total ?? 0} Buzz`);
const submitted = await submitWorkflow(client, body);Per-engine input shapes (aspectRatio, resolution, numImages, etc.) come from the OpenAPI spec at https://orchestration.civitai.com/openapi/v2-consumers.json — ImageGenInput is intentionally pass-through so new engine fields work without an SDK release.
Public vs. confidential clients
Civitai's OAuth server supports both:
- Confidential — your server holds
CIVITAI_CLIENT_SECRET. Use this for any starter that has a server side at all (Next.js, SvelteKit, or PWAs with a BFF). This is what every current starter uses. - Public — no
client_secret, PKCE alone is the security boundary. Civitai's token endpoint supports CORS for browser-direct exchange. Useful for fully static PWAs. We don't currently ship a static-PWA starter; planned for a later milestone.
Pass clientSecret to exchangeCode / refreshToken / revokeToken for confidential, omit for public.
Buzz mechanics (important)
When your app calls the orchestrator with a user's OAuth access token, the orchestrator debits the user's Buzz, not yours. This is the right tenant model for a third-party app, but it means:
- Request
AIServicesWritescope at consent time. Without it the user can't grant their Buzz for generation. - Show cost before spending. Call
estimateCostfirst — it returnscost.totalin Buzz without debiting. Display it. Let the user confirm. - Show balance. Request
BuzzReadscope, then call Civitai's balance endpoint. Don't surprise users. - Handle the cap-denial case. Per-app spending caps (set by the user at consent and at Account → Connected Apps) can cause a successful
whatifto be rejected at real submit time with a genericBAD_REQUEST. Treat that as "insufficient or denied" in your UI.
TODOs / future work
scopes/index.tsis hand-copied fromcivitai/civitai'ssrc/shared/constants/token-scope.constants.ts. Plan: replace with build-time codegen from/.well-known/openid-configuration.- Add a
tokenStore/abstraction so starters can plug in alternative storage (Redis, KV) without rewriting auth handlers. - Static-PWA helpers for the public-client flow (browser-side token exchange via CORS, in-memory token storage, no refresh persistence).
