CARLOS rastrillo docs

๐Ÿค– vault

amadan.net/rastrillo/rastrillo/vault

The client half of the Pegamento vault: one person's named sealed blobs and per-method wrapped seed on a home service your operator may not run. The server lives at amadan.net/carlos/pegamento; this package speaks its v1 wire, seals and opens with keyring keys so plaintext never crosses the package boundary, and refuses undeclared blob names before any request leaves the process.

Two rulings bind everything here, inherited from the doctrine. Strictly additive: an app that configures no home constructs no Client and makes no request, ever. Closed namespace: every blob name the app may touch is declared at construction.

New and Config

type Config struct {
	Home   string
	Ring   keyring.Ring
	Blobs  []string
	Token  string
	Seed   []byte
	Client *http.Client
}

func New(cfg Config) (*Client, error)

A Client is one person's bound home session, not a service handle โ€” construct it when you hold a link token and an unwrapped seed, never at boot. New validates everything and makes no request: Home needs a scheme, Blobs is the closed namespace with every name matching [a-z0-9-]{1,64}, Seed is the person's 32 bytes. Client defaults to a 10-second timeout.

Blobs

const Create = "0"

func (c *Client) Get(ctx context.Context, name string) (plaintext []byte, version string, err error)
func (c *Client) Put(ctx context.Context, name string, plaintext []byte, version string) (newVersion string, err error)
func (c *Client) PutPadded(ctx context.Context, name string, plaintext []byte, target int, version string) (newVersion string, err error)
func (c *Client) Blobs(ctx context.Context) ([]BlobInfo, error)

type BlobInfo struct {
	Name    string
	Version string
}

Plaintext in, plaintext out: Put seals under Ring.BlobKey(seed, name) and Get opens, so a caller cannot accidentally store plaintext. Versions are opaque strings the client compares only for equality; Create is the create-only sentinel. PutPadded pads to a fixed target first, for a blob whose length leaks (a server list); the target is a floor, not a cap. Blobs lists names and versions, never content.

type ErrStale struct{ Current string }

var ErrNotFound = errors.New("rastrillo/vault: blob not found")
var ErrUndeclared = errors.New("rastrillo/vault: undeclared blob name")

Put refuses a stale version with ErrStale carrying the current one โ€” re-read, merge, retry. Get answers ErrNotFound for a blob never written (first run, not an outage). An undeclared name answers ErrUndeclared locally, before any dial. ErrStale.Error prints the current version so an unhandled one still reads well in a log.

Methods

type Method struct {
	ID        string
	Kind      string
	CreatedAt string
}

func (c *Client) Methods(ctx context.Context) ([]Method, error)
func (c *Client) Wrapped(ctx context.Context, methodID string) ([]byte, error)
func (c *Client) Enrol(ctx context.Context, methodID string, wrapped []byte, ceremonyProof string) error
func (c *Client) RemoveMethod(ctx context.Context, methodID, ceremonyProof string) error

A method is one way into the vault โ€” a passkey, an identity anchor, a synced item โ€” and the unit the wrapped seed is keyed by. Wrapped fetches the seed wrapped for one method (unwrap with keyring.Ring.UnwrapSeed); Enrol stores a new wrap and RemoveMethod removes one, both demanding a fresh ceremony proof because they change who can open everything. The home's last-method guard refuses to strand the vault; it surfaces here as an ordinary error.

The handoff

type Handoff struct {
	Sessions   *sessions.Sessions
	Home       string
	Origin     string
	SigninPath string
}

func (h Handoff) Enrol(w http.ResponseWriter, r *http.Request)
func (h Handoff) Restore(w http.ResponseWriter, r *http.Request)

The instance's two POST handlers. Mount Enrol behind sessions.Require and Restore on the signed-out router, both under the app-wide csrf middleware. Enrol mints a fresh Method: "vault" session row via sessions.Mint โ€” never the browser's own cookie โ€” and answers the home URL whose fragment carries it. Restore adopts the token the browser unwrapped via sessions.Adopt and redirects to its same-site return_to; a dead token lands on SigninPath (default /signin) with no cookie, so a failed restore is a sign-in prompt, never an error page. The instance and the home never speak: everything secret rides URL fragments through the person's browser.

type EnrolAnswer struct {
	EnrolURL string
	Nonce    string
	Entry    EnrolEntry
}

type EnrolEntry struct {
	URL   string
	Token string
}

EnrolAnswer is Enrol's JSON response; EnrolEntry is what the home stores in the person's servers blob โ€” this instance's origin and the token that re-admits them here.

type RestoreRequest struct {
	V      int
	Ret    string
	Nonce  string
	EphPub []byte
}

func NewRestoreRequest(ret string) (RestoreRequest, *crypto.Keypair, error)
func OpenRestoreReturn(kp *crypto.Keypair, nonce string, sealed []byte) (token string, err error)

The restore round trip's pure half, for CLI-shaped consumers and tests. NewRestoreRequest mints the ephemeral keypair and one-time nonce; the home seals its answer to EphPub under rastrillo/vault/restore/v1; OpenRestoreReturn opens it and verifies the nonce, failing closed on a stale, replayed, or tampered return.

JS

func JS() []byte

The browser twin (js/vault.mjs): restoreRequest and openRestoreReturn over WebCrypto, plus fragment encode/decode. Serve it beside crypto.JS() โ€” the sibling import is the deployment contract, keyring's pattern exactly, and the package's tests prove a Go-sealed return opens in JS.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.