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
- Variables
- func ClaimableActive() bool
- func ClaimableClientIDFor(project, env string) string
- func ClaimableDeviceID() (string, error)
- func CredentialFromToken(tok *oauth2.Token, now time.Time) credentials.Credential
- func CredentialsPath() (string, error)
- func DeviceLabel() string
- func ForgetClaimable(ep Endpoint, project, env string) error
- func LoadClaimable(ep Endpoint, project, env string, now time.Time) (credentials.Credential, error)
- func LoginScopes() []string
- func OpenBrowser(target string) error
- func OrganizationReply(orgID string) map[string]any
- func RecordClaimableURL(ep Endpoint, project, env, url string) error
- func ResolveOrganization(ctx context.Context, c Caller) (string, error)
- func RevokeIdentity(ctx context.Context, ep Endpoint, token string) error
- func SignInHint() string
- func SolvePoW(ctx context.Context, ch ClaimableChallenge) (string, error)
- func StoreClaimable(ep Endpoint, project, env string, d ClaimableDeploy, now time.Time) (credentials.Credential, error)
- func TokenIdentity(token string) string
- func UseClaimableCredential(ep Endpoint, token string, expiresAt *time.Time) (restore func())
- func VerifyToken(ctx context.Context, ep Endpoint, cred Credential) error
- type BrowserLogin
- type Caller
- type ClaimableAPI
- type ClaimableAvailability
- type ClaimableCause
- type ClaimableChallenge
- type ClaimableClient
- type ClaimableDeploy
- type ClaimableLimits
- type ClaimableStartRequest
- type ClaimableThrottle
- type Client
- type Credential
- type CredentialSource
- type Declaration
- type Endpoint
- type Error
- type ErrorDetail
Constants ¶
const ( ReasonClaimableThrottled = "claimable_throttled" 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.
const ( ProcStartClaimableDeploy = "controlplane.v1.ClaimableService/StartClaimableDeploy" ProcGetClaimableChallenge = "controlplane.v1.ClaimableService/GetClaimableChallenge" ProcGetClaimableAvailability = "controlplane.v1.ClaimableService/GetClaimableAvailability" )
The ClaimableService procedures forge calls.
const ( ClaimableCauseDisabled = "disabled" 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).
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" // 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.
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" 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.
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.
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).
const ClientID = "forge-cli"
ClientID is forge's OAuth client id at a control plane.
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".
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.
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.
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.
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.
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.
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.
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.
const VersionHeader = "Forge-Version"
VersionHeader is the request header this client names its forge release in, on every control-plane call.
Variables ¶
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
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
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 ¶
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
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 ¶
OpenBrowser launches the platform's URL handler.
func OrganizationReply ¶ added in v1.0.0
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
RecordClaimableURL remembers the deploy's live address beside its token, so `forge env status --claimable` can print it again without a credential.
func ResolveOrganization ¶
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
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
TokenIdentity is Credential.Identity for a bare token string.
func UseClaimableCredential ¶ added in v1.0.0
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 ¶
func (b BrowserLogin) Run(ctx context.Context) (credentials.Credential, error)
Run performs the flow and returns the credential to store.
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
func (c *ClaimableClient) Challenge(ctx context.Context) (ClaimableChallenge, error)
Challenge fetches a fresh proof-of-work puzzle.
func (*ClaimableClient) Start ¶ added in v1.0.0
func (c *ClaimableClient) Start(ctx context.Context, req ClaimableStartRequest) (ClaimableDeploy, error)
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 ¶
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
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:
- flagToken — an explicit --token on the command line
- os.Getenv(ep.TokenEnv) — the env var the environment DECLARED
- the credentials file — what `forge login` stored FOR ep.URL
- 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 ¶
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) HasCode ¶
HasCode reports whether this failure carries the given Connect code. Use it instead of matching message text.
func (*Error) MissingScope ¶ added in v1.0.0
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
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.