api

package
v0.1.39 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 2, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

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

View Source
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.

View Source
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.

View Source
const BuzzAccountPath = "/api/trpc/buzz.getBuzzAccount"

BuzzAccountPath is the tRPC route that returns the spendable Buzz balance.

View Source
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`).

View Source
const DefaultSubmitPath = "/api/v1/blocks/submit-version"

DefaultSubmitPath is the token-authenticated submit-version route.

View Source
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.

View Source
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).

View Source
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

View Source
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.

View Source
var ErrNoRefresh = fmt.Errorf("token source cannot refresh")

ErrNoRefresh is returned by a non-refreshable (personal-key) TokenSource.

View Source
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

func New(baseURL, token, submitPath string) *Client

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

func (c *Client) GetForgejoCloneInfo(ctx context.Context, app string) (*ForgejoCloneInfo, error)

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

func (c *Client) GetSubmission(ctx context.Context, id, blockID string) (*Submission, error)

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

func (c *Client) ListSubmissions(ctx context.Context, blockID string) ([]Submission, error)

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

func (c *Client) MintDevToken(ctx context.Context, slug string, scopes []string) (string, error)

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

func (c *Client) WhoAmI(ctx context.Context) (*Identity, error)

WhoAmI verifies the token against /api/v1/me, refreshing the OAuth access token transparently if needed.

func (*Client) WithdrawRequest added in v0.1.8

func (c *Client) WithdrawRequest(ctx context.Context, publishRequestID string) error

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

type DeviceFlowError struct {
	Code        string
	Description string
}

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

func (id *Identity) CanReadBuzz() bool

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

func (id *Identity) CanSpendBuzz() bool

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

func (id *Identity) CredentialType() string

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

func (id *Identity) DecodeScopes() []string

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

func (id *Identity) IsOAuth() bool

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

func (id *Identity) ScopeKnown() bool

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) String

func (s Scope) String() string

String returns the scope as a plain string for storage/display.

func (*Scope) UnmarshalJSON

func (s *Scope) UnmarshalJSON(b []byte) error

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.

func (StaticToken) Refresh

func (s StaticToken) Refresh(context.Context) (string, error)

Refresh always fails: a personal key has no refresh path.

func (StaticToken) Token

func (s StaticToken) Token(context.Context) (string, error)

Token returns the static token.

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

type Subject struct {
	Type string `json:"type"`
	ID   string `json:"id"`
}

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 Verifier

type Verifier interface {
	WhoAmI(ctx context.Context) (*Identity, error)
}

Verifier verifies a token and returns the authenticated identity.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL