docs(19.1): research phase domain
This commit is contained in:
915
.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md
Normal file
915
.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md
Normal file
@@ -0,0 +1,915 @@
|
||||
# Phase 19.1: Chat satellites - Research
|
||||
|
||||
**Researched:** 2026-10-17
|
||||
**Domain:** PHP-to-Go port of five shared `sm-*-plugin` satellites (chatbots, chatvideo, forum, herald, userfriends) on lighthouse / lagoon / festival / conga / fetchguard, plus three locked third-party libraries (webpush-go, livekit/server-sdk-go, bluemonday)
|
||||
**Confidence:** HIGH for frozen PHP contracts, analog plugin layout, live-PHP schema (migrations Read this session), and framework seams; MEDIUM for LiveKit JWT claim byte-identity vs `protocol/auth` (golden test is the gate); LOW for live Wavepath MySQL column types (this session: `ERROR 1698 Access denied for user 'root'@'localhost'`)
|
||||
|
||||
<user_constraints>
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
**CRITICAL:** Locked decisions from `/msd:discuss-phase` are NON-NEGOTIABLE. Copied verbatim.
|
||||
|
||||
### Locked Decisions
|
||||
|
||||
### Carried forward from Phase 19 (not re-decided)
|
||||
- **D-01:** **The frozen port contract is Wavepath's tree.**
|
||||
- The commits are chatbots `1ecf8c8`, chatvideo `25135b4`, forum `ad10b54`, herald `a749f83` and userfriends `182b514`, read from `/media/nvme/dev/jz/wavepath.org/plugins/golem15/`.
|
||||
- The P15/P18/P19 frozen-source rule applies. These names and shapes are binding: tables and columns, migration-equivalent schema, realtime event names, channels and payloads, serializer output, console command names, config keys, env names and lang keys.
|
||||
- **Reversibility:** costly. The names are copied into the database and clients of every app.
|
||||
- **D-02:** **One squashed, idempotent baseline per plugin,** equal to the final PHP schema, that adopts an existing Wavepath database unchanged (Phase 5, P18.1 D-04, P19 D-04). The researcher diffs each baseline against a real Wavepath database.
|
||||
- The demo-seed migrations (chatvideo `seed_demo_users`, userfriends `seed_demo_friends`) are not ported as behavior (D-07). Their version IDs stay recorded so that adopted databases stay consistent, and fresh databases get no demo users.
|
||||
- **Reversibility:** one-way. Migration IDs and table shapes become the shared contract.
|
||||
- **D-03:** **The P19 seam rules apply to every satellite.**
|
||||
- Contexts register through the build-time morph registry (P19 D-11). Stored PHP class names in morph columns are rewritten to aliases on adoption (P19 D-12). That covers `golem15_forum_categories.context_type` and any satellite morph column the researcher finds.
|
||||
- Cross-plugin hooks are typed `festival` events, and no plugin patches another's model (P19 D-13). The GDPR user-delete cascades listen to `sm-user-plugin` events:
|
||||
- chatbots deletes the `bot_flags` row;
|
||||
- forum sets `user_id` to null on threads and posts and keeps the bodies;
|
||||
- userfriends hands group ownership to the oldest admin, else the oldest member, else hard-deletes the empty group, then removes the user's member and ban rows.
|
||||
- Every access seam is deny-all by default and has a setter, and Boot logs a Warn while the default is active (P19 D-14). That covers `ForumAccessResolver`, `BotAuthResolver` and herald's `PresenceGate`.
|
||||
- Realtime goes through `lighthouse`, and user-targeted pushes go to the `user` namespace that `sm-user-plugin` registers (P19 D-16, D-17).
|
||||
- Nothing names QuestStream, family, parent or child (P19 D-02), and READMEs never name a consuming app.
|
||||
- **D-04:** **A test-only host per plugin repo** (P18 D-03, P19 D-21): testcontainers Postgres, the lighthouse memory driver, fakes for external services (a LiveKit Twirp fake and a push-service fake), neutral `acme` fixture contexts, and `sm-user-plugin`, plus `sm-chat-plugin` where the plugin requires it. Every PHP test method maps to a Go case or to an explicit "not applicable" note. Golden payloads and events are captured from Wavepath. The mapping is completed in the last plan.
|
||||
|
||||
### HTTP surface
|
||||
- **D-05:** **Every satellite ships a typed Go service API plus a generic headless JSON API under its own prefix,** following P19 D-06 to D-09:
|
||||
- `/_forum/api/v1` mirrors `/_wave/api/v1/community/forums/{groupId}/...` (categories with create, rename, reorder and delete; threads, posts, edit and delete, read marks, unread count), re-rooted on context alias and id: `/_forum/api/v1/contexts/{type}/{id}/categories`. Access is decided by `ForumAccessResolver`.
|
||||
- `/_chatvideo/api/v1` mirrors `/_wave/api/v1/community/groups/{id}/video` (start, join, leave, end, status) and `community/call-links/{code}`, addressed by conversation, or by context through chat's context registry.
|
||||
- `/_userfriends/api/v1` mirrors `/_wave/api/v1/community/friends*` (list, requests, sent, request, accept, reject, cancel, unfriend) and the group routes (`community/groups*`: groups, members, invites, bans).
|
||||
- `/_herald/api/v1/push/subscription` with POST, PATCH and DELETE, plus `GET /_herald/api/v1/push/vapid-public-key`, mirroring `/_wave/api/v1/push/*`.
|
||||
- `/_chatbots/api/v1` covers the command catalog, filtered by `isAvailable`, and command dispatch, mirroring Wavepath's `CommunityCommandController`. The invoker is always the authenticated actor (PHP CMD-06).
|
||||
- Auth is `jwt.auth`, mounted with the `lighthouse.Mount` surfaces pattern so the app picks the guard. PHP's named rate buckets carry over: `video-initiate` at 5 per minute per user, the friend-request bucket (`community-join`), and a general bucket per API. The actor id always comes from the token, never from the request body.
|
||||
- One serializer set serves both the generic API and the future `/_wave` aliases, so payloads are identical.
|
||||
- **Reversibility:** one-way. These are published APIs that apps will consume.
|
||||
- **D-06:** **This reverses the PHP "herald ships no routes" rule** (herald README, D-78-04) for consistency with the other plugins. The herald README's reuse contract is rewritten around the generic API and the Go seams.
|
||||
- **D-07:** **Dropped, as in P19 D-10:**
|
||||
- the legacy plugin-owned routes `/api/video/*` and `/api/friends/*`, which no client calls;
|
||||
- the WinterCMS components `VideoCall`, `FriendsList` and `FriendRequests` and their templates;
|
||||
- chatvideo's bundled `livekit-client.umd.min.js`, `video-chat.js`, `video-vue.js` and CSS;
|
||||
- the demo seeders (D-02).
|
||||
- **D-08:** **Satellites couple through generic context events, never through each other's types.**
|
||||
- userfriends fires typed festival events for group created, group deleted, member joined and member left.
|
||||
- forum exposes a generic `DeleteContext(type, id)`, a soft-delete cascade over categories, threads and posts. It does not import userfriends.
|
||||
- The app, or a small optional bridge in the app, wires group-deleted to `forum.DeleteContext`. This replaces PHP forum's `class_exists(FriendGroup)` hook.
|
||||
- The other group hooks in `wavepath/Plugin.php` (spawning a chat or channel on group create, ending video on group delete) stay app wiring for the Wavepath port.
|
||||
- userfriends registers its chat context adapter (the `FriendContext` provider) softly, only when chat is registered, through an optional-capability check rather than a hard requirement. This keeps PHP's "chat is the lower layer" rule.
|
||||
|
||||
### Herald (Web Push)
|
||||
- **D-09:** **Web Push uses `github.com/SherClockHolmes/webpush-go`,** which covers VAPID (RFC 8292) and aes128gcm (RFC 8291).
|
||||
- The decision note required by the roadmap goes in `.planning/notes/` before any herald code lands. It records the version, maintenance state, transitive dependencies (it should already share `golang-jwt/jwt/v5`), and how its HTTP client is swapped for fetchguard's (D-10).
|
||||
- The researcher verifies that the library's `Options.HTTPClient` accepts a `fetchguard.Client`-backed doer. If it doesn't, the note records the adapter.
|
||||
- **Reversibility:** reversible. It sits behind the plugin's own sender.
|
||||
- **D-10:** **Every push send goes through a `fetchguard` PublicOnly client** (HTTPS only, private and reserved IPs refused at dial time).
|
||||
- There is no `EndpointGuard` seam. Dial-time checks close the subscribe-then-send DNS-rebind gap that PHP's IP pinning (`CURLOPT_RESOLVE`) and the host-bound `SsrfGuard` existed for.
|
||||
- Subscribe time also validates the endpoint URL (HTTPS, public host).
|
||||
- This is fail-closed by default, where PHP's `AllowAllEndpointGuard` was fail-open.
|
||||
- **D-11:** **Sends run as a `conga` job, one per send.**
|
||||
- `SendEvent` and `SendUnconditional` enqueue a River job inside the caller's transaction, so a rolled-back write pushes nothing.
|
||||
- The job runs `PresenceGate.ShouldSend` (for `SendEvent` only), builds the payload with `HeraldProfile`, sends to every subscription the user has, and prunes 404 and 410 endpoints.
|
||||
- River provides retries and backoff. PHP's rate-limited OBS-01 warning for other delivery failures carries over.
|
||||
- The PHP defaults carry over: TTL 2419200 (4 weeks) and urgency `normal`, both overridable per send.
|
||||
- **D-12:** **VAPID keys are config keys** `golem15.herald.vapid.{subject,public_key,private_key}` with the default subject `mailto:admin@example.com`.
|
||||
- The PHP env names `VAPID_SUBJECT`, `VAPID_PUBLIC_KEY` and `VAPID_PRIVATE_KEY` are mapped so an adopted `.env` works unchanged.
|
||||
- `summer herald:vapid-generate` (the PHP `herald:vapid-generate`) prints a new keypair. The private key is never logged and never appears in a payload.
|
||||
- Boot warns when the keys are missing. Keys are never generated automatically, because rotating them invalidates every subscription.
|
||||
|
||||
### ChatVideo (LiveKit)
|
||||
- **D-13:** **chatvideo uses the official `github.com/livekit/server-sdk-go`** for access tokens and RoomService (create, delete, remove participant, participant count, list rooms).
|
||||
- A decision note in `.planning/notes/`, written before the code lands, records the version, the transitive dependency footprint (`livekit/protocol`, twirp, protobuf; flag anything heavy such as pion), and how tests fake it.
|
||||
- The token claims must match PHP exactly, verified by a golden test: HS256, `iss` set to the API key, `sub` set to the user id as a string, `name`, `iat`/`nbf`/`exp` with `token_ttl` defaulting to 900, a `video` grant with `room`, `roomJoin`, `canPublish` and `canSubscribe`, and an optional JSON `metadata` string carrying the avatar.
|
||||
- The admin URL keeps PHP's fallback: `livekit_admin_url`, else `livekit_url` with `wss://` or `ws://` rewritten to `https://` or `http://`. An error for a missing room is swallowed as in PHP (`participantCount` returns 0, `deleteRoom` is a no-op).
|
||||
- Config keys and env names carry over: `livekit_url` and `LIVEKIT_URL`, the API key, the secret, `max_duration` (3600), `token_ttl` (900), `livekit_admin_url`, `max_participants` (8) and `screen_share`.
|
||||
- **Reversibility:** reversible. It sits behind the plugin's LiveKit service.
|
||||
- **D-14:** **The stale-session reconciler moves from the Wavepath app into chatvideo.**
|
||||
- A generic reconcile service ends sessions whose LiveKit room is gone or empty, or that have run past `max_duration`. It is exposed as `summer chatvideo:reconcile-sessions` and as a `conga` periodic job (every 5 minutes, as Wavepath schedules it).
|
||||
- The Wavepath port's `wave:reconcile-video-sessions` becomes a thin alias. The researcher reads `wavepath/console/ReconcileVideoSessions.php` and keeps only its generic part.
|
||||
- **D-15:** **Realtime events are the PHP events, published through lighthouse.**
|
||||
- `video:call:incoming` goes to recipients' `user:{id}` channels.
|
||||
- `video:call:accepted`, `declined`, `ended`, `video:participant:joined` and `left` go to `video:session:{id}`.
|
||||
- chatvideo registers the `video` namespace authorizer. It allows `video:session:{id}` only to participants of the session's conversation and grants capability `["prs"]`. It replaces `VideoChannelAuthorizer`.
|
||||
- A publish failure is logged and not raised, as in PHP.
|
||||
- The one-active-session-per-conversation unique index and the `card_message_id` and `join_code` columns carry over.
|
||||
|
||||
### Forum (Markdown)
|
||||
- **D-16:** **Sanitizing uses `github.com/microcosm-cc/bluemonday`,** recorded in a decision note.
|
||||
- The policy is the PHP HTMLPurifier config, 1:1:
|
||||
- Allowed tags: `p, br, strong, em, b, i, h1-h6, ul, ol, li, blockquote, pre, code, a[href|rel|target], table, thead, tbody, tr, th, td, hr, del, ins, sub, sup`.
|
||||
- URL schemes: `http`, `https` and `mailto`.
|
||||
- Links get `target=_blank` and `rel=nofollow`.
|
||||
- Conversion uses `goldmark`, already a dependency, with GFM. That covers tables, strikethrough and autolinks (the external-link behavior), with raw HTML input stripped (PHP `html_input=strip`) and unsafe links disallowed.
|
||||
- **D-17:** **The renderer is a framework module, not forum code.**
|
||||
- It is a small shared package in `summercms.go` (or an extension of cabana's markdown support; the quick task decides) that exports `Render(markdown)` and `Purify(html)` with a named policy.
|
||||
- The user's direction: "Framework module, done with gsd-quick by summer agent. We'll need that in multiple places." It is delivered as a separate `/msd-quick` task in `summercms.go` by the summer agent. It is not a plan of this phase. The forum plans consume it, and the Wavepath port's `purify()` callers (trainings) use it too.
|
||||
- If the quick task hasn't landed when forum execution starts, the forum plan blocks on it. The forum plan does not grow its own renderer.
|
||||
- The module's README and `docs/` page follow the Documentation rules in CLAUDE.md.
|
||||
- **D-18:** **`content_html` parity is semantic, and stored rows are kept.**
|
||||
- Adopted `content_html` rows are served as stored and never re-rendered on read. New and edited posts render in Go, on write, as in PHP.
|
||||
- Golden tests compare a normalized DOM (the same tags, attributes and text) against PHP output for a corpus that covers the PHP `ForumMarkdownRenderTest` XSS cases (`<script>`, `javascript:`, `<iframe>`, `onerror`), GFM tables, code blocks, links and autolinks.
|
||||
- Byte-identical HTML is not required.
|
||||
|
||||
### Claude's Discretion
|
||||
- Go package, type and identifier names for the services, seams, events and jobs. Exact generic API paths within D-05.
|
||||
- How chatbots resolves bot bearer tokens through `BotAuthResolver` (deny-all default) on the generic API, and where `is_bot` and the bot flag show up in serializers. Follow Wavepath's `BotRestReachTest` and `WaveBotAuthResolverTest` for the expected behavior.
|
||||
- How chatvideo addresses a call in the generic API (by conversation id, or by context alias through chat's registry), as long as Wavepath's group-video payloads are reproducible.
|
||||
- Queue names and job shapes on `conga` (herald send, video reconcile).
|
||||
- Lang strings port to phrasebook in EN and PL. Plugin display names are neutral.
|
||||
- Whether the D-12-style alias rewrite for satellite morph columns extends chat's adoption command or each plugin ships its own.
|
||||
- Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan". Expect userfriends and forum early (no external services), then chatbots, then herald and chatvideo (new dependencies and their decision notes).
|
||||
- Run the security-review agent. The phase adds public authenticated APIs, an outbound push sender, a token issuer for a media server, an HTML sanitizer boundary and GDPR delete cascades.
|
||||
|
||||
### Deferred Ideas (OUT OF SCOPE)
|
||||
- The framework Markdown/sanitizer module (D-17): delivered by a separate `/msd-quick` task in `summercms.go`, not by this phase's plans.
|
||||
- A `vue-community-layer` or similar frontend components for friends, forum, video and push: once a client other than Wavepath wants them.
|
||||
- Wavepath's `/_wave` routes and host seam implementations: with the Wavepath app port.
|
||||
|
||||
### Reviewed Todos (not folded)
|
||||
- The todo matcher returned only keyword noise (translate editor, cabana richeditor, faq port, mailblocker). None concern the satellites, so none were folded.
|
||||
</user_constraints>
|
||||
|
||||
## Project Constraints (from CLAUDE.md)
|
||||
|
||||
Treat these with the same authority as locked CONTEXT decisions.
|
||||
|
||||
- One Go module for the framework (`git.golem15.com/golem15/summercms`); plugins live in their own `sm-*-plugin` repos (`git.golem15.com/golem15/sm-<name>-plugin`), plugin ID and package keep the plain names (`golem15.chatbots` / package `chatbots`).
|
||||
- Compiled plugins only. A plugin implements `pact` capability interfaces; `party` runs `Register` then `Boot`; `backpack.App` is the container. No runtime plugin loading.
|
||||
- Request path: `surf` + `bouncer` + `towel` + `wire`. Response shapes are not "improved" during a port; the existing frontend's requests define the contract.
|
||||
- Data: GORM on Postgres only; per-plugin gormigrate; YAML via `goccy/go-yaml` never `gopkg.in/yaml.v3`.
|
||||
- Standard library first. A new dependency needs a research doc or a phase/decision note (`.planning/research/STACK.md`). CONTEXT already locked: webpush-go (D-09), livekit/server-sdk-go (D-13), bluemonday (D-16). Goldmark is already in `go.mod`.
|
||||
- Framework code and plugin READMEs never name a consuming application (use `blog` / `acme`). Wavepath may be named in this RESEARCH.md as the frozen PHP source.
|
||||
- Module README structure and identifier-checked docs; API/config/CLI changes update README + `docs/` in the same commit.
|
||||
- Lean planning: few large plans; present suggested plan count and wait for confirmation before writing PLAN.md files.
|
||||
- Unit tests are the last plan of every phase.
|
||||
- Update `.planning/STATE.md` only via `msd_run query state.*` handlers.
|
||||
- No co-author tags. One logical change per commit; planning docs and code in separate commits.
|
||||
- Stage files by path (never `git add -A`); leave other sessions' uncommitted edits alone.
|
||||
- Don't push, tag or deploy without the user's go-ahead.
|
||||
- `go vet` and `go test ./...` green at every commit. Docker-backed tests skip under `-short`. `TestMain` that starts containers must call `flag.Parse()` and check `testing.Short()`.
|
||||
- Plugin layout: WinterCMS directories as Go packages; `models/` is a leaf (`.planning/notes/plugin-layout-winter-directories.md`).
|
||||
- Core plugin contracts stay non-breaking unless the user asks (`.planning/notes/core-plugins-own-repos.md`).
|
||||
- Commit and push the plugin repo first, then the pointer bump in an app as a separate commit.
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 19.1 ports five Wavepath-frozen PHP plugins into five new `sm-*-plugin` repos, following the Phase 19 analog (`sm-chat-plugin` / `sm-chatchannels-plugin`): Winter directories, one squashed `CREATE TABLE IF NOT EXISTS` baseline per plugin, generic `/_x/api/v1`, deny-all resolvers with a Boot Warn, a test-only `acme` host, lighthouse `Bind`/`Emit`, and festival user-lifecycle listeners. Frozen HEADs this session: chatbots `1ecf8c8bb6b81102508c0b8e3a999aaacf9d4963`, chatvideo `25135b46c1571a6bc722fdc094157176d70ad717`, forum `ad10b54eb7be8e11de8e55dd85cfa0c92bbb15c9`, herald `a749f83912a7c88994eb5341740a60b3488463f9`, userfriends `182b5145561f0609ab54f59e24258e12a96332b1`.
|
||||
|
||||
The planner must treat this as five tracer-first vertical slices (userfriends → forum → chatbots → herald → chatvideo) plus a last-plan PHP-method map. Do not port Wavepath `/_wave` controllers, guest-call, CallNotifier, flipCallCardToEnded, or a forum-owned markdown renderer. D-17 has **not** landed: there is no dedicated markdown module; `seaglass` is the richeditor UGC policy (nofollow off, `tel:` allowed); `cabana.RenderMarkdown` is an admin regex gate without GFM. Forum execution **blocks** until the quick task exports `Render(markdown)` and `Purify(html)` with a named forum policy. ROADMAP SC2 still says "Chatbots is wired to `sm-golem-plugin`" — drop it in the first planning commit; PHP `$require` is exactly `Golem15.Chat` and `Golem15.User`. SC3 is already met (fonoteka PHP tree is byte-identical to Wavepath at 32 files, `Plugin.php` cmp equal, commit `182b514`).
|
||||
|
||||
**Primary recommendation:** Create the five repos on the P19 layout. Write three decision notes **before** herald/chatvideo/forum-sanitizer code: webpush-go v1.4.0 (`Options.HTTPClient` is the `Do` interface — pass `*fetchguard.Client` directly), livekit **v2** module path `github.com/livekit/server-sdk-go/v2` v2.19.0 (flag pion / media-sdk / buildkit; wrap RoomService behind an interface; golden-test PHP JWT claims rather than trusting SDK grant JSON), bluemonday v1.0.27 as a **named forum policy** (not `seaglass`). Do not use `flare` for herald (D-09 locked webpush-go; different config keys and command name). Do not call `lighthouse.ChannelID` for `video:session:{id}` (segment 1 is `"session"` → 0).
|
||||
|
||||
## Architectural Responsibility Map
|
||||
|
||||
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||
|------------|-------------|----------------|-----------|
|
||||
| userfriends friendships / groups | API / Backend | Database / Storage | Typed service + `/_userfriends/api/v1`; GDPR on `user.Deleting` |
|
||||
| FriendContext chat adapter | API / Backend | — | Soft optional-capability registration with `sm-chat-plugin` (D-08) |
|
||||
| forum categories / threads / posts / reads | API / Backend | Database / Storage | `ForumAccessResolver` deny-all; `DeleteContext` generic cascade (D-08) |
|
||||
| Markdown render + HTML purify | API / Backend | — | D-17 framework module (not this phase); forum consumes it on write (D-18) |
|
||||
| chatbots slash-command registry / dispatch | API / Backend | Database / Storage | `bot_flags`; invoker is JWT actor (CMD-06); no Golem |
|
||||
| herald subscriptions + send | API / Backend | Database / Storage | conga job + fetchguard PublicOnly + PresenceGate deny-all |
|
||||
| Web Push network | API / Backend | — | webpush-go through fetchguard; prune 404/410 |
|
||||
| chatvideo sessions + LiveKit tokens | API / Backend | Database / Storage | RoomService + HS256 tokens; TrustedMode HTTP to operator LiveKit URL |
|
||||
| video realtime | API / Backend | — | lighthouse `video` namespace; `user:{id}` for incoming |
|
||||
| video reconciler | API / Backend | Database / Storage | conga periodic 5 min; LiveKit empty/gone or `max_duration` only (D-14) |
|
||||
| Vue friends/forum/video/push UI | Browser / Client | — | Deferred (`vue-community-layer`) |
|
||||
| Wavepath `/_wave` + host seams | API / Backend | — | Out of scope (Wavepath app port) |
|
||||
|
||||
<phase_requirements>
|
||||
## Phase Requirements
|
||||
|
||||
ROADMAP lists Requirements as TBD. Map the four success criteria and locked D-IDs so the planner can attach IDs later. Omit a REQUIREMENTS.md ID table.
|
||||
|
||||
| Success criterion (ROADMAP) | Locked support | Research support |
|
||||
|-----------------------------|----------------|------------------|
|
||||
| SC-19.1-1 Each plugin ports schema, models, routes and events 1:1 against its PHP original | D-01, D-02, D-05, D-15 | Frozen inventory below; generic `/_x/api/v1` not `/_wave` |
|
||||
| SC-19.1-2 Herald decision note names VAPID/Web Push dep before code. (ROADMAP also says "Chatbots is wired to sm-golem-plugin" — **drop** that clause) | D-09, D-13, D-16 | PHP chatbots `$require = ['Golem15.Chat', 'Golem15.User']` [VERIFIED: chatbots/Plugin.php:21]. Add LiveKit + bluemonday notes |
|
||||
| SC-19.1-3 Userfriends checked against fonoteka first | D-01 userfriends `182b514` | Already met: 32 files each, `Plugin.php` identical, `diff -rq` empty this session |
|
||||
| SC-19.1-4 Unit tests in the phase's last plan | D-04 | PHP method counts: chatbots 22, forum 25, herald 14, userfriends 31, chatvideo 0 in-plugin |
|
||||
|
||||
First planning commit also patches ROADMAP: Repos line adds `summercms.go` for D-17; SC2 drops Golem and names webpush-go / LiveKit / bluemonday notes; SC3 marked already met.
|
||||
</phase_requirements>
|
||||
|
||||
<standard_stack>
|
||||
## Standard Stack
|
||||
|
||||
### Core (new in this phase — each needs a `.planning/notes/` decision note before code)
|
||||
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| `github.com/SherClockHolmes/webpush-go` | v1.4.0 (2025-01-02, MIT, Go 1.13) | VAPID + aes128gcm send | D-09 locked. Official source [CITED: https://github.com/SherClockHolmes/webpush-go/blob/v1.4.0/webpush.go]. Transitive: `github.com/golang-jwt/jwt/v5`, `golang.org/x/crypto` |
|
||||
| `github.com/livekit/server-sdk-go/v2` | v2.19.0 (2026-10-08, Apache-2.0, Go 1.26.8) | RoomService + token helpers | D-13 locked official SDK. Current README install is **`/v2`** [CITED: https://github.com/livekit/server-sdk-go]. v1 path frozen at v1.1.8 (2024-01-25) — do not pin v1. Tokens actually live in `github.com/livekit/protocol/auth` |
|
||||
| `github.com/microcosm-cc/bluemonday` | v1.0.27 (already in framework `go.mod`) | HTML allowlist | D-16 locked. Already used by `seaglass`. Forum needs a **named dedicated policy**, not `seaglass.UGCPolicy` |
|
||||
|
||||
### Supporting (already in the framework / analog plugins)
|
||||
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| `github.com/yuin/goldmark` | v1.8.6 [VERIFIED: go.mod:34] | Markdown → HTML | D-16/D-17. `extension.GFM`; **do not** pass `html.WithUnsafe()` (default omits raw HTML = PHP `html_input=strip`) |
|
||||
| `github.com/golang-jwt/jwt/v5` | v5.3.1 [VERIFIED: go.mod:16] | HS256 LiveKit tokens + VAPID JWT (shared with webpush-go) | Golden LiveKit claims may be minted with jwt/v5 to match PHP byte-for-byte if SDK grant JSON differs |
|
||||
| `fetchguard` | in-tree | SSRF dial-time | D-10 PublicOnly for push; D-13 TrustedMode for operator LiveKit URL |
|
||||
| `lighthouse` | in-tree | realtime | D-15 `video` namespace; `user:{id}` incoming; analog chat authorizer (do **not** use `ChannelID` on 3-segment channels) |
|
||||
| `conga` / River | in-tree / STACK | jobs | D-11 one send job; D-14 periodic 5 min reconciler |
|
||||
| `festival` | in-tree | typed events | D-03 GDPR; D-08 group lifecycle |
|
||||
| `lagoon` / gormigrate | in-tree | Postgres | squashed IF NOT EXISTS baselines |
|
||||
| `surf` / `bouncer` / `cooler` | in-tree | routes, jwt.auth, named buckets | D-05 |
|
||||
| `testcontainers-go` | v0.44.0 (STACK.md) | plugin harness | D-04 |
|
||||
| `flare` | in-tree | stdlib Web Push | **Do not use for herald.** D-09 locked webpush-go. flare config is `push.*`, command is `websockets:generate-vapid-keys` [VERIFIED: modules/flare/commands.go:26] |
|
||||
|
||||
### Alternatives Considered (locked out)
|
||||
|
||||
| Instead of | Could Use | Why not |
|
||||
|------------|-----------|---------|
|
||||
| webpush-go | `flare.VAPIDPusher` | D-09 locked; different config/command namespace; flare takes `*http.Client` not the `HTTPClient` interface |
|
||||
| livekit SDK | hand-rolled jwt/v5 + Twirp HTTP | D-13 locked official SDK; jwt/v5 still allowed **inside** the plugin's LiveKit wrapper for the golden claim test |
|
||||
| bluemonday forum policy | `seaglass` UGC | seaglass explicitly turns **off** nofollow, allows `tel:`, and does not add `target=_blank` [VERIFIED: modules/seaglass/seaglass.go:22-27] |
|
||||
| goldmark GFM module in forum | `cabana.RenderMarkdown` | cabana uses `goldmark.New()` with no GFM and a regex gate [VERIFIED: modules/cabana/field_markdown.go:14-34]; D-17 forbids a forum-owned renderer |
|
||||
| LiveKit v1 module path | `github.com/livekit/server-sdk-go` without `/v2` | Official README now installs `/v2`; v1 is stale. Decision note records the v2 path as the official current module |
|
||||
|
||||
**Installation (plugin go.mod, after decision notes):**
|
||||
```bash
|
||||
# sm-herald-plugin
|
||||
go get github.com/SherClockHolmes/webpush-go@v1.4.0
|
||||
# sm-chatvideo-plugin
|
||||
go get github.com/livekit/server-sdk-go/v2@v2.19.0
|
||||
# forum consumes framework D-17 module (bluemonday + goldmark already in summercms.go)
|
||||
```
|
||||
</standard_stack>
|
||||
|
||||
## Package Legitimacy Audit
|
||||
|
||||
`msd-tools package-legitimacy check` only supports `--ecosystem npm|pypi|crates`. A crates check of the three Go import paths returned `SLOP` / `does-not-exist` (wrong ecosystem). That verdict does **not** apply to Go modules.
|
||||
|
||||
Go confirmation this session (not a legitimacy-OK tag):
|
||||
- `go list -m -versions github.com/SherClockHolmes/webpush-go` → v1.4.0 current; pkg.go.dev lists MIT, jwt/v5 + x/crypto.
|
||||
- `go list -m -versions github.com/livekit/server-sdk-go/v2` → v2.19.0 current; official GitHub README `go get github.com/livekit/server-sdk-go/v2`.
|
||||
- `github.com/microcosm-cc/bluemonday v1.0.27` already required by `summercms.go` [VERIFIED: go.mod:24].
|
||||
|
||||
Do not tag these `[VERIFIED: npm registry]`. Library **names** are CONTEXT-locked. Versions are `[CITED: proxy.golang.org / pkg.go.dev]` this session. classify-confidence for `webfetch --verified` returned **LOW**; planner should treat version pins as confirmation checkpoints inside the decision-note tasks, not as reopenable library choices.
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### System Architecture Diagram
|
||||
|
||||
```
|
||||
JWT actor
|
||||
│
|
||||
▼
|
||||
surf /_x/api/v1 ──jwt.auth + named buckets──► plugin Service
|
||||
│ │
|
||||
│ ├─ lagoon/GORM (Postgres)
|
||||
│ ├─ festival (user.Deleting, group.*)
|
||||
│ ├─ lighthouse.Emit / Bind
|
||||
│ └─ conga enqueue (same Tx)
|
||||
│
|
||||
├─ /_userfriends → Friendship/Group services → optional chat.Registry adapter
|
||||
├─ /_forum → ForumService + ForumAccessResolver
|
||||
│ └─ D-17 Render/Purify (framework) ──► content_html on write
|
||||
├─ /_chatbots → SlashCommandRegistry + CommandDispatcher + BotAuthResolver
|
||||
├─ /_herald → SubscriptionService
|
||||
│ └─ SendEvent job → PresenceGate? → webpush-go
|
||||
│ └─ fetchguard.PublicOnly Do
|
||||
└─ /_chatvideo → VideoService
|
||||
├─ LiveKit wrapper (TrustedMode HTTP)
|
||||
├─ lighthouse video:session:{id} (parse segment 2)
|
||||
└─ user:{id} video:call:incoming
|
||||
```
|
||||
|
||||
### Recommended Project Structure
|
||||
|
||||
Each new repo mirrors `sm-chat-plugin` / `sm-chatchannels-plugin`:
|
||||
|
||||
```
|
||||
sm-<name>-plugin/
|
||||
├── plugin.go # ID, Requires, pact capabilities, embed config+lang
|
||||
├── boot.go # deny-all default, Boot Warn, listeners, authorizer
|
||||
├── routes.go # /_<name>/api/v1 + surf.BucketProvider
|
||||
├── models/ # leaf
|
||||
├── classes/ # services, deny-all resolvers
|
||||
├── contracts/ # seams
|
||||
├── events/ # festival types (userfriends, maybe others)
|
||||
├── console/ # herald:vapid-generate, chatvideo:reconcile-sessions
|
||||
├── jobs/ # herald send, video reconcile
|
||||
├── updates/ # one squashed baseline + version ID comments
|
||||
├── config/ lang/
|
||||
├── harness_test.go # testcontainers, acme fixture, memory lighthouse
|
||||
├── testmain_test.go # flag.Parse + testing.Short
|
||||
└── testdata/goldens/
|
||||
```
|
||||
|
||||
Plugin IDs: `golem15.chatbots`, `golem15.chatvideo`, `golem15.forum`, `golem15.herald`, `golem15.userfriends`.
|
||||
Requires (PHP → Go):
|
||||
- chatbots: `golem15.chat`, `golem15.user` [VERIFIED: chatbots/Plugin.php:21]
|
||||
- chatvideo: `golem15.chat` (PHP also `$require` `Golem15.WebSockets` [VERIFIED: chatvideo/Plugin.php:7] → lighthouse, not a plugin require)
|
||||
- forum: `golem15.user` [VERIFIED: forum/Plugin.php:23]
|
||||
- herald: PHP `$require = ['Golem15.Apparatus']` [VERIFIED: herald/Plugin.php:20] → Go: **no user-model import beyond id**; no chat require
|
||||
- userfriends: `golem15.user` only [VERIFIED: userfriends/Plugin.php:16]; chat is soft
|
||||
|
||||
### Pattern 1: Squashed idempotent baseline (P18.1 / P19 D-04)
|
||||
**What:** One gormigrate ID equal to the final PHP version, `CREATE TABLE IF NOT EXISTS` + indexes matching PHP names.
|
||||
**When:** Every satellite. Record demo-seed version IDs as comments, do not insert demo rows (D-02 / D-07).
|
||||
**MySQL adopt vs Postgres:** enums become `varchar` + check if needed; `json`/`text` for keys; no `FIELD()` in GDPR heir query (PHP already SQLite-falls-back to `orderBy('role')` [VERIFIED: userfriends/Plugin.php:81-85]).
|
||||
|
||||
### Pattern 2: Deny-all seam + setter + Boot Warn (P19 D-14)
|
||||
Analog: `sm-chatchannels-plugin/classes/deny_all.go` `CanAccessContext`/`CanManageContext` always false [VERIFIED: sm-chatchannels-plugin/classes/deny_all.go:16-20]. Apply to `ForumAccessResolver`, `BotAuthResolver`, `PresenceGate`. HeraldProfile default is the PHP `NeutralHeraldProfile` (app-name-only), not deny-all.
|
||||
|
||||
### Pattern 3: lighthouse authorizer parses segment 2
|
||||
`lighthouse.ChannelID` returns segment 1 [VERIFIED: modules/lighthouse/channel.go:35-45]. `video:session:{id}` → `"session"` → 0. Chat already documents this and splits itself [VERIFIED: sm-chat-plugin/classes/authorizer.go:17-20]. Video authorizer copies that parser: three segments `video`, `session`, digits; capabilities `["prs"]` [VERIFIED: chatvideo/classes/VideoChannelAuthorizer.php:11-40].
|
||||
|
||||
### Pattern 4: Test-only acme host (D-04)
|
||||
testcontainers Postgres, `realtime.driver=memory`, fakes for LiveKit Twirp and push HTTP, `sm-user-plugin`, plus `sm-chat-plugin` where required. Neutral fixture context alias `acme` (not FriendGroup FQCN).
|
||||
|
||||
### Pattern 5: Generic `/_x/api/v1` prefix
|
||||
One serializer set. Actor id from token only. Mount via `lighthouse.Mount` surfaces so the app picks `jwt.auth`.
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
- **Forum-owned goldmark/bluemonday:** D-17; block the forum plan instead.
|
||||
- **Importing `seaglass` for forum posts:** opposite link policy.
|
||||
- **Using `flare` for herald:** D-09.
|
||||
- **`lighthouse.ChannelID` on `video:session:{id}`:** always 0.
|
||||
- **Hard-requiring chat from userfriends:** PHP soft registration [VERIFIED: userfriends/Plugin.php:9-16, 40-58].
|
||||
- **Wiring chatbots to Golem:** PHP never calls Golem.
|
||||
- **Porting Wavepath reapIfOrphaned whole:** GuestCall, call cards, CallNotifier stay in the app (D-14).
|
||||
- **Calling `html.WithUnsafe`:** would keep raw `<script>` and violate `html_input=strip`.
|
||||
- **Re-rendering adopted `content_html` on read:** D-18.
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| Web Push encryption + VAPID | stdlib aes/gcm + ES256 JWT | webpush-go (D-09) | RFC 8291 record padding, HKDF, subscription key decode |
|
||||
| LiveKit RoomService Twirp | hand-rolled protobuf client | official SDK behind an interface (D-13) | protocol + twirp already in the SDK; tests fake the interface |
|
||||
| LiveKit join JWT matching PHP | trust SDK grant JSON blindly | jwt/v5 encode of the PHP payload **or** SDK + golden decode | D-13 lists exact claims |
|
||||
| HTML XSS allowlist | regex (`cabana.RenderMarkdown`) | bluemonday named policy (D-16) | Journal note lists regex bypasses |
|
||||
| SSRF on push endpoints | PHP `EndpointGuard` + CURLOPT_RESOLVE | fetchguard PublicOnly (D-10) | dial-time IP check closes rebind |
|
||||
| Job retries | PHP queue + Cache::add OBS-01 only | conga/River (D-11) | transactional enqueue |
|
||||
| Markdown GFM | roll a converter | goldmark `extension.GFM` in D-17 | already a framework dep |
|
||||
|
||||
**Key insight:** The three new libraries are locked; the work is adapters (fetchguard Doer, LiveKit interface + golden JWT, named bluemonday policy) and not library shopping.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: lighthouse.ChannelID is the wrong segment
|
||||
**What goes wrong:** `video` authorizer allows nobody or the wrong session.
|
||||
**Why:** ChannelID reads explode[1]; for `video:session:{id}` that is `"session"` → 0 [VERIFIED: modules/lighthouse/channel.go:35-45]. PHP explodes and uses `$parts[2]` [VERIFIED: chatvideo/classes/VideoChannelAuthorizer.php:11-21].
|
||||
**How to avoid:** Copy `parseChatConversationChannel` (three named segments).
|
||||
**Warning signs:** memory-driver subscribe tests pass with session id 0.
|
||||
|
||||
### Pitfall 2: D-13 deleteRoom swallow vs PHP LiveKitService
|
||||
**What goes wrong:** planner copies PHP `LiveKitService::deleteRoom` which does **not** swallow [VERIFIED: chatvideo/classes/LiveKitService.php:34-37]. CONTEXT D-13 says missing-room is a no-op. The swallow lives in Wavepath `CommunityVideoService::endSession` try/catch [VERIFIED: wavepath/classes/video/CommunityVideoService.php:204-220]. `participantCount` swallows to 0 in the plugin [VERIFIED: chatvideo/classes/LiveKitService.php:49-56].
|
||||
**How to avoid:** Go LiveKit wrapper swallows delete/missing-room per D-13 (locked), even though the PHP plugin method does not.
|
||||
**Warning signs:** reconciler tests fail when the fake returns Twirp not-found.
|
||||
|
||||
### Pitfall 3: D-14 reconciler is not Wavepath ReconcileVideoSessions
|
||||
**What goes wrong:** chatvideo imports GuestCallService, flips chat call cards, resyncs stuck cards, CallNotifier close-push.
|
||||
**Why:** Wavepath command does all of that [VERIFIED: wavepath/console/ReconcileVideoSessions.php:23-50]. D-14 keeps only LiveKit empty/gone + `max_duration`. `start_grace_seconds` is Wavepath-only (`config('chatvideo.start_grace_seconds', 120)` [VERIFIED: CommunityVideoService.php:351]) — **not** in chatvideo `config/config.php`.
|
||||
**How to avoid:** Generic service: if room missing/empty or `started_at + max_duration` elapsed → end session + deleteRoom (swallowed) + `video:call:ended`. No card metadata, no guests.
|
||||
**Warning signs:** chatvideo go.mod imports a host plugin.
|
||||
|
||||
### Pitfall 4: video-initiate lives on a dropped route
|
||||
**What goes wrong:** generic start has no 5/min bucket.
|
||||
**Why:** PHP `video-initiate` is only on `/api/video/initiate` [VERIFIED: chatvideo/routes.php:11-27], which D-07 drops. Wavepath community video inherits `throttle:wave-api` (240/min). D-05 still carries `video-initiate` 5/min onto generic start.
|
||||
**How to avoid:** `surf.BucketProvider` named `video-initiate` `Limit::perMinute(5)` on start only; `call-link` 10/min on resolve [VERIFIED: wavepath/routes.php:86-88]; general per-API bucket on the rest.
|
||||
**Warning signs:** start unthrottled in routes.go.
|
||||
|
||||
### Pitfall 5: seaglass ≠ forum HTMLPurifier
|
||||
**What goes wrong:** forum links lose `nofollow`/`target=_blank`; images/`tel:` leak in.
|
||||
**Why:** seaglass starts from `UGCPolicy`, sets `RequireNoFollowOnLinks(false)`, allows `tel:` [VERIFIED: modules/seaglass/seaglass.go:29-32]. PHP sets `HTML.TargetBlank` true, `HTML.Nofollow` true, schemes http/https/mailto only, no `img` [VERIFIED: forum/classes/markdown/MarkdownRenderer.php:62-73].
|
||||
**How to avoid:** D-17 named policy: `AllowElements` of the PHP list, `AllowURLSchemes("http","https","mailto")`, `RequireNoFollowOnLinks(true)`, `AddTargetBlankToFullyQualifiedLinks(true)`.
|
||||
**Warning signs:** forum tests import `seaglass`.
|
||||
|
||||
### Pitfall 6: D-17 not landed
|
||||
**What goes wrong:** forum plan grows a local renderer.
|
||||
**Why:** this session: no `modules/*markdown*` directory; `cabana.RenderMarkdown` is the wrong gate.
|
||||
**How to avoid:** forum plan first task is "require D-17 export"; if missing, **block**, do not substitute.
|
||||
**Warning signs:** `forum/classes/markdown.go`.
|
||||
|
||||
### Pitfall 7: flare looks like herald
|
||||
**What goes wrong:** executor uses `flare.From` / `push.*` / `websockets:generate-vapid-keys`.
|
||||
**Why:** flare already implements RFC 8291/8292 in-tree. D-09 still locks webpush-go. PHP command is `herald:vapid-generate` [VERIFIED: herald/console/GenerateVapidKeys.php:18]; config `golem15.herald::herald.vapid.*` [VERIFIED: herald/config/herald.php:13-16].
|
||||
**How to avoid:** decision note records the divergence in one paragraph.
|
||||
**Warning signs:** herald importing `modules/flare`.
|
||||
|
||||
### Pitfall 8: fetchguard.Client is not `*http.Client`
|
||||
**What goes wrong:** note claims an adapter is required, or code does `http.Client{Transport: fetchguard}`.
|
||||
**Why:** webpush-go `HTTPClient` is `Do(*http.Request) (*http.Response, error)` [CITED: github.com/SherClockHolmes/webpush-go v1.4.0 webpush.go]. `fetchguard.Client.Do` matches [VERIFIED: modules/fetchguard/client.go:85-86]. Pass `*fetchguard.Client` as `Options.HTTPClient`. No wrapper type needed. *If* a future library required `*http.Client`, that would need an adapter — current lib does not.
|
||||
**How to avoid:** set `Options.HTTPClient = publicOnlyClient`.
|
||||
**Warning signs:** extra `type fetchguardDoer struct`.
|
||||
|
||||
### Pitfall 9: LiveKit v2 transitive bomb
|
||||
**What goes wrong:** chatvideo tests import pion/webrtc; `go test` downloads buildkit.
|
||||
**Why:** v2.19.0 go.mod requires `github.com/pion/webrtc/v4`, `github.com/livekit/media-sdk`, `github.com/moby/buildkit` (Read module cache this session).
|
||||
**How to avoid:** decision note flags pion. Production code talks to a narrow `RoomAPI` interface. Tests ship a fake; do not construct `lksdk.Room` (that's the realtime client). Prefer `NewRoomServiceClient` + `protocol/auth` only.
|
||||
**Warning signs:** test file imports `pion`.
|
||||
|
||||
### Pitfall 10: PHP morph FQCNs in forum (and chat) columns
|
||||
**What goes wrong:** Go registry looks up alias `friendgroup` and adopted rows still say `Golem15\UserFriends\Models\FriendGroup`.
|
||||
**Why:** P19 D-12 rewrite. Live MySQL not readable this session; PHP hook queries that FQCN [VERIFIED: forum/Plugin.php:114-116]. Chat conversations also store Channel / FriendContext / FriendGroup class names (P19 already owns that rewrite).
|
||||
**How to avoid:** discretion: extend `chat:rewrite-context-types` **and/or** forum ships `forum:rewrite-context-types` for `golem15_forum_categories.context_type`. Recommend **forum ships its own** so forum can adopt without chat. Map PHP class → registered alias.
|
||||
**Warning signs:** DeleteContext tests only use aliases and skip SQL rewrite.
|
||||
|
||||
### Pitfall 11: community-join is 10/min, not 5
|
||||
**What goes wrong:** friend-request bucket copied from video-initiate.
|
||||
**Why:** `RateLimiter::for('community-join'… Limit::perMinute(10)` [VERIFIED: wavepath/routes.php:76-78]; applied to `community/friends/request` [VERIFIED: wavepath/routes.php:621].
|
||||
**How to avoid:** userfriends send-request uses `community-join` 10/min; groups join/preview too if those routes are in D-05.
|
||||
**Warning signs:** 5/min on friends/request.
|
||||
|
||||
### Pitfall 12: DeleteContext is a new Go API
|
||||
**What goes wrong:** planner looks for PHP `ForumService::deleteContext` and finds nothing.
|
||||
**Why:** PHP uses `class_exists(FriendGroup)` model hook [VERIFIED: forum/Plugin.php:111-120]. D-08 replaces it with generic `DeleteContext(type, id)` that forum owns. Go **adds** this method.
|
||||
**How to avoid:** implement on ForumService; no userfriends import.
|
||||
**Warning signs:** forum/go.mod requires sm-userfriends-plugin.
|
||||
|
||||
### Pitfall 13: EndpointGuard PHP tests become N/A
|
||||
**What goes wrong:** last plan tries to port `test_endpoint_guard_pins_validated_ip_into_curl_resolve`.
|
||||
**Why:** D-10 removes the seam. Map those methods to fetchguard PublicOnly unit tests (or N/A + pointer to fetchguard's own tests).
|
||||
**Warning signs:** herald reintroduces EndpointGuard.
|
||||
|
||||
### Pitfall 14: user.Deleting already exists — do not re-add
|
||||
**What goes wrong:** satellite plans edit sm-user-plugin to add events Phase 19 already shipped.
|
||||
**Why:** `CreatedEventName = "user.Created"`, `DeletingEventName = "user.Deleting"` [VERIFIED: fonoteka.go/plugins/golem15/user/classes/events.go:9-12]. Chat already listens. Satellites only `Listen[*userclasses.DeletingEvent]`.
|
||||
**How to avoid:** no sm-user-plugin schema work in 19.1 unless a golden proves a gap.
|
||||
**Warning signs:** satellite plan touches `sm-user-plugin/classes/events.go`.
|
||||
|
||||
<code_examples>
|
||||
## Code Examples
|
||||
|
||||
### webpush-go + fetchguard (no adapter type)
|
||||
|
||||
```go
|
||||
// Source: webpush-go v1.4.0 Options.HTTPClient [CITED: github.com/SherClockHolmes/webpush-go/blob/v1.4.0/webpush.go]
|
||||
// fetchguard.Client.Do [VERIFIED: modules/fetchguard/client.go:85-86]
|
||||
public, err := fetchguard.NewClient(fetchguard.Policy{Mode: fetchguard.PublicOnlyMode}, cfg)
|
||||
resp, err := webpush.SendNotificationWithContext(ctx, payload, &webpush.Subscription{
|
||||
Endpoint: sub.Endpoint,
|
||||
Keys: webpush.Keys{Auth: sub.Auth, P256dh: sub.P256dh},
|
||||
}, &webpush.Options{
|
||||
HTTPClient: public, // implements Do(*http.Request)
|
||||
Subscriber: subject, // mailto:… from golem15.herald.vapid.subject
|
||||
VAPIDPublicKey: pub,
|
||||
VAPIDPrivateKey: priv,
|
||||
TTL: 2419200,
|
||||
Urgency: webpush.Urgency("normal"),
|
||||
})
|
||||
```
|
||||
|
||||
TTL default `2419200` and urgency `normal` [VERIFIED: herald/classes/PushSender.php:111-113]. Prune when status is 404 or 410 (PHP `isSubscriptionExpired`) [VERIFIED: herald/classes/PushSender.php:182-185].
|
||||
|
||||
### LiveKit token claims (PHP payload to golden)
|
||||
|
||||
```php
|
||||
// [VERIFIED: chatvideo/classes/LiveKitService.php:80-99]
|
||||
$payload = [
|
||||
'iss' => $this->apiKey,
|
||||
'sub' => (string) $userId,
|
||||
'name' => $userName,
|
||||
'iat' => $now,
|
||||
'exp' => $now + $ttl, // token_ttl default 900
|
||||
'nbf' => $now,
|
||||
'video' => [
|
||||
'room' => $roomName,
|
||||
'roomJoin' => true,
|
||||
'canPublish' => true,
|
||||
'canSubscribe' => true,
|
||||
],
|
||||
];
|
||||
// optional: $payload['metadata'] = json_encode($metadata);
|
||||
JWT::encode($payload, $this->apiSecret, 'HS256');
|
||||
```
|
||||
|
||||
Admin URL fallback [VERIFIED: chatvideo/classes/LiveKitService.php:109-114]: `livekit_admin_url` else rewrite `wss://`/`ws://` → `https://`/`http://`. Room name: `'chat-conversation-' . $conversationId . '-' . bin2hex(random_bytes(8))` [VERIFIED: chatvideo/classes/LiveKitService.php:102-105].
|
||||
|
||||
SDK mint (official README) — **verify decoded claims against the PHP golden**, do not assume grant JSON keys match:
|
||||
|
||||
```go
|
||||
// [CITED: https://github.com/livekit/server-sdk-go README Token creation]
|
||||
at := auth.NewAccessToken(apiKey, apiSecret)
|
||||
at.SetVideoGrant(&auth.VideoGrant{RoomJoin: true, Room: room}).
|
||||
SetIdentity(identity).SetValidFor(time.Duration(ttl) * time.Second)
|
||||
token, err := at.ToJWT()
|
||||
```
|
||||
|
||||
If `ToJWT` omits `name` / `nbf` / nested `video` keys, mint with `jwt/v5` MapClaims copying the PHP payload instead, still behind the same wrapper.
|
||||
|
||||
LiveKit HTTP: `fetchguard.Policy{Mode: fetchguard.TrustedMode}` [VERIFIED: modules/fetchguard/policy.go:23-29] — operator URL, may be loopback `http://localhost:7880`.
|
||||
|
||||
### Forum bluemonday policy (D-16) — belongs in D-17 module
|
||||
|
||||
```go
|
||||
// APIs [VERIFIED: bluemonday@v1.0.27/policy.go:620-693]
|
||||
p := bluemonday.NewPolicy()
|
||||
p.AllowElements("p","br","strong","em","b","i","h1","h2","h3","h4","h5","h6",
|
||||
"ul","ol","li","blockquote","pre","code","a","table","thead","tbody","tr","th","td",
|
||||
"hr","del","ins","sub","sup")
|
||||
p.AllowAttrs("href","rel","target").OnElements("a")
|
||||
p.AllowURLSchemes("http", "https", "mailto")
|
||||
p.RequireNoFollowOnLinks(true)
|
||||
p.AddTargetBlankToFullyQualifiedLinks(true)
|
||||
```
|
||||
|
||||
goldmark: `goldmark.New(goldmark.WithExtensions(extension.GFM))` — GFM = Linkify+Table+Strikethrough+TaskList [VERIFIED: goldmark@v1.8.6/extension/gfm.go:10-18]. Do not pass `html.WithUnsafe()`.
|
||||
|
||||
D-17 interface the forum plan consumes (quick task owns names):
|
||||
|
||||
```go
|
||||
Render(markdown string) string // convert + named policy
|
||||
Purify(html string) string // policy only (PHP MarkdownRenderer::purify)
|
||||
```
|
||||
|
||||
XSS corpus [VERIFIED: forum/tests/security/ForumMarkdownRenderTest.php:19-27]: `<script>alert(1)</script>`, `[x](javascript:alert(1))`, `<iframe src=x></iframe>`, `<img src=x onerror=alert(1)>`.
|
||||
|
||||
### video authorizer (do not use ChannelID)
|
||||
|
||||
```go
|
||||
// PHP [VERIFIED: chatvideo/classes/VideoChannelAuthorizer.php:11-40]
|
||||
// analog [VERIFIED: sm-chat-plugin/classes/authorizer.go:17-20]
|
||||
parts := strings.Split(strings.TrimPrefix(ch, "presence:"), ":")
|
||||
if len(parts) != 3 || parts[0] != "video" || parts[1] != "session" || !digits(parts[2]) {
|
||||
return lighthouse.Denied("invalid video channel format")
|
||||
}
|
||||
```
|
||||
|
||||
Events [VERIFIED: chatvideo/classes/VideoCallNotifier.php:18-66]:
|
||||
|
||||
| Event | Channel | Payload keys |
|
||||
|-------|---------|--------------|
|
||||
| `video:call:incoming` | `user:{id}` via publishToUsers | session_id, conversation_id, caller_name, caller_avatar |
|
||||
| `video:call:accepted` | `video:session:{id}` | session_id, user_id, user_name |
|
||||
| `video:call:declined` | same | session_id, user_id |
|
||||
| `video:call:ended` | same | session_id, ended_by, duration_seconds |
|
||||
| `video:participant:joined` | same | session_id, user_id, user_name |
|
||||
| `video:participant:left` | same | session_id, user_id |
|
||||
|
||||
Publish failure logged, not raised [VERIFIED: VideoCallNotifier.php:73-75].
|
||||
|
||||
### Slash commands
|
||||
|
||||
```go
|
||||
// RESERVED [VERIFIED: chatbots/classes/SlashCommandRegistry.php:31]
|
||||
const ReservedHelp = "help"
|
||||
// register() rejects reserved + duplicates; registerBuiltin() skips reserved check
|
||||
// [VERIFIED: SlashCommandRegistry.php:53-74]
|
||||
// modes [VERIFIED: CommandResult.php:21-22]
|
||||
const ModeEphemeral, ModeAnnounce = "ephemeral", "announce"
|
||||
// CMD-06: actor from caller, never from input [VERIFIED: CommandDispatcher.php:20-22, 43]
|
||||
```
|
||||
|
||||
Unrecognized trigger returns nil (fall through to a normal message) [VERIFIED: CommandDispatcher.php:54-56].
|
||||
|
||||
### GDPR listeners (festival)
|
||||
|
||||
```go
|
||||
app.Events.Listen[*userclasses.DeletingEvent](PluginID, p.onUserDeleting)
|
||||
```
|
||||
|
||||
- chatbots: delete `golem15_chatbots_bot_flags` for that user_id [VERIFIED: chatbots/Plugin.php:54-59]
|
||||
- forum: `user_id = null` on threads and posts [VERIFIED: forum/Plugin.php:89-92]
|
||||
- userfriends: heir oldest admin else oldest member else `forceDelete` group; then delete member and ban rows [VERIFIED: userfriends/Plugin.php:73-98]
|
||||
</code_examples>
|
||||
|
||||
<sota_updates>
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Minishlink WebPush (PHP) | webpush-go v1.4.0 | D-09 / 2025-01-02 | HTTPClient interface fits fetchguard |
|
||||
| Agence104 LiveKit PHP + firebase/jwt | livekit/server-sdk-go **v2** | D-13; v2 current 2026-10 | Pin `/v2`; golden-test claims |
|
||||
| HTMLPurifier + League CommonMark GFM | goldmark GFM + bluemonday named policy | D-16/D-17 | Framework module, not forum |
|
||||
| Golem15.WebSockets authorizer | lighthouse Registry | P19 | video namespace |
|
||||
| PHP EndpointGuard + CURLOPT_RESOLVE | fetchguard PublicOnly | D-10 | fail-closed |
|
||||
| User::extend beforeDelete | festival `user.Deleting` | P19 | already in sm-user-plugin |
|
||||
| class_exists(FriendGroup) | forum.DeleteContext + app bridge | D-08 | no cross-plugin types |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
- `github.com/livekit/server-sdk-go` v1 (v1.1.8, 2024-01-25)
|
||||
- ROADMAP SC2 Golem clause
|
||||
- PHP herald "ships no routes" (D-06)
|
||||
- `cabana.RenderMarkdown` as a UGC renderer
|
||||
</sota_updates>
|
||||
|
||||
## Assumptions Log
|
||||
|
||||
| ID | Claim | Tag | Disposition |
|
||||
|----|-------|-----|-------------|
|
||||
| A1 | Live Wavepath MySQL matches PHP migrations | unverified this session (`ERROR 1698`) | Use PHP migrations as schema SoT; optional executor dump |
|
||||
| A2 | livekit `protocol/auth` ToJWT equals PHP firebase payload | [ASSUMED] until golden | Decision-note task: decode both; if differ, mint with jwt/v5 |
|
||||
| A3 | D-17 will export `Render`/`Purify` + named policy before forum execute | CONTEXT | Forum plan blocks if missing |
|
||||
| A4 | classify-confidence webfetch=LOW even `--verified` | seam output | Library versions are checkpoints inside locked names |
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **LiveKit JWT: SDK vs jwt/v5**
|
||||
- What we know: PHP payload is explicit MapClaims [VERIFIED: LiveKitService.php:80-99]. Official Go samples use `auth.AccessToken`.
|
||||
- What's unclear: extra claims (`jti`, grant nesting).
|
||||
- Recommendation: first chatvideo task is a golden round-trip; pick mint path that matches PHP. Do not reopen D-13.
|
||||
|
||||
2. **D-17 module shape**
|
||||
- What we know: not landed; must not be cabana or seaglass.
|
||||
- What's unclear: package name (beach-theme), whether `Purify` policies are a registry.
|
||||
- Recommendation: forum consumes whatever the quick task exports; document the call in the forum plan, do not specify the package name here.
|
||||
|
||||
3. **Morph rewrite ownership**
|
||||
- Discretion. Recommendation: forum owns `forum:rewrite-context-types` for `golem15_forum_categories.context_type`; chat already rewrites conversation context_type.
|
||||
|
||||
4. **chatvideo generic addressing**
|
||||
- Discretion: conversation id vs context alias. Recommendation: **conversation id** as primary (`/_chatvideo/api/v1/conversations/{id}/video/...`) plus optional context lookup through chat's registry so Wavepath group-video payloads stay reproducible.
|
||||
|
||||
5. **Herald NeutralHeraldProfile vs deny-all**
|
||||
- PHP binds NeutralHeraldProfile + DenyAllPresenceGate. Go should keep both defaults. PresenceGate is the fail-closed send gate.
|
||||
|
||||
6. **OBS-01 cooldown without Laravel Cache**
|
||||
- PHP `Cache::add('obs:warn:herald.delivery', 1, 60)` [VERIFIED: PushSender.php:213]. Recommendation: process-local sync.Map 60s (PHP already has an in-process fallback [VERIFIED: PushSender.php:227-246]) plus optional cooler if already in the plugin test host. Do not add Redis as a herald dependency.
|
||||
|
||||
## Environment Availability
|
||||
|
||||
| Dependency | Required By | Available | Version | Fallback |
|
||||
|------------|------------|-----------|---------|----------|
|
||||
| Go | All code | ✓ | 1.27.0-X:nodwarf5 | — |
|
||||
| Docker | D-04 testcontainers | ✓ | 29.7.2 (`docker ps` ok) | — |
|
||||
| Postgres (local) | optional | ✓ | pg_isready on /run/postgresql:5432 | testcontainers |
|
||||
| tea | create sm-*-plugin repos | ✓ | 0.16.0 | Gitea UI |
|
||||
| mysql/mariadb client | D-02 live diff | ✓ client 12.3.3 | dump **failed** this session (`ERROR 1698`) | PHP migrations |
|
||||
| ctx7 CLI / Context7 MCP | research-plan provider | ✗ | — | GitHub raw + pkg.go.dev via WebFetch |
|
||||
| graphify | research graph | disabled | — | skipped |
|
||||
| LiveKit server | chatvideo e2e vs real SFU | ✗ in this audit | — | Twirp fake (D-04) |
|
||||
| D-17 markdown module | forum | ✗ | — | **block forum plan** |
|
||||
|
||||
**Missing dependencies with no fallback:**
|
||||
- D-17 framework markdown module (forum execution blocker, not a research blocker).
|
||||
- Live Wavepath MySQL credentials (schema SoT = PHP migrations).
|
||||
|
||||
**Missing with fallback:**
|
||||
- Context7: official GitHub/pkg.go.dev used.
|
||||
- Real LiveKit: interface fake.
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
`workflow.nyquist_validation` is `true` in `.planning/config.json` [VERIFIED: .planning/config.json:20].
|
||||
|
||||
Five plugin repos + three decision notes + last-plan PHP maps. Nyquist: sample **every repo that a task touches**, not a random subset. A task that only writes `sm-forum-plugin` still runs `go test -short ./...` **in that module**; it does not stand in for herald. Decision-note tasks have no Go tests — sample by `grep` that the note names the locked module path + version before the importing commit.
|
||||
|
||||
### Test Framework
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | Go `testing` + testcontainers-go v0.44.0 (Postgres) |
|
||||
| Config file | none at framework root — each plugin is its own module |
|
||||
| Quick run | `go test -short ./...` inside the touched plugin (skip Docker) |
|
||||
| Full suite | `go test ./...` (testcontainers + lighthouse memory + fakes) |
|
||||
| Analog | `sm-chat-plugin` `harness_test.go` / `http_tracer_test.go` / `testmain_test.go` (`flag.Parse` + `testing.Short`) |
|
||||
|
||||
Per-plugin fakes: LiveKit Twirp (`RoomAPI` interface), push HTTP (`httptest` + webpush-go `HTTPClient` stub), D-17 renderer stub only if the forum plan is allowed to compile against a test fake **after** the real module exists (do not ship a forum-local stub as production).
|
||||
|
||||
### Phase Requirements → Test Map
|
||||
|
||||
| Req | Behavior | Test Type | Automated Command | File Exists? |
|
||||
|-----|----------|-----------|-------------------|--------------|
|
||||
| SC-19.1-1 | each baseline creates final PHP tables | integration | `go test -short ./updates -run TestBaseline` ×5 | ❌ Wave 0 |
|
||||
| SC-19.1-1 | generic API tracer (one E2E per plugin) | integration | `go test ./... -run TestHTTPTracer` ×5 | ❌ Wave 0 |
|
||||
| SC-19.1-2 | notes exist before importing commits | manual+grep | `test -f .planning/notes/2026-10-*-webpush-go.md` (etc.) | ❌ Wave 0 |
|
||||
| SC-19.1-2 | chatbots has **no** Golem import | unit | `go test -short ./... -run TestNoGolem` | ❌ Wave 0 |
|
||||
| SC-19.1-3 | userfriends source = Wavepath 182b514 | already met | `diff -rq` this session empty | ✓ research |
|
||||
| SC-19.1-4 | PHP method map complete | unit (last plan) | `go test ./...` + map file | ❌ Wave 0 |
|
||||
| D-10 | subscribe rejects http/private IP | unit | `go test -short ./classes -run TestSubscribeEndpoint` | ❌ Wave 0 |
|
||||
| D-11 | SendEvent enqueues in Tx; rollback sends 0 | integration | `go test ./... -run TestSendEventRollback` | ❌ Wave 0 |
|
||||
| D-13 | JWT claims golden vs PHP payload | unit | `go test -short ./classes -run TestLiveKitTokenClaims` | ❌ Wave 0 |
|
||||
| D-15 | video authorizer segment 2 + prs | unit | `go test -short ./classes -run TestVideoAuthorizer` | ❌ Wave 0 |
|
||||
| D-16/D-18 | XSS + GFM semantic HTML | unit | D-17 tests + forum write-path | ❌ Wave 0 (D-17) |
|
||||
| D-08 | forum does not import userfriends | unit | `go test -short ./... -run TestNoUserfriendsImport` | ❌ Wave 0 |
|
||||
|
||||
### Sampling Rate
|
||||
|
||||
- **Per task commit:** `go vet ./...` and `go test -short ./...` in **each** plugin module the task modified. A decision-note-only commit: `git grep` the locked import in the note, no `go test`.
|
||||
- **Per wave merge:** `go test ./...` in every plugin the wave touched (Docker).
|
||||
- **Phase gate:** `scripts/check-phase19.1.sh` (create in last plan) listing named tests across the five modules + the three note paths, analog to `scripts/check-phase19.sh`.
|
||||
|
||||
Five repos means a task that "finishes forum" is not evidence for herald. Nyquist sampling advice: **n = number of independent deliverables (5 plugins + 3 notes + 1 map)**. Sample all 9 at the phase gate; per-commit sample the subset in the diff.
|
||||
|
||||
### Last-plan PHP-test-method mapping (counts Read this session)
|
||||
|
||||
| Plugin | `function test` | Test.php files | Go fate |
|
||||
|--------|-----------------|----------------|---------|
|
||||
| chatbots | **22** | 7 | all port (registry, dispatcher, help, deny-all, RODO, boundary, additivity) |
|
||||
| forum | **25** | 6 | all port; markdown goldens consume D-17 |
|
||||
| herald | **14** | 5 | port except EndpointGuard pin/reject → N/A (D-10) + fetchguard pointer. `test_default_endpoint_guard_pins_and_rejects_nothing` N/A |
|
||||
| userfriends | **31** | 3 | all port (GroupPolicy, GroupVisibilityGate, GroupModels). Components N/A (D-07) |
|
||||
| chatvideo | **0** in-plugin | 0 | No in-plugin PHP tests. Wavepath host tests (`CommunityChannelVideoTest` 11, `CommunityCallLinkTest` 8, `CommunityVideoSeverTest` 5, `CallNotificationLifecycleTest` 12, `GuestCallLinkTest` 8, `GuestLiveKitGatewayTest` 3, …) are **N/A** except the generic start/join/leave/end/status/call-link behaviors those files happen to cover — map each Wavepath method to generic-API Go or explicit N/A (guest, call cards, CallNotifier) |
|
||||
|
||||
### Tracer-first (one production-quality E2E per plugin)
|
||||
|
||||
| Plugin | Tracer |
|
||||
|--------|--------|
|
||||
| userfriends | JWT user A sends request to B → accept → list friends 200; send-request hits `community-join` 10/min |
|
||||
| forum | allow-all test resolver; create category+thread+post; `content_html` has `<table>` for GFM and no `<script>`; mark read; unread count 0 |
|
||||
| chatbots | register `/ping`; dispatch as JWT actor; ephemeral returns payload and inserts 0 chat messages; `/help` listed |
|
||||
| herald | POST subscription (https public fake); `SendUnconditional` job after commit hits fake push; 410 prunes row; rollback sends 0 |
|
||||
| chatvideo | start session (unique active_conversation_id); token claims golden; subscribe `video:session:{id}` as participant allowed / stranger denied; reconcile empty room → ended |
|
||||
|
||||
### Wave 0 Gaps
|
||||
|
||||
- [ ] Five repos: `go.mod` with `replace` to `../../../../summercms.go`, `plugin.go`, baseline, `harness_test.go` / `testmain_test.go`
|
||||
- [ ] `testdata/goldens/` per plugin (serializers, events, LiveKit JWT, forum HTML)
|
||||
- [ ] `acme` fixture plugin (test files only) where a context is needed (forum, userfriends adapter)
|
||||
- [ ] LiveKit `RoomAPI` + push `HTTPClient` fakes
|
||||
- [ ] Three decision notes before importing commits
|
||||
- [ ] D-17 module (separate quick task) before forum execute
|
||||
- [ ] PHP→Go method map markdown in the last plan
|
||||
- [ ] `scripts/check-phase19.1.sh`
|
||||
|
||||
Existing fetchguard / lighthouse / seaglass / flare tests do **not** cover satellite behavior.
|
||||
|
||||
## Security Domain
|
||||
|
||||
`security_enforcement` is not set to false. CONTEXT requires the security-review agent.
|
||||
|
||||
### Applicable ASVS Categories
|
||||
|
||||
| ASVS Category | Applies | Standard Control |
|
||||
|---------------|---------|-----------------|
|
||||
| V2 Authentication | yes | `jwt.auth`; bot bearer via `BotAuthResolver` deny-all; actor id from token (CMD-06) |
|
||||
| V3 Session Management | no | JWT |
|
||||
| V4 Access Control | yes | ForumAccessResolver, conversation participation for video, group policy matrix, IDOR → 404 |
|
||||
| V5 Input Validation / Sanitization | yes | D-16 bluemonday; subscribe URL https+public; join_code alphabet |
|
||||
| V6 Cryptography | yes | VAPID keys; LiveKit HS256 secret; private key never logged |
|
||||
| V13 API | yes | named rate buckets; generic prefixes |
|
||||
| V13 SSRF | yes | fetchguard PublicOnly on push; TrustedMode only for operator LiveKit URL |
|
||||
|
||||
### Known Threat Patterns
|
||||
|
||||
| Pattern | STRIDE | Standard Mitigation |
|
||||
|---------|--------|---------------------|
|
||||
| SSRF via push endpoint | Elevation | D-10 PublicOnly at subscribe and send; no EndpointGuard |
|
||||
| VAPID private key leak | Information disclosure | `herald:vapid-generate` prints env lines; never log [VERIFIED: GenerateVapidKeys.php:11-13] |
|
||||
| LiveKit token as any identity | Elevation | `sub` = authenticated user id string; secret server-side |
|
||||
| XSS in forum posts | Tampering | D-16 policy; render-on-write; adopted HTML served stored (D-18) not re-parsed unsafely |
|
||||
| IDOR on session/join_code/friends | Information disclosure | JWT actor + participation/membership; call-link 10/min [VERIFIED: wavepath/routes.php:86-88] |
|
||||
| video:session subscribe as stranger | Elevation | authorizer re-checks conversation participants; capabilities `prs` only |
|
||||
| Slash command spoofed actor | Elevation | CMD-06: actor from token, never body [VERIFIED: CommandDispatcher.php:20-22] |
|
||||
| GDPR leftover bot_flags / authorship | Information disclosure | user.Deleting listeners |
|
||||
| DNS rebind FCM endpoint | Elevation | dial-time fetchguard (replaces CURLOPT_RESOLVE) |
|
||||
| Enumerate join codes | Information disclosure | `call-link` 10/min; uniform 404 |
|
||||
|
||||
## Frozen PHP inventory (planner cheat-sheet)
|
||||
|
||||
Frozen HEADs this session (full SHA):
|
||||
- chatbots `1ecf8c8bb6b81102508c0b8e3a999aaacf9d4963`
|
||||
- chatvideo `25135b46c1571a6bc722fdc094157176d70ad717`
|
||||
- forum `ad10b54eb7be8e11de8e55dd85cfa0c92bbb15c9`
|
||||
- herald `a749f83912a7c88994eb5341740a60b3488463f9`
|
||||
- userfriends `182b5145561f0609ab54f59e24258e12a96332b1`
|
||||
|
||||
Live Wavepath MySQL: **not dumped this session**. Schema below is PHP migrations.
|
||||
|
||||
### chatbots schema (v1.0.0)
|
||||
|
||||
[VERIFIED: chatbots/updates/version.yaml + create_bot_flags_table.php:13-26]
|
||||
|
||||
| Table | Columns / notes |
|
||||
|-------|-----------------|
|
||||
| `golem15_chatbots_bot_flags` | id, user_id unique `chatbots_bot_flags_user_unique`, is_bot bool default true, timestamps; FK user_id → users cascade |
|
||||
|
||||
### chatvideo schema (v1.0.4 final)
|
||||
|
||||
[VERIFIED: chatvideo/updates/version.yaml:1-16] Demo seed is v1.0.1 — record ID, no rows (D-02).
|
||||
|
||||
| Table | Columns / notes |
|
||||
|-------|-----------------|
|
||||
| `golem15_chatvideo_sessions` | id, conversation_id FK chat_conversations cascade, `card_message_id` nullable uint, `active_conversation_id` nullable unique `uniq_chatvideo_active_conversation`, `livekit_room_name` varchar(100) unique, `join_code` varchar(12) nullable unique, status enum `pending\|active\|ended` default pending, started_by FK users cascade, started_at, ended_at nullable, duration_seconds nullable; indexes conversation, status. Model: no updated_at; CREATED_AT=`started_at` [VERIFIED: VideoSession.php:24-26]. JOIN_CODE alphabet `abcdefghijklmnopqrstuvwxyz0123456789` length 10 [VERIFIED: VideoSession.php:17-18] |
|
||||
| `golem15_chatvideo_participants` | id, video_session_id FK cascade, user_id FK cascade, status enum `invited\|joined\|declined\|left` default invited, joined_at, left_at; unique `(video_session_id, user_id)` `uq_chatvideo_session_user` |
|
||||
|
||||
Config [VERIFIED: chatvideo/config/config.php:4-11]: `livekit_url`/`LIVEKIT_URL` default `ws://localhost:7880`, `livekit_api_key`/`LIVEKIT_API_KEY`, `livekit_api_secret`/`LIVEKIT_API_SECRET`, `max_duration`/`VIDEO_MAX_DURATION` 3600, `token_ttl`/`VIDEO_TOKEN_TTL` 900, `livekit_admin_url`/`LIVEKIT_ADMIN_URL`, `max_participants`/`VIDEO_MAX_PARTICIPANTS` 8, `screen_share`/`VIDEO_SCREEN_SHARE` true.
|
||||
|
||||
Command: `summer chatvideo:reconcile-sessions` (D-14). Periodic every 5 minutes.
|
||||
|
||||
### forum schema (v1.1.0 final)
|
||||
|
||||
[VERIFIED: forum/updates/version.yaml:1-13]
|
||||
|
||||
| Table | Columns / notes |
|
||||
|-------|-----------------|
|
||||
| `golem15_forum_categories` | id, context_type varchar(255) **NOT NULL**, context_id uint, slug, title, description text nullable, sort_order default 0, timestamps, deleted_at; index `(context_type, context_id)` `forum_categories_context_index`; unique `(context_type, context_id, slug)` `forum_categories_owner_slug_unique`; **no FK** |
|
||||
| `golem15_forum_threads` | id, category_id FK categories cascade, user_id nullable FK users **set null**, title, is_pinned default false, is_locked default false, last_post_at, post_count default 0, timestamps, deleted_at, deleted_by nullable **no FK**, deletion_reason nullable; indexes (category_id, last_post_at), user_id |
|
||||
| `golem15_forum_posts` | id, thread_id FK threads cascade, user_id nullable FK users set null, content text, content_html text nullable, timestamps, deleted_at, deleted_by nullable no FK, deletion_reason nullable, edited_at nullable; indexes (thread_id, created_at), user_id |
|
||||
| `golem15_forum_thread_reads` | id, user_id FK users cascade `forum_thread_reads_user_foreign`, thread_id FK threads cascade `forum_thread_reads_thread_foreign`, last_read_at, timestamps; unique `(user_id, thread_id)` `forum_thread_reads_user_thread_unique`; index `(thread_id, user_id)` |
|
||||
|
||||
deletion_reason: `'self' \| 'moderator' \| NULL` [VERIFIED: add_deletion_marker_to_forum_posts_and_threads.php:16-17].
|
||||
|
||||
### herald schema (v1.0.0)
|
||||
|
||||
[VERIFIED: herald/updates/create_push_subscriptions_table.php:20-53]
|
||||
|
||||
| Table | Columns / notes |
|
||||
|-------|-----------------|
|
||||
| `golem15_herald_push_subscriptions` | id, user_id, endpoint varchar(500), keys text (JSON `{p256dh, auth}`), user_agent varchar(500) nullable, subscribed_at, last_used_at nullable, timestamps; unique `(user_id, endpoint)` `herald_push_user_endpoint_unique`; index user_id `herald_push_user_id_idx`; FK user_id → users cascade **only if `users` table exists** |
|
||||
|
||||
Config [VERIFIED: herald/config/herald.php:13-16]: `vapid.subject` env `VAPID_SUBJECT` default `mailto:admin@example.com`, `vapid.public_key` `VAPID_PUBLIC_KEY`, `vapid.private_key` `VAPID_PRIVATE_KEY`. Go keys: `golem15.herald.vapid.{subject,public_key,private_key}` (D-12). Command `herald:vapid-generate` [VERIFIED: GenerateVapidKeys.php:18] prints `VAPID_PUBLIC_KEY=` / `VAPID_PRIVATE_KEY=` lines.
|
||||
|
||||
### userfriends schema (v1.1.0 final)
|
||||
|
||||
[VERIFIED: userfriends/updates/version.yaml] Demo seed v1.0.1 — record ID, no rows.
|
||||
|
||||
| Table | Columns / notes |
|
||||
|-------|-----------------|
|
||||
| `golem15_userfriends_contexts` | `id` unsigned PK; insert id=1 [VERIFIED: create_friendships_tables.php:12-16] — FriendContext singleton row |
|
||||
| `golem15_userfriends_friendships` | id, requester_id FK users cascade, recipient_id FK users cascade, status varchar(20) default `pending`, timestamps; unique (requester_id, recipient_id); indexes (recipient_id, status), (requester_id, status) |
|
||||
| `golem15_userfriends_groups` | id, owner_id FK users cascade, name, description text nullable, is_open bool default false, soft deletes, timestamps; index owner_id |
|
||||
| `golem15_userfriends_group_members` | id, group_id FK groups cascade, user_id FK users cascade, role varchar(20) default `member`, timestamps; unique (group_id, user_id) `uf_group_member_unique`; index (group_id, role) `uf_group_member_role_idx` |
|
||||
| `golem15_userfriends_group_invites` | id, group_id FK cascade, invite_code varchar(64) nullable unique, created_by nullable, timestamps; unique group_id `uf_group_invite_group_unique` |
|
||||
| `golem15_userfriends_group_bans` | id, group_id FK cascade, user_id FK cascade, banned_by nullable, status varchar(20) default `banned`, timestamps; unique (group_id, user_id) `uf_group_ban_unique` |
|
||||
|
||||
Constants [VERIFIED: Friendship.php:15-17] `pending`/`accepted`/`declined`; [VERIFIED: GroupMember.php:17-19] `owner`/`admin`/`member`; GroupBan `banned`.
|
||||
|
||||
### Rate buckets (D-05)
|
||||
|
||||
| Bucket | Limit | Applies to |
|
||||
|--------|-------|------------|
|
||||
| `video-initiate` | 5/min per user id or IP [VERIFIED: chatvideo/routes.php:11-13] | generic video **start** (even though PHP only throttled dropped `/api/video/initiate`) |
|
||||
| `community-join` | 10/min [VERIFIED: wavepath/routes.php:76-78] | friend request; group join/preview if in generic API |
|
||||
| `call-link` | 10/min [VERIFIED: wavepath/routes.php:86-88] | GET call-links/{code} |
|
||||
| general per API | analog chat general bucket | remaining routes |
|
||||
| Guest call-* buckets | Wavepath host | **out of scope** (D-07 / deferred `/_wave`) |
|
||||
|
||||
### Commands
|
||||
|
||||
| PHP | Go |
|
||||
|-----|-----|
|
||||
| `herald:vapid-generate` | `summer herald:vapid-generate` |
|
||||
| (none in chatvideo plugin) | `summer chatvideo:reconcile-sessions` + conga periodic 5 min |
|
||||
| `wave:reconcile-video-sessions` | Wavepath port thin alias later; not this phase |
|
||||
|
||||
## Discretion recommendations (planner defaults)
|
||||
|
||||
| Item | Recommendation |
|
||||
|------|----------------|
|
||||
| Packages / IDs | `chatbots`, `chatvideo`, `forum`, `herald`, `userfriends`; IDs `golem15.<name>` |
|
||||
| userfriends events | `userfriends.GroupCreated`, `GroupDeleted`, `MemberJoined`, `MemberLeft` (fields: GroupID, ActorID, MemberID) |
|
||||
| forum DeleteContext | `ForumService.DeleteContext(ctx, type, id)` soft-deletes categories (and cascaded threads/posts) for that morph pair |
|
||||
| Morph rewrite | forum ships `forum:rewrite-context-types`; do not require chat |
|
||||
| chatvideo paths | `/_chatvideo/api/v1/conversations/{id}/video/{start,join,leave,end,status}` + `/_chatvideo/api/v1/call-links/{code}` |
|
||||
| forum paths | `/_forum/api/v1/contexts/{type}/{id}/categories` + threads/posts/read/unread as D-05 |
|
||||
| userfriends paths | `/_userfriends/api/v1/friends` (list, requests, sent, request, accept, reject, cancel, unfriend) + `/groups` (members, invites, bans) |
|
||||
| herald paths | `POST/PATCH/DELETE /_herald/api/v1/push/subscription`, `GET /_herald/api/v1/push/vapid-public-key` |
|
||||
| chatbots paths | `GET /_chatbots/api/v1/commands` (`isAvailable` filter), `POST /_chatbots/api/v1/dispatch` body `{input}` actor from JWT |
|
||||
| Herald job | Kind `golem15.herald.send`, queue `default` or `push`, enqueue in caller Tx |
|
||||
| Video job | Kind `golem15.chatvideo.reconcile_sessions`, periodic 5 min, MaxAttempts 1 |
|
||||
| LiveKit wrapper | interface `RoomAPI` { CreateRoom, DeleteRoom, RemoveParticipant, ParticipantCount, ActiveRoomNames }; SDK client in prod; fake in tests |
|
||||
| Token mint | Prefer jwt/v5 MapClaims copying PHP if SDK golden fails; still import official SDK for RoomService |
|
||||
| D-17 consume | `Render` + `Purify`; forum never vendors goldmark/bluemonday |
|
||||
| Lang | EN/PL phrasebook; plugin.name `Chat bots` / `Forum` / `Herald` / `Friends` / `Chat video` — never Wavepath/Fonoteka |
|
||||
| sm-user-plugin | **no edit** (events already exist) |
|
||||
| summercms.go | **only** if D-17 is missing (not this phase) or a lighthouse/fetchguard/conga/festival gap appears — none found for satellites |
|
||||
| Repo create | `tea repos create --owner golem15 sm-<name>-plugin` then analog go.mod/replace |
|
||||
| Plan split (present and wait) | **6 plans:** (1) userfriends repo + baseline + models + services + events + generic API tracer; (2) forum repo + baseline + models + ForumService + DeleteContext + generic API tracer (**blocks on D-17**); (3) chatbots repo + flags + registry/dispatcher + generic API tracer; (4) **webpush-go decision note** then herald repo + subscriptions + sender job + generic push tracer; (5) **LiveKit + (if not earlier) bluemonday decision notes** then chatvideo repo + sessions + wrapper + authorizer + reconciler + generic video tracer; (6) unit tests + goldens + PHP maps + `scripts/check-phase19.1.sh`. First planning commit also patches ROADMAP Repos + SC2 + SC3. Bluemonday note may land at the start of plan 2 if D-17 has not already written it; forum still must not vendor the sanitizer. |
|
||||
| Security-review agent | Run once after plan 5 (APIs + push + tokens + HTML + GDPR all exist) or per CONTEXT at phase start of execute |
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
|
||||
- Frozen PHP trees under `/media/nvme/dev/jz/wavepath.org/plugins/golem15/{chatbots,chatvideo,forum,herald,userfriends}` (rev-parse + Read this session)
|
||||
- Wavepath `routes.php` (rate buckets, friends/video/forum/push/command prefixes), `console/ReconcileVideoSessions.php`, `classes/video/CommunityVideoService.php`
|
||||
- Analog: `sm-chat-plugin/{plugin.go,classes/authorizer.go,http_tracer_test.go}`, `sm-chatchannels-plugin/classes/deny_all.go`
|
||||
- Framework: `modules/{lighthouse/channel.go,fetchguard/client.go,fetchguard/policy.go,cabana/field_markdown.go,seaglass/seaglass.go,flare/commands.go,flare/README.md}`
|
||||
- `fonoteka.go/plugins/golem15/user/classes/events.go` (`user.Created` / `user.Deleting`)
|
||||
- `.planning/notes/{core-plugins-own-repos.md,plugin-layout-winter-directories.md}`
|
||||
- `.planning/research/STACK.md`, `go.mod` (goldmark, jwt/v5, bluemonday)
|
||||
- Fonoteka PHP userfriends tree: 32 files, `diff -rq` empty vs Wavepath, `Plugin.php` identical
|
||||
- bluemonday v1.0.27 `policy.go` (RequireNoFollowOnLinks, AddTargetBlankToFullyQualifiedLinks, AllowURLSchemes) Read from module cache
|
||||
- goldmark v1.8.6 `extension/gfm.go` Read from module cache
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
|
||||
- webpush-go v1.4.0 source [CITED: https://raw.githubusercontent.com/SherClockHolmes/webpush-go/v1.4.0/webpush.go] — classify-confidence webfetch=LOW; claims cross-checked against fetchguard.Do
|
||||
- LiveKit server-sdk-go README [CITED: https://github.com/livekit/server-sdk-go] Token creation + RoomServiceClient; v2.19.0 go.mod transitive list from proxy.golang.org
|
||||
- pkg.go.dev webpush-go@v1.4.0 (MIT, jwt/v5, x/crypto)
|
||||
- ASVS 5.0 V5 / V13 [CITED: https://github.com/OWASP/ASVS]
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
|
||||
- Live Wavepath MySQL `wave2` (this session: Access denied)
|
||||
- livekit `auth.AccessToken` claim JSON vs PHP firebase payload (needs golden)
|
||||
- `package-legitimacy check --ecosystem crates` on Go import paths (`SLOP`/`does-not-exist` — wrong ecosystem, ignore)
|
||||
|
||||
## Metadata
|
||||
|
||||
**Research scope:**
|
||||
- Core technology: five WinterCMS plugins → compiled `sm-*-plugin` repos
|
||||
- Ecosystem: webpush-go, livekit/server-sdk-go/v2, bluemonday, goldmark, fetchguard, lighthouse, conga, festival
|
||||
- Patterns: P19 analog layout, deny-all seams, generic `/_x/api/v1`, squashed baselines, tracer-first
|
||||
- Pitfalls: ChannelID segment, flare vs D-09, seaglass vs forum policy, D-17 missing, LiveKit transitives, Wavepath-only reconciler extras
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH for locked names and in-repo versions; MEDIUM for livekit/webpush-go version pins (CITED); LOW webfetch seam
|
||||
- Architecture: HIGH — PHP services/migrations and analog plugins Read this session
|
||||
- Pitfalls: HIGH for ChannelID, D-14 scope, D-17, seaglass, Golem, EndpointGuard
|
||||
- Code examples: HIGH for PHP quotes; MEDIUM for Go SDK mint until golden
|
||||
|
||||
**Research date:** 2026-10-17
|
||||
**Valid until:** 2026-11-16 (30 days — LiveKit v2 moves fast; re-check v2.19.0 at execute)
|
||||
|
||||
---
|
||||
|
||||
*Phase: 19.1-chat-satellites*
|
||||
*Research completed: 2026-10-17*
|
||||
*Ready for planning: yes (forum execute blocked on D-17; planning may proceed)*
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user