Help us shape the future of Go! Take the 2026 Go Developer Survey

cloud

package
v1.0.14 Latest Latest
Warning

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

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

Documentation

Overview

Package cloud resolves the two things forge needs to talk to a hosted control plane, and keeps them apart on purpose:

  • the ENDPOINT, which is a per-environment fact declared in that environment's KCL (forge.ControlPlane) and lives in git.
  • the CREDENTIAL, which is an opaque bearer string that must never live in git, resolved at call time from a flag, an environment variable, the shared credentials file `forge login` writes (forge/pkg/credentials) keyed by THAT endpoint, or a credential helper the host application names (forge/pkg/cloudcred).

This split mirrors internal/secrets: KCL emits the DECLARATION, Go resolves the VALUE. It is also why there is no `forge context use`. A stateful "current endpoint" would make the answer to "which server did that command hit" depend on machine-local state that never appears in a diff; deriving it from the env argument makes the answer a property of the repository instead.

Nothing here imports control-plane or reliant, and nothing may. The endpoint is an arbitrary URL and the credential an opaque string — the same relationship forge's External deploy target has with flyctl, which needs FLY_API_TOKEN without forge linking Fly's SDK.

Index

Constants

View Source
const (
	ReasonClaimableThrottled   = "claimable_throttled"
	ReasonClaimableUnavailable = "claimable_unavailable"
	ReasonClaimableClaimed     = "claimable_claimed"
	ReasonClaimableExpired     = "claimable_expired"
	ReasonClaimableTakenDown   = "claimable_taken_down"
	ReasonClaimableInvalidPoW  = "claimable_invalid_pow"
)

The reasons (x-forge-error-reason) the ClaimableService answers with, and the two access-token reasons that mean the same thing for a stored token.

View Source
const (
	ProcStartClaimableDeploy     = "controlplane.v1.ClaimableService/StartClaimableDeploy"
	ProcGetClaimableChallenge    = "controlplane.v1.ClaimableService/GetClaimableChallenge"
	ProcGetClaimableAvailability = "controlplane.v1.ClaimableService/GetClaimableAvailability"
)

The ClaimableService procedures forge calls.

View Source
const (
	ClaimableCauseDisabled              = "disabled"
	ClaimableCauseDependencyUnavailable = "dependency_unavailable"
	ClaimableCauseNotSupported          = "not_supported"
	// ClaimableCauseNotDeployed is forge's own: the control plane does not
	// serve the ClaimableService at all.
	ClaimableCauseNotDeployed = "not_deployed"
)

The cause codes (control-plane internal/claimable).

View Source
const (
	// ReasonAccessTokenExpired: the token existed and its expiry has passed.
	ReasonAccessTokenExpired = "access_token_expired"
	// ReasonAccessTokenRevoked: the token was revoked.
	ReasonAccessTokenRevoked = "access_token_revoked"
	// ReasonAccessTokenUnknown: well-formed, but this control plane has no
	// such token — typically a token minted for another environment.
	ReasonAccessTokenUnknown = "access_token_unknown"
	// ReasonAccessTokenMalformed: not the shape of an access token at all.
	ReasonAccessTokenMalformed = "access_token_malformed"
	// ReasonAccessTokenUnavailable: the control plane could not check the
	// token. NOT a verdict on it: retrying later is the right response, and
	// signing in again is not.
	ReasonAccessTokenUnavailable = "access_token_unavailable"
	// ReasonClientUpgradeRequired: this forge is older than the control plane
	// serves (it reads VersionHeader). The server's message names the oldest
	// release it accepts and how to upgrade; nothing was executed.
	ReasonClientUpgradeRequired = "client_upgrade_required"
)

The reasons a control plane gives, in ReasonHeader, for refusing a bearer that claims to be an access token. They say WHICH way the credential is dead, so forge can word the fix and decide whether asking for a fresh one can help.

