docs(19.5): re-verification after gap closure 05 and UAT items
This commit is contained in:
@@ -3,7 +3,7 @@ status: testing
|
||||
phase: 19.5-social-login-in-sm-user-plugin
|
||||
source: [19.5-VERIFICATION.md]
|
||||
started: 2026-10-21T00:24:54.347Z
|
||||
updated: 2026-10-21T13:00:00.000Z
|
||||
updated: 2026-10-21T13:20:00.000Z
|
||||
---
|
||||
|
||||
## Current Test
|
||||
@@ -24,19 +24,25 @@ retested_by: 19.5-05
|
||||
|
||||
### 2. Walk the bm callback view in a browser (see behavior_unverified_items).
|
||||
expected: As in behavior_unverified_items.
|
||||
result: [pending]
|
||||
result: skipped
|
||||
reason: superseded by the D-28 view test (sm-bm-app frontend/src/views/OAuthCallbackView.test.ts, 5/5, in the phase gate); the user chose "Yes, add the test" instead of a manual browser walk on 2026-10-21
|
||||
retested_by: 19.5-05
|
||||
|
||||
### 3. Review the judgment-tier prohibition verdicts in the 'Prohibitions' section of this report (non-authoritative LLM-judge verdicts).
|
||||
expected: Confirm or dispute each verdict; all are recorded as no violation found.
|
||||
result: [pending]
|
||||
|
||||
### 4. Decide on 19.5-REVIEW CR-02: an account pre-created by sociallogin from an unverified provider email (created activated, S classes/registration.go) keeps the attacker's Microsoft or Facebook identity after the real owner proves the address with a reset-code password reset.
|
||||
expected: Either accept it (PHP parity on account creation) and correct sm-sociallogin-plugin README.md:50, which offers 'proves the address with a password reset' as the recovery; or schedule the follow-up the review proposes (create the account inactive unless p.EmailVerified, and/or drop unverified identities when the reset stamps email_verified_at).
|
||||
result: [pending]
|
||||
|
||||
## Summary
|
||||
|
||||
total: 3
|
||||
total: 4
|
||||
passed: 1
|
||||
issues: 0
|
||||
pending: 2
|
||||
skipped: 0
|
||||
skipped: 1
|
||||
blocked: 0
|
||||
|
||||
## Gaps
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
phase: 19.5-social-login-in-sm-user-plugin
|
||||
verified: 2026-10-21T08:30:00Z
|
||||
verified: 2026-10-21T12:59:50Z
|
||||
status: human_needed
|
||||
score: 15/16 must-haves verified
|
||||
score: 23/23 must-haves verified
|
||||
covered_files:
|
||||
- .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-01-PLAN.md
|
||||
- .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-01-SUMMARY.md
|
||||
@@ -12,6 +12,8 @@ covered_files:
|
||||
- .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-03-SUMMARY.md
|
||||
- .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-04-PLAN.md
|
||||
- .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-04-SUMMARY.md
|
||||
- .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-05-PLAN.md
|
||||
- .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-05-SUMMARY.md
|
||||
- docs/services/caching.md
|
||||
- modules/cooler/README.md
|
||||
- modules/cooler/cooler.go
|
||||
@@ -22,217 +24,187 @@ covered_files:
|
||||
- modules/cooler/pull_test.go
|
||||
- modules/cooler/rediscache.go
|
||||
- scripts/check-phase19.5.sh
|
||||
covered_digest: "v2:sha256:2ca5d998566ef58a4d12cb1ef9527308eca83072bcc092cc78e81badca4a05bb"
|
||||
covered_digest: "v2:sha256:73370391391a90bf632aabe78b77fb693a74535477d0245c0a2f279c06a698fa"
|
||||
# Files outside this repository are pinned by commit instead of digest:
|
||||
# sm-sociallogin-plugin d7ee28d (tag v0.1.1), sm-user-plugin c43ba87 (v0.1.4; master c7635c5, test-only on top),
|
||||
# sm-demo-app 61a00e9, sm-bm-app 99af8c2, fonoteka.go f4d5728.
|
||||
behavior_unverified: 1
|
||||
# sm-user-plugin 35cf166 (local, unpushed; released v0.1.4 = c43ba87), sm-sociallogin-plugin c4ebf05 (local, unpushed;
|
||||
# released v0.1.1 = d7ee28d), sm-bm-app 5fd0cc3 (local, unpushed), sm-demo-app b325460 (foreign 19.8 commit on 61a00e9),
|
||||
# fonoteka.go f4d5728 (worktree plugins/golem15/user at 35cf166, unstaged).
|
||||
# RELEASE HOLD (user decision at 19.5-05 Task 6): the D-26/D-27 fix is not in any app pin. sm-demo-app and sm-bm-app
|
||||
# still run sm-user-plugin v0.1.4 + sm-sociallogin-plugin v0.1.1, i.e. the password-registration takeover path stays
|
||||
# open in the deployed apps until the release in .planning/todos/pending/email-proof-release.md (g15office sqnwn1zi).
|
||||
behavior_unverified: 0
|
||||
overrides_applied: 0
|
||||
behavior_unverified_items:
|
||||
- truth: "The bm frontend's /auth/oauth/callback view removes ?code= from the address bar and history before it redeems the code exactly once, routes ?pending_registration= to the consent view and shows ?error= messages (plan 03 must-have, T-19.5-26)."
|
||||
test: "In sm-bm-app with Microsoft credentials (or any configured provider), sign in from /logowanie; on return, watch the URL and browser history; press Back after landing; also open a callback URL with ?error=Test and one with ?pending_registration=x&provider=microsoft."
|
||||
expected: "The address bar never keeps ?code= after the view mounts; Back does not land on a callback URL with a code; exactly one GET /_user/api/v1/oauth-complete per sign-in; ?error= shows the message; ?pending_registration= lands on /auth/oauth/rejestracja."
|
||||
why_human: "Only src/lib/oauth.ts (URL building and parsing) is unit-tested; the view's ordering invariant (router.replace before completeOAuth, the redeemed-set guard) is browser behaviour no test exercises."
|
||||
human_verification:
|
||||
re_verification:
|
||||
previous_status: human_needed
|
||||
previous_score: 15/16
|
||||
gaps_closed:
|
||||
- "Truth 16 (bm OAuthCallbackView: code dropped before the single redeem, pending and error routing): was PRESENT_BEHAVIOR_UNVERIFIED, now VERIFIED by src/views/OAuthCallbackView.test.ts (D-28)"
|
||||
- "Human item 1 (password-registration pre-account takeover): user chose (a) tighten auto-link; implemented as users.email_verified_at (D-26, D-27) and verified below"
|
||||
gaps_remaining: []
|
||||
regressions: []
|
||||
human_verified:
|
||||
- test: "Decide on the residual pre-account-takeover path left open by the T-19.5-38 fix. ownerVerifiedEmail treats has_self_set_password=true as proof the owner controls the address, but POST /register with golem15.user activation.activate_mode=auto (the default) creates an activated account with has_self_set_password=true and no email check. An attacker who registers victim@example.com with a password before the victim's first Google/GitHub sign-in gets the victim's verified identity auto-linked onto the attacker's account, and keeps password access."
|
||||
expected: "Either accept it explicitly (it is exactly PHP's D-10 behaviour, and the variant T-19.5-38 closed was the social-created one), or schedule a follow-up (for example: auto-link only when the account was activated by mail or set its password through the reset code, or require activate_mode=user for apps mounting sociallogin). Also correct 19.5-SECURITY-REVIEW.md / README wording that a self-set password means the owner 'proved the address'."
|
||||
why_human: "Product/security decision on a deliberate PHP-parity behaviour; no test covers the /register-then-verified-sign-in path (TestPreAccountTakeover only covers accounts created by an unverified social sign-in)."
|
||||
- test: "Walk the bm callback view in a browser (see behavior_unverified_items)."
|
||||
expected: "As in behavior_unverified_items."
|
||||
why_human: "Browser history and redirect ordering; no view-level test."
|
||||
uat_row: 1
|
||||
recorded_at: "2026-10-21T12:54:14.000Z"
|
||||
human_verification:
|
||||
- test: "Review the judgment-tier prohibition verdicts in the 'Prohibitions' section of this report (non-authoritative LLM-judge verdicts)."
|
||||
expected: "Confirm or dispute each verdict; all are recorded as no violation found."
|
||||
expected: "Confirm or dispute each verdict, including the four 19.5-05 prohibitions added in this re-verification; all are recorded as no violation found."
|
||||
why_human: "Judgment-tier prohibitions never pass silently (ADR-550 D4)."
|
||||
- test: "Decide on 19.5-REVIEW CR-02: an account pre-created by sociallogin from an unverified provider email (created activated, S classes/registration.go) keeps the attacker's Microsoft or Facebook identity after the real owner proves the address with a reset-code password reset."
|
||||
expected: "Either accept it (PHP parity on account creation) and correct sm-sociallogin-plugin README.md:50, which offers 'proves the address with a password reset' as the recovery; or schedule the follow-up the review proposes (create the account inactive unless p.EmailVerified, and/or drop unverified identities when the reset stamps email_verified_at)."
|
||||
why_human: "Open high-severity review finding with no user decision; not covered by D-26..D-28. Since D-26 makes the reset the route to auto-linking, the documented recovery hands the owner an account that still carries the attacker's identity. bm runs Microsoft with tenant common."
|
||||
---
|
||||
|
||||
# Phase 19.5: Social login in sm-user-plugin Verification Report
|
||||
|
||||
**Phase Goal:** `sm-user-plugin` signs users in with Google and Facebook through `/oauth/{provider}` and its callback, with a provider registry open to more providers, the SPA one-time-code handoff and the pending-registration branch, matching the PHP user plugin's contract. Per CONTEXT D-01 the code lives in the new `sm-sociallogin-plugin`, with hooks in sm-user-plugin.
|
||||
**Verified:** 2026-10-21
|
||||
**Phase Goal:** `sm-user-plugin` signs users in with Google and Facebook through `/oauth/{provider}` and its callback, with a provider registry open to more providers, the SPA one-time-code handoff and the pending-registration branch, matching the PHP user plugin's contract. Per CONTEXT D-01 the code lives in the new `sm-sociallogin-plugin`, with hooks in sm-user-plugin. The 19.5-05 gap closure adds D-26..D-28.
|
||||
**Verified:** 2026-10-21T12:59:50Z
|
||||
**Status:** human_needed
|
||||
**Re-verification:** No, initial verification
|
||||
**Re-verification:** Yes, after gap-closure plan 19.5-05 (previous: human_needed, 15/16)
|
||||
|
||||
**MVP-mode note:** ROADMAP marks this phase `**Mode:** mvp`, but the goal is not a user story (`user-story.validate` returns `valid: false`). The verifier contract says to refuse MVP-mode verification and ask for `/msd mvp-phase 19.5`. Every phase in this ROADMAP carries `Mode: mvp`, and all four plans record the decision not to invent a story. So this report runs the standard goal-backward verification against the ROADMAP goal and SC-1..SC-4. Reformatting the goal is a process choice for the user; this verification does not depend on it.
|
||||
**Release state.** The user answered the 19.5-05 Task 6 checkpoint with **hold**. I confirmed it in the repositories. The unpushed commits are sm-user-plugin `add1f7a`, `a99cb46`, `35cf166`; sm-sociallogin-plugin `3bb5650`, `962190b`, `c4ebf05`; sm-bm-app `5fd0cc3`. Neither `v0.1.5` (user) nor a new sociallogin tag exists on the remotes. The app gitlinks are unchanged: sm-demo-app and sm-bm-app `plugins/golem15/user` = c43ba87 (v0.1.4) and `plugins/golem15/sociallogin` = d7ee28d (v0.1.1); the fonoteka.go gitlink is c43ba87. sm-sociallogin-plugin go.mod still requires sm-user-plugin v0.1.4. I count this as a recorded user decision, and plan 05's must-have has an explicit hold branch for it. It is not a code gap. **But the deployed sm-demo-app and sm-bm-app keep the password-registration pre-account-takeover path (CR-01 / T-19.5-39) until the release in `.planning/todos/pending/email-proof-release.md` (g15office sqnwn1zi) ships.** The demo and bm stages of the gate ran at the old pins. Plan 05 Task 7's check at the new pins has not run.
|
||||
|
||||
**MVP-mode note:** As in the previous report, the ROADMAP goal is not a user story. All five plans recorded the decision not to invent one, so this report runs standard goal-backward verification against SC-1..SC-4, D-01..D-28 and the plan must-haves.
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths
|
||||
|
||||
Truths 1-15 come from the previous report and got a regression check here: the full gate, plus spot reads of the code paths 19.5-05 touched. Truth 16 was rerun in full. Truths 17-23 are plan 19.5-05's must-haves, and each was verified at all three levels.
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 1 | SC-1: `/oauth/google` and `/oauth/facebook` (and github, microsoft) with `/callback` keep the PHP paths (PHP `routes.php:239-250`, `prefix oauth`, `throttle:20,1`) | VERIFIED | S `routes.go`: `GroupRaw("/oauth", surf.Use("throttle:20,1"))` with `/{provider}` and `/{provider}/callback`; `RegisterBuiltins` registers google, facebook, github, microsoft. Tests: `TestRoutes`, `TestRouteMiddleware`, `TestTracerSmoke`; demo `TestSocialLoginRedirect` (real app: Location host accounts.google.com); bm `TestSocialLoginMounted` (login.microsoftonline.com/<tenant>). All passed in my gate run. |
|
||||
| 2 | SC-2a: SPA mode redirects to `{frontend_callback}?code=<64 chars>`, and `GET /_user/api/v1/oauth-complete` exchanges it once (200 `{token,user,action,return_to}`, 422 empty, 410 missing or used) | VERIFIED | S `controllers/callback.go` `finishCallback` → `PutCompletion`; `controllers/complete.go` uses `PullCompletion` (cooler `Pull`); bodies match PHP `ApiController.php:1097-1116` byte for byte (messages, defaults `login` and `/`). Tests: `TestTracerSmoke` (second use 410), `TestOAuthComplete`, `TestCodes` (32 concurrent pulls, 1 winner), `TestTokenCookieMode`. |
|
||||
| 3 | SC-2b: a new social user goes to `?pending_registration=<code>[&reason=]&provider=[&email=]` and finishes with `POST oauth-register-complete` (PHP fields, 422 consent body with PHP Polish text, 410, `email_exists_link_account`); a 422 does not consume the code | VERIFIED | S `classes/flow.go` `pendingOutcome`, `controllers/callback.go` (PHP parameter order), `controllers/register_complete.go` (Get, all 422 checks, then `PullPending` inside the final transaction via `CompletePending`). Tests: `TestFlowSmoke/register_complete_keeps_code_on_422`, `TestRegisterComplete` (incl. 16 concurrent submits → one 200), `TestPHPSocialAuthGate`, `TestPHPPendingRegistrationTakeover`. |
|
||||
| 4 | SC-3a: identities are linked on login (identity match, verified email match, `action=link`) | VERIFIED | S `classes/flow.go` `loginWithIdentity`/`upsertIdentity` under `SELECT ... FOR UPDATE`, `linkIdentity`; tokens encrypted with `token_expires_at`, avatar only in `profile_data`. Tests: `TestMatching`, `TestLink`, `TestLinkTicket`, `TestIdentityTokens`, `TestFlowSmoke/verified_match_links`. |
|
||||
| 5 | SC-3b: identities are listed and unlinked through the existing (host-mounted) identity routes, with the PHP unlink rule, race-safe | VERIFIED | U `controllers/oauth_identities.go`: allow-list adds microsoft and reads the published source's `KnownOAuthProviders`; `FOR UPDATE` then the guarded DELETE (PHP User.php:913). Tests: `TestOAuthIdentitiesDestroyRule`, `TestOAuthIdentitiesDestroyRace` (both orders), `TestOAuthIdentitiesDestroyConcurrentSmoke`; `TestPHPProfileOAuthLink` drives `OAuthIdentitiesIndex` and `OAuthIdentitiesDestroy` over identities the plugin linked. Note: per D-14 the host mounts these routes; only fonoteka.go mounts them today, so sm-demo-app and sm-bm-app users cannot list or unlink an identity (bm's profile only links). This is within D-14, not a gap. |
|
||||
| 6 | SC-3c: the consuming applications still build and pass their tests | VERIFIED | Gate `--demo` (3 tests), `--bm` (Go 1+2, vitest 211, typecheck), `--fonoteka` (vet + `go test -short ./...`, sociallogin not mounted) all PASS in my run. fonoteka's Docker-only `TestPhase09SecurityRoutes` failure is pre-existing and unrelated (five cabana admin routes; deferred-items.md). |
|
||||
| 7 | SC-4: the new code has unit tests, delivered in the last plan (19.5-04) | VERIFIED | 19.5-04 adds `modules/cooler/pull_test.go`, 3 user-plugin test files and 15 sociallogin test files. Gate `--all` with `PHASE195_REQUIRE_DOCKER=1` ran 4+2+9+28+10+1 named tests, all passing, including the TestPHP* ports and `TestPHPMethodMap` (38 PHP methods mapped). |
|
||||
| 8 | Provider registry open to more providers (D-03): plugin-owned `Provider`/`Registry`, no goth type exported, fakes registered through the same registry | VERIFIED | S `classes/provider.go` (no goth import; `Register` refuses reserved and duplicate names); goth imported only in `classes/providers_goth.go` (grep). `Plugin.Register` publishes the registry so another plugin can add providers. Tests: `TestRegistry`, and every flow test registers fake providers. |
|
||||
| 9 | D-01/D-02: separate plugin requiring `golem15.user`; goth v1.82.0 without gothic; no gorilla/sessions; framework go.mod untouched | VERIFIED | `Requires() = ["golem15.user"]`; S go.mod `github.com/markbates/goth v1.82.0`; no `gothic`, `UseProviders` or `gopkg.in/yaml` in S Go sources; gate `--sociallogin` checks `go list -deps`; summercms.go go.mod has no commit since 26ebb6c. |
|
||||
| 10 | D-07: state, action, callback, return_to, consent, beta key, link user and goth session in one AES-256-GCM cookie (HKDF purpose key, AAD the cookie name), HttpOnly, SameSite=Lax, Path=/oauth, 600 s, cleared on every callback outcome, state compared in constant time | VERIFIED | S `classes/statecookie.go` (lines 26-32, 122-127, 188-196), `controllers/callback.go` (clear first; `subtle.ConstantTimeCompare`; empty state refused). Test: `TestStateCookie` (refused cases, age window, seal limits, key domain separation, callback outcomes). |
|
||||
| 11 | D-08: `cooler.Cache.Pull` is atomic on memory and Redis; the plugin pins a framework release that contains it | VERIFIED | `memory.go:90` (otter `Compute` + `InvalidateOp`), `rediscache.go:57` (GETDEL); `git tag --contains 5e0a04e` → v0.3.1; S go.mod requires summercms v0.3.1. Tests: `TestPullConcurrent` (64 pullers), `TestPullExpired`, `TestPullPrefixIsolation`, `TestPullDocker` (redis:7-alpine). |
|
||||
| 12 | D-10/D-11: PHP matching table; Google verified only by a boolean flag, GitHub by primary-verified `/user/emails`, Facebook and Microsoft never (nOAuth); plus the user-approved T-19.5-38 divergence (no auto-link onto a password-less account) | VERIFIED | S `classes/flow.go` `HandleCallback`, `CompletePending`, `ownerVerifiedEmail`; `classes/providers_goth.go` (rawTrue strict bool; Facebook and Microsoft `EmailVerified = false`; UPN fallback). Tests: `TestMatching`, `TestEmailVerified`, `TestNOAuth`, `TestPreAccountTakeover`, `TestSoftDeleted`, `TestInactiveAndBanned`. See the residual noted under Human Verification 1. |
|
||||
| 13 | D-15/D-22: `oauth-providers` stays in sm-user-plugin, is unchanged without a source, and lists enabled providers when sociallogin publishes one; no sm-user-plugin migration | VERIFIED | U `api_controller.go` `OAuthProviders` looks up `classes.OAuthProviderSource` per request; S `plugin.go` `app.Publish[userclasses.OAuthProviderSource]`. `git diff 4c5d3b8 -- updates` is empty. Tests: `TestOAuthProvidersSourceSmoke` (exact bytes `{"providers":[],"success":true}`), `TestOAuthProviders`, `TestProvidersList`. |
|
||||
| 14 | D-16/D-18: credentials from config, legacy env and the admin settings overlay read per request; secrets encrypted and write-only; enabled iff id and secret; redirect URI override or app.url + `/oauth/<p>/callback` | VERIFIED | S `classes/credentials.go`, `plugin.go` `copyLegacyEnv`, `models/settings.go`. Tests: `TestSettings` (incl. `admin_json_hides_secrets`, `no_secret_in_logs`), `TestSettingsOverrideSmoke`. |
|
||||
| 15 | D-21/D-23: sm-demo-app and sm-bm-app mount the plugin at v0.1.1 (every post-release fix included), pushed in D-21 order; bm links Microsoft through the 60 s one-time link ticket with no JWT in a URL | VERIFIED | Gitlinks: sm-demo-app and sm-bm-app `plugins/golem15/sociallogin` = d7ee28d (= v0.1.1), `plugins/golem15/user` = c43ba87 (= v0.1.4); fonoteka.go user = c43ba87. `origin/master..master` is empty in sm-sociallogin-plugin, sm-user-plugin, sm-demo-app and sm-bm-app. The three production fixes (21f3fe5, c99f5f9, d7ee28d) are ancestors of both app pins. bm `ProfileView.vue` → `oauthLinkTicket` → `oauthStartUrl(... linkTicket)`. Tests: `TestLinkTicket` (incl. `no_jwt_in_locations`), `oauth.test.ts`. |
|
||||
| 16 | bm callback view: removes the code from the address bar before redeeming it once; routes pending and error | PRESENT_BEHAVIOR_UNVERIFIED | `frontend/src/views/OAuthCallbackView.vue` calls `router.replace` before `auth.completeOAuth`, with a module-level `redeemed` set; the pending branch replaces to `oauth-register`. The code is present and wired, but no test mounts the view. See Human Verification 2. |
|
||||
| 1 | SC-1: `/oauth/{google,facebook,github,microsoft}` and `/callback` keep the PHP paths with `throttle:20,1` | VERIFIED (regression) | Gate `--sociallogin` and `--docker`: `TestRoutes`, `TestRouteMiddleware`, `TestTracerSmoke`; `--demo` `TestSocialLoginRedirect`; `--bm` `TestSocialLoginMounted`. All PASS in my run. |
|
||||
| 2 | SC-2a: SPA one-time code, `oauth-complete` exchanges once (200 / 422 / 410) | VERIFIED (regression) | `TestTracerSmoke`, `TestOAuthComplete`, `TestCodes` in the 29 Docker sociallogin tests (PASS). |
|
||||
| 3 | SC-2b: pending registration via `?pending_registration=`, `oauth-register-complete` with PHP fields, 422 does not consume the code | VERIFIED (regression) | `TestFlowSmoke`, `TestRegisterComplete`, `TestPHPPendingRegistrationTakeover` (PASS). |
|
||||
| 4 | SC-3a: identities linked on login (identity match, verified owner-proved email match, `action=link`) | VERIFIED | Matching now also requires owner proof (truth 17). `TestMatching`, `TestLink`, `TestLinkTicket` PASS. The harness's `e.user` carries proof, so link-path tests still exercise linking, and `e.unprovenUser` models accounts without it. |
|
||||
| 5 | SC-3b: list and unlink via host-mounted identity routes, PHP unlink rule, race-safe | VERIFIED (regression) | U `controllers/oauth_identities.go` unchanged by 05 (`git diff c7635c5 --stat` does not list it); `TestOAuthIdentitiesDestroyConcurrentSmoke` in the 12 Docker user tests PASS. |
|
||||
| 6 | SC-3c: the consuming applications still build and pass their tests | VERIFIED (at current pins) | `--demo` (build, vet, 3 tests), `--bm` (build, vet, 1+2 Go tests, vitest 216/216, typecheck), `--fonoteka` (vet and short suite with the user plugin worktree at 35cf166) all PASS. The apps at the *new* pins are not tested (release on hold). |
|
||||
| 7 | SC-4: the new code has unit tests in the last plan | VERIFIED | 19.5-04 delivered the phase suite. 19.5-05, the gap closure and now the last plan, adds `TestOwnerVerifiedEmail` (8), `TestEmailVerifiedAt` (12), `TestAdminEmailVerifiedAt` (6), `TestEmailVerifiedAtMigration` (5) and the 5-case view test. All are required by name in the gate and all PASS. |
|
||||
| 8 | D-03 provider registry open, no goth type exported | VERIFIED (regression) | Gate deps check: "goth only in classes/providers_goth.go"; `TestRegistry` PASS. |
|
||||
| 9 | D-01/D-02 separate plugin, goth v1.82.0 without gothic, no gorilla/sessions, framework go.mod untouched by this phase | VERIFIED | Deps check PASS. The framework go.mod's latest commits (e903cc3, 8d4d7ce, 3f38862) belong to phase 19.8 (diary, bonfire), not 19.5. |
|
||||
| 10 | D-07 sealed state cookie | VERIFIED (regression) | `TestStateCookie` PASS. |
|
||||
| 11 | D-08 atomic `cooler.Cache.Pull`; plugin pins a release with it | VERIFIED (regression) | Covered files are unchanged; `TestPullConcurrent`, `TestPullDocker` PASS. |
|
||||
| 12 | D-10/D-11 matching table, Google flag, GitHub `/user/emails`, Facebook and Microsoft never verified (nOAuth); the owner side is now recorded proof (D-26) | VERIFIED | S `classes/flow.go:98` (`HandleCallback`) and `:203` (`CompletePending`) both gate on `profile.EmailVerified && ownerVerifiedEmail(match)`; `ownerVerifiedEmail` at `:266` is `u != nil && u.EmailVerifiedAt != nil`; `HasSelfSetPassword` is no longer read in S non-test code (grep). `TestMatching`, `TestEmailVerified`, `TestNOAuth`, `TestPreAccountTakeover` PASS. |
|
||||
| 13 | D-15/D-22 `oauth-providers` stays in sm-user-plugin; D-22's no-migration rule now relaxed only by D-27 | VERIFIED | Gate additive check: "no exported func, method or type removed and no migration changed beyond the allowed additions in the user plugin since 4c5d3b8 (13 declarations added; 1 allowed migration(s) added, column only)". `TestOAuthProvidersSourceSmoke`, `TestOAuthProviders` PASS. |
|
||||
| 14 | D-16/D-18 credentials and settings overlay | VERIFIED (regression) | `TestSettings`, `TestSettingsOverrideSmoke` PASS. |
|
||||
| 15 | D-21/D-23 sm-demo-app and sm-bm-app mount sociallogin v0.1.1; bm links Microsoft via the link ticket | VERIFIED (released state) | Gitlinks: sociallogin d7ee28d and user c43ba87 in both apps; `TestLinkTicket` PASS. Remotes are no longer in sync: S, U and B each carry the unpushed 19.5-05 commits (hold), and sm-demo-app carries foreign `b325460` (19.8). |
|
||||
| 16 | D-28 / T-19.5-26: the bm callback view removes `?code=` before the single redeem, never redeems twice, shows `?error=`, replaces to `oauth-register` for pending; no new npm dependency | VERIFIED (behavioral test) | `sm-bm-app/frontend/src/views/OAuthCallbackView.test.ts` mounts the real `<script setup>` module (module-level `redeemed` set included) on a `createMemoryHistory` router. It asserts that the route at the moment `completeOAuth` starts has no `code` and fullPath `/auth/oauth/callback`, that `replace` comes before `completeOAuth` in `invocationCallOrder`, and that Back lands on `/`. Remounting with the same code gives no second call; the alert text equals the `?error=`; pending gives `oauth-register` with `{pending, provider, reason, email}` and Back lands on `/`; a 410 shows the expiry message. I ran it: 5/5 PASS, `oauth.test.ts` 10/10. The gate's `--bm` stage requires both files (`REQUIRE_FILES`). The last commit touching `package.json`/`package-lock.json` is `fe967c4`, from before the phase. |
|
||||
| 17 | D-26: `/register` in `activate_mode=auto` then a verified Google sign-in for the same email goes pending `reason=unverified_match`, `provider=google`, links nothing; `oauth-register-complete` answers 422 `email_exists_link_account` and keeps the code | VERIFIED | `owner_verified_test.go:173` drives the real `/register`, asserts the account is activated with `EmailVerifiedAt == nil`, then signs in with an uppercase email, asserts pending with no link, posts the completion (422 + `linkMessage`), and checks `pendingExists` and that the identity has no owner. It also covers `action=register`. `TestOwnerVerifiedEmail` PASS in my Docker run. SUMMARY records RED against the old rule. |
|
||||
| 18 | D-26: a mail-activated (activate / activate-by-code) or reset-code-reset account receives the verified identity and is signed in | VERIFIED | U `classes/codes.go:133-145`: `VerifyActivationCode` sets `email_verified_at` in the same `UpdateColumns` that activates (callers `api_controller.go:369` `/activate`, `:420` `/activate-by-code`). U `controllers/api_controller.go:336-344`: `ResetPassword` sets it in the `Updates` that stores the new password, after `VerifyResetCode` and hashing. A failed reset writes nothing. `TestOwnerVerifiedEmail/mail_activated_by_code_links`, `reset_code_password_links`, and `TestPreAccountTakeover/after_mailbox_proof_links` (real `/forgot-password` + `/reset-password`) PASS. |
|
||||
| 19 | D-26: accounts without proof go pending: `/register` auto/admin, admin create and Activate action, social-created accounts, pre-v0.1.5 accounts; nothing backfills | VERIFIED | `classes.ActivateUsers` (`admin_actions.go:64-90`) writes no `email_verified_at`; the S sources write nothing (grep); the migration has no UPDATE. Tests: `TestOwnerVerifiedEmail/legacy_account_pending`, `register_action_pending`, `admin_activation_not_proof`, `unverified_provider_never_links_proven_account`; `TestEmailVerifiedAt/register_auto_no_proof`, `register_admin_mode_no_proof`, `register_ignores_submitted_proof`, `change_password_no_proof`; `TestAdminEmailVerifiedAt/admin_create_no_proof`, `admin_activate_action_no_proof`; `TestEmailVerifiedAtMigration/no_backfill`. All PASS. |
|
||||
| 20 | D-27: `users.email_verified_at` nullable TIMESTAMPTZ via migration `202610210001_add_users_email_verified_at` (ADD COLUMN IF NOT EXISTS, no rows, no-op rollback); two writers; cleared on email change through `/update` and the admin form (case-only change keeps it); not fillable, in no payload, not in admin YAML; no route, payload or config key change | VERIFIED | Migration file: exactly that SQL, registered once (`history_once`). `models/user.go:50`: `gorm:"column:email_verified_at" json:"-"`, not in `Fillable()` (`models/user.go:88`). No YAML under the plugin mentions it (grep). `api_controller.go:483-496` and `users_admin_controller.go:229-236` clear on `!strings.EqualFold`. Tests: `update_email_change_clears`, `update_same_email_keeps`, `update_ignores_submitted_proof`, `admin_email_change_clears`, `admin_save_keeps_proof`, `admin_json_hides_proof`, `column_shape`, `rollback_keeps_column`, `existing_column_kept` all PASS. The gate's migration allow-list accepts only this file (`refused_migration_changes`, self-tested). |
|
||||
| 21 | D-27 wording: the READMEs, the PHP-map row and 19.5-SECURITY-REVIEW describe proof as `email_verified_at`; none says a self-set password proves the address | VERIFIED | U `README.md:34` and S `README.md:49-55,145` ("a self-set password is not proof"); `PHP_METHOD_MAP.md:26`; SECURITY-REVIEW T-19.5-38/-39/-42. The only remaining "self-set password" mentions concern the unlink rule (D-13), which is correct. |
|
||||
| 22 | The gate fails closed on the new tests and the migration rules; `PHASE195_REQUIRE_DOCKER=1 --all` passes | VERIFIED | `scripts/check-phase19.5.sh`: `USER_ADDED_MIGRATIONS` (`:82`), `refused_migration_changes` (`:378`), `migration_writes_rows` (`:398`), and self-tests for both (`:487-512`); `DOCKER_USER_UPDATES_TESTS` (`:145`); `REQUIRE_FILES` with `missing-view-file` (`:434-439`, `:638`). My run of `PHASE195_REQUIRE_DOCKER=1 TESTCONTAINERS_HOST_OVERRIDE=127.0.0.1 bash scripts/check-phase19.5.sh --all` exited 0. All nine stages PASS, and every new test appears by name in a passed run. |
|
||||
| 23 | SC-3/D-21 release, hold branch: nothing pushed or tagged, a todo with a g15office task tracks the release | VERIFIED (hold branch) | `ls-remote --tags`: user tags end at v0.1.4, sociallogin tags at v0.1.1. No pin moved, and nothing is staged in fonoteka.go. The todo `email-proof-release.md` exists, carries `g15office: sqnwn1zi` (e819f7b) and the full D-21 step list. **The release-branch half of this truth (tags, pin bumps, gate at new pins) has not happened by user decision.** |
|
||||
|
||||
**Score:** 15/16 truths verified (1 present, behavior-unverified)
|
||||
**Score:** 23/23 truths verified (0 present, behavior-unverified)
|
||||
|
||||
### Required Artifacts
|
||||
### Required Artifacts (19.5-05)
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `.planning/notes/2026-10-20-sociallogin-goth.md` | D-02 decision note | VERIFIED | Names markbates/goth v1.82.0 (gate `--docs`); its commit 26ebb6c is an ancestor of Pull (5e0a04e) |
|
||||
| `modules/cooler/{cooler,memory,rediscache}.go` | `Cache.Pull` | VERIFIED | Interface plus both drivers; README and docs in the same commit 5e0a04e |
|
||||
| U `classes/oauth_providers.go` | `OAuthProviderSource` | VERIFIED | Implemented by S `service.go`, looked up by U `OAuthProviders` and the unlink allow-list |
|
||||
| U `controllers/oauth_identities.go` | row-locked PHP unlink | VERIFIED | `FOR UPDATE` then the guarded DELETE |
|
||||
| U `controllers/registration.go` | `PasswordLess`, `FinishLogin`, exports | VERIFIED | Called by S `classes/registration.go` and `flow.go` |
|
||||
| S `classes/provider.go` | Provider, Profile, Registry | VERIFIED | 207 lines, no goth |
|
||||
| S `classes/statecookie.go` | sealed state | VERIFIED | Contains `golem15.sociallogin.oauth-state.v1` |
|
||||
| S `classes/flow.go` | D-10 matching | VERIFIED | Contains `unverified_match` and `fb_no_email`; `lower(email) = lower(?)` |
|
||||
| S `classes/providers_goth.go` | 4 providers, the only goth importer | VERIFIED | azureadv2, `verified_email`, `user/emails`, `userPrincipalName` |
|
||||
| S `controllers/{redirect,callback,complete,register_complete,link_ticket}.go` | handlers | VERIFIED | Mounted in `routes.go` |
|
||||
| S `testdata/PHP_METHOD_MAP.md` + `php_method_map_test.go` | 38-method map plus checker | VERIFIED | 17 ported, 18 adapted, 3 not-applicable (relaxed change-password bootstrap, todo filed) |
|
||||
| `scripts/check-phase19.5.sh` | fail-closed gate | VERIFIED | `--self-test` PASS; `--all` exit 0 |
|
||||
| `sm-demo-app/sociallogin_test.go`, `sm-bm-app/sociallogin_test.go` | app-level mount tests | VERIFIED | PASS in the gate |
|
||||
| `sm-bm-app/frontend/src/lib/oauth.ts`, `views/OAuthCallbackView.vue`, `OAuthRegisterView.vue` | bm frontend wiring | VERIFIED (lib) / behavior-unverified (views) | See truth 16 |
|
||||
| U `updates/202610210001_add_users_email_verified_at.go` | `ADD COLUMN IF NOT EXISTS email_verified_at` | VERIFIED | 33 lines, registered via `init()`, wired into `All()` (`TestCreateOrganisationsMigration` expects it last) |
|
||||
| U `models/user.go` | `gorm:"column:email_verified_at" json:"-"` | VERIFIED | Read by S `flow.go:267` |
|
||||
| U `email_verified_test.go` | `TestEmailVerifiedAt`, `TestAdminEmailVerifiedAt` | VERIFIED | 377 lines; run by name in `--docker` |
|
||||
| U `updates/email_verified_at_test.go` | `TestEmailVerifiedAtMigration` | VERIFIED | 5 subtests; run by name |
|
||||
| S `classes/flow.go` | `EmailVerifiedAt != nil` | VERIFIED | Used in both match sites |
|
||||
| S `owner_verified_test.go` | `register_auto_then_google_pending` | VERIFIED | 8 subtests driving real user-plugin routes |
|
||||
| B `frontend/src/views/OAuthCallbackView.test.ts` | `completeOAuth` | VERIFIED | 5 cases; required by the gate |
|
||||
| `scripts/check-phase19.5.sh` | `USER_ADDED_MIGRATIONS` | VERIFIED | Self-test plus `--all` exit 0 |
|
||||
|
||||
### Key Link Verification
|
||||
### Key Link Verification (19.5-05)
|
||||
|
||||
| From | To | Via | Status |
|
||||
|------|----|-----|--------|
|
||||
| U `api_controller.go` `OAuthProviders` | U `classes.OAuthProviderSource` | `app.Lookup[classes.OAuthProviderSource]()` per request | WIRED |
|
||||
| S `plugin.go` Boot | U `OAuthProviderSource` | `app.Publish[userclasses.OAuthProviderSource](&providerSource{...})` | WIRED |
|
||||
| S `controllers/complete.go` | `cooler.Cache.Pull` | `PullCompletion` → `cooler.FromApp(app).Pull` | WIRED |
|
||||
| S `controllers/register_complete.go` | `PullPending` | `consume` closure run inside the final transaction | WIRED |
|
||||
| S `classes/registration.go` | U `RegisterUser{PasswordLess}`, `FireRegisterEvent`, `FireCreatedEvent`, `FinishLogin`, `IssueToken`, `APIArray` | direct calls | WIRED |
|
||||
| S `routes.go` | surf throttles | `throttle:20,1` on /oauth, `throttle:user-api` on /_user/api/v1 | WIRED (`TestRouteMiddleware`) |
|
||||
| U unlink | users row lock | `SELECT id FROM users WHERE id = ? FOR UPDATE` | WIRED (mutation-checked) |
|
||||
| `modules/cooler/rediscache.go` | Redis GETDEL | `r.client.GetDel` | WIRED |
|
||||
| bm `OAuthCallbackView.vue` | `/_user/api/v1/oauth-complete` | `auth.completeOAuth` → `userApi.oauthComplete` | WIRED |
|
||||
| bm `ProfileView.vue` | `/_user/api/v1/oauth-link-ticket` | `userApi.oauthLinkTicket` → top-level navigation with `link_ticket` | WIRED |
|
||||
| sm-demo-app `summer.yaml` | `plugins.gen.go` | `golem15.sociallogin` / `git.golem15.com/golem15/sm-sociallogin-plugin` | WIRED (`TestBootAllPlugins`) |
|
||||
| U `classes/codes.go` `VerifyActivationCode` | `users.email_verified_at` | same `UpdateColumns` as `is_activated` | WIRED |
|
||||
| U `controllers/api_controller.go` `ResetPassword` / `Update` | `users.email_verified_at` | stamped with the new password; cleared on a changed email | WIRED |
|
||||
| U `users_admin_controller.go` `FormBeforeUpdate` | `users.email_verified_at` | `user.EmailVerifiedAt = nil`, persisted by cabana Save (`admin_email_change_clears` PASS, RED recorded) | WIRED |
|
||||
| S `classes/flow.go` | U `models.User.EmailVerifiedAt` | `ownerVerifiedEmail` in `HandleCallback` and `CompletePending`; `userByEmail` loads the full row (no `Select`) | WIRED |
|
||||
| `scripts/check-phase19.5.sh` | B `OAuthCallbackView.test.ts` | `REQUIRE_FILES` in the bm stage | WIRED |
|
||||
| S `go.mod` | sm-user-plugin v0.1.5 | Task 7 require | NOT YET (hold); S builds against the workspace checkout through go.work, while go.mod still says v0.1.4 |
|
||||
|
||||
### Data-Flow Trace (Level 4)
|
||||
|
||||
| Artifact | Data | Source | Real data | Status |
|
||||
|----------|------|--------|-----------|--------|
|
||||
| `oauth-providers` response | providers | `providerSource.EnabledOAuthProviders` → `ResolveCredentials` (config + settings row per request) | yes | FLOWING |
|
||||
| `oauth-complete` response | token, user | callback → `IssueToken` and `APIArray` from the matched or created user → sealed cooler value → `Pull` | yes | FLOWING |
|
||||
| identity row | provider tokens, profile | goth `FetchUser` (or fake) → `upsertIdentity` (lagoon.Encrypted) | yes | FLOWING |
|
||||
| bm login button | providers list | `userApi.oauthProviders()` | yes | FLOWING |
|
||||
| auto-link decision | `match.EmailVerifiedAt` | `userByEmail` → Postgres `users.email_verified_at`, written only by the mail activation code and the reset-password write | yes | FLOWING |
|
||||
| bm callback view | `code`, `error`, `pending_registration` | `route.query` → `parseOAuthCallback` → `auth.completeOAuth` (stubbed in the view test; real store → `userApi.oauthComplete`, as in the previous report) | yes | FLOWING |
|
||||
|
||||
### Behavioral Spot-Checks
|
||||
|
||||
| Behavior | Command | Result | Status |
|
||||
|----------|---------|--------|--------|
|
||||
| Whole phase gate, Docker required | `PHASE195_REQUIRE_DOCKER=1 bash scripts/check-phase19.5.sh --all` | exit 0; self-test, go (4), userplugin (2), sociallogin (9), demo (3), bm (1+2, vitest 211), fonoteka, docs, docker (1 + 10 + 28) all PASS | PASS |
|
||||
| Post-release fixes reach the app pins | `git merge-base --is-ancestor d7ee28d <gitlink>` for sm-demo-app and sm-bm-app | both contain d7ee28d | PASS |
|
||||
| Remotes in sync | `git log origin/master..master` in S, U, D, B | empty | PASS |
|
||||
| Docker loopback usable | `TESTCONTAINERS_HOST_OVERRIDE=127.0.0.1 go test -short -run '^TestPermissionSetScan$' ./plugins/golem15/user/classes` (fonoteka.go) | ok 4.66 s (the VPN did not block it) | PASS |
|
||||
| Whole phase gate, Docker required | `PHASE195_REQUIRE_DOCKER=1 TESTCONTAINERS_HOST_OVERRIDE=127.0.0.1 bash scripts/check-phase19.5.sh --all` | exit 0; self-test, go (4), userplugin (2 + additive/migration check), sociallogin (9), demo (3), bm (1+2, vitest 216/216, typecheck), fonoteka, docs, docker (cooler 1, user 12, user updates 2, sociallogin 29) | PASS |
|
||||
| bm view test directly | `npx vitest run src/views/OAuthCallbackView.test.ts src/lib/oauth.test.ts` | 15/15 | PASS |
|
||||
| Hold intact | `git ls-remote --tags origin` in U and S; `git ls-tree HEAD` app gitlinks | no v0.1.5 or new sociallogin tag; pins c43ba87 / d7ee28d | PASS |
|
||||
|
||||
### Probe Execution
|
||||
|
||||
| Probe | Command | Result | Status |
|
||||
|-------|---------|--------|--------|
|
||||
| (none declared; no `scripts/*/tests/probe-*.sh` in the phase) | n/a | the phase gate above plays this role | SKIPPED |
|
||||
| (none declared; no `scripts/*/tests/probe-*.sh`) | n/a | the phase gate above plays this role | SKIPPED |
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
The phase has no REQUIREMENTS.md IDs. Every ID in the plan frontmatter is accounted for: the union of the four plans covers SC-1..SC-4 and D-01..D-25.
|
||||
No REQUIREMENTS.md IDs. Plan 05 declares D-26, D-27, D-28 and SC-3. The previous report covered SC-1..SC-4 and D-01..D-25, and every one is still satisfied (regression above).
|
||||
|
||||
| Requirement | Source Plan(s) | Status | Evidence |
|
||||
|-------------|----------------|--------|----------|
|
||||
| SC-1 | 02, 03 | SATISFIED | Truth 1 |
|
||||
| SC-2 | 02 | SATISFIED | Truths 2, 3 |
|
||||
| SC-3 | 01, 02, 03 | SATISFIED | Truths 4-6 |
|
||||
| SC-4 | 04 | SATISFIED | Truth 7 |
|
||||
| D-01 | 02, 03 | SATISFIED | Truth 9 |
|
||||
| D-02 | 01, 02 | SATISFIED | Truth 9; human package check "approved" (19.5-01-SUMMARY) |
|
||||
| D-03 | 02, 04 | SATISFIED | Truth 8 |
|
||||
| D-04 | 02, 03, 04 | SATISFIED | 4 built-ins; unknown or reserved → 404; disabled → `?error=` (`TestRoutes`) |
|
||||
| D-05 | 02, 03 | SATISFIED | `mode` ignored; invalid or missing callback → `default_callback` (`TestLinkSmoke/bad_callback_falls_back`) |
|
||||
| D-06 | 02, 03, 04 | SATISFIED | Query contract; `NormalizeReturnTo` (`TestNormalizeReturnTo`) |
|
||||
| D-07 | 02, 04 | SATISFIED | Truth 10 |
|
||||
| D-08 | 01, 02, 04 | SATISFIED | Truth 11; validate-before-pull (truth 3) |
|
||||
| D-09 | 02, 04 | SATISFIED | Truths 2, 3; opt-in token cookie (`TestTokenCookieMode`) |
|
||||
| D-10 | 02, 04 | SATISFIED (with a user-approved divergence) | Truth 12 |
|
||||
| D-11 | 02, 03, 04 | SATISFIED | Truth 12 |
|
||||
| D-12 | 02, 04 | SATISFIED | `CheckBetaKey` / `betaKeyMatches` constant time on both paths (`TestBetaGate`) |
|
||||
| D-13 | 01, 04 | SATISFIED | Truth 5 |
|
||||
| D-14 | 02, 03, 04 | SATISFIED | Link via ticket or `RequestUser`; `account_already_linked` (`TestLink`) |
|
||||
| D-15 | 01, 04 | SATISFIED | Truth 13 |
|
||||
| D-16 | 02, 04 | SATISFIED | Truth 14 |
|
||||
| D-17 | 02, 03, 04 | SATISFIED | Exact-host allow-list, fixed path, rebuilt target (`TestNormalizeFrontendCallback`, 44 cases) |
|
||||
| D-18 | 02, 03 | SATISFIED | Truth 14 |
|
||||
| D-19 | 02, 04 | SATISFIED | `TestIdentityTokens` |
|
||||
| D-20 | 02, 04 | SATISFIED | `TestRouteMiddleware` (21st request 429) |
|
||||
| D-21 | 01-04 | SATISFIED | Truth 15 |
|
||||
| D-22 | 01, 02 | SATISFIED | Truth 13; consent details in `RegisterEvent.Payload` |
|
||||
| D-23 | 02, 03, 04 | SATISFIED | Truth 15 |
|
||||
| D-24 | 01, 03 | SATISFIED | fonoteka at c43ba87, gate `--fonoteka` PASS |
|
||||
| D-25 | 01, 04 | SATISFIED | 4 plans, unit tests last |
|
||||
| Requirement | Source Plan | Status | Evidence |
|
||||
|-------------|-------------|--------|----------|
|
||||
| D-26 | 05 | SATISFIED (in local commits) | Truths 12, 17-19 |
|
||||
| D-27 | 05 | SATISFIED (in local commits; release pending per hold) | Truths 13, 20, 21; the "ships as v0.1.5 / v0.1.2 / pointer bumps" part is the held release (truth 23) |
|
||||
| D-28 | 05 | SATISFIED | Truth 16 |
|
||||
| SC-3 | 01-05 | SATISFIED at current pins | Truths 4-6; the D-26 fix is not in the apps until the release |
|
||||
|
||||
No orphaned requirements.
|
||||
|
||||
### Prohibitions (judgment tier; non-authoritative LLM-judge verdicts, unverified-prohibition: human review recommended)
|
||||
|
||||
The verdicts for plans 01-04 are unchanged from the previous report (all "no violation found"), with one update: the framework go.mod has new commits, but they are 19.8's (e903cc3, 8d4d7ce, 3f38862), not 19.5's. Plan 05 adds four:
|
||||
|
||||
| Plan | Prohibition | Judge verdict | Evidence |
|
||||
|------|-------------|---------------|----------|
|
||||
| 01, 02 | No JWT, provider token, secret or code in a log line or error | no violation found | 19.5-SECURITY-REVIEW "Log calls" table matches the S controllers; `TestSettings/no_secret_in_logs` |
|
||||
| 01, 02 | sm-user-plugin gets no migration, column, route, payload or config-key change | no violation found | `git diff 4c5d3b8 -- updates` is empty; the gate's additive check passes |
|
||||
| 01 | Framework go.mod gains no module; no goth before the human check | no violation found | No summercms.go go.mod commit since 26ebb6c |
|
||||
| 01 | Framework code and docs name no consuming application | no violation found | Gate `--docs`: 429 files checked |
|
||||
| 01-04 | Nothing pushed or tagged before the go-ahead; never force-pushed | no violation found (process) | Replies recorded in the 02 and 04 SUMMARYs; the 02 push also carried another session's planning commit 5cf5c4f (recorded) |
|
||||
| 02 | No JWT in a URL; no `?token=` read | no violation found | Redirect reads no token query; `TestLinkTicket/no_jwt_in_locations`, `TestLink` (token in query ignored) |
|
||||
| 02 | gothic and UseProviders never used; no gorilla/sessions in deps | no violation found | grep plus gate `--sociallogin` deps check |
|
||||
| 02 | No web-mode fallback; no admin identity screen | no violation found | No /select or session-login code in S |
|
||||
| 02 | goccy only; no gopkg.in/yaml.v3 import | no violation found | grep plus `go list -deps` |
|
||||
| 03 | No real credentials committed | no violation found | grep for non-empty `CLIENT_SECRET=` or `client_secret:` in D and B config and deploy: none |
|
||||
| 03 | bm never puts the JWT in a URL; the callback replaces the route | no violation found in code | `oauth.ts`, `OAuthCallbackView.vue` (behaviour: truth 16) |
|
||||
| 03 | fonoteka does not mount sociallogin | no violation found | `summer.yaml` has no sociallogin (gate `--fonoteka`) |
|
||||
| 03 | No new npm dependency in bm | no violation found | `frontend/package.json` and the lockfile are unchanged across the phase's bm commits |
|
||||
| 04 | Only test-exposed production fixes, each in its own commit | no violation found | 21f3fe5, c99f5f9, d7ee28d, each with a test |
|
||||
| 04 | Tests never call real providers or hold real secrets | no violation found | The fake transport refuses unknown hosts (`TestGothProviders/fake_transport_refuses_unknown_hosts`) |
|
||||
| 04 | A skipped Docker test never counts as passed | no violation found | My gate run used `PHASE195_REQUIRE_DOCKER=1`; the detector fails on skips |
|
||||
| 05 | sm-user-plugin stays additive: no route, payload key, config key, existing migration file or ID changes; the new migration adds a nullable column and writes no rows | no violation found | `git diff c7635c5 -- updates/` lists only the new migration and its test, plus the history list in `organisations_test.go`. The gate's additive and migration checks pass. `json:"-"`; no YAML hit. |
|
||||
| 05 | Proof is recorded only by the mailed activation code and the completed reset-code reset | no violation found | grep: the only writers are `codes.go:139` and `api_controller.go:339`; `ActivateUsers`, `Register`, `ChangePassword` and S write nothing. The non-writer tests pass. |
|
||||
| 05 | No new npm dependency in sm-bm-app/frontend and no new Go module anywhere | no violation found | `package.json`/lockfile last touched in `fe967c4` (before the phase). U and S go.mod are unchanged since their release tags (S `git diff d7ee28d --stat` does not list go.mod). |
|
||||
| 05 | Nothing pushed or tagged before the Task 6 reply; no force-push or deploy; fonoteka.go and summercms.go never pushed; only `plugins/golem15/user` staged in fonoteka.go | no violation found | Remote tags unchanged. The unpushed ranges hold the 05 commits. fonoteka.go has nothing staged; the user gitlink is modified but unstaged, and feedback/golem belong to other sessions. |
|
||||
|
||||
### Anti-Patterns Found
|
||||
|
||||
| File | Line | Pattern | Severity | Impact |
|
||||
|------|------|---------|----------|--------|
|
||||
| (none) | | No TBD, FIXME, XXX, TODO or placeholder markers in S non-test sources, the changed U files, cooler, the gate script, or the bm OAuth views and lib | | |
|
||||
| S `classes/flow.go` `ownerVerifiedEmail` + README / SECURITY-REVIEW wording | | The doc says a self-set password means the owner "proved the address". That holds after a mailed reset, not after `/register` in `activate_mode=auto` | Warning | Overstates the T-19.5-38 closure; the residual path matches PHP (see Human Verification 1) |
|
||||
| (all 05-changed files in U, S, B and the gate) | | No TBD, FIXME, XXX, TODO, HACK or placeholder markers | | |
|
||||
| S `README.md` | 50 | Offers "proves the address with a password reset" as the recovery for an unproven match. Per CR-02, a reset does not remove a pre-planted unverified identity on a social-created account | Warning | Recovery advice overstates the result for that case; see Human Verification 2 |
|
||||
| `19.5-VALIDATION.md` | 72-73 | Rows 19.5-05-06/-07 still read "awaiting the Task 6 reply"; the reply was "hold" | Info | Stale status text; the todo tracks the release |
|
||||
| `sm-bm-app/sm-bm-app` | | 70 MB untracked binary left in the bm worktree by the gate's `go build ./...` (single main package writes its binary); not gitignored | Info | Risk of an accidental commit; the gate could build with `-o /dev/null` or into a temp dir |
|
||||
| B `OAuthCallbackView.test.ts` | | Compiles the view's template at test time and uses memory history with stubbed stores | Info | The `<script setup>` ordering invariant is tested on the real module. Real `window.history` and the build-compiled template are not, which is acceptable for D-28 as written |
|
||||
|
||||
### Human Verification Required
|
||||
|
||||
#### 1. Residual pre-account takeover through password registration (decision)
|
||||
UAT row 1 (the takeover decision) passed on 2026-10-21 with decision (a), and its implementation is verified above (truths 17-21), so it is recorded under `human_verified`. A behavioral test now covers UAT row 2 (bm callback view in a browser): truth 16 is VERIFIED, so this report does not ask for it again. The orchestrator may mark row 2 as superseded by `OAuthCallbackView.test.ts`.
|
||||
|
||||
**Test:** Default config (`golem15.user.activation.activate_mode: auto`). An attacker POSTs `/_user/api/v1/register` with victim@example.com and a password. The account is activated with `has_self_set_password=true` and no email proof. The victim later signs in with Google (verified email).
|
||||
**Expected (current code):** `HandleCallback` matches the email, `ownerVerifiedEmail` returns true, and the victim's Google identity is linked onto the attacker's account (signed in as the attacker's account; the attacker keeps password access). PHP does the same (D-10 verbatim). Decide whether to accept this and document it accurately, or follow up, for example by requiring mail activation or a reset-set password before auto-linking.
|
||||
**Why human:** A security and product decision on deliberate PHP parity. No test covers this path.
|
||||
#### 1. Prohibition verdicts (UAT row 3)
|
||||
|
||||
#### 2. bm OAuth callback view in a browser
|
||||
|
||||
**Test:** Sign in through Microsoft 365 (or any configured provider) on bm. Check the URL and history after return. Open callback URLs carrying `?error=` and `?pending_registration=`.
|
||||
**Expected:** The code leaves the address bar and history before the single `oauth-complete` call; the error is shown; pending goes to `/auth/oauth/rejestracja`.
|
||||
**Why human:** Browser behaviour; only the URL helpers are unit-tested.
|
||||
|
||||
#### 3. Prohibition verdicts
|
||||
|
||||
**Test:** Read the Prohibitions table above.
|
||||
**Expected:** Confirm the "no violation found" verdicts.
|
||||
**Test:** Review the judgment-tier prohibition verdicts in the 'Prohibitions' section of this report (non-authoritative LLM-judge verdicts).
|
||||
**Expected:** Confirm or dispute each verdict, including the four new 19.5-05 rows.
|
||||
**Why human:** Judgment-tier prohibitions need human sign-off.
|
||||
|
||||
Live Google, Facebook and Microsoft round-trips with real credentials are not required by the goal. The four goth wrappers are exercised against faked provider endpoints (`TestGothProviders`, `TestGothProvidersSmoke`), and the full flow runs end to end through registry-registered fakes on Postgres. Live setup stays tracked in 19.5-USER-SETUP.md (the demo's nginx does not yet route `/oauth`).
|
||||
#### 2. CR-02 decision (new)
|
||||
|
||||
**Test:** Read 19.5-REVIEW CR-02 and 19.5-SECURITY-REVIEW "Open findings".
|
||||
**Expected:** Either accept the behaviour and correct the S README recovery wording, or schedule the follow-up (inactive account unless `p.EmailVerified`, or removal of unverified identities when a reset stamps the proof).
|
||||
**Why human:** This is a security and product decision on an open high-severity finding outside D-26..D-28.
|
||||
|
||||
### Gaps Summary
|
||||
|
||||
No blocking gaps. All four ROADMAP success criteria and all 25 CONTEXT decisions have code, wiring and passing tests. I re-ran the full fail-closed gate here with Docker required, and it exited 0. The released plugin v0.1.1 (d7ee28d) is the commit both applications pin, and every repository is pushed. What remains needs a human:
|
||||
- the bm callback view's browser behaviour;
|
||||
- a decision on the password-registration variant of pre-account takeover, which is PHP-parity behaviour that the T-19.5-38 fix (option b) does not cover and its documentation overstates;
|
||||
- sign-off on the judgment-tier prohibitions.
|
||||
No blocking gaps in code. D-26, D-27 and D-28 are implemented as specified. The proof column has exactly two writers and two clearers; the sociallogin matcher reads it at both match sites; nothing backfills it; the bm callback view's ordering invariant is pinned by a passing test. I re-ran the full fail-closed gate with Docker required, and it exited 0 with every new test passing by name.
|
||||
|
||||
What is not done is deliberate: the **release is on hold** by user decision. sm-user-plugin v0.1.5, sm-sociallogin-plugin v0.1.2 and the app pin bumps wait in `email-proof-release.md` (g15office sqnwn1zi) for the 19.8 session. Until then, sm-demo-app and sm-bm-app run v0.1.4/v0.1.1, where an auto-mode `/register` account squatting an address still receives the owner's verified Google or GitHub identity.
|
||||
|
||||
Two items remain for a human: sign-off on the judgment-tier prohibitions, and a decision on CR-02.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-10-21_
|
||||
_Verified: 2026-10-21T12:59:50Z_
|
||||
_Verifier: Claude (msd-verifier)_
|
||||
|
||||
Reference in New Issue
Block a user