appapi

package
v0.1.88 Latest Latest
Warning

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

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

Documentation

Overview

Package appapi is the CLI-internal client for the Civitai App Blocks developer surface: bundle submission, submission status / withdraw, the per-app dev-token and dev-tunnel machinery, the owner-only Forgejo clone info, and the OAuth device-authorization login flow. It builds on the public read/download SDK (github.com/civitai/cli/pkg/civitai) — reusing its TokenSource contract and error-kind classification — but is deliberately NOT part of that SDK's exported compatibility surface: these operations are internal to the `civitai` CLI and only the read/download client is a public contract.

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 (
	StartDevTunnelPath = "/api/trpc/blocks.startDevTunnel"
	StopDevTunnelPath  = "/api/trpc/blocks.stopDevTunnel"
)

StartDevTunnelPath / StopDevTunnelPath are the non-batched tRPC routes.

View Source
const (
	// ClientID is the public OAuth client id for the CLI.
	ClientID = "civitai-cli"

	// DeviceScope is UserRead|AppBlocksSubmit|AppBlocksDevTunnel (bit flags), the
	// fixed scope the CLI requests on login. 100663297 == (1<<0)|(1<<25)|(1<<26):
	//   - 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).
	//   - AppBlocksDevTunnel  (1<<26 = 67108864) — `app dev-tunnel` (start/stop/status).
	//     The dev-tunnel tRPC procs require this bit on an OAuth token; without it a
	//     login token 403s the scope gate and only a Full personal API key works.
	// This is exactly the civitai-cli OauthClient.allowedScopes set (100663297). 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.
	//
	// 🔴 SERVER DEPENDENCY: AppBlocksDevTunnel (bit 26) + the widened civitai-cli
	// allowedScopes (100663297) must be LIVE on prod auth (migration applied) BEFORE
	// this ships — else the device request exceeds allowedScopes and login 400s
	// (invalid_scope). Do NOT release this ahead of the civitai server change.
	//
	// What a login token can do: MINT an App-Blocks dev token (the mint gate needs
	// AppBlocksSubmit), open an on-site dev tunnel (AppBlocksDevTunnel), and drive
	// the read/estimate harness paths — cost preview / whatif, catalog browsing, 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 = "100663297"
)

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 (

	// ImageUploadPath mints a presigned PUT URL for a full-resolution asset
	// (cover / screenshot). Bearer-authed; returns {id, uploadURL}. The `id` (a
	// uuid) is the key persistAssetImage stores.
	ImageUploadPath = "/api/v1/image-upload"
)
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 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 AttachResult added in v0.1.87

type AttachResult struct {
	Status      string `json:"status"`
	IconID      *int   `json:"iconId,omitempty"`
	CoverID     *int   `json:"coverId,omitempty"`
	ID          string `json:"id,omitempty"` // screenshot id
	Order       *int   `json:"order,omitempty"`
	ScanPending bool   `json:"scanPending,omitempty"`
}

AttachResult is the (loosely-parsed) union result of setIcon/setCover/ addScreenshot. `scanPending` means the image was stored while still scanning — the CLI polls before attaching, so it should already be false.

type BuzzAccount

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

func (a *BuzzAccount) Total() int64

Total is the sum of the blue, green, and yellow balances.

type BuzzReader

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     civitai.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
	// MaxResponseBody overrides the per-response body read cap (see
	// maxResponseBody) when > 0. Tests set a small value to exercise the over-cap
	// guard without allocating 64 MiB.
	MaxResponseBody int64
}

Client is the CLI-internal App Blocks HTTP client. It carries its own auth + submit plumbing (see appblocks.go) and shares only the read/download SDK's exported TokenSource contract and error-kind helpers.

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 civitai.TokenSource, submitPath string) *Client

NewWithSource builds a Client backed by a TokenSource (which may refresh).

func (*Client) AddScreenshot added in v0.1.87

func (c *Client) AddScreenshot(ctx context.Context, listingID string, imageID int, caption string) (*AttachResult, error)

AddScreenshot appends an ingested screenshot (with an optional caption).

func (*Client) BeginListingRevision added in v0.1.87

func (c *Client) BeginListingRevision(ctx context.Context, listingID string) (shadowID string, created bool, err error)

BeginListingRevision opens (or reuses) a shadow-draft revision of an approved listing, returning the shadow id to attach media against.

func (*Client) GetAssetScanStatuses added in v0.1.87

func (c *Client) GetAssetScanStatuses(ctx context.Context, imageIDs []int) ([]ScanStatus, error)

GetAssetScanStatuses polls the scan state of the given image ids.

func (*Client) GetBuzzAccount

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

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) GetMyListingForApp added in v0.1.87

func (c *Client) GetMyListingForApp(ctx context.Context, appBlockID string) (*ListingRef, error)

GetMyListingForApp resolves the caller's own listing (by its backing appBlockId) to an AppListing id + lifecycle status. NOT_FOUND (404) means no listing row exists for the app yet.

func (*Client) GetMyListingForEdit added in v0.1.87

func (c *Client) GetMyListingForEdit(ctx context.Context, listingID string) (*ListingEditView, error)

GetMyListingForEdit reads the effective listing media (icon/cover/screenshots) for the given AppListing id. 🔴 Side effect: for an APPROVED parent the server idempotently opens a shadow revision and returns ITS assets.

func (*Client) GetSubmission

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) IngestAssetFromDataURI added in v0.1.87

func (c *Client) IngestAssetFromDataURI(ctx context.Context, data []byte, mimeType string) (int, error)

IngestAssetFromDataURI ingests inline icon bytes as a `data:image/...;base64,…` URI (the icon-only lean path) and returns the scannable Image id. The server caps the decoded size at ~2 MiB and rasterizes to PNG. `kind` is fixed to "icon" — the proc's schema only accepts icons on this path.

func (*Client) IngestAssetFullRes added in v0.1.87

func (c *Client) IngestAssetFullRes(ctx context.Context, data []byte, info ImageInfo) (int, error)

IngestAssetFullRes mints an upload URL, PUTs the raw bytes, then persists the Image row via persistAssetImage — the full-resolution path for cover / screenshot (the data-URI path is icon-only). Returns the scannable Image id.

func (*Client) ListSubmissions

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

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) MintImageUpload added in v0.1.87

func (c *Client) MintImageUpload(ctx context.Context) (id, uploadURL string, err error)

MintImageUpload mints a presigned PUT URL for a full-resolution asset upload.

func (*Client) RemoveScreenshot added in v0.1.87

func (c *Client) RemoveScreenshot(ctx context.Context, screenshotID string) error

RemoveScreenshot removes a screenshot by its id.

func (*Client) ReorderScreenshots added in v0.1.87

func (c *Client) ReorderScreenshots(ctx context.Context, listingID string, orderedIDs []string) error

ReorderScreenshots writes the listing's screenshots into the given order (orderedIds MUST be exactly the current set).

func (*Client) SetCover added in v0.1.87

func (c *Client) SetCover(ctx context.Context, listingID string, imageID int) (*AttachResult, error)

SetCover attaches an ingested (cover) image to the listing.

func (*Client) SetIcon added in v0.1.87

func (c *Client) SetIcon(ctx context.Context, listingID string, imageID int) (*AttachResult, error)

SetIcon attaches an ingested (icon) image to the listing.

func (*Client) StartDevTunnel

func (c *Client) StartDevTunnel(ctx context.Context, blockID, sshPublicKey string, declaredScopes []string) (*DevTunnelSession, error)

StartDevTunnel POSTs blocks.startDevTunnel and returns the minted session. The OAuth access token is refreshed transparently on a 401.

func (*Client) StopDevTunnel

func (c *Client) StopDevTunnel(ctx context.Context, sessionID, blockID string) (bool, error)

StopDevTunnel POSTs blocks.stopDevTunnel. A non-empty sessionID selects by session (preferred); otherwise blockID selects the caller's active tunnel for that app. Returns whether the server tore a session down.

func (*Client) SubmitListingRevision added in v0.1.87

func (c *Client) SubmitListingRevision(ctx context.Context, shadowID, changelog string) (*SubmitRevisionResult, error)

SubmitListingRevision submits a prepared shadow revision for moderator review.

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

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

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 DevTunnelController

