# ctx.cat API

ctx.cat exposes a small HTTP API for encrypted private shares, plaintext public
and unlisted shares, comments, discovery, signing capabilities, and verification.
Hosted private-content operations are zero-knowledge: content, metadata, and
comments are encrypted client-side before upload, and decrypt keys stay in URL
fragments.

## Base URLs

- Web: `https://www.ctx.cat`
- API: `https://api.ctx.cat`
- Local default: `http://127.0.0.1:8787`

Use `CTX_CAT_BASE_URL` in local clients when targeting staging or a self-hosted
backend.

## Critical Fragment Handling

URL fragments are client-side secrets. Browsers do not send fragments to
servers, and agents must preserve that boundary manually.

- Correct HTTP request: `GET https://api.ctx.cat/Ab3XyZ9kLm`
- Wrong HTTP request: `GET https://api.ctx.cat/Ab3XyZ9kLm#key=...`
- Do not send fragments to an LLM, backend, logs, analytics, search index, or
  error tracker.
- Strip the fragment before HTTP fetch, then decrypt locally with the fragment
  key.
- Owner fragments grant mutation rights and must be treated like credentials.
- `mutateKey` from private share creation and `ownerKey` from public/unlisted
  shares are write capabilities. Treat them like decrypt keys: never send them
  to an LLM, logs, analytics, backend services that do not require them, or
  error trackers.
- Redact `X-Ctx-Mutate-Key`, `X-Ctx-Owner-Key`, `#key`, and `#owner` before
  logging request context or errors.

## Common Mistakes

Do not copy these patterns into agents or integrations:

```js
// WRONG: sends or logs a fragment-bearing URL.
await fetch("https://api.ctx.cat/Ab3XyZ9kLm#key=secret");
console.error("failed to fetch", "https://www.ctx.cat/Ab3XyZ9kLm#key=secret");

// WRONG: stores plaintext in a field that must carry encrypted bytes.
await fetch("https://api.ctx.cat/", {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-Ctx-Upload-Format": "json" },
  body: JSON.stringify({ bodyBase64: btoa(plaintext) }),
});
```

Safe debugging context is a share id, status code, and generic error message:
`PATCH Ab3XyZ9kLm returned 403`. Full URLs with fragments, capability headers,
`mutateKey`, and `ownerKey` are not safe debugging context.

## Crypto Contract

Use the official TypeScript client, CLI, or MCP server for crypto operations.
Only use raw HTTP if implementing the crypto contract in `public/client.ts`
exactly:

- Generate a random base64url decrypt key for the URL fragment.
- Derive an AES-256-GCM key from that fragment key using HKDF-SHA-256 and the
  per-share `salt`.
- Encrypt private content with AES-256-GCM and the body `iv`.
- Encrypt private metadata separately with AES-256-GCM and a metadata IV stored
  inside `encryptedMeta`.
- Never send plaintext private content, plaintext private metadata, decrypt
  keys, owner capabilities, or signing private keys to the hosted backend.

## Headers

Security: never log `X-Ctx-Mutate-Key`, `X-Ctx-Owner-Key`, or any header
containing write capabilities. These are secret credentials equivalent to
passwords.

- `Content-Type: application/octet-stream`: encrypted binary private upload body.
- `Content-Type: application/json`: public/unlisted JSON payloads, comments,
  signing requests, verification requests, and the private JSON upload fallback.
- `X-Ctx-Meta`: base64 JSON metadata envelope for private uploads. Contains
  `salt`, `iv`, and encrypted metadata.
- `X-Ctx-Upload-Format: json`: ctx.cat private upload fallback for large
  encrypted metadata. The JSON body carries `bodyBase64` ciphertext and
  `uploadMeta`.
- `X-Ctx-Meta-Location`: response header pointing to `/<id>/meta` when
  encrypted metadata is too large to safely return in `X-Ctx-Meta`.
