Desktop tray client for Orebuoys Soundboard: global hotkeys for clips and functions on Wayland (portal/KGlobalAccel)
  • Go 91.6%
  • JavaScript 5.1%
  • HTML 1.6%
  • CSS 1.3%
  • Makefile 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Angus Kelsey 1eef0b91af
All checks were successful
Release / Build and publish release (push) Successful in 33s
Give the portal our app id, and clean up actions we removed
Two KDE symptoms, one identity and one reconciliation problem.

The GlobalShortcuts portal names its KGlobalAccel component after the caller's
app id. An unsandboxed process started from a terminal inherits the terminal's
app id (org.kde.konsole), so shortcuts appeared under a second "Konsole" entry.
The host registry only accepts an app id that names an installed desktop file,
and the per-user install had none. The portal now installs
io.github.psychofox.OrebuoysDesktop.desktop under XDG_DATA_HOME before
Register, unless a packaged copy is already visible. It never overwrites a
file it did not create. doctor reports whether the entry is visible.

BindShortcuts reconciles removed actions only when it is called with the new
set. bind() used to return early for an empty config, so removing the last
shortcut left its action in KDE forever. It now performs the empty bind (and
tolerates a portal that refuses it). run and reload with no shortcuts open a
short-lived cleanup session instead of skipping the backend. KGlobalAccel has
no equivalent bind call, so the fallback lists the component's actions and
unregisters every ID no longer in the config.

Tests cover the desktop entry (creation, 0644, no rewrite, packaged precedence)
and the stale-action diff. Live KDE dialog grouping and cleanup still need the
Plasma session.
2026-10-18 19:21:02 +10:30
.forgejo/workflows Pairing-first setup and tag-driven releases 2026-10-18 18:08:00 +10:30
docs Give the portal our app id, and clean up actions we removed 2026-10-18 19:21:02 +10:30
internal Give the portal our app id, and clean up actions we removed 2026-10-18 19:21:02 +10:30
packaging Pairing-first setup and tag-driven releases 2026-10-18 18:08:00 +10:30
tools/release Pairing-first setup and tag-driven releases 2026-10-18 18:08:00 +10:30
.gitignore orebuoys-desktop v0.1.0: global hotkeys for the board on Wayland 2026-10-18 17:40:11 +10:30
go.mod orebuoys-desktop v0.1.0: global hotkeys for the board on Wayland 2026-10-18 17:40:11 +10:30
go.sum orebuoys-desktop v0.1.0: global hotkeys for the board on Wayland 2026-10-18 17:40:11 +10:30
LICENSE orebuoys-desktop v0.1.0: global hotkeys for the board on Wayland 2026-10-18 17:40:11 +10:30
main.go orebuoys-desktop v0.1.0: global hotkeys for the board on Wayland 2026-10-18 17:40:11 +10:30
Makefile orebuoys-desktop v0.1.0: global hotkeys for the board on Wayland 2026-10-18 17:40:11 +10:30
README.md A setup page, and no more bootstrap dead ends 2026-10-18 19:04:15 +10:30

Orebuoys Desktop

A pure-Go tray daemon that fires an Orebuoys board's API from global keyboard shortcuts, on KDE Plasma 6 / Wayland first.

It is deliberately small. The board owns the library, tags, uploads, roles, history and playback controls; this process owns a token, a list of bindings and a tray icon. There is no embedded browser, no Electron, no Qt, no CGO and no local audio path — shortcuts queue playback in the board's voice channel, which is the only place the audio already lives.

  • Global shortcuts: the XDG GlobalShortcuts portal (org.freedesktop.portal.GlobalShortcuts), with a KGlobalAccel D-Bus fallback for KDE sessions where the portal is absent.
  • Tray: StatusNotifierItem + com.canonical.dbusmenu over D-Bus.
  • Board API: bearer token, ?guild= for multi-server boards.
  • Pairing: setup opens the board, you approve in the browser, and the token is saved without copying a secret by hand.

Quick start

orebuoys-desktop setup       # pair: approve the device in your browser
orebuoys-desktop configure   # open the setup page (or use the tray item)
# choose shortcuts, capture keys, accept the portal's assignment dialog
orebuoys-desktop run         # the tray daemon binds the saved keys

setup seeds snapshot, silence and repeat with no keys when the config has no bindings, so configure has rows to edit and run never fails with "the config has no shortcuts". configure works whether or not run is already active; a save asks a running daemon to reload.

