Documentation
¶
Overview ¶
Package api is the (thin) HTTP client for the Civitai Apps surface.
Auth/submit contract (civitai/civitai PR #2644):
- The token-authenticated bundle-upload route is POST /api/v1/blocks/submit-version (Bearer access token). This is the default the CLI targets; CIVITAI_SUBMIT_PATH overrides it.
- The legacy /api/blocks/submit-version route is session-cookie + moderator authenticated and not used by the CLI.
Auth is supplied via a TokenSource so OAuth (device-flow) access tokens are refreshed transparently before a request and once on a 401; a personal API key is a static source with no refresh. Keeping the network call behind the Submitter interface makes the whole thing testable without a live server.
Index ¶
- Constants
- Variables
- type BuzzAccount
- type BuzzReader
- type Client
- func (c *Client) GetBuzzAccount(ctx context.Context) (*BuzzAccount, error)
- func (c *Client) GetForgejoCloneInfo(ctx context.Context, app string) (*ForgejoCloneInfo, error)
- func (c *Client) GetSubmission(ctx context.Context, id, blockID string) (*Submission, error)
- func (c *Client) ListSubmissions(ctx context.Context, blockID string) ([]Submission, error)
- func (c *Client) MintDevToken(ctx context.Context, slug string, scopes []string) (string, error)
- func (c *Client) SubmitVersion(ctx context.Context, zipBytes []byte, slug, version string) (*SubmitResult, error)
- func (c *Client) WhoAmI(ctx context.Context) (*Identity, error)
- func (c *Client) WithdrawRequest(ctx context.Context, publishRequestID string) error
- type DevTokenMinter
- type DeviceAuth
- type DeviceFlowError
- type ForgejoCloneInfo
- type Identity
- type OAuthClient
- type Scope
- type StaticToken
- type StatusReader
- type Subject
- type Submission
- type SubmitResult
- type Submitter
- type TokenResponse
- type TokenSource
- type Verifier
- type Withdrawer
Constants ¶
const ( ScopeUserRead = 1 << 0 ScopeUserWrite = 1 << 1 ScopeModelsRead = 1 << 2 ScopeModelsWrite = 1 << 3 ScopeModelsDelete = 1 << 4 ScopeMediaRead = 1 << 5 ScopeMediaWrite = 1 << 6 ScopeMediaDelete = 1 << 7 ScopeArticlesRead = 1 << 8 ScopeArticlesWrite = 1 << 9 ScopeArticlesDelete = 1 << 10 ScopeBountiesRead = 1 << 11 ScopeBountiesWrite = 1 << 12 ScopeBountiesDelete = 1 << 13 ScopeAIServicesRead = 1 << 14 ScopeAIServicesWrite = 1 << 15 // spend Buzz on AI services (generation) ScopeBuzzRead = 1 << 16 // read the user's Buzz balance ScopeCollectionsRead = 1 << 17 ScopeCollectionsWrite = 1 << 18 ScopeSocialWrite = 1 << 19 ScopeSocialTip = 1 << 20 ScopeNotificationsRead = 1 << 21 ScopeNotificationsWrite = 1 << 22 ScopeVaultRead = 1 << 23 ScopeVaultWrite = 1 << 24 ScopeAppBlocksSubmit = 1 << 25 // ScopeFull is the OR of bits 0..24 — every scope a personal key carries. It // EXCLUDES AppBlocksSubmit (1<<25), matching the upstream Full constant // (1<<25)-1. ScopeFull = (1 << 25) - 1 )
Token-scope bits, mirrored from @civitai/auth token-scope (civitai/civitai src/shared/constants/token-scope.constants.ts). These are STABLE/frozen bit positions in the tokenScope bitmask GET /api/v1/me returns.
const ( // ClientID is the public OAuth client id for the CLI. ClientID = "civitai-cli" // DeviceScope is UserRead|AppBlocksSubmit (bit flags), the fixed scope the CLI // requests on login. 33554433 == (1<<0)|(1<<25) in the server's scope bitset: // - UserRead (1<<0 = 1) — whoami / identity. // - AppBlocksSubmit (1<<25 = 33554432) — `app submit` AND the dev-token mint // gate (both require this on an OAuth token). // This is exactly the civitai-cli OauthClient.allowedScopes set (33554433). We // deliberately do NOT request AIServicesWrite: the server's device-flow // validateScope is all-or-nothing — requesting any scope the client doesn't // allow REJECTS the whole login — and we don't want every login token to carry // general Buzz-spend authority. // // What a login token can do: it can MINT an App-Blocks dev token (the mint gate // only needs AppBlocksSubmit) and drive the read/estimate harness paths — cost // preview / whatif, catalog browsing, and app storage. It CANNOT run a real // generation: the dev-token mint applies a uniform AIServicesWrite ceiling on // the budgeted-spend scope, so a login-minted dev token has ai:write:budgeted // STRIPPED (read/estimate only). Real-Buzz dev:live needs a personal API key // with full scope (civitai.com/user/account), which carries AIServicesWrite. DeviceScope = "33554433" )
OAuth device-authorization-grant client for the civitai-cli public client.
Contract: the OAuth provider lives on a dedicated auth origin (production: auth.civitai.com) discovered via OpenID well-known metadata. civitai.com itself does NOT serve the OAuth endpoints or the discovery document — it 404s to the SPA. The endpoints (resolved from the discovery doc) are:
- device init: POST {issuer}/api/auth/oauth/device (device_authorization_endpoint)
- device poll: POST {issuer}/api/auth/oauth/device-token (issuer + pathDeviceToken; not in the doc)
- token refresh: POST {issuer}/api/auth/oauth/token (token_endpoint)
ALL THREE are application/x-www-form-urlencoded AND must carry an Origin: {issuer} header — the auth host enforces a host-wide same-origin guard on form POSTs (origin-less/cross-site form POST -> 403; a JSON body -> 400 "Missing client_id"). See resolveEndpoints / postForm.
civitai-cli is a PUBLIC client (PKCE/device): no client secret.
const BuzzAccountPath = "/api/trpc/buzz.getBuzzAccount"
BuzzAccountPath is the tRPC route that returns the spendable Buzz balance.
const CloneInfoPath = "/api/trpc/blocks.getMyForgejoCloneInfo"
CloneInfoPath is the tRPC query that returns the caller's per-user Forgejo clone info for one of THEIR apps (owner-only, App-Blocks-flag-gated). Backs `civitai app pull`. The token is embedded in CloneURL (HTTP-Basic) — caller must treat it as a secret (see the leakage caveat in `civitai app pull`).
const DefaultSubmitPath = "/api/v1/blocks/submit-version"
DefaultSubmitPath is the token-authenticated submit-version route.
const DevTokenPath = "/api/v1/blocks/dev-token"
DevTokenPath is the invite-gated route that mints a short-lived dev block token for `npm run dev:live` (POST {"slug": ..., "scopes"?: [...]}; civitai/civitai src/pages/api/v1/blocks/dev-token.ts). 200 { token, ... } on success; a PENDING (un-approved) slug is accepted, and a slug with NO app row yet mints from the request-body `scopes` (the dev's LOCAL manifest scopes, clamped server-side) — so `create → dev-token → dev:live` works with no submit step. Error bodies are {message}: 404 slug registered to a different account / genuinely not found, 403 not-invited/insufficient-scope, 429 rate-limited, 503 flag-off. The minted token's CAPABILITIES depend on the bearer: a full-scope personal API key mints a spend-capable token; an OAuth (`civitai login`) credential mints a read-only one.
const SubmissionsPath = "/api/v1/blocks/submissions"
SubmissionsPath is the token-authenticated, self-scoped submission-status route (GET; civitai/civitai src/pages/api/v1/blocks/submissions.ts).
const WithdrawPath = "/api/v1/blocks/withdraw"
WithdrawPath is the token-authenticated, self-scoped withdraw route (POST {"publishRequestId": ...}; civitai/civitai src/pages/api/v1/blocks/withdraw.ts). 200 on success (incl. an idempotent already-withdrawn), 404 not-found-or-not-yours, 409 not in a withdrawable (pending) state.
Variables ¶
var ErrBuzzScope = fmt.Errorf("credential lacks the Buzz-read scope")
ErrBuzzScope is returned by GetBuzzAccount when the stored credential lacks the Buzz-read scope (the server answers 403 FORBIDDEN). The command layer maps this to actionable, personal-key guidance.
var ErrNoRefresh = fmt.Errorf("token source cannot refresh")
ErrNoRefresh is returned by a non-refreshable (personal-key) TokenSource.
var ErrSlugRegisteredToOtherAccount = errors.New("slug is registered to a different account")
ErrSlugRegisteredToOtherAccount is wrapped by MintDevToken's error when the dev-token route 404s with the bare "App not found" — the server's anti-shadow guard: the requested slug is an APPROVED app owned by a DIFFERENT account, so the no-row local-manifest mint path is refused. It is the ONLY rename-retriable 404 (the caller can pick a new, free slug and retry). Other 404s (e.g. an owned-but-not-yet-deployed app, which carries a "no live deployment" message) are NOT retriable and do NOT wrap this sentinel. Callers branch with errors.Is(err, ErrSlugRegisteredToOtherAccount) rather than matching strings.
Functions ¶
This section is empty.
Types ¶
type BuzzAccount ¶ added in v0.1.9
type BuzzAccount struct {
Blue int64 `json:"blue"`
Green int64 `json:"green"`
Yellow int64 `json:"yellow"`
}
BuzzAccount is the spendable Buzz balance from buzz.getBuzzAccount.
func (*BuzzAccount) Total ¶ added in v0.1.38
func (a *BuzzAccount) Total() int64
Total is the sum of the blue, green, and yellow balances.
type BuzzReader ¶ added in v0.1.9
type BuzzReader interface {
// GetBuzzAccount returns the caller's Buzz balance. A credential lacking the
// Buzz-read scope yields ErrBuzzScope (the server answers 403).
GetBuzzAccount(ctx context.Context) (*BuzzAccount, error)
}
BuzzReader reads the caller's spendable Buzz balance.
type Client ¶
type Client struct {
BaseURL string
Tokens TokenSource
SubmitPath string // route for submit-version; CIVITAI_SUBMIT_PATH overrides
HTTP *http.Client
// SubmitTimeout overrides the submit-upload timeout when non-zero; it
// defaults to submitTimeout. Used by tests to exercise the timeout-recovery
// path without a real slow upload.
SubmitTimeout time.Duration
// SubmitPollDelay overrides the inter-attempt delay of the post-timeout
// recovery poll when set (>= 0 with the zero value meaning "use the
// default"); tests set it to 0 to avoid sleeping.
SubmitPollDelay *time.Duration
}
Client is the default HTTP implementation.
func New ¶
New builds a Client with sane defaults from a static token (personal API key or a one-shot access token). For refreshable OAuth credentials use NewWithSource.
func NewWithSource ¶
func NewWithSource(baseURL string, src TokenSource, submitPath string) *Client
NewWithSource builds a Client backed by a TokenSource (which may refresh).
func (*Client) GetBuzzAccount ¶ added in v0.1.9
func (c *Client) GetBuzzAccount(ctx context.Context) (*BuzzAccount, error)
GetBuzzAccount reads the caller's spendable Buzz balance via the buzz.getBuzzAccount tRPC route, refreshing the OAuth access token if needed. On 200 it returns the {blue,green,yellow} balance; on a 403 (the credential lacks the Buzz-read scope) it returns ErrBuzzScope so the command layer can print the personal-key guidance. The tRPC success envelope is {"result":{"data":{"json":{...}}}}.
func (*Client) GetForgejoCloneInfo ¶ added in v0.1.38
GetForgejoCloneInfo calls the owner-only getMyForgejoCloneInfo tRPC query for the given app (a slug — the repo name — or an appBlockId). It lazily provisions the caller's scoped Forgejo identity server-side and returns the tokened clone URL the `pull` command hands to git.
func (*Client) GetSubmission ¶ added in v0.1.2
GetSubmission returns a single submission. Exactly one of id (a pubreq id) or blockID (an app slug) should be set; id takes precedence if both are given.
func (*Client) ListSubmissions ¶ added in v0.1.2
ListSubmissions returns the caller's own submissions (newest first). An empty blockID lists all; a non-empty blockID narrows to that app's submissions.
func (*Client) MintDevToken ¶ added in v0.1.15
MintDevToken mints a short-lived dev block token for the given app slug, returning the JWT from the response's .token field. scopes carries the caller's local manifest scopes for the server's no-row (no app registered yet) mint path; they are clamped server-side and omitted from the body when empty/nil (registered-app and read-only paths are unaffected). The OAuth access token is refreshed transparently on a 401. A non-2xx is mapped by devTokenError.
func (*Client) SubmitVersion ¶
func (c *Client) SubmitVersion(ctx context.Context, zipBytes []byte, slug, version string) (*SubmitResult, error)
SubmitVersion uploads the bundle to the token-authenticated submit route, refreshing the OAuth access token transparently if needed.
The upload can complete server-side while its HTTP response is slow or never arrives within the timeout — observed in the wild as a false "context deadline exceeded" failure on a submit that had actually landed, leaving the user to retry into "you already have a pending submission". So when (and only when) the POST fails with a timeout / deadline-exceeded / no-response error (as opposed to a clean HTTP error status), this polls GET /api/v1/blocks/submissions for a submission matching slug+version and, if one is now present, reports it as a success — surfacing the pubreq id. If no matching submission is found, it returns a clear error telling the user to check `civitai app status` before resubmitting.
func (*Client) WhoAmI ¶
WhoAmI verifies the token against /api/v1/me, refreshing the OAuth access token transparently if needed.
func (*Client) WithdrawRequest ¶ added in v0.1.8
WithdrawRequest withdraws the caller's own pending publish request. A 200 is success (the server is idempotent: already-withdrawn also returns 200). A non-2xx is mapped to an actionable error by withdrawError.
type DevTokenMinter ¶ added in v0.1.15
type DevTokenMinter interface {
// MintDevToken mints a dev block token for the given app slug and returns
// the JWT. scopes carries the caller's LOCAL block.manifest.json scopes for
// the server's no-row mint path (clamped server-side); pass nil/empty when
// no manifest is available. A non-2xx is mapped by devTokenError.
MintDevToken(ctx context.Context, slug string, scopes []string) (string, error)
}
DevTokenMinter mints a short-lived dev block token for `npm run dev:live`.
type DeviceAuth ¶
type DeviceAuth struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
DeviceAuth is the device-init response.
type DeviceFlowError ¶
DeviceFlowError is a terminal OAuth error (expired_token, access_denied, …) surfaced to the caller. authorization_pending / slow_down are handled inline by PollToken and never returned as this.
func (*DeviceFlowError) Error ¶
func (e *DeviceFlowError) Error() string
type ForgejoCloneInfo ¶ added in v0.1.38
type ForgejoCloneInfo struct {
NotYetAvailable bool `json:"notYetAvailable"`
Slug string `json:"slug"`
Message string `json:"message"`
ForgejoUsername string `json:"forgejoUsername"`
Token string `json:"token"`
HTTPURL string `json:"httpUrl"`
CloneURL string `json:"cloneUrl"`
}
ForgejoCloneInfo mirrors the getMyForgejoCloneInfo result. When the app's first version has not yet been ZIP-approved the server returns NotYetAvailable=true (no credential is minted) with a Message explaining why.
type Identity ¶
type Identity struct {
Username string `json:"username"`
ID int `json:"id"`
// TokenScope is the bearer token's scope bitmask. Decode it with the Scope*
// bits below to learn what the credential can do (spend Buzz, read balance,
// …). A personal full-scope key has every bit; an OAuth device-login token
// typically has neither AIServicesWrite nor BuzzRead. nil ⇒ unknown (absent
// from the response, e.g. cookie auth).
TokenScope *int `json:"tokenScope,omitempty"`
// BuzzLimit is the credential's per-window spend cap, when the server reports
// one. nil ⇒ absent/unknown.
BuzzLimit *int64 `json:"buzzLimit,omitempty"`
// Subject identifies the credential (OAuth login vs personal API key). nil ⇒
// cookie/session auth (not applicable to the CLI).
Subject *Subject `json:"subject,omitempty"`
}
Identity is the authenticated-user view `whoami` reports. TokenScope, BuzzLimit, and Subject are pointers because GET /api/v1/me omits them for some auth kinds (e.g. cookie/session), and a nil TokenScope must degrade to "scopes unknown" rather than decode as "no capabilities".
func (*Identity) CanReadBuzz ¶ added in v0.1.9
CanReadBuzz reports whether the identity's token can read the Buzz balance. An unknown scope is treated as false.
func (*Identity) CanSpendBuzz ¶ added in v0.1.9
CanSpendBuzz reports whether the identity's token carries the AI-Services (Buzz-spend) scope. An unknown scope is treated as false.
func (*Identity) CredentialType ¶ added in v0.1.38
CredentialType is a human label for the credential behind the token: "OAuth login", "personal API key", or "unknown" when the subject is absent.
func (*Identity) DecodeScopes ¶ added in v0.1.38
DecodeScopes returns the names of every set scope bit (low → high). A nil (unknown) mask returns nil.
func (*Identity) IsOAuth ¶ added in v0.1.38
IsOAuth reports whether the credential is an OAuth device-login token (subject.type == "oauth"). A nil/absent subject is not OAuth.
func (*Identity) ScopeKnown ¶ added in v0.1.38
ScopeKnown reports whether the identity carries a decodable scope bitmask. When false, capability queries are unknowable and the caller should say so rather than reporting "no".
type OAuthClient ¶
type OAuthClient struct {
BaseURL string
HTTP *http.Client
// contains filtered or unexported fields
}
OAuthClient talks the device-flow + refresh endpoints.
func NewOAuthClient ¶
func NewOAuthClient(baseURL string) *OAuthClient
NewOAuthClient builds an OAuthClient with sane defaults.
func (*OAuthClient) PollToken ¶
func (c *OAuthClient) PollToken(ctx context.Context, auth *DeviceAuth, sleep func(time.Duration)) (*TokenResponse, error)
PollToken polls device-token until approval, a terminal error, or the device-flow deadline (auth.ExpiresIn). It blocks for `interval` seconds between polls and increases the interval by 5s on slow_down. sleep is injectable for tests; pass nil for the real time.Sleep.
func (*OAuthClient) Refresh ¶
func (c *OAuthClient) Refresh(ctx context.Context, refreshToken string) (*TokenResponse, error)
Refresh exchanges a refresh token for a new access token. The server may rotate the refresh token; callers must persist tr.RefreshToken if non-empty.
func (*OAuthClient) StartDevice ¶
func (c *OAuthClient) StartDevice(ctx context.Context) (*DeviceAuth, error)
StartDevice initiates the device-authorization grant.
The device endpoint (auth.civitai.com) requires application/x-www-form-urlencoded AND a same-origin Origin header — a JSON body 400s ("Missing client_id") and a form POST without a matching Origin 403s ("Cross-site POST form submissions are forbidden"). The endpoint URL + Origin come from OpenID discovery.
type Scope ¶
type Scope string
Scope is a token scope that tolerates BOTH JSON shapes the server emits: the device-token (login) route returns a plain string (`scope.toString()`), while the token/refresh route returns the @node-oauth/oauth2-server shape where scope is an ARRAY of strings (e.g. ["33554433"]). Declaring scope as a plain `string` made json.Unmarshal of the refresh response fail the whole struct, killing Refresh() after the 1h access-token TTL. UnmarshalJSON normalizes either shape to a single space-joined string (OAuth convention). The CLI only stores/displays scope, it never enforces it, so this is safe.
func (*Scope) UnmarshalJSON ¶
UnmarshalJSON accepts a JSON string OR a JSON array of strings.
type StaticToken ¶
type StaticToken string
StaticToken is a TokenSource for a fixed token (personal API key). It never refreshes.
type StatusReader ¶ added in v0.1.2
type StatusReader interface {
// ListSubmissions returns the caller's submissions, newest first. An empty
// blockId lists all of them.
ListSubmissions(ctx context.Context, blockID string) ([]Submission, error)
// GetSubmission returns a single submission. Exactly one of id (a
// pubreq_<ULID>) or blockID (an app slug) must be set.
GetSubmission(ctx context.Context, id, blockID string) (*Submission, error)
}
StatusReader reads the caller's own App-Block submission review/deploy state.
type Subject ¶ added in v0.1.38
Subject identifies the credential behind a token as returned by GET /api/v1/me. Type == "oauth" means an OAuth device-login token (from `civitai login`); any other type (e.g. "apiKey"/"user") is a personal API key. Absent when auth is cookie/session (not applicable to the CLI).
type Submission ¶ added in v0.1.2
type Submission struct {
ID string `json:"id"`
BlockID string `json:"blockId"` // the app slug; builds <blockId>.civit.ai
AppBlockID *string `json:"appBlockId"`
Version string `json:"version"`
Status string `json:"status"` // pending | approved | rejected | withdrawn
RejectionReason *string `json:"rejectionReason"`
ApprovalNotes *string `json:"approvalNotes"`
DeployState *string `json:"deployState"` // null | building | deploying | live | failed
DeployDetail *string `json:"deployDetail"`
DeployUpdatedAt *string `json:"deployUpdatedAt"`
SubmittedAt string `json:"submittedAt"`
ReviewedAt *string `json:"reviewedAt"`
UpdatedAt string `json:"updatedAt"`
CreatedAt string `json:"createdAt"`
LiveURL *string `json:"liveUrl"` // set once serving (approved+live)
}
Submission mirrors the shaped row from GET /api/v1/blocks/submissions (civitai/civitai src/pages/api/v1/blocks/submissions.ts -> shapeRow). Field names + JSON casing track the server EXACTLY.
type SubmitResult ¶
type SubmitResult struct {
PublishRequestID string `json:"publishRequestId"`
Slug string `json:"slug"`
Version string `json:"version"`
Status string `json:"status"`
}
SubmitResult is the publish-request result the server returns.
type Submitter ¶
type Submitter interface {
SubmitVersion(ctx context.Context, zipBytes []byte, slug, version string) (*SubmitResult, error)
}
Submitter submits a packaged bundle and returns the server's response. The slug + version identify the submission so that, if the upload's response is lost to a timeout, the submit path can poll for a landed submission and recover rather than reporting a false failure (see SubmitVersion).
type TokenResponse ¶
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
Scope Scope `json:"scope"`
}
TokenResponse is the successful device-token / refresh response. RefreshToken may be empty on a refresh that doesn't rotate.
type TokenSource ¶
type TokenSource interface {
// Token returns the current bearer token, refreshing first if it is known
// to be expired. An empty token means "unauthenticated".
Token(ctx context.Context) (string, error)
// Refresh forces a refresh (used on a 401) and returns the new token.
// Sources that cannot refresh return ("", ErrNoRefresh).
Refresh(ctx context.Context) (string, error)
}
TokenSource yields the Bearer token to send. Refresh, if supported, refreshes the token (e.g. after a 401) and returns the new one; a static personal-key source returns ErrNoRefresh.
type Withdrawer ¶ added in v0.1.8
type Withdrawer interface {
// WithdrawRequest withdraws the publish request with the given id. It is
// idempotent: a 200 (incl. already-withdrawn) is success; a 409 means the
// request is not in a withdrawable (pending) state.
WithdrawRequest(ctx context.Context, publishRequestID string) error
}
Withdrawer withdraws the caller's own pending App-Block publish request so a new bundle can be submitted for the same slug.