Skip to content

Commit aac6c4b

Browse files
DTTerastarclaude
andcommitted
fixup: address PR review — README, gofmt, cache schema version
Review feedback on #44: - **Must fix #1: README.md stale.** Replaced the "no token cache" paragraph with the cache location ($XDG_CACHE_HOME), the CRONOMETER_NO_CACHE opt-out, the `auth logout` subcommand, and a security note ("treat session.json like a password"). Folds in the reviewer's nice-to-have #5 (security guidance) at the same time. - **Must fix #2: gofmt.** Ran `gofmt -w` on `cmd/format.go` and `internal/cronoclient/daterange_test.go` (trailing blank line + struct field alignment). `internal/cronoapi/gwt.go` is also flagged but is pre-existing from #37 — landed as a separate chore commit so this PR's diff stays scoped to release-blocker work. - **Nice-to-have #4: cache schema version.** Added `cacheSchemaVersion = 1` and a `Version int` field on cachedSession. Old/mismatched versions are silently treated as a miss so a future incompatible bump triggers a transparent re-login instead of a JSON-shape error. New test: TestSessionCacheVersionMismatch. Skipped: race on concurrent fresh logins (#6) — reviewer agreed this is fine for a single-user CLI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fd7f1fd commit aac6c4b

5 files changed

Lines changed: 53 additions & 5 deletions

File tree

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,17 @@ export CRONOMETER_USERNAME="you@example.com"
7878
export CRONOMETER_PASSWORD="your-cronometer-password"
7979
```
8080

81-
The CLI logs in on every invocation; there's no token cache. Cronometer doesn't (yet) offer SSO or API tokens for individuals, so a real password is the only auth option.
81+
Cronometer doesn't (yet) offer SSO or API tokens for individuals, so a real password is the only auth option.
82+
83+
After the first successful login, the session (auth token + cookies) is cached at `$XDG_CACHE_HOME/crono-export/session.json` (file mode `0600`, directory mode `0700`; on macOS this resolves to `~/Library/Caches/crono-export/session.json`). Subsequent invocations reuse the cached session and skip the login handshake — useful for LLM-agent workflows that fire several commands in quick succession (a fresh login on every call trips Cronometer's "Too Many Attempts" throttle after ~6 requests). When the cached session goes stale, the CLI transparently re-logs in and retries once.
84+
85+
Escape hatches:
86+
87+
- `CRONOMETER_NO_CACHE=1` forces a fresh login on every invocation (and skips writing the cache).
88+
- `crono-export auth logout` deletes the cached session.
89+
- `crono-export auth status` reports whether credentials are set and whether a session is cached.
90+
91+
Treat `session.json` like a password: don't sync it to a shared backup, and don't commit it.
8292

8393
## Usage
8494

cmd/format.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,4 +413,3 @@ func pickKey(row map[string]any, candidates ...string) string {
413413
}
414414
return ""
415415
}
416-

internal/cronoclient/daterange_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ func TestResolveDateRange(t *testing.T) {
1010
day := func(y int, m time.Month, d int) time.Time { return time.Date(y, m, d, 0, 0, 0, 0, time.UTC) }
1111

1212
cases := []struct {
13-
name string
14-
since, until string
13+
name string
14+
since, until string
1515
wantStart, wantEnd time.Time
16-
wantEmpty bool
16+
wantEmpty bool
1717
}{
1818
{
1919
name: "both empty defaults to 7d ending today",

internal/cronoclient/session.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ import (
1111
"github.com/quantcli/crono-export-cli/internal/cronoapi"
1212
)
1313

14+
// cacheSchemaVersion is bumped whenever the on-disk session shape
15+
// changes incompatibly. An older cache is silently ignored (treated
16+
// as a miss) so existing users transparently re-login on upgrade.
17+
const cacheSchemaVersion = 1
18+
1419
// cachedSession is the on-disk representation of a Cronometer login.
1520
// We key by username so flipping CRONOMETER_USERNAME invalidates the
1621
// cache automatically instead of replaying another user's session.
1722
type cachedSession struct {
23+
Version int `json:"version"`
1824
Username string `json:"username"`
1925
Session cronoapi.Session `json:"session"`
2026
SavedAt time.Time `json:"saved_at"`
@@ -60,6 +66,9 @@ func loadCachedSession(user string) (*cachedSession, error) {
6066
if err := json.Unmarshal(data, &s); err != nil {
6167
return nil, nil
6268
}
69+
if s.Version != cacheSchemaVersion {
70+
return nil, nil
71+
}
6372
if s.Username != user {
6473
return nil, nil
6574
}
@@ -81,6 +90,7 @@ func saveCachedSession(user string, snap cronoapi.Session) error {
8190
return err
8291
}
8392
data, err := json.Marshal(cachedSession{
93+
Version: cacheSchemaVersion,
8494
Username: user,
8595
Session: snap,
8696
SavedAt: time.Now(),

internal/cronoclient/session_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cronoclient
33
import (
44
"net/http"
55
"os"
6+
"path/filepath"
67
"testing"
78

89
"github.com/quantcli/crono-export-cli/internal/cronoapi"
@@ -64,3 +65,31 @@ func TestSessionCacheRoundTrip(t *testing.T) {
6465
t.Errorf("delete-when-missing should be no-op, got %v", err)
6566
}
6667
}
68+
69+
// TestSessionCacheVersionMismatch confirms that a cache written under
70+
// a different schema version is silently treated as a miss, so a
71+
// future incompatible bump triggers a transparent re-login instead of
72+
// a JSON-shape error.
73+
func TestSessionCacheVersionMismatch(t *testing.T) {
74+
tmp := t.TempDir()
75+
t.Setenv("XDG_CACHE_HOME", tmp)
76+
t.Setenv("HOME", tmp)
77+
78+
p := SessionCachePath()
79+
if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil {
80+
t.Fatal(err)
81+
}
82+
// Hand-written cache pretending to be a future schema version.
83+
body := `{"version":999,"username":"alice@example.com","session":{"user_id":1,"auth_token":"deadbeef","cookies":null}}`
84+
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
85+
t.Fatal(err)
86+
}
87+
88+
got, err := loadCachedSession("alice@example.com")
89+
if err != nil {
90+
t.Fatalf("load: %v", err)
91+
}
92+
if got != nil {
93+
t.Errorf("expected nil cache for mismatched version, got %+v", got)
94+
}
95+
}

0 commit comments

Comments
 (0)