type DevTunnelController interface {
	// StartDevTunnel mints a tunnel credential + host for blockId, binding it to
	// the caller's ephemeral SSH public key. declaredScopes carries the LOCAL
	// manifest's `scopes` so the server can grant them to an UNSUBMITTED app's
	// tunnel token (empty = read-only). Returns the assigned host + the /apps/dev
	// URL the developer opens.
	StartDevTunnel(ctx context.Context, blockID, sshPublicKey string, declaredScopes []string) (*DevTunnelSession, error)
	// StopDevTunnel revokes the caller's tunnel by sessionId (preferred) or, when
	// sessionId is empty, by blockId. Returns whether a session was torn down.
	StopDevTunnel(ctx context.Context, sessionID, blockID string) (bool, error)
}

DevTunnelController mints + revokes a dev-tunnel session. Behind an interface so the command layer is testable without a live server.

type DevTunnelForbiddenError

type DevTunnelForbiddenError struct {
	ServerMsg         string
	InsufficientScope bool
}

DevTunnelForbiddenError is returned when the dev-tunnel mint is refused with 403. Typed so the command layer can errors.As it and give the RIGHT fix, which differs by cause:

  • InsufficientScope: the CLI's credential lacks Full scope (the token-scope gate runs before the author/flag gates) → fix is a full-scope personal API key, NOT a different account.
  • otherwise: the account lacks the Apps-author invite + dev-tunnel flag → fix is signing in as an enrolled account.

func (*DevTunnelForbiddenError) Error

func (e *DevTunnelForbiddenError) Error() string

type DevTunnelSession

type DevTunnelSession struct {
	SessionID string `json:"sessionId"`
	// Host is the assigned unguessable `dev-<16hex>.<APPS_DOMAIN>` the reverse
	// tunnel binds to; the CLI passes it to `ssh -R` as the remote bind host.
	Host string `json:"host"`
	// URL is the `/apps/dev/<blockId>` page the developer opens in their browser.
	URL string `json:"url"`
	// ExpiresAt is the hard-TTL expiry (unix seconds) after which the server
	// reaper reclaims the route even if the CLI never calls stopDevTunnel.
	ExpiresAt int64 `json:"expiresAt"`
	// SpendCapBuzz is the per-session cumulative Buzz ceiling (backstop).
	SpendCapBuzz int64 `json:"spendCapBuzz"`
	// SSHHostPublicKey is the sish endpoint's OpenSSH host public-key line
	// (`ssh-ed25519 AAAA...`) — a NON-SECRET value the CLI PINS as the SSH
	// HostKeyCallback so the `ssh -R` bind can't be MITM'd (an on-path attacker
	// impersonating sish would reach the dev's localhost + tamper tunneled
	// traffic). The mint returns it; the CLI fails closed if it is absent
	// (never falls back to InsecureIgnoreHostKey).
	SSHHostPublicKey string `json:"sshHostPublicKey"`
}

