wire

package
v0.1.7 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 1 Imported by: 0

Documentation

Overview

Package wire is the public, versioned contract for the curious.pub /v1 HTTP API. It contains ONLY request/response types, JSON tags, and error codes — no HTTP client, no CLI logic, no config loading. Both the `curious` CLI and the private `platform` control plane import this package, and platform's server implementation must conform to it.

The contract is additive-only within v1

Once a field, type, or error code in this package has shipped in a released CLI build, it may never be renamed, removed, retyped, or repurposed. New fields and new error codes may be added freely; nothing existing may change meaning underneath a client that is already in someone's hands.

This matters because the CLI is distributed as a compiled binary that users update on their own schedule (or not at all). A server-side change that quietly renames a JSON field or reinterprets an error code breaks every copy of the CLI still running the old contract, with no way for the server to know which clients are affected. Treating v1 as additive-only — and encoding that discipline via the golden tests in wire_test.go — is what lets the server evolve without a coordinated client release.

Unknown fields are IGNORED ON BOTH SIDES: clients tolerate newer servers, and servers tolerate newer clients. This is what makes additive evolution and rollback safe. Tolerance in one direction only is not a weaker version of the same property — it is a different and much worse one, because it turns every additive request field into a deployment-order obligation and makes rolling a server back a client-breaking act. TestForwardCompatibleDecoding pins the client half; the server half is the platform's own decoding discipline, and is stated here because this package is where both halves are agreed.

Enum vocabularies evolve additively too, and unknown members are RENDERED

Some exported types in this package (DeployStatus, Phase, and others this package will grow later) are closed string enums, not free-form data — but the additive-only rule applies to them exactly as it applies to fields: a new member may be added to a vocabulary at any time, and a client built before that member existed must keep working. The obligation this places on every consumer is stated once, here, because it is the same obligation for every such type: an unknown value is RENDERED, never switched on exhaustively. A switch that lists every currently-known value and panics or errors on the rest looks correct right up until the server ships a value the switch predates — and for an enum carried on a streamed event (DeployStatus's and Phase's home), that failure mode is the most tempting to write by accident, because an unhandled case in a progress display looks like nothing at all rather than like an error. DeployStatus and Phase each restate this in their own terms, next to their constants, because that is the one place a client author is certain to look; it is stated here as the general rule so a future enum in this package inherits it without having to say so again.

The deploy event stream has a GRAMMAR, and both sides are bound by it

GET /v1/deploys/{id}/events emits named events whose payloads are the types below. The sequence itself is contract, not an implementation detail, because a client must decide when to stop reading and a server must decide how to stop writing — and if that is left unstated, whatever the first server happens to do becomes binding on every client written against it, without anyone having decided it:

  • EXACTLY ONE `done` event terminates every stream, and the final DeployStatus is inside it. A stream that ends without `done` ended abnormally (a dropped connection, a crashed relay) and a client should treat it as such, not as a result.
  • `error` is DIAGNOSTIC, never terminal. It explains what went wrong; it does not end the stream. A failed build still ends with `done{status: "failed"}` — so a client's termination condition is "`done` arrived", never "`error` arrived".
  • NOTHING follows `done`.
  • An UNKNOWN event type is SKIPPED. This is the additive rule for the event vocabulary, the same shape as the render-unknown obligation DeployStatus and Phase carry: a server may add an event type at any time, and a client built before it existed must ignore it and keep reading rather than treat the stream as corrupt.

The grammar binds both directions symmetrically. The server may not end a stream with `error`, and a client may not treat one as the end.

Success is signalled by the HTTP status alone; error semantics live in the error envelope. No client behaviour may depend on the contents of a success body — several are empty objects, reserved for fields that may be added additively later.

If a breaking change is ever truly necessary, it belongs in a new version (v2), not a mutation of v1.

Index

Constants