What is unit-tested, and what is not

This repository was written on a machine with no compositor session, so the desktop integration is reasoned and unit-tested, not live-tested.

Unit-tested here (see go test ./...):

  • config loading, saving, the 0600 permission refusal, validation, stable shortcut IDs;
  • the pairing client (start, pending, token, expiry/404), injected polling, guild selection, browser-open fallback and the no-token run path;
  • trigger parsing and the KGlobalAccel Qt key encoding;
  • exact HTTP method, path, ?guild=, bearer header and JSON body for every action, against httptest;
  • tag/library name resolution, overlap/variant/exclusion propagation, repeat and silence;
  • the D-Bus signatures of portal and KGlobalAccel message structs;
  • the tray menu model (ids, labels, separators, clicks, revision changes);
  • the setup page: Host/Origin/token rejection, state shape without the board token, clip-search proxying and board errors, add/edit/remove validation, atomic saves, daemon reload, concurrent saves and repairing duplicate keys;
  • key-capture keysym names, plus Node DOM harnesses that drive the row editor's Edit → Capture → Save path (PUT carries the key, no second shortcut) and adding the same clip twice (second binding gets -2, two config entries);
  • portal fallback selection and the configure lifecycle, including flags with no config file and file creation on first save;
  • doctor formatting and its failure exits.

Still needs a real Plasma Wayland session: portal binding and the permission dialog, activation delivery from the compositor, tray rendering, KGlobalAccel registration, and of course audio in a voice channel. The checklist is at the end of docs/wayland-hotkeys.md.

