Commit Graph

423 Commits

Author SHA1 Message Date
Jakub Zych
ad1b76085a test(09-03): add failing tests for form locale and dropdown options
- The same cached schema must localize pl and en independently, including Accept-Language parent fallback and raw keys
- YAML option maps keep order and scalar type, and a method provider is required at boot
2026-09-24 18:20:21 +02:00
Jakub Zych
4c814e5f63 feat(09-03): compile ordered Winter form schemas
- Strict config_form and fields documents keep source order and JSON scalar types
- Unknown keys, partials, path escape, and a mismatched modelClass fail activation with plugin context
- List-only controllers still activate when config_form.yaml is absent
2026-09-24 18:17:45 +02:00
Jakub Zych
ea3f0705f4 test(09-03): add failing test for strict form schema compilation
- All locked field kinds, empty and single documents, and source order fail closed
- Unknown keys, types, duplicates, path escape, modelClass, partials, and missing assets must name the plugin, controller, and file
2026-09-24 18:12:53 +02:00
Jakub Zych
af3312aa92 docs(09-02): complete backend identity lifecycle plan
- Record the migration, JWT lifecycle, and admin command results
2026-09-24 17:59:31 +02:00
Jakub Zych
5f218977e4 feat(09-02): add admin create and reset-password commands
- Commands hash with bcrypt, validate role codes, and revoke tokens on reset
- Generated app main appends cabana.RuntimeCommands exactly once
2026-09-24 17:56:34 +02:00
Jakub Zych
d27f442c49 test(09-02): add failing tests for admin create and reset commands
- admin:create and admin:reset-password are not registered yet
- Generated app main does not append cabana runtime commands
2026-09-24 17:54:42 +02:00
Jakub Zych
9740c3dcdb feat(09-02): implement backend JWT lifecycle, throttle, and auth logs
- Refresh, logout, and me use a separate PostgreSQL jti blacklist and safe profile
- Login stamps last_login only after a successful check and throttles repeated attempts
2026-09-24 17:50:41 +02:00
Jakub Zych
0953308e26 test(09-02): add failing tests for the backend auth lifecycle
- Login does not stamp last_login and logout, refresh, and me are unmounted
- Repeated logins are not throttled and auth events are not logged
2026-09-24 17:47:15 +02:00
Jakub Zych
06a7292dea feat(09-02): implement exact backend identity migrations
- Add the admin jti table, reset cutoff, and Winter indexes without AutoMigrate
- Reapply the developer and publisher seed idempotently and allow repeated role codes
2026-09-24 17:40:34 +02:00
Jakub Zych
448faa465f test(09-02): add failing tests for backend identity migrations
- Fresh migrate is missing tokens_valid_after and the admin blacklist table
- Reapplying the seed is not idempotent and role codes reject Winter duplicates
2026-09-24 17:38:53 +02:00
Jakub Zych
0ed980e332 docs(09-01): complete separate-admin genre list tracer plan 2026-09-24 17:23:23 +02:00
Jakub Zych
18b2e85106 feat(09-01): reject cross-audience tokens on both guards
- Frontend verification accepts a missing audience for PHP tokens
- An explicit audience must match the guard, even when the secret is shared
2026-09-24 17:20:57 +02:00
Jakub Zych
8c1de83f01 test(09-01): add failing audience crossover and authorization-order tests
- Same-secret backend token is still accepted by the frontend guard
- Permission denial must not invoke the schema or database callback
- Admin error bodies must not echo secrets or raw tokens
2026-09-24 17:19:57 +02:00
Jakub Zych
dfa00f7e3a feat(09-01): implement separate-admin genre list tracer
- Audience-aware mint, verify, refresh, and backend guard keep frontend tokens compatible
- Cabana mounts raw admin login, list schema, and record list behind admin.jwt.secret
- Framework migration seeds Winter backend users and developer/publisher roles
2026-09-24 17:17:19 +02:00
Jakub Zych
01d3871510 docs(09): create phase plan 2026-09-24 16:45:45 +02:00
Jakub Zych
1fbf492450 docs(09): resolve research planning rules 2026-09-24 15:57:19 +02:00
Jakub Zych
316bd4088f docs(phase-9): add validation strategy 2026-09-24 15:56:40 +02:00
Jakub Zych
f99950bf95 docs(09): research phase domain 2026-09-24 15:47:45 +02:00
Jakub Zych
310a4cd7a8 docs(08): add code review report 2026-09-24 01:16:02 +02:00
Jakub Zych
95e8ccde3d docs(phase-08): evolve PROJECT.md after phase completion 2026-09-24 01:14:16 +02:00
Jakub Zych
23f295824c docs(phase-08): complete phase execution 2026-09-24 01:13:55 +02:00
Jakub Zych
0fcd06fde8 docs(08-10): complete coverage, security review and final gate plan
Phase 8 closed on user approval (Playwright UI matrix gap carried forward).
AUTH-05/06/07 marked complete; ROADMAP and STATE reflect 10/10 plans done.
2026-09-24 01:04:00 +02:00
Jakub Zych
482944b160 docs(08-10): add the coverage, security-review and final-gate summary
Documents the 103-method audit closure, the self-performed 11/11-closed
security review (with disclosure), the real defects check-phase8.sh's first
end-to-end run found and fixed, and the checkpoint decision to close Phase 8
with the Playwright UI matrix gap carried forward.
2026-09-24 01:02:48 +02:00
Jakub Zych
2d551c09d7 docs(08-10): record the checkpoint decision and carry the Playwright UI matrix gap forward
scripts/check-phase8.sh's final gate ran once with every stage green except
stage_ui_harness's Playwright browser matrix, a deliberate fatal() never
authored by 08-05. The user approved closing Phase 8 with this gap carried
forward; 08-VALIDATION.md flips 08-W0-07 green, marks 08-W0-08 partially
verified, and sets nyquist_compliant: false honestly. deferred-items.md
records what the follow-up spec needs to do.
2026-09-24 00:56:20 +02:00
Jakub Zych
e562bf6f5b fix(08-10): correct check-phase8-mcp-client.mjs's connected-apps field name and revoke ordering
Two real defects surfaced by the gate's first live run against the real
fonoteka-mcp SDK:

