Plaid Dashboard Login Implementation Plan
Date: 2026-05-13
Goal
Make Plaid setup in money feel close to the official Plaid CLI while preserving money's local-first boundaries.
The target user experience:
- During
money setup, after choosing Plaid, the user can choose either:- Open a browser, sign in to the Plaid Dashboard, and let
moneyfetch Plaid API keys automatically. - Manually paste
client_idandsecret, preserving the existing BYOK path.
- Open a browser, sign in to the Plaid Dashboard, and let
- The user can also run
money plaid logindirectly, without going through setup, to bootstrap Plaid credentials through the browser. - The canonical provider command remains available as
money providers plaid login;money plaid loginis a convenience alias for the Plaid-specific workflow.
Constraints
- Do not copy Plaid CLI source or donor code.
- Do not add AI chat, hosted billing, telemetry, a persistent web server, or a managed provider proxy.
- Do not silently fall back from Dashboard login to manual credentials. If Dashboard login fails, return an explicit error and tell the user to run the manual command if they choose.
- Keep Dashboard OAuth and Dashboard API behavior Plaid-specific. Do not add it to the generic Provider interface.
- Keep provider credentials in
money's config model: secrets in.env, YAML using explicitenv:references. - Do not print Plaid API secrets, Dashboard OAuth tokens, or reversible partial previews.
- Treat Plaid Dashboard endpoints as private Plaid CLI-compatible endpoints. The implementation must fail clearly if Plaid rejects non-official clients or changes the contract.
- Plaid Dashboard login is best-effort. It reuses the Plaid CLI-compatible OAuth client behavior observed during donor research, and it may stop working if Plaid changes Dashboard backend behavior.
- If Plaid locks down or rejects the
plaid-cliOAuth client path, stop the Dashboard login flow immediately. Do not try alternate private clients, hidden fallbacks, or scraping. Return a typed error that points users to manualmoney providers configure plaid.
Current State
Already present:
money setupcreates config,.env, encryption key, and encrypted SQLite database.money providers configure plaidwritesPLAID_CLIENT_IDandPLAID_SECRETinto the resolved.envand YAMLenv:references into config.money link <institution-query>andmoney providers plaid linkcreate a Plaid Link token, start a short-lived localhost helper, open Plaid Link, receive the public token, exchange it, and store the linked Provider Item in the encrypted store.
Missing:
- Plaid Dashboard OAuth.
- Plaid Dashboard team selection.
- Automatic Plaid API key fetch.
money plaid logintop-level convenience command.- Setup branch that lets the user choose automatic Plaid Dashboard login instead of manual key entry.
Command Surface
New Commands
money plaid login
money plaid logout
money providers plaid login
money providers plaid logoutmoney plaid login and money providers plaid login call the same implementation.
This is an intentional asymmetric top-level namespace. Plaid gets money plaid ... because the Dashboard OAuth bootstrap is Plaid-specific and modeled from the Plaid CLI donor; this does not imply future Providers must receive matching top-level namespaces unless they have similarly useful Provider-specific workflows.
Initial login flags:
--no-open Print the Dashboard OAuth URL without opening a browser.
--team <selector> Select a team by ID, client ID, name, or 1-based index when available.
--environment Plaid environment to write into money config. Default: sandbox.
--force Overwrite existing PLAID_CLIENT_ID / PLAID_SECRET values.--json implies automation mode and behaves as --no-open: print the authorization URL to stderr, do not wait for Enter, do not open the browser, wait for the localhost callback until timeout, and write exactly one final JSON envelope to stdout. Do not implement a JSON mode that prompts, blocks on stdin, or mixes progress text into stdout.
Later optional Dashboard commands, only if they are still useful after login is working:
money plaid teams list
money plaid teams use <selector>
money plaid keys fetchDo not implement teams and keys fetch until the core login flow proves useful. They expose more of Plaid's private Dashboard API surface and are not required for the setup experience.
Related follow-up plans:
docs/plans/2026-05-13-plaid-link-hardening.mddocs/plans/2026-05-13-plaid-sandbox-link.md
Those are deliberately outside this first implementation slice. This plan should land Dashboard login, setup UX, prompt infrastructure, contracts, and docs without also changing Plaid Link product behavior or adding Sandbox direct linking.
Setup UX
When money setup detects Plaid is unconfigured and the user chooses Plaid, show a second Plaid-specific choice using the project's standard Hermes-style selector implemented with charm.land/huh/v2. Do not use a numbered menu.
How do you want to configure Plaid?
> Sign in with Plaid Dashboard and fetch API keys automatically
Paste client ID and secret manually
Skip Plaid for nowBehavior:
- Dashboard login choice calls the same flow as
money plaid login. - Manual credential choice calls the existing provider configuration flow.
- Skip choice returns to setup without writing a partial Plaid provider block.
- Non-interactive usage must provide flags or return a validation error. It must not choose a path implicitly.
- The existing
runSetupWizardprovider-selection prompt currently uses a numbered menu. Migrate that prompt to the samehuh/v2selector while adding the Plaid method selector, so this plan does not preserve a known anti-pattern.
The setup flow should finish with:
- Config path.
- Env path.
- Database path.
- Whether Plaid credentials are present.
- Next command:
money link <institution-query>ormoney providers plaid link.
Dashboard OAuth Flow
Create a Plaid-specific Dashboard login module, not a generic provider abstraction.
Suggested package boundary:
internal/plaidlogin/
oauth.go PKCE, auth URL construction, token exchange inputs.
callback.go Short-lived localhost OAuth callback server.
dashboard.go Dashboard API HTTP client.
credentials.go Convert fetched Dashboard keys into money config writes.OAuth steps:
- Require an existing base
moneyconfig. Ifmoney plaid loginruns beforemoney setup, do not initialize config implicitly.- Human mode: print guidance to run
money setup, then exit withBASE_CONFIG_MISSING. - JSON mode: return a normal error envelope with
BASE_CONFIG_MISSING. - Detection must resolve the intended config path, stat/read that file, and classify
errors.Is(err, fs.ErrNotExist)asBASE_CONFIG_MISSING. Do not callconfig.Setup()frommoney plaid login;config.Setup()creates the config skeleton and would hide the missing-base-config error. - Do not rely only on full
config.Load()for this first check.config.Load()resolves everyenv:reference and can fail when Plaid credentials are exactly whatmoney plaid loginis trying to repair. Add or reuse a config helper that resolves the config path andenv_filepath from raw YAML without requiring Provider secrets to resolve.
- Human mode: print guidance to run
- Start a short-lived localhost callback server with separate bind and redirect hosts:
bindHost = "127.0.0.1"for the listener, so the server binds only loopback without depending on localhost DNS or IPv6 behavior.redirectHost = "localhost"for the OAuth redirect URI, matching the Plaid CLI donor evidence and avoiding strictredirect_urimismatch risk.
- Generate a random state and PKCE verifier.
- Build the Dashboard OAuth URL:
- Auth URL:
https://dashboard.plaid.com/oauth/authorize - Token URL:
https://api.dashboard.plaid.com/oauth/token - Client ID: Plaid CLI-compatible Dashboard OAuth client, currently observed as
plaid-cli - Redirect URL:
http://localhost:<port>/oauth/callback
- Auth URL:
- Print the URL. If
--no-openis set or--jsonis set, never open a browser. - Otherwise follow the same GitHub-style browser flow used by
money link: wait for the user to press Enter, then open the browser. - Wait for
/oauth/callback?code=...&state=.... - Reject missing code, OAuth error, wrong state, duplicate callback, wrong method, and timeout.
- Exchange the authorization code using PKCE.
- Store Dashboard auth only after token exchange succeeds. At this point
team_idandclient_idmay still be absent. - After team selection and key fetch, update the same Dashboard auth file with selected
team_idand Plaidclient_idwhen available.
Plaid CLI Compatibility Boundary
This feature is based on Plaid CLI-compatible Dashboard behavior observed from the installed Plaid CLI version 20260507-4d1b0ca0.
Known donor evidence:
- OAuth authorize URL:
https://dashboard.plaid.com/oauth/authorize. - OAuth token URL:
https://api.dashboard.plaid.com/oauth/token. - OAuth client ID:
plaid-cli. - Redirect URI string:
http://localhost:<port>/oauth/callback. - Token exchange body uses
grant_type=authorization_code,code,redirect_uri,client_id, and PKCEcode_verifier. - Refresh body uses
grant_type=refresh_tokenandrefresh_token. - Dashboard teams endpoint path:
/cli/teams/list. - Dashboard keys endpoint path:
/cli/keys/fetch. - Keys payload is expected to include
client_idplus environment secrets for at leastsandboxand optionallyproduction.
Details still requiring live or binary re-verification in Phase 1:
- Whether
/cli/keys/fetchis currentlyGETorPOST. - Exact team response field names.
- Exact keys response field names and nesting.
- Exact 401 body shape for
team_selection_required,api_keys_fetch_required, and any new Dashboard auth errors.
Implementation requirements:
- Add a Plaid CLI compatibility version constant in
internal/plaidlogin/, currently20260507-4d1b0ca0. The exact Go identifier may be exported or unexported depending on whetherdoctororlogin --jsonneeds to report it. - Add a short source comment beside that constant explaining that the Dashboard API contract is private and was last verified against the Plaid CLI version above.
- Document in
README.mdanddocs/GETTING_STARTED.mdthat Dashboard login is best-effort and manual provider configuration remains the durable fallback. - If
/cli/teams/list,/cli/keys/fetch, or OAuth token responses cannot be parsed into the expected typed structures, returnDASHBOARD_CONTRACT_CHANGED. - If Plaid rejects the OAuth client or returns an authorization failure that indicates the CLI-compatible path is blocked, return
PLAID_DASHBOARD_LOGIN_REJECTED. - Error messages for both cases must explicitly tell the user to run
money providers configure plaidmanually and include the Plaid keys URL fromconfig.PlaidSpec.HelpURL. - Do not retry with different undocumented endpoints, alternate client IDs, or browser scraping.
Dashboard Token Storage
Persist Dashboard OAuth state separately from provider API credentials:
<dirname(resolved config path)>/plaid-dashboard-auth.jsonFile mode: 0600.
Path rules:
- Default profile:
~/.money/plaid-dashboard-auth.json. - Named profile:
~/.money/profiles/<profile>/plaid-dashboard-auth.json. - Explicit
--config /tmp/foo/config.yaml:/tmp/foo/plaid-dashboard-auth.json. - Resolution follows the same
--config,MONEY_CONFIG, and--profilepath rules as normal config loading. money doctorwarns when the auth file exists with permissions broader than0600, matching.envpermission behavior.- Like
.env, POSIX permission warnings/fixes are skipped on Windows.
Schema:
{
"access_token": "...",
"refresh_token": "...",
"expires_at": "2026-05-13T15:04:05Z",
"team_id": "...",
"client_id": "..."
}team_id and client_id are optional until team selection and key fetch complete. A failed key fetch may leave a valid Dashboard auth file without Provider API credentials; that is acceptable because Dashboard auth is bootstrap state, not the Provider credential contract.
Rationale:
- Dashboard auth is bootstrap state. It must be readable before opening the encrypted SQLite store, otherwise
money plaid loginwould need to unlock or initialize the finance database before it can fetch API credentials. - Dashboard tokens have TTL and refresh-rotation semantics, which do not match the static
.envplus YAMLenv:Provider credential model. - The auth file uses the same local-user file-security level as
.env: resolved beside config, written0600, never printed, and warned on broad permissions. - The Provider adapter only needs
PLAID_CLIENT_ID,PLAID_SECRET,PLAID_ENV,PLAID_PRODUCTS,PLAID_COUNTRY_CODES, andPLAID_REDIRECT_URI. - Keeping Dashboard auth outside YAML avoids adding token fields to the stable Provider config contract.
money plaid logoutcan delete this file without touching API keys or linked Provider Items.
Team and Key Fetch Flow
After OAuth token exchange:
- Call Dashboard teams list.
- If no teams are returned, fail explicitly and write no Plaid credential changes.
- If one team is returned, select it.
- If multiple teams are returned:
- Use
--teamif provided. - Otherwise prompt interactively with the project's standard
huh/v2selector. - In non-interactive mode, return a validation error listing team selectors.
- Use
- Fetch API keys for the selected team.
- Choose the Plaid environment to configure:
- Default to
sandbox. - Honor
--environment production. - Reject any other value with validation error
INVALID_ENUM. - If the selected environment has no secret, return
PLAID_ENVIRONMENT_NOT_PROVISIONED.
- Default to
- Write
PLAID_CLIENT_IDandPLAID_SECRETthrough the shared Provider credential writer described below. - Update Plaid config fields:
environment: from--environment, defaulting tosandbox.products: from flag if provided, otherwiseconfig.PlaidSpec.OptionalFields.country_codes: from flag if provided, otherwiseconfig.PlaidSpec.OptionalFields.redirect_uri: from flag if provided, otherwiseconfig.PlaidSpec.OptionalFields.
- Run shared diagnostics and show only global blockers plus Plaid status.
Do not write partial provider credentials. If team selection or key fetch fails, leave existing Plaid credentials unchanged unless --force has already been validated and a complete fetched key set is ready.
Schema drift behavior is governed by the Plaid CLI compatibility boundary above. Missing required response fields, changed token shape, unrecognized team shape, or missing selected environment secrets are not recoverable by guessing.
Dashboard does not provide products, country_codes, or redirect_uri; money writes those from flags or from local Plaid defaults. Do not infer these values from Dashboard responses.
Team selector rules:
- Preserve the Dashboard
/cli/teams/listresponse order for display and 1-based index matching. - Team ID and client ID selectors use exact matching.
- Team name selectors use case-insensitive full-string matching.
- If one selector matches multiple teams or multiple selector modes, return an ambiguity validation error instead of choosing the first match.
Config and Secret Rules
- Reuse
config.ConfigureProviderfor final credential writes only after its current overwrite and env-path behavior is made compatible with this flow. - Current implementation trap:
config.ConfigureProvidercurrently skips existing env vars whenforce=falseand writes the default.envbeside config. Before Dashboard login depends on it, either update that function or add a small shared helper so provider credential writes:- resolve config path and env path without requiring existing Provider secrets to load,
- honor
cfg.EnvPath, including explicitenv_file, - check existing
PLAID_CLIENT_ID/PLAID_SECRETbefore writing, - ask or fail before overwrite according to the rules below,
- report
keys_writtenaccurately, and - never claim success when fetched keys were skipped because old env values already existed.
- The helper must still write YAML
env:references and never write direct YAML secrets. - Existing
PLAID_CLIENT_ID/PLAID_SECRETare not overwritten unless--forceis set or the user explicitly confirms in human mode. - JSON and non-interactive mode require
--forceto overwrite. - If existing Plaid API credentials are present and the selected environment matches the current config environment,
money plaid loginmay refresh Dashboard auth and preserve API keys without--force; report this ascredential_action: "preserved_existing"andkeys_written: 0. - If existing Plaid API credentials are present and the selected environment differs from current config, or the command would otherwise replace
PLAID_CLIENT_ID/PLAID_SECRET, require--forcein JSON/non-interactive mode or explicit human confirmation before OAuth starts. - If loaded config has effective
read_only: trueorMONEY_READ_ONLY=1, stop before OAuth or file writes with a read-only safety error; do not fetch tokens that cannot be persisted. - Read-only detection may require a partial raw-config load when full
config.Load()fails due to missing Provider credentials;MONEY_READ_ONLY=1is sufficient to block immediately, and rawread_only: truemust also block before OAuth. money plaid login --jsonmust keep stdout as one final JSON envelope. Progress messages and browser URL go to stderr. The CLI wiring must pass or use a stderr writer for this command, for example via Cobra'scmd.ErrOrStderr(), because the current providers command constructor only receives stdout.
Repeated login semantics:
- Re-running
money plaid loginalways starts a new browser OAuth flow and overwrites the Dashboard auth file after successful token exchange. - Token refresh happens inside the Dashboard client when a Dashboard API call needs it; it is not the user-visible behavior of the
logincommand. - Re-running
money plaid logindoes not overwrite existingPLAID_CLIENT_IDorPLAID_SECRETunless the fetched key write is explicitly allowed. - Human mode asks before replacing existing Plaid API credentials, defaulting to No through the standard
huh/v2selector. - JSON and non-interactive mode require
--forcebefore overwriting existing Plaid API credentials. PLAID_SECRETstores only the currently selected Plaid environment's secret. Switching environment requiresmoney plaid login --environment <sandbox|production> --force; this overwrites the previousPLAID_SECRET. Keeping both Sandbox and Production secrets locally is out of scope until the Provider config contract explicitly supports per-environment secrets.
Logout semantics:
money plaid logoutdeletes onlyplaid-dashboard-auth.json.- If the auth file is already absent, logout succeeds with
dashboard_auth_removed: false; this is idempotent and still preserves API keys. money plaid logoutdoes not deletePLAID_CLIENT_ID,PLAID_SECRET, linked Provider Items, accounts, transactions, or sync state.- Human output must say:
API keys remain in <env_path>. To remove them, edit the file or run money providers configure plaid with new values.
JSON Contracts and Errors
All new --json commands must use the standard envelope from docs/CONTRACTS.md: one stdout object with ok, data, meta, warnings, and errors.
money plaid login --json success data:
{
"provider": "plaid",
"team_id": "team_...",
"environment": "sandbox",
"keys_written": 2,
"credential_action": "written",
"dashboard_auth_path": "/Users/you/.money/plaid-dashboard-auth.json",
"next_command": "money link <institution-query>",
"config_path": "/Users/you/.money/config.yaml",
"env_path": "/Users/you/.money/.env"
}Do not include secrets, tokens, masked secrets, refresh tokens, or reversible previews.
money plaid logout --json success data:
{
"provider": "plaid",
"dashboard_auth_removed": true,
"dashboard_auth_path": "/Users/you/.money/plaid-dashboard-auth.json",
"api_keys_preserved": true,
"env_path": "/Users/you/.money/.env"
}Add these shared error codes to docs/CONTRACTS.md during implementation:
| Code | Category | Retryable | Exit code | Meaning |
|---|---|---|---|---|
BASE_CONFIG_MISSING | config | false | 3 | money plaid login was run before base config exists. |
NOT_LOGGED_IN | auth | false | 3 | Dashboard-only operation needs Dashboard auth. |
TEAM_SELECTION_REQUIRED | validation | false | 2 | Multiple teams exist and no interactive or --team selection was available. |
API_KEYS_FETCH_REQUIRED | auth | true | 3 | Dashboard auth exists but API keys still need to be fetched. |
DASHBOARD_TOKEN_REFRESH_FAILED | auth | false | 3 | Stored Dashboard refresh token cannot refresh; rerun money plaid login. |
DASHBOARD_CONTRACT_CHANGED | api | false | 6 | Plaid Dashboard private response shape no longer matches the known contract. |
PLAID_DASHBOARD_LOGIN_REJECTED | auth | false | 3 | Plaid rejected the CLI-compatible Dashboard OAuth path. |
PLAID_ENVIRONMENT_NOT_PROVISIONED | validation | false | 2 | Dashboard returned no secret for the selected Plaid environment. |
READ_ONLY_VIOLATION | safety | false | 4 | Dashboard login/logout would mutate local config, env, or auth files while read-only mode is enabled. |
In this repo's current code, "shared registry" means a central docs/CONTRACTS.md error-code section plus Go const names reused where errors are emitted. Do not invent a runtime map unless the implementation first introduces a small, obvious contracts-level constant block and updates existing call sites deliberately.
Prompt Boundary
Add the prompt abstraction before changing setup UX:
- Add a small
internal/promptboundary around human-mode selectors. - Use
charm.land/huh/v2behind that boundary for arrow-key selectors. - Keep JSON and non-TTY behavior prompt-free: required missing choices return validation errors.
- Provide fake prompt implementations for CLI/setup tests so tests do not depend on terminal UI or
bufio.Readermenu parsing. - Replace the existing
runSetupWizardnumbered provider menu through this boundary before adding the Plaid method selector.
Out of Scope
- Plaid CLI analytics.
- Plaid CLI feedback submission through Dashboard API.
- Dashboard webhooks.
- Trial plan onboarding.
- Item limit display.
- Full per-environment Plaid credential vault in
money. - A persistent local server.
- Generic OAuth support for all Providers.
Implementation Checklist
Phase 1: Evidence and Contract Lock
- [x] Re-read
AGENTS.md,docs/ARCHITECTURE.md,docs/CONFIG.md,docs/CONTRACTS.md, and this plan. - [x] Re-check current Plaid CLI docs and the installed Plaid CLI help before coding, because Dashboard endpoints are drift-prone.
- [x] Confirm exact Dashboard OAuth endpoint strings and CLI-compatible client ID from current Plaid CLI behavior.
- [x] Add a Plaid CLI compatibility version constant with value
20260507-4d1b0ca0and document the private compatibility boundary. - [x] Confirm the manual fallback path and error messages include the Plaid keys URL.
- [x] Confirm listener bind host and OAuth redirect host remain separate:
127.0.0.1for bind,localhostfor redirect URI.
Phase 2: Plaid Dashboard Login Module
- [x] Add Plaid Dashboard OAuth URL builder with PKCE and random state.
- [x] Add short-lived OAuth callback server.
- [x] Add token exchange client.
- [x] Add Dashboard token refresh using the persisted auth file.
- [x] Add Dashboard teams list client.
- [x] Add Dashboard keys fetch client.
- [x] Add typed errors for not logged in, team selection required, key fetch required, token refresh failed, Dashboard API contract changes, and Plaid Dashboard login rejection.
- [x] Add contract-drift tests for missing fields, unexpected fields, and unknown 401 Dashboard codes.
- [x] Add team selector logic with exact ID/client-ID matching, case-insensitive full-name matching, 1-based index matching, and ambiguity detection.
- [x] Add environment validation for
sandboxandproduction, withPLAID_ENVIRONMENT_NOT_PROVISIONEDwhen the selected environment has no fetched secret.
Phase 3: Config Integration
- [x] Add atomic Dashboard auth file read/write/delete helpers.
- [x] Store Dashboard auth at
<dirname(resolved config path)>/plaid-dashboard-auth.json. - [x] Add doctor warning for Dashboard auth file permissions broader than
0600, skipping POSIX permission checks on Windows. - [x] Add or expose a config path/env-file resolver that can read raw config path metadata without resolving Provider secret env refs.
- [x] Add fetched-key-to-provider-config adapter.
- [x] Ensure Plaid API credentials still end up in the resolved
cfg.EnvPathand YAMLenv:references. - [x] Fix or wrap
config.ConfigureProviderso Dashboard login honors explicitenv_fileand cannot silently skip fetched keys when existing env values are present. - [x] Write
products,country_codes, andredirect_urifrom flags orconfig.PlaidSpec.OptionalFields; do not expect them from Dashboard. - [x] Ensure direct YAML secrets are not written.
- [x] Ensure existing values require
--forceor explicit human confirmation. - [x] Enforce read-only mode before OAuth, token persistence, credential writes, and logout deletion.
- [x] Add the new shared error codes to
docs/CONTRACTS.mdwith category, retryable, and exit-code semantics. - [x] Add Go
constnames for the new error codes where they are emitted; do not introduce a runtime registry unless it is a deliberate separate refactor.
Phase 4: Prompt Boundary
- [x] Add
internal/promptwith a small selector interface. - [x] Add
charm.land/huh/v2behind that interface for human mode. - [x] Return validation errors rather than prompting in JSON or non-TTY mode.
- [x] Add fake prompt implementation for setup/login tests.
- [x] Migrate the existing
runSetupWizardprovider-selection prompt from numbered input to the prompt boundary.
Phase 5: CLI Commands
- [x] Add
money providers plaid login. - [x] Add
money plaid loginalias command. - [x] Add logout commands.
- [x] Wire
--no-open,--team,--environment,--force, and--json. - [x] Implement JSON live-login as
--no-open: print the authorization URL to stderr, do not wait for Enter, do not open the browser, wait only for callback/timeout, and keep stdout as the final JSON envelope. - [x] Ensure
money plaid loginfails withBASE_CONFIG_MISSINGif setup has not been run. - [x] Detect missing base config by resolving/statting the intended config file and checking
errors.Is(err, fs.ErrNotExist); do not callconfig.Setup()frommoney plaid login. - [x] Ensure
money plaid logincan repair missing Plaid env refs instead of failing early because fullconfig.Load()cannot resolve existingproviders.plaid.*references. - [x] Match
money linkbrowser ergonomics: print URL, wait for Enter, then open unless--no-open. - [x] Ensure
money plaid loginalways starts a new browser OAuth flow and only token refreshes inside Dashboard client calls. - [x] Ensure environment switching requires
--forcewhen it overwrites existingPLAID_SECRET. - [x] Implement repeated login and logout semantics exactly as documented above.
- [x] Keep errors explicit and categorized.
Phase 6: Setup Integration
- [x] Add Plaid-specific method choice after selecting Plaid in setup.
- [x] Implement Plaid method choice through
internal/prompt, not a numbered menu. - [x] Route automatic choice to the login implementation.
- [x] Route manual choice to the existing provider configure implementation.
- [x] Ensure skip writes nothing.
- [x] Preserve the existing
!state.json && state.stdin != nilgate around the setup wizard. In--jsonor non-TTY mode,money setupmust skip the Plaid method selector entirely and behave like today: write base config and return success without prompting. - [x] Ensure setup summaries and diagnostics reflect the chosen path.
Phase 7: Tests
- [x] Unit-test OAuth URL construction, state, PKCE verifier use, and redirect URL.
- [x] Unit-test bind host and redirect URI host are intentionally different.
- [x] Unit-test callback server success, OAuth error, missing code, wrong state, duplicate callback, and timeout.
- [x] Unit-test Dashboard client request paths, auth headers, response parsing, and error classification with
httptest. - [x] Unit-test team selection by index, ID, client ID, and name.
- [x] Unit-test multi-team interactive selection through the shared
huh/v2prompt boundary. - [x] Unit-test ambiguous team selectors return validation errors.
- [x] Unit-test key fetch writes the selected environment only.
- [x] Unit-test selected environment without a secret returns
PLAID_ENVIRONMENT_NOT_PROVISIONED. - [x] Unit-test
products,country_codes, andredirect_uricome from flags/defaults, not Dashboard. - [x] Unit-test credential writes honor explicit
env_file. - [x] Unit-test login repairs a config whose Plaid
env:refs exist but whose env values are missing. - [x] Unit-test existing Plaid env vars require
--forceor human confirmation and cannot produce a misleading success withkeys_written: 0. - [x] Unit-test existing Plaid env vars with same environment can preserve API keys and report
credential_action: "preserved_existing"pluskeys_written: 0. - [x] Unit-test read-only mode blocks login/logout before OAuth or file writes.
- [x] Unit-test fake Dashboard drift responses with missing fields, unexpected fields, and new 401 codes return typed errors instead of hidden fallback behavior.
- [x] Unit-test no partial writes on failed OAuth, failed teams, failed keys, or missing selected environment secret.
- [x] Unit-test
money plaid logoutdeletes onlyplaid-dashboard-auth.json, is idempotent when the auth file is already absent, and preservesPLAID_CLIENT_ID/PLAID_SECRET. - [x] CLI-test
money plaid loginandmoney providers plaid loginagainst fakes. - [x] Setup-test automatic Plaid login choice, manual choice, and skip choice.
- [x] Regression-test existing manual
money providers configure plaid. - [x] Regression-test existing Plaid Link flow remains unchanged.
Phase 8: Docs
- [x] Update
README.mdcommand list. - [x] Update
docs/GETTING_STARTED.mdsetup walkthrough. - [x] Update
docs/PRD.mdif the setup/login user-facing requirement changes. - [x] Update
docs/CONFIG.mdfor Plaid Dashboard auth storage beside the resolved config file. - [x] Update
docs/ARCHITECTURE.mdto describe Plaid Dashboard login as a credential-bootstrap flow. - [x] Update
docs/CONTRACTS.mdfor new command JSON envelopes and errors. - [x] Update
docs/DONORS.mdto record the Plaid CLI donor behavior and private-endpoint boundary. - [x] Update
docs/ROADMAP.mdwith the Plaid Dashboard Login milestone/status. - [x] Check
IMPLEMENTATION_PLAN.mdfor Plaid sections that need synchronization.
Phase 9: Verification
- [x] Run
go test ./...with writable Go caches if needed:
env GOCACHE=/private/tmp/money-go-build-2 GOPATH=/private/tmp/money-go go test ./...- [x] Run manual help smoke tests:
go run ./cmd/money plaid login --help
go run ./cmd/money providers plaid login --help
go run ./cmd/money setup --help- [x] Run a fake Dashboard login smoke test if the implementation exposes a local fake server.
- [x] Live Plaid Dashboard login was not run because it requires explicit user approval.
User Decisions Needed Before Implementation
- Approve use of Plaid CLI-compatible Dashboard OAuth and Dashboard API endpoints despite them being Plaid-specific and potentially private.
- Confirm
sandboxshould be the default environment written by Dashboard login.