View Source
const (
	// MaxPackedBytes bounds the PACKED tarball — the number
	// DeployCreateRequest.Bytes declares, and the one the server
	// validates that declaration against before any upload URL exists.
	//
	// Deliberately EQUAL to MaxSourceTotalBytes, which bounds the
	// UNCOMPRESSED tree. The two measure different things, so the equality
	// is a decision: one number for a user to remember, and no band
	// between two caps in which a deploy is refused for a reason nobody
	// can state simply.
	//
	// It is NOT a guarantee that anything passing the uncompressed cap
	// will pack within this one, and an earlier version of this comment
	// wrongly said so ("compression can only help"). Compression helps the
	// FILE DATA; the archive adds its own bytes. A tar entry costs a
	// 512-byte header plus padding to the next 512-byte boundary, so 3,000
	// incompressible 10,000-byte files — exactly 30,000,000 bytes of data,
	// inside every uncompressed limit — pack to 32,257,024 bytes before
	// gzip has added anything, and are refused here.
	//
	// That case is pathological (incompressible files, every one just past
	// a block boundary, at the exact cap) and the equality stands, because
	// the alternative is a second number that exists only to accommodate
	// it. But a client hitting it deserves a message naming the packed
	// size rather than one implying it should have fitted.
	MaxPackedBytes = 30_000_000

	MaxSourceFiles      = 3_000
	MaxSourceFileBytes  = 5_000_000
	MaxSourceTotalBytes = 30_000_000
	MaxOutputFiles      = 1_000
	MaxOutputTotalBytes = 30_000_000
)

The limit constants below are contract, not local policy: the MCP tool descriptions must state them, and the client and server must agree on the exact byte, because a client blocking at 30 MiB against a server enforcing 30 MB (decimal SI) is a class of bug nobody would find quickly. They are untyped so they compare and assign against either int or int64 call sites without a conversion.

The allowed-extension list deliberately stays out of this package: it is an enforcement policy the trial tier may tighten, and freezing it in an additive-only contract would convert a policy into a promise. It lives server-side.

Variables

AllDeployStatuses is every DeployStatus this contract defines, in declaration order — the lifecycle order, `built` between `building` and `live`. As with AllErrorCodes, this exists so neither side of the wire keeps a hand-written copy that can silently fall behind: the server ranges it to prove every status has a transition rule, the client ranges it to prove it renders every one. A new status is added to this slice in the same commit that declares the constant; the contract guards in this package fail a constant that is declared and not listed.

AllErrorCodes is every ErrorCode this contract defines, in declaration order. It exists so that both sides of the wire can range the full set instead of each keeping a hand-written copy that silently falls behind: a client can prove it handles every code it may receive, and the server can prove — in a test rather than a comment — that every code it may be asked to write has an HTTP status and a Retry-After decision. Without it, a code added here is invisible to the other side until it appears in a response nobody wrote a branch for.

A new code is added to this slice in the same commit that declares the constant; the contract guards in this package fail a constant that is declared and not listed. Callers must treat the slice as read-only — it is package-level state shared by every importer.

AllEventTypes is every EventType this contract defines, in the order a well-formed stream tends to produce them: log and phase interleave throughout, error may appear at any point, and done comes last exactly once. As with the other vocabularies here, it exists so neither side keeps a copy that can fall behind — a client can prove it handles (or deliberately skips) every type it may receive.

AllFailureOrigins lists every origin this contract defines, in the order above: unstated first, because it is the zero value and the case every consumer meets before any server sends anything else, then the attributions in the order they joined the contract.

JOINING ORDER rather than a semantic grouping, deliberately. Any grouping worth having ("who acts", "how bad") is a judgement that would move an existing entry the next time somebody disagreed with it, and the order is pinned by a test — so the rule that never argues with itself is the one where a new value only ever appends.

AllPhases is every Phase this contract defines, in declaration order — and that declaration order IS the documented lifecycle order. The server's exit test asserts that `phase` events arrive in this order and cites this slice directly; nothing else states the order, so the order and the statement of the order cannot drift apart. A new phase is added to this slice in the same commit that declares the constant, for the same reason as AllDeployStatuses and AllErrorCodes.

Functions

