mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
169e918a8c
## What Completes the `/user/account` redesign behind the `accountSettingsV2` flag. The earlier commits on this branch built the two-pane shell and converted three panes; this finishes the other five, then polishes the result against the Pencil canvas (`designs/user-account.pen`). **OFF serves the legacy single-column page byte-identically.** See "Rollout" below. ## Panes | Pane | Change | |---|---| | Overview | Tier badge art in the Membership tile (links `/user/membership`); Standing replaces the creator-score figure; username renders its nameplate + badge cosmetics; identity card stacks on mobile | | Profile & Account | `ProfileCard` / `SocialProfileCard` flattened; Account standing shows the exact score; session refresh and delete are pointer rows, grouped | | Preferences | Regrouped to Media playback / Generation / File preferences / Features; image format moved to File preferences; assistant folded into Features | | Content & Browsing | Eye callout; mature-content rows; Topics as chips; hidden tags/users flattened | | Creator | Placement, remix and metric-visibility sections; sticker inventory pointer moved inside Stickers | | Membership & Billing | Subscription / payment methods / payouts flattened; gifts point at `/pricing/gift`; membership row stacks on mobile; empty states when the user has neither a membership nor a Creator Program payout config | | Security & Apps | Sign-in methods, API keys, OAuth apps, connected apps flattened; create buttons on the section heading | | Notifications | Delivery section; per-category icons; more room in an open category; `Other` sorted last | ## Decisions worth a reviewer's attention - **Cards take a `flat` prop rather than being forked.** The legacy page mounts the same components while the flag is alive; two copies of a settings form is how one of them silently loses a field. - **One rule per section.** Eight rows had nine dividers and read as a table. Rows are spaced instead. - **`/user/account/overview` is a new URL.** On mobile the index renders the section *menu*, so an overview reachable only at the index has no way in. `AccountLayout` takes `isIndex` from the route now; inferring it from `section.path` rendered the menu at both URLs. Covered by a test — deleting the alias 404s that URL. - **Standing thresholds moved to `accountStandingFromPoints`** (`strike.schema.ts`). Two surfaces show standing and it derives from active *points*, not the strike count. - **The sticker-inventory pointer survives its host section's bail paths.** It is not gated on placement, so nesting it inside that section would drop it whenever the placement controls cannot render (flag off, or a failed spaces read). - **`BrowsingCategories` switched to chips outright**, including the legacy card, rather than growing a variant prop — one rendering, no fork. - **First use of a Tailwind `has-[…]` variant in this repo** (`SettingRow`, to keep switch rows inline at every width). Tailwind is 3.4.17, so it is supported. - **Billing empty states are `flat`-only.** `SubscriptionCard` and `UserPaymentConfigurationCard` both returned `null` with nothing to show, which left the whole pane blank. They now offer the plans / the Creator Program instead — but only in the flat panes, so the legacy page keeps hiding them and stays byte-identical. Both reuse the metric-visibility upsell, extracted as `UpsellPanel`. ## Verification Typecheck clean; no new lint warnings. Covering suites green: `account-sections` (17), `strike.service` + `process-strikes` (75), the four Account browser suites (24), and the notification suites (79). `SettingsCard.earlyAdopter.browser` needed one assertion updated — it pinned the literal early-adopter copy. Kept its intent (the opt-in must explain itself) and split it into the promise and the caveat rather than loosening it. Walked every pane at 1440px and 390px as a subscribed Creator Program account, and the billing/notification changes on a free account with no Creator Program. ## Not in this PR - `designs/user-account.pen` has uncommitted local changes that predate this work; left alone deliberately. ## Rollout `accountSettingsV2` → Flipt key `account-settings-v2`. `availability: ['mod']` is the STATIC FALLBACK only — it decides nothing while Flipt answers, so it matters solely during a Flipt outage, where mods get the new shell and everyone else keeps the legacy page. The Flipt rollout is the on-switch: `account-settings-v2` is `enabled: false` with no rollouts today, so the page is off for everyone until a segment or threshold rollout is merged in `flipt-state`. Instant rollback = drop that rollout / set the threshold to 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013NnY26APwddt5dySmmubkZ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NnY26APwddt5dySmmubkZ
462 lines
21 KiB
Markdown
462 lines
21 KiB
Markdown
# Civitai OAuth — Developer Guide
|
||
|
||
## Overview
|
||
|
||
Civitai supports OAuth 2.0 for third-party applications to authenticate users and access the Civitai API on their behalf. This guide covers how to register an application, implement the authorization flow, and use access tokens.
|
||
|
||
## Quick Start
|
||
|
||
1. Go to **Account Settings → OAuth Applications** and register your app
|
||
2. Implement the **Authorization Code + PKCE** flow
|
||
3. Exchange the authorization code for an access token
|
||
4. Use the access token as a Bearer token in API requests
|
||
|
||
## Registering an Application
|
||
|
||
Visit your [Account Settings](/user/account) → **Security & Apps** and find the **OAuth Applications** section. Click **Register App** and fill in:
|
||
|
||
- **App Name** — displayed to users on the consent screen
|
||
- **Description** — what your app does
|
||
- **Redirect URIs** — where users are sent after authorization (must be exact match, HTTPS required in production)
|
||
- **Client Type**:
|
||
- **Confidential** — server-side apps that can keep a secret (you'll get a client secret)
|
||
- **Public** — SPAs, mobile apps, CLI tools (no client secret, PKCE provides security)
|
||
- **Allowed Scopes** — the maximum permissions your app can request
|
||
|
||
After registration, you'll receive a **Client ID** and (for confidential clients) a **Client Secret**. Save the secret — it won't be shown again.
|
||
|
||
## Authorization Code Flow (with PKCE)
|
||
|
||
PKCE (Proof Key for Code Exchange) is **required** for all authorization requests.
|
||
|
||
### Step 1: Generate PKCE Values
|
||
|
||
```javascript
|
||
// Generate a random code_verifier (43-128 chars)
|
||
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
||
|
||
// Hash it to create the code_challenge
|
||
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
||
```
|
||
|
||
### Step 2: Redirect User to Authorization
|
||
|
||
> **Note:** OAuth endpoints are served from the hub origin `https://auth.civitai.com`. The old `https://civitai.com/api/auth/oauth/...` URLs are legacy 308-redirect shims kept for back-compat — point new integrations at `auth.civitai.com`.
|
||
>
|
||
> **OIDC issuer:** if you integrated "Sign in with Civitai" (OpenID Connect) against the `https://civitai.com` issuer, update your configured issuer to `https://auth.civitai.com` — the discovery doc, `id_token.iss`, and JWKS are all served from the hub now. (Plain OAuth API-token integrations are unaffected — only OIDC clients that validate `iss` need this change.)
|
||
|
||
```
|
||
GET https://auth.civitai.com/api/auth/oauth/authorize
|
||
?client_id=YOUR_CLIENT_ID
|
||
&redirect_uri=https://yourapp.com/callback
|
||
&response_type=code
|
||
&scope=SCOPE_BITMASK
|
||
&state=RANDOM_STATE
|
||
&code_challenge=CODE_CHALLENGE
|
||
&code_challenge_method=S256
|
||
```
|
||
|
||
Parameters:
|
||
|
||
- `client_id` — your app's client ID
|
||
- `redirect_uri` — must exactly match a registered redirect URI
|
||
- `response_type` — always `code`
|
||
- `scope` — integer bitmask of requested permissions (see Scopes below)
|
||
- `state` — random string for CSRF protection (required)
|
||
- `code_challenge` — SHA-256 hash of your code_verifier, base64url-encoded
|
||
- `code_challenge_method` — always `S256`
|
||
|
||
The user will see a consent screen listing the requested permissions. If they approve, they'll be redirected to your `redirect_uri` with an authorization code.
|
||
|
||
### Step 3: Exchange Code for Token
|
||
|
||
```
|
||
POST https://auth.civitai.com/api/auth/oauth/token
|
||
Content-Type: application/x-www-form-urlencoded
|
||
|
||
grant_type=authorization_code
|
||
&code=AUTHORIZATION_CODE
|
||
&redirect_uri=https://yourapp.com/callback
|
||
&client_id=YOUR_CLIENT_ID
|
||
&client_secret=YOUR_CLIENT_SECRET (confidential clients only)
|
||
&code_verifier=ORIGINAL_CODE_VERIFIER
|
||
```
|
||
|
||
Response:
|
||
|
||
```json
|
||
{
|
||
"access_token": "civitai_abc123...",
|
||
"token_type": "Bearer",
|
||
"expires_in": 3600,
|
||
"refresh_token": "civitai_def456...",
|
||
"scope": "33554431"
|
||
}
|
||
```
|
||
|
||
### Step 4: Use the Access Token
|
||
|
||
```
|
||
GET https://auth.civitai.com/api/auth/oauth/userinfo
|
||
Authorization: Bearer civitai_abc123...
|
||
```
|
||
|
||
Response (standard OIDC UserInfo claims):
|
||
|
||
```json
|
||
{
|
||
"sub": "12345",
|
||
"id": 12345,
|
||
"username": "creator",
|
||
"preferred_username": "creator",
|
||
"name": "Creator",
|
||
"picture": "https://...",
|
||
"image": "https://...",
|
||
"email": "creator@example.com",
|
||
"email_verified": true
|
||
}
|
||
```
|
||
|
||
`email` and `email_verified` are released under the **UserRead** scope. UserRead is a mandatory baseline granted on **every** OAuth token — an app always needs to know whose account it's acting on — so the userinfo endpoint always works and `email` is present whenever the account has an email on file (unverified emails are still returned, with `email_verified: false`). Note that **existing** tokens issued before this change keep their original scope until they refresh — their access tokens (1h TTL) and the next refresh pick up the `UserRead` baseline automatically.
|
||
|
||
Or use it with any Civitai API/tRPC endpoint:
|
||
|
||
```
|
||
Authorization: Bearer civitai_abc123...
|
||
```
|
||
|
||
## Refreshing Tokens
|
||
|
||
Access tokens expire after 1 hour. Use the refresh token to get a new one:
|
||
|
||
```
|
||
POST https://auth.civitai.com/api/auth/oauth/token
|
||
Content-Type: application/x-www-form-urlencoded
|
||
|
||
grant_type=refresh_token
|
||
&refresh_token=civitai_def456...
|
||
&client_id=YOUR_CLIENT_ID
|
||
&client_secret=YOUR_CLIENT_SECRET (confidential clients only)
|
||
```
|
||
|
||
The old access and refresh tokens are revoked, and new ones are issued.
|
||
|
||
## Device Authorization Flow
|
||
|
||
For CLI tools and devices without a browser:
|
||
|
||
### Step 1: Request Device Code
|
||
|
||
```
|
||
POST https://auth.civitai.com/api/auth/oauth/device
|
||
Content-Type: application/x-www-form-urlencoded
|
||
|
||
client_id=YOUR_CLIENT_ID
|
||
&scope=SCOPE_BITMASK
|
||
```
|
||
|
||
Response:
|
||
|
||
```json
|
||
{
|
||
"device_code": "abc123...",
|
||
"user_code": "ABCD-EFGH",
|
||
"verification_uri": "https://auth.civitai.com/login/oauth/device",
|
||
"verification_uri_complete": "https://auth.civitai.com/login/oauth/device?code=ABCD-EFGH",
|
||
"expires_in": 900,
|
||
"interval": 5
|
||
}
|
||
```
|
||
|
||
### Step 2: Display Code to User
|
||
|
||
Show the user the `user_code` and `verification_uri`. They visit the URL in a browser and enter the code.
|
||
|
||
### Step 3: Poll for Token
|
||
|
||
Poll the token endpoint every `interval` seconds:
|
||
|
||
```
|
||
POST https://auth.civitai.com/api/auth/oauth/device-token
|
||
Content-Type: application/x-www-form-urlencoded
|
||
|
||
grant_type=urn:ietf:params:oauth:grant-type:device_code
|
||
&device_code=abc123...
|
||
&client_id=YOUR_CLIENT_ID
|
||
```
|
||
|
||
Responses:
|
||
|
||
- `authorization_pending` — user hasn't approved yet, keep polling
|
||
- `access_denied` — user denied the request
|
||
- `expired_token` — device code expired
|
||
- Success — returns access token + refresh token
|
||
|
||
> **Over-polling returns HTTP `429` with `{"error":"rate_limited"}`, not `slow_down`.** Poll no
|
||
> faster than `interval`; on a 429, add 5 seconds to your interval and keep polling. The limit is
|
||
> per client id, so every instance of an app shares one budget.
|
||
|
||
## Client Credentials Flow
|
||
|
||
For server-to-server communication (no user context):
|
||
|
||
```
|
||
POST https://auth.civitai.com/api/auth/oauth/token
|
||
Content-Type: application/x-www-form-urlencoded
|
||
|
||
grant_type=client_credentials
|
||
&client_id=YOUR_CLIENT_ID
|
||
&client_secret=YOUR_CLIENT_SECRET
|
||
&scope=SCOPE_BITMASK
|
||
```
|
||
|
||
The token acts on behalf of the client owner's account, scoped to the client's allowed permissions.
|
||
|
||
## Revoking Tokens
|
||
|
||
```
|
||
POST https://auth.civitai.com/api/auth/oauth/revoke
|
||
Content-Type: application/x-www-form-urlencoded
|
||
|
||
token=civitai_abc123...
|
||
&token_type_hint=access_token
|
||
&client_id=YOUR_CLIENT_ID
|
||
```
|
||
|
||
What a call may revoke depends on how it authenticates, and the three paths differ:
|
||
|
||
- **Public client** — send `client_id`. Revokes the token only if that client is the one it was
|
||
issued to.
|
||
- **Confidential client** — send `client_id` and `client_secret`. Authenticates as the client's
|
||
**owner account**: it revokes tokens owned by that account whichever client issued them, and does
|
||
not revoke a token it issued to anyone else.
|
||
- **Session cookie** — revokes tokens owned by the signed-in user.
|
||
|
||
Revoking a refresh token also drops that user's access tokens for the same client — this is the
|
||
sign-out call.
|
||
|
||
Always returns 200, even if the token was already revoked, so a 200 is not confirmation on its own.
|
||
|
||
## Token Introspection
|
||
|
||
RFC 7662. Lets a **first-party backend service** ask the hub whether an opaque `civitai_…` access
|
||
token is still live, and what it carries. Access tokens are random strings — nothing about them can
|
||
be checked offline — so a service that is handed one has no alternative.
|
||
|
||
**Not open to third-party apps.** The caller must be a **confidential** OAuth client *and* its
|
||
client id must be on the hub's `OAUTH_INTROSPECTION_CLIENT_IDS` allowlist. Everything else gets
|
||
`401 {"error":"invalid_client"}`. Today the only caller is `link-service`, which validates a Civitai Link client's token — the desktop
|
||
app or the ComfyUI node pack — before minting an instance key. Ask us if you have a service that
|
||
needs it.
|
||
|
||
```
|
||
POST https://auth.civitai.com/api/auth/oauth/introspect
|
||
Authorization: Basic base64(client_id:client_secret)
|
||
Content-Type: application/x-www-form-urlencoded
|
||
|
||
token=civitai_abc123...
|
||
```
|
||
|
||
Client credentials go in HTTP Basic (above) **or** as `client_id` / `client_secret` in the body.
|
||
The body may be form-encoded or JSON.
|
||
|
||
Active token:
|
||
|
||
```json
|
||
{
|
||
"active": true,
|
||
"sub": "123",
|
||
"username": "manuel",
|
||
"scope": "159383553",
|
||
"client_id": "civitai-link-desktop",
|
||
"exp": 1756800000,
|
||
"token_type": "Bearer"
|
||
}
|
||
```
|
||
|
||
- `scope` is the **decimal bitmask string** this provider uses everywhere, not the RFC's
|
||
space-separated list. Test a permission with a bitwise AND against the values in
|
||
[Scopes](#scopes).
|
||
- `exp` is Unix seconds, and is **omitted** for a token with no expiry.
|
||
- `client_id` is the OAuth client the token was issued to.
|
||
|
||
Anything else returns `200 {"active": false}` — unknown token, expired token, a refresh token, a
|
||
personal API key (a different key type; only OAuth **access** tokens introspect as active), a
|
||
missing `token` parameter, or a live token whose **owner's account is closed or suspended**. The
|
||
endpoint never distinguishes those cases.
|
||
|
||
So `active` covers the subject as well as the token: `active: true` means the token is live *and*
|
||
its owner is entitled to act, which is what lets a caller treat one introspection as a whole
|
||
go/no-go.
|
||
|
||
Responses are `Cache-Control: no-store`, and the endpoint adds none of the wildcard CORS headers the
|
||
token endpoints carry: this is a server-to-server call.
|
||
|
||
## Scopes
|
||
|
||
Scopes are represented as a bitmask integer. Combine scopes with bitwise OR.
|
||
|
||
> **UserRead is always granted.** Every issued token includes the `UserRead` bit regardless of what you request — an app always needs to identify the user it's acting on. You don't need to add it explicitly, and it can't be omitted.
|
||
|
||
| Scope | Value | Description |
|
||
| ------------------ | ------------ | ----------------------------------------------- |
|
||
| UserRead | 1 | Read profile, settings & email (always granted) |
|
||
| UserWrite | 2 | Update profile & settings |
|
||
| ModelsRead | 4 | Browse & download models |
|
||
| ModelsWrite | 8 | Upload & edit models |
|
||
| ModelsDelete | 16 | Delete models |
|
||
| MediaRead | 32 | View images, videos & posts |
|
||
| MediaWrite | 64 | Upload media & create posts |
|
||
| MediaDelete | 128 | Delete media & posts |
|
||
| ArticlesRead | 256 | Read articles |
|
||
| ArticlesWrite | 512 | Create & edit articles |
|
||
| ArticlesDelete | 1024 | Delete articles |
|
||
| BountiesRead | 2048 | View bounties |
|
||
| BountiesWrite | 4096 | Create & manage bounties |
|
||
| BountiesDelete | 8192 | Delete bounties |
|
||
| AIServicesRead | 16384 | View generation & training history |
|
||
| AIServicesWrite | 32768 | Generate, train & scan |
|
||
| BuzzRead | 65536 | View buzz balance & history |
|
||
| CollectionsRead | 131072 | View collections |
|
||
| CollectionsWrite | 262144 | Manage collections |
|
||
| SocialWrite | 524288 | Follow, react, comment & review |
|
||
| SocialTip | 1048576 | Tip other users |
|
||
| NotificationsRead | 2097152 | Read notifications |
|
||
| NotificationsWrite | 4194304 | Manage notification preferences |
|
||
| VaultRead | 8388608 | View vault |
|
||
| VaultWrite | 16777216 | Manage vault |
|
||
| **Full** | **33554431** | All permissions |
|
||
|
||
> **Opt-in scopes are not in `Full`.** `AppBlocksSubmit` (33554432), `AppBlocksDevTunnel`
|
||
> (67108864) and `LinkConnect` (134217728) are granted only to clients whose registration lists
|
||
> them, and are deliberately excluded from `Full` so an existing key is never silently widened.
|
||
|
||
### Common Scope Combinations
|
||
|
||
| Use Case | Scopes | Value |
|
||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
|
||
| Read-only browsing | UserRead, ModelsRead, MediaRead, ArticlesRead, BountiesRead, BuzzRead, CollectionsRead, AIServicesRead, NotificationsRead, VaultRead | Combine with OR |
|
||
| AI generation | AIServicesWrite, AIServicesRead, BuzzRead | 16384 \| 32768 \| 65536 = 114688 |
|
||
| Publishing | ModelsWrite, MediaWrite, ArticlesWrite + all reads | Combine with OR |
|
||
|
||
## Endpoints
|
||
|
||
| Endpoint | Method | Description |
|
||
| --------------------------------------- | ------ | -------------------------------------- |
|
||
| `/api/auth/oauth/authorize` | GET | Start authorization flow |
|
||
| `/api/auth/oauth/token` | POST | Exchange code for token, refresh token |
|
||
| `/api/auth/oauth/userinfo` | GET | Get authenticated user profile |
|
||
| `/api/auth/oauth/revoke` | POST | Revoke a token |
|
||
| `/api/auth/oauth/introspect` | POST | Introspect a token (allowlisted) |
|
||
| `/api/auth/oauth/device` | POST | Start device authorization |
|
||
| `/api/auth/oauth/device-token` | POST | Poll for device token |
|
||
| `/.well-known/openid-configuration` | GET | OpenID Connect discovery |
|
||
| `/.well-known/jwks.json` | GET | JSON Web Key Set (token-signing keys) |
|
||
|
||
> Endpoints are served from `https://auth.civitai.com` (e.g. `https://auth.civitai.com/api/auth/oauth/authorize`). Discovery: `https://auth.civitai.com/.well-known/openid-configuration`; JWKS: `https://auth.civitai.com/.well-known/jwks.json`. The matching `https://civitai.com/api/auth/oauth/...` paths still work for pre-cutover clients: `authorize` 308-redirects to the hub, and every other path is a transparent reverse proxy that forwards method, body and headers unchanged. Only `authorize` redirects, because a cross-origin redirect makes clients strip `Authorization` — which would break `client_secret_basic` and `Bearer` auth.
|
||
|
||
## Rate Limits
|
||
|
||
Buckets are keyed differently per endpoint — check which one applies before sizing your client:
|
||
|
||
- Token endpoint (`/token`): 20 requests/minute per **IP**
|
||
- Device authorization (`/device`): 30 requests/minute per **IP**
|
||
- Device token poll (`/device-token`): 20 requests/minute per **`device_code`** — one sign-in attempt —
|
||
plus 240/minute per **IP** charged before the code is validated. Poll at the `interval` the
|
||
authorization response gives you and you will not reach either
|
||
- Authorization endpoint: 10 requests/minute per **user**
|
||
- Revocation endpoint: 20 requests/minute per **IP**
|
||
- Introspection endpoint: 60 requests/minute per **client id**, plus 120/minute per **IP** charged
|
||
before client authentication
|
||
|
||
The hub sends no rate-limit headers and no `Retry-After`. A throttled request is `429` with `{"error":"rate_limited"}` — back off on the 429 itself.
|
||
|
||
## Token Lifetime
|
||
|
||
- Access tokens: 1 hour
|
||
- Refresh tokens: 30 days
|
||
- Authorization codes: 10 minutes
|
||
- Device codes: 15 minutes
|
||
|
||
## Security Best Practices
|
||
|
||
1. **Always use PKCE** — required for all authorization code requests
|
||
2. **Validate the `state` parameter** — prevent CSRF attacks
|
||
3. **Store tokens securely** — never expose in URLs or client-side storage for confidential apps
|
||
4. **Use short-lived access tokens** — refresh when they expire
|
||
5. **Request minimal scopes** — only ask for what your app needs
|
||
6. **Register exact redirect URIs** — no wildcards allowed
|
||
|
||
## What tokens _cannot_ do
|
||
|
||
Some Civitai actions are **only available to session-authenticated users**, regardless of token scope. Hitting these via a Bearer token returns `403 FORBIDDEN`:
|
||
|
||
- Tipping (`buzz.tipUser`)
|
||
- Bounty creation (`bounty.upsert`)
|
||
- Cosmetic shop purchases (`cosmeticShop.purchaseShopItem`, `purchasableReward.purchase`)
|
||
- Comic chapter purchases (`comics.purchaseChapterAccess`)
|
||
- Event donations (`event.donate`, `donationGoal.donate`)
|
||
- Auction bids (`auction.createBid`)
|
||
- Paid AI judge reviews (`challenge.requestReview`)
|
||
- Paid game starts (`games.chopped.start`)
|
||
- Model version early-access purchases (`modelVersion.earlyAccessPurchase`)
|
||
- Creator-program bank/extract/withdraw (`creator-program.*`)
|
||
- Direct user-to-club buzz transfers (`buzz.depositClubFunds`)
|
||
|
||
Buzz-spending operations that flow through the orchestrator (image generation, training, scanning, recommenders) **are** available to tokens — that's the entire point of the OAuth/API key surface. The orchestrator enforces buzz spend on its side using each token's per-subject budget.
|
||
|
||
## /api/v1/me — token introspection
|
||
|
||
Hitting `/api/v1/me` with a Bearer token returns the user's identity plus token-specific fields:
|
||
|
||
```jsonc
|
||
{
|
||
"id": 12345,
|
||
"username": "...",
|
||
"tier": "...",
|
||
"status": "active" | "muted" | "banned",
|
||
"isMember": false,
|
||
"subscriptions": [],
|
||
|
||
// UserRead is always granted, so these are present for any token
|
||
"email": "creator@example.com",
|
||
"emailVerified": true,
|
||
|
||
// Present only when authenticated via a non-Full token
|
||
"tokenScope": 4194303,
|
||
"subject": { "type": "apiKey" | "oauth", "id": <number | string> },
|
||
"buzzLimit": [{ "type": "sliding", "limit": 5000, "window": "day", "unit": 1 }] | null
|
||
}
|
||
```
|
||
|
||
`subject` is the `(type, id)` pair Civitai's orchestrator buckets buzz spend by:
|
||
|
||
- For User-type API keys, `id` is the numeric `ApiKey.id`.
|
||
- For OAuth-issued tokens, `id` is the `clientId` — stable across access-token refresh rotations, so spend tracking persists when the access token rotates.
|
||
|
||
`buzzLimit` is `null` (or omitted) when the user has not configured a limit on this token. Otherwise it's an array of budgets that cap how much buzz this token may spend in the orchestrator.
|
||
|
||
## Buzz spend limits — `BuzzBudget[]`
|
||
|
||
Each entry in `buzzLimit` is one of three discriminated variants:
|
||
|
||
```ts
|
||
type BuzzBudget =
|
||
| { type: 'absolute'; currencies?: string[]; limit: number }
|
||
| {
|
||
type: 'sliding';
|
||
currencies?: string[];
|
||
limit: number;
|
||
window: 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month';
|
||
unit: number;
|
||
}
|
||
| { type: 'rollover'; currencies?: string[]; limit: number; cron: string };
|
||
```
|
||
|
||
- **absolute** — hard cap, no time component.
|
||
- **sliding** — rolling window of `unit × window` (e.g. `window: 'day', unit: 7` = rolling 7-day window).
|
||
- **rollover** — calendar-based reset driven by a cron expression. Cron syntax matches Hangfire Cronos.
|
||
- Optional `currencies` restricts the cap to specific buzz pools (e.g. `["yellow"]`).
|
||
|
||
Civitai's UI today only exposes a single sliding budget (limit + day/week/month period), but the JSON shape supports the full set. Programmatic clients with a Full-scope key can set any combination via the tRPC `apiKey.setBuzzLimit` and `oauthConsent.setBuzzLimit` mutations. A token cannot modify the limit on its own subject — use a different management key or session auth.
|