Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
68e267d
relay: accounts: keys, sessions, the account API and the admin commands
mishan Oct 3, 2026
294ce75
relay: rooms know who is in them; the document socket takes a ticket
mishan Oct 3, 2026
16132fd
jam: an Account dialog on the join card; guests marked as guests
mishan Oct 3, 2026
58bf74c
docs: accounts on the relay: the variables, the volume, backups, mode…
mishan Oct 3, 2026
a488586
jam: accounts are the site's relay's; a rename and a delete take more
mishan Oct 3, 2026
d571c78
relay: harden accounts and the document socket
mishan Oct 3, 2026
3f918c1
relay: a cursor's colors only in the shape the page draws them
mishan Oct 3, 2026
47d6a69
jam: where the relay is, worked out once
mishan Oct 3, 2026
cc9949e
relay: handles fold by Unicode's confusables; a sweep never loses a key
mishan Oct 3, 2026
d94c3bc
relay: closing is final, cursors survive a reconnect, frames are capped
mishan Oct 3, 2026
e269000
relay: confusables.js carries the Unicode License v3 notice
mishan Oct 3, 2026
8bf5bbb
relay: a client's IPv6 zone cut by index, not a pattern
mishan Oct 3, 2026
2dacba9
relay: awareness and the run's log bounded; who sent what is the relay's
mishan Oct 3, 2026
66a1afa
relay: a new key ends the session that asked for it too
mishan Oct 3, 2026
1bb39fa
docs: what a restore brings back; compose: a memory limit
mishan Oct 3, 2026
9ec6c8a
relay: nobody a guest without accounts; CORS_ORIGIN must be an origin
mishan Oct 3, 2026
c91e04c
relay: say so when a trusted proxy sends no X-Forwarded-For
mishan Oct 3, 2026
01a32a5
jam: a lost room is joined again; the name is locked only by accounts
mishan Oct 3, 2026
650f0ce
ci: the relay's image joined with accounts on, too
mishan Oct 3, 2026
76ba631
docs: the relay's runbook; JAM.md and the backlog caught up
mishan Oct 3, 2026
a24dc42
relay: CORS_ORIGIN never `*'; a command counted in bytes; no handles …
mishan Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,12 @@ jobs:
working-directory: wasm/web
run: node relaytest.mjs

# The relay's accounts over HTTP, on a clock the script turns: keys,
# handles, sessions, the limits and the admin commands.
- name: Accounts
working-directory: wasm/web
run: node accountstest.mjs

# The room's second gate: a Chromium page and a Firefox page in one
# room on a relay, each with a live AudioContext, one pressing Play, a
# knob moved from each side, and one tape between them -- genwav's,
Expand Down Expand Up @@ -1008,23 +1014,34 @@ jobs:

# A room joined, not just the health line: that one answers before
# a room has been seeded from the image's gen/ and dsp/.
# Twice: as it starts with nothing set, and with CORS_ORIGIN, which
# is what turns its accounts on (and opens its database).
- name: It seeds and welcomes a room
run: |
join() {
docker exec "$1" node --input-type=module -e "
import WebSocket from 'ws';
const ws = new WebSocket('ws://127.0.0.1:8787/room/ci');
ws.on('open', () => ws.send(JSON.stringify(
{ type: 'hello', name: 'ci', protocol: 1, tickets: true })));
ws.on('message', (m) => {
const d = JSON.parse(m);
console.log(JSON.stringify(d));
process.exit(d.type === 'welcome' && d.piece ? 0 : 1);
});
ws.on('error', (e) => { console.error(e.message); process.exit(1); });
setTimeout(() => process.exit(1), 10000);"
}
docker run -d --init --name relay -p 127.0.0.1:8787:8787 thinksynth-relay
for i in $(seq 30); do curl -fs 127.0.0.1:8787/ && break; sleep 1; done
docker exec relay node --input-type=module -e "
import WebSocket from 'ws';
const ws = new WebSocket('ws://127.0.0.1:8787/room/ci');
ws.on('open', () => ws.send(JSON.stringify(
{ type: 'hello', name: 'ci', protocol: 1 })));
ws.on('message', (m) => {
const d = JSON.parse(m);
console.log(JSON.stringify(d));
process.exit(d.type === 'welcome' && d.piece ? 0 : 1);
});
ws.on('error', (e) => { console.error(e.message); process.exit(1); });
setTimeout(() => process.exit(1), 10000);"
docker run -d --init --name accounts -p 127.0.0.1:8788:8787 \
-e CORS_ORIGIN=https://pages.example.org thinksynth-relay
for i in $(seq 30); do curl -fs 127.0.0.1:8787/ && curl -fs 127.0.0.1:8788/ && break; sleep 1; done
curl -fs 127.0.0.1:8787/ | jq -e '.accounts == false'
curl -fs 127.0.0.1:8788/ | jq -e '.accounts == true'
join relay
join accounts
docker logs relay
docker logs accounts

- name: Publish
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ repo/
# The browser build's test harness (wasm/web/package.json) installs here.
node_modules/

# A relay run from the tree keeps its accounts beside itself (relay.mjs).
/wasm/web/relay.db*

# wasm/compare.mjs renders every piece twice, natively and through the
# module, and `-k DIR' leaves the pair on disk rather than in a temp dir --
# which is what CI passes, as `-k compare'. That is 458 MB of wav for the
Expand Down
20 changes: 20 additions & 0 deletions docker/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,23 @@ services:
init: true
ports:
- "127.0.0.1:8787:8787" # only the proxy on this host reaches it
# A relay that runs away with memory is stopped here and restarted,
# rather than left to take the host with it; node's heap stays under
# the limit so that it fails as itself first.
mem_limit: 512m
environment:
NODE_OPTIONS: --max-old-space-size=384
# The room page's origin (the Pages site, say). Unset, the relay
# has no accounts: the account API is not there, and a session in
# a hello is ignored.
CORS_ORIGIN: ${CORS_ORIGIN:-}
# nginx in front, appending the client's address to X-Forwarded-For,
# which the account API's rate limits read.
TRUST_PROXY: "1"
volumes:
# The accounts (DB=/data/relay.db, set by the image). Back it up:
# docs/RELAY.md.
- accounts:/data

