Stage D — MEDIUM-severity sweep (parent plan plan-2026-05-16-prelaunch-audit, sub-plan plan-stage-d-medium-sweep). First of the two Stage D patch releases. Bundles D.1 (DB lifecycle, 10 items), D.2 (API webhook hygiene, 14 items), D.3 (revenue + cron, 6 items), D.4 (architecture cleanup, 7 items), and the SQL-LIMIT structural drift-guard (P-DG-001). 37 medium-severity items + 1 structural rule = 38 deliverables shipped under Stage D's first release. The second release 1.11.1 will bundle D.5–D.6 and two more drift-guards.
This release intentionally pauses before tag / publish / sync / deploy per the operator's 2026-05-17 scope decision — all code changes are committed and gates pass, but the ceremony is deferred to a separate session for operator approval.
Added
website/src/lib/audit-write.ts+website/supabase/migrations/040_audit_log_unattributed.sql(P-M-034) — single helper for every audit_log write (28+ callsites migrated). Null-org events route to a newaudit_log_unattributedtable instead of being silently skipped. Pattern Scanner Check 22 + drift-guardaudit-write-coverage.test.tsenforce the structural ban on directfrom('audit_log').insert(...)outside the helper.website/src/data/lemon-squeezy-config.ts(P-M-025) — single SoT for Lemon Squeezy variant→tier mapping + test/live mode flag. Drift-guardlemon-squeezy-variants-sot.test.tsforbids hardcoded variant URLs outside the SoT.website/src/app/api/auth/rate-limit-probe/route.ts(P-M-017) — server-side/loginrate-limit probe (5/min per email + 20/min per IP). Client cannot bypass with multi-tab. Drift-guardlogin-server-rate-limit.test.ts(5 cases).website/src/app/api/ebook/download/route.ts(P-M-027) — entitlement-gated ebook download path with 5-minute signed URLs as a defense-in-depth fallback alongside Lemon Squeezy's primary fulfillment. Requires operator to upload PDF+EPUB toebook-fulfillment-fallbackbucket before deploy.website/src/lib/crypto/constant-time-compare.ts(P-M-019) —crypto.timingSafeEqual-backed helper to replace!==on auth headers (CRON_SECRET path migrated; drift-guard test enforces).- Pattern Scanner Check 19 — bans
console.log/error/warnon hot paths inpackages/core/src(allowlist via inline// @stdout-allow:marker). Closes wave2-architecture F-ARCH-008 (P-M-035). - Pattern Scanner Check 21 — caps
packages/core/srcTypeScript modules at 1000 LOC (allowlist via// @scanner-allow:large-filemarker). Closes wave2-architecture F-ARCH-004 (P-M-031). The two known >1000 LOC files (knowledge-tools.ts, memory-db.ts, plus tools.ts + commands/init.ts caught by the check at ship time) carry explicit allowlist markers documenting the deferred mechanical decomposition. - Pattern Scanner Check 22 — bans direct
from('audit_log').insert(...)outsideaudit-write.ts(P-M-034). - Pattern Scanner Check 23 — warns on new
// TODO|FIXME|workaround|for now|good enoughcomments missing a@plan:<token>or@issue:<#>allowlist marker (P-M-037). - Pattern Scanner Check 25 + ESLint rule
massu/no-unbounded-sql-all(P-DG-001) — forbiddb.prepare(SELECT ...).all()chains without a LIMIT clause. The ESLint rule is the authoritative AST gate (wired intowebsite/eslint.config.mjs); Check 25 is the grep-level safety net for environments where ESLint isn't invoked. Drift-guardeslint-rule-no-unbounded-sql-all.test.ts(7 cases) exercises the rule via hand-built ESTree fragments. governance_rules:top-level field inmassu.config.yamlschema (P-M-036) — customer-authored CR-style rules loaded intoknowledge_ruleswithsource='customer-config'. Distinct from the existingrules:path-scoped lint-hints field. Drift-guardcustom-governance-rules-config-loading.test.ts(4 cases) pins the cross-contamination invariant. New docs pagewebsite/content/docs/reference/custom-governance-rules.mdxexplains the two fields' separate purposes.- 13 new Supabase migrations (
028–040) plus their drift-guards (migration-partial-apply-safety,migration-idempotency,migration-service-role-policy-coverage,handle-new-user-email-confirm-gate,migration-grant-discipline). Closes the partial-apply window + ON CONFLICT idempotency + explicit service_role policy + blanket-grant discipline bug classes from wave1-schema-sync.
Changed
- D.1 hooks —
pre-compact.tsLIMIT 1000 (P-M-001);post-tool-use.tsmodule-scope mtime-cachedreadConventions()(P-M-002);fix-detector.tsskip-on-slow-git auto-disable (P-M-003); 10 hooks standardized onJSON.stringify({message})output via newhooks/lib/write-hook-message.tshelper (P-M-004). - D.1 architecture —
memory.db+knowledge.dbnow opened once per dispatcher process viaserver-dispatch.tscache (P-M-010), matching thecodegraph.db+data.dbpattern from Stage C plan-1.6.2. - D.2 webhook dispatcher —
deliverWebhook()re-validates the URL pulled from the DB before fetch AND pins the resolved IP via undici Agentconnect.lookupto defeat DNS rebinding (P-M-012). NewvalidateResolvedAddress()mirrors the IP-blocklist checks for resolved addresses. Drift-guardswebhook-dispatcher-revalidates.test.ts+dns-rebinding-resistant.test.ts. - D.2 SSO OIDC callback — config_id encoded into OAuth state (base64url JSON
{v:1, config_id, nonce}); callback routes directly to the named config (P-M-016). Closes the try-each enumeration timing leak and premature-code-consumption risk. Drift-guardoidc-callback-state-routing.test.ts(5 cases). - D.2 stripe webhook — handler now calls a single
stripe_event_applyRPC (migration 036) that wraps idempotency + plan update + audit_log + activity_feed in one Postgres transaction (P-M-018/024). Closes the SELECT-then-INSERT race window AND the audit-mid-failure-leaves-plan-updated partial-write class. - D.3 book purchases — RLS hardening: SECURITY DEFINER
book_purchases_safeview + role-gated SELECT on the underlying table (owners + admins only see PII / license_key columns). Migration 034. Drift-guardbook-purchases-role-gated.test.ts. - D.3 cron trial-email idempotency —
cron_acquire_email_lock+cron_record_email_failureRPCs (migration 037) bundle email-send + log-insert into a single transaction with 72h retry semantics. Closes the silent-skip-on-log-INSERT-failure window from wave2-book-redeem F8 (P-M-026). - D.4 license cache (P-M-023) — Ed25519 signed payload column on
license_cache(in-place schema upgrade via PRAGMA introspection). On cache read, the validator verifies the stored signature and re-extracts trusted fields from the verified payload — direct SQLite edits totier/valid_untilcolumns are structurally a no-op. Strict mode (MASSU_REQUIRE_SIGNED_LICENSE=true) rejects unsigned rows entirely; transition mode (default) emits a one-shot stderr warning. - D.4 tier metadata bijection — runtime assertion at
annotateToolDefinitions()end pins TOOL_TIER_MAP as the single source forannotations.tier+ description prefix (P-M-033). Drift-guardtier-metadata-bijection.test.ts(5 cases). - D.4 governance_rules schema —
governance_rules:top-level field added tomassu.config.yamlZod schema (P-M-036). Loaded intoknowledge_rulesat config-refresh time withsource='customer-config'(new column on the table, added via PRAGMA-introspected ALTER).
Fixed
- D.2 API hygiene —
/api/v1/audit?actor=returns clear note vs. silent empty for actor-no-rows vs org-no-rows (P-M-011);/api/v1/audit/reportLIMIT 10000 + cursor pagination (P-M-013); evidence PDF downloads now write to audit_log + are per-IP rate-limited 30/60s (P-M-014); SAML Audience element is MANDATORY (wasif (audience && ...)) (P-M-015);lib/rate-limit.tsfail-closed in production (P-M-022). - D.3 redemption surface —
/activatepage DELETED with permanent 301 redirect to/redeem, removed from sitemap, internal links audited (P-M-028). - D.3 billing anchor —
organizations.billing_period_startcolumn added (migration 039) and populated by Stripe checkout + Lemon Squeezy activate handlers + backfilled viatrial_ends_at - INTERVAL '<n> days'(P-M-029).prevent_billing_column_tamperingtrigger extended. - D.4 hot-path stdout —
validate-features-runner.ts8 sites +tree-sitter-loader.ts2 sites migrated fromconsole.*toprocess.stderr.writewith explicit\nterminators (P-M-035). Pattern Scanner Check 19 prevents recurrence.
Removed
/activatepage (P-M-028) — duplicate redemption surface with worse retry UX than/redeem. Permanent 301 redirect vianext.config.mjsensures bookmarks + email links keep working.@massu/adapter-phoenix,@massu/adapter-aspnet,@massu/adapter-go-chiworkspace packages plus theirdetect/adapters/re-export shims and test files (P-M-032). Each had exactly ONE consumer (a 1-line re-export). Drift-guardadapter-package-consumer-tracking.test.tsdetects any future zero-consumer adapter package. Operator action required AFTER publish:npm deprecate '@massu/adapter-phoenix@' '@massu/adapter-aspnet@' '@massu/adapter-go-chi@'. Rails + Spring retained per operator decision (Stage E reassessment with adoption telemetry).tools.ts:103,207,261directconfig.framework.router/.orm ===comparisons were already migrated tosupportsRouter()/supportsOrm()helpers in 1.10.8 — this release adds no further removals in that lane.
Security
- D.2 webhook SSRF defense-in-depth (P-M-012) — DNS rebinding defeated via undici Agent
connect.lookupIP-pinning. Re-validation at delivery time means a URL that passed create-time validation but flipped its DNS post-validation is still blocked. - D.2 cron auth constant-time (P-M-019) —
crypto.timingSafeEqualreplaces!==onCRON_SECRETcomparison. Closes the theoretical character-by-character timing oracle. - D.2 book_purchases RLS (P-M-020) —
license_key+ PII columns no longer SELECTable by non-admin org members. Member-tier dashboards read from thebook_purchases_safeview. - D.2 grant discipline (P-M-021) — blanket
GRANT SELECT ON ALL TABLES IN SCHEMA public TO authenticatedfrom migration 001 replaced by per-table explicit grants. New tables require explicit grants or a-- @no-grant:comment. - D.2 rate-limit fail-closed in production (P-M-022) —
lib/rate-limit.tsthrowsRateLimitFailClosedErrorat module-load time whenVERCEL_ENV === 'production'AND Upstash env vars are absent. Closes the per-Vercel-region cold-start enforcement gap. - D.4 license cache signing (P-M-023) — see above. Editing
license_cachedirectly in SQLite no longer grants any tier.