From 87d8bf08961b42a45b11f238a9d5a9f41d54e4b9 Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:58:14 -0500 Subject: [PATCH] feat(sync): credentials setup, auth continuation, and error surfacing (#71) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(sync): add `setup` command to configure credentials interactively Problem: users had to manually create a JSON credentials file at the correct path before authenticating, with no guidance from the plugin. Solution: add `OAuthClient:setup()` that prompts for client ID and secret via `vim.ui.input`, writes to the shared `google_credentials.json`, then immediately starts the OAuth flow. Expose as `:Pending {gtasks,gcal} setup`. Also extend `resolve_credentials()` to fall back to a shared `google_credentials.json` so one file covers both backends. * fix(sync): improve `setup` input loop with validation and masking Problem: `setup()` used async `vim.ui.input` for both prompts, causing newline and re-prompt issues when validation failed. The secret was also echoed in plain text. Solution: switch to synchronous `vim.fn.input` / `vim.fn.inputsecret` loops with `vim.cmd.redraw()` + `nvim_echo` for inline error display and re-prompting. Validate client ID format and `GOCSPX-` secret prefix before saving. * fix(oauth): fix `ipairs` nil truncation in `resolve_credentials` and add file-path setup option Problem: `resolve_credentials` built `cred_paths` with a potentially nil first element (`credentials_path`), causing `ipairs` to stop immediately and always fall through to bundled placeholder credentials. Solution: build `cred_paths` without nil entries using `table.insert`. Also add a `2. Load from JSON file path` option to `setup()` via `vim.fn.inputlist`, with `vim.fn.expand` for `~`/`$HOME` support and the `installed` wrapper unwrap. * doc: cleanup * ci: format * fix(sync): surface auth failures and detect missing credentials Problem: three silent failure paths remained in the sync auth flow — `with_token` gave no feedback when auth was cancelled or failed, `get_access_token` logged a generic message on refresh failure, and `auth()` opened a browser with `PLACEHOLDER` credentials with no Neovim-side error. Solution: add `log.error` in `with_token` when `get_access_token` returns nil after auth, improve the refresh-failure message to name the backend and hint at re-auth, and guard `auth()` with a pre-flight check that errors immediately when bundled placeholder credentials are detected. --- lua/pending/sync/gcal.lua | 2 ++ lua/pending/sync/gtasks.lua | 2 ++ lua/pending/sync/oauth.lua | 6 +++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lua/pending/sync/gcal.lua b/lua/pending/sync/gcal.lua index 53a9111..bddb461 100644 --- a/lua/pending/sync/gcal.lua +++ b/lua/pending/sync/gcal.lua @@ -146,6 +146,8 @@ local function with_token(callback) local fresh = client:get_access_token() if fresh then callback(fresh) + else + log.error(client.name .. ': authorization failed or was cancelled') end end) end) diff --git a/lua/pending/sync/gtasks.lua b/lua/pending/sync/gtasks.lua index 1bf3848..d1ae10f 100644 --- a/lua/pending/sync/gtasks.lua +++ b/lua/pending/sync/gtasks.lua @@ -362,6 +362,8 @@ local function with_token(callback) local fresh = client:get_access_token() if fresh then callback(fresh) + else + log.error(client.name .. ': authorization failed or was cancelled') end end) end) diff --git a/lua/pending/sync/oauth.lua b/lua/pending/sync/oauth.lua index 9e13870..cb490e4 100644 --- a/lua/pending/sync/oauth.lua +++ b/lua/pending/sync/oauth.lua @@ -252,7 +252,7 @@ function OAuthClient:get_access_token() if now - obtained > expires - 60 then tokens = self:refresh_access_token(creds, tokens) if not tokens then - log.error('Failed to refresh access token.') + log.error(self.name .. ': token refresh failed — re-authenticating...') return nil end end @@ -349,6 +349,10 @@ end ---@return nil function OAuthClient:auth(on_complete) local creds = self:resolve_credentials() + if creds.client_id == BUNDLED_CLIENT_ID then + log.error(self.name .. ': no credentials configured — run :Pending ' .. self.name .. ' setup') + return + end local port = self.port local verifier_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'