api

package
v0.8.2 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package api talks to the krowk artifact registry.

Uploading is three calls, because the bytes never pass through the registry:

  1. POST /v1/artifacts declare the file, get a presigned PUT
  2. PUT <presigned url> bytes go straight to object storage
  3. PUT /v1/artifacts/{slug}/finalization the registry verifies what landed

Only the first and third are ours; the second is object storage, signed for exactly the size, content type and checksum declared in step one. That is why the size and digest are computed up front rather than discovered while streaming: they are part of what gets signed, so they have to be known before the first call is made.

A fourth call recovers step two rather than repeating step one:

POST /v1/artifacts/{slug}/upload            mint the presigned PUT again

A signature is good for 15 minutes and the artifact waits far longer, so an upload can reach storage with one that has already lapsed. Declaring the file again would work and is the wrong answer — a second declare is a second slug, and the first link is already pasted somewhere.

The registry's API is resourceful all the way down, so what would be a verb hanging off an artifact is a nested resource instead — the finalization of an artifact, the claim on one, the run it belongs to, the completion of a run. The verb follows from whether the call can be repeated: finalizing, completing and setting an artifact's run are idempotent, so they are PUTs; claiming spends a one-shot token, so it is a POST. Checking a key is the same rule read the other way — GET /v1/key names the key this request is made with, because asking what it may do changes nothing about it.

Index

Constants

View Source
const (
	// DefaultBaseURL is overridden by KROWK_API_URL.
	DefaultBaseURL = "https://api.krowk.com/v1"
	// DevBaseURL is where the local stand-in registry (`go run
	// ./internal/devregistry`) listens, and what --dev points at, so testing
	// against a local registry needs no environment plumbing.
	DevBaseURL = "http://localhost:8787/v1"
)
View Source
const (
	AuthorizationPending  = "pending"
	AuthorizationApproved = "approved"
	AuthorizationDenied   = "denied"
)

The states an authorization reports while it still exists. Spent and expired are not among them: both are gone, and the registry says so with a 410 rather than with a body, the way it does for an artifact that has lapsed.

View Source
const (
	TokenSourceEnv  = "KROWK_TOKEN"
	TokenSourceFile = "credentials file"
	TokenSourceNone = "none"
)

Where ReadToken would take its token from, as reported by TokenSource.

View Source
const (
	KindArtifact = "art"
	KindRun      = "run"
)

Slug kinds, spelled as the registry prefixes them. A kind is what a command is asking for: `uploads show` wants an artifact and `runs show` wants a run, and a link that carries the wrong one is worth saying so about rather than sending on to be answered as a 404.

Variables

This section is empty.

Functions

func AdoptIdentity added in v0.2.1

func AdoptIdentity(token string, id Identity) (changed bool, err error)

AdoptIdentity re-files a stored token under the identity the registry has now vouched for. It finds whichever entry holds exactly this token, moves it under id.Workspace with the identity recorded, replaces any entry already filed there — a key the registry vouches for outranks whatever stale record held the name — points the default at it when the default pointed at the entry that moved, and reports whether anything changed.

This is how an offline login gets healed. A `--token` login the registry could not reach files under "default" with no identity at all, and a repo that pins its own workspace then finds no key there and tells the user to log in — the command they just ran. `krowk auth verify` is the moment the registry finally answers, so it is the moment the store can be told what the key really was.

A token no entry holds changes nothing: it came from the environment or was never stored, and neither is this store's business to rewrite. The read is the strict one, because healing a store is no excuse for overwriting one that could not be read.

func BaseURLFor

func BaseURLFor(dev bool, env func(string) string) string

BaseURLFor picks which registry to talk to. dev is an explicit request — a command-line flag — so it wins over an ambient environment variable; KROWK_API_URL then beats KROWK_DEV because it names a specific target.

func ContentType

func ContentType(path string) string