Requirements

  • Go 1.24 or newer to build (developed with Go 1.27).
  • KDE Plasma 6 on Wayland for the intended experience, with:
    • xdg-desktop-portal (frontend),
    • xdg-desktop-portal-kde (implements GlobalShortcuts),
    • a StatusNotifier host (Plasma's system tray),
    • xdg-utils for xdg-open (the setup page and the board open in your browser; the URL is printed if it is missing).
  • A board account whose roles include the permissions for the actions you bind: play for clips/filters/silence/repeat, record for snapshots. A token is exactly its owner; it cannot exceed them.

Build and install

make check          # gofmt check, go vet, go test
make build          # CGO_ENABLED=0 builds ./orebuoys-desktop
sudo make install   # binary, desktop file and systemd user unit

make install puts the desktop file at /usr/local/share/applications/io.github.psychofox.OrebuoysDesktop.desktop. That file's basename is the application id the portal associates with this process (through org.freedesktop.host.portal.Registry). Install it; without it the portal may see an empty app id and may not remember key assignments.

Releases

Pushing a v* tag builds both architectures on the buildah Forgejo runner and publishes a release. A manual Release workflow dispatch takes the tag as an input, so an existing tag can be rebuilt and re-published.

Asset Contents
orebuoys-desktop-linux-amd64, -arm64 the static binary
orebuoys-desktop-linux-amd64.tar.gz, -arm64.tar.gz binary, README, LICENSE, desktop file and systemd user unit
SHA256SUMS checksums for the four files above
tar -xzf orebuoys-desktop-linux-amd64.tar.gz   # or -arm64
cd orebuoys-desktop-amd64                       # or orebuoys-desktop-arm64
sudo install -Dm755 orebuoys-desktop /usr/local/bin/orebuoys-desktop
sudo install -Dm644 io.github.psychofox.OrebuoysDesktop.desktop \
  /usr/local/share/applications/io.github.psychofox.OrebuoysDesktop.desktop
sudo install -Dm644 orebuoys-desktop.service \
  /usr/local/lib/systemd/user/orebuoys-desktop.service
cd ..                        # back to the directory with the downloaded assets
sha256sum -c SHA256SUMS

The release body comes from docs/releases/<tag>.md; see the template and the v0.1.0 notes. If there is no file for the tag, the workflow writes the commits since the previous v* tag (without merge commits). Either way it appends the install section and the asset table with checksums, and prints the whole body into the job log. A manual dispatch reads the notes and the release tool from the default branch, but builds the source at the tag it is given.

The workflow authenticates with secrets.RELEASE_TOKEN when that repository secret exists, otherwise with the automatic secrets.GITHUB_TOKEN. Forgejo's automatic token is writable for push and tag events and read-only for pull requests from forks; if the API answers 403, create a repository secret RELEASE_TOKEN with repository write and rerun the job. The token goes to the release helper in an environment file and is never printed.

Pair this device

The normal setup path is pairing. The app opens the board in your browser; you sign in and approve it, and the app receives its own token and saves it.

orebuoys-desktop setup

setup prompts for the board URL when the config has none. The usual flags also work, so the flow can be scripted:

orebuoys-desktop setup --server https://soundboard.example.com
orebuoys-desktop setup --server https://soundboard.example.com \
  --guild 900000000000000010

--name overrides the name shown on the approval page (default: “Orebuoys Desktop on ”).

What happens:

  1. setup checks /api/board and /healthz, then asks the board for a pairing code (POST /api/auth/desktop/start).
  2. It opens the verification URL with xdg-open. If no browser opens, paste the printed URL into a browser on this machine or another device.
  3. Sign in to the board and approve the device. Approval requires the tokens permission. The request expires after ten minutes; if it expires, run orebuoys-desktop setup again.
  4. The app polls until approval, saves the token in the 0600 config, then reads /api/me and /api/guilds. One enabled server is selected automatically; several produce a numbered prompt, and --guild answers it without a prompt.
  5. It prints the user, the default server and the token expiry.

If the config had no shortcuts, setup seeds snapshot, silence and repeat with no keys. That is deliberate: a paired device is not a configured one, and a binding with no key lets the portal ask for an assignment instead of leaving run with nothing to bind.

Then choose shortcuts and keys in the setup page:

orebuoys-desktop configure

The page opens in your browser on a loopback address. Search the board's clips, add function/clip/filter bindings, capture keys, and ask the portal to assign them. It works with or without a running daemon. Finally start the daemon:

orebuoys-desktop run

The tray's Configure shortcuts… opens the same page in a separate configure process, which is why it also works when the daemon has no shortcut backend yet.

Running run with no config token, no OREBUOYS_TOKEN and no --token offers the same pairing flow before it starts.

Running run with an empty shortcuts list does not fail. The daemon stays up with its tray and control bus, logs that no shortcuts are configured, and the tray's Configure shortcuts… adds them; a saved change asks the running daemon to reload. A list with shortcuts but no keys behaves the same way and says "no keys assigned yet" in the tray status.

The token is stored only in ~/.config/orebuoys-desktop/config.json (mode 0600). This program never prints it, logs it, puts it in a URL or passes it on a command line. Revoke it in the board under Account → API tokens; the row is named after the name you approved. Revocation is immediate, and running setup again creates a replacement.

Manual token (advanced / automation)

A token minted in the board still works as an override. Open Account → API tokens (or call POST /api/me/tokens as your signed-in session, e.g. { "name": "desktop", "expiresInDays": 90 }) and use it in either form:

OREBUOYS_TOKEN=sb_… orebuoys-desktop run
orebuoys-desktop run --token sb_…

--token wins over OREBUOYS_TOKEN, which wins over token in the config. Prefer the 0600 config file for a daemon that starts at login: a token in the environment or on a command line is visible to anything that can read your process list. A manually supplied token has no recorded expiry, so doctor describes it as not paired. It carries exactly its owner's permissions and guild access, so treat it as a password: mint one with an expiry, and revoke it when this machine stops using it.

Configuration

Default path: ~/.config/orebuoys-desktop/config.json, mode 0600. The file may contain the token, so group- or other-readable files are refused with the chmod command to run. --config PATH overrides the location.

{
  "server": "https://soundboard.example.com",
  "guild": "900000000000000010",
  "token": "sb_…",
  "paired": true,
  "tokenExpiresAt": "2027-01-16T12:00:00Z",
  "defaults": {
    "overlap": false,
    "variant": "normalized",
    "excludeRecent": 5,
    "tagMode": "or"
  },
  "shortcuts": [
    {
      "action": "clip",
      "clipId": "0f8c2c1e-…",
      "keys": "CTRL+ALT+1"
    },
    {
      "action": "filter",
      "q": "bomb",
      "tagName": "impact",
      "library": "SFX",
      "keys": "CTRL+ALT+2",
      "overlap": true,
      "excludeRecent": 8
    },
    { "action": "snapshot", "keys": "CTRL+ALT+S" },
    { "action": "silence",  "keys": "CTRL+ALT+X" },
    { "action": "repeat",   "keys": "CTRL+ALT+R" }
  ]
}
chmod 600 ~/.config/orebuoys-desktop/config.json
orebuoys-desktop doctor

Field notes:

  • server — board base URL. http/https, no query or fragment. A base path is allowed if the board is reverse-proxied under one.
  • guild — optional Discord server id. When set, every guild-scoped request carries ?guild=; the board still authorises it against your grants. setup writes it after reading /api/guilds; --guild overrides the choice. Omit it on a one-server board and the server chooses its default.
  • token — optional; --token wins over OREBUOYS_TOKEN, which wins over this field. setup writes the token here. Never pass the token on a command line you do not control: it is visible in /proc.
  • paired and tokenExpiresAt — bookkeeping written by setup so doctor can say the token came from pairing and when it expires. They grant nothing; a manually supplied token leaves them unset.
  • defaults — overlap joins the mix immediately instead of waiting in the queue; variant is original or normalized (omit for the board's setting); excludeRecent avoids the N most recently played clips; tagMode is or or and.
  • shortcuts[].keys — an XDG shortcuts-spec trigger such as CTRL+ALT+1 or CTRL+SHIFT+b. At least one modifier is required; a bare global key would steal that key from every application. Optional: without it the portal asks you for a key at startup (or the action appears unbound in System Settings under KGlobalAccel); the setup page can capture one for you. Two shortcuts may not share a trigger. With the KGlobalAccel fallback a configured key is applied at startup and therefore wins over an earlier System Settings edit; omit keys to leave that assignment to KGlobalAccel. A key KGlobalAccel cannot encode (for example a keypad keysym) is left unassigned with a warning, rather than taking the other bindings down; assign it in System Settings or use the portal.
  • shortcuts[].id — optional explicit id. Otherwise a stable id is derived: play-<clip id> (lower-cased and truncated to 20 safe characters) for clips, play-<hash> for filters, and snapshot, silence, repeat for the fixed actions. IDs are what your assigned key is stored against, so they do not change when you reorder the list. The setup page appends -2, -3, … when a derived id is already used, which is how one clip can have several hotkeys; two identical explicit id values are still refused, as is an explicit id that collides with a derived one.
  • Per-shortcut overlap, variant, excludeRecent, tagMode override the matching default for that binding.

Unknown JSON fields are refused, so a typo is an error rather than a shortcut that silently does nothing.

Commands

orebuoys-desktop [run] [flags]        run the tray daemon (default)
orebuoys-desktop setup [flags]        pair this device through the board in your browser
orebuoys-desktop configure [flags]    open the loopback shortcut setup page
orebuoys-desktop doctor [flags]       diagnose server, token, portal, tray and bindings
orebuoys-desktop list clips [flags]   list clips with their ids
orebuoys-desktop list tags [flags]    list tags with their ids and names
orebuoys-desktop --version

Global flags: --config, --server, --token, --guild, -h/--help. setup additionally accepts --name for the label shown on the approval page.

list clips accepts --q TEXT, --tag NAME|ID and --limit N (1–500). doctor exits non-zero when a required check fails and prints the fix next to the failure: which piece is missing, what to install or enable, and which permission a token lacks.

Examples:

orebuoys-desktop setup
orebuoys-desktop setup --server https://soundboard.example.com
orebuoys-desktop run
orebuoys-desktop list clips --q bomb
orebuoys-desktop list tags
orebuoys-desktop doctor --config ~/.config/orebuoys-desktop/config.json
orebuoys-desktop configure

What each action sends

All requests use Authorization: Bearer <token> and, when guild is set, ?guild=. The routes are the board's existing API; nothing here needs a new endpoint.

Action Request
clip POST /api/play?guild=… with {"clipId": …, "overlap"?, "variant"?}
filter optionally GET /api/tags and GET /api/libraries to resolve tagName/library to ids, then POST /api/play with {"q"?, "tagIds"?, "tagMode", "libraryId"?, "excludeRecent"?, "overlap"?, "variant"?}
snapshot POST /api/capture/snapshot?guild=… (needs record, a grant for the server, and you in the voice channel)
silence DELETE /api/play?guild=…
repeat POST /api/play with the clipId returned by the last successful play
list clips GET /api/clips?guild=…&sort=name&q=…&tags=…&limit=…
list tags GET /api/tags?guild=…&sort=name
doctor GET /healthz, GET /api/me, GET /api/guilds, plus the targets above

Silence is not a stop button. The board's member-side route withdraws your own waiting plays; it cannot cut off the clip the channel is already hearing, by design. Skip/clear are admin playback controls in the web board. There is no local preview, so there is nothing local to silence.

repeat remembers the last clip this process queued (including the clip the server picked for a filter). After a restart, or after Reload config (which creates a fresh dispatcher), it has nothing to repeat until you play something. A deleted clip comes back as the board's NOT_FOUND sentence.

A hotkey press that arrives while the previous action is still in flight is dropped, not queued. The board's own per-principal cooldown and play rate limit still apply.

Shortcut configuration

orebuoys-desktop configure (and the tray's Configure shortcuts…) opens a setup page on a loopback address. It edits the config file; it does not change the board or the token. It does not need a running daemon: the file is the source of truth, and the next run reads it. If a daemon is running, each save asks it to reload over the session bus, so the change is live immediately; the page says so, or says that the next run will pick it up.

The page shows:

  • the pairing state: board URL, whether the token came from pairing, your user and role, the pinned server, and the token's recorded expiry;
  • the binding list with the action description, keys, and Edit/Remove;
  • a clip search that goes through the daemon-side board client (/api/clips, with the configured guild and bearer token) and shows title, tags, duration and library, with an Add clip binding button. If no key was captured, the new row opens in the editor so it is not left silently unbound. Adding the same clip again creates a second binding with a suffixed id (play-<id>-2), so one sound can have several hotkeys;
  • function actions (snapshot, silence, repeat) and filter actions (search text, tag name/id, library name/id);
  • a key-capture control and a Set keys in the system dialog action.

The board token never reaches the page: searches and identity reads happen in the configure process. The page has no external assets, no inline script and no network access except back to its own loopback origin. It reads the token from the config file or OREBUOYS_TOKEN; a token supplied to the daemon only as run --token is not copied into the child process (argv is visible in /proc), so pair with setup or keep the token in the 0600 config if you want clip search in the page.

configure --server https://board.example.com --token sb_… works before a config file exists: the flags are the effective server and token, the page can search clips, and the first save creates the file (mode 0600) with those values. The usual flow still comes from setup; the flags make the page scriptable and are useful when the daemon runs from an environment token.

The page's security model

The listener binds 127.0.0.1 on an ephemeral port. Loopback is not a trust boundary — every local process and every page loaded in your browser can reach it — so the page also requires:

  • a per-run 256-bit random token, printed inside the URL fragment. The fragment is not sent in the HTTP request line or a server access log; the page reads it and sends it as Authorization: Bearer …. A token in the query string is refused.
  • an exact Host match for the listener's 127.0.0.1:port. A DNS-rebinding page that resolves its own name to 127.0.0.1 still sends its own Host and is refused.
  • an exact Origin match on every POST/PUT/DELETE, plus a JSON content type when there is a body, so another local page cannot submit a form to it.
  • a restrictive Content-Security-Policy (default-src 'none', scripts and styles only from self), X-Frame-Options: DENY, nosniff and no-referrer.

Every write is validated as a whole file (actions, filter shapes, trigger syntax, no duplicate IDs, no two shortcuts on one trigger) and replaced atomically with mode 0600; other fields such as defaults, the token, and the pairing metadata are preserved. The page can repair a file whose only problem is two shortcuts sharing a trigger, by removing or re-keying one.

Keys, the portal and browser capture

Capturing a key in the page only writes a preference to shortcuts[].keys. It is what the daemon asks the portal to use as preferred_trigger; the system dialog is what actually grants the global assignment. On Wayland the portal (the compositor) is the key authority, so expect it to confirm or ask you to assign the same key again.

Set keys in the system dialog asks a running daemon to call the portal's ConfigureShortcuts (portal interface version 2), which opens Plasma's shortcut editor for this app. With no daemon, the page opens a temporary GlobalShortcuts session with the file's bindings and requests the same dialog; that session stays alive until the page stops.

At startup the daemon calls BindShortcuts with your keys as preferred_trigger. Plasma shows its global-shortcuts dialog; accept it to grant the bindings. If you reject the dialog and no shortcut ends up active, startup fails with a clear error and nothing is registered. A portal that keeps existing bindings may answer success with the new ones unassigned; the daemon logs the bound subset, so "no key yet" is visible rather than guessed at.

The KGlobalAccel fallback has no per-application configuration method. The page's system-dialog action reports that and names System Settings → Shortcuts → Orebuoys Desktop, where you assign keys. This fallback is KDE-only and is not a Wayland-independent mechanism — KWin still owns the input.

Do not run two daemons: the second refuses to start because the control bus name is taken. configure is not a daemon; it is a page that can talk to the daemon you already have.

Tray

A left click opens the board in your browser. The menu is:

  • Status — the board URL, the pinned server, the number of bindings and the active shortcut backend.
  • Configure shortcuts… — starts orebuoys-desktop configure in a separate process and opens the same loopback setup page. It needs no shortcut backend, so it is also the way out of a config with no bindings.
  • Silence my waiting clips — DELETE /api/play; it withdraws your waiting plays only.
  • Reload config — re-reads the config and re-binds shortcuts. A reload creates a new shortcut session and a fresh repeat history.
  • Open board — xdg-open on the configured server.
  • Quit — stops the daemon cleanly (closes the portal session/KGlobalAccel actions and releases the tray and control bus names).

Multi-server boards

Set guild to the server the shortcuts should act in. setup asks once when the token can act in several enabled servers and writes the answer; orebuoys-desktop setup --guild <id> makes the same choice non-interactively. The daemon sends ?guild= on guild-scoped routes; the board checks that your token's owner may act there and that the server is enabled. Without guild, the board's own default (the seeded server when you may act in it) is used.

One daemon drives one server. Two instances on the same session would collide on the control bus name, the portal app id and the fixed shortcut ids (snapshot, silence, repeat), so switch servers by editing guild and using Reload config (or restarting), not by running two copies.

Run at login

The installed user unit is a sample:

mkdir -p ~/.config/systemd/user
cp /usr/local/lib/systemd/user/orebuoys-desktop.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now orebuoys-desktop.service
journalctl --user -u orebuoys-desktop -f

The unit starts with graphical-session.target and restarts on failure. Pair once with orebuoys-desktop setup; the unit then reads the token from the 0600 config. Do not put the token in ExecStart or an environment file that others can read.

Wayland and compositor caveats

The short version: under Wayland an application cannot grab keys itself; the compositor must do it through the portal. That works on Plasma 6. It does not become a general-purpose global-hotkey system for every nested or remote compositor.

  • Browser key capture is a preference, not a grab: the setup page can only record what you pressed while it was focused. It cannot make that key global; the portal (or KGlobalAccel) does. If the portal asks you to confirm or assign the key again, do that — the captured string is a suggestion.
  • gamescope / nested compositors: a nested compositor may not expose the host's GlobalShortcuts portal, and a portal service on a different session bus will not see the keys you press in the host session. Run the daemon in the session whose compositor owns the input and whose bus hosts the portal; test the binding there. See the checklist for a concrete test.
  • X11: the portal may still be available with the KDE backend; otherwise the KGlobalAccel fallback is the path. This build has not been live-tested on X11.
  • No portal backend: doctor reports the portal error and whether KGlobalAccel is reachable. On a non-KDE Wayland compositor without a GlobalShortcuts backend, there is no supported way for this daemon to grab keys; that is a compositor limitation, not a daemon setting.
  • Portal restarts: a portal restart invalidates the session. Restart the daemon (systemctl --user restart orebuoys-desktop) or use the tray's Reload config, which creates a fresh session and re-binds.
  • Tray host restarts: StatusNotifierItem registration is one-shot. The daemon retries for a minute at startup for a late Plasma tray, but if the tray host restarts later, restart the daemon to re-register the icon.

More detail, with the protocol evidence and limits: docs/wayland-hotkeys.md.

Security

  • The config may hold a token; it is required to be 0600. setup writes it with 0600 and preserves the rest of the file. The daemon never logs the token, the Authorization header, or request URLs with credentials; the pairing code is sent in a POST body, not in a URL this program logs.
  • The token is a board personal access token: it is exactly its owner, including the ability to play, snapshot (with record) and any other permission that owner holds. Pairing mints one with the board's default expiry (90 days) and records it in tokenExpiresAt; revoke it under Account → API tokens when this machine stops using it. Run doctor to see the recorded expiry.
  • The control object io.github.psychofox.OrebuoysDesktop is on the session bus, which is already a per-user trust boundary. Anything running as your user can ask it to reload, open the portal dialog or quit; that grants no new board authority.
  • The setup page is a loopback server, not a local-only file: the per-run URL token, exact Host/Origin checks and JSON content type are what stop another local page from driving it. The board token stays in the configure process; only status is returned to the page. Treat the printed URL as a short-lived secret while the page is running, and stop it with Ctrl-C when you are done.

Licence

AGPL-3.0. See LICENSE.

The board, its API and its documentation are a separate project; this repository contains only the desktop daemon.