Sprint 6: Dockerized stack + pre-deploy hardening — one-command boot, webhook at-least-once, memory bounds - #17
Merged
Merged
Conversation
Scope decided with Steve: prod-style images via turbo prune, full-stack compose behind a stack profile, CI e2e against containers, webhook hardening (#8), rate-limiter memory (#10), offer-id validation (#13), polish + test-hardening sweep (#11, #14). Cloud accounts and Sentry deferred. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rdening Ten tasks, hardening-first ordering: contracts (messageId, not_found) → webhook delivery status + redelivery/retention → lifecycle plumbing (scan window, graceful shutdown) → memory bounds (#10) → offer-id validation (#13) → polish (#11) → test hardening (#14) → docker images + compose stack → CI against containers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ead-letter retention Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ting test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oses #8 scope pt 1) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…utdown (closes #8 scope pt 2) Bounds the timed-event scan feed and closes the process down cleanly: - cms: timedEventsAll accepts ?endedWithinMinutes=<positive int>, excluding events whose endsAt is older than the cutoff; absent/invalid -> unfiltered. - adapter-strapi: StrapiConfigPlane gains allTimedEventsEndedWithinMinutes, appending the query param to getAllTimedEvents when set. No core port change. - api: plane now shares the scan-grace env with the lifecycle scheduler; new apps/api/src/shutdown.ts installs one shared SIGTERM/SIGINT handler that stops the scheduler, drains the HTTP server (10s force-exit fallback, unref'd), closes the db pool, then exits — wired into index.ts, which now captures the previously-discarded scheduler-stop and server handles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…th tested force-exit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… cache (closes #10) The rate limiter's in-process bucket Map and the Strapi adapter's verifyKey auth cache both grew unboundedly from unique invalid tokens/keys, each leaving a permanent entry behind. Cap the rate limiter at RATE_LIMIT_MAX_BUCKETS (default 10000, env-configurable), sweeping expired buckets on each rollover and routing new keys to a shared overflow bucket once at cap (still counted and 429-able, never unlimited or denied outright). Cap the adapter's cached `null` verifyKey results at maxNegativeAuthEntries (default 1000), evicting the oldest on overflow while leaving positive entries untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… creation Restructures the lazy bucket sweep to fire only on pre-existing bucket rollover, not on new-key creation. This fixes an O(n²) CPU regression under attack (distinct random tokens): the sweep now skips new-key ramp-ups and only reclaims on legitimate rollover events. Memory remains bounded by the cap independently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ne, release fixes (closes #11) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ry; nameById fallback (closes #14) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uickstart docs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… env NaN guards, doc corrections Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the Sprint 6 plan (
docs/superpowers/plans/2026-07-08-sprint-6-docker-hardening.md): the whole platform now boots as production-style containers with one command, CI exercises those containers on every merge, and the pre-deploy hardening backlog is closed.turbo prune --dockerimages for api/cms/demo (alpine, non-root,sharpbuilds clean — no slim fallback needed); composestackprofile with healthcheck-gated ordering (postgres→cms→api(readyz)→demo); dev flow (docker compose up -d postgres) untouched; documented.envcontract incl. the previously-missingapps/api/.env.exampleif: always()teardownmessageIdon all webhook payloads;delivered_at/attemptscolumns (with an0005backfill migration so existing DBs don't burst-redeliver on upgrade); redelivery sweep restores at-least-once (5-attempt cap → dead-letter + mark, never silent orphans); dead-letter TTL retention; SIGTERM graceful shutdown (reentrancy-safe, force-exit fallback, live-proven: api stops in 0.18s, cms ~240ms); ended-event scan window with a single-sourced clamp against the redelivery gracenot_found), fail-open on config outage — stats can no longer be polluted by injected idsGET /docs; npmfilesallowlists;release.ymltests-before-publish + tag push; log-retention note;docs/retros/nameByIdfallbackBehavior notes for existing consumers
messageId— additive for receivers (HMAC covers the raw body as sent), but strict schema validators must allow the field. Redeliveries are new messages with fresh ids: dedup bymessageId, replay-check the signedcreatedAt.POST /v1/offers/:id/impression|clickreturn404 not_foundfor unknown offer ids where they previously returned 200 and recorded. First-party widgets unaffected..env.exampleflipsLOG_PLAINTEXT_KEYStofalse— seeded keys no longer print in cms logs on fresh setups.Test plan
docker compose --profile stack up -d --waitfrom wiped volumes → readyz 200, seeded demo, e2e 3/3 against containers; gracefuldocker stoptimings captured; README quickstart executed verbatim from a clean cloneReview notes
Ten tasks through the full gate cycle (four needed fix→re-review loops: zod idiom,
markDeliveredvalue-stability, shutdown reentrancy/clamp single-sourcing, sweep gating) plus a final whole-branch review — verdict ready to merge with fixes, fix wave16ab9c5applied and re-approved (backfill migration, exhausted-claim dead-lettering, env NaN guards, doc corrections). Known accepted limits, documented in-repo: scheduler downtime longer than the scan grace drops those transitions; overlapping ticks can duplicate a redelivery (at-least-once semantics); overflow-bucket keeps new keys rate-limited-but-shared at cap.🤖 Generated with Claude Code