diff --git a/.planning/phases/19.1-chat-satellites/19.1-04-SUMMARY.md b/.planning/phases/19.1-chat-satellites/19.1-04-SUMMARY.md new file mode 100644 index 00000000..37cb8662 --- /dev/null +++ b/.planning/phases/19.1-chat-satellites/19.1-04-SUMMARY.md @@ -0,0 +1,227 @@ +--- +phase: 19.1-chat-satellites +plan: 04 +subsystem: herald +tags: [herald, webpush, vapid, fetchguard, postgres, gorm, testcontainers, jwt, conga] + +# Dependency graph +requires: + - phase: 19 + provides: analog sibling-plugin layout (boot steps, IF NOT EXISTS baseline, jwt.auth generic API, test-only proof host) + - phase: 19.1 + provides: plan 01 tea-create / proceed-and-push remote policy + - phase: 13 + provides: fetchguard PublicOnlyMode and WithTransport +provides: + - "decision note .planning/notes/2026-10-18-webpush-go.md pinning github.com/SherClockHolmes/webpush-go v1.4.0" + - "new repository sm-herald-plugin (module git.golem15.com/golem15/sm-herald-plugin, package herald, plugin golem15.herald), pushed to git.golem15.com at 46af07a" + - "idempotent baseline 202610180001_create_golem15_herald_baseline (golem15_herald_push_subscriptions, unique (user_id, endpoint) herald_push_user_endpoint_unique, FK user_id cascade)" + - "PublicOnly subscribe/send, transactional Enqueue Kind golem15.herald.send, PresenceGate DenyAll, NeutralHeraldProfile, herald:vapid-generate, generic /_herald/api/v1 push API" +affects: [19.1-06] + +actuals: + tokens: 29450 + tasks: 4 + commits: 3 +plan_head_before: f3ca32e486abbd3d46421a83d0bea0e50627078a +plan_head_after: 56b4a72a6a7b27eb47a29d9e4bbd7140b2287874 + +tech-stack: + added: [github.com/SherClockHolmes/webpush-go v1.4.0] + patterns: + - "Options.HTTPClient = *fetchguard.Client Do directly; tests inject fetchguard.WithTransport; no adapter / extra Doer / EndpointGuard" + - "SendEvent and SendUnconditional Enqueue Kind golem15.herald.send on the caller tx; worker consults PresenceGate only when Gated; 404/410 prune" + - "Generic herald API: actor from JWT; POST/PATCH/DELETE /push/subscription; GET /push/vapid-public-key public key only" + +key-files: + created: + - .planning/notes/2026-10-18-webpush-go.md + - ../sm-herald-plugin/plugin.go + - ../sm-herald-plugin/boot.go + - ../sm-herald-plugin/routes.go + - ../sm-herald-plugin/classes/sender.go + - ../sm-herald-plugin/classes/subscription_service.go + - ../sm-herald-plugin/classes/deny_all.go + - ../sm-herald-plugin/classes/neutral_profile.go + - ../sm-herald-plugin/classes/payload.go + - ../sm-herald-plugin/contracts/presence_gate.go + - ../sm-herald-plugin/contracts/herald_profile.go + - ../sm-herald-plugin/console/vapid_generate.go + - ../sm-herald-plugin/jobs/send.go + - ../sm-herald-plugin/api/push.go + - ../sm-herald-plugin/updates/202610180001_create_golem15_herald_baseline.go + - ../sm-herald-plugin/http_tracer_test.go + - ../sm-herald-plugin/README.md + modified: [] + +key-decisions: + - "Task 2 reply verbatim: proceed" + - "Locked D-02 baseline id 202610180001_create_golem15_herald_baseline and D-05 prefix /_herald/api/v1; remote policy follows plan 01 proceed-and-push" + - "tea repos create --owner golem15 --name sm-herald-plugin succeeded; main pushed at 46af07a (never force). summercms.go was not pushed." + +patterns-established: + - "herald proof host: golem15.user + golem15.herald + a test-only acme fixture; TestFixtureAbsentFromProduction keeps the fixture id and retired tokens out of production sources" + +requirements-completed: [SC-19.1-1, SC-19.1-2, D-04, D-05, D-06, D-09, D-10, D-11, D-12] + +coverage: + - id: D1 + description: "webpush-go v1.4.0 decision note before any herald import (D-09); MIT, jwt/v5 share, Options.HTTPClient = *fetchguard.Client Do; in-tree flare rejected" + requirement: D-09 + verification: + - kind: other + ref: "test -f .planning/notes/2026-10-18-webpush-go.md && grep SherClockHolmes/webpush-go and v1.4.0" + status: pass + human_judgment: false + - id: D2 + description: "Human lock of D-02 baseline id 202610180001_create_golem15_herald_baseline and D-05 prefix /_herald/api/v1 before the sibling repo existed" + requirement: D-05 + verification: [] + human_judgment: true + rationale: "Task 2 was checkpoint:decision gate=blocking-human; the user replied proceed and that lock cannot be asserted by a unit test" + - id: D3 + description: "sm-herald-plugin with subscribe HTTPS PublicOnly, SendUnconditional after commit, 410 prune, rollback sends 0" + requirement: D-10 + verification: + - kind: unit + ref: "go -C ../sm-herald-plugin test -count=1 -short ./... # TestSubscribeEndpoint" + status: pass + - kind: integration + ref: "../sm-herald-plugin/http_tracer_test.go#TestHTTPTracer" + status: pass + - kind: integration + ref: "../sm-herald-plugin/http_tracer_test.go#TestSendEventRollback" + status: pass + human_judgment: false + - id: D4 + description: "PresenceGate DenyAll with Boot Warn, NeutralHeraldProfile without Warn, herald:vapid-generate env lines, PATCH/DELETE /push/subscription and GET /push/vapid-public-key" + requirement: D-12 + verification: + - kind: unit + ref: "go -C ../sm-herald-plugin vet ./... && go -C ../sm-herald-plugin test -count=1 -short ./..." + status: pass + - kind: other + ref: "grep herald:vapid-generate, golem15.herald.vapid, ShouldSend; grep modules/flare|websockets:generate-vapid-keys is 0" + status: pass + human_judgment: false + - id: D5 + description: "Gitea remote golem15/sm-herald-plugin created and main pushed (never force)" + requirement: SC-19.1-1 + verification: + - kind: other + ref: "tea repos create --owner golem15 --name sm-herald-plugin; git push -u origin main" + status: pass + human_judgment: false + +duration: 11min +completed: 2026-10-18 +status: complete +--- + +# Phase 19.1 Plan 04: Herald repo, PublicOnly send, vapid-generate Summary + +**Sibling `sm-herald-plugin` with the webpush-go v1.4.0 note, IF NOT EXISTS subscriptions baseline, fetchguard PublicOnly send on a caller-tx conga job, deny-all PresenceGate, `herald:vapid-generate`, and a JWT generic `/_herald/api/v1` push API.** + +## Performance + +- **Duration:** 11 min +- **Started:** 2026-10-18T11:37:31Z +- **Completed:** 2026-10-18T11:48:50Z +- **Tasks:** 4 +- **Files modified:** 35 (1 note in summercms.go; 34 in the sibling plugin) + +## Task 2 human reply (verbatim) + +proceed + +Meaning recorded at resume: lock D-02 baseline id `202610180001_create_golem15_herald_baseline` and D-05 prefix `/_herald/api/v1`; implement; remote policy follows plan 01 proceed-and-push. After Task 4 is green, tea-create the remote and push main (never force). summercms.go is not pushed. Task 2 was not re-asked. + +## Accomplishments + +- Decision note `.planning/notes/2026-10-18-webpush-go.md` (hashids shape) pins `github.com/SherClockHolmes/webpush-go` v1.4.0 (MIT, jwt/v5 share, `Options.HTTPClient` is the Do interface so `*fetchguard.Client` is passed directly). In-tree `modules/flare` is the rejected alternative (`push.*`, `websockets:generate-vapid-keys`). No keys generated in Task 1. `go get` ran only after the note was committed. +- New independent git repo `../sm-herald-plugin` (not a submodule): module `git.golem15.com/golem15/sm-herald-plugin`, package `herald`, PluginID `golem15.herald`, `Requires()` empty. Display name Herald / Herald. Never imports user models, `modules/flare`, or `websockets:generate-vapid-keys`. +- Idempotent baseline `202610180001_create_golem15_herald_baseline` for `golem15_herald_push_subscriptions` (`endpoint` varchar 500, `keys` text JSON p256dh/auth, unique `(user_id, endpoint)` `herald_push_user_endpoint_unique`, `user_id` FK cascade). +- `classes.SubscriptionService.Subscribe` rejects non-HTTPS and non-public hosts (D-10). No EndpointGuard type. Send uses `fetchguard.NewClient` `PublicOnlyMode`; `Options.HTTPClient = *fetchguard.Client`. TTL 2419200, urgency normal. 404/410 prune. +- `SendEvent` and `SendUnconditional` Enqueue `jobs.SendArgs` Kind `golem15.herald.send` on the caller tx (D-11). Rollback sends 0. Worker consults `PresenceGate.ShouldSend` only when `Gated`. OBS-01 process-local `sync.Map` 60s. +- PresenceGate default `classes.DenyAll` (`ShouldSend` always false). `SetPresenceGate(nil)` becomes DenyAll. Boot Warns while DenyAll remains. HeraldProfile default `classes.NeutralHeraldProfile` (app name only); no Warn. +- Config keys `golem15.herald.vapid.{subject,public_key,private_key}`. PHP env names overlay empty values at Boot. Boot Warns when keys are missing; never auto-generates. `summer herald:vapid-generate` prints `VAPID_PUBLIC_KEY=` then `VAPID_PRIVATE_KEY=`. Private key is never slog'd. `GET /push/vapid-public-key` returns `{"publicKey"}`. +- Generic API `herald.APIPrefix` (`/_herald/api/v1`) behind `jwt.auth` and `herald-api` 120/min (D-05, D-06): `POST/PATCH/DELETE /push/subscription`, `GET /push/vapid-public-key`. Actor from the JWT, never the body. +- `TestSubscribeEndpoint` (short): http and private-IP rejected. `TestHTTPTracer` (Docker): HTTPS subscribe, SendUnconditional after commit hits the fake, 410 deletes the row. `TestSendEventRollback` (Docker): enqueue in a rolled-back tx; fake send count stays 0. Tests inject `fetchguard.WithTransport`. +- Remote created: `https://git.golem15.com/golem15/sm-herald-plugin`. `git push -u origin main` succeeded (never force). summercms.go was not pushed. + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: webpush-go decision note before any herald import** - `56b4a72` (docs, summercms.go) +2. **Task 2: Confirm D-02 herald baseline id and D-05 prefix** - none (decision only; human: proceed) +3. **Task 3: End-to-end push — subscribe https, SendUnconditional after commit, 410 prune, rollback sends 0** - `f20baac` (feat, sm-herald-plugin) +4. **Task 4: PresenceGate, NeutralHeraldProfile, vapid-generate, remaining push routes** - `46af07a` (feat, sm-herald-plugin) + +**Sibling HEAD:** `46af07a` on `origin/main`. + +## Files Created/Modified + +- `.planning/notes/2026-10-18-webpush-go.md` - D-09 pin of webpush-go v1.4.0 before import +- `../sm-herald-plugin/` - full plugin tree (go.mod Go 1.27.0, analog pins, replace summercms => ../summercms.go) + +## Decisions Made + +- Task 2 reply verbatim: proceed. +- Locked D-02 / D-05 as named in the continuation. Remote policy follows plan 01 proceed-and-push. +- After Task 4 verify was green, `tea repos create --owner golem15 --name sm-herald-plugin` then `git push -u origin main` (never force). + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] PublicOnlyMode lives in classes.NewSender** +- **Found during:** Task 3 +- **Issue:** Acceptance `grep PublicOnlyMode ../sm-herald-plugin/classes/*.go` failed while `NewClient` lived in `boot.go`. +- **Fix:** `classes.NewSender` builds the PublicOnly `fetchguard.Client` when HTTP is nil. +- **Files modified:** `../sm-herald-plugin/classes/sender.go`, `../sm-herald-plugin/boot.go` +- **Verification:** grep PublicOnlyMode in classes/*.go prints at least one line; `go test -short` pass +- **Committed in:** `f20baac` + +**2. [Rule 3 - Blocking] analog pins after go mod tidy** +- **Found during:** Task 3 +- **Issue:** `go mod tidy` pinned summercms v0.1.9 / sm-user-plugin v0.0.0. +- **Fix:** `go get` summercms@v0.2.2, sm-user-plugin@v0.1.1, pgx@v5.10.0; replace summercms => ../summercms.go and user => ../fonoteka.go/plugins/golem15/user. +- **Files modified:** `../sm-herald-plugin/go.mod`, `../sm-herald-plugin/go.sum` +- **Verification:** `go vet ./...` and `go test -short ./...` pass +- **Committed in:** `f20baac` + +--- + +**Total deviations:** 2 auto-fixed (Rule 3 blocking) +**Impact on plan:** Required so the PublicOnly grep and analog pins match the sibling layout. No scope creep. + +## Issues Encountered + +The environment injects a `Co-authored-by: Cursor` trailer on commits; it was not added in the commit message source. Task 2 was not re-asked. The sibling directory did not exist at Task 3 start. + +## TDD Gate Compliance + +N/A — TDD is not applicable for this plan. Tracer verify is automated-only (`HUMAN_VERIFY_MODE=end-of-phase`). After Task 3 automated verify passed: ⚡ Tracer verified end-to-end — expanding. + +## Auth gates + +Task 2 `checkpoint:decision` `blocking-human` — user replied `proceed`. No further auth gates. + +## Known Stubs + +None. + +## User Setup Required + +None - no external service configuration required. The Gitea repo was created in-session. Hosts still paste `herald:vapid-generate` env lines into config (never auto-generated). + +## Next Phase Readiness + +Ready for 19.1-05 / 19.1-06 on their own gates. Herald tracer is green; PHP method map and goldens stay plan 06. + +--- +*Phase: 19.1-chat-satellites* +*Completed: 2026-10-18* + +## Self-Check: PASSED