DevTunnelSession mirrors blocks.startDevTunnel's result (the server's StartDevTunnelResult in dev-tunnel.service.ts). Field names + JSON casing track the server EXACTLY.

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

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 raw per-window spend-cap payload as returned
	// by the server. Its shape is server-owned and has changed over time (a bare
	// number in older responses, an array of {type,limit,window,unit} windows in
	// current ones), so it is kept as RawMessage: whoami does not render it, and
	// it must never break the parse of the core identity. nil ⇒ absent/unknown.
	BuzzLimit json.RawMessage `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 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". The volatile, unrendered fields (BuzzLimit, Subject.ID) are json.RawMessage so a server-side type change to a peripheral field can never break the parse of the core identity whoami prints (see WhoAmI's core-identity fallback for the belt-and-suspenders guarantee).

func (*Identity) CanReadBuzz

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

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) CanSubmitApps added in v0.1.85

func (id *Identity) CanSubmitApps() bool

CanSubmitApps reports whether the credential can clear `civitai app submit`'s SCOPE gate. The backend scope-gates submit ONLY on OAuth tokens: an OAuth device-login token must carry the opt-in AppBlocksSubmit bit (bit 25, excluded from ScopeFull), whereas a personal API key is NOT scope-gated for submit at all — submit-version runs the AppBlocksSubmit check only when subject.type == "oauth", so any personal key clears it. (The remaining author-cohort / not-banned gates are server-side and not visible here, so a "yes" means the credential's SCOPE permits submit, not that the account is in the author cohort.) An unknown/absent credential is treated as false.

func (*Identity) CredentialType

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

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

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

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 ImageInfo added in v0.1.87

type ImageInfo struct {
	Width    int
	Height   int
	MimeType string // "image/png" | "image/jpeg" | "image/webp"
}

ImageInfo is the minimal media metadata the full-resolution persist path (`persistAssetImage`) needs: pixel dimensions + the wire MIME type.

func DecodeImageInfo added in v0.1.87

func DecodeImageInfo(data []byte) (ImageInfo, error)

DecodeImageInfo reads width/height/MIME from an image HEADER without a full decode. PNG/JPEG go through the stdlib `image.DecodeConfig`; WebP (which the stdlib does not register) is parsed from its RIFF/VP8 container header. It returns an error for an unrecognized, unsupported, or truncated image so the caller can fail cleanly BEFORE any upload.

Only png/jpeg/webp are accepted — the exact set the listing-asset attach validation allows (`LISTING_ASSET_ALLOWED_MIME`).

type ListingAsset added in v0.1.87

type ListingAsset struct {
	ImageID *int    `json:"imageId"`
	URL     *string `json:"url"`
}

ListingAsset mirrors ListingEditAsset ({imageId, url}); a nil/zero imageId + empty url means the slot is unset.

func (ListingAsset) Present added in v0.1.87

func (a ListingAsset) Present() bool

Present reports whether the asset slot is populated.

type ListingEditView added in v0.1.87

type ListingEditView struct {
	ParentID           string  `json:"parentId"`
	Slug               string  `json:"slug"`
	Status             string  `json:"status"`
	HasPendingRevision bool    `json:"hasPendingRevision"`
	ShadowID           *string `json:"shadowId"`
	Assets             struct {
		Icon        ListingAsset        `json:"icon"`
		Cover       ListingAsset        `json:"cover"`
		Screenshots []ListingScreenshot `json:"screenshots"`
	} `json:"assets"`
}

ListingEditView mirrors getMyListingForEdit's result (the subset the CLI reads to render `status` + the trailing floor line). For an APPROVED parent the server resolves the media from the in-flight shadow revision (an idempotent begin), so the assets reflect the pending revision, not the live listing.

type ListingRef added in v0.1.87

type ListingRef struct {
	AppListingID       string `json:"appListingId"`
	Status             string `json:"status"` // draft|pending|approved|rejected|removed
	ContentRating      string `json:"contentRating"`
	HasPendingRevision bool   `json:"hasPendingRevision"`
}

ListingRef is the result of getMyListingForApp — the entry read that resolves an app's backing AppListing + its lifecycle status.

type ListingScreenshot added in v0.1.87

type ListingScreenshot struct {
	ID      string  `json:"id"`
	ImageID *int    `json:"imageId"`
	URL     *string `json:"url"`
	Caption *string `json:"caption"`
	Order   int     `json:"order"`
}

ListingScreenshot mirrors ListingEditScreenshot.

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 ScanStatus added in v0.1.87

type ScanStatus struct {
	ImageID int    `json:"imageId"`
	Status  string `json:"status"` // "scanned" | "blocked" | "pending"
}

ScanStatus is a per-image scan state (getAssetScanStatuses).

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 StatusReader

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

type Subject struct {
	Type string `json:"type"`
	// ID is the credential's identifier. Its JSON shape is server-owned and
	// varies by credential kind — a numeric api-key id (e.g. 96633526) or a
	// string oauth subject — so it is kept as RawMessage to tolerate either
	// shape. whoami does not render it; only Type drives CredentialType/IsOAuth.
	ID json.RawMessage `json:"id,omitempty"`
}

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

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 SubmitRevisionResult added in v0.1.87

type SubmitRevisionResult struct {
	PublishRequestID string `json:"publishRequestId"`
	ShadowID         string `json:"shadowId"`
	Slug             string `json:"slug"`
}

SubmitRevisionResult mirrors submitListingRevision's result.

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 Verifier

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

Verifier verifies a token and returns the authenticated identity.

type Withdrawer

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