From 9ca67b780bda79f3d5b1205a62929595b86e3511 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sat, 17 Oct 2026 21:54:37 +0200 Subject: [PATCH] docs(19-04): complete ChatChannels API and users.handle plan Co-authored-by: Cursor --- .../19-chat-and-chatchannels/19-04-SUMMARY.md | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 .planning/phases/19-chat-and-chatchannels/19-04-SUMMARY.md diff --git a/.planning/phases/19-chat-and-chatchannels/19-04-SUMMARY.md b/.planning/phases/19-chat-and-chatchannels/19-04-SUMMARY.md new file mode 100644 index 00000000..8499be62 --- /dev/null +++ b/.planning/phases/19-chat-and-chatchannels/19-04-SUMMARY.md @@ -0,0 +1,260 @@ +--- +phase: 19-chat-and-chatchannels +plan: 04 +subsystem: chatchannels +tags: [chatchannels, handle, lighthouse, jwt, postgres, gorm, testcontainers] + +# Dependency graph +requires: + - phase: 19-03 + provides: ChannelService PHP public method set, MentionParser, AccrueForMessage, SeverUserFromContext, acme.teams proof host + - phase: 19-02 + provides: /_chat/api/v1 analog (APIPrefix, jwt.auth, named buckets, IDOR 404) +provides: + - "users.handle unique nullable VARCHAR(255) IF NOT EXISTS plus HandleGenerator in sm-user-plugin" + - "user.Created / user.Deleting festival events; user:{id} lighthouse authorizer skip-if-registered" + - "Published /_chatchannels/api/v1 with jwt.auth and chatchannels-api 120/min" + - "Participant-only @handle mentions excluding sender; GDPR SeverUserFromContext on user.Deleting" +affects: [19-05, Wavepath port, sm-user-plugin, sm-chatchannels-plugin] + +# Actuals (#2632) — chars/4 over the realized plugin diffs +actuals: + tokens: 25077 + tasks: 3 + commits: 4 +plan_head_before: c7018b4e27ba0093a8b8c52aba7374eaa43caea0 +plan_head_after: pending-summary-commit + +tech-stack: + added: [] + patterns: + - "users.handle is additive IF NOT EXISTS on the shared users table; rollback no-op; omitted from public JSON" + - "lighthouse.From then Registry.Register(\"user\") if free; ChannelID on the two-segment name" + - "jwt.auth before throttle so chatchannels-api keys by frontend user id else IP" + - "ChannelService.WithDB binds Accrue/Sever to the parent lagoon.Transaction" + +key-files: + created: + - ../fonoteka.go/plugins/golem15/user/updates/202610160002_add_handle_to_users.go + - ../fonoteka.go/plugins/golem15/user/classes/handle_generator.go + - ../fonoteka.go/plugins/golem15/user/classes/authorizer.go + - ../fonoteka.go/plugins/golem15/user/console/backfill_handles.go + - ../sm-chatchannels-plugin/routes.go + - ../sm-chatchannels-plugin/api/json.go + - ../sm-chatchannels-plugin/api/errors.go + - ../sm-chatchannels-plugin/api/auth.go + - ../sm-chatchannels-plugin/api/channels.go + - ../sm-chatchannels-plugin/api/categories.go + - ../sm-chatchannels-plugin/api/prefs.go + - ../sm-chatchannels-plugin/api/members.go + - ../sm-chatchannels-plugin/api/pins.go + - ../sm-chatchannels-plugin/api/mentions.go + - ../sm-chatchannels-plugin/api/search.go + modified: + - ../fonoteka.go/plugins/golem15/user/models/user.go + - ../fonoteka.go/plugins/golem15/user/classes/events.go + - ../fonoteka.go/plugins/golem15/user/plugin.go + - ../fonoteka.go/plugins/golem15/user/controllers/registration.go + - ../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go + - ../fonoteka.go/plugins/golem15/user/README.md + - ../sm-chatchannels-plugin/boot.go + - ../sm-chatchannels-plugin/plugin.go + - ../sm-chatchannels-plugin/classes/channel_service.go + - ../sm-chatchannels-plugin/classes/mention_parser.go + - ../sm-chatchannels-plugin/http_tracer_test.go + - ../sm-chatchannels-plugin/README.md + +key-decisions: + - "Task 1 reply verbatim: proceed" + - "Handle charset is a subset of MentionParser [A-Za-z0-9._-]; generated handles are 2–255" + - "jwt.auth runs before throttle so chatchannels-api keys by frontend user id else IP" + - "Did not bump fonoteka.go submodule pointer" + - "sm-chatchannels-plugin main pushed; sm-user-plugin left local (non-fast-forward vs origin 24b16b1)" + +patterns-established: + - "Generic channel routes live under /_chatchannels/api/v1 with jwt.auth; Wavepath aliases the same door as /_chat" + - "user.Deleting is the GDPR seam; chatchannels never GORM-hooks User" + +requirements-completed: [SC-19-1, D-06, D-07, D-08, D-09, D-13, D-15, D-17] + +coverage: + - id: D1 + description: "Task 1 recorded proceed; users.handle unique nullable VARCHAR(255) IF NOT EXISTS with HandleGenerator" + requirement: D-15 + verification: + - kind: unit + ref: "go -C ../fonoteka.go/plugins/golem15/user test -count=1 -short ./...#TestHandle" + status: pass + - kind: other + ref: "grep column:handle and ADD COLUMN IF NOT EXISTS handle" + status: pass + human_judgment: false + - id: D2 + description: "@handle among participants accrues a mention; non-participant and sender do not (T-19-14)" + requirement: D-15 + verification: + - kind: integration + ref: "go -C ../sm-chatchannels-plugin test -count=1 -run '^TestHandleMention$' -v .#TestHandleMention" + status: pass + human_judgment: false + - id: D3 + description: "Published /_chatchannels/api/v1 with jwt.auth and chatchannels-api 120/min; member POST+list, stranger 404" + requirement: D-06 + verification: + - kind: unit + ref: "go -C ../sm-chatchannels-plugin test -count=1 -short ./...#TestChannelHTTPAuth" + status: pass + - kind: integration + ref: "go -C ../sm-chatchannels-plugin test -count=1 -run '^TestChannelHTTPTracer$' -v .#TestChannelHTTPTracer" + status: pass + - kind: other + ref: "grep const APIPrefix = \"/_chatchannels/api/v1\"" + status: pass + human_judgment: false + - id: D4 + description: "lighthouse user:{id} allowed only for that user; skip-if-registered (T-19-10)" + requirement: D-17 + verification: + - kind: unit + ref: "go -C ../fonoteka.go/plugins/golem15/user test -count=1 -short ./...#TestUserAuthorizer" + status: pass + - kind: other + ref: "grep Register(\"user\" in sm-user-plugin classes" + status: pass + human_judgment: false + - id: D5 + description: "user.Deleting fires before unscoped delete; chatchannels SeverUserFromContext (T-19-07)" + requirement: D-13 + verification: + - kind: integration + ref: "go -C ../sm-chatchannels-plugin test -count=1 -run '^TestDeletingCascade$' -v .#TestDeletingCascade" + status: pass + - kind: other + ref: "grep DeletingEventName and SeverUserFromContext in boot.go" + status: pass + human_judgment: false + +duration: 15min +completed: 2026-10-17 +status: complete +--- + +# Phase 19 Plan 04: ChatChannels API and users.handle Summary + +**users.handle plus user:{id} lighthouse authorizer and user.Created/user.Deleting, proven by @handle mentions, then `/_chatchannels/api/v1` behind jwt.auth** + +## Performance + +- **Duration:** 15 min +- **Started:** 2026-10-17T19:39:01Z +- **Completed:** 2026-10-17T19:53:45Z +- **Tasks:** 3 +- **Files modified:** 34 (sm-user-plugin 17, sm-chatchannels-plugin 17) + +## Accomplishments + +- Task 1 reply recorded verbatim: **proceed** +- A newly created user gets a unique handle; `@that-handle` in a channel message accrues a mention only for a participant other than the sender (`TestHandleMention` PASS). +- `/_chatchannels/api/v1` is mounted with `jwt.auth` and `chatchannels-api` 120/min; a member JWT creates and lists a channel, a stranger JWT is 404 (`TestChannelHTTPTracer` PASS). +- `user:{id}` is authorized in sm-user-plugin; deleting a user severs channel prefs/members/personal categories (`TestDeletingCascade` PASS). + +## Task Commits + +Production commits live in sm-user-plugin and sm-chatchannels-plugin (this repo had no production diff): + +1. **Task 1: Confirm users.handle and /_chatchannels prefix** — checkpoint; human reply `proceed` (no code commit) +2. **Task 2: End-to-end @handle mention** — `15b19c7` (feat) in sm-user-plugin; `db014bb` (feat) in sm-chatchannels-plugin +3. **Task 3: /_chatchannels/api/v1, user authorizer, Deleting cascade, backfill** — `c077029` (feat) in sm-user-plugin; `4269059` (feat) in sm-chatchannels-plugin + +**Plan metadata:** pending this SUMMARY commit in summercms.go + +_Note: sm-chatchannels-plugin `main` was pushed to origin at `4269059` (includes Task 2 `db014bb`). sm-user-plugin push was rejected (non-fast-forward; origin/master has `24b16b1` not in this checkout). Did not force. Did not bump fonoteka.go submodule pointer. summercms.go was not pushed._ + +## Files Created/Modified + +- `../fonoteka.go/plugins/golem15/user/updates/202610160002_add_handle_to_users.go` — `ADD COLUMN IF NOT EXISTS handle VARCHAR(255) NULL`; unique index; rollback no-op +- `../fonoteka.go/plugins/golem15/user/classes/handle_generator.go` — charset subset of MentionParser, length 2–255 +- `../fonoteka.go/plugins/golem15/user/classes/authorizer.go` — `Register("user")` if free; allow only `user:{id}` +- `../fonoteka.go/plugins/golem15/user/classes/events.go` — `CreatedEventName` / `DeletingEventName` +- `../fonoteka.go/plugins/golem15/user/console/backfill_handles.go` — `user:backfill-handles` +- `../sm-chatchannels-plugin/routes.go` — `const APIPrefix = "/_chatchannels/api/v1"` +- `../sm-chatchannels-plugin/api/*.go` — handlers that call `ChannelService`; Lookup[*classes.ChannelService] +- `../sm-chatchannels-plugin/boot.go` — Listen `user.Deleting` then `SeverUserFromContext` +- `../sm-chatchannels-plugin/http_tracer_test.go` — TestHandleMention, TestChannelHTTPAuth, TestChannelHTTPTracer, TestDeletingCascade + +## Decisions Made + +- Task 1 reply verbatim: **proceed** +- `jwt.auth` wraps before throttle so the bucket key sees the frontend user (D-09 analog of 19-02) +- Handle is omitted from public user JSON (`json:"-"`) +- `RegisterUserAuthorizer` skips when `lighthouse.From` fails so Boot stays additive on hosts without realtime +- Did not bump the fonoteka.go submodule pointer + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Nested transaction needs the parent handle** +- **Found during:** Task 2 (`TestHandleMention`) +- **Issue:** `AccrueForMessage` / `RecordMentions` used `s.db` (root) while ctx held chat's `lagoon.Transaction` buffer: "nested transaction requires the parent transaction handle" +- **Fix:** `ChannelService.WithDB` shallow copy; Boot listeners pass `ev.Tx` +- **Files modified:** `../sm-chatchannels-plugin/classes/channel_service.go`, `../sm-chatchannels-plugin/boot.go` +- **Verification:** TestHandleMention PASS +- **Committed in:** `db014bb` + +**2. [Rule 2 - Missing Critical] jwt.auth before throttle** +- **Found during:** Task 3 +- **Issue:** plan text `Use("throttle:chatchannels-api", "jwt.auth")` keys every request by IP because the principal is not set yet +- **Fix:** `Use("jwt.auth", "throttle:"+APIBucket)` matching chat 19-02 +- **Files modified:** `../sm-chatchannels-plugin/routes.go` +- **Verification:** TestChannelHTTPTracer (authenticated member path) +- **Committed in:** `4269059` + +**3. [Rule 1 - Bug] User-plugin rollback history counts** +- **Found during:** Task 2 +- **Issue:** additive handle migration bumped gormigrate history by one +- **Fix:** organisations 10→11, user_session 9/8/7→10/9/8, user_groups 6→7 +- **Files modified:** `updates/organisations_test.go`, `updates/user_session_test.go`, `updates/user_groups_test.go` +- **Verification:** `go test -short ./...` in sm-user-plugin +- **Committed in:** `15b19c7` + +--- + +**Total deviations:** 3 auto-fixed (1 missing critical, 2 bug) +**Impact on plan:** Required for nested writes, D-09 user-keyed buckets, and the extra migration row. No scope creep. + +## Issues Encountered + +- sm-user-plugin `git push origin master` rejected: origin has `24b16b1` (`fix: open Postgres for one-shot console commands`) not in this checkout. Left local at `c077029`. Did not force, did not rebase, did not bump app submodule pointers. +- `TestHandleMention`, `TestChannelHTTPTracer` and `TestDeletingCascade` need Postgres (skip under `-short` via `freshDB`). `TestHandle`, `TestUserAuthorizer` and `TestChannelHTTPAuth` run under `-short`. + +## Authentication Gates + +None. + +## User Setup Required + +None - no external service configuration required. Applications that migrate sm-user-plugin gain `users.handle`; existing rows can run `user:backfill-handles`. + +## Next Phase Readiness + +Ready for 19-05 (unit tests LAST). The Wavepath port can thin-alias `/_chatchannels/api/v1` the same way it aliases `/_chat`. Mentions.changed already Emits to `user:{id}` from ChannelService. + +**TestHandleMention:** PASS (Docker, 0.86s) +**TestChannelHTTPTracer:** PASS (Docker, 0.88s) +**TestDeletingCascade:** PASS (Docker, 0.67s) +**sm-chatchannels-plugin pushed:** yes, `origin/main` at `4269059` (never force) +**sm-user-plugin pushed:** no (non-fast-forward vs `24b16b1`) + +## Self-Check: PASSED + +- Task 1 reply recorded verbatim as `proceed` +- `const APIPrefix = "/_chatchannels/api/v1"` in routes.go +- `Register("user"` in classes/authorizer.go +- `DeletingEventName` / `user.Deleting` in classes/events.go +- `SeverUserFromContext` in boot.go +- plugin commits present; fonoteka.go submodule pointer not bumped + +--- +*Phase: 19-chat-and-chatchannels* +*Completed: 2026-10-17*