Files
sausage/host/src/api/README.md
T

7.0 KiB

Host API and host pages

handle(&Api, Caller, Request<Bytes>) answers requests without a transport. The later wasi:http hook supplies the caller from the process table. It must not accept caller identity from headers, environment variables, or JSON. Reads are open to every caller. JSON routes return 200 on success. Errors use {"error":"kind","message":"kind"} with static, secret-free text. Missing routes return 404. Invalid parameters return 400. Authorization failures return 403. Conflicts return 409. The filing budget returns 429.

Host listener

sausage run WORLD starts the console and the host listener with one shared Arc<Cred>. The listener owns its passkey connection and handles requests on a dedicated thread. The console receives the same credentials store, including its decision notifications. The listener stops when the console returns. No protected harness or process module needs a change.

Options on sausage run:

  • --api-listen 127.0.0.1:8081: the default listener address.
  • --api-rp-id localhost: the default WebAuthn RP ID.
  • --api-origin http://localhost:8081: the default browser origin.

The origin must name the host pages, not the world server. The listener speaks HTTP. A production HTTPS origin needs a reverse proxy to this listener. The world's UI can link to the host pages. It does not serve their code. The listener identifies browser calls as /host/browser, with the host PID and no conversation. Passkey proofs supply the approver identity independently of that caller.

Pages and assets

  • GET /approve/{request} renders the host's stored summary and public details. The page sends only the human's decision and optional limit overrides. The host builds the binding from the stored request and its payload digest.
  • GET /register registers the first passkey or adds another passkey.
  • GET /entry/{item}/{field} displays a blank secret-entry form. The host embeds its public encryption key and the selected field's public metadata.
  • GET /app.js serves the shared ceremony code and WebCrypto encryption.

The checked-in pages use plain HTML and JavaScript with no build step or external dependencies. The host escapes review data before it enters HTML. No page includes a stored secret value.

Public JSON routes

  • GET /whoami: the transport's caller.
  • GET /cred/items: public item snapshots.
  • GET /cred/items/{id}: one public snapshot.
  • GET /cred/requests?state=pending: requests with diffs and summaries. Omit state to return every request.
  • GET /cred/requests/{id}: one request with its diff and summary.
  • POST /cred/requests: {"kind":"grant","key":"stable-key","request":{...}}. The request contains items and optional duration_secs, max_uses, and validator. Kind edit accepts OperationRequest with item, base_version, and change. The response is {"request":"id"}. Filing does not approve anything.
  • GET /cred/grants: the grant ledger.

Explicit request keys cannot begin with inject: or ssh:. Those prefixes belong to automatic requests. Explicit requests, automatic SSH requests, and new ceremonies share a per-process budget. The budget permits 32 attempts per 60-second window. Reads and granted SSH operations do not spend that budget.

Decisions

  • POST /cred/requests/{id}/challenge: {"decision":"approve","overrides":{...}}. Decisions are approve or deny. Overrides use T27's typed Overrides structure. Unknown fields fail. A denial accepts only default overrides. With no passkey, the response is {"unlocked":true}. Otherwise, the response is {"id":"challenge id","options":{...}}.
  • POST /cred/requests/{id}/decide: {"challenge_id":"id","assertion":{...}}. The host reads the decision and overrides from the verified binding. An optional explicit decision must match that binding. The library checks the request ID, decision, overrides, and stored payload digest inside its transaction. Without passkeys, this route accepts {"decision":"approve","overrides":{...}} instead. The library checks its Unlocked proof inside the transaction.
  • POST /cred/grants/{id}/revoke: {"key":"stable-key"}. This route files a revocation request and returns {"request":"id"}. Use the normal challenge and decision routes to approve it.

Successful mutations return {"ok":true}. A successful assertion produces a non-cloneable Verified proof. A replay or a mismatched binding cannot authorize another action.

Passkeys

  • GET /cred/passkeys: public credential IDs and labels, not serialized passkeys.
  • POST /cred/passkeys/register/challenge: {"label":"new label"}. The returned assertion challenge binds the exact label.
  • POST /cred/passkeys/register/start: {"label":"new label","gate":{"id":"challenge id","response":{...}}}. The first registration permits an absent gate, as D53 requires. Every later registration needs the existing passkey's enrollment assertion.
  • POST /cred/passkeys/register/finish: {"challenge_id":"id","response":{...}}.

WebAuthn options and responses use the library's browser JSON formats. The host retains the RP configuration and pending ceremony state. Restart discards pending ceremonies but retains registered passkeys.

Secret entry

  • GET /cred/entry/key: {"public_key":[...]} in SEC1 format.
  • POST /cred/items/{id}/fields/{field}/challenge: {"ciphertext_sha256":"lowercase hex"}. The host loads the field metadata and constructs the binding itself.
  • POST /cred/items/{id}/fields/{field}: {"challenge_id":"id","assertion":{...},"envelope":{...}}.

The field parameter is its stable ID. Secret entry requires an assertion. Register a passkey first. The host compares the item, field, and envelope hash before it calls the encrypted field API. That API checks the typed proof and metadata before it decrypts inside its transaction. Public fields cannot use secret entry. Non-UTF-8 plaintext and invalid envelopes fail without an echo. See cred/entry.rs for the ECDH, HKDF, AES-GCM, and hash formats.

SSH

  • POST /ssh/identity: {"host":"example.com"}. The response is {"item":"id","public_key":"base64 SSH public key blob","user":null}. A public user field supplies the optional default user.
  • POST /ssh/sign: {"host":"example.com","item":"id","payload":"base64"}. The response is {"signature":"base64 SSH wire signature"}.

SSH operations match SSH rules and enforce free use or a caller grant. Both operations evaluate item and grant validators through QuickJS, as D58 requires. Identity discovery checks access without spending a signing use. Signing reserves one use. Preparation commits its pending audit row in the same transaction. Successful operations complete that row. Signing records only the payload hash, never the payload. An ungranted caller receives 403 with error, message, item, and request. The host files the request before it responds. Two matching items produce 409. The signing item must match the selected host rule. No SSH response returns a private key or decrypted field value.