View Source
const (
	// CodeNotFound: the thing does not exist. Several reads turn this into
	// an ANSWER rather than a failure — "never cut" is not an error.
	CodeNotFound = "not_found"
	// CodeAlreadyExists: a create collided with an existing row.
	CodeAlreadyExists = "already_exists"
	// CodeFailedPrecondition: the server refused given the current state.
	// The category every promote refusal shares; Reason says which.
	CodeFailedPrecondition = "failed_precondition"
	// CodeInvalidArgument: the request itself was wrong.
	CodeInvalidArgument = "invalid_argument"
	// CodeUnimplemented: this control plane does not serve the procedure.
	CodeUnimplemented = "unimplemented"
	// CodeUnauthenticated and CodePermissionDenied: the credential was
	// missing/stale, or lacks the scope.
	CodeUnauthenticated   = "unauthenticated"
	CodePermissionDenied  = "permission_denied"
	CodeUnavailable       = "unavailable"
	CodeDeadlineExceeded  = "deadline_exceeded"
	CodeResourceExhausted = "resource_exhausted"
)

The Connect error codes forge branches on. Named so a caller never writes the literal, and never searches for it in a message.

View Source
const (
	KindHuman    = "human"
	KindOrgLogin = "org-login"
)

Credential kinds a browser login can produce.

KindHuman is an identity across the user's memberships: the server resolves every operation's organization from its hosted project target.

KindOrgLogin is what a control plane that predates human identities issues for the same flow (its token response carries no credential_kind): an `rlat_` token bound to the user's organization. It is a delegated capability, never reclassified as an identity, so it takes the delegated path (GetTenant, no ResolveHostedProject) that such a control plane serves. Once the control plane issues human identities, the next `forge login` stores one instead.

View Source
const ClaimableClientIDPrefix = "forge-cli-claimable"

ClaimableClientIDPrefix starts the credentials-file client id of every anonymous deploy token. The id is not the bare prefix: a machine can hold one claimable deploy per (project, env), and the control plane's idempotency key is the same triple, so each gets its own entry (ClaimableClientIDFor).

View Source
const ClientID = "forge-cli"

ClientID is forge's OAuth client id at a control plane.

View Source
const ContentScannedTimeout = 12 * time.Minute

ContentScannedTimeout is the request timeout for the procedures in contentScannedProcedures. The control plane scans an unclaimed org's uploaded artifacts (ClamAV, Web Risk) inside these requests, within a 10m budget (control-plane contentscan.ScanBudget), and its gateway gives them 11m (CONTENT_SCAN_ROUTE_TIMEOUT). A container image's scan routinely exceeds the client's 30s default. A shorter client timeout only abandons a scan the server is still running and turns its verdict into a bare "context deadline exceeded".

View Source
const CredentialOrder = "--token, then the env's declared token_env, then what `forge login` stored, " +
	"then the credential helper $" + cloudcred.HelperEnv + " (a host application's session — Reliant sets it)"

CredentialOrder is the resolution order in one line, for help text and errors that explain where a credential comes from.

View Source
const DefaultEndpoint = "https://admin.reliantapi.com"

DefaultEndpoint is Reliant's production control plane: the one a plain `go install`'d forge targets with no configuration.

It is the Go copy of KCL's forge.RELIANT_CLOUD_ENDPOINT, which is what `forge.ControlPlane {}` resolves to. KCL owns the default for every env; this copy exists for the one command that runs with no env to render — `forge login` outside a project (or in one that declares no control plane). TestControlPlaneDefault_KCLMatchesGo renders the schema and fails if the two drift.

It is the PUBLIC origin serving forge's whole hosted surface: the controlplane.v1.* RPCs and the CLI OAuth endpoints (/oauth/authorize, /oauth/token). reliant's API host is a different server and answers 404 for both, so pointing this at it would break login for every user.

View Source
const DefaultTokenEnv = "FORGE_CONTROL_PLANE_TOKEN"

DefaultTokenEnv is the environment variable forge reads a control-plane credential from when an environment's ControlPlane declaration does not name a different one.

View Source
const HumanClientID = "forge-cli-human"

HumanClientID is a separate storage namespace from delegated forge-cli capabilities. Storing an automation token never replaces a human identity.

View Source
const MaxPoWDifficultyBits = 28

MaxPoWDifficultyBits is the hardest puzzle forge will solve. The server's default costs about two seconds; a difficulty far above it is a broken or hostile server, and solving it would hang the deploy for hours.

View Source
const ProcGetOrganization = "controlplane.v1.DeployService/GetTenant"

ProcGetOrganization is the one control-plane call that answers "which organization does this credential act for". The server derives the answer from the authenticated token row — never from anything the client sends. The method's name is the control plane's identifier and cannot be renamed here.