- `X-Ctx-Expires`: expiration timestamp or `never`.
- `X-Ctx-Mutate-Key`: private owner capability for private edits and deletes.
- `X-Ctx-Owner-Key`: public/unlisted owner capability for plaintext edits,
  deletes, and owner-marked comments.
- `X-Ctx-Mode`: `public` or `unlisted` for plaintext share creation.
- `X-Ctx-Signature-Public-Key`: SSH public key used for optional public
  verification metadata.
- `X-Ctx-Signature-Github-User`: claimed GitHub username.
- `X-Ctx-Signature-Created-At`: author signature timestamp.
- `X-Ctx-Signature-Content-Sha256`: signed content hash.
- `X-Ctx-Signature-Metadata-Sha256`: signed metadata hash.

Legacy `X-Mux-*` request headers are accepted as temporary aliases for old
clients, but new integrations should only emit `X-Ctx-*`. Responses emit
`X-Ctx-Meta` or `X-Ctx-Meta-Location`.

## Capability Headers

All write capabilities are secrets.

```text
Share type | Response field | HTTP header       | Security level
Private    | mutateKey      | X-Ctx-Mutate-Key  | secret write capability
Public     | ownerKey       | X-Ctx-Owner-Key   | secret write capability
Unlisted   | ownerKey       | X-Ctx-Owner-Key   | secret write capability
```

It is safe to log a share id such as `Ab3XyZ9kLm` or a base URL without a
fragment. It is not safe to log fragments, full fragment URLs, capability
headers, `mutateKey`, or `ownerKey`.

## Private Shares

Critical: private means you encrypt before upload. The server stores exactly
the bytes you send. Never send plaintext as a private share body.

`POST /`

Creates a private encrypted share when the request omits `X-Ctx-Mode`.

Request body is ciphertext bytes with `X-Ctx-Meta`, or a JSON envelope when
`X-Ctx-Upload-Format: json` is set. The server stores ciphertext, encrypted
metadata, hashed owner capability, size, expiration, optional public
verification metadata, and optional provenance. It never receives the decrypt
key.

Agents should use the official client path unless they have implemented the
crypto contract above.

Response:

```json
{
  "id": "Ab3XyZ9kLm",
  "mutateKey": "owner-capability"
}
```

`GET /<id>`

Returns ciphertext bytes. Small encrypted metadata is returned in `X-Ctx-Meta`.
Large encrypted metadata is retrieved from `GET /<id>/meta` when
`X-Ctx-Meta-Location` is present.

`GET /<id>/meta`

Returns encrypted metadata fields and non-sensitive storage metadata:

```json
{
  "encryptedMeta": "...",
  "iv": "...",
  "salt": "...",
  "size": 1234,
  "publicVerification": {},
  "provenance": {}
}
```

`PATCH /<id>`

With `X-Ctx-Mutate-Key`, updates encrypted content, encrypted metadata, or
expiration. The body follows the same private upload format as `POST /`.

`DELETE /<id>`

With `X-Ctx-Mutate-Key`, deletes a private share.

## Public And Unlisted Shares

`POST /`

Creates a plaintext public or unlisted share when `X-Ctx-Mode` is `public` or
`unlisted`.

Security: `ownerKey` grants edit/delete/comment ownership. Treat it as a secret
capability. Do not log it, send it to an LLM, send it to analytics, or include
it in user-visible errors.

Private `mutateKey` grants the same class of write access for private shares
and must be handled with the same secrecy.

Request:

```json
{
  "content": "# notes",
  "name": "notes.md",
  "signature": {}
}
```

Response includes an `ownerKey`. The owner key belongs in the URL fragment and
is never needed for normal reading.

`GET /<id>`

Returns the plaintext record as JSON.

`PATCH /<id>`

With `X-Ctx-Owner-Key` or `X-Ctx-Mutate-Key`, updates content, name,
expiration, mode, or signature.

`DELETE /<id>`

With owner capability, deletes the share.

## Comments

`GET /<id>/comments`

Returns public/unlisted comments or encrypted private comment records.

`POST /<id>/comments`