ContentType guesses from the extension, without the charset parameter: the type is signed into the upload URL and stored on the artifact, so the shortest accurate string is the one worth committing to.

func CredentialsPath

func CredentialsPath() string

CredentialsPath is ~/.config/krowk/credentials.json, XDG_CONFIG_HOME honoured. Not os.UserConfigDir: that is ~/Library/Application Support on macOS, and the CLI documents one path on every platform.

func KeyRejected

func KeyRejected(err error) bool

KeyRejected reports whether an error from VerifyKey is the registry saying it will not accept this key, as opposed to not managing to answer at all.

The distinction is the whole of `auth login`'s judgement. A 401 is a verdict on the key itself and there is nothing to retry — the token is wrong now and will be wrong later. Anything else, from no network to a 503 to something that is not a registry answering, is a verdict on the moment, and the key may well be fine. Only the first is grounds for refusing to store it.

func ParseSlug added in v0.5.0

func ParseSlug(kind, input string) (string, error)

ParseSlug takes what somebody typed where a slug belongs — the slug itself, or a link that carries one — and answers with the slug.

The pitch is the pasted link: what an agent and a person both hold after an upload is `krowk.com/a/art_…`, or the CDN URL under it, not a bare slug they would have to cut out by hand. Taking the link is the whole leniency, and it is additive: anything that is not link-shaped comes back untouched, so a slug the registry would have accepted before is still passed on verbatim — this function is not a validator, and the registry stays the authority on what a real slug is.

A link that carries no slug of the wanted kind is refused here rather than sent on, because the registry can only answer "no such record" about it, which reads as a slug that expired rather than as a URL in the wrong place.

func ReadToken

func ReadToken(env func(string) string, workspace string) string

ReadToken prefers KROWK_TOKEN so CI never has to write a file. An empty workspace takes the store's default entry; a name takes that entry, and comes back empty when no key is stored under it.

func ResolveToken added in v0.2.1

func ResolveToken(env func(string) string, workspace string) (string, error)

ResolveToken answers which token a command run against workspace should send, applying the whole policy in one place: KROWK_TOKEN wins over everything; a workspace named but holding no key is a refusal, not an anonymous fallback; and "" means the store's default — where a default pointer naming a missing entry is also a refusal, because a store that SAYS which key to use and cannot produce it must not quietly produce none. No workspace named and nothing stored is the anonymous case: ("", nil), which is how uploading without an account keeps working.

Every consumer that has to decide between authenticating, refusing and going anonymous calls this — the CLI's client, `auth token`, the MCP server — so that the three of them cannot drift into three different answers about the same store. ReadToken above stays the lenient low-level read and reports a missing key as an empty string; a caller that wants any of the refusals above must come through here, because ReadToken cannot tell them apart.

func RetryAfterFor

func RetryAfterFor(err error, now time.Time) (time.Duration, bool)

RetryAfterFor reads the wait a failure asked for, when it asked for one.

It exists for a caller pacing a loop of its own — `auth login` polling an authorization — which would otherwise come straight back after its own interval having just been told to slow down. Parsed and capped exactly as the retry loop inside a single call parses it, so there is one policy about that header rather than two.

func SaveCredentials

func SaveCredentials(token string, id Identity) (string, error)

SaveCredentials stores the token under the workspace the registry named for it — or under "default" when the registry named none — points the default at it, and returns where the store landed.

Every other stored key is left exactly as it was. That is the whole point: logging in to a second workspace used to cost you the first, and adding a key should never be a way to lose one.

Storing under a name that already holds a key replaces it. A second key for the same workspace is a re-login, and the token it replaces is very likely the one that was just revoked to make it — keeping it would leave a dead key in the store for someone to be confused by later.

The fresh key becomes the default because logging in is how a person says which workspace they mean to be working in now. Anything else would have the login appear to do nothing.

