From 8af7c22cfc9efeb04aaf34bd0cfd13a6c44ff47f Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 21 Oct 2026 15:01:58 +0200 Subject: [PATCH] docs(19.5): re-verification after gap closure 05 and UAT items --- .../19.5-UAT.md | 14 +- .../19.5-VERIFICATION.md | 260 ++++++++---------- 2 files changed, 126 insertions(+), 148 deletions(-) diff --git a/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-UAT.md b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-UAT.md index e52afbf1..02027b66 100644 --- a/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-UAT.md +++ b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-UAT.md @@ -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 diff --git a/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-VERIFICATION.md b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-VERIFICATION.md index a8b01e7f..ee4669f2 100644 --- a/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-VERIFICATION.md +++ b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-VERIFICATION.md @@ -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/). 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=[&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/

/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 `