Files
pad/cmd
xarmian d915cc3cf8 feat(cli): per-server credentials in credentials.json with v1 → v2 migration (TASK-1228) (#435)
Implements IDEA-1226. ~/.pad/credentials.json is now a map keyed by
server URL so one developer machine can stay logged in to multiple Pad
instances simultaneously — `apm/` repo on Pad Cloud, `target/` repo on
local, `testing/` repo on staging — without each `pad init --url <other>`
clobbering the previous server's credentials.

## On-disk format

v2 (new):
  {
    "version": 2,
    "credentials": {
      "https://app.getpad.dev":  {"token": "...", "user_id": "...", ...},
      "http://127.0.0.1:7777":   {"token": "...", "user_id": "...", ...}
    }
  }

v1 (legacy, read-only): {"server_url": "...", "token": "...", "user_id": "...", ...}

Reads transparently migrate v1 → v2 in memory; writes always emit v2.
Side-effect-free reads — the on-disk file stays v1 until login/logout/
setup triggers a Save, which is when migration becomes durable. This
keeps `pad <read-only-command>` from rewriting credentials.json on
every invocation just because the binary upgraded.

## API

Replaces the three top-level helpers (LoadCredentials / SaveCredentials /
DeleteCredentials) with a CredentialStore type:

  - LoadStore() (*CredentialStore, error)
  - (s).Get(serverURL) *Credentials       // nil-receiver safe
  - (s).Set(serverURL, *Credentials)
  - (s).Delete(serverURL)
  - (s).Save() error
  - WipeCredentialsFile() error           // file-level — replaces DeleteCredentials

URL canonicalization is built in: trailing slash + surrounding whitespace
are stripped before lookup/store, so http://x:7777 and http://x:7777/
hit the same bucket. Same rule cmd/pad/server_info.go was already
applying via its now-redundant normalizeURL — removed.

No top-level `default` field. The configured server (cfg.BaseURL() from
~/.pad/config.toml or --url) is always the source of truth for "which
server am I targeting" — a separate `default` would create a second
source of truth and the split-brain bugs that follow.

## Behavioral changes

- `pad init --url <other>` against a server you've authed to before now
  reuses the saved credential instead of clobbering it.
- `pad auth logout` removes only the configured server's entry. Other
  servers' tokens stay intact (pre-fix: wiped the whole file).
- `pad auth whoami` reads only the entry matching the configured server.
- Single-server users see no behavior change — one entry, identical
  shape per entry, identical UX.

## Compat shims removed

LoadCredentials / SaveCredentials / DeleteCredentials are deleted
outright (no // Deprecated lifecycle) — they're internal package
helpers with no external API contract. All 10 call sites in cmd/pad/
and internal/cli/ are migrated to the per-server API in this PR.

## Tests

internal/cli/credentials_test.go (15 tests):
- File missing / empty → empty store (callers don't need nil checks)
- v1 format reads + migrates in memory
- v1 with empty token → empty store (no phantom entries)
- v1 migration is durable on first Save (file flips to v2)
- v2 round-trip preserves multiple entries
- Set adds + replaces; mirrors URL into ServerURL field
- Delete keeps siblings (multi-server keystone behavior)
- Delete on absent key is a no-op
- Nil receiver Get/Delete don't panic (NewClientFromURL relies on this)
- URL normalization (trailing slash + whitespace)
- Save preserves all entries across the file boundary
- Save uses 0600 permissions
- WipeCredentialsFile removes the file + is idempotent
- Garbage file errors loudly (so we never silently lose data)

Existing tests unchanged. Full suite + lint + web-check green.

Closes: TASK-1228.
Implements: IDEA-1226.
2026-05-07 20:23:13 -04:00
..