docs(11-04): complete flare Web Push and websockets commands plan
This commit is contained in:
@@ -0,0 +1,308 @@
|
|||||||
|
---
|
||||||
|
phase: 11-jobs-realtime-and-search-infrastructure
|
||||||
|
plan: 04
|
||||||
|
subsystem: realtime
|
||||||
|
tags: [web-push, vapid, rfc8291, rfc8292, aes128gcm, centrifugo, cli, bonfire]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 11-jobs-realtime-and-search-infrastructure
|
||||||
|
provides: "11-03 centrifugo.Config/LoadConfig, Client (Enabled, DebugInfo, post with apikey header), ErrNotConfigured"
|
||||||
|
- phase: 11-jobs-realtime-and-search-infrastructure
|
||||||
|
provides: "11-05 fonoteka plugin.go and README layout (edited after it)"
|
||||||
|
provides:
|
||||||
|
- "modules/flare: Pusher, Subscription, SendOptions, SubscriptionSource, SubscriptionInfo, Service/From, VAPIDPusher, Encrypt (RFC 8291), VAPIDHeader/GenerateVAPIDKeys/ParseVAPIDKeys (RFC 8292), HostAllowed allowlist"
|
||||||
|
- "websockets:generate-vapid-keys [--update] [--show-current] and websockets:test-push <user_id> [--show-config] (flare.Commands)"
|
||||||
|
- "centrifugo.Client.Info and websockets:health (centrifugo.Commands)"
|
||||||
|
- "compass.Config.Persist keeps keys saved earlier in overrides.yaml"
|
||||||
|
- "fonoteka.go: the three websockets commands in the binary, config/push.yaml with push disabled, PUSH_* env mapping"
|
||||||
|
affects: [11-07 unit tests, a future app plugin that stores push subscriptions, 15 cutover env mapping]
|
||||||
|
|
||||||
|
actuals:
|
||||||
|
tokens: 22995
|
||||||
|
tasks: 2
|
||||||
|
commits: 3
|
||||||
|
plan_head_before: a8305a015ddb32a401655a8df2168f64e12a9959
|
||||||
|
plan_head_after: f55cb444ab32eefb0ad25ea1c89077ec1281dc71
|
||||||
|
# fonoteka.go (separate repository) received 1 more commit: cdb87d9
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Web Push is hand-rolled on stdlib crypto (crypto/ecdh, crypto/hkdf, AES-GCM) plus golang-jwt ES256; no push library"
|
||||||
|
- "Outbound requests to user-supplied URLs: https only, host allowlist checked before dialing, redirects never followed, errors name the host not the URL"
|
||||||
|
- "Commands print their PHP-shaped lines through bonfire.Output and return a short '<command>: failed' error so the binary exits 1 without repeating the message"
|
||||||
|
- "App-provided data seams for operator commands: the app publishes an interface (flare.SubscriptionSource) and the command looks it up"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- modules/flare/flare.go
|
||||||
|
- modules/flare/vapid.go
|
||||||
|
- modules/flare/encrypt.go
|
||||||
|
- modules/flare/commands.go
|
||||||
|
- modules/flare/encrypt_test.go
|
||||||
|
- modules/flare/send_test.go
|
||||||
|
- modules/flare/commands_test.go
|
||||||
|
- modules/flare/README.md
|
||||||
|
- modules/lighthouse/centrifugo/commands.go
|
||||||
|
- modules/lighthouse/centrifugo/commands_test.go
|
||||||
|
- ../fonoteka.go/config/push.yaml
|
||||||
|
modified:
|
||||||
|
- README.md
|
||||||
|
- modules/lighthouse/centrifugo/client.go
|
||||||
|
- modules/lighthouse/README.md
|
||||||
|
- modules/compass/persist.go
|
||||||
|
- modules/compass/persist_test.go
|
||||||
|
- modules/compass/README.md
|
||||||
|
- ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||||
|
- ../fonoteka.go/README.md
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "flare's VAPID driver never follows redirects (CheckRedirect returns ErrUseLastResponse on a copy of any injected client), so a push service cannot bounce a request to a host outside push.allowed_hosts"
|
||||||
|
- "compass.Config.Persist now merges runtime values over the existing overrides.yaml instead of replacing it; otherwise websockets:generate-vapid-keys --update would silently wipe every other persisted override"
|
||||||
|
- "websockets:health calls Centrifugo's info API method as the connectivity probe; unlike publishing, a 200 answer with an error body fails the check"
|
||||||
|
- "Configured keys shorter than 20 characters are shown as '...' only, because first 8 + last 4 would reveal most of the value"
|
||||||
|
- "websockets:test-push takes no user name from the source (SubscriptionSource returns subscriptions only), so it prints 'Testing push for user ID <n>'; the PHP notification-icon block is not ported because it reads another plugin's config"
|
||||||
|
- "Command failures print their lines through Output and return '<command>: failed' (or ': check failed'); main prints that one line and exits 1"
|
||||||
|
- "push.subject defaults to empty, not the PHP app's mailto address, as the plan specifies; VAPIDHeader refuses a subject that is not mailto: or https:"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "HostAllowed: exact host or '*.suffix' for any subdomain (never the apex), case-insensitive, trailing dot ignored"
|
||||||
|
- "Secrets in config structs: String, GoString and LogValue redact the private key"
|
||||||
|
|
||||||
|
requirements-completed: [RT-01]
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
- id: D1
|
||||||
|
description: "RFC 8291 aes128gcm encryption reproduces the Appendix A body, header, ciphertext, CEK and nonce byte for byte (values copied from the RFC text); Encrypt round-trips and refuses payloads above 3993 bytes"
|
||||||
|
requirement: RT-01
|
||||||
|
verification:
|
||||||
|
- kind: unit
|
||||||
|
ref: "modules/flare/encrypt_test.go#TestRFC8291AppendixA"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D2
|
||||||
|
description: "VAPID send: an httptest TLS push service verifies the ES256 JWT with the key from k=, checks aud (origin), exp within 24h, sub, typ/alg, TTL, Content-Encoding, Content-Type, Urgency and Topic, and decrypts the body to the payload; 404/410 map to ErrSubscriptionGone and 500 to StatusError without the body"
|
||||||
|
requirement: RT-01
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "modules/flare/send_test.go#TestVAPIDSendRoundTrip"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D3
|
||||||
|
description: "Endpoint allowlist: http, unlisted host, foreign host, user info, relative, other scheme, empty host and host-suffix tricks are refused with ErrEndpointNotAllowed before any request; redirects are not followed; a disabled pusher sends nothing; errors never expose the endpoint path; default allowlist table"
|
||||||
|
requirement: RT-01
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "modules/flare/send_test.go#TestSendRefusesDisallowedEndpoint"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D4
|
||||||
|
description: "websockets:generate-vapid-keys prints an 87/43-char pair that ParseVAPIDKeys accepts with SUMMER_PUSH__* lines; --show-current prints only first8...last4 with lengths and check marks; --update persists both keys (reloaded config returns them, earlier override kept, file mode 0600)"
|
||||||
|
requirement: RT-01
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "modules/flare/commands_test.go#TestGenerateVAPIDKeysCommand"
|
||||||
|
status: pass
|
||||||
|
- kind: unit
|
||||||
|
ref: "modules/compass/persist_test.go#TestPersistKeepsEarlierOverrides"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D5
|
||||||
|
description: "websockets:test-push: no source prints 'no subscription source registered' and exits 1; unknown user 'User 5 not found'; no subscriptions; push disabled lists subscriptions but sends nothing; a working source sends one encrypted push per subscription with the PHP-shaped payload; a failed send exits 1 after attempting all; the private key never appears"
|
||||||
|
requirement: RT-01
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "modules/flare/commands_test.go#TestTestPushCommand"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D6
|
||||||
|
description: "websockets:health: empty api_key prints 'Centrifugo not configured (API key missing)' and exits 1 with no request; /info 200 prints Configuration OK and the API URL/Enabled/API Key Set table; 500, an error body and an unreachable server print 'Connection check failed:' and exit 1; the API key is never printed"
|
||||||
|
requirement: RT-01
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "modules/lighthouse/centrifugo/commands_test.go#TestHealthCommand"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D7
|
||||||
|
description: "fonoteka binary registers the three commands and, with the committed empty api_key, websockets:health prints the not-configured line and exits 1"
|
||||||
|
requirement: RT-01
|
||||||
|
verification:
|
||||||
|
- kind: e2e
|
||||||
|
ref: "cd ../fonoteka.go && SUMMER_GOLEM15__USER__JWT__SECRET=test-only-cli-secret go run . websockets:health 2>&1 | grep -q 'Centrifugo not configured (API key missing)'"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D8
|
||||||
|
description: "A real browser subscription receives a push from the VAPID driver (FCM or Mozilla autopush)"
|
||||||
|
verification: []
|
||||||
|
human_judgment: true
|
||||||
|
rationale: "Needs a live push service and a browser; Płytarium has no subscription store, so this can only be exercised by an app that publishes a SubscriptionSource"
|
||||||
|
|
||||||
|
duration: 17min
|
||||||
|
completed: 2026-09-30
|
||||||
|
status: complete
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 11 Plan 04: flare Web Push and the websockets console commands Summary
|
||||||
|
|
||||||
|
**A new framework package, `flare`, sends Web Push with a standard-library VAPID driver: RFC 8291 aes128gcm encryption that matches the RFC's Appendix A vector byte for byte, and an RFC 8292 ES256 `vapid t=…, k=…` header. It sends only to https endpoints on a push-service allowlist and never follows redirects. The three WinterCMS websockets commands (`websockets:health`, `websockets:generate-vapid-keys`, `websockets:test-push`) now run from the fonoteka binary, and none of them prints a configured secret.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 17 min
|
||||||
|
- **Started:** 2026-09-30T11:35:45Z
|
||||||
|
- **Completed:** 2026-09-30T11:53:01Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 19 (16 in summercms.go, 3 in fonoteka.go)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- **flare core (D-15, user decision 3).**
|
||||||
|
- Types: `Pusher`, `Subscription`, `SendOptions`, `SubscriptionSource` and `SubscriptionInfo`.
|
||||||
|
- Errors: `ErrPushDisabled`, `ErrEndpointNotAllowed`, `ErrSubscriptionGone`, `ErrUserNotFound` and `StatusError`.
|
||||||
|
- `From(app)` builds the service from `push.*` and publishes it. It reads `enabled`, `public_key`, `private_key` and `subject`, plus `ttl` (default 2419200 s) and `allowed_hosts` (default FCM, Mozilla autopush, `*.push.apple.com` and `*.notify.windows.com`).
|
||||||
|
- **RFC 8291.** `Encrypt` uses an ephemeral P-256 key through `crypto/ecdh` and the HKDF-SHA-256 key schedule through `crypto/hkdf`. It writes one AES-128-GCM record with the 0x02 delimiter, and the header is salt, rs 4096, idlen 65 and the key id. Payloads over 3993 bytes are refused. `TestRFC8291AppendixA` compares the output against the published body, header, ciphertext, CEK and nonce, with values copied verbatim from the RFC text. Changing the nonce label makes the test fail.
|
||||||
|
- **RFC 8292.**
|
||||||
|
- `GenerateVAPIDKeys` returns an unpadded base64url pair (87 and 43 characters).
|
||||||
|
- `ParseVAPIDKeys` accepts padded or unpadded input and checks that the public key matches the private key.
|
||||||
|
- `VAPIDHeader` signs an ES256 JWT (golang-jwt/v5) with `aud` set to the endpoint origin, `exp` 12 h ahead and a `sub` that must be mailto: or https:.
|
||||||
|
- **VAPIDPusher.**
|
||||||
|
- Refusals come first. It refuses while disabled. Before dialing, it refuses any endpoint that is not https, carries user info, or has a host outside the allowlist.
|
||||||
|
- It POSTs `TTL`, `Content-Encoding: aes128gcm` and `Content-Type: application/octet-stream`, the optional `Urgency` and `Topic`, and the VAPID `Authorization`. The request times out after 10 s.
|
||||||
|
- 2xx is success. 404 and 410 return `ErrSubscriptionGone`, and any other status returns `StatusError` without the body.
|
||||||
|
- It never follows redirects, and its errors name the host, never the endpoint path.
|
||||||
|
- **Commands.**
|
||||||
|
- `websockets:health` ports CentrifugoHealthCheck. It now calls the real `info` API through the new `Client.Info`, because PHP's `getDebugInfo` never contacted the server.
|
||||||
|
- `websockets:generate-vapid-keys` ports GenerateVapidKeys. Configured keys are truncated. `--show-current` stops after showing them. `--update` saves through compass `Set`/`Persist` to a 0600 overrides file. Without it, the command prints manual `SUMMER_PUSH__*` lines.
|
||||||
|
- `websockets:test-push` ports TestPushNotifications. It reads subscriptions through the app-published `SubscriptionSource` and asks a confirm prompt whose default is yes. It refuses to send while push is disabled and reports a result for each subscription.
|
||||||
|
- **fonoteka.go.**
|
||||||
|
- `Commands()` appends `centrifugo.Commands(p.app)` and `flare.Commands(p.app)`.
|
||||||
|
- `config/push.yaml` ships with push disabled.
|
||||||
|
- The README maps `PUSH_*` to `SUMMER_PUSH__*` and notes that Płytarium registers no `SubscriptionSource`.
|
||||||
|
- **Docs.**
|
||||||
|
- A new `modules/flare/README.md` follows the standard structure and gets a root modules row with the same summary sentence.
|
||||||
|
- The lighthouse README gains `Client.Info`, `Commands` and a CLI commands section.
|
||||||
|
- The compass README describes the new Persist merge.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
summercms.go:
|
||||||
|
1. **Task 1: VAPID driver, RFC 8291/8292, flare README and root row (tracer)**: `a9af0d7` (feat)
|
||||||
|
2. **Task 2 (prerequisite fix): compass Persist keeps earlier overrides**: `5fb22c2` (fix)
|
||||||
|
3. **Task 2: websockets:health, generate-vapid-keys and test-push**: `f55cb44` (feat)
|
||||||
|
|
||||||
|
fonoteka.go:
|
||||||
|
1. **Task 2: command registration, config/push.yaml, README**: `cdb87d9` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `modules/flare/flare.go`: types, errors, `Config`/`LoadConfig` (with redaction), `Service`/`From`, `VAPIDPusher`, `HostAllowed`
|
||||||
|
- `modules/flare/encrypt.go`: `Encrypt`, the RFC 8291 key schedule, base64url decoding
|
||||||
|
- `modules/flare/vapid.go`: `VAPIDKeys`, `GenerateVAPIDKeys`, `ParseVAPIDKeys`, `VAPIDHeader`, origin serialization
|
||||||
|
- `modules/flare/commands.go`: `Commands`, the two push commands and their name constants
|
||||||
|
- `modules/flare/*_test.go`: the RFC vector, the TLS push-service round trip with a test-side RFC 8291 decrypt, the allowlist refusals, and the command behaviour list
|
||||||
|
- `modules/lighthouse/centrifugo/client.go`: `Client.Info`
|
||||||
|
- `modules/lighthouse/centrifugo/commands.go`, `commands_test.go`: `Commands`, `HealthCommandName`, `websockets:health` and its tests
|
||||||
|
- `modules/compass/persist.go`, `persist_test.go`, `README.md`: Persist merges over the saved file
|
||||||
|
- `README.md`, `modules/flare/README.md`, `modules/lighthouse/README.md`: docs
|
||||||
|
- fonoteka.go: `plugins/golem15/fonoteka/plugin.go`, `config/push.yaml`, `README.md`
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
See `key-decisions` in the frontmatter.
|
||||||
|
|
||||||
|
## TDD Gate Compliance (Task 2)
|
||||||
|
|
||||||
|
- **RED:** The tests were written against stubs with the final signatures, where each command's Run returned nil and `Client.Info` returned an empty map. Every subtest of `TestGenerateVAPIDKeysCommand`, `TestTestPushCommand` and `TestHealthCommand` failed on its assertions, with no build or load errors. The compass test `TestPersistKeepsEarlierOverrides` failed with `app.name = "base", want the earlier persisted value`. `gsd-tools check tdd-red-evidence` returned `RED_EVIDENCE_OK` (`target_test_failed`) for all three records (TestTestPushCommand, TestHealthCommand, TestPersistKeepsEarlierOverrides).
|
||||||
|
- **GREEN:** `5fb22c2` and `f55cb44`. All subtests pass, including under `-race`.
|
||||||
|
- **Gate note:** there are no separate `test(11-04)` commits. The project requires `go vet` and `go test ./...` to be green at every commit, so tests and code landed together, as in plans 11-01 to 11-06.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 2 - Missing critical] compass Persist dropped earlier overrides**
|
||||||
|
- **Found during:** Task 2, while reading `compass/persist.go` before wiring `--update`
|
||||||
|
- **Issue:** `Persist` wrote only this process's runtime values. `websockets:generate-vapid-keys --update` would therefore replace `overrides.yaml` with the two push keys and silently delete every other persisted override.
|
||||||
|
- **Fix:** `Persist` loads the existing file, merges the runtime values over it and writes the result atomically with mode 0600, as before. The README and doc comment describe the merge.
|
||||||
|
- **Files modified:** modules/compass/persist.go, persist_test.go, README.md
|
||||||
|
- **Verification:** `TestPersistKeepsEarlierOverrides` (RED, then GREEN). The existing compass tests pass. The flare `--update` subtest asserts that an earlier `app.name` override survives.
|
||||||
|
- **Committed in:** `5fb22c2`
|
||||||
|
|
||||||
|
**2. [Rule 2 - Security] The VAPID driver refuses redirects**
|
||||||
|
- **Found during:** Task 1
|
||||||
|
- **Issue:** The allowlist is checked before dialing, but Go's default client follows redirects. A push service, or anything that answers on an allowed host, could bounce the request to an arbitrary host (T-11-22).
|
||||||
|
- **Fix:** `NewVAPIDPusher` copies any given client and sets `CheckRedirect` to `http.ErrUseLastResponse`. A 3xx answer is returned as a `StatusError`.
|
||||||
|
- **Verification:** The redirect case in `TestSendRefusesDisallowedEndpoint`. The redirect target gets zero requests.
|
||||||
|
- **Committed in:** `a9af0d7`
|
||||||
|
|
||||||
|
**3. [Rule 2 - Security] Secret-safe formatting and errors**
|
||||||
|
- `flare.Config` and `flare.VAPIDKeys` redact the private key in `String`, `GoString` and `LogValue`.
|
||||||
|
- Transport errors drop `*url.Error`'s repeated endpoint URL and name only the host, because an endpoint path is a capability.
|
||||||
|
- The test asserts that `%v`/`%#v` never print the private key.
|
||||||
|
- **Committed in:** `a9af0d7`
|
||||||
|
|
||||||
|
**4. [Additions] Extra exported surface, all in the READMEs and checked with go doc**
|
||||||
|
- flare:
|
||||||
|
- `Config` (`Keys`, `String`, `GoString`, `LogValue`) and `LoadConfig`
|
||||||
|
- `Service.Config`, `Service.Enabled`, `Service.SetHTTPClient` (test injection) and `Service.Logger`
|
||||||
|
- `VAPIDPusher` and `NewVAPIDPusher`
|
||||||
|
- `HostAllowed` and `DefaultAllowedHosts`
|
||||||
|
- `StatusError`, `ErrPayloadTooLarge`, `ErrInvalidVAPIDKeys` and `ErrInvalidSubject`
|
||||||
|
- the constants `ContentEncoding`, `MaxPayloadSize`, `DefaultTTL`, `DefaultTimeout`, `VAPIDTokenLifetime`, `PublicKeyLength` and `PrivateKeyLength`
|
||||||
|
- `GenerateVAPIDKeysCommandName` and `TestPushCommandName`
|
||||||
|
- centrifugo: `HealthCommandName`.
|
||||||
|
- The name constants keep each command name literal to one occurrence per file, as the acceptance greps require.
|
||||||
|
|
||||||
|
**5. [Output shape] Documented differences from PHP**
|
||||||
|
- Titles are framework-neutral. For example, "Push Notification Tester" replaces "QuestStream Push Notification Tester".
|
||||||
|
- "Testing push for user ID <n>" replaces PHP's name line, because a SubscriptionSource returns no user name.
|
||||||
|
- The notification icon/badge block is not ported. It reads the absent notifications plugin's config.
|
||||||
|
- A failing command ends with one `<command>: failed` line, which the binary prints before it exits 1.
|
||||||
|
- `websockets:health` hints at `SUMMER_REALTIME__CENTRIFUGO__API_KEY` instead of `.env`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 3 auto-fixed (all Rule 2), plus 2 documented notes.
|
||||||
|
**Impact on plan:** Fix 1 prevents config data loss from the new `--update` flag. Fixes 2 and 3 harden the T-11-22 and T-11-11 mitigations. No scope creep.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None. `TestRFC8291AppendixA` passed on the first run. A mutation of the nonce label confirmed that the test is sensitive to it.
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
|
||||||
|
None. Płytarium publishing no `SubscriptionSource` is intended, per the plan: the PHP app has no subscription store. `websockets:test-push` reports this and exits 1.
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
None beyond the plan's threat model. The only new surface is outbound HTTPS to push services, which T-11-22 covers with the allowlist and, now, the refused redirects. No inbound endpoint was added.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None. To enable push in a deployment later:
|
||||||
|
1. Run `fonoteka websockets:generate-vapid-keys`.
|
||||||
|
2. Set `SUMMER_PUSH__PUBLIC_KEY`, `SUMMER_PUSH__PRIVATE_KEY`, `SUMMER_PUSH__SUBJECT` and `SUMMER_PUSH__ENABLED=true`.
|
||||||
|
3. Have an app plugin publish a `flare.SubscriptionSource`.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- Plan 11-07 (unit tests) can add:
|
||||||
|
- `HostAllowed` edge cases beyond the smoke table
|
||||||
|
- `LoadConfig` parsing (`ttl` as a duration string, `allowed_hosts` as a comma string)
|
||||||
|
- `ParseVAPIDKeys` mismatch and padded input
|
||||||
|
- `VAPIDHeader` subject and origin rules (default port stripped, IPv6)
|
||||||
|
- the `ago` formatting
|
||||||
|
- `Service.SetHTTPClient`
|
||||||
|
- An application that stores push subscriptions only needs to publish a `flare.SubscriptionSource` and call `flare.From(app).Pusher().Send`.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 11-jobs-realtime-and-search-infrastructure*
|
||||||
|
*Completed: 2026-09-30*
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- All 11 key created files exist on disk.
|
||||||
|
- summercms.go commits `a9af0d7`, `5fb22c2` and `f55cb44` exist, and so does fonoteka.go commit `cdb87d9`. The fonoteka.go working tree is clean.
|
||||||
|
- Task 1 verify: `go vet ./...` passes. TestRFC8291AppendixA, TestVAPIDSendRoundTrip and TestSendRefusesDisallowedEndpoint each report `--- PASS`, with no SKIP.
|
||||||
|
- Task 2 verify: `go vet ./... && go test ./...` passes in summercms.go. The fonoteka.go vet and test commands pass for all three modules. `fonoteka websockets:health` prints `Centrifugo not configured (API key missing)` and exits 1.
|
||||||
|
- Every acceptance-criteria grep and `go doc` check passed in both tasks, including the exact counts of 1.
|
||||||
Reference in New Issue
Block a user