docs(19.6-04): draft summary at the Task 5 release checkpoint

This commit is contained in:
2026-10-26 15:06:45 +01:00
parent 692beb5538
commit 12b60d0194
2 changed files with 260 additions and 1 deletions

View File

@@ -0,0 +1,250 @@
---
phase: 19.6-two-factor-auth-and-device-flow-in-sm-user-plugin
plan: 04
subsystem: testing
tags: [sm-user-plugin, sm-mfa-plugin, sm-sociallogin-plugin, unit-tests, security-review, phase-gate, testcontainers, webauthn, totp, device-flow, d-14]
requires:
- phase: 19.6-01
provides: login gates, password-change hook, Reset 2FA slot, device authorization (sm-user-plugin v0.1.12)
- phase: 19.6-02
provides: sm-mfa-plugin core (TOTP, recovery and email codes, failure cap, trusted devices, settings, admin reset)
- phase: 19.6-03
provides: WebAuthn and passkeys (sm-mfa-plugin v0.1.0), the social-login gate (sm-sociallogin-plugin v0.1.4), adoption in sm-demo-app, sm-bm-app, fonoteka.go
provides:
- scripts/check-phase19.6.sh, a fail-closed gate over seven repositories (self-test, go, userplugin, mfa, sociallogin, demo, demo-web, bm, fonoteka, docs, docker)
- sm-user-plugin unit tests for the gate chain, Login order, password-change sources, the reset action, the whole device flow and its migration
- sm-mfa-plugin unit tests, one per decision, the PHP bodies, the login-path invariant and the PHP security test map
- sm-sociallogin-plugin TestSocialGate
- 19.6-SECURITY-REVIEW.md with the explicit D-14 section; 19.6-VALIDATION.md validated
- one post-release fix in sm-mfa-plugin (71d8f27), unreleased pending the Task 5 reply
affects: [19.6 verification, sm-demo-app, sm-bm-app, fonoteka.go, sm-mfa-plugin release]
actuals:
tokens: 97673 # MEASURED chars/4 over added lines: summercms.go e95277a..692beb5 (80239), sm-user-plugin 3b22134..1f3266a (94434), sm-mfa-plugin 33ad164..d08f1ec (204699), sm-sociallogin-plugin b752177..14d5f93 (11321)
tasks: 4 # Tasks 1-4 done; Task 5 is the open checkpoint, Task 6 follows the reply
commits: 5 # MEASURED in summercms.go before this SUMMARY commit: git rev-list --count e95277a..692beb5
sm_user_plugin_commits: 3 # MEASURED: git rev-list --count 3b22134..1f3266a
sm_mfa_plugin_commits: 5 # MEASURED: git rev-list --count 33ad164..d08f1ec
sm_sociallogin_plugin_commits: 1 # MEASURED: git rev-list --count b752177..14d5f93
plan_head_before: e95277a21049bf92a1f427d2d9379c4d26da35d8
plan_head_after: 692beb5
sm_user_plugin_head_before: 3b22134
sm_user_plugin_head_after: 1f3266a
sm_mfa_plugin_head_before: 33ad164
sm_mfa_plugin_head_after: d08f1ec
sm_sociallogin_plugin_head_before: b752177
sm_sociallogin_plugin_head_after: 14d5f93
tech-stack:
added: []
patterns:
- "Phase gate per phase: go test -json detector refusing fail, skip, zero tests and missing names; --self-test on planted output"
- "Token-mint inventory test: every mint call site read from the source and listed with its reason"
- "Dry-run GORM dialector in tests to run DB-touching ceremony code without Postgres"
- "Mutation checks on guarded UPDATEs recorded in the security review"
key-files:
created:
- scripts/check-phase19.6.sh
- .planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/19.6-SECURITY-REVIEW.md
- ../fonoteka.go/plugins/golem15/user/login_gate_test.go
- ../fonoteka.go/plugins/golem15/user/password_changed_test.go
- ../fonoteka.go/plugins/golem15/user/reset_two_factor_test.go
- ../fonoteka.go/plugins/golem15/user/device_auth_test.go
- ../fonoteka.go/plugins/golem15/user/classes/device_auth_test.go
- ../fonoteka.go/plugins/golem15/user/updates/device_auth_sessions_test.go
- ../sm-mfa-plugin/{gate,challenge,totp,recovery,email,trusted,settings,status,admin,tokens_at_rest,migrations,webauthn,passwordless,routes,php_bodies,login_paths,php_method_map,php_port}_test.go
- ../sm-mfa-plugin/testdata/PHP_METHOD_MAP.md
- ../sm-mfa-plugin/testdata/php_methods.txt
- ../sm-sociallogin-plugin/social_gate_test.go
modified:
- .planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/19.6-VALIDATION.md
- .planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/deferred-items.md
- ../fonoteka.go/plugins/golem15/user/README.md
- ../fonoteka.go/plugins/golem15/user/updates/postgres_test.go
- ../sm-mfa-plugin/{classes/config.go,models/settings.go,lang/en/lang.yaml,lang/pl/lang.yaml,README.md,harness_test.go}
- ../sm-sociallogin-plugin/README.md
key-decisions:
- "An empty settings list (methods or enforce groups) is an empty value and falls back to the configuration, as the README promised; the form stores it as NULL (sm-mfa-plugin 71d8f27, post-release fix)"
- "Error bodies are pinned byte for byte; multi-key success bodies by key set and fixed values, because Go maps sort keys where PHP kept insertion order (recorded, not changed)"
- "Finding F-1 (activate-by-code mints without the second factor when require_activation is off) is raised at Task 5 rather than fixed: it overturns a recorded plan 01 decision and needs a contract and release choice"
patterns-established:
- "check-phase<N>.sh stage per repository, names appended per plan, docker NOT RUN explicit"
requirements-completed: [] # plan not complete: Task 5 (checkpoint) and Task 6 are open
duration: about 76 min (2026-10-26 12:50Z to 14:06Z), to the Task 5 checkpoint
completed: 2026-10-26
status: draft
---
# Phase 19.6 Plan 04: Unit tests and security review Summary
**Status: draft (Task 5 checkpoint).** Tasks 1-4 are done and committed; the whole gate passes across seven repositories with Docker. One post-release fix (sm-mfa-plugin `71d8f27`) waits for the Task 5 go-ahead, and finding F-1 needs a decision. Nothing was pushed or tagged.
**One fail-closed gate over seven repositories, 41 new named unit tests across sm-user-plugin, sm-mfa-plugin and sm-sociallogin-plugin (one per decision, the PHP bodies, the token-mint inventory and every login path), the PHP security test mapped, a security review with the D-14 passkey section, one real defect fixed (an untouched settings form disabled every second factor) and one finding raised (activate-by-code).**
## Performance
- **Started:** 2026-10-26T12:50Z
- **Checkpoint:** 2026-10-26T14:06Z
- **Tasks:** 4 of 6 done; Task 5 is the open checkpoint
## Commits per repository (in order)
**summercms.go** (base `e95277a`):
| Task | Commit | Message |
|------|--------|---------|
| 1 | `947009a` | test(19.6): fail-closed phase gate |
| 2 | `e9785f7` | test(19.6): gate requires the user-plugin unit tests |
| 3 | `531e7ab` | test(19.6): gate requires the mfa and social gate tests |
| 4 | `ea4680e` | test(19.6): gate requires the PHP map |
| 4 | `692beb5` | docs(19.6): security review and validation map |
**sm-user-plugin** (`../fonoteka.go/plugins/golem15/user`, base `3b22134` = v0.1.12; branch master, 3 commits ahead of origin):
| Task | Commit | Message |
|------|--------|---------|
| 2 | `6844d36` | test: login gates, password-changed sources and the reset action |
| 2 | `2c68979` | test: start the updates package's Postgres only without -short |
| 2 | `1f3266a` | test: device authorization flow and its migration |
**sm-mfa-plugin** (base `33ad164` = v0.1.0):
| Task | Commit | Message |
|------|--------|---------|
| 3 | `71d8f27` | fix: an empty settings list falls back to the configuration |
| 3 | `8ec140e` | test: TOTP, QR, recovery and email codes, the gate, challenges and the failure cap |
| 3 | `2aff160` | test: trusted devices, revocation, settings, status, admin reset, tokens at rest and migrations |
| 3 | `ab886f8` | test: WebAuthn, passkeys, routes, PHP bodies and the login-path invariant |
| 4 | `d08f1ec` | test: PHP security test map |
**sm-sociallogin-plugin** (base `b752177` = v0.1.4):
| Task | Commit | Message |
|------|--------|---------|
| 3 | `14d5f93` | test: social sign-in gate |
No push, tag or remote operation. sm-demo-app, sm-bm-app and fonoteka.go have no commit from this plan (fonoteka.go's working tree shows ` M plugins/golem15/user` because its user submodule now sits on the three test commits; the committed pointer is still `3b22134`).
## Tests added (names the gate requires)
- **sm-user-plugin, Docker-free:** `TestLoginGates` (also under `-race`), `TestDeviceAuthURL`, `TestDeviceDisplayName`, `TestDeviceCodes` (classes).
- **sm-user-plugin, Postgres:** `TestLoginGateOrder`, `TestPasswordChangedSources`, `TestResetTwoFactorAction`, `TestDeviceInitiate`, `TestDeviceStatus`, `TestDeviceAuthorize`, `TestDeviceHandOverConcurrent`, `TestDeviceListAndRevoke`, `TestDeviceRevokeAfterRefresh`, `TestDeviceTokensAtRest`, `TestDeviceAuthSessionsMigration` (updates).
- **sm-mfa-plugin, Docker-free:** `TestTOTP`, `TestQRSVG`, `TestRecoveryCodeFormat`, `TestMaskEmail`, `TestEmailExpiryMinutes`, `TestWebAuthnShapes`, `TestRoutesAndBuckets`, `TestPHPMethodMap`, `TestPHPUser005`.
- **sm-mfa-plugin, Postgres:** `TestGate`, `TestChallengeLifecycle`, `TestFailureCap`, `TestRecovery`, `TestEmailCode`, `TestTrustedDevice`, `TestRevocation`, `TestSettings`, `TestStatusShape`, `TestAdminReset`, `TestTokensAtRest`, `TestMigrations`, `TestWebAuthn`, `TestPasswordless`, `TestPHPErrorBodies`, `TestEveryLoginPathIsGated`.
- **sm-sociallogin-plugin, Postgres:** `TestSocialGate`.
- The smoke tests of plans 01-03 stay and are required by name too.
## Gate stages (`scripts/check-phase19.6.sh --all`, clean run 13:59Z, 4 m 55 s)
| Stage | State | What ran |
|-------|-------|----------|
| self-test | PASS | detectors on planted output, additive, pin, mount and migration checks, docker NOT RUN rule on a faked PATH |
| go | PASS | vet and `-short` of the framework; its go.mod requires none of the three libraries |
| userplugin | PASS | vet and `-short` through the fonoteka.go workspace, 5 + 1 (`-race`) + 1 (classes) named tests, additive check against `50da92c` (56 declarations added, none removed or changed), migration check (only `202610260001_create_device_auth_sessions` added) |
| mfa | PASS | vet and `-short`, 11 named tests, no `gopkg.in/yaml.v3`, the three pins in go.mod |
| sociallogin | PASS | vet and `-short`, 9 named tests, Phase 19.5 dependency rules |
| demo | PASS | build, vet, `TestBootAllPlugins`, `TestGoModRequiresTags`, `TestMFAMounted` |
| demo-web | PASS | `pnpm test`: 202 tests, `app/lib/twoFactor.test.ts` required |
| bm | PASS | build, vet, `TestSocialLoginMounted`, `TestMFAMounted` |
| fonoteka | PASS | vet and `-short` of the root and the four workspace modules; no golem15.mfa mount; no go-webauthn, pquerna or boombuler module |
| docs | PASS | decision note pins, STACK rows, `TestDocsTree`, `docs:build --check`, no consuming application named in modules/, docs/ or the two plugin READMEs |
| docker | PASS | 15 + 1 user-plugin, 21 mfa and 2 sociallogin Postgres tests by name |
**Docker:** reachable as golem; no stage was NOT RUN. Not part of the gate (scoped out, predating this phase, deferred-items.md): the five Docker-backed failures in fonoteka.go's `plugins/golem15/fonoteka` (the fonoteka stage runs `-short`), and sm-demo-app's `scripts/build.sh release`, which refuses the leftover journal and oauthserver submodule moves. **NOT RUN:** a real-browser WebAuthn ceremony and the demo screens in a browser (manual-only, 19.6-VALIDATION.md).
Mutation checks (19.6-SECURITY-REVIEW.md "Method"): seven mitigations removed one at a time, each caught by its test, each reverted. The first `--all` run overlapped those edits, so the result above is a second, clean run.
## Defects found and fixed
| Commit | Repo | Found by | Defect | Fix |
|--------|------|----------|--------|-----|
| `71d8f27` | sm-mfa-plugin | `TestSettings/form_round_trip_keeps_the_configuration` (written for this) | The admin form shows a NULL method list as an empty taglist and sends `[]` back on every save, so saving the settings once stored `[]` and made every method unavailable (TOTP setup answered 403). | Empty lists are stored as NULL (`Settings.BeforeValidate`) and an empty stored list falls back to the configuration (`overlay`), as the README promised; README and en/pl comments updated. |
| `2c68979` | sm-user-plugin (test harness) | the gate's userplugin stage | The `updates` package's TestMain scanned `os.Args` for `-test.short`, so every `-short` run started Postgres (and would fail without Docker). | `flag.Parse` and `testing.Short()`, as CLAUDE.md prescribes. Test-only. |
## Post-release fixes
Production commits this plan made after plan 03's release (outside `*_test.go`, `testdata/` and README-only changes), against every pin that lacks them:
| Repository | Commit | Files | Released pins that lack it |
|------------|--------|-------|----------------------------|
| sm-mfa-plugin | `71d8f27` | `classes/config.go`, `models/settings.go`, `lang/en/lang.yaml`, `lang/pl/lang.yaml`, `README.md` | tag `v0.1.0` (`33ad164`); sm-demo-app gitlink `plugins/golem15/mfa` (`33ad164`) and its go.mod require `sm-mfa-plugin v0.1.0`; sm-bm-app gitlink `plugins/golem15/mfa` (`33ad164`) |
`git -C ../sm-mfa-plugin merge-base --is-ancestor 71d8f27 33ad164` exits 1: no pin contains the fix. sm-user-plugin and sm-sociallogin-plugin have test and README commits only: nothing to release there (their pins stay correct). Before the reply, no new tag exists: `git -C ../sm-mfa-plugin tag --points-at HEAD` is empty.
## Task 5 reply
Pending: the checkpoint was returned to the orchestrator. Plan 03's Task 4 reply ("1. release 2. push demo 3. you can sudo chown golem:golem sm-sociallogin-plugin if needed.") did not pre-authorise plan 04 patch releases.
Decisions asked at Task 5:
1. Release the sm-mfa-plugin fix (proposed tag v0.1.1 on the commit after the release, with the corrected mode names in the tag message), then the demo pointer, go.mod require and go.sum, and the bm gitlink in separate commits, pushed after the usual fetch and range check; or hold (todo plus g15office task).
2. Finding F-1: (a) fix activate-by-code in sm-user-plugin with a patch release, (b) accept with the corrected reason, or (c) defer to a todo.
3. Whether to push the test-only commits of sm-user-plugin (3), sm-mfa-plugin (4) and sm-sociallogin-plugin (1) along with the release.
## Accomplishments
- **Gate (Task 1).** `scripts/check-phase19.6.sh` copies check-phase19.5.sh's detector, adds the mfa, demo-web and fonoteka module checks, an additive check that also catches removed or changed const and var declarations (gofmt realignment and moves pass), and the device-session migration check. `--self-test` proves every detector on planted output.
- **sm-user-plugin (Task 2).** The gate chain and Login's order (refused first factors never reach a gate; rehash before a challenge; restore and reactivate mail only after the gates; gate answers pass through; refresh never asks a gate); each password-change source fires once in its transaction (a listener's write commits with it, its error rolls both back); the reset action's offer, permission, transaction and rollback; the device flow in every state and refusal, including 50 concurrent polls, 10 concurrent authorizes by two users, the bounded cleanup, a real short-code collision forced by a trigger, revoke after one and two refreshes and the migration's constraints.
- **sm-mfa-plugin (Task 3).** One test per decision of RESEARCH's map (above), the PHP bodies of both route tables, and `TestEveryLoginPathIsGated`, which reads every token mint site of golem15.user and golem15.mfa from source and fails on any unlisted one, then drives each first-factor path of a TOTP user.
- **sm-sociallogin-plugin (Task 3).** `TestSocialGate` over an existing identity, a verified email match and a new account, in token-cookie mode.
- **PHP map, review, validation (Task 4).** `AuthenticationTest.php` has 3 methods: USER-003 maps to `TestStateCookie` (sociallogin, adapted), USER-004 is not applicable (no PIN routes in Go), USER-005 to `TestPHPUser005`. The review maps T-19.6-01..38 and the four SC rows, has the D-14 and D-18 sections, ASVS notes, a log sweep, residual risks and F-1.
## Deviations from Plan
**1. [Rule 1 - Bug] Settings form disabled every method (sm-mfa-plugin `71d8f27`).** See "Defects found and fixed". A production change after the release, hence the Post-release fixes row and the Task 5 stop.
**2. [Rule 3 - Blocking] updates TestMain short detection (sm-user-plugin `2c68979`).** Test-only; needed so the userplugin stage's `-short` run is Docker-free as the gate's NOT RUN rule assumes.
**3. [Rule 4 - raised, not fixed] Finding F-1, activate-by-code.** Reproduced through the mfa harness (`require_activation: false`, `activate_mode: user`: password login, TOTP enabled, activate-by-code answers a token without a challenge). Plan 01 accepted this path because "a not-yet-activated account cannot have 2FA yet", which is false in that configuration. Fixing it changes a recorded decision and an endpoint's answer and needs a user-plugin release, so it goes to the Task 5 decision (security review "Open findings").
**4. [Carry-forward] D-07 follows the code.** `TestPasswordChangedSources` listens on `AccessRevokedEvent` and checks `PasswordChanged()` (no `PasswordChangedEvent` exists). The administrator's save fires from `FormBeforeUpdate`, before cabana writes the record in the same transaction, so a listener reads the old hash there; the test pins that and proves the transaction with a marker write that commits or rolls back with the password.
**5. [Carry-forward] Migration and base.** The gate allows `202610260001_create_device_auth_sessions` (the plan's `202610210002` is named in a comment); the additive base is `50da92c` (v0.1.11), overridable with `PHASE196_USER_BASE`.
**6. [Plan text vs code] An enabled webauthn row without a credential passes the gate.** The plan said such a user is "challenged with available_methods [] as PHP"; plan 02 deviation 7 chose to challenge only with a usable method. `TestGate/no_usable_method_passes` pins the code.
**7. [Adapted] Success bodies compared by keys.** `TestPHPErrorBodies` compares every error and 422 body byte for byte, but multi-key success bodies by key set and fixed values: Go maps emit sorted keys, PHP kept insertion order. Recorded as an observation (security review, deferred-items.md), not changed.
**8. [Adapted] Management 401 bodies.** An unauthenticated management call is answered by golem15.user's jwt.auth (`{"error":true,"message":"Token not provided"}`, `"Wrong number of segments"`, `"User not found"`) before the handler, so the handler's `{"error":"Unauthorized"}` is unreachable; the test pins the reachable bodies. Likewise surf checks the `{id}` digit constraint inside the middleware chain, so without a session a non-numeric id gets 401; with a session it gets the router's 404 (`TestWebAuthn`).
**9. [Adapted] Short-code input.** Surrounding whitespace is trimmed from the field; inner spaces count toward the 8-character check (422), as PHP's hyphen-only strip did. Pinned in `TestDeviceAuthorize`.
**10. [Test design] Dry-run GORM handle.** `TestWebAuthnShapes` runs the plugin's own `BeginRegistration` and `FinishRegistration` on a dry-run dialector (gorm's own packages, no new module), so the creation options and the single-use state are pinned without Postgres.
**11. [Scope] fonoteka stage runs every workspace module.** The root `./...` covers only the root module (plan 03 found this), so the stage also vets and short-tests `plugins/golem15/{feedback,fonoteka,golem,user}`.
**12. [Environment] sm-user-plugin checkout was on a detached HEAD at `3b22134`.** The first test commit landed detached; `master` (at `3b22134`, the same commit) was checked out and fast-forwarded to it. No history changed.
**13. [Orchestrator update] sm-mfa-plugin is public.** The gate needs no credential note; deferred-items.md marks the private-repository item resolved.
**Total deviations:** 13 (1 bug fixed in production, 1 test-harness fix, 1 finding raised, 2 carry-forwards, 1 plan-text mismatch, 3 adapted assertions, 1 test design, 1 scope, 2 environment).
## Known Stubs
None.
## Threat Flags
None: plan 04 adds tests, a script and documents; the one production change narrows behaviour to what the README documents.
## Issues and follow-ups
- **F-1** (above): decision at Task 5.
- **deferred-items.md** gains: the `classes` package of sm-user-plugin starts Postgres under `-short` (predates the phase; same fix as `2c68979`); the JSON key-order observation; F-1. The private-repository item is resolved.
- **fonoteka.go** user submodule sits three test commits ahead of its committed pointer (no release needed; bump only if wanted).
## g15office
This executor has no g15office tool; the orchestrator posts the plan 04 update on milestone task `avoroeb4` (gate result: all 11 stages PASS with Docker).
## Self-Check: PASSED
- Commits exist: summercms.go `947009a`, `e9785f7`, `531e7ab`, `ea4680e`, `692beb5`; sm-user-plugin `6844d36`, `2c68979`, `1f3266a`; sm-mfa-plugin `71d8f27`, `8ec140e`, `2aff160`, `ab886f8`, `d08f1ec`; sm-sociallogin-plugin `14d5f93`.
- Files exist: `scripts/check-phase19.6.sh` (executable), `19.6-SECURITY-REVIEW.md`, the test files listed in key-files, `../sm-mfa-plugin/testdata/PHP_METHOD_MAP.md` and `php_methods.txt`.
- Acceptance greps of Tasks 1-4 pass (gate: `PHASE196_REQUIRE_DOCKER` 7, mode lines 12, `TestWebAuthnCeremonySmoke` 2, `202610210002_create_device_auth_sessions` 2; review: T-19.6 references 44, `D-14` 4; validation: `nyquist_compliant: true` 1, `check-phase19.6.sh` 10).
- Working trees: summercms.go keeps only other sessions' `STATE.md`, `state.json` and untracked paths; the three plugin repositories are clean.

View File

@@ -21,8 +21,17 @@
status: open
**What:** the config values are `disabled`, `optional` and `enforced`. The tag is pushed and is not rewritten (that would need a force-push). The README and `config/config.yaml` are correct. The next release's tag message should name the right values.
- sm-demo-app: the release build needs read access to the private sm-mfa-plugin repository
status: open
status: resolved (2026-10-26: the user made golem15/sm-mfa-plugin public; anonymous HTTPS `git ls-remote` of v0.1.0 works)
**What:** found by 19.6-03 Task 6 part (1). `scripts/build.sh release` and `scripts/check-pins.sh` run with `GOWORK=off GOPRIVATE=git.golem15.com/*`, so Go fetches every required plugin over anonymous HTTPS. The other `sm-*-plugin` repositories allow that; sm-mfa-plugin is private, so the fetch fails ("could not read Username for 'https://git.golem15.com'"). On this machine the release build and the pins check pass, because v0.1.0 is already in the module cache: `go get` filled it through a one-command `url.git@git.golem15.com:.insteadOf https://git.golem15.com/` rewrite in the environment, and no global config was changed. A clean module cache, or another build machine, fails. The user decides: make sm-mfa-plugin public like the other plugins, or give the build host credentials (an `insteadOf` rewrite to SSH or a netrc) and document it in DEPLOY.md. sm-bm-app is not affected (it builds from the submodule through a replace).
- sm-demo-app: uncommitted journal and oauthserver submodule moves left over from Phase 19.7 plan 05
status: open
**What:** tracked as the pending todo `.planning/todos/pending/2026-10-26-sm-demo-app-journal-oauthserver-pointers.md`. 19.6-03 left both unstaged.
- sm-user-plugin: the `classes` package starts Postgres under `-short`
status: open
**What:** found by 19.6-04. `classes/throttle_test.go` `throttleDB` (used by `TestCodes` and other `classes` tests) starts a testcontainers Postgres without checking `testing.Short()`, so `go test -short ./...` of the user plugin needs Docker (about 45 s on this host) and fails on a host without it. Predates this phase. 19.6-04 fixed the same problem in the `updates` package (`2c68979`, its TestMain scanned `os.Args` for `-test.short`). Fix it the same way in a `test:` commit.
- sm-mfa-plugin and sm-user-plugin: multi-key success bodies carry Go's sorted key order
status: open (observation)
**What:** found by 19.6-04 `TestPHPErrorBodies`. Bodies built from Go maps (the login challenge, `2fa/status`, `totp/setup`, `email/send`, the WebAuthn register answer, the device initiate and status bodies) list their keys alphabetically, where PHP's `response()->json([...])` kept insertion order. Values and key sets match PHP; error bodies and the 422 bodies are byte-identical. JSON clients do not depend on key order, so nothing was changed; recorded in 19.6-SECURITY-REVIEW.md "Residual risks" for a decision if a client ever compares raw bytes.
- sm-user-plugin: activate-by-code mints a session without the second factor when `require_activation` is off (finding F-1)
status: open (decision at plan 04 Task 5)
**What:** see 19.6-SECURITY-REVIEW.md "Open findings".