A store that is there but cannot be read fails the login instead of replacing it. Losing every other key is a far worse outcome than a login someone can simply run again once they have looked at the file the error names.

func SetDefaultWorkspace added in v0.2.0

func SetDefaultWorkspace(name string) (string, error)

SetDefaultWorkspace repoints the default at a key that is already stored, and says which path it wrote so the caller can name it.

Naming an entry that is not there is an error, and no write happens. A pointer at nothing would make every later command report "not logged in" while the keys are all still sitting in the file — a confusing way to fail, and one the user could not connect to the command that caused it. The error lists what is stored instead, because the usual cause is a typo or a half-remembered name and the answer is right there.

Like a login, it refuses outright when the store is there but unreadable, rather than repointing a default in a file it would write with one entry in place of everything the user has.

func TokenSource

func TokenSource(env func(string) string, workspace string) string

TokenSource names where ReadToken just got its token, so diagnostics can say which of the two a surprising key came from. Answering "no key" is a source too — it is the difference between anonymous by choice and a login that never landed. The workspace means what it means for ReadToken, so that the two always describe the same lookup.

func Truthy

func Truthy(v string) bool

Truthy reads the spellings people actually type into an environment variable.

Types

type Artifact

type Artifact struct {
	Slug        string       `json:"slug"`
	State       string       `json:"state"`
	Filename    string       `json:"filename"`
	ContentType string       `json:"content_type"`
	ByteSize    int64        `json:"byte_size"`
	Checksum    string       `json:"checksum,omitempty"`
	Region      string       `json:"region,omitempty"`
	Run         *ArtifactRun `json:"run,omitempty"`
	// URL is the card page, krowk.com/a/{slug}: the link to paste. It is what
	// unfurls into a preview card, and it stays a link to the artifact rather
	// than to whatever object storage happens to be serving the bytes.
	URL string `json:"url"`
	// FileURL is the public byte URL on the CDN — what `url` used to be. Only
	// an image embed should be built from it, because a paste destination
	// renders an image only where the link resolves to image bytes; everything
	// a person or an agent is handed points at the card instead.
	FileURL  string `json:"file_url,omitempty"`
	Markdown string `json:"markdown,omitempty"`
	// Paste is this artifact in the forms its destinations need, computed by
	// the registry. It is served rather than assembled so that how a krowk
	// reference looks is one deploy away from changing everywhere, including in
	// the clients already installed. Nil where the registry does not send it,
	// which is what every reader here has to be ready for.
	Paste     *Paste `json:"paste,omitempty"`
	ExpiresAt string `json:"expires_at,omitempty"`
	CreatedAt string `json:"created_at,omitempty"`
	// Metadata is the artifact's own production record — stamped at the moment
	// the file was pushed, so it travels with the artifact wherever it is
	// claimed or attached. Metadata is public: the card page is keyless.
	Metadata json.RawMessage `json:"metadata,omitempty"`

	// Upload and NextStep only ever appear on the create response.
	Upload   *Upload `json:"upload,omitempty"`
	NextStep string  `json:"next_step,omitempty"`

	// ClaimToken is shown exactly once, by the call that created an anonymous
	// artifact. It is the only way to keep that artifact past its expiry, so it
	// is carried through to the output rather than dropped here.
	ClaimToken string `json:"claim_token,omitempty"`
}

Artifact is one stored file, as the registry reports it.

func (*Artifact) RunSlug

func (a *Artifact) RunSlug() string

RunSlug names the run this artifact belongs to, or "" when it belongs to none. Everything that only wants the slug goes through here, so a nil run reads as absent rather than panicking on the way to finding that out.

type ArtifactRun

type ArtifactRun struct {
	Slug      string          `json:"slug"`
	Metadata  json.RawMessage `json:"metadata,omitempty"`
	CreatedAt string          `json:"created_at,omitempty"`
}

ArtifactRun is the run an artifact belongs to, as the artifact reports it: a nested object, not the bare slug it used to be. The run's metadata comes along, so a read that wants to know what produced the file needs no second call.

