Documentation
¶
Overview ¶
Package pairing owns the device tokens that gate a serve's origin passthrough once it is exposed beyond loopback (GDK-433).
Model: each paired device — a remote gadak binding this serve as its workspace origin — gets an opaque random token, presented as `Authorization: Bearer <token>` (the Jira DC PAT shape; the Cloud email:token Basic shape is deliberately not reused). The server stores only the SHA-256 hash plus metadata, in `pairing.json` inside the profile directory: next to config.json and its 0600 atomic-write convention, and never inside the mirror (gadak.db is a disposable cache of the origin, not a place for originals) or any export. The plaintext exists once, in `gadak pairing mint` output, inside a one-line offer.
Gate semantics (single owner: server.handleOriginREST): while at least one active — unrevoked, unexpired — token exists, every request under origin.RESTPrefix must present a valid Bearer token. With no active token the passthrough behaves exactly as before (implicit loopback trust, decision 0003). There is no loopback bypass: a tailnet proxy reaches the serve as loopback, so the token is the only identity the server can distinguish.
Index ¶
- Constants
- func EncodeOffer(o Offer) (string, error)
- func Explain(dir, bearer string, now time.Time) (Verdict, Reason)
- func FormatExpiry(t time.Time) string
- func RemotePath(dir string) string
- func SaveRemote(dir string, r Remote) error
- func StorePath(dir string) string
- type Meta
- type Offer
- type Reason
- type Remote
- type Verdict
Constants ¶
const HomeLabel = "_home"
HomeLabel is the reserved label of the home routing token. It is not a device: revoking it locks local writes while a serve is running.
const OfferV1 = 1
OfferV1 is the only offer format this build understands. A version bump means capability change, not app version: mixed-version clients syncing against one serve is the normal state (GDK-433 prior-art survey), so an unknown version is an explicit refusal, never a silent best-effort parse.
const RemoteRel = "remote-origin.json"
RemoteRel is the profile-relative file holding this workspace's stored pairing credential: the workspace's origin is a remote gadak serve, and this is how to reach and authenticate it.
It sits next to config.json (the credential convention — 0600, atomic write, profile-scoped) rather than inside it, for the same reason the server's token store is a separate file: config.json is what settings surfaces read and rewrite; a credential file has one writer, the pairing path. The token is plaintext here because the client must present it.
const ScopeLocalRouting = "local-routing"
ScopeLocalRouting is the home machine's own routing token. pairing list uses this so the `_home` row is not mistaken for a paired device.
const ScopeOrigin = "origin"
ScopeOrigin is full origin passthrough — the device-token scope. It exists in the stored metadata so a narrower scope (read-only, wiki-only, local-routing) can be told apart from tokens minted before scopes mattered.
const StoreRel = "pairing.json"
StoreRel is the profile-relative path of the token store. A separate file, not a config.json key: config.json is settings the UI edits and exports surface; this is a credential-adjacent secret list.
Variables ¶
This section is empty.
Functions ¶
func EncodeOffer ¶
EncodeOffer renders o as the single-line base64url form.
func Explain ¶
Explain classifies a rejected bearer. Authorize remains the gate decision (and last_used_at); this is the reason the 401 body carries.
hash in store, RevokedAt set → revoked hash in store, ExpiresAt elapsed → expired empty bearer or hash not in the store → unknown
func FormatExpiry ¶
FormatExpiry renders an offer's ExpiresAt for mint output; "" when unset.
func RemotePath ¶
RemotePath is the absolute credential path inside a profile directory.
func SaveRemote ¶
SaveRemote writes the pairing credential atomically at 0600, creating the profile directory if needed. Called only after a successful verify-before-save round trip — a credential that was never proven good must not reach disk.
Types ¶
type Meta ¶
type Meta struct {
Label string `json:"label"`
Scope string `json:"scope"`
Hash string `json:"hash"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
RevokedAt *time.Time `json:"revoked_at,omitempty"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
}
Meta is one stored token. Hash is the hex SHA-256 of the plaintext; the plaintext is never persisted anywhere.
func List ¶
List returns every stored token, earliest created first, including revoked and expired ones — revocation is audit history, not noise.
func Mint ¶
Mint creates a token for label, persists its hash, and returns the plaintext. The plaintext appears exactly here; callers print it once (as an offer) and never store it. A duplicate *active* label is refused so `pairing revoke <label>` stays unambiguous; a revoked label may be reused.
func Revoke ¶
Revoke revokes the token selected by exact label or hash prefix and returns it. Refuses ambiguities instead of guessing, and refuses an already-revoked token rather than no-op'ing silently. Revoked entries stay in the store as audit history but never stand as candidates: a re-minted label revokes by label again, pointed at the live mint.
type Offer ¶
type Offer struct {
V int `json:"v"`
Endpoint string `json:"endpoint"`
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
Label string `json:"label"`
}
Offer is the one-line pairing secret `gadak pairing mint` prints and a remote gadak consumes via `gadak init --pairing-code`. Base64url of a JSON document: one pasteable line, no shell-hostile characters.
The offer carries the token itself, so it is treated like a credential: never echoed into a log line, an error message, or argv more than the one flag the user already typed. ExpiresAt is advisory for the human (RFC3339, home origin's clock); the gate judges expiry from the store.
func DecodeOffer ¶
DecodeOffer parses an offer line. Errors describe the problem without quoting the payload — the token inside must not leak through an error path. Unknown version, missing endpoint, and missing token are distinct errors so `init --pairing-code` can tell the user what to redo.
type Reason ¶
type Reason string
Reason is why a rejected bearer was refused. Only tokens that were minted (hash in the store) get a detailed reason; anything else is unknown so a probe cannot tell unused strings from never-issued ones.
type Remote ¶
type Remote struct {
Endpoint string `json:"endpoint"`
Token string `json:"token"`
Label string `json:"label,omitempty"`
PairedAt string `json:"pairedAt,omitempty"`
}
Remote is the stored client side of a pairing: where the home serve is, the device token, and the label the home shows in `pairing list`.
func LoadRemote ¶
LoadRemote reads the stored pairing credential. Missing file is (nil, nil): most workspaces are not paired, and that is not an error.
type Verdict ¶
type Verdict int
Verdict is Authorize's answer.
const ( // VerdictOff: no active token exists, so the gate does not apply and // the passthrough keeps its pre-pairing behavior. VerdictOff Verdict = iota // VerdictAccept: a valid Bearer token was presented. VerdictAccept // VerdictReject: active tokens exist but the request carried no valid // one. Missing and wrong are the same answer — do not reveal which. VerdictReject )