docs(19-04): complete ChatChannels API and users.handle plan
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
260
.planning/phases/19-chat-and-chatchannels/19-04-SUMMARY.md
Normal file
260
.planning/phases/19-chat-and-chatchannels/19-04-SUMMARY.md
Normal file
@@ -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*
|
||||
Reference in New Issue
Block a user