func CarriesRetryAfter

func CarriesRetryAfter(code ErrorCode) bool

CarriesRetryAfter reports whether an HTTP /v1 response using code always includes a Retry-After header. This is a property of the protocol, not of any one implementation: the server has no freedom to omit the header for these codes, and a client may rely on it being present.

HTTP RESPONSES ONLY, and the scope is deliberate. The same ErrorCode vocabulary also travels on the deploy event stream, where the `error` event reuses wire.Error precisely so a client can switch on the same values — and an SSE event has no headers. So a stream carrying rate_limited or capacity_closed cannot carry a Retry-After, and a client that took an unqualified "always" at its word would look for a header that cannot exist. Narrowing this promise is only additive BEFORE a released client relies on it, which is why it is being said now rather than when the first such event is emitted (raised by line review, 2026-08-17). What a client should do on the stream: treat these as retry-later without a stated time, exactly as it already must for maintenance and internal over HTTP.

The name says what the contract guarantees rather than what a caller should do about it. It is NOT a general "should I retry?" — maintenance and internal are both worth retrying later, and neither carries the header, because neither has a reset time the server can honestly name.

An unknown code reports false: a code this contract does not define carries no obligation, and inventing one for it would be a guess.

func StreamOnly added in v0.1.1

func StreamOnly(code ErrorCode) bool

StreamOnly reports whether code is carried only by the deploy event stream's `error` event and never by an HTTP /v1 response. A server holds itself to this: such a code has no HTTP status, and no HTTP path may write one. A client can rely on never meeting one in an HTTP error body.

An unknown code reports false: a code this contract does not define carries no obligation.

Types

type AuthStartRequest

type AuthStartRequest struct {
	Email string `json:"email"`
}

AuthStartRequest is the body of POST /v1/auth/start: the first step of the email + 6-digit-code login flow.

type AuthStartResponse

type AuthStartResponse struct{}

AuthStartResponse is the success body of POST /v1/auth/start: an empty JSON object, for the same reason as WaitlistResponse. Note that this response is deliberately identical whether or not a code was actually sent — the endpoint must not reveal whether an address is registered, rate-limited, or in cooldown — so there is nothing here for a client to branch on by design. Reserved for future additive fields.

type AuthVerifyRequest

type AuthVerifyRequest struct {
	Email          string `json:"email"`
	Code           string `json:"code"`
	MarketingOptIn bool   `json:"marketing_opt_in"`
}

AuthVerifyRequest is the body of POST /v1/auth/verify: the second step of the login flow, submitting the code sent to Email.

type AuthVerifyResponse

type AuthVerifyResponse struct {
	Token string `json:"token"`
}

AuthVerifyResponse is the response body of POST /v1/auth/verify, carrying the bearer token for subsequent requests.

type CapacityResponse

type CapacityResponse struct {
	Open         bool      `json:"open"`
	AccountsLeft int       `json:"accounts_left"`
	ResetsAt     time.Time `json:"resets_at"`
}

CapacityResponse is the body of GET /v1/capacity. Clients call this before any other work, and stop if Open is false, so that a user is never asked to do work that cannot land.

type DeployCreateRequest

type DeployCreateRequest struct {
	// Bytes is the size, in bytes, of the PACKED tarball — a stat of the
	// file just written to disk, not the uncompressed pre-flight total the
	// client-side limits measure (see MaxSourceTotalBytes below).
	// The two numbers are easy to confuse and this is deliberately the
	// compressed one: the server signs the presigned PUT with this value
	// as an exact Content-Length, so a client that sends the wrong number
	// gets every upload refused by the object store, not a soft warning.
	Bytes int64 `json:"bytes"`
}

DeployCreateRequest is the body of POST /v1/deploys.

type DeployCreateResponse

type DeployCreateResponse struct {
	DeployID  string    `json:"deploy_id"`
	UploadURL string    `json:"upload_url"`
	ExpiresAt time.Time `json:"expires_at"`
}

DeployCreateResponse is the success body of POST /v1/deploys.