View Source
const ReasonHeader = "x-forge-error-reason"

ReasonHeader is the Connect error-metadata key under which a control plane delivers a stable, machine-readable domain reason code. It matches forge's own server-side convention (pkg/svcerr.ReasonHeader), and the value is an app-defined snake_case code such as "promotion_conflict".

Declared here rather than imported from pkg/svcerr on purpose: this is the CLIENT half of the contract, and it must not acquire a dependency on forge's server library to read one header name. HTTP header keys are case-insensitive, so the canonicalization does not matter.

View Source
const VersionHeader = "Forge-Version"

VersionHeader is the request header this client names its forge release in, on every control-plane call.

Variables

View Source
var ErrNoCredential = errors.New("no control-plane credential")

ErrNoCredential is returned when no credential could be resolved. Use errors.Is to detect it; the message is already actionable (it names the remedy for this machine — `forge login`, or signing in to the host application — and the environment variable), so callers should surface it as-is rather than wrapping it in a generic auth failure.

Functions

func ClaimableActive added in v1.0.0

func ClaimableActive() bool

ClaimableActive reports whether this process is deploying as the anonymous claimable user (UseClaimableCredential is installed). The claimable plan is platform-funded and never billed, so rules that exist only to keep a bill equal to a request (the hosted shape band) do not apply under it.

func ClaimableClientIDFor added in v1.0.0

func ClaimableClientIDFor(project, env string) string

ClaimableClientIDFor is the client id the anonymous token of (project, env) is stored under. It is never equal to ClientID, which is all that makes a stored claimable token unable to stand in for a `forge login`.

func ClaimableDeviceID added in v1.0.0

func ClaimableDeviceID() (string, error)

ClaimableDeviceID is this machine's stable claimable-deploy identifier, generated once and kept beside the credentials file. Only its hash is stored server-side; it is the unit of the "3 live per device" limit and part of the idempotency key, so it must not change between runs.

func CredentialFromToken

func CredentialFromToken(tok *oauth2.Token, now time.Time) credentials.Credential

CredentialFromToken turns a token response into the stored entry.

func CredentialsPath

func CredentialsPath() (string, error)

CredentialsPath is where the shared credentials file lives, from $FORGE_HOME, $XDG_CONFIG_HOME and the home directory. forge/pkg/credentials owns both the precedence and the lookup (DirsFromEnv), so forge and every other CLI sharing the file resolve it the same way.

func DeviceLabel

func DeviceLabel() string

DeviceLabel names this machine in the minted token's name (forge-cli@<device>). A re-login from the same device replaces that device's token server-side rather than accumulating new ones.

func ForgetClaimable added in v1.0.0

func ForgetClaimable(ep Endpoint, project, env string) error

ForgetClaimable drops the stored anonymous deploy of (project, env).

func LoadClaimable added in v1.0.0

func LoadClaimable(ep Endpoint, project, env string, now time.Time) (credentials.Credential, error)

LoadClaimable returns the stored anonymous deploy of (project, env) at ep, or credentials.ErrNotFound. A stored entry whose token has expired is reported as not found: its deploy is gone.

func LoginScopes

func LoginScopes() []string

LoginScopes names the CLI's requested use cases on the OAuth approval page. Human login authenticates the user, not an org grant snapshot; the server authorizes each target operation against current membership and permissions.

func OpenBrowser

func OpenBrowser(target string) error

OpenBrowser launches the platform's URL handler.

func OrganizationReply added in v1.0.0

func OrganizationReply(orgID string) map[string]any

OrganizationReply is the body a control plane answers ProcGetOrganization with, for a fake control plane in a test of code that resolves the org. It lives beside the reader so the wire spelling exists in one file.

func RecordClaimableURL added in v1.0.0

func RecordClaimableURL(ep Endpoint, project, env, url string) error

RecordClaimableURL remembers the deploy's live address beside its token, so `forge env status --claimable` can print it again without a credential.

func ResolveOrganization

func ResolveOrganization(ctx context.Context, c Caller) (string, error)

ResolveOrganization asks the control plane which organization the credential behind c acts for.

forge does not DECLARE the organization: it is a fact about the credential, and a declared copy could only ever agree with it or be wrong. Reading it from the authority that enforces it (the registry realm scopes pushes to this same org) leaves nothing to drift.

