Repository navigation
feat(auth): device pairing, self-revoke, and usable file RPCs - #85
Merged
Merged
Conversation
The pairing token travels in an "authorization: Bearer <token>" header (RFC 6750) rather than a bespoke x- key, so proxies and observability tooling that already understand bearer tokens keep working. This comment is the closest thing the contract has to a spec, since each client hardcodes the header name independently.
These RPCs existed in the proto but had no server implementation, so nothing depended on their shape. Revising them now, before anything does, is free; revising them later would not be. WriteFile becomes client-streaming. As a unary call with a bytes field it could not exceed gRPC's 4 MB default message limit, which made it unusable for anything this device produces. Sizes widen to uint64 and timestamps to int64. The largest recording measured on a device is 3.39 GB -- already within 21% of uint32's 4 GB ceiling, so a slightly longer run would have silently reported a truncated size. The uint32 timestamps would have broken in 2038. ListFiles takes a request rather than Empty, so a path can be listed at all, and files carry a path relative to the data root plus is_dir, so a result can be passed straight back to ReadFile or DeleteFile. ReadFile gains a resume offset; DeleteFile requires an explicit recursive flag. Paths are relative to the data root and must stay inside it. SFTP served the whole filesystem to any authenticated user; this does not.
Field 5 was `string type`, which shipped in @science-corporation/synapse 2.4.0, so generated clients expecting a string there already exist. Writing `bool is_dir = 5` over it changed the wire type on that slot: a bool is a varint, a string is length-delimited, so an old client would hit a wire-type mismatch rather than ignoring a field it does not recognise. is_dir moves to 6 and 5 is reserved by number and by name, so nothing can claim it again. A new server's response now carries no field 5 at all, which an old client skips as an unknown field. Checked the rest of the message set the same way. Nothing else changes a wire type: the uint32 -> uint64/int64 widenings on size, created, modified, start_offset and file_total_length are varint on both sides, and the name -> path renames keep their numbers and types, since names are not on the wire.
ListAuthClients and RevokeAuthClient let a paired client (in practice the device's touchscreen) see which computers hold access and revoke one. The touchscreen's own client is never listed.
SetAuthClientLifetime changes one paired client's lifetime (days from approval, 0 = never), and AuthClient reports it.
Plainer field comments, a one-line note on the reserved slot, and uint64 for timestamps like every other time field in the API (wire-compatible with int64 for the non-negative values sent).
ListAuthClients, RevokeAuthClient and SetAuthClientLifetime let any paired client list, revoke or extend every other client. Managing paired clients now happens on the device itself; the only remote call left is RevokeAuth, which revokes the token it is called with.
calvinleng-science
added a commit
that referenced
this pull request
Oct 9, 2026
calvinleng-science
force-pushed
the
calvin/grpc-auth
branch
from
October 9, 2026 21:55
e7a6ade to
d613b65
Compare
size, start_offset and file_total_length become size_bytes, start_offset_bytes and file_size_bytes, so the unit is in the name like the API's _ns/_ms time fields and available_disk_space_bytes. Field numbers are unchanged, so this is wire-compatible.
calvinleng-science
added a commit
to sciencecorp/synapse-python
that referenced
this pull request
Oct 14, 2026
Adds device pairing to `synapsectl` and the Python client, and moves `synapsectl file` and `deploy-model` from SFTP to the gRPC file RPCs. Companion to sciencecorp/synapse-api#85 (merged), sciencecorp/synapse-typescript#56 (released as 2.5.0), and the SciFi firmware that serves these RPCs. ## Pairing Newer SciFi firmware refuses every call except `Info` and `RequestAuth` until the caller is paired, and a person at the device approves each pairing. * **`synapsectl pair`** asks the device for access. The device's screen and the terminal both show the same six-digit code; the person at the device compares them and taps Allow. The token is saved in `~/.scifi-env` (mode 0600, written atomically), keyed on the device's serial. * **Every call carries the token automatically.** A client interceptor looks up the token for the device's serial on the first gated call and sends it as `authorization: Bearer <token>`. Open calls never trigger the lookup. * **`synapsectl unpair --uri <device>`** revokes this computer's access on the device (`RevokeAuth`), then forgets the token. `unpair <serial-or-name>` only forgets the local token, for a device that is gone. ## File transfer over gRPC SFTP needed its own password, so a device carried two credentials. The file RPCs are governed by the same pairing token as everything else, so the SFTP password goes away on firmware that serves them; the reachable tree is the same data directory the SFTP account was confined to. * `synapsectl file ls | get | put | rm`, now over gRPC. `put` streams in 1 MiB chunks (a single message could not exceed gRPC's 4 MB limit); `get` streams too. * **Safe interrupted transfers.** Downloads land in `<name>.partial` and are renamed into place only when complete, so a cancelled download never leaves a half-written file under the real name or touches an existing copy. `get` resumes a `.partial`, but only if the device's file is unchanged since (its size and modified time are recorded in `<name>.partial.json`); otherwise it starts over rather than splicing two files together. When interrupted it says where the partial is. * **Asks before overwriting**, locally on `get` and on the device on `put` (default no, `-y` to skip the prompt). `get` of a missing file or a directory says so plainly and exits non-zero. * `ls` hides dotfiles (the device keeps `.scifi-checked` markers beside recordings) unless `-a`. * **SFTP fallback for older device firmware.** If the device answers the file RPCs with `UNIMPLEMENTED`, `file ls | get | put | rm` and `deploy-model` say the device firmware predates pairing and should be updated, then use the previous SFTP path with the device's file-transfer password. `--username`, `--env-file` and `--forget-password` are live again for that path. The fallback asks before overwriting and before `rm -r`, like the gRPC path. On that firmware the SFTP root is not writable, so a top-level upload says to use a folder such as `disk_writer/` or `models/`. * **`deploy-model`** uploads over gRPC too. The device creates parent directories itself, so the separate mkdir/stat steps are gone. Version → **2.8.0**. Tracks synapse-api `main` (`f9fc77f`).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds device pairing (a human at the device approves each computer before it can control it) and makes the file RPCs able to carry real device files, so file transfer can move off SFTP onto the same authenticated channel.
Changes
api/auth.proto,RequestAuth(server-streaming). A client sendsAuthRequest{client_label}; the device answers with anAuthChallenge(a six-digit code shown both on the device screen and in the client), then anAuthResultonce the person at the device allows or denies. On success the result carries a token, which the client sends on every later call as anauthorization: Bearer <token>header.InfoandRequestAuthstay callable without a token; everything else requires one.RevokeAuth. Revokes the token the call carries, i.e. the caller's own. Listing, revoking or changing the expiry of other paired clients is deliberately not in the API: that happens on the device itself, so a paired client cannot extend its own access or lock others out.api/files.proto, file RPCs made usable:WriteFileis now client-streaming. As a unary call with onebytesfield it could not exceed gRPC's 4 MB default message size.ListFilestakes aListFilesRequest{path, recursive}instead ofEmpty, so a directory can actually be listed.name→paththroughout;ReadFileRequest.start_offsetfor resuming;DeleteFileRequest.recursive.uint64. The largest recording measured on a device was 3.39 GB, within 21% ofuint32's ceiling.ListFilesResponse.File.is_diris field 6, with 5 reserved: 5 wasstring type, and reusing it for aboolwould change its wire type under any client generated against the old message.VERSION→ 2.6.0.Testing
calvin/grpc-auth(scifi-server). Reference client: sciencecorp/synapse-pythoncalvin/grpc-auth(synapsectl pair,synapsectl unpair --uri,synapsectl file ls/get/put/rm).