F6b — speak songbird's real cookie-session auth (corrects F6's brief contract) - #16
Conversation
The brief's §4 contract was wrong (no bearer-token auth in songbird). Reworked
the finalize integration against songbird source @ 89f894e:
- Auth: LOGIN-PER-SEND. HttpImportPoster does POST /api/v1/auth/login {username,
password} → captures the songbird_session cookie from Set-Cookie → POST
/api/v1/import with a Cookie header → best-effort POST /api/v1/auth/logout
(net-zero session rows; sessions are 30-day, accumulate per login). Explicit
cookie handling, no global CookieManager. Credentials are header/body only,
never logged.
- Seam: ImportPoster.send(baseUrl, username, password, json) -> SongbirdExchange
{login, imported}; PostResult unchanged.
- ImportResult.from(SongbirdExchange): UNREACHABLE / LOGIN_REJECTED (login 401) /
HTTP_ERROR (login other non-2xx, or any import non-2xx incl. 401/502 after a
good login) / SUCCESS parsing the real ImportSummary
{annotations,sermon_notes each created/skipped/failed, errors[]}; failed>0 is
surfaced prominently with the first error. Tolerant parsing kept.
- Settings: base URL + username + masked password (EncryptedSharedPreferences);
the dead bearer token pref is gone (no migration — it never worked). canSend +
isConfigured now require all three.
- UI: SettingsFragment username/password fields; FinalizeFragment passes creds +
renders login-rejected / with-failures.
Tests reworked against the real shape: ImportResultTest (13) + committed fixture
import_summary.json (provenance: songbird tests/import_export_test.py @ 89f894e),
FinalizeViewModelTest (4, fake two-step poster), SongbirdSettingsTest (3-field).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Brief errata under Environment: brief §4's Bearer-token auth and
{"annotations":{created,skipped}} response are superseded — songbird is Argon2
cookie-session (login → songbird_session cookie → cookied import → logout) and
the import response is ImportSummary (annotations/sermon_notes each
created/skipped/failed + errors[]). Verified against songbird source @ 89f894e;
the brief stays unedited. Secrets line now: username+password, encrypted.
Slice map: annotate F6 as built to a wrong brief auth contract; add F6b.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The merge gate runs :app:lintPaddleDebug (build-release.yml), which my slice
verifications never ran (assemble + unit tests only). Lint aborts on
MissingDefaultResource: no_document_to_share_export_first existed in 10 locale
files (values-{de,es,fa,hi,hu,it,pl,pt,ro,ru}) with no default — dead cruft left
by F1c's incomplete locale sweep (an export/share message; nothing references it).
Removed it from all 10 locales; the full gate (compile + testPaddleDebugUnitTest +
lintPaddleDebug) is now green.
CLAUDE.md: document that lintPaddleDebug is part of the gate (run it, not just
assemble), and correct the first-run note to username/password (cookie-session).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Build fix pushed ( The failure was Android Lint, not the F6b auth code. The merge gate runs Removed the dead string from all 10 locales. The full gate is now green: CLAUDE.md now documents that |
On-device Send failed with "couldn't reach songbird" though songbird was online and reachable. Root cause: Android (targetSdk 36) blocks cleartext HTTP by default and the manifest set no policy — so the http:// login POST threw IOException → mapped to UNREACHABLE. Not a server/contract issue: live GET /healthz → 200 (songbird 1.6.0) and POST /api/v1/auth/login → 200 + Set-Cookie: songbird_session both succeed with the supplied URL+creds. Add res/xml/network_security_config.xml (cleartextTrafficPermitted=true) and wire it via <application android:networkSecurityConfig>. The operator points the app at an arbitrary LAN/Tailscale http host (no TLS, no fixed domain), and this is a private non-store sideloaded app — cleartext is the correct policy. Full gate green (compile + testPaddleDebugUnitTest + lintPaddleDebug) + assemble; APK manifest carries networkSecurityConfig; identity unchanged; frozen-core zero-diff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Cleartext fix pushed ( Root cause: Android, not songbird. Proven against the live server (supplied URL + creds): Fix: Please re-send on-device with the same Settings — it should now reach songbird and report created/skipped (and skipped=1 on a second send). I did not curl a real import, to avoid writing a junk annotation under your user; your on-device send imports the actual scanned note. |
F6b — speak songbird's real auth (cookie-session)
The F6 integration gate failed: songbird has no bearer-token auth. F6b reworks the finalize integration against songbird source (
github.com/kbennett2000/songbird@89f894e546c7b8203f2f8a5faf503b686eb34858). The brief stays unedited; CLAUDE.md carries the errata.Verified contract (file/line citations)
POST /api/v1/auth/login, body{username,password}(api/auth.py login;api/schemas.py:23-25 LoginRequest{username:str,password:str}). Success 200 +Set-Cookie: songbird_session=…(httponly, Path=/, SameSite=Lax, Max-Age 30d; Secure off for LAN-HTTP) (core/cookies.py COOKIE_NAME="songbird_session"). Bad creds → 401{"detail":{"code":"INVALID_CREDENTIALS","message":"Invalid username or password"}}(api/auth.py; envelopecore/errors.py raise_http).api/deps.py get_current_user). Unauthenticated routes:/healthz,/api/v1/auth/register,/api/v1/auth/login.POST /api/v1/import(api/import_export.py import_notes). Success 200 =ImportSummary(api/schemas.py:211-222):{"annotations":{created,skipped,failed},"sermon_notes":{created,skipped,failed},"errors":[…]}. Verified bytests/import_export_test.py:120 (created:2), :165 (re-importskipped:1), :221-223 (failed:1+2 errors). Import w/o cookie → 401 (:245); Concord down → 502 (:235). The brief's{"annotations":{"created","skipped"}}was missingfailed,sermon_notes,errors.core/sessions.py SESSION_TTL), cleaned only when expired → login-per-send accumulates.logout(POST /api/v1/auth/logout→ 204) deletes the row.As built
HttpImportPoster): login → capturesongbird_sessionfromSet-Cookie→ cookiedPOST /import→ best-effortlogout(your approved resolution: net-zero session rows). Explicit cookie handling, no global CookieManager; connect 5s/read 15s, no retries (idempotent). Credentials are login-body/cookie-header only — never logged.ImportPoster.send(baseUrl, username, password, json) → SongbirdExchange{login, imported}.canSend/isConfiguredrequire all three.Failure matrix (
ImportResult.from(SongbirdExchange))UNREACHABLELOGIN_REJECTEDHTTP_ERRORUNREACHABLEHTTP_ERRORSUCCESSfailed>0, surfaced prominently with the firsterrors[]reasonTests (real shape; fake two-step poster — no MockWebServer)
ImportResultTest(13) — first import (committed fixture), idempotent skipped, failed>0+errors, sermon-notes failed counted, missing/malformed summary, login 401→LOGIN_REJECTED, login network→UNREACHABLE, login 500→HTTP_ERROR, import-401-after-login→HTTP_ERROR (not login-rejected), import 502→HTTP_ERROR, import network→UNREACHABLE, snippet truncation.FinalizeViewModelTest(4) — login-ok/import-ok (asserts creds passed), login-fail, login-ok/import-fail, login-unreachable.SongbirdSettingsTest(4) — three-field gating + normalize.app/src/test/resources/songbird/import_summary.json— provenance: songbirdtests/import_export_test.py@89f894e(values = our single-annotation first import).Step-0 findings
http://192.168.1.62:8077/healthz+ a junk-credential login timed out from this session (songbird not reachable now; no real creds used). Contract is fully source-pinned; your real-login round-trip is the gate.Verification
:app:assemblePaddleDebug+:app:testPaddleDebugUnitTest— green. Full suite 1192 tests, 0 failures, 0 skipped.grepclean: noBearer/getToken/KEY_TOKEN/UNAUTHORIZED/settings_token_hintremain; no password/username near anyLog./Toast.Gate
Real end-to-end with your login: configure Settings (URL + username + password) → scan → edit → Send → verify in songbird → send again, confirm skipped=1. Then merge.
🤖 Generated with Claude Code