volumes:
accounts:
10 changes: 10 additions & 0 deletions docker/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,27 @@
#
# which adds the listen 443 and certificate lines.

# A document socket's ticket rides in its query string, and a log is no
# place for one: requests are logged by path alone.
log_format thinksynth '$remote_addr - [$time_local] '
'"$request_method $uri $server_protocol" $status '
'$body_bytes_sent "$http_user_agent"';

server {
listen 80;
listen [::]:80;
server_name relay.example.org;
access_log /var/log/nginx/access.log thinksynth;

location / {
proxy_pass http://127.0.0.1:8787;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# The client's address, for the account API's rate limits; the
# relay reads one proxy's worth of it (TRUST_PROXY=1).
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# A document socket can sit quiet for as long as nobody types, and
# nginx's 60 s default would cut it.
proxy_read_timeout 1h;
Expand Down
9 changes: 8 additions & 1 deletion docker/relay.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ WORKDIR /srv/thinksynth/wasm/web
COPY wasm/web/package.json wasm/web/package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force

COPY wasm/web/relay.mjs wasm/web/doc.js wasm/web/commands.js ./
COPY wasm/web/relay.mjs wasm/web/doc.js wasm/web/commands.js \
wasm/web/account.js wasm/web/accounts.mjs wasm/web/wordlist.mjs \
wasm/web/confusables.js ./
COPY gen /srv/thinksynth/gen
COPY dsp /srv/thinksynth/dsp

# The accounts' file, somewhere the relay's user can write and a volume
# can be mounted over (compose.yaml): losing it loses every account.
RUN mkdir /data && chown node:node /data
ENV DB=/data/relay.db

USER node
EXPOSE 8787

Expand Down
4 changes: 4 additions & 0 deletions docker/relay.Dockerfile.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
!wasm/web/relay.mjs
!wasm/web/doc.js
!wasm/web/commands.js
!wasm/web/account.js
!wasm/web/accounts.mjs
!wasm/web/wordlist.mjs
!wasm/web/confusables.js
!gen
!dsp
10 changes: 7 additions & 3 deletions docs/JAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,13 @@ Where it stands:
the mirror, as the solo page does, so an `osc::sample` instrument
sounds in a room.
- **Text chat**, a pane on the room page (section 4).
- In progress: an invite link, so a room is joined without typing its
name; a list of the relay's rooms before joining; and a rework of the
room's layout.
- **Accounts.** A handle nobody else can join as, logged in with an
eight-word key from the relay; guests are marked as guests. The
document socket is let in by a ticket from the room socket. Running
it is [RELAY.md](RELAY.md#accounts).
- **An invite link**, so a room is joined without typing its name, and
**a list of the relay's rooms** before joining.
- In progress: a rework of the room's layout.
- Not yet: TURN -- peers whose NATs defeat STUN fall back to the relay
forwarding their commands; the `.patch` presets in the picker, which
offers `.dsp` graphs; and the done-when, four people in two cities for
Expand Down
14 changes: 12 additions & 2 deletions docs/JAM_BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ can edit. Before any audience feature:
carries the persistent id. Both are small in M3's relay and awkward once
rooms are live.

*Done of this:* names and a persistent id, for accounts. The `hello`
carries an account's session, the relay plays it under its handle, and
the account's id in the relay's database is the persistent id; nobody
else can take the handle, or one its owner renamed from in the last 30
days. Guests are still a name per session, marked as guests wherever
the room shows names. The document is no longer open to anyone who can
reach the relay: its socket needs a short-lived ticket the room socket
hands out. Roles, visibility and moderation within a room are not done.

## 1. The headless peer, and load

**What.** A peer with no page and no sound card: the Node wasm build,
Expand Down Expand Up @@ -247,7 +256,7 @@ Grouped by what unlocks what. Sizes are relative to a milestone.

| | Item | Needs | Size |
|---|---|---|---|
| 1 | Roles, visibility, persistent id (section 0) | M3 | S |
| 1 | Roles and visibility (section 0; the persistent id is done, for accounts) | M3 | S |
| 2 | Headless peer and load testing (section 1) | M3, before M5 | M |
| 3 | The recording format, fixed (section 4.1) | M3 | S |
| 4 | Spectators, with the relay fan-out and the delay (section 2) | M4, 1, 2 | M |
Expand Down Expand Up @@ -349,7 +358,8 @@ tens of thousands of concurrent sessions.
The short list of things that are free now and costly later, gathered
from above:

1. The relay enforces roles; `hello` carries a persistent id.
1. The relay enforces roles; `hello` carries a persistent id. (The id is
done, for accounts: section 0.)
2. Musicians' pages send a copy of their commands to the relay when the
room has spectators or recording on.
3. The recording format is the protocol plus a header naming the build
Expand Down
Loading
Loading