The id is a lowercase UUID, because that is the path segment the platform registry's grammar uses (`<org>/<project>/<image>`).

func RevokeIdentity added in v1.0.0

func RevokeIdentity(ctx context.Context, ep Endpoint, token string) error

RevokeIdentity logs this device out across every organization, not one org.

func SignInHint

func SignInHint() string

SignInHint is the "authenticate and retry" remedy for THIS process. Under a host application that provides a credential helper, the user is meant to be signed in once, to the host — so the remedy is the host's sign-in, not a second login to forge. A standalone forge says `forge login`.

func SolvePoW added in v1.0.0

func SolvePoW(ctx context.Context, ch ClaimableChallenge) (string, error)

SolvePoW finds the nonce for ch, spreading the search over the CPUs.

func StoreClaimable added in v1.0.0

func StoreClaimable(ep Endpoint, project, env string, d ClaimableDeploy, now time.Time) (credentials.Credential, error)

StoreClaimable records the anonymous deploy of (project, env) at ep.

func TokenIdentity added in v1.0.0

func TokenIdentity(token string) string

TokenIdentity is Credential.Identity for a bare token string.

func UseClaimableCredential added in v1.0.0

func UseClaimableCredential(ep Endpoint, token string, expiresAt *time.Time) (restore func())

UseClaimableCredential makes ResolveCredential return the anonymous token for ep, for this process, until the returned function runs. It is how the ordinary hosted publish (a dozen call sites that each resolve a credential) runs as the anonymous user without any of them learning about claimable deploys.

It is installed only by the --claimable path, and only after a real credential was found NOT to resolve, so it can never shadow a login.

func VerifyToken

func VerifyToken(ctx context.Context, ep Endpoint, cred Credential) error

VerifyToken checks a credential against the endpoint before storing it, so `forge login --token` fails at login rather than at the first real command.

Types

type BrowserLogin

type BrowserLogin struct {
	// Endpoint is the control plane being logged in to.
	Endpoint Endpoint
	// OpenURL is the browser launcher. Injectable so a test drives the
	// whole flow without opening a real browser.
	OpenURL func(string) error
	// Timeout bounds the wait for the browser. A login that hangs forever is
	// indistinguishable from one about to succeed.
	Timeout time.Duration
	// Out receives the "open this URL" notice.
	Out io.Writer
}

BrowserLogin runs the interactive half of `forge login`: the OAuth 2.0 authorization-code flow with PKCE (RFC 7636) against the endpoint's /oauth/authorize and /oauth/token, with a loopback redirect (RFC 8252).

The mechanics are forge/pkg/oauth2.LoopbackLogin, which `reliant auth login` also runs. forge depends on no reliant code, deliberately. A user deploying to hosted infra without the reliant harness must never have to install reliant first, so `forge login` stands alone.

func (BrowserLogin) Run

Run performs the flow and returns the credential to store.

type Caller

type Caller interface {
	Call(ctx context.Context, procedure string, req, out any) error
}

Caller is the one method ResolveOrganization needs.

type ClaimableAPI added in v1.0.0

type ClaimableAPI interface {
	Availability(ctx context.Context) (ClaimableAvailability, error)
	Challenge(ctx context.Context) (ClaimableChallenge, error)
	Start(ctx context.Context, req ClaimableStartRequest) (ClaimableDeploy, error)
}

ClaimableAPI is the unauthenticated half of the ClaimableService. A seam so a test states the control plane's answers; production is ClaimableClient.

type ClaimableAvailability added in v1.0.1

type ClaimableAvailability struct {
	Available        bool             `json:"available"`
	Causes           []ClaimableCause `json:"causes,omitempty"`
	ComputeAvailable bool             `json:"computeAvailable"`
	ComputeCauses    []ClaimableCause `json:"computeCauses,omitempty"`
	// Limits are the claimable plan's caps, as the control plane enforces
	// them. Nil from a control plane that predates publishing them.
	Limits *ClaimableLimits `json:"limits,omitempty"`
}

ClaimableAvailability is GetClaimableAvailability's answer: can a start be admitted now (enough for a static site), can an unclaimed service and database run, and every reason not.

type ClaimableCause added in v1.0.1

type ClaimableCause struct {
	// Code is disabled | dependency_unavailable | not_supported.
	Code string `json:"code"`
	// Component is the switch or the dependency (claim_secret,
	// anonymous_signup, content_scan_web_risk, network_policing, ...).
	Component string `json:"component,omitempty"`
	Message   string `json:"message"`
}