A pointer on Artifact, because belonging to no run is a real state — a keyless upload never has one — and null is how the registry says it.

type CLIAuthorization

type CLIAuthorization struct {
	Slug  string `json:"slug"`
	State string `json:"state"`
	// Code is the short pair a person matches against the terminal, so an
	// authorization opened by something else cannot be approved by mistake. Its
	// alphabet leaves out 0/O and 1/I, which is the pair a read-aloud code gets
	// wrong.
	Code string `json:"code,omitempty"`
	// VerificationURL is the page that shows the code and mints the key. It
	// arrives in a response body and is about to be handed to the desktop's URL
	// handler, so the CLI judges it before opening it.
	VerificationURL string `json:"verification_url,omitempty"`
	// Interval is how many seconds to wait between polls. The registry sets the
	// pace, because it is the side that knows what its rate limit allows.
	Interval  int    `json:"interval,omitempty"`
	ExpiresAt string `json:"expires_at,omitempty"`

	// Token, KeyID and Workspace are the key itself, and appear on the one read
	// that finds the authorization approved. The plaintext is gone from the
	// registry after that read, so a second one answers 410 `spent`.
	Token     string `json:"token,omitempty"`
	KeyID     string `json:"key_id,omitempty"`
	Workspace string `json:"workspace,omitempty"`
	// WorkspaceName travels with the grant for the same reason /key sends it:
	// what login records here is all a picker ever has to show a person.
	WorkspaceName string `json:"workspace_name,omitempty"`
}

CLIAuthorization is one browser login in progress: the CLI asks the registry for one, a person approves it on the app surface, and the key that approval mints is collected from here.

Two capabilities, deliberately split. Slug is what collects the key and is never shown in the browser; Code is what a person reads and can only approve or deny. Knowing the code must therefore never yield the key — which is why the poll is by slug and the page is by code.

type Client

type Client struct {
	BaseURL string
	Token   string
	HTTP    *http.Client
	// Sleep is swapped out in tests so backoff does not cost wall clock.
	Sleep func(time.Duration)
}

Client is safe for a single CLI invocation; it holds no state between calls.

func New

func New(baseURL, token string) *Client

New builds a client against baseURL, falling back to the public registry.

func (*Client) AttachRun

func (c *Client) AttachRun(ctx context.Context, artifactSlug, runSlug string) (*Artifact, error)

AttachRun puts an artifact under a run after it was uploaded, which is the only way an upload that started out anonymous ever gets one: a keyless upload cannot name a run at create time, and claiming it does not give it one.

A PUT for the same reason finalizing is one: the artifact ends up under the same run however many times it is asked for, so a retry is a success rather than an error. An artifact belongs to one run, so naming a different one moves it. Both slugs resolve in the key's workspace, so an artifact has to be claimed before it can be attached.

func (*Client) Authenticated

func (c *Client) Authenticated() bool

Authenticated reports whether calls will carry a key. Runs — and so all run metadata — are only available to a keyed client.

func (*Client) ClaimArtifact

func (c *Client) ClaimArtifact(ctx context.Context, slug, claimToken string) (*Artifact, error)

ClaimArtifact spends a claim token to move an anonymous artifact into the key's workspace, where it stops expiring.

func (*Client) CreateRun

func (c *Client) CreateRun(ctx context.Context, metadata any) (*Run, error)

CreateRun opens a run to hang artifacts off. Needs a key: a run belongs to a workspace, and a keyless upload has none.

Retried, now that an Idempotency-Key makes it safe to. It used to be sent once and only once: a run committed under a lost response would be duplicated by the retry, an orphan whose slug never surfaces anywhere, and that was worse than failing the push outright. The key removes the choice — the retry is answered with the run the first attempt opened — so a transient 502 on the way to opening a run no longer costs the whole upload.

