Repository navigation
feat: sign locallibrary plugins in to services that want a client secret, through the website's token exchange - #261
Merged
Conversation
…'s client secrets Mixcloud, SoundCloud and Google want a client secret when a code is traded for tokens (SoundCloud at every refresh too), and a secret can't ship inside an app people download. POST /api/token-exchange/<service> takes the token request locallibrary would have sent the service, as JSON, adds the client id and secret from the environment, sends it to the service's token endpoint the way that service takes it, and answers what the service answered. The services live in src/lib/server/token-exchange/services.json, with a schema generated from the zod schema that reads it. A service without its two environment variables answers 503 temporarily_unavailable. The exchange only sends to the token endpoints listed there, follows no redirect, reads at most 64 KiB of a request or an answer, and logs the service, grant and status, never a code, token or secret.
…'s token exchange An account may name token_exchange in token_url's place: Local Network's https://localnetwork.computer/api/token-exchange/<service>, or the same path on this computer for a test server. Any other address is refused, since the exchange is sent the user's codes and refresh tokens, and an account names exactly one of the two. The host sends the exchange the same fields it would have sent the token endpoint, as a JSON object, because SvelteKit refuses a form posted without an Origin header. Signing in, refreshing and what an error says work as they do with a token endpoint. The RPC contract, the crate README, the plugins guide and the changelog say so.
Coverage Report for apps/web
File Coverage
SizeCompared with main. KiB are 1,024 bytes.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Rust coverageFrom run 472. Both are held to the thresholds in .cargo/config.toml.
|
bnb
approved these changes
Oct 26, 2026
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.
What and why
Mixcloud, SoundCloud and Google want a client secret when a sign-in's code is traded for tokens, and SoundCloud wants one at every refresh. A secret can't ship inside locallibrary, so the website now holds each one and makes the trade, and a plugin's account can name that exchange instead of the service's token endpoint.
Added:
POST /api/token-exchange/<service>(apps/web/src/routes/api/token-exchange/[service]/+server.ts, logic inapps/web/src/lib/server/token-exchange/exchange.ts). It takes the token request locallibrary would have sent the service, as JSON (authorization_codewithcode,redirect_uriandcode_verifier, orrefresh_token), adds the client id and secret from the environment, sends it to the service's token endpoint the way that service takes it, and answers what the service answered with the same status andcache-control: no-store. A success withouttoken_typegetsbearer. It sends only to the registry's token endpoints, follows no redirects, reads at most 64 KiB of a request or an answer, times out after 10 s, and logs the service, grant, status and timing but never a code, token or secret. It's JSON because SvelteKit's CSRF check refuses a form posted without anOriginheader.apps/web/src/lib/server/token-exchange/services.json, the services it knows: Google and SoundCloud as form POSTs, Mixcloud as a GET with its query (codes only, since Mixcloud documents no refresh). Each entry names its token URL, its request style and the environment variables for its client id and secret.services.schema.jsonis generated from the zod schema that reads it, and a test fails when it's stale.GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET,MIXCLOUD_CLIENT_ID/MIXCLOUD_CLIENT_SECRETandSOUNDCLOUD_CLIENT_ID/SOUNDCLOUD_CLIENT_SECRET. A service without both answers 503temporarily_unavailable, "Local Network isn't set up to sign in to X yet". An unknown service (including__proto__) is a 404.token_exchangeon[[contributes.accounts]](crates/loclib-plugin/src/hooks/accounts.rs), in place oftoken_url. It must behttps://localnetwork.computer/api/token-exchange/<service>(or that path on this computer, for tests), since whatever it names is sent the user's codes and refresh tokens. An account names exactly one of the two.crates/locallibrary/src/plugin/accounts.rs). Signing in, refreshing, refresh-token rotation and error messages work as they do with a token endpoint.Changed:
token_urlis optional in the manifest and the RPC contract (apps/locallibrary/docs/rpc/). Existing manifests stay valid.apps/web/README.md(configuration, a token exchange section, security notes, layout),apps/web/.env.example,crates/loclib-plugin/README.md,apps/locallibrary/docs/plugins.mdand the CHANGELOG.Not covered here: Mixcloud takes its access token as a query parameter and SoundCloud's API expects
Authorization: OAuth, while the host always sendsBearer, and SoundCloud matches its registered redirect URI exactly. Plugins for those services will need the host to say how an account's token is sent.How it was tested
apps/web/src/lib/server/token-exchange/exchange.test.tsandservices.test.tsrun the route against stand-in token endpoints at the registry's real addresses: the secret reaching the service, refresh and rotation, Mixcloud's GET, unknown services, the unset-environment answer, size limits, an unreachable or garbled service, and that logs never hold a code, token or secret.cargo fmt --check, workspace clippy with-D warnings, nextest for loclib-plugin, locallibrary and locallibrary-cli, and coverage over loclib-plugin and locallibrary (lines 97.36, functions 95.34, regions 94.87).Checklist
apps/locallibrary/CHANGELOG.mdhas a line under Unreleased, if people using it will noticedocs/and--helpstill say what the code does (apps/locallibrary/docs/rpc/too, for a sidecar change)