From bda982c05b665ba54709981d0f099e967c63ec5e Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 20 Oct 2026 23:30:54 +0200 Subject: [PATCH] docs(19.5-01): complete social sign-in foundations plan - goth v1.82.0 package check approved (Task 4 reply recorded) - commits, exported identifiers for plan 02, Docker test runs, deviations Claude-Session: https://claude.ai/code/session_01Wz3g1um1sz68zQJLpui4Qk --- .../19.5-01-SUMMARY.md | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-01-SUMMARY.md diff --git a/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-01-SUMMARY.md b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-01-SUMMARY.md new file mode 100644 index 00000000..6d29c8ab --- /dev/null +++ b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-01-SUMMARY.md @@ -0,0 +1,323 @@ +--- +phase: 19.5-social-login-in-sm-user-plugin +plan: 01 +subsystem: auth +tags: [goth, oauth, social-login, cooler, redis, getdel, otter, postgres, row-lock, sm-user-plugin] + +# Dependency graph +requires: + - phase: 19.4 + provides: "sm-user-plugin as its own repo with the host-mounted OAuth identity routes and the console withDB rule (24b16b1)" +provides: + - "D-02 decision note: github.com/markbates/goth v1.82.0 (core + google, facebook, github, azureadv2; never gothic), human package check approved" + - "cooler.Cache.Pull: atomic get-and-delete (Redis GETDEL, otter Compute + InvalidateOp)" + - "classes.OAuthProvider / classes.OAuthProviderSource; /oauth-providers reads a published source per request" + - "controllers.RegistrationSettings, RegistrationsFromIP, RefreshTTL, RemoteIP, FinishLogin; RegisterOptions.PasswordLess" + - "OAuthIdentitiesDestroy: PHP last-identity rule under a users-row lock; microsoft in the default allow-list" + - "user:backfill-handles opens its own database when none is published" +affects: [19.5-02, 19.5-03, 19.5-04, sm-sociallogin-plugin, sm-demo-app, sm-bm-app, fonoteka.go] + +actuals: + tokens: 9550 + tasks: 4 + commits: 3 +plan_head_before: c1ae1bb57120fb7296d83f8472c114a970d28c10 +plan_head_after: 464f7fa0bb2a56c958b0d57fd70f5e99bda876c6 +# commits counts summercms.go only (measured from the ledger). sm-user-plugin carries 3 more: +# 1fc0ce7..c43ba87 (git rev-list --count = 3). Combined: 6 code/doc commits before this SUMMARY. + +tech-stack: + added: [] + patterns: + - "Typed publish/lookup of a cross-plugin source: app.Publish[classes.OAuthProviderSource](src), looked up per request (never at construction)" + - "Race-safe guarded DELETE: SELECT ... FOR UPDATE on the parent row, then one conditional DELETE, RowsAffected decides 204/409" + - "One-time values in cooler via Cache.Pull (at most one winner)" + +key-files: + created: + - .planning/notes/2026-10-20-sociallogin-goth.md + - modules/cooler/pull_smoke_test.go + - modules/cooler/example_test.go + - ../fonoteka.go/plugins/golem15/user/classes/oauth_providers.go + - ../fonoteka.go/plugins/golem15/user/oauth_providers_smoke_test.go + - ../fonoteka.go/plugins/golem15/user/social_hooks_smoke_test.go + modified: + - .planning/research/STACK.md + - .planning/todos/done/user-plugin-commands-open-database.md (moved from pending) + - modules/cooler/cooler.go + - modules/cooler/memory.go + - modules/cooler/rediscache.go + - modules/cooler/README.md + - docs/services/caching.md + - ../fonoteka.go/plugins/golem15/user/controllers/api_controller.go + - ../fonoteka.go/plugins/golem15/user/controllers/registration.go + - ../fonoteka.go/plugins/golem15/user/controllers/oauth_identities.go + - ../fonoteka.go/plugins/golem15/user/console/backfill_handles.go + - ../fonoteka.go/plugins/golem15/user/console_test.go + - ../fonoteka.go/plugins/golem15/user/oauth_identities_test.go + - ../fonoteka.go/plugins/golem15/user/README.md + +key-decisions: + - "goth v1.82.0 accepted after the blocking human package check (Task 4 reply: approved); installed only by plan 02 in sm-sociallogin-plugin, never in the framework go.mod" + - "Unlink follows the PHP rule (self-set password or another identity) under SELECT ... FOR UPDATE on the users row; fonoteka gets the relaxation too (D-24), no opt-out flag" + - "Plan 02 records a new account's IP with surf.ClientIP(r, surf.TrustedProxies(app.Config)), as /register does; controllers.RemoteIP is the bare RemoteAddr host /login uses for its throttle" + - "FinishLogin fires no event (Go has no login event); the caller authenticates and checks throttle, activation and ban" + +patterns-established: + - "Cross-plugin capability source: interface in the providing plugin's classes package, published with an explicit type argument, looked up per request" + - "Go-only docs snippets need a runnable Example in the package (docs checker rejects src-less Go blocks)" + +requirements-completed: [SC-3, D-02, D-08, D-13, D-15, D-21, D-22, D-24, D-25] + +coverage: + - id: D1 + description: "goth decision note lands before any code and names v1.82.0, azureadv2, gothic exclusion, gorilla/sessions, 132 KiB" + requirement: "D-02" + verification: + - kind: other + ref: "git merge-base --is-ancestor 26ebb6c 5e0a04e; grep checks on .planning/notes/2026-10-20-sociallogin-goth.md" + status: pass + human_judgment: false + - id: D2 + description: "cooler.Cache.Pull consumes a value exactly once on memory and Redis drivers; expired keys report not found" + requirement: "D-08" + verification: + - kind: unit + ref: "modules/cooler/pull_smoke_test.go#TestPullSmoke" + status: pass + - kind: unit + ref: "modules/cooler/example_test.go#ExampleCache_Pull" + status: pass + - kind: other + ref: "go test -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check" + status: pass + human_judgment: false + - id: D3 + description: "/_user/api/v1/oauth-providers keeps the empty body without a source and lists a published source's enabled providers in order" + requirement: "D-15" + verification: + - kind: unit + ref: "sm-user-plugin oauth_providers_smoke_test.go#TestOAuthProvidersSourceSmoke" + status: pass + human_judgment: false + - id: D4 + description: "RegisterOptions.PasswordLess creates has_self_set_password=false users; FinishLogin restores, records IP, touches last_seen, sends reactivate mail" + requirement: "D-22" + verification: + - kind: integration + ref: "sm-user-plugin social_hooks_smoke_test.go#TestSocialHooksSmoke (Postgres)" + status: pass + - kind: integration + ref: "sm-user-plugin #TestLogin (Postgres, unchanged)" + status: pass + human_judgment: false + - id: D5 + description: "Unlink follows the PHP last-identity rule and is race-safe under concurrent requests" + requirement: "D-13" + verification: + - kind: integration + ref: "sm-user-plugin oauth_identities_test.go#TestOAuthIdentitiesDestroyLastIdentityWithPassword" + status: pass + - kind: integration + ref: "sm-user-plugin oauth_identities_test.go#TestOAuthIdentitiesDestroyLastMethodBlockedENAndPL" + status: pass + - kind: integration + ref: "sm-user-plugin oauth_identities_test.go#TestOAuthIdentitiesDestroyNoContentKeepsSibling" + status: pass + - kind: integration + ref: "sm-user-plugin oauth_identities_test.go#TestOAuthIdentitiesDestroyConcurrentSmoke (mutation: removing FOR UPDATE makes it fail)" + status: pass + human_judgment: false + - id: D6 + description: "user:backfill-handles opens the database itself when none is published; folded todo closed" + verification: + - kind: integration + ref: "sm-user-plugin console_test.go#TestBackfillHandlesOpensFromConfigWhenUnpublished (Postgres)" + status: pass + human_judgment: false + - id: D7 + description: "Additive core-plugin change: fonoteka.go builds and its plugin suite passes; sm-oauthserver-plugin builds" + requirement: "SC-3" + verification: + - kind: other + ref: "go -C ../fonoteka.go build ./... && go -C ../fonoteka.go test -short ./plugins/golem15/user/... ./plugins/golem15/fonoteka/... && go -C ../sm-oauthserver-plugin build ./..." + status: pass + human_judgment: false + - id: D8 + description: "goth v1.82.0 package legitimacy check" + requirement: "D-02" + verification: + - kind: manual_procedural + ref: "Task 4 checkpoint:human-verify gate=blocking-human; reply recorded below" + status: pass + human_judgment: true + rationale: "Package legitimacy for Go modules cannot be verified by the seam; a human confirmed it (reply: approved)" + +duration: 34min +completed: 2026-10-20 +status: complete +--- + +# Phase 19.5 Plan 01: Social sign-in foundations Summary + +**goth v1.82.0 decided and approved, an atomic `cooler.Cache.Pull` (Redis GETDEL, otter Compute + InvalidateOp), and additive sm-user-plugin hooks: a published `OAuthProviderSource` for /oauth-providers, the PHP last-identity unlink rule under a users-row lock, exported sign-in helpers (`FinishLogin`, `RegisterOptions.PasswordLess`, ...), and `user:backfill-handles` opening its own database** + +## Performance + +- **Duration:** about 34 min (first commit 20:55 UTC+2 ... SUMMARY 23:29 UTC+2, including the checkpoint wait) +- **Started:** 2026-10-20T20:55:08Z (first plan commit) +- **Completed:** 2026-10-20T21:29:35Z +- **Tasks:** 4 of 4 (Task 4 is the human package check) +- **Files modified:** 20 (10 in summercms.go, 10 in sm-user-plugin) + +## Accomplishments + +- The goth decision is on record before any code (`26ebb6c` is an ancestor of `5e0a04e`), with the STACK.md row; no go.mod anywhere requires goth. +- `cooler.Cache.Pull(ctx, key) ([]byte, bool, error)` on both drivers, documented in the cooler README and docs/services/caching.md ("One-time values"), with `TestPullSmoke` and a runnable `ExampleCache_Pull`. +- `GET /_user/api/v1/oauth-providers` looks up `classes.OAuthProviderSource` per request; without one it answers the unchanged bytes `{"providers":[],"success":true}`. +- `OAuthIdentitiesDestroy` takes `SELECT id FROM users WHERE id = ? FOR UPDATE` and runs the guarded DELETE ported from PHP User.php; 409/404 bodies unchanged; `microsoft` added to the default allow-list; the allow-list falls back to the source's `KnownOAuthProviders()`. +- `user:backfill-handles` opens and closes its own database when none is published; the folded todo moved to `.planning/todos/done/`. + +## Task Commits + +Planning docs first, then framework, then sm-user-plugin (D-21): + +1. **Task 1a: goth decision note + STACK.md** - `26ebb6c` (docs) - summercms.go +2. **Task 1b: cooler.Cache.Pull + README/docs** - `5e0a04e` (feat) - summercms.go +3. **Task 2: provider source + sign-in helpers** - `78a867b` (feat) - sm-user-plugin (`../fonoteka.go/plugins/golem15/user`) +4. **Task 3a: atomic unlink with the PHP last-identity rule (microsoft added)** - `1741618` (fix) - sm-user-plugin +5. **Task 3b: user:backfill-handles opens its own database** - `c43ba87` (fix) - sm-user-plugin +6. **Task 3c: close the folded todo** - `464f7fa` (docs) - summercms.go +7. **Task 4: goth package legitimacy check** - no commit (human checkpoint, recorded below) + +**Plan metadata:** this SUMMARY commit, `docs(19.5-01): ...` (summercms.go) + +sm-user-plugin base was `1fc0ce7`; it is now 3 commits ahead of origin (`c43ba87`). Nothing pushed or tagged. Nothing staged in the fonoteka.go superproject (plan 03 bumps the pointer). + +## Task 4 reply + +> approved + +Recorded verbatim from the human's reply to the `checkpoint:human-verify` (gate `blocking-human`) for `github.com/markbates/goth v1.82.0`. + +Orchestrator verification before the reply: `go list -m -json github.com/markbates/goth@v1.82.0` reported +- Origin URL: `https://github.com/markbates/goth` +- Ref: `refs/tags/v1.82.0` +- Hash: `6c7fc319f1f5149273c813736418e5b27fce988e` +- Time: `2025-08-18T03:17:23Z` + +At the time of the reply `grep -c markbates/goth go.mod` printed 0 and `../sm-sociallogin-plugin` did not exist yet. goth was not installed in this plan; plan 02 runs `go get github.com/markbates/goth@v1.82.0` in sm-sociallogin-plugin only (never gothic, never the framework go.mod). + +## Exported identifiers plan 02 consumes + +Framework (`git.golem15.com/golem15/summercms/modules/cooler`): +- `cooler.Cache.Pull(ctx context.Context, key string) ([]byte, bool, error)` + +sm-user-plugin (`git.golem15.com/golem15/sm-user-plugin`): +- `classes.OAuthProvider{Name, Label}` (JSON `name`, `label`) +- `classes.OAuthProviderSource` with `EnabledOAuthProviders(ctx context.Context) []OAuthProvider` and `KnownOAuthProviders() []string`; publish with `app.Publish[classes.OAuthProviderSource](src)` +- `controllers.RegistrationSettings(app)`, `controllers.RegistrationsFromIP(ctx, gdb, ip)`, `controllers.RefreshTTL(app)`, `controllers.RemoteIP(r)`, `controllers.FinishLogin(ctx, app, db, user, ip)` +- `controllers.RegisterOptions.PasswordLess` + +No name deviates from the plan. + +## Files Created/Modified + +summercms.go: +- `.planning/notes/2026-10-20-sociallogin-goth.md` - D-02 decision note +- `.planning/research/STACK.md` - goth row in Supporting Libraries +- `.planning/todos/done/user-plugin-commands-open-database.md` - folded todo closed +- `modules/cooler/cooler.go`, `memory.go`, `rediscache.go` - `Cache.Pull` and both drivers +- `modules/cooler/pull_smoke_test.go`, `example_test.go` - `TestPullSmoke`, `ExampleCache_Pull` +- `modules/cooler/README.md`, `docs/services/caching.md` - Pull documented + +sm-user-plugin: +- `classes/oauth_providers.go` - provider source interface +- `controllers/api_controller.go` - per-request source lookup, exported helpers +- `controllers/registration.go` - `PasswordLess`, `FinishLogin` +- `controllers/oauth_identities.go` - row-locked guarded unlink, microsoft +- `console/backfill_handles.go` - opens its own DB +- `oauth_providers_smoke_test.go`, `social_hooks_smoke_test.go`, `oauth_identities_test.go`, `console_test.go` - tests +- `README.md` - OAuth providers, identities, helpers, CLI command + +## Verification + +- summercms.go: `go vet ./...`, `go test -short ./...`, `TestDocsTree`, `summer docs:build --check` green. +- sm-user-plugin: vet and short suite green. +- Docker-backed tests, all RUN and passed: `TestSocialHooksSmoke`, `TestLogin`, `TestOAuthProvidersSourceSmoke`, `TestOAuthIdentitiesDestroyLastIdentityWithPassword`, `TestOAuthIdentitiesDestroyLastMethodBlockedENAndPL`, `TestOAuthIdentitiesDestroyNoContentKeepsSibling`, `TestOAuthIdentitiesDestroyConcurrentSmoke`, `TestBackfillHandlesOpensFromConfigWhenUnpublished`. None NOT RUN. +- Mutation check: removing `FOR UPDATE` makes `TestOAuthIdentitiesDestroyConcurrentSmoke` fail; restored byte-identical. +- fonoteka.go builds and its fonoteka plugin suite passes at the new user-plugin code; sm-oauthserver-plugin builds. + +## Decisions Made + +- `RemoteIP` is documented as the bare RemoteAddr host that `/login` uses for its throttle; `/register` records `surf.ClientIP` with trusted proxies. Plan 02 should use `surf.ClientIP(r, surf.TrustedProxies(app.Config))` for a new account's IP. +- Otherwise followed the plan's recorded decisions (FinishLogin fires no event; D-13 relaxation applies to fonoteka without an opt-out). + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] Docs checker rejects src-less Go snippets** +- **Found during:** Task 1 +- **Issue:** `summer docs:build --check` failed on an inline Go snippet for Pull in docs/services/caching.md. +- **Fix:** Added a runnable `ExampleCache_Pull` in `modules/cooler/example_test.go` and pointed the docs page at it via `src=`. +- **Files modified:** modules/cooler/example_test.go, docs/services/caching.md +- **Committed in:** `5e0a04e` + +**2. [Rule 1 - Bug] Empty-list byte assertion corrected** +- **Found during:** Task 2 +- **Issue:** The plan named `{"success":true,"providers":[]}`, but the real pre-change Go bytes are `{"providers":[],"success":true}` (wire sorts map keys). Same JSON value as the PHP parity recording. +- **Fix:** `TestOAuthProvidersSourceSmoke` asserts the actual unchanged bytes. +- **Committed in:** `78a867b` + +**3. [Doc correction] RemoteIP doc comment** +- **Found during:** Task 2 +- **Issue:** The plan said RemoteIP is "host of RemoteAddr as /register records it"; /register actually uses `surf.ClientIP` with trusted proxies. +- **Fix:** Doc states RemoteIP is the bare RemoteAddr host `/login` uses for its throttle. +- **Committed in:** `78a867b` + +**4. [Rule 1 - Bug] gofmt fix** +- **Found during:** Task 3 +- **Issue:** `forgotPasswordBody` line not gofmt-clean. +- **Fix:** Folded into `1741618`. + +**5. [Rule 3 - Blocking] Backfill test sets app.key** +- **Found during:** Task 3 +- **Issue:** `lagoon.OpenFromApp` loads encryption keys, so the test app needs `app.key`. +- **Fix:** `TestBackfillHandlesOpensFromConfigWhenUnpublished` sets it. +- **Committed in:** `c43ba87` + +**6. [Context] sm-user-plugin base** +- The checkout was at `1fc0ce7` (two pushed Turnstile commits past the planned `4c5d3b8`), in sync with origin; no migration changed since `4c5d3b8` (acceptance criterion holds). + +--- + +**Total deviations:** 6 (2 blocking, 2 bug, 1 doc correction, 1 context note) +**Impact on plan:** No scope creep; names and contracts as planned. + +## Issues Encountered + +None beyond the deviations above. + +## g15office + +The comment on the Phase 19.5 milestone task `onunbz3l` naming plan 19.5-01's results is posted by the orchestrator (not available in this executor session). + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +- Plan 02 can create sm-sociallogin-plugin and run `go get github.com/markbates/goth@v1.82.0` there (approved). +- Plan 03 bumps the sm-user-plugin pointer in fonoteka.go, sm-demo-app and sm-bm-app; sm-user-plugin must be pushed first (release go-ahead in plan 02 Task 5). + +## Self-Check: PASSED + +- Files: all key created files found; the pending todo is gone and the done copy exists. +- Commits: `26ebb6c`, `5e0a04e`, `464f7fa` (summercms.go) and `78a867b`, `1741618`, `c43ba87` (sm-user-plugin) exist; `26ebb6c` is an ancestor of `5e0a04e`. +- `grep -c markbates/goth go.mod` = 0; `../sm-sociallogin-plugin` does not exist yet. + +--- +*Phase: 19.5-social-login-in-sm-user-plugin* +*Completed: 2026-10-20*