func (*Client) FinalizeArtifact

func (c *Client) FinalizeArtifact(ctx context.Context, slug string) (*Artifact, error)

FinalizeArtifact confirms the upload landed. A PUT because it is idempotent: the artifact ends up in the same state however many times it is asked for, so a retry is a success rather than an error.

func (*Client) FinishRun

func (c *Client) FinishRun(ctx context.Context, slug string) (*Run, error)

FinishRun closes a run. A PUT for the same reason finalizing is one: a CI cleanup step that runs twice should get the same success both times, and the run keeps the moment it first finished.

func (*Client) Insecure

func (c *Client) Insecure() bool

Insecure reports whether this registry is reached over plaintext http, which is something the caller chose: a local registry, or a self-hosted one inside a private network.

It lives here rather than wherever the question gets asked, because every other judgement about the base URL — whether it is local, whether a redirect stayed on its origin, which schemes a storage host may use — is made here too, against this same field. A second parse somewhere else is a second home for the same policy.

func (*Client) ListArtifacts

func (c *Client) ListArtifacts(ctx context.Context, before string, limit int) (*Page, error)

ListArtifacts reads a page of the key's workspace. Unlike the rest of the artifact endpoints this one needs a key: keyless requests all share the anonymous workspace, so listing it would show everyone's uploads.

before is the cursor from a previous page's Next; limit is clamped by the registry rather than here, so asking for more than it serves gets the most it serves.

func (*Client) ListRunArtifacts

func (c *Client) ListRunArtifacts(ctx context.Context, runSlug, before string, limit int) (*Page, error)

ListRunArtifacts reads a page of what one run produced.

A collection of the run rather than a filter on the workspace listing, which is the registry's shape and worth keeping: the run is looked up first, so an unknown slug is a 404. A filter would answer an empty page instead, and a caller cannot tell that apart from a run that genuinely produced nothing.

func (*Client) ListRuns

func (c *Client) ListRuns(ctx context.Context, before string, limit int) (*RunPage, error)

ListRuns reads a page of the key's runs. Needs a key: a run belongs to a workspace, and a keyless caller has none of its own.

func (*Client) PrepareArtifact

func (c *Client) PrepareArtifact(ctx context.Context, spec Spec) (*Artifact, error)

PrepareArtifact records the artifact and returns the presigned upload.

Named with an Idempotency-Key, because this is the call a lost response makes expensive: without one a retry declares a second artifact, the first is left to expire, and both count as uploads. With one, the retry is answered with the artifact the first attempt already made.

func (*Client) PresignUpload

func (c *Client) PresignUpload(ctx context.Context, slug, claimToken string) (*Artifact, error)

PresignUpload mints the upload of an artifact again — a fresh presigned PUT over the same slug, the same storage key and the same declared size and digest, so the link that may already be pasted somewhere is the one the bytes land behind.

It exists because two clocks disagree. A presigned URL is good for 15 minutes, while an artifact waits for its bytes with no deadline shorter than a day, so a client that digests a large file, meets a slow network or retries after a crash can arrive at the PUT with a signature that has already lapsed. Declaring the file again would also produce a working URL, and it is the wrong answer: a second declare is a second slug, and the first one is dead in whatever it was pasted into.

A POST though nothing is recorded — the one place the verb does not follow the record. What comes back is a new capability, permission to write these bytes for another 15 minutes, and asking for it means presenting a credential.

Which credential depends on the artifact. A key's authority is the workspace it acts in, so a keyed caller needs nothing else. A keyless caller's is the claim token, and the slug will not do: a slug travels in whatever the link was pasted into, and a reader of a link must not be able to decide what they are reading. It costs the honest caller nothing — the token came back in the same response as the slug and the URL being replaced.

func (*Client) Push

func (c *Client) Push(ctx context.Context, spec Spec) (*Artifact, error)

Push runs the whole upload for one file: declare, send the bytes, finalize.