ClaimableCause is one reason claimable deploys, or their compute, are unavailable (GetClaimableAvailability, and the metadata of a claimable_unavailable refusal).

func ClaimableUnavailableCauseOf added in v1.0.1

func ClaimableUnavailableCauseOf(err error) (ClaimableCause, bool)

ClaimableUnavailableCauseOf reads the cause a claimable_unavailable refusal carries in its metadata. ok is false for any other error, or a refusal from a control plane that names no cause.

type ClaimableChallenge added in v1.0.0

type ClaimableChallenge struct {
	Challenge      string    `json:"challenge"`
	DifficultyBits uint32    `json:"difficultyBits"`
	ExpiresAt      time.Time `json:"expiresAt"`
}

ClaimableChallenge is the proof-of-work puzzle: find a decimal nonce such that SHA-256(Challenge || ":" || nonce) has at least DifficultyBits leading zero bits.

type ClaimableClient added in v1.0.0

type ClaimableClient struct {
	// contains filtered or unexported fields
}

ClaimableClient speaks the ClaimableService over Connect's JSON binding, with NO credential: the caller has none, and StartClaimableDeploy is how it gets one.

func NewClaimableClient added in v1.0.0

func NewClaimableClient(ep Endpoint) *ClaimableClient

NewClaimableClient builds the client for ep.

func (*ClaimableClient) Availability added in v1.0.1

func (c *ClaimableClient) Availability(ctx context.Context) (ClaimableAvailability, error)

Availability asks whether a claimable deploy can start now. It creates nothing and costs no proof of work.

func (*ClaimableClient) Challenge added in v1.0.0

Challenge fetches a fresh proof-of-work puzzle.

func (*ClaimableClient) Start added in v1.0.0

Start mints (or, idempotently, finds) the anonymous deploy.

type ClaimableDeploy added in v1.0.0

type ClaimableDeploy struct {
	AccessToken string    `json:"accessToken"`
	OrgID       string    `json:"orgId"`
	ClaimURL    string    `json:"claimUrl"`
	ExpiresAt   time.Time `json:"expiresAt"`
	PurgeAfter  time.Time `json:"purgeAfter"`
	Reused      bool      `json:"reused"`
}

ClaimableDeploy is StartClaimableDeploy's response: the anonymous deploy token, the org it acts for and the claim link.

type ClaimableLimits added in v1.0.1

type ClaimableLimits struct {
	MaxStaticSites              int32 `json:"maxStaticSites,omitempty"`
	MaxStaticStoredMib          int32 `json:"maxStaticStoredMib,omitempty"`
	MaxServices                 int32 `json:"maxServices,omitempty"`
	MaxReplicas                 int32 `json:"maxReplicas,omitempty"`
	MaxMigrationJobs            int32 `json:"maxMigrationJobs,omitempty"`
	WorkloadCPUMillicores       int32 `json:"workloadCpuMillicores,omitempty"`
	WorkloadMemoryMib           int32 `json:"workloadMemoryMib,omitempty"`
	WorkloadEphemeralStorageMib int32 `json:"workloadEphemeralStorageMib,omitempty"`
	MaxDatabases                int32 `json:"maxDatabases,omitempty"`
	DatabaseInstances           int32 `json:"databaseInstances,omitempty"`
	DatabaseCPUMillicores       int32 `json:"databaseCpuMillicores,omitempty"`
	DatabaseMemoryMib           int32 `json:"databaseMemoryMib,omitempty"`
	DatabaseStorageMib          int32 `json:"databaseStorageMib,omitempty"`
	DatabaseMaxConnections      int32 `json:"databaseMaxConnections,omitempty"`
	DatabaseDeclaredStorageGib  int32 `json:"databaseDeclaredStorageGib,omitempty"`
}

ClaimableLimits is the control plane's ClaimableLimits message (Connect's JSON names): what one unclaimed deploy may run. Every value is a hard cap; "per workload" holds for the service and, separately, its migration job, each request = limit.

type ClaimableStartRequest added in v1.0.0

type ClaimableStartRequest struct {
	Project      string `json:"project"`
	Env          string `json:"env"`
	DeviceID     string `json:"deviceId"`
	PoWChallenge string `json:"powChallenge"`
	PoWNonce     string `json:"powNonce"`
}