For private shares, send encrypted comment `body` and `iv`. For public/unlisted
shares, send plaintext `content`. Owner capability marks the comment as
`owner`; otherwise it is `anonymous`.

Private comment encryption uses a key derived locally from the private share key
and share id. Agents should not implement this crypto manually unless they are
matching the TypeScript client exactly. Prefer `addPrivateComment` from the
TypeScript client, CLI, or MCP tools. A private comment request shape is:

```json
{
  "body": "encrypted-comment-base64",
  "iv": "comment-iv-base64"
}
```

## Discovery

`GET /public.json`

Returns recent public shares. Private and unlisted shares are excluded.

`GET /health` and `GET /healthz`

Return service health without exposing secrets:

```json
{
  "ok": true,
  "service": "ctx-cat-api",
  "storage": "configured",
  "timestamp": "2026-05-14T00:00:00.000Z"
}
```

## Signing And Verification

`GET /signing/capabilities`

Reports whether remote signing is enabled and whether a local signing key is
available. Hosted ctx.cat defaults to remote signing disabled.

`POST /signing/sign`

This endpoint is disabled on hosted ctx.cat to prevent plaintext exposure and
returns `403`. It creates a server-side signature envelope only when
`CTX_CAT_ENABLE_REMOTE_SIGNING=true` in local, self-hosted, or explicitly
trusted deployments. Hosted clients should use local signing instead so
plaintext does not leave the user environment.

`POST /signing/verify-github-key`

Checks whether a signing SSH public key is published for a claimed GitHub user
and optionally whether the user belongs to an allowed org.

Request:

```json
{
  "githubUser": "octocat",
  "publicKey": "ssh-ed25519 AAAA..."
}
```

Response includes a verification state, key fingerprint, and optional org state.

## Limits

Default request body limit is 50 MiB and can be changed with
`CTX_CAT_MAX_BYTES`. Large agent traces should use private encrypted uploads.
Large encrypted metadata automatically uses the `X-Ctx-Upload-Format: json`
fallback to avoid unsafe HTTP header sizes.

## JSON Upload Fallback

Critical: `bodyBase64` must be encrypted ciphertext, not plaintext. Encrypt
content first with AES-256-GCM, then base64 encode the ciphertext.

Use the JSON fallback when encrypted metadata would exceed safe header size
limits:

```json
{
  "bodyBase64": "encrypted-body-base64",
  "uploadMeta": {
    "encryptedMeta": "encrypted-metadata-base64",
    "iv": "body-iv-base64",
    "salt": "hkdf-salt-base64"
  }
}
```

## Safe Error Handling

- If decryption fails, log only the share id and a generic failure reason.
- If owner or mutate capability validation fails, log only the share id and
  status code.
- Never log fragment keys, `mutateKey`, `ownerKey`, `X-Ctx-Mutate-Key`, or
  `X-Ctx-Owner-Key`.
- Never include full URLs with `#` fragments in stack traces, support tickets,
  issue text, analytics, or prompts to an LLM.
- If asking an LLM to help debug an integration, redact all capabilities first.
  Safe example: `Failed to PATCH share Ab3XyZ9kLm with HTTP 403`.
  Unsafe example: a full ctx.cat URL with `#key`, an `ownerKey`, a `mutateKey`,
  or an owner/mutate header value.

## Rate Limiting And Retries

- Use exponential backoff for transient `5xx` errors.
- Do not retry `4xx` errors except `429`.
- Respect `Retry-After` when present.
- When logging retry attempts, redact all capabilities and fragments from
  context. Safe example: `Retry 2/5 for PATCH Ab3XyZ9kLm after 429`.

## Errors

Errors are JSON on the API host:

```json
{
  "error": "Invalid mutate key"
}
```

Common statuses:

- `400`: invalid JSON, invalid metadata, malformed expiration, or missing
  required fields.
- `403`: invalid owner or mutate capability.
- `404`: share or route not found.
- `413`: payload larger than the configured body limit.
- `500`: unexpected server error.