The finalized artifact is what comes back, but the claim token only ever appears on the create response — so it is carried across, because losing it means an anonymous upload can never be kept.

func (*Client) PutBytes

func (c *Client) PutBytes(ctx context.Context, prepared *Artifact, spec Spec) error

PutBytes streams the file to object storage using exactly the headers the URL was signed for. Anything else — an unsigned header, a different length — and the signature no longer matches what arrives.

The presign is treated as perishable rather than as a fixed address. It is good for 15 minutes and the artifact it belongs to waits far longer, so an upload can meet a URL whose window has already closed — and retrying a dead URL is three attempts spent proving it is dead, ending in advice to push again, which mints a second slug and kills the first link. Where a failure looks like that, a fresh presign is fetched between attempts and the bytes are sent again: same artifact, same slug, same storage key, so nothing about where the link points moves.

func (*Client) ReadCLIAuthorization

func (c *Client) ReadCLIAuthorization(ctx context.Context, slug string) (*CLIAuthorization, error)

ReadCLIAuthorization reads one back: still pending, approved — and carrying the key, exactly once — or denied. A 410 means it is gone, spent or expired.

Keyless for the same reason the create is: the authority for this call is the slug, which is a capability the caller already holds, and a stale key in the environment has no business deciding whether a login can finish.

func (*Client) Root

func (c *Client) Root(ctx context.Context) (*Service, error)

Root reads the service descriptor. It is the one endpoint needing neither a key nor a payload, so it is what a reachability check should ask for.

func (*Client) ShowArtifact

func (c *Client) ShowArtifact(ctx context.Context, slug string) (*Artifact, error)

ShowArtifact reads one artifact back. Works without a key: for a keyless request the slug is the capability, and it resolves within the anonymous workspace.

func (*Client) ShowRun

func (c *Client) ShowRun(ctx context.Context, slug string) (*Run, error)

ShowRun reads one run back — its status, when it started and finished, and the metadata recorded on it, which is where everything about an upload's origin lives since the registry keeps none on the artifact itself.

func (*Client) StartCLIAuthorization

func (c *Client) StartCLIAuthorization(ctx context.Context) (*CLIAuthorization, error)

StartCLIAuthorization opens a browser login and returns the code to show, the page to open, and the slug to collect the key from.

Retried like any other call, and without an Idempotency-Key — the one create in this API that needs none. A lost response means the caller never saw the code, so the authorization it belongs to can never be approved: it costs no row worth deduplicating, reserves no storage, charges nothing, and lapses within the quarter hour on its own.

Call it on a keyless client. The endpoint exists for a machine that has no key and takes none, and sending one would have the registry meter the request as that key's rather than as this address's — so logging in again somewhere already logged in would be counted against the key being replaced.

func (*Client) TakeDownArtifact

func (c *Client) TakeDownArtifact(ctx context.Context, slug, claimToken string) error

TakeDownArtifact removes an artifact's bytes and leaves a tombstone behind, so the link answers 410 rather than 404 — it is already pasted somewhere, and its reader deserves to be told the artifact was removed rather than sent hunting for a typo.

Immediate and unrecoverable, which is the point rather than a limitation. This is what someone reaches for when a secret was uploaded by accident, and a secret that can be restored is still leaked — so nothing here routes through a window that could hand the bytes back.

Two authorities take an artifact down, and which one is used decides how the request is made. A key's authority is the workspace it acts in. A claim token's is the one artifact it was issued for, and it is needed at all because a slug travels in whatever the link was pasted into: authorising a takedown by slug alone would let every reader of the paste destroy what they read.

Nothing comes back but a 204. There is no artifact left to report, and a url and markdown naming bytes that are gone would be a lie.

Retried like any other call: taking down what is already down is a success on both sides, so a lost response costs nothing to ask again.

func (*Client) VerifyKey

