From bef3ccf4521d4ed431f3cdfc66e618c619ce28e4 Mon Sep 17 00:00:00 2001 From: "Simen A. W. Olsen" Date: Tue, 3 Nov 2026 22:08:49 +0100 Subject: Share one Fiken login between programs fikenconfig keeps an OAuth app and a grant per account in ~/.config/fiken, and finds the account that reaches a company. Renewing a grant takes a lock, so programs sharing it take turns. --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) (limited to 'README.md') diff --git a/README.md b/README.md index 7e59e41..1c6c2ad 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ gets a token for it. go get simenandre.no/fiken ``` -Two packages, and the split is the point: +Three packages, and the split is the point: - **`fiken`** knows how to put a bearer token in a header and how to read Fiken's answers. Where the token came from is not its business. @@ -15,6 +15,8 @@ Two packages, and the split is the point: loopback redirect, refreshing an expired grant, and a `0600` file to store it in. Refreshing is a side effect of asking for a token rather than a step the caller has to remember, which is the only way it gets done everywhere. +- **`fikenconfig`** is the login every program on the machine shares, so + logging in once, from any of them, logs in all of them. ```go store := fikenauth.NewStore(filepath.Join(dir, "fiken-token.json")) @@ -24,6 +26,60 @@ source.LoginCommand = "myapp fiken login" // named in the error if the grant die client, err := fiken.NewWithTokens(source) ``` +## One login for every program + +`fikenconfig` keeps one OAuth app and a grant per Fiken account in +`~/.config/fiken` (`$XDG_CONFIG_HOME/fiken`, or `$FIKEN_CONFIG_DIR`): + +``` +config.json the app, and each account with the companies it reaches +accounts/.json each account's grant +``` + +```json +{ + "app": { + "client_id": "...", + "client_secret": "...", + "redirect_url": "http://localhost:8137/callback" + }, + "accounts": { + "simen@sokkel.io": { + "name": "Simen", + "companies": [{ "name": "Sokkel AS", "slug": "sokkel-as" }] + }, + "post@bjerk.io": { "companies": [...], "token": "a personal API token" } + } +} +``` + +A program names the company it works for and gets the account that reaches +it. Which company that is stays the program's business: + +```go +cfg, err := fikenconfig.Open("") +cfg.LoginCommand = "myapp login" +client, err := cfg.Client(ctx, "sokkel-as") +``` + +Logging in records the grant under the email of the Fiken user who gave it: + +```go +grant, err := cfg.Auth().Login(ctx, announce) +email, err := cfg.AddGrant(ctx, grant) +``` + +A company added in Fiken after the login is found without logging in again: +when no account has it, `Client` asks each account once more. + +Sharing the grant file is the only safe way to share a grant, because Fiken +rotates the refresh token on every renewal and two copies means the first to +renew silently logs out the other. Renewing takes a lock beside the file and +reads it again under the lock, so two programs renewing at once take turns. +`$FIKEN_CLIENT_ID` and `$FIKEN_CLIENT_SECRET` override the app in the file. + +## Without the shared login + A personal API token works too, with no OAuth at all: ```go @@ -48,7 +104,10 @@ that is already gone is usually the state the caller wanted, not a failure. again, the other means try again. **Retries are bounded and only for what is retryable.** A 5xx or 429 is retried; -a 4xx is not, because sending the same bad request again cannot help. +a 4xx is not, because sending the same bad request again cannot help. Nothing +that creates something is retried at all: a 5xx does not say whether Fiken +acted before failing, and a draft or contact made twice is worse than an +error. ## Status -- cgit v1.3.1