There is deliberately no site_id field, in either direction. The data model already lets a site point at its current deploy, so redeploying an existing site is clearly intended eventually — but nothing yet knows whether the trial CLI can even name a site to redeploy, and there is no mechanism by which it would remember one. A field nothing populates is the worst kind to freeze into an additive-only contract: adding one later is always allowed, removing one never is.

~~UploadURL also carries no expiry field: the client learns the URL has expired by the PUT failing, which it must handle regardless, and a second representation of the same fact is a second thing to keep true.~~ CORRECTED 2026-09-08, and kept struck because the reasoning was half right and the half that was wrong is the interesting one.

The client does learn by the PUT failing. What it does NOT learn is WHICH failure it had. The server signs this URL with an exact Content-Length (see DeployCreateRequest.Bytes), so an object store answers 403 both for a link whose window has closed and for a body whose length does not match the signature — one is "wait and try again", the other is a client bug that will reproduce forever. A client holding only the status renders one of them and is right half the time; rendering "the upload link expired, run it again" at a size mismatch is worse than saying nothing, because it names the one cause that has been ruled out and sends a person round a loop with no exit.

The alternative was to read the store's error body. That is rejected: the response is the substrate's, not this contract's, and a public client that switches on a specific object store's error vocabulary has made the substrate part of the contract by accident. ExpiresAt is the same fact expressed in the one vocabulary both ends already share.

Like DeployPublishResponse.ExpiresAt, this is carried because a client CANNOT derive it: the window is measured from a signing the client never sees, and nothing it holds determines it.

It selects a SENTENCE, not an action

This package's rule is that no client behaviour may depend on the contents of a success body, and this field is inside it rather than an exception to it. Every value leads to the identical protocol action: PUT the tarball. What it decides is which sentence a person reads AFTER a failure the client was going to handle either way — the rule governs what a client DOES, not what it SAYS.

A client built before this field existed decodes the zero value and renders the honest ambiguous copy ("the store refused the upload"), which is what makes the addition additive in fact and not only in form. A zero ExpiresAt therefore means "not told", never "already expired", and a client must not read it as a deadline in the past.

type DeployPublishResponse

type DeployPublishResponse struct {
	Subdomain string    `json:"subdomain"`
	ExpiresAt time.Time `json:"expires_at"`
}

DeployPublishResponse is the success body of POST /v1/deploys/{id}/publish: the step that makes a built deploy answer at a URL.

It carries the SUBDOMAIN LABEL rather than an assembled URL, and the choice is worth stating because the sibling DeployCreateResponse goes the other way with UploadURL. The reason is where the base domain lives: the server has no representation of it at all today — no constant, no config key — so returning a URL would mean introducing one, and doing that inside a review fix is how a contract acquires a field nobody designed. The label is exactly what the server already holds.

A URL field may be added later; that is additive and allowed. Removing Subdomain would not be, which is why the smaller commitment is the one made first.

ExpiresAt is carried because a client CANNOT derive it: the window is the server's, measured from the publish that set it, and nothing the client can see determines it. Contrast DeployCreateResponse, which deliberately omits an expiry for UploadURL — there the client learns by the PUT failing, and a second representation of a fact it can discover is a second thing to keep true. Here there is no discovering it; there is only being told.

Per this package's own rule, no client behaviour may depend on these contents: they are shown to a person, not branched on.

type DeployStartResponse

type DeployStartResponse struct {
	Status DeployStatus `json:"status"`
}

DeployStartResponse is the success body of POST /v1/deploys/{id}/start.

Status INFORMS; it never BRANCHES

Status reports what the deploy is doing now, and a client takes the IDENTICAL action for every value it can carry: the build is under way, go read the event stream. No client behaviour may depend on which value arrived.

That distinction is the whole reason this field is safe to add. The endpoint answers 202 both when this call started the build and when it found that another call already had — a caller whose request timed out and retried learns its build is running, which is the thing it wanted to know, without the server inventing a second success shape or a conflict code for a difference the client cannot act on. A success body whose CONTENTS changed what a client does would be the thing this contract is shaped to prevent (see the package doc: no client behaviour may depend on the contents of a success body).