func (c *Client) VerifyKey(ctx context.Context) (*Key, error)

VerifyKey reads back the key the client is holding. There is no "valid" field to consult: a key the registry will not accept gets the same 401 here as anywhere else, so a success is the answer. What is checked is that a key came back at all — a 200 with no key_id is some other service answering, not a registry saying yes.

type Error

type Error struct {
	Status int
	Body   map[string]any
}

Error carries a failure flattened into one map, so everything downstream reads it the same way regardless of whether it came from the registry, from object storage, or from this client.

func Fail

func Fail(code, fix string) *Error

Fail builds a client-side error in the same shape as a server-side one.

func (*Error) Code

func (e *Error) Code() string

Code is the stable error identifier, e.g. checksum_mismatch.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Fix

func (e *Error) Fix() string

Fix is the human- and agent-readable remedy, when there is one.

func (*Error) Retryable

func (e *Error) Retryable() bool

Retryable honours an explicit verdict; absent that, 429 and 5xx are worth another attempt and nothing else is.

type Identity

type Identity struct {
	KeyID         string `json:"key_id,omitempty"`
	Workspace     string `json:"workspace,omitempty"`
	WorkspaceName string `json:"workspace_name,omitempty"`
}

Identity is what the registry said the stored key was, recorded once at login. It is a record of an answer, not a claim about the key now: a key can be revoked between logging in and using it, and only the registry can say so.

func ReadIdentity

func ReadIdentity(env func(string) string, workspace string) (Identity, bool)

ReadIdentity returns the identity recorded at login for the named workspace — empty meaning the default entry — and whether there is one worth reporting.

It is deliberately silent when KROWK_TOKEN is set. ReadToken prefers the environment, so the key doing the work is not the key the file describes, and the file's workspace would name somewhere uploads are not going — a wrong answer given confidently, which is worse than no answer. An identity is also withheld when the entry holds no token: a workspace with nothing to reach it with is left over from a login that has since been replaced.

The Workspace reported is the entry's own recorded field, not the name it is filed under, so a key the registry could not confirm still reports no workspace rather than the placeholder it is stored beside.

type Key

type Key struct {
	KeyID     string `json:"key_id,omitempty"`
	Name      string `json:"name,omitempty"`
	Workspace string `json:"workspace,omitempty"`
	// WorkspaceName is the workspace's human name, beside the slug. The slug is
	// what the CLI stores and matches on — names can be renamed under it — but a
	// person choosing between workspaces has nothing to choose with in a slug.
	// Older registries do not send it, so it can be empty.
	WorkspaceName string `json:"workspace_name,omitempty"`
	ExpiresAt     string `json:"expires_at,omitempty"`
	LastUsedAt    string `json:"last_used_at,omitempty"`
	CreatedAt     string `json:"created_at,omitempty"`
	// Status is the HTTP status the read answered with. Diagnostics print what
	// actually arrived rather than assuming 200. Transport detail, not part of
	// the key itself.
	Status int `json:"-"`
}

Key is the key a request is made with, as the registry reports it. Asking beats guessing: a token string says nothing about itself — it can be revoked, expired, or for a workspace other than the one the caller expects, and all three look identical until an upload fails.

type Page

type Page struct {
	Artifacts []*Artifact `json:"artifacts"`
	Next      string      `json:"next,omitempty"`
}

Page is one page of a workspace's artifacts, newest first. Next carries the slug to pass back as before for the following page, and is empty on the last.

type Paste added in v0.6.0

type Paste struct {
	// Markdown is the krowk block: the image, the caption, the link through to
	// the card, and the expiry when there is one.
	Markdown string `json:"markdown"`
	// URL is the bare card link, for the places that unfurl it themselves.
	URL string `json:"url"`
	// Destinations maps a tool name to the form it wants — "markdown" or
	// "url" — with `_default` answering for every tool not named. The table is
	// the registry's, so a tool proving out reaches clients that were installed
	// before anyone had heard of it.
	Destinations map[string]string `json:"destinations,omitempty"`
}

