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
This commit is contained in:
@@ -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*
|
||||
Reference in New Issue
Block a user