This type shipped as an empty object "reserved for fields that may be added additively later". This is the first draw on that reservation, and it cost one field and no released client.

type DeployStatus

type DeployStatus string

DeployStatus is the lifecycle state of a deploy, as recorded in the deploy record's status attribute. It is what the record says the deploy IS — contrast Phase, which is what the build pipeline is DOING. The two vocabularies share two spellings ("queued", "building") for related but distinct moments, and are declared as distinct Go types for exactly that reason: the compiler refuses a comparison between a DeployStatus and a Phase, even where nothing stops a human eye sliding from one to the other while reading a log.

An unknown status is rendered, never switched on exhaustively — see the package doc's enum-evolution rule. A CLI binary already in someone's hands must survive a status added to this vocabulary after it was built.

const (
	StatusQueued   DeployStatus = "queued"
	StatusBuilding DeployStatus = "building"
	// StatusBuilt means output validated and stored, with nothing serving
	// it STEADILY — not simply "nothing serving it". The publish step
	// writes the edge routing entry *before* flipping the status to live,
	// deliberately, so a crash between those two writes leaves a deploy
	// that is still `built` while its site has already started serving.
	// Read this before assuming `built` means "not yet reachable": for one
	// crash-shaped window, it does not. The exception is bounded and
	// self-healing, not a standing risk: the site's expiry is written
	// earlier still, so the residue cannot outlive it, and publish is
	// idempotent, so a retry always completes the transition.
	StatusBuilt DeployStatus = "built"
	// StatusLive is what a deploy's record says once the publish step has
	// made it the deploy its site serves, and a stream opened on a deploy
	// already published ends with a `done` event carrying it. It was
	// declared before anything produced it, deliberately: the vocabulary
	// ships complete, because the cost of a client never having to think
	// about a status lands in installed binaries, not compile errors — see
	// the package doc.
	StatusLive   DeployStatus = "live"
	StatusFailed DeployStatus = "failed"
)

type DoneEvent

type DoneEvent struct {
	Status DeployStatus `json:"status"`

	// Origin says WHOSE FAULT a failure was, and it exists because the
	// client could not tell and said something false.
	//
	// `status: "failed"` covers two different events — a build that ran
	// and failed, and a build that never started — and nothing on the
	// wire distinguished them. So the client's copy told a person that
	// what went wrong "is a problem in the project rather than in curious
	// or the service", on a deploy where the service had refused to
	// create the build session at all. Every clause of that was false,
	// and the person was sent to fix a project that was fine.
	//
	// IT IS A FIELD AND NOT A MESSAGE, deliberately. A client must be
	// able to branch on this without reading English, and a message that
	// happens to contain the word "service" is not something a program
	// can act on.
	//
	// IT IS MEANINGFUL ONLY WHEN Status IS StatusFailed. On any other
	// status there is no failure to attribute, and a server states
	// OriginUnstated there rather than inventing an attribution for a
	// build that succeeded.
	Origin FailureOrigin `json:"origin"`
}

DoneEvent is the data: payload of the SSE `done` event, marking the end of the stream. It carries only the final Status: url and expires_at are to be added later, additively, once the site is actually being served and there is something to make them true.

The SSE `error` event reuses the existing wire.Error rather than a dedicated type — one error shape on the wire, not two, so a client that already switches on ErrorCode for HTTP responses switches on the same values here.

type Error

type Error struct {
	Code    ErrorCode `json:"code"`
	Message string    `json:"message"`
}

Error is the machine-readable error carried in every non-2xx /v1 response, wrapped in ErrorResponse.

type ErrorCode

type ErrorCode string

ErrorCode identifies the kind of error returned by the /v1 API. This is the codes' only registry: consumers switch on these values, so they are contract in the same sense the JSON field names are. The server owns the mapping from code to HTTP status and must not define a second registry of its own.

