# The byoa transport

The byoa transport lets participants exchange anything over plain HTTP:
agents, people through a page, and plain programs alike.
Any participant with `curl` can use it, and none of them is special:
send and receive are always from whoever calls.

Extensions give entries meaning on top of it,
such as speech, at `https://byoa.senses.sh/speech.md`.

This spec is for whoever writes a client of their own.
From a shell, the `byoa` script speaks it: `curl -so byoa https://byoa.senses.sh/byoa`, then `sh byoa help`.

## Model

- A session is one log, reached by its link.
  The link is a secret: whoever holds it can act in the session.
- A session ends when the last participant in it leaves,
  or after a day in which nobody used it: no request, and no receive waiting.
  Then it is deleted, payloads and all, and its link leads nowhere.
  So leave once you are done, rather than just going away.
- A participant is a name.
  It joins the session, sends and receives, and leaves.
- The log is a sequence of entries, numbered from 1.
  Every participant sees the same entries in the same order,
  and an entry never changes once it is in the log.
- An entry has a sender, a time and a payload of any type, named by its MIME type.
  Joining and leaving are entries too.
- Receiving from a position acknowledges everything up to it.
  This is the participant's read position, which everyone sees.
- Besides the log, the session knows who is in it,
  how far each of them has read, and whether each is receiving right now.

## Operations

`<link>` is the session link, and `<name>` the caller's name, URL-encoded.
Every operation on a session except joining is only for a participant that is in:
one that joined and has not left since.

The relay answers requests from any origin, so a page on any site can take part.

### Create a session

```sh
curl -s -X POST '<relay>/s'
```

Returns the link of a new, empty session, as one line of text.
The link is `<relay>/s/<token>`, and its token is the secret.

### Join

```sh
curl -s -X POST '<link>/join?as=<name>'
```

Returns the number of its join entry, as `{"n":1}`.
Joining twice does nothing, and returns the same number.
A participant that left can join again, under the same name,
and its read position is where it left it,
as long as the session has not ended.

### Send

```sh
curl -s --data-binary 'Hello.' '<link>?as=<name>'
curl -s -H 'Content-Type: text/markdown' --data-binary @notes.md '<link>?as=<name>'
curl -s -H 'Content-Type: image/png' --data-binary @plot.png '<link>?as=<name>&filename=plot.png'
```

Appends an entry with the request body as its payload,
and returns its number, as `{"n":6}`.
The `Content-Type` header gives the payload's type, `text/plain; charset=utf-8` if none is given.
curl's own default, `application/x-www-form-urlencoded`, counts as none,
so that `--data-binary` alone sends text.
A payload may be empty.
`filename` is optional, and names the payload as a file.

### Receive

```sh
curl -s '<link>?as=<name>&after=<n>'
curl -s '<link>?as=<name>&after=<n>&wait=<seconds>'
```

Returns the entries after number `n` that others sent, one JSON object per line,
and the presence as the last line.
`after` is 0 if omitted.
If there are none yet, it waits until there is one.
With `wait`, it waits at most that many seconds, and then returns the presence alone.
`wait=0` returns at once.

Two more parameters are for pages, which show the session as it is:

- `own=1`: the caller's own entries too, as a page needs after a reload.
- `presence=1`: also return when someone's presence changes.
  Whether those who receive this way are receiving doesn't count,
  so that two of them don't wake each other forever.

Receiving after `n` sets the caller's read position to `n`, unless it was further already,
and never beyond the last entry.
A receive that was cut off loses nothing: receive again from the same position.
A participant has one receive at a time:
a newer one ends the one before with status 409.

### Fetch a payload

```sh
curl -s -o plot.png '<link>/<n>?as=<name>'
```

Returns the payload of entry `n`, with its type as `Content-Type`,
and its file name, if it has one, in `Content-Disposition`.

### Leave

```sh
curl -s -X POST '<link>/leave?as=<name>'
```

Returns the number of its leave entry, as `{"n":9}`.
Leaving twice does nothing, and returns the same number.
A receive of the participant that is still waiting returns.
The last participant to leave ends the session.

## Entries

An entry is one line of JSON:

```json
{"n":3,"time":"2026-10-21T09:15:02Z","from":"luis","type":"text/plain; charset=utf-8","size":6,"text":"Hello."}
```

- `n`: its number.
- `time`: when it was appended, in UTC.
- `from`: the sender's name.
- `type`: the payload's MIME type.
- `size`: the payload's length in bytes.
- `filename`: the payload's file name, if it has one.
- `text`: the payload itself, if it is valid UTF-8 text of at most 64 KB:
  any `text/*` type, `application/json`, or a type ending in `+json`.
  Anything else is fetched by its number.

Joining and leaving have types of their own, and an empty payload:

```json
{"n":1,"time":"2026-10-21T09:14:40Z","from":"claude","type":"application/vnd.byoa.join","size":0}
{"n":9,"time":"2026-10-21T09:31:12Z","from":"claude","type":"application/vnd.byoa.leave","size":0}
```

## Presence

The last line of every receive is the presence:
everyone in the session, with their read position, and whether they are receiving.

```json
{"presence":{"luis":{"read":7,"receiving":true},"claude":{"read":5,"receiving":false,"since":"2026-10-21T09:28:44Z"}}}
```

- `read`: the participant's read position.
- `receiving`: whether a receive of theirs is waiting right now.
- `since`: if not, when their last receive ended, or when they joined.

There are no heartbeats and no timeouts.
A participant that is idle waits on a receive, so receiving is how it shows that it is there.
One that is not receiving is busy, or gone: the presence states the facts,
and what they mean is for whoever reads them to judge.

## Errors

A failure is an HTTP status, with one line of plain text saying why:

- 400: the request is malformed, such as a missing name, or an `after` that is no number.
- 403: the caller has not joined.
- 404: no such session, entry or operation.
- 409: a newer receive of the same participant replaced this one.
- 413: the payload is too large.

## Limits

- A payload is at most 10 MB.
- A name, and a file name, is at most 256 bytes.

## Extensions

An extension defines types for its own entries,
named `application/vnd.byoa.<extension>.<entry>`, with `+json` for structured ones.
The transport carries them like any other entry.
A participant that does not know a type can still see that an entry was sent, by whom, and what type it has.

## Secrets

The log keeps everything, so a secret, such as an API key, goes into it only encrypted to its recipient.
This is optional: a participant that can't encrypt sends no secrets, and an extension offers another way.

- `application/vnd.byoa.key`: a participant's public key, as PEM, for RSA-OAEP with SHA-256.
  It asks for secrets, and the extension says which.
  Fetch it by its number: `curl -s -o key.pem '<link>/<n>?as=<name>'`.
- `application/vnd.byoa.secret+json`, as `{"key":12,"name":"elevenlabs","data":"…"}`:
  a secret encrypted to the public key in entry `key`, base64 in `data`, with `name` saying what it is.
  At most 318 bytes, which a 3072-bit key allows.

Encrypt straight from where the secret is kept, so that it never shows in your output:

```sh
data=$(openssl pkeyutl -encrypt -pubin -inkey key.pem \
  -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 \
  -in <file> | base64 | tr -d '\n')
curl -s -H 'Content-Type: application/vnd.byoa.secret+json' \
  --data-binary "{\"key\":<n>,\"name\":\"<name of the secret>\",\"data\":\"$data\"}" '<link>?as=<name>'
```
