api

package
v0.1.8 Latest Latest
Warning

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

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

Documentation

Overview

Package api is the (thin) HTTP client for the Civitai App Blocks 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 (
	// 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 (civitai/civitai PR #2644):

  • device init: POST {BaseURL}/api/auth/oauth/device
  • device poll: POST {BaseURL}/api/auth/oauth/device-token
  • token refresh: POST {BaseURL}/api/auth/oauth/token

civitai-cli is a PUBLIC client (PKCE/device): no client secret.

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

DefaultSubmitPath is the token-authenticated submit-version route.

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 ErrNoRefresh = fmt.Errorf("token source cannot refresh")

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

Functions

This section is empty.

Types

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

type Identity struct {
	Username string `json:"username"`
	ID       int    `json:"id"`
}

Identity is the minimal authenticated-user view `whoami` reports.

type OAuthClient

type OAuthClient struct {
	BaseURL string
	HTTP    *http.Client
}

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.

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