const (
	CodeBadRequest     ErrorCode = "bad_request"
	CodeUnauthorized   ErrorCode = "unauthorized"
	CodeForbidden      ErrorCode = "forbidden"
	CodeNotFound       ErrorCode = "not_found"
	CodeRateLimited    ErrorCode = "rate_limited"
	CodeCapacityClosed ErrorCode = "capacity_closed"
	CodeMaintenance    ErrorCode = "maintenance"
	CodeInternal       ErrorCode = "internal"

	// CodeDeployFailed and CodeDeployNotReady both say a deploy cannot be
	// published, and they exist as two codes because a client must do
	// two different things about them.
	//
	// Publishing refuses anything not in the built state, and the state
	// it observed used to reach the client only inside the human message
	// — with bad_request as the code for every one of them. So a client
	// wanting to tell "this build failed, stop" from "this build has not
	// finished, wait" had to parse prose written for a person. That is
	// the same thing this contract already refuses to do with an object
	// store's error body, and refusing it here while doing it there
	// would have been a rule that only applies to other people's
	// substrates.
	//
	// A MESSAGE IS READ BY A PERSON; A CODE IS SWITCHED ON BY A CLIENT.
	// The distinction a client needs belongs on the side of that line
	// that is stable, and putting it there is what the additive-only
	// vocabulary is for.
	//
	// CodeDeployFailed is terminal: the build did not produce something
	// publishable and never will. Note that it can arrive AFTER a stream
	// has ended saying the build finished — the terminal event carries
	// the builder's own claim, and validation runs after it — so a client
	// seeing this has not been lied to twice, it has been told the second
	// half.
	//
	// CodeDeployNotReady is not terminal: the deploy is still queued or
	// building, and asking again later can succeed. It carries no
	// Retry-After, because the server has no honest figure for how long
	// a build has left.
	CodeDeployFailed   ErrorCode = "deploy_failed"
	CodeDeployNotReady ErrorCode = "deploy_not_ready"

	// CodeBuildFailed and CodeLimitReached are what the deploy event
	// stream's `error` event says when a build stops, and each agrees with
	// the FailureOrigin its terminal `done` event carries. There are three
	// origins and three codes: CodeInternal for the service, and these two
	// for the other two sides.
	//
	// They exist because one code for every stopped build said the wrong
	// thing two times in three. An error line reading `internal` beside a
	// done event reading `project` told a person the service had broken
	// when their own build had stopped it, and the right action (read the
	// log, fix the project) is the opposite of the one `internal` implies
	// (wait, try again).
	//
	// CodeBuildFailed means THE PROJECT'S OWN BUILD STOPPED: installing its
	// dependencies or building it failed, and the log is the explanation.
	// It never means the service failed.
	//
	// CodeLimitReached means A PLATFORM LIMIT STOPPED THE BUILD: today, the
	// time one build is allowed. The person can act, and trying again
	// unchanged will reach the same limit. That pair is OriginLimit's, and
	// this code carries it onto the error line.
	//
	// BOTH ARE STREAM-ONLY: see StreamOnly. No HTTP response carries them.
	CodeBuildFailed  ErrorCode = "build_failed"
	CodeLimitReached ErrorCode = "limit_reached"

	// CodeStoreFull means the store the platform publishes sites through
	// is full, so this deploy cannot be published now. It is the service's
	// to fix, and trying again will not help until someone has, so a
	// client should not advise it. It carries no Retry-After: a full store
	// is not a window that reopens at a time anyone can name.
	//
	// It is a code of its own because the answer it replaces, CodeInternal,
	// also carried "something went wrong, please try again", so the same
	// code carried opposite advice. A client could only tell them apart by
	// reading the message, which this contract refuses to make anyone do.
	CodeStoreFull ErrorCode = "store_full"
)

type ErrorResponse

type ErrorResponse struct {
	Error Error `json:"error"`
}

ErrorResponse is the JSON body of every non-2xx /v1 response.

type EventType

type EventType string

