diff options
Diffstat (limited to 'docs/multi-account-spec.org')
| -rw-r--r-- | docs/multi-account-spec.org | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/docs/multi-account-spec.org b/docs/multi-account-spec.org new file mode 100644 index 0000000..32a7527 --- /dev/null +++ b/docs/multi-account-spec.org @@ -0,0 +1,122 @@ +#+TITLE: pearl — Multi-Account Support Spec +#+AUTHOR: Craig Jennings +#+DATE: 2026-05-24 +#+STARTUP: showall + +* Status + +*DRAFT — design proposal; nothing in =pearl.el= has changed.* Open questions for Craig at the end. + +Lets one Emacs talk to more than one Linear workspace — a work account and a personal account — and switch between them without re-customizing. Raised by Craig: "I have a work account and a personal account. I would like to choose which account I'm working on easily and switch back and forth between the two fairly straightforwardly." + +* Problem + +Everything that identifies a workspace is a single global value today: + +- =pearl-api-key= — one key. +- =pearl-graphql-url= — one endpoint (always the same for Linear, but conceptually per-account). +- =pearl-org-file-path= — one active file. +- =pearl-default-team-id= — one team. +- the lookup caches (=pearl--cache-teams=, =-states=, =-team-collections=, =-views=, =-viewer=) — global, and *workspace-specific*: a personal account's teams and a work account's teams must never bleed together. + +To work the other account you'd re-customize the key (and team, and file) by hand and clear the cache. There's no notion of "which account am I on." + +* Current state + +- =pearl-api-key= is a plaintext defcustom, optionally loaded from =LINEAR_API_KEY= via =pearl-load-api-key-from-env=. =pearl--headers= reads it directly and errors when unset. +- =pearl-org-file-path= defaults to =gtd/linear.org=; one file holds the active view. +- The caches are module-level =defvar=s; =pearl-clear-cache= resets them all. Nothing scopes them to a workspace, so switching keys without clearing the cache would serve stale (wrong-account) teams/views/viewer. +- =pearl--cache-viewer= caches "who am I" — inherently per-account. + +* Proposed design + +** The account model + +A new defcustom =pearl-accounts=: an alist of named accounts, each a plist of the per-workspace settings. + +#+begin_src elisp +(setq pearl-accounts + '(("work" :api-key-source (auth-source "api.linear.app" "work") + :org-file "~/org/work-linear.org" + :default-team-id "TEAM_WORK") + ("personal" :api-key-source (auth-source "api.linear.app" "personal") + :org-file "~/org/personal-linear.org" + :default-team-id nil))) +#+end_src + +Each entry carries what's currently global: the credential, the org file, the default team, and (rarely needed) the endpoint. =:api-key-source= is how the key is *found*, not the key itself (see Credentials below). + +=pearl-active-account= holds the current account name. =pearl-default-account= picks the one used at startup. + +** Switching accounts + +=pearl-switch-account= (interactive): =completing-read= over =pearl-accounts= names, then: + +1. Set =pearl-active-account=. +2. Resolve and bind the active account's settings — the key, the org file, the default team — so the existing code keeps reading =pearl-api-key= / =pearl-org-file-path= / =pearl-default-team-id= unchanged (they become "the active account's values"). +3. *Clear the lookup caches* — they're workspace-specific; a switch must invalidate teams/states/collections/views/viewer so the next lookup refetches against the new workspace. +4. Optionally surface the new account's org file. + +The cleanest implementation keeps the rest of the package oblivious: a single resolver sets the three global-looking variables from the active account, and everything downstream (=pearl--headers=, =pearl--update-org-from-issues=, =new-issue=) works as-is. The accounts layer is a front-end over the existing globals. + +** Credentials + +Storing two API keys in plaintext customize is the wrong default. *Proposed: resolve keys through =auth-source=* (so =~/.authinfo.gpg= holds them), keyed per account. =:api-key-source= names the host + user to look up. A literal =:api-key "lin_..."= form stays supported as an escape hatch (and for the env-var path), but the documented default is encrypted auth-source. This also fixes a latent issue with the single-key model — the key sits in customize today. + +** Cache isolation + +Two options (open question 1): + +- *Clear on switch* (simpler): the caches stay global =defvar=s; =switch-account= clears them. Correct as long as every switch goes through the command. Minimal change. +- *Keyed by account* (stricter): the caches become per-account maps, so switching back to "work" reuses its warm cache instead of refetching. More code, better ergonomics for frequent flippers. + +Lean: clear-on-switch for v1 (it reuses =pearl-clear-cache=), keyed caches as a vNext nicety. + +** The active-file question + +Each account gets its own =:org-file= (proposed), so "work" issues and "personal" issues never share a buffer. Switching accounts switches which file is active. The alternative — one file with a per-account section — fights the active-file model (one view at a time) and risks cross-account =LINEAR-ID= collisions in the same buffer. Per-account files keep the existing single-active-view model intact, just parameterized by account. + +** What's account-scoped + +Everything that hits the API or writes the file: list/view/saved-query fetches, all the sync/save commands, field setters, comment commands, =new-issue=, the caches, the viewer identity (for comment-edit permission — "your own comments" is per-account). The account is resolved once at switch time into the globals the commands already read, so no command needs to know about accounts individually. + +* Proposed v1 decisions (this feature) + +1. =pearl-accounts= alist (name → plist of key-source / org-file / default-team / optional url); =pearl-active-account= + =pearl-default-account=. +2. =pearl-switch-account= sets the active account, resolves its settings into the existing global variables, and clears the caches. +3. Credentials resolve through =auth-source= by default; a literal key stays supported. +4. One org file per account (=:org-file=); switching switches the active file. +5. The accounts layer is a front-end over the current globals — downstream commands are unchanged. + +* Files touched + +- =pearl.el=: =pearl-accounts= / =pearl-active-account= / =pearl-default-account= defcustoms; =pearl-switch-account= + a =pearl--resolve-account= helper that binds the globals; an =auth-source= key resolver; a one-time migration of the legacy single-key config; a transient/keymap entry for switching. +- =docs/=: this spec. +- =README.org=: a multi-account setup section (auth-source entries, example =pearl-accounts=). + +* Test plan + +- =pearl--resolve-account=: a named account's plist sets the expected key / org-file / default-team; an unknown name errors. +- =switch-account= clears all five caches (assert each is nil after). +- auth-source resolver: a stubbed =auth-source-search= yields the key; a missing entry gives a clear error naming the account, not a generic "key not set". +- back-compat: with =pearl-accounts= nil and the legacy =pearl-api-key= set, everything works exactly as before (single-account path). +- viewer cache: switching accounts invalidates =pearl--cache-viewer= so comment-edit permission re-resolves against the new account. + +* Migration + +Back-compatible. With =pearl-accounts= unset, the package behaves exactly as today off the legacy =pearl-api-key= / =pearl-org-file-path= / =pearl-default-team-id=. First-run-with-accounts can offer to seed a single "default" account from the legacy values. The credential move to auth-source is opt-in: the literal-key form keeps working, so no one is forced to migrate secrets to adopt accounts. + +* Open questions for Craig + +1. *Cache strategy*: clear-on-switch (simple, refetches after every flip) or per-account keyed caches (warm on switch-back, more code)? How often do you actually flip — a few times a day, or constantly? +2. *Credential default*: auth-source / =~/.authinfo.gpg= as the documented path, with literal-key as the escape hatch — good? Or do you keep keys somewhere else (a password manager, env vars per account)? +3. *Active file*: one file per account (proposed) versus a single file you re-point — any reason you'd want both accounts' issues reachable without switching? +4. *Switch surface*: a plain =M-x pearl-switch-account=, plus a mode-line indicator of the current account? The indicator matters most — pushing a work edit while you think you're on personal (or vice versa) is the failure mode worth guarding against. +5. *Endpoint*: is =graphql-url= ever actually different per account (self-hosted / EU region), or is per-account =:url= over-engineering for two linear.app workspaces? + +* vNext / out of scope + +- Per-account keyed caches (if clear-on-switch proves annoying). +- Showing both accounts at once (split buffers / frames) — explicitly out; the model is one active account at a time. +- Per-account =pearl-saved-queries= (v1 shares the saved-query list across accounts; revisit if names collide or filters don't translate). +- Auto-detecting the account from the active file when you visit it. |