Paste is one artifact ready to go into a comment, in the two forms the places it lands need, with the table that says which is which.

Two forms because there is no single one: GitHub renders the markdown block and builds no preview card for a third-party link, while Slack renders no markdown image embed at all and unfurls a bare URL into a card of its own.

func (*Paste) FormFor added in v0.6.0

func (p *Paste) FormFor(destination string) string

FormFor is the form this destination wants, "" when there is nothing served to answer with. An unknown destination gets `_default`, per the table: the worst case of the block where it does not render is informative text, and the worst case of a bare link is a link nobody can tell anything about.

type Run

type Run struct {
	Slug       string          `json:"slug"`
	Status     string          `json:"status"`
	StartedAt  string          `json:"started_at,omitempty"`
	FinishedAt string          `json:"finished_at,omitempty"`
	Metadata   json.RawMessage `json:"metadata,omitempty"`
}

Run groups the artifacts one agent run produced, and is where the facts about the work live; each artifact carries its own production record.

type RunPage

type RunPage struct {
	Runs []*Run `json:"runs"`
	Next string `json:"next,omitempty"`
}

RunPage is one page of a workspace's runs, newest first. Next carries the slug to pass back as before for the following page, and is empty on the last.

type Service

type Service struct {
	Service  string   `json:"service"`
	Versions []string `json:"versions"`
}

Service is the descriptor at the API root, which is how doctor tells a reachable registry from a reachable something-else.

type Spec

type Spec struct {
	Path        string `json:"-"`
	Filename    string `json:"filename"`
	ContentType string `json:"content_type"`
	ByteSize    int64  `json:"byte_size"`
	Checksum    string `json:"checksum"`
	Run         string `json:"run,omitempty"`
	// Metadata is the artifact's own production record — stamped at the moment
	// this file is pushed, so it travels with the artifact wherever it is
	// claimed or attached. Metadata is public: the card page is keyless.
	Metadata any `json:"metadata,omitempty"`
}

Spec is a file the client is about to upload, measured and digested so the registry can sign an upload URL that only these exact bytes fit.

Path is local and deliberately not serialized: the registry is told the basename, never where the file sat on someone's laptop.

func Inspect

func Inspect(path string) (Spec, error)

Inspect measures and digests a file. The digest is read off disk up front because it is signed into the upload URL — which is what lets storage refuse corrupted bytes at the edge rather than after we have paid to store them.

type Upload

type Upload struct {
	Method    string            `json:"method"`
	URL       string            `json:"url"`
	Headers   map[string]string `json:"headers"`
	ExpiresAt string            `json:"expires_at"`
}

Upload is where the bytes go, signed for one specific body.

type WorkspaceKey added in v0.2.0

type WorkspaceKey struct {
	Name      string `json:"name"`
	KeyID     string `json:"key_id,omitempty"`
	Workspace string `json:"workspace,omitempty"`
	// WorkspaceName is the human title, where login recorded one.
	WorkspaceName string `json:"workspace_name,omitempty"`
	Default       bool   `json:"default"`
}

WorkspaceKey is one stored key as `krowk workspaces` lists it: the name it is filed under, what the registry said about it, and whether it is the one a command with no workspace named would use.

func StoredWorkspaces added in v0.2.0

func StoredWorkspaces() []WorkspaceKey

StoredWorkspaces lists every stored key, sorted by name so the listing is the same twice in a row — Go's map order is not, and a list that reshuffles between runs is hard to read and impossible to diff. Nothing stored is an empty slice, not an error: having no keys is a normal state, not a failure.

Tokens are deliberately not among the fields. This is what a listing command prints, and printing secrets into a terminal, a scrollback buffer or a CI log is not something a list command should be able to do.

Jump to

Keyboard shortcuts

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