--- phase: 14.1-oauth-identities-and-fonoteka-me-routes plan: 01 subsystem: api tags: [oauth-identities, sm-user-plugin, me-token, parity, gormigrate, lagoon.Encrypted, wire.Time, throttle] requires: - phase: 07-user-plugin-and-authentication provides: "sm-user-plugin sessionApp/insertUser, jwt.auth, lang YAML, controllers.writeJSON" - phase: 08-oauth2-1-authorization-server provides: "JWT group in fonoteka routes.go, MeToken personal-token bootstrap, TokenSurfaceIsolation coverage" - phase: 14-domain-jobs-and-external-integrations provides: "parity corpus at 175 PHP routes with 3 pending D-09 identity/me entries, fonoteka seed extras, php_parity.sh recording" provides: - "OAuthIdentity model and gormigrate 202610050001_create_oauth_identities in sm-user-plugin (table golem15_user_oauth_identities, full PHP column set)" - "Exported OAuthIdentitiesIndex, OAuthIdentitiesDestroy, OAuthIdentitiesOptions; host JWT mounts GET/DELETE /oauth-identities with Winter HTML 404 and throttle:10,1 on DELETE" - "D-09 MeToken collection_ids JSON null when CollectionIDs is invalid or empty; scopes still wire.Slice" - "Parity extras oauth-identities-linked and me-unrestricted; three manifest routes ported; TestParityCorpus recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0" affects: [14.1-02 unit tests, 15-cutover (zero pending routes)] actuals: tokens: 5100 tasks: 4 commits: 5 plan_head_before: c3bbbcbb9f7e247c353e262d0fec8126fac78e04 plugin_repo_head_after: ac8aa16217556f70f1e1b2c0a3495c7eea6b5747 app_repo_head_after: 1584d7390d80b8f1d3e8de53c2f0144deb9ebd64 tech-stack: added: [] patterns: - "Host-mounted constructors: sm-user-plugin exports Index/Destroy; the host chooses the path, throttle and WriteNotFound (Winter HTML 404), never a /_user route" - "Parity extras keyed as \"#\" on both PHP tinker reset and Go seedFonotekaCase" - "assertPortedMismatch wraps a ported fixture with mutateStatus and requires tide.MismatchError" key-files: created: - ../fonoteka.go/plugins/golem15/user/models/oauth_identity.go - ../fonoteka.go/plugins/golem15/user/updates/202610050001_create_oauth_identities.go - ../fonoteka.go/plugins/golem15/user/controllers/oauth_identities.go - ../fonoteka.go/plugins/golem15/user/oauth_identities_test.go - ../fonoteka.go/parity/fixtures/routes/GET___fonoteka_api_v1_oauth-identities_jwt__linked.yaml - ../fonoteka.go/parity/fixtures/routes/GET__api_v1_fonoteka_me_personal_token__unrestricted.yaml modified: - ../fonoteka.go/plugins/golem15/user/lang/en/lang.yaml - ../fonoteka.go/plugins/golem15/user/lang/pl/lang.yaml - ../fonoteka.go/plugins/golem15/user/README.md - ../fonoteka.go/plugins/golem15/fonoteka/routes.go - ../fonoteka.go/plugins/golem15/fonoteka/phase08_coverage_test.go - ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go - ../fonoteka.go/parity/manifest.yaml - ../fonoteka.go/parity/parity_test.go - ../fonoteka.go/parity/parity_contract_test.go - ../fonoteka.go/parity/fonoteka_seed_test.go - ../fonoteka.go/parity/fonoteka_reset.php - ../fonoteka.go/parity/genres_seed_test.go - ../fonoteka.go/parity/fixtures/routes/GET__api_v1_fonoteka_me_personal_token.yaml key-decisions: - "Task 1 answered ship-shared-full before dispatch: table golem15_user_oauth_identities lives in sm-user-plugin with the full PHP column set (locked D-01 and D-07)" - "routes.go in sm-user-plugin stays byte-identical; identity handlers are host-mounted on the fonoteka JWT group" - "Last-method 409 counts identities only (D-06); HasSelfSetPassword is unused" - "Restricted /me was re-recorded from the fonoteka reset so {{id:collection}} resolves; the Phase 2 {{id:token}} placeholder would not" patterns-established: - "OAuth identity list is an explicit {provider, linked_at} map; Encrypted tokens and profile_data are Hidden and json:\"-\"" - "DELETE {provider} is unconstrained on the mux so unknown providers reach WriteWinterHTTPError" - "MeToken: scopes use wire.Slice; collection_ids is a bare any that stays JSON null when empty" requirements-completed: [HTTP-04, HTTP-06] requirements-blocked-until-02: [API-09, HTTP-01, HTTP-03, DATA-02, DATA-07, I18N-01] coverage: - id: D1 description: "golem15_user_oauth_identities exists in sm-user-plugin via gormigrate with Encrypted tokens, Hidden secrets, and empty Fillable" requirement: DATA-02 verification: - kind: unit ref: "plugins/golem15/user#TestOAuthIdentitiesIndexEmptyList" status: pass human_judgment: false - id: D2 description: "JWT GET /_fonoteka/api/v1/oauth-identities returns {\"data\":[]} and, with extra oauth-identities-linked, facebook then google linked_at +00:00 without secrets" requirement: HTTP-06 verification: - kind: e2e ref: "parity#TestParityCorpus coverage line recorded 175/175 passing 175" status: pass - kind: unit ref: "plugins/golem15/user#TestOAuthIdentitiesIndexEmptyList" status: pass human_judgment: false - id: D3 description: "JWT DELETE /oauth-identities/{provider} is throttle:10,1, unconstrained, 204 keeps the sibling row" requirement: HTTP-01 verification: - kind: unit ref: "plugins/golem15/user#TestOAuthIdentitiesDestroyNoContentKeepsSibling" status: pass - kind: unit ref: "plugins/golem15/fonoteka#TestOAuthTokenSurfaceIsolationCoverage/test_oauth_identity_routes_exist_on_jwt_surface_only" status: pass human_judgment: false - id: D4 description: "Unrestricted /me emits collection_ids JSON null; three manifest routes are ported at 175/175/0" requirement: API-09 verification: - kind: e2e ref: "parity#TestParityCorpus recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0" status: pass human_judgment: false - id: D5 description: "sm-user-plugin README names OAuthIdentitiesIndex/Destroy/Options as host-mounted constructors without a consuming-application name" verification: - kind: other ref: "grep OAuthIdentitiesIndex plugins/golem15/user/README.md" status: pass human_judgment: false duration: 90min completed: 2026-10-05 status: complete --- # Phase 14.1: OAuth identities and fonoteka me routes — Plan 01 Summary **Shared-plugin OAuth identities table and host-mounted GET/DELETE, `/me` collection_ids JSON null, and the three orphan routes ported at 175/175/0** ## Performance - **Duration:** 90 min - **Started:** 2026-10-05T19:04:22Z - **Completed:** 2026-10-05T19:24:44Z - **Tasks:** 4 - **Files modified:** 20 ## Accomplishments - `golem15_user_oauth_identities` ships in sm-user-plugin with the full PHP column set, gormigrate ID `202610050001_create_oauth_identities`, `lagoon.Encrypted` tokens, and Hidden `profile_data`. - Host JWT group mounts `OAuthIdentitiesIndex` and `OAuthIdentitiesDestroy` (Winter HTML 404, DELETE `throttle:10,1`, unconstrained `{provider}`). User plugin `routes.go` is unchanged. - Unrestricted `/me` emits `"collection_ids": null`; restricted `/me` still lists the pinned collection as `{{id:collection}}`. - Extras `oauth-identities-linked` and `me-unrestricted` recorded from isolated PHP; corpus `recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0`. - README documents the exported constructors as host-mounted; it never names a consuming application. ## Task Commits Each task was committed atomically: 1. **Task 1: Table ownership checkpoint** — no code commit (decision `ship-shared-full` answered before dispatch) 2. **Task 2: Tracer — table, Index, empty GET** — `63ac504` (sm-user-plugin feat), `1bed435` (fonoteka.go feat) 3. **Task 3: Destroy, 409 lang, JWT DELETE throttle** — `8e172c5` (sm-user-plugin feat), `696a35e` (fonoteka.go feat) 4. **Task 4: `/me` null, D-10 extras, ported 175, README** — `ac8aa16` (sm-user-plugin docs), `1584d73` (fonoteka.go feat) **Plan metadata:** this SUMMARY commit _Note: TDD was not applicable for this plan._ ## Files Created/Modified - `plugins/golem15/user/models/oauth_identity.go` — `OAuthIdentity`, `TableName`, Hidden secrets, empty Fillable - `plugins/golem15/user/updates/202610050001_create_oauth_identities.go` — gormigrate up/down - `plugins/golem15/user/controllers/oauth_identities.go` — Index/Destroy/Options - `plugins/golem15/user/oauth_identities_test.go` — empty list and 204 sibling smokes - `plugins/golem15/user/lang/{en,pl}/lang.yaml` — `oauth.last_method_blocked` - `plugins/golem15/user/README.md` — table, constructors, migration, model - `plugins/golem15/fonoteka/routes.go` — JWT GET/DELETE mounts - `plugins/golem15/fonoteka/phase08_coverage_test.go` — jwt-only surfaces + DELETE throttle - `plugins/golem15/fonoteka/controllers/api/me_token_controller.go` — D-09 null `collection_ids` - `parity/manifest.yaml`, `parity_test.go`, `parity_contract_test.go`, seed/reset PHP+Go, recorded fixtures ## Decisions Made - **ship-shared-full** (Task 1, pre-dispatch): the table lives in sm-user-plugin with the full PHP column set. Locked D-01 and D-07. - Identity HTTP is host-mounted; sm-user-plugin `/_user` routes stay unchanged. - Last-method 409 uses identity count only (D-06). ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] Isolated PHP has no `user_api_tokens` table** - **Found during:** Task 4 (PHP recording) - **Issue:** `fonoteka_reset.php` wiped `user_api_tokens`; this Winter checkout still stores tokens in `golem15_fonoteka_api_tokens`, so tinker aborted before vars were written. - **Fix:** Guard both token tables and `golem15_user_oauth_identities` with `Schema::hasTable` and wipe the fonoteka token table the PHP mint actually uses. - **Files modified:** `../fonoteka.go/parity/fonoteka_reset.php` - **Verification:** tinker `reset ok`; identities and unrestricted token rows present - **Committed in:** `1584d73` (Task 4) **2. [Rule 3 - Blocking] Restricted `/me` fixture used Phase 2 `{{id:token}}`** - **Found during:** Task 4 (fonoteka seed_hook) - **Issue:** The existing `/me` case was recorded from the genres bootstrap (`id:token` = rock). Under `seed_hook: fonoteka` that placeholder does not resolve. - **Fix:** Re-recorded the case from a plain `PARITY_CASE=` tinker reset. Body is `collection_ids:[{{id:collection}}]`, `name` `parity-mcp`. - **Files modified:** `../fonoteka.go/parity/fixtures/routes/GET__api_v1_fonoteka_me_personal_token.yaml` - **Verification:** `TestParityCorpus` `recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0` - **Committed in:** `1584d73` (Task 4) --- **Total deviations:** 2 auto-fixed (2 blocking) **Impact on plan:** Both needed so PHP recording and Go replay share one seed. No scope creep. ## Issues Encountered - First tinker died with `no such table: user_api_tokens` (deviation 1). Isolated PHP serve on 127.0.0.1:8423 stayed up; recording resumed after the guarded wipe. - Tracer verified end-to-end — expanding (Task 2 automated verify; no human-verify checkpoint, HUMAN_VERIFY_MODE=end-of-phase). ## User Setup Required None - no external service configuration required. ## Next Phase Readiness - Plan 14.1-02 can port `OAuthIdentityApiTest.php` (EN/PL 409, Winter 404s, 401 on `/google`, secret-leak, migration up/down) against this code. - API-09 stays Pending until 14.1-02 SUMMARY exists (`requirements.ready-ids` shared-ID gate). - Do not push sm-user-plugin or fonoteka.go (plan does not ask). Plugin HEAD `ac8aa16` is local-only. --- *Phase: 14.1-oauth-identities-and-fonoteka-me-routes* *Completed: 2026-10-05*