Repository navigation
feat(locallibrary): sign in to each service once, and give plugins access to the account - #335
Merged
Merged
Conversation
…ts]] table
A plugin can say which account type it needs (google, soundcloud), the scopes it asks
for and why, and optional groups of scopes it asks for later. Each type comes once,
scopes are names asked for once, and the plugin needs a net: capability for the
requests its tokens go into. An account plugin can't ask for accounts itself.
account_placeholder reads {account:<type>} and {account:<type>/<access id>}. Plugins
still sign in with [[contributes.accounts]] until the host fills these.
Reading, writing and moving secrets become free functions over a Place (keychain service, item key, file), so accounts kept once per store can share them with plugin secrets. Signing in takes the endpoints it needs from an Endpoints view, built from a plugin's account or an account type. Plugin secrets are kept exactly where they were. No behaviour change.
…ccount plugins An account is kept once per store, named by the account plugin that signed it in and who signed in there (the account type's whoami answer, a string or a number). Its record (type, name, granted scopes, expiry) is in <store>/accounts.json under a lock, and its tokens alone in the keychain (account:<plugin>:<subject>@<store>) or the store's secrets file. Signing in asks for the type's always scopes, what the account already has, and what's being added, with one_account's params to sign in again as a known account and another_account's to add one. The granted scope is read from the token response, or the asked one when there's none. whoami follows redirects only within its origin. A refresh happens once per account across processes, under a lock per account, re-reading the tokens first. invalid_grant signs the account out, and signing out revokes where the type can. Plugins don't reach these accounts yet; [[contributes.accounts]] works as before.
… only where its scope covers the request
An access is the scopes one plugin may use on one account, kept beside the accounts in
accounts.json so it outlasts signing out. A plugin's first access for a type is its
default; {account:<type>} names the default and {account:<type>/<access id>} another
of its own. The host fills a token only when the plugin has the access, the account
is signed in, the access still has every scope the manifest requires, one of its
scopes covers the request's method and URL, and the account was granted what that
scope asks the service for. Otherwise the request fails before it's sent, with kind
access or scope and actions built from the plugin's manifest. A redirect hop no
scope covers loses the token. A plugin asking for a scope no installed account plugin
lists, or for hosts its net: capabilities don't reach, is left out at load.
…the sidecar accounts.list (accounts and accesses, never tokens; a signed-out account with accesses still listed), accounts.sign_in.begin/finish/paste/cancel (a type, and optionally the account plugin, the plugin it's for, the account to sign in again as, and scopes), accounts.sign_out and accounts.access.list/set_default/take_away. A sign-in for a plugin answers with the access it gave.
…cess, on the command line locallibrary account lists the DJ account, then every other account with its scopes and the plugins that have access to it. locallibrary account <type> sign-in (--with, --for, --scope) adds one, and <type>:<name> sign-out|status works on one. locallibrary plugin access <plugin> lists a plugin's accesses, with --default and --take-away. Before publishing to a plugin destination, a terminal asks once which account a plugin without access may use. --json, --yes, no terminal and the sidecar never ask: they fail saying which command gives the access.
The palette signs in to each account type, signs out of each account, and opens "access for <plugin>", a form that names the plugin, the account plugin when a type has several, the scopes and why the plugin asks.
youtube asks for [[accounts]] type google, scope youtube-upload, in place of its own Google account, and a 401 points at locallibrary account google sign-in --for youtube.
mixcloud asks for [[accounts]] type mixcloud, scope upload, in place of its own Mixcloud account, and a 401 points at locallibrary account mixcloud sign-in --for mixcloud.
soundcloud asks for [[accounts]] type soundcloud, scope upload, which covers both the upload and the look among your newest tracks after one that failed. A 401 points at locallibrary account soundcloud sign-in --for soundcloud.
Every plugin now signs in through account types, so the per-plugin flow goes: its
sign-in, the {account:<id>} fallback in net.rs, plugin account on the command line,
plugins.account* in the sidecar and its palette entries. The behaviour its tests
covered (the token exchange, declined, foreign and cancelled sign-ins, the port, public
https only, misplaced tokens, redirects) is tested on account types instead. plugin list
says which account types a plugin asks for, and plugin info its accesses.
Its Account struct and validation go. A manifest that still has the table fails saying contributes.accounts isn't a key locallibrary knows.
…s with properties The CLI account property now also covers plugin info and serve for an account plugin, a refused scope, pasting the address a sign-in came back to (and one from another sign-in), and status for an account that refreshes and one that doesn't. A new property walks the nine ways a request through an access is refused and checks the plugin passes each on. Hegel-only coverage goes from 95.32/93.79/93.38 to 95.62/94.04/93.71 (lines/functions/regions).
The player's walk property could start signing in through a built-in account plugin from the palette, which opened the real browser on the service's sign-in page and waited for it. In debug builds the opener now refuses to launch anything when LOCALLIBRARY_NO_OPEN is set or never_open() was called; the CLI test harness sets the variable and the player's test app calls never_open(), and the walks cancel a sign-in they start. Release builds are unchanged.
bnb
previously approved these changes
Oct 30, 2026
3 tasks done
# Conflicts: # apps/locallibrary/src/drive_cmd.rs # apps/locallibrary/src/plugin_cmd/account.rs # crates/locallibrary/src/plugin/net.rs # crates/locallibrary/tests/it/plugins/net.rs # crates/loclib-plugin/src/lib.rs # crates/loclib-plugin/src/manifest/tests.rs
bnb
previously approved these changes
Nov 1, 2026
SizeCompared with main. KiB are 1,024 bytes.
|
Rust coverageFrom run 91. Both are held to the thresholds in .cargo/config.toml.
|
CodeQL reads any name with "account" in it as sensitive, so printing the sign-in link from begin_account_sign_in and account_login_begin, and anything derived from the account subcommand's clap variant, counted as logging account data. A barrier model in .github/codeql/extensions says those return values carry no token, secret or verifier (an authorize URL with client_id, redirect_uri, scope, state, code_challenge and login_hint), and the variant is Command::Login, still the `account` subcommand.
bnb
previously approved these changes
Nov 1, 2026
# Conflicts: # crates/locallibrary/src/opener.rs # crates/loclib-plugin/README.md
bnb
approved these changes
Nov 1, 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
Follows #289. Each plugin used to declare and keep its own account (
[[contributes.accounts]]), so two plugins using Google signed in twice. Now a plugin asks for an account type and the scopes it needs, locallibrary keeps each account once per store, and the user gives each plugin access to an account. This is the second of the account PRs; signing in again for more scopes,accounts/ensureand expiry warnings follow.Added:
[[accounts]]in a plugin's manifest: an account type, its scopes and why, and optional scope groups. A plugin asking for a scope no installed account plugin lists, or for hosts itsnet:capabilities don't reach, is left out at load.<store>/accounts.jsonunder a lock, tokens alone in the keychain (account:<plugin>:<subject>@<store>) or the store's secrets file. Sign-in goes through the account type's endpoints, asks who signed in (whoami, same-origin redirects only) and keys the account by account plugin and subject. A refresh happens once per account across processes.invalid_grantsigns out; signing out revokes where the type can.{account:<type>}names the default,{account:<type>/<access id>}another. A token is filled in only when an access covers the request's method and URL and the service granted what that scope asks for; otherwise the request fails before it's sent, with kindaccessorscopeand actions built from the manifest. A redirect hop no scope covers loses the token.locallibrary account(lists the DJ account, then every account and who has access),locallibrary account <type> sign-in [--with] [--for] [--scope],<type>:<name> sign-out|status,locallibrary plugin access <plugin> [--default] [--take-away]; sidecaraccounts.list,accounts.sign_in.*,accounts.sign_out,accounts.access.*; the player's palette and an access form. Before publishing to a plugin destination, a terminal asks once which account a plugin may use;--json,--yes, no terminal and the sidecar never ask and fail with the command that gives access.LOCALLIBRARY_NO_OPENis set ornever_open()was called. The player's walk property could otherwise start a sign-in through a built-in account plugin and open the real browser on the service's sign-in page. Release builds are unchanged.Changed:
youtube-upload,upload,upload), and their 401 messages point atlocallibrary account <type> sign-in --for <plugin>.Placeshared by plugin secrets and accounts, with no change to where plugin secrets live.Removed:
[[contributes.accounts]], the per-plugin sign-in flow,plugin accountand the sidecar'splugins.account*. A manifest that still has the table fails sayingcontributes.accountsisn't a key locallibrary knows.How it was tested
account --jsonagainstaccounts.list, prompts only where they can be answered, and the nine ways a request through an access is refused. Each new test was broken once to see it fail. No test opens a browser or reaches a real service: they use a mock account plugin and a mock OAuth service.main: fmt, clippy (workspace and the CLI's CI feature sets), ast-grep,docs/rpccurrent, full nextest. Coverage: full suite 97.69% lines, 96.68% functions, 95.94% regions; hegel alone 95.62 / 94.04 / 93.71.Checklist
apps/locallibrary/CHANGELOG.mdhas a line under Unreleaseddocs/and--helpstill say what the code does (apps/locallibrary/docs/rpc/too)