EventType is the NAME of an event on the deploy event stream — the SSE `event:` line, not its payload. These were contract strings living only in doc comments, hand-written on both sides of the wire, which is the "hand-written copy that silently falls behind" failure AllErrorCodes exists to prevent: a typo in either repo was uncheckable through this package.

An unknown event type is SKIPPED, per the stream grammar in the package doc. That is what makes adding an event type additive.

const (
	// EventLog carries one line of combined build output (LogEvent).
	EventLog EventType = "log"
	// EventPhase reports pipeline progress (PhaseEvent).
	EventPhase EventType = "phase"
	// EventDone terminates the stream exactly once (DoneEvent).
	EventDone EventType = "done"
	// EventError is diagnostic and never terminal; its payload is the
	// same Error the HTTP surface uses, so a client switches on the same
	// ErrorCode values in both places.
	EventError EventType = "error"
)

type FailureOrigin added in v0.1.1

type FailureOrigin string

FailureOrigin attributes a failed build to the side that caused it.

The zero value is UNSTATED, and that is what makes this addable

This contract is additive-only, so a server that predates this field sends no `origin` key and a client decodes the zero value. That value is OriginUnstated, whose meaning is exactly "nobody said" — which is the truth in that case, and is why no separate presence flag is needed and why this field carries no `omitempty` (banned in this package: a key that vanishes at its zero value is a key no fixture pins).

The unknown branch is not optional

A consumer MUST have a branch for a value it does not recognise, and that branch must claim NEITHER side. This is the render-unknown obligation stated at the top of this package, and it bites harder here than for DeployStatus or Phase: the values are accusations. A client that treats an unrecognised origin as the project's fault repeats the original defect against every origin added after it shipped, and one that treats it as the service's blames an outage for a broken build.

OriginLimit is what that obligation looks like when it is paid: it was added after the first client shipped, and every such client renders it through the unknown branch — accusing nobody, which is worse copy than the limit branch and is not a false statement.

OriginUnstated and an unrecognised value are deliberately the SAME branch rather than two. Both mean the client does not know whose fault it was, and a client cannot tell "an old server said nothing" from "a new server said something I predate" in any way that changes what it should tell a person.

const (
	// OriginUnstated is the zero value: no attribution was made. It is
	// what an older server's absent key decodes to, and what a server
	// sends on a status that is not a failure.
	OriginUnstated FailureOrigin = ""

	// OriginProject means the project caused it: a non-zero build, a
	// missing dependency, a broken config. The log the person just
	// watched is the explanation, and acting on it is what fixes this.
	OriginProject FailureOrigin = "project"

	// OriginService means WE caused it: a build session that could not be
	// created, an image that could not be fetched, a refused call. There
	// may be NO LOG AT ALL, so copy for this origin may not send anyone
	// to read one.
	OriginService FailureOrigin = "service"

	// OriginLimit means a PLATFORM LIMIT was reached — today, the wall
	// clock a single build is allowed.
	//
	// IT IS A THIRD ORIGIN BECAUSE IT IS NEITHER OF THE OTHER TWO, and
	// filing it under either would produce advice that does not work.
	// As the service's, the copy says to wait and try again unchanged —
	// and the same build will reach the same limit every time. As the
	// project's, it says to fix what the log reports — and the log may
	// contain no error at all, because nothing failed; the build was
	// still going when the clock ran out.
	//
	// What is true of it is the pair: the person CAN act, and retrying
	// unchanged will not help. That pair is the whole reason this value
	// exists, and copy for it has to carry both halves.
	OriginLimit FailureOrigin = "limit"
)

type HealthResponse added in v0.1.1

type HealthResponse struct {
	UptimeSeconds  int    `json:"uptime_seconds"`
	ArtefactSHA256 string `json:"artefact_sha256"`
	DeployedCommit string `json:"deployed_commit"`
}

HealthResponse is the body of GET /v1/health: a LIVENESS answer, and deliberately nothing else. The handler reads no datastore and reaches no other service, so a 200 means "this process is up and serving requests" and never "its dependencies are healthy". Those are two different questions, and one endpoint answering both can only ever answer the weaker one — while reporting the stronger.