- stage_revoke looked up the connected app by a.name; ConnectedAppsIndex
  actually serializes client_name (confirmed against
  controllers/api/connected_app_controller.go serializeConnectedApp).
- Even with that fixed, stage_revoke ran after stage_replay, by which
  point RevokeLineage's forward walk (presenting the pre-refresh spent
  secret) had already cascade-revoked the live post-refresh access token
  too -- correct, intentional T-08-REFRESH-REPLAY behavior, and the exact
  same effect 08-09-PLAN.md's own mcp-lifecycle fixture ordering already
  documented ('connected-apps would already be empty if list ran after
  replay'). stage_refresh now captures the connected-app id while the
  session is still live; stage_revoke DELETEs that id directly instead of
  re-listing (ConnectedAppsDestroy has no revoked_at filter on its own
  lookup, so this still exercises the real endpoint, idempotently, against
  the id the real MCP-driven session actually owned).
2026-09-24 00:51:28 +02:00
Jakub Zych
fef037efe8 fix(08-10): close real defects found by check-phase8.sh's first end-to-end run
08-10 Task 3 is the first time this gate has actually been executed
against real Docker/Postgres/the real fonoteka CLI/the real fonoteka-mcp
process. Four independent, previously-undetected defects surfaced:

- stage_postgres never set POSTGRES_INITDB_ARGS for the ICU pl-PL locale
  lagoon.Use requires (every other Postgres testcontainer in this project
  already does); the app failed to boot at all.
- stage_app_boot's seed step POSTed to
  /_fonoteka/api/v1/onboarding/bootstrap, a route routes.go never mounts
  (its own comment marks that group deliberately empty, pending a later
  phase). The gate's test user/collection are now seeded directly with
  SQL, matching every app-level OAuth test's own real-Postgres seeding.
- phase8_workdir() assigned PHASE8_WORKDIR from inside a function body
  that is always invoked via command substitution (a subshell): the
  assignment never escaped back to the calling shell, so every separate
  caller (stage_postgres, stage_app_boot, each phase8_mcp_stage call, ...)
  minted its own fresh mktemp directory. This silently fragmented one
  run's state (app.log, the MCP client's gate-state.json) across dozens
  of directories that never saw each other's writes -- the MCP client's
  dcr stage could never see discovery's saved metadata. PHASE8_WORKDIR is
  now set once, directly, in run_full_gate before any stage runs.