ClaimableStartRequest is StartClaimableDeploy's request.

type ClaimableThrottle added in v1.0.0

type ClaimableThrottle struct {
	// Scope is network | device | platform.
	Scope string
	// RetryAfter is how long to wait.
	RetryAfter time.Duration
}

ClaimableThrottle is a refusal to mint, with when to come back.

func ClaimableThrottleOf added in v1.0.0

func ClaimableThrottleOf(err error) (ClaimableThrottle, bool)

ClaimableThrottleOf reads a throttle out of err: a ResourceExhausted refusal (or one with reason claimable_throttled). The facts come from the ClaimableThrottle error detail, and from the metadata the server also sends for a client that ignores details.

type Client

type Client struct {
	Endpoint   Endpoint
	Credential Credential
	HTTP       *http.Client
	// contains filtered or unexported fields
}

Client calls a hosted control plane over Connect's HTTP+JSON binding: POST <base>/<package>.<Service>/<Method> with a JSON body and a JSON reply.

WHY HTTP+JSON AND NOT A GENERATED CLIENT. A generated client would mean vendoring the control plane's protos into forge, which is the one coupling that must not ship — it would make forge's deploy story specific to our control plane in exactly the way forge's External target is deliberately NOT specific to Fly.io. Connect's JSON binding is a documented, stable part of the protocol and needs nothing but net/http, so the wire contract stays a contract rather than a shared build dependency.

The cost is real and worth naming: forge does not get compile-time checking of request and response shapes against the server's protos. A server-side field rename surfaces as a missing value at runtime rather than a build failure. That is the accepted trade for independence, and it is why Call decodes into explicitly-declared local structs — the shape forge expects is written down in one place instead of inferred.

func NewClient

func NewClient(ep Endpoint, cred Credential) *Client

NewClient builds a Client with a bounded default timeout. A deploy CLI that hangs forever on an unreachable endpoint is worse than one that fails: CI would sit until its own job timeout with no diagnosis.

func (*Client) Call

func (c *Client) Call(ctx context.Context, procedure string, req, out any) error

Call invokes one Connect procedure. procedure is the fully-qualified path WITHOUT a leading slash, e.g. "controlplane.v1.DeployService/ListReleases".

req is marshalled to JSON and out is unmarshalled from the reply. A non-200 becomes a *Error (see error.go), which carries the Connect code, the domain reason and the server's error details as DATA. Callers branch on those fields — never on the message text, which is display copy the server is free to improve.

func (*Client) CallAnonymous added in v1.0.0

func (c *Client) CallAnonymous(ctx context.Context, procedure string, req, out any) error

CallAnonymous invokes one Connect procedure with NO Authorization header and none of Call's credential refresh or scope elevation: there is no credential to refresh.

type Credential

type Credential struct {
	Kind   string
	Token  string
	Source CredentialSource
	// From is the human-readable origin: the flag name, the env var name,
	// the credentials file path, or the helper's description of the
	// session it used. Safe to print; never the token itself.
	From string
	// ExpiresAt is when the token stops being accepted, when known: a
	// helper's tokens and a stored login carry one; nil means unknown.
	ExpiresAt *time.Time
	// Scopes the issuer reported granting, when known (a stored login records
	// them). Informational — the server decides.
	Scopes []string
}

Credential is a resolved bearer token plus where it came from.

func ResolveCredential

func ResolveCredential(flagToken string, ep Endpoint) (Credential, error)

ResolveCredential returns the bearer credential for ep, in this precedence order:

  1. flagToken — an explicit --token on the command line
  2. os.Getenv(ep.TokenEnv) — the env var the environment DECLARED
  3. the credentials file — what `forge login` stored FOR ep.URL
  4. the credential helper — $FORGE_CREDENTIAL_HELPER, a host application that holds the user's session (Reliant sets it), minting a short-lived token for ep on demand

WHY THIS ORDER. It runs most-explicit to most-ambient. A flag is typed for one command and can mean nothing else, so it must win — that is what makes a one-off override possible at all, and what makes a bug report reproducible ("run it with --token X"). The env var comes next because CI is where it is used: a pipeline has no browser and no persistent home directory, so the variable IS its credential, and it must beat any file that happens to exist in a cached runner image. The file comes before the helper because a human who ran `forge login` chose that credential for that endpoint deliberately, and a host application signed in as somebody else must not displace it. The helper is last because it is the most ambient of all: it is simply "whoever this machine is signed in to the host as" — the right DEFAULT, and the wrong override.

An EXPIRED `forge login` is not a dead end when a helper is configured: the user is still signed in to the host, so resolution falls through to it rather than demanding a second login. Without a helper it is reported as expired, because then `forge login` really is the fix.

The file lookup is keyed by the endpoint. A login to staging can never be presented to prod: a prod command finds no entry and says so, instead of sending staging's token to prod's server and failing with an indistinguishable 401. The helper is asked FOR ep, and refuses an endpoint its session does not belong to — the endpoint comes from a project's KCL, and a repository is not a trusted party.

func (Credential) Identity added in v1.0.0

func (c Credential) Identity() string

Identity names the credential WITHOUT its secret: the issuer's display prefix for an `rlat_` access token ("rlat_A3f9Kd2p…" — what the console's token list shows, so a human can match it), else only its length.

Every human-facing line that says WHICH credential was used prints this, never Token. An agent or a person who cannot tell which credential failed opens the credentials file to find out, and the file holds the secret — which is how a forge credential came to be pasted into a session log.

type CredentialSource

type CredentialSource string

CredentialSource says where a resolved credential came from. Commands print it so "which credential did that use" never requires guessing — the single most common confusion when a CI run and a laptop disagree.

const (
	SourceFlag  CredentialSource = "flag"             // --token
	SourceEnv   CredentialSource = "env"              // the declared token_env
	SourceLogin CredentialSource = "credentials file" // written by `forge login`
	// SourceHelper is a token the credential helper named by
	// $FORGE_CREDENTIAL_HELPER minted from a host application's session
	// (pkg/cloudcred). forge does not know which application; Credential.From
	// carries the helper's own description of it.
	SourceHelper CredentialSource = "credential helper"
)

The places a credential can come from, in the precedence order resolution tries them. An explicit flag beats the environment, the environment beats the credentials file on disk, and the file beats a host application's session. That order is what makes a CI run overridable, a human's own `forge login` deliberate, and a signed-in host the ambient default rather than a thing that silently wins.

const SourceClaimable CredentialSource = "claimable deploy"

SourceClaimable is the Credential.Source of the anonymous token.

type Declaration

type Declaration struct {
	Endpoint string
	TokenEnv string
}

Declaration is the per-environment control-plane declaration, as read from the environment's rendered KCL.

Declared HERE, at the consumer, rather than importing the CLI's entity type: this package needs two strings, and taking a dependency on the renderer's struct would point the dependency the wrong way (cli depends on cloud, not the reverse) and drag the whole entity graph in behind it.

type Endpoint

type Endpoint struct {
	// ProjectID is a server-owned target identity, never credential/org selection.
	ProjectID   string
	ProjectName string
	// Env is the environment this endpoint was declared by. Carried so
	// error messages can name the KCL file to edit.
	Env string
	// URL is the base URL, trailing slash trimmed.
	URL string
	// TokenEnv is the env var NAME the credential is read from.
	TokenEnv string
}

Endpoint is one environment's resolved hosted control plane: where to send requests, and which env var holds the credential.

There is deliberately no package-level "current endpoint" and no way to set one. An Endpoint only ever comes from a specific environment's declaration, which is what makes two environments resolvable to two different endpoints in the same process — and what removes the class of bug where a command silently targets whatever was selected last.

func ResolveEndpoint

func ResolveEndpoint(env string, decl *Declaration) (Endpoint, error)

ResolveEndpoint turns one environment's declaration into an Endpoint.

decl == nil means the environment declares no hosted control plane. That is the overwhelmingly common case, so it returns a plain error naming the exact file to edit rather than falling back to DefaultEndpoint. An env that declares nothing is not hosted; defaulting here would mean a project with no hosted anything could send a request somewhere by accident, which is the one outcome this signature is shaped to prevent. The Reliant-cloud default belongs to the DECLARATION (`forge.ControlPlane {}` fills endpoint in KCL), never to its absence.

type Error

type Error struct {
	// HTTPStatus is the response status. Kept because a response that is
	// not a Connect envelope at all (a proxy's HTML error page) has no
	// code, and the status is then the only classification available.
	HTTPStatus int

	// Code is the Connect error code as its wire name: "already_exists",
	// "not_found", "failed_precondition", "unauthenticated". Empty when
	// the body was not a Connect envelope.
	Code string

	// Reason is the app-defined domain reason from ReasonHeader, e.g.
	// "promotion_conflict" or "rollout_in_flight". Empty when the server
	// sent none.
	//
	// THIS IS THE FIELD TO BRANCH ON. The Connect code says the category
	// (FailedPrecondition), and three different refusals share it; the
	// reason says which one, and is the server's committed contract.
	Reason string

	// Scopes are the scopes a "scope_missing" refusal names, from the
	// response's accesstoken.ScopesHeader. Empty when the server named none.
	Scopes []string

	// Message is the server's own message, verbatim and untruncated. For
	// DISPLAY — never for control flow.
	Message string

	// Details are the Connect error details, each left in its wire form.
	//
	// NOT DECODED, and that is deliberate: decoding a google.protobuf.Any
	// would mean vendoring the control plane's protos into forge, which is
	// the one coupling forge's deploy story must not take on. Connect's
	// JSON binding includes a `debug` member carrying the message as
	// protojson when the server could produce it, so a caller reads the
	// payload as JSON (see DetailJSON) without a generated type.
	Details []ErrorDetail

	// Header is the response's headers, for the few facts a control plane
	// sends there beside the reason (a throttle's retry-after).
	Header http.Header

	// Procedure and Endpoint are where this happened, for the message.
	Procedure string
	Endpoint  string
	// contains filtered or unexported fields
}

Error is a failed control-plane call, as a TYPE rather than a sentence.

WHY THIS EXISTS. Call used to flatten every failure into a formatted string, and callers then recovered the classification by searching that string — `strings.Contains(err.Error(), "already_exists")`. That is wrong in both directions. A server that improves its prose breaks forge's control flow, which is a silent break: the branch simply stops matching and the error falls through to a generic failure. And forge's OWN message text contains the words it searches for, so a message that merely mentions a code reads as that code.

Worse, the structured half was discarded entirely. A promote refusal carries a DeployPromoteRefusal detail naming the promotion that actually landed — exactly what a pipeline needs to print instead of "someone else moved prod" — and flattening to a string threw it away.

So every primitive maps REASONS, not message text, to exit codes.

func (*Error) DetailJSON

func (e *Error) DetailJSON(typeName string) (json.RawMessage, bool)

DetailJSON returns the protojson payload of the first detail whose type matches typeName, and whether one was found.

Matches on the SUFFIX after the last '/' and accepts either a bare full name or a type URL, because a server may send "controlplane.v1.X" or "type.googleapis.com/controlplane.v1.X" and both name the same message.

A detail with no `debug` member is reported as not found: the base64 proto bytes alone are unreadable without the descriptor forge deliberately does not have, and returning them would hand a caller a payload it cannot use.

func (*Error) Error

func (e *Error) Error() string

func (*Error) HasCode

func (e *Error) HasCode(code string) bool

HasCode reports whether this failure carries the given Connect code. Use it instead of matching message text.

func (*Error) HasReason

func (e *Error) HasReason(reason string) bool

HasReason reports whether this failure carries the given domain reason.

func (*Error) MissingScope added in v1.0.0

func (e *Error) MissingScope() (string, bool)

MissingScope reports the scope a 403 says the credential lacks (the server's message names it as "<scope> scope"), so a caller can tell a credential that needs a scope from a request the server refused.

func (*Error) Unauthenticated added in v1.0.0

func (e *Error) Unauthenticated() bool

Unauthenticated reports whether the control plane refused the credential itself (HTTP 401), as opposed to refusing what it may do (403).

type ErrorDetail

type ErrorDetail struct {
	// Type is the protobuf full name, e.g.
	// "controlplane.v1.DeployPromoteRefusal".
	Type string `json:"type"`
	// Value is the base64 (raw std, unpadded) proto binary.
	Value string `json:"value"`
	// Debug is the same message as protojson, present when the server had
	// descriptors for it. This is the member a forge caller reads.
	Debug json.RawMessage `json:"debug,omitempty"`
}

ErrorDetail is one Connect error detail in its wire form.

Jump to

Keyboard shortcuts

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