This route is UNAUTHENTICATED, because a liveness check that needs a credential cannot be used by the thing that starts the process, which is the first caller that needs it. It carries nothing a stranger could not already infer from the service answering at all.

No client behaviour depends on this type, and the CLI never calls this route. It is here because the contract is defined once, in this package, for whoever does call it — a supervisor deciding whether to proceed, or a person establishing that a port which accepts connections is attached to a server that answers them.

Both string fields may be empty, and empty is not an error

ArtefactSHA256 is what the running binary computes of ITSELF at startup: its own identity rather than an assertion about it, which is the one form of this fact that cannot disagree with reality. A process that cannot read its own file reports nothing here and still serves — a liveness endpoint that failed on self-inspection would not be a liveness endpoint.

DeployedCommit is CONFIGURATION, and the field name says so rather than implying the binary knows. Whatever installed this binary is the only thing that knows which source revision produced it, so the value is exactly as trustworthy as that installation and no more. It is not compiled in: stamping a revision at build time would make the binary's bytes depend on where it came from, and the build that produces it is pinned precisely so its output is reproducible from its inputs alone. Empty means nobody told this process, which is a fact worth reporting and not a reason to refuse.

UptimeSeconds is measured from one reading taken at startup. It is a count rather than a timestamp deliberately: at zero it reads as zero, where an unset instant renders as a date in year one that every reader has to decode before dismissing.

type LogEvent

type LogEvent struct {
	Line string `json:"line"`
}

LogEvent is the data: payload of the SSE `log` event on GET /v1/deploys/{id}/events. It carries the line and nothing else: the stream is combined stdout/stderr by design, so there is no separate stream to name, and a timestamp belongs to the persisted log object, which is server-side plain text. Both are addable later if a client ever needs them.

type Phase

type Phase string

Phase is a label for what the build pipeline is DOING right now, emitted on the `phase` SSE event — contrast DeployStatus, which is what the deploy record IS. "queued" and "building" appear in both vocabularies with related but distinct meanings, and Phase is a distinct Go type for exactly the reason DeployStatus is: the compiler refuses a comparison between them, which the reader has no compiler to check while staring at a log line.

Only the middle four constants below are emitted today: the build agent's own four internal phase names — extracting, installing, building, uploading — are translated onto them by a single server-side function, so an internal rename never reaches this public contract. Queued, starting and publishing have no producer yet. They ship in the vocabulary for the reason PhasePublishing gives, and a stream that never sends them is a correct stream.

An unknown phase is rendered as generic progress, never switched on exhaustively — see the package doc's enum-evolution rule, restated here in Phase's own terms because a stream is where a client is most tempted to switch exhaustively.

const (
	PhaseQueued     Phase = "queued"
	PhaseStarting   Phase = "starting"
	PhaseExtracting Phase = "extracting"
	PhaseInstalling Phase = "installing"
	PhaseBuilding   Phase = "building"
	PhaseUploading  Phase = "uploading"
	// PhasePublishing ships with no producer yet, deliberately, for the
	// same reason StatusLive does: the publish step is what will emit it,
	// and shipping the constant now costs one line while shipping it late
	// would cost every binary already in a user's hands.
	PhasePublishing Phase = "publishing"
)

type PhaseEvent

type PhaseEvent struct {
	Phase Phase `json:"phase"`
}

PhaseEvent is the data: payload of the SSE `phase` event. Phases are progress labels, not control flow — see Phase's doc comment for the render-unknown obligation this event carries.

type WaitlistRequest

type WaitlistRequest struct {
	Email string `json:"email"`
}

WaitlistRequest is the body of POST /v1/waitlist.

type WaitlistResponse

type WaitlistResponse struct{}

WaitlistResponse is the success body of POST /v1/waitlist: an empty JSON object. It is deliberately empty — success is the HTTP status, and a per-endpoint constant string would carry no information a caller doesn't already have. Reserved for future additive fields.

Jump to

Keyboard shortcuts

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