- gate-state.json (the MCP client's shared cross-invocation state) holds
  raw live secrets by design and is never redacted; stage_secret_scan
  correctly flagged it. It is now deleted once the MCP lifecycle stages
  are done with it, before the scan runs -- the scan itself stays exactly
  as strict as it already was.

stage_security_review also now refuses a nonzero threats_open count or a
missing required T-08-* row, not just a missing/unverified file, and gains
--security-review-only, a focused mode for Task 2's own verify command.
2026-09-24 00:51:17 +02:00
Jakub Zych
0c173df25c docs(08-10): add the Phase 8 security review; mark Wave 0 green
08-SECURITY-REVIEW.md: status: verified, 11/11 T-08 threats closed,
0 open, 0 accepted risks. Performed directly by the 08-10 executor
(no Task/Agent tool available this run, per the plan's documented
fallback) with re-executed named-test evidence for every threat; found
and fixed one real gap during the review (see the paired fix commit).

08-VALIDATION.md: 08-W0-01 through 08-W0-06 flip to green with their
automated commands re-run; nyquist_compliant and wave_0_complete are
now true. 08-W0-07/08-W0-08 (the full scripts/check-phase8.sh gate)
stay pending until 08-10 Task 3 actually executes it.
2026-09-24 00:12:36 +02:00
Jakub Zych
034f63907d feat(08-10): fail-closed 08-SECURITY-REVIEW.md checks in check-phase8.sh
stage_security_review now also refuses a nonzero threats_open count and
any missing required T-08-* threat row, not just a missing/unverified
file. Adds --security-review-only, a focused mode running just this
stage (Task 2's own verify command) with no services booted.
2026-09-24 00:12:28 +02:00
Jakub Zych
eb40d1bdc4 docs(08-10): add the 103-method PHP-to-Go OAuth test map
Auditable one-to-one map of every PHP OAuth functional/security test
method to its named Go test/subtest evidence, mechanically verified by
fonoteka.go/parity/oauth_audit_test.go's TestPHPTestMap.
2026-09-24 00:05:58 +02:00
Jakub Zych
f4259560c2 test(08-10): close wristband 103-method PHP audit gaps
- register loopback http:// acceptance, javascript: URI rejection, and
  error-body no-secret/no-stack-trace evidence
- token-exchange plain PKCE rejection even when verifier equals the
  stored (non-S256) challenge
2026-09-24 00:05:41 +02:00
Jakub Zych
d358bbf907 docs(08-09): complete parity-and-real-mcp-gate plan 2026-09-23 23:23:12 +02:00
Jakub Zych
e87346f9e3 feat(08-09): complete the fail-closed Phase 8 final unchanged-MCP gate
Fills in every scripts/check-phase8.sh stage skeleton with real logic:
disposable Postgres (docker run + pg_isready), the assembled Go app built
and served against it with a throwaway onboarding-seeded gate account,
the real unchanged fonoteka-mcp process started with all three required
environment variables, and the full scripted SDK lifecycle -- discovery
(MCP's own RFC 9728 401 hint, verified separately from authorization
server metadata), DCR, PKCE authorize, JWT login/consent, token, an MCP
tool call, refresh, replay of the spent refresh token, revoke, and a
post-revoke refresh failure -- delegated to the new
scripts/check-phase8-mcp-client.mjs driver, which resolves the MCP SDK's
auth helpers from fonoteka-mcp's own node_modules (no new dependency,
same pattern as parity/capture_clients.mjs). Both repositories'
vet/test/race, the full parity/corpus/secret-scan gate, the existing
check-phase8-ui.mjs --final-gate UI harness, an unchanged-client git-diff
check for both MCP_ROOT and NUXT_ROOT, and a 08-SECURITY-REVIEW.md
status:verified gate close out the stage list.

--contract-self-test validates structure only (stage names/order,
cleanup trap, loopback-only binding, the three MCP env vars, the
redaction helper, no pre-final full-run flag, read-only unchanged-client
references) in well under 30 seconds -- it boots no services. The
--red-contract self-test from Task 1 is preserved unchanged. run_full_gate
(the no-flag invocation) is 08-10 Task 3's sole execution site; 08-09
never invokes it.
2026-09-23 23:18:15 +02:00
Jakub Zych
6cc07a42e2 fix(08-09): match wristband OAuth byte contract to live-recorded PHP
Recording the full mcp-lifecycle fixture against real isolated PHP
(08-09-PLAN.md Task 2) uncovered three byte-level gaps between wristband's
assumed contract and actual production PHP behavior:

- Every explicit "Cache-Control: no-store" PHP sets is actually delivered
  as "no-store, private" (Laravel's session-cookie default merges "private"
  onto any explicit value); wristband's own default for unheadered JSON
  error responses is "no-cache, private" (matching the house convention
  already used elsewhere), not empty.
- PHP's redirect responses (authorize success and every error redirect)
  render Symfony's default HTML redirect body with Content-Type
  "text/html; charset=utf-8"; Go's bare 302 with no body never matched.
  wristband/redirect_html.go ports that exact byte template, including
  PHP's htmlspecialchars(ENT_QUOTES) escaping (Go's html.EscapeString uses
  different quote entities).

tide/normalize.go: isIDKey now also masks "_ids" plural array fields
(e.g. collection_ids), a latent parity-corpus gap no prior fixture had
exercised with a literal, non-empty, non-placeholder array value.
2026-09-23 23:12:02 +02:00
Jakub Zych
246a488412 test(08-09): add check-phase8.sh gate skeleton with RED self-test
- Declares the ordered Phase 8 stage list and stage function skeletons
- --red-contract <stage> is a permanent RED-harness self-test hook
  (exit 86, PHASE8_STAGE:<stage>:FAIL:PHASE8_RED:real-mcp-stage)
- --contract-self-test and the full gate are completed in Task 3/08-10
2026-09-23 22:44:20 +02:00
Jakub Zych
f13f76ed4b docs(08-08): complete mcp-me-prerequisite plan 2026-09-23 22:23:40 +02:00
Jakub Zych
27845490e8 docs(08-07): complete oauth-client-command plan 2026-09-23 22:05:44 +02:00
Jakub Zych
398353b135 feat(08-07): wire repeatable bonfire flags and export client-issuing helpers
- bonfire.wrap registers a Repeatable Flag as a Cobra StringSlice so
  Input.Flags returns every repeated --name=value occurrence in order;
  scalar/bare flags are unaffected (D-19)
- wristband.IssueClientCredentials/RejectRedirectURI export the exact
  random-id/secret/hash and redirect-URI validation RFC 7591
  registration already uses, so the fonoteka:oauth-client operator
  command shares one hash/validation path with DCR (T-08-SECRET-TIMING)
2026-09-23 21:56:00 +02:00
Jakub Zych
7096a90235 test(08-07): add failing repeatable-flag RED anchor for bonfire
- TestPhase8RedBonfireFlags asserts Input.Flags preserves ordered
  repeated --redirect-uri/--scope values while existing scalar --mode
  flags via Input.Flag stay unaffected
- Adds the compiling seam (Flag.Repeatable, Input.Flags,
  cobraInput.Flags) without wiring Cobra StringSlice registration yet,
  so the test fails with PHASE8_RED:bonfire-flags (D-19)
2026-09-23 21:54:44 +02:00
Jakub Zych
d70e412138 docs(08-06): complete lifecycle and sweeps plan 2026-09-23 21:42:32 +02:00
Jakub Zych
dab2b8f31a feat(08-06): implement refresh rotation, replay lineage-kill, and token sweep
rotateRefreshToken ports OAuthCodeManager::rotateRefresh: a fresh refresh
token rotates atomically (revoke old access token, mint successor, link
rotated_to_id) while a replayed (already-rotated) token instead revokes the
whole lineage and commits that kill before Token maps it to invalid_grant
outside the transaction (T-08-REFRESH-REPLAY). Token also runs the D-17
expiry sweep (DeleteExpiredCodes/DeleteExpiredRefreshTokens) before grant
processing. Server.Revoke is the new cascade-revoke seam a connected-app
controller uses instead of touching refresh rows directly.
2026-09-23 21:26:35 +02:00
Jakub Zych
b2c2cc0bb7 test(08-06): add failing refresh-lifecycle RED anchor and store interface
TestPhase8RedLifecycleFramework drives exchange -> rotate -> replay against
the real (in-memory-backed) Server.Token and fails while rotateRefreshToken
is 08-04's invalid_grant placeholder (PHASE8_RED:lifecycle-framework,
verified fail-closed via scripts/check-phase8-red.sh). Extends the
RefreshTokenStore/AuthCodeStore interfaces with the store seams Task 2's
implementation needs (ByAPITokenIDForUpdate, MarkRotated,
DeleteExpiredCodes, DeleteExpiredRefreshTokens) and updates the framework's
in-memory test double to satisfy them.
2026-09-23 21:26:05 +02:00
Jakub Zych
da01ea105c docs(08-05): record fetchguard flake resolution in deferred items 2026-09-23 21:15:43 +02:00
Jakub Zych
64f9009d3d test(fetchguard): widen streaming-cap ceiling to stop flake under parallel runs
TestFetchTooLargeIsStreaming asserted the server wrote at most 64 KiB, but
the handler keeps flushing 64-byte chunks until the client's close propagates,
which under a loaded full-suite run exceeds that (observed ~80 KiB). The
assertion guards against unbounded buffering toward 8 MiB, so 1 MiB keeps
the intent and removes the flake.
2026-09-23 21:15:43 +02:00
Jakub Zych
7f37311120 docs(08-05): complete consent plan 2026-09-23 21:11:45 +02:00
Jakub Zych
fac9648ac9 feat(08-05): add read-only UI-contract harness for consent and connected apps
check-phase8-ui.mjs encodes 08-UI-SPEC.md's full consent/connected-app
state matrix, accessibility, responsive, and i18n contract as a versioned
32-scenario catalog across 7 categories. --contract-self-test validates
catalog completeness, guarded Nuxt source-file hashes (proving the
harness itself never writes inside vue-fonoteka-app), and that
@playwright/test resolves from the already-installed dependency, all
without booting a browser or service (runs in ~50ms).

--final-gate (running verify:oauth-return-path, verify:oauth-i18n, and
the real Playwright matrix) is scaffolded but refuses to run without
PHASE8_UI_ALLOW_FINAL_GATE=1 and is explicitly 08-10's closing-checkpoint
responsibility, not executed by this plan.
2026-09-23 21:04:34 +02:00
Jakub Zych
a1fa9c6f44 feat(08-05): add wristband consent issue/deny operations
Server.PendingRequest/IssueCode/DenyPending port PHP
OAuthConsentController::pendingFor/OAuthCodeManager::issueCode as
app-agnostic protocol operations (08-CONTEXT.md D-08): every missing,
foreign-owner, used, expired, or already-issued pending row collapses to
the identical ErrPendingNotFound (T-08-CROSS-USER/T-08-REQUEST-LEAK).
IssueCode trusts the caller's already-computed granted scopes/collection
ids and returns the ordered redirect_to URL built through the existing
RFC 3986 encoder.

AuthCodeStore.MarkIssued gains scopes/collectionIDs/expiresAt parameters
(PHP's issueCode overwrites all three, not just code_hash/user_id) and
ClientStore gains MarkConsented, both required for D-08's consented_at
stamping and server-derived grant persistence. Options gains CodeTTL
(600s PHP-parity default) following the established Options-extension
pattern.
2026-09-23 20:59:24 +02:00
Jakub Zych
a459f74897 docs(08-04): complete token-exchange plan 2026-09-23 20:35:49 +02:00
Jakub Zych
4bd3b3db4f feat(08-04): implement atomic PKCE-bound code exchange in wristband
- Server.Token: JSON rejection before ParseForm, body-over-query precedence,
  Basic-over-form client auth, exact invalid_request/unsupported_grant_type/
  invalid_client/invalid_grant bodies, Cache-Control/Pragma on success only
- authenticateClient: public/confidential dispatch, constant-time secret
  compare (T-08-SECRET-TIMING)
- exchangeAuthorizationCode: single WithinTx lock/consume/mint/refresh-create
  covering code/client/redirect/resource/PKCE binding and single-use replay
  (T-08-CODE-REPLAY), sequential and concurrent proofs
- rotateRefreshToken: grant_type=refresh_token dispatches per PHP validity
  but is a deliberate invalid_grant placeholder; full rotation is 08-06
- full token_test.go behavior matrix appended alongside the RED anchor
2026-09-23 20:28:07 +02:00
Jakub Zych
5ca830beef test(08-04): add failing code-exchange RED test in wristband
- Server.Token 501 stub and TestPhase8RedCodeExchange (PHASE8_RED:code-exchange)
- Options gains AccessTokenTTL/RefreshTokenTTL with PHP-parity defaults
2026-09-23 20:25:16 +02:00
Jakub Zych
5dc8c35e06 docs(08-03): record parity test fix and fetchguard flake in deferred items 2026-09-23 20:16:21 +02:00