You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tracking issue for the next backmerge wave from Zealot-Monorepo, same mechanic as PR #70 (lane/outbox hardening). Sourced from a 3-way scan on 2026-07-09: Zealot main commits since 7/2, Zealot PR #1510, and all open Zealot PRs, each diffed against this repo. (Replaces Linear ZLT-3378 — tracking lives here.)
Re-scanned 2026-07-21 (main 7/9→7/21, 50 commits; #1510 branch commits through 7/21, 50 commits; open-PR sweep + merge-gate statuses). Deltas tagged (7/21). Merge gates lifted: Zealot #1610, #1611, #1661, #1663, #1682 all merged — the affected items below now port straight from Zealot main.
clientIp trusted-hop util (Zealot #1611 / ZLT-3195 — merged 2026-07-10, 5d50d941): our middleware/rateLimit.ts trusts x-forwarded-for[0] — spoofable; any client mints a fresh identity per request and bypasses IP-keyed limits. Port apps/api/src/lib/clientIp.ts + test (rightmost-trusted-hop XFF, IPv6 /64 bucketing) and swap into apiRateLimit/rateLimit. Security fix — port first regardless of the rest.
incrementFixedWindows Lua limiter (Zealot #1611 — merged, same sha): atomic INCR+PEXPIRE returning [count, pttl]. Our limiter has the crash-strands-TTL-less-key bug (incr then separate expire). Upgrade standalone even without MCP.
(7/21) isWriteConflictError (Zealot #1510, 656cb…): duck-typed P2034 (deadlock/serialization victim) narrowing — first-create races treat "deadlock victim, winner committed" like P2002 (re-read the winner). Util only; its consumer is product code. Pairs naturally with the item above; applies to Postgres (P2034 covers deadlock + serialization failure).
(7/21) unregisterDbHook(name) (Zealot #1510, 480fb4a): targeted hook deregistration (registrations map keyed by name; unregister filters the handler out of each slot). Our packages/db/src/extensions/hookRegistry.ts only has clearHookRegistry() (nukes production hooks — unusable mid-suite), so a leaked test hook is unremovable across bun's shared process. Registry shape matches — near-verbatim drop-in.
withRetry (packages/shared/src/utils/retry.ts from Zealot PR #1510): generic exponential backoff + isRetryable matcher; we have no retry util. #1510 still open (merges ~early Aug); the file is stable since extraction (ceba2a64, unchanged through 7/21) — safe to cherry-pick the two files now.
ws/pubsub.ts publish-always-through-Redis (Zealot #1682 — merged 2026-07-11, 96163d3e): our publish() falls back to local delivery when pubsub is uninitialized, and jobs/worker.ts never calls initWebSocketPubSub() — a job handler emitting a websocket handoff would silently drop it. Latent today; port Zealot's worker-safe publish path. Re-verify against 0f32ac81 (ws pubsub init-after-subscribe fix) first.
Wave 2 — CI rules (scripts/ci/rules/** is protected, so explicit user-driven change)
✅ All shipped in PR #81 (2026-07-21) — user-authorized ("do all of this"). no-direct-prisma-in-tests landed diff-mode (~70 legacy sites converge as files are touched; packages/db/src/test/** exempt).
no-select-on-mutations (Zealot #1610 — merged 2026-07-11, e49f5b7c): CI rule + mutationLifeCycle seam throw + pass/fail fixtures. Our mutationLifeCycle.ts has no select/omit guard. Biggest CI-rule candidate (M with the seam throw).
(7/21) env-override test seam + no-test-env-mutation rule (Zealot #1510, 0ae3928f — M, strongest new item): setEnvOverride/resetEnvOverrides/withEnv/getEnv layered on the zod-parsed env (reads resolve override ?? real), global afterEach(resetEnvOverrides) backstop, plus a CI rule + fixtures banning env.X = …/process.env.X = … in tests — the process-global leak class in bun's single-process suite. We commit this sin today: apps/api/src/jobs/outbox/drain/tests/drain.test.ts (8 sites), apps/api/src/jobs/handlers/tests/sendWebhook.test.ts:28, packages/db/src/lib/encryption/helpers.test.ts:14; no-raw-global-assign-in-tests.sh doesn't catch env mutation. Our env.ts has the identical zod shape, so the seam drops in; port cost includes converting the ~10 violating call sites.
no-raw-transaction: bans raw $transaction in favor of db.txn() (raw bypasses the AsyncLocalStorage store mutationLifeCycle reads). We have the same primitives, no rule.
no-direct-bulk-writes: bans createMany/updateMany (lifecycle hooks never fire) in favor of *AndReturn. Verify our mutationLifeCycle handles the AndReturn variants first.
no-direct-prisma-in-tests: factories-only in tests; we have the factories. Adjust the @zealot/db-test path.
Better no-prisma-maps: Zealot's awk block-parser allows enum-value @map (legacy stored strings) while banning model-level @map/@@map; ours is a blunt grep that false-positives on enum maps. Take Zealot's version.
Wave 3 — needs design input before porting
MCP delivery slice (Zealot #1611 / ZLT-3195 — merged 2026-07-10; follow-on #1626/ZLT-3254 826d0367 merged 2026-07-21, M–L): apps/api/src/modules/mcp/ (OpenAPI→tools agent surface, in-process dispatch via app.fetch) + serializeBracketQuery move packages/ui → packages/shared (prereq: apps/api can't import packages/ui). Design blocker: we have no AuthTier / x-auth-tier extensions — route templates use buildTags({ admin, internal }) — so the agent-surface filter needs a template-native tier story, and Token has no scopes field (scope-less v1 or schema add). Also take the small operationId-override fix (our read.ts spreads ...args before operationId:, clobbering overrides). (7/21) the slice grew: acting-principal context getters (getActingBrandUserUuid — session brand user, else token's configured actor; getActingPrincipalLabel — ⚠️ exists to feed legacy string audit columns, which collides with our no-attribution-columns doctrine; our shape = API-token actor feeds the audit actor context, which the audit workstream partially covers); the tool-exposure guard test pattern (backfillToolExposure.test.ts — mount real route aggregators, run the production derivation per scope, pin the expected tool-name set); ApiToken factory note (unique on (brandUuid, name) → derive factory name from key prefix). Consider splitting to its own issue when picked up.
Audit-log deltas (Zealot PR #1663 — merged 2026-07-13): parameterized ignoreFields/redactFields registries and shared hooks/shared/hookRows.ts. Taken over by the observe-channel workstream (template hookRows already pushed there) — coordinate before touching; do NOT port Zealot's sync fire-and-forget AppEvent persist (we route observe through BullMQ; CLAUDE.md §6 — divergence is a decision).
AI-review pipeline skeleton (Zealot #1664/#1676 — both merged 2026-07-09, L): engine + eval harness + parseUnifiedDiff + 3 workflows + KB-drift gate are nearly Zealot-free; the knowledge base / .opencode agents / corpus are not. Port = engine + workflows + template-authored taxonomy and fresh docs; start with an empty corpus. Decide if/when worth it.
(7/21) check-lockfile.sh (Zealot #1780, open): self-contained ~77-line lockfile-drift gate (bun install --dry-run against the pinned bun version) — the concrete answer to the ecosystem-wide stale-lockfile epidemic. We have no .github/workflows/ by doctrine (deploy-via-init; CI is a parked want), so this sits in the parked-CI bucket: take the script whenever CI unparks.
silent-success-failures skill (Zealot #1457, S): the 8 failure modes are fully generic; every example is Zealot. Port the skeleton, rewrite examples against template models.
runJobInlineForTest chain serialization (Zealot #1679, S, optional): AsyncLocalStorage 1-concurrency chaining so concurrent test enqueues run sequentially without nested-enqueue self-deadlock. Only needed if tests hit fan-out races.
Email/COMM mirror track (carry-at-port items, not this issue's waves)
Rides the COMM tickets, listed here so they're not lost: #1657 composition contract + #1658 MJML slot grammar (both merged); (7/21) #1698 regions/parseBlocks/authoring (open, large — extension of the same track); (7/21) #1655 system.* reserved tokens (~40 lines engine: system.now/system.year resolver allowlist, non-overridable, BCP-47 locale in InterpolateOptions); #1689 {{#each}} loop fixes per the 07-12 review.
Reverse direction (template → Zealot, tracked here so it isn't lost)
(7/21) Zealot's merged #1661 interpolate lacks our prototype-pollution guard: bare lodash get with a segment regex that admits __proto__/constructor, and no function-value skip (functions render as source text). Our packages/email/src/render/interpolate.ts has UNSAFE_PATH_SEGMENTS + the function skip. Open Zealot #1655 touches the same file but guards only its new system.* prefix. Fix shape: extend the guard to all prefixes — best delivered as a review comment on #1655.
Scanned and ruled out (don't re-scan)
Zealot #1684 lens deletedAt: null injection — we landed first (3125e22f) with the fuller at-every-visit shape; Zealot cites it.
Zealot #1679 inline enqueue — was Zealot catching up to us ("template parity"); already here.
Worktree scripts — parallel forks; our slot system is ahead. (7/21) confirmed again: Zealot a584db6e docker-PATH fix is the parallel workstream landing on Zealot main; our create.sh/destroy.sh already carry the Postgres-adapted version.
perBrandFlag/posthogClient — feature-flag provider adapter is a design decision, flagged not ported. (7/21) same ruling covers #1510's useFeatureFlagdefaultValue enhancement.
(7/21) Zealot c051230e typecheck plumbing — we don't share the broken pattern: apps/api/tsconfig.json already maps bare @template/db, every workspace has typecheck, root aggregates via bun run '--filter=*' typecheck.
(7/21) Zealot 4de700cd PDF-iframe sanitizer — no UGC-HTML surface exists here (zero DOMPurify/dangerouslySetInnerHTML hits); remember the pattern (host allowlist + forced sandbox via ADD_TAGS hook) if we ever grow rich text. Related landmine if dompurify is ever added: Zealot pinned 3.3.1 because later versions sanitize silently wrong under happy-dom.
(7/21) Zealot e161b457 webhook-breaker integration exemption — keys on integrationSourceUuid (ownership concept we don't have) + their metrics counter; our sendWebhook.ts breaker is otherwise the same. Product-conditioned.
(7/21) Zealot 52f4d311/5eb29e12/479e9ab/566e328c (db.parallel, afterCommit drain, false-polymorphic AuditLog) — all template→Zealot ports; our versions are the origin and richer. 5208a4a5 delegate-caching fix — lives in their emulated createMany/updateMany paths; we ban those and resolve delegates fresh per call.
(7/21) Zealot fetchAllPages — we don't have the base util; adopting it is a from-scratch decision, not a backmerge. If ever adopted, carry #1510's maxRows/onCapped delta (d1502fd7).
Tracking issue for the next backmerge wave from Zealot-Monorepo, same mechanic as PR #70 (lane/outbox hardening). Sourced from a 3-way scan on 2026-07-09: Zealot main commits since 7/2, Zealot PR #1510, and all open Zealot PRs, each diffed against this repo. (Replaces Linear ZLT-3378 — tracking lives here.)
Re-scanned 2026-07-21 (main 7/9→7/21, 50 commits; #1510 branch commits through 7/21, 50 commits; open-PR sweep + merge-gate statuses). Deltas tagged (7/21). Merge gates lifted: Zealot #1610, #1611, #1661, #1663, #1682 all merged — the affected items below now port straight from Zealot main.
Wave 1 — standalone, no dependencies (all S)
✅ All shipped in PR #81 (2026-07-21).
clientIptrusted-hop util (Zealot #1611 / ZLT-3195 — merged 2026-07-10,5d50d941): ourmiddleware/rateLimit.tstrustsx-forwarded-for[0]— spoofable; any client mints a fresh identity per request and bypasses IP-keyed limits. Portapps/api/src/lib/clientIp.ts+ test (rightmost-trusted-hop XFF, IPv6 /64 bucketing) and swap intoapiRateLimit/rateLimit. Security fix — port first regardless of the rest.incrementFixedWindowsLua limiter (Zealot #1611 — merged, same sha): atomic INCR+PEXPIRE returning [count, pttl]. Our limiter has the crash-strands-TTL-less-key bug (incrthen separateexpire). Upgrade standalone even without MCP.isUniqueConstraintError(from the ZLT-3008 squash, not covered by PR feat(jobs): harden supersede lanes — TTL refresh + fenced releaseLane #70): duck-typed P2002 narrowing; we re-declare the inline check inerrorHandlerMiddleware.tsandhooks/contactRules/hook.ts.isWriteConflictError(Zealot #1510,656cb…): duck-typed P2034 (deadlock/serialization victim) narrowing — first-create races treat "deadlock victim, winner committed" like P2002 (re-read the winner). Util only; its consumer is product code. Pairs naturally with the item above; applies to Postgres (P2034 covers deadlock + serialization failure).unregisterDbHook(name)(Zealot #1510,480fb4a): targeted hook deregistration (registrations map keyed by name; unregister filters the handler out of each slot). Ourpackages/db/src/extensions/hookRegistry.tsonly hasclearHookRegistry()(nukes production hooks — unusable mid-suite), so a leaked test hook is unremovable across bun's shared process. Registry shape matches — near-verbatim drop-in.withRetry(packages/shared/src/utils/retry.tsfrom Zealot PR #1510): generic exponential backoff +isRetryablematcher; we have no retry util. #1510 still open (merges ~early Aug); the file is stable since extraction (ceba2a64, unchanged through 7/21) — safe to cherry-pick the two files now.ws/pubsub.tspublish-always-through-Redis (Zealot #1682 — merged 2026-07-11,96163d3e): ourpublish()falls back to local delivery when pubsub is uninitialized, andjobs/worker.tsnever callsinitWebSocketPubSub()— a job handler emitting a websocket handoff would silently drop it. Latent today; port Zealot's worker-safe publish path. Re-verify against0f32ac81(ws pubsub init-after-subscribe fix) first.Wave 2 — CI rules (
scripts/ci/rules/**is protected, so explicit user-driven change)✅ All shipped in PR #81 (2026-07-21) — user-authorized ("do all of this"). no-direct-prisma-in-tests landed diff-mode (~70 legacy sites converge as files are touched; packages/db/src/test/** exempt).
no-select-on-mutations(Zealot #1610 — merged 2026-07-11,e49f5b7c): CI rule +mutationLifeCycleseam throw + pass/fail fixtures. OurmutationLifeCycle.tshas no select/omit guard. Biggest CI-rule candidate (M with the seam throw).no-test-env-mutationrule (Zealot #1510,0ae3928f— M, strongest new item):setEnvOverride/resetEnvOverrides/withEnv/getEnvlayered on the zod-parsed env (reads resolveoverride ?? real), globalafterEach(resetEnvOverrides)backstop, plus a CI rule + fixtures banningenv.X = …/process.env.X = …in tests — the process-global leak class in bun's single-process suite. We commit this sin today:apps/api/src/jobs/outbox/drain/tests/drain.test.ts(8 sites),apps/api/src/jobs/handlers/tests/sendWebhook.test.ts:28,packages/db/src/lib/encryption/helpers.test.ts:14;no-raw-global-assign-in-tests.shdoesn't catch env mutation. Ourenv.tshas the identical zod shape, so the seam drops in; port cost includes converting the ~10 violating call sites.no-raw-transaction: bans raw$transactionin favor ofdb.txn()(raw bypasses the AsyncLocalStorage store mutationLifeCycle reads). We have the same primitives, no rule.no-direct-bulk-writes: banscreateMany/updateMany(lifecycle hooks never fire) in favor of*AndReturn. Verify our mutationLifeCycle handles the AndReturn variants first.no-direct-prisma-in-tests: factories-only in tests; we have the factories. Adjust the@zealot/db-testpath.no-prisma-maps: Zealot's awk block-parser allows enum-value@map(legacy stored strings) while banning model-level@map/@@map; ours is a blunt grep that false-positives on enum maps. Take Zealot's version.Wave 3 — needs design input before porting
826d0367merged 2026-07-21, M–L):apps/api/src/modules/mcp/(OpenAPI→tools agent surface, in-process dispatch viaapp.fetch) + serializeBracketQuery movepackages/ui→packages/shared(prereq: apps/api can't import packages/ui). Design blocker: we have no AuthTier /x-auth-tierextensions — route templates usebuildTags({ admin, internal })— so the agent-surface filter needs a template-native tier story, andTokenhas noscopesfield (scope-less v1 or schema add). Also take the smalloperationId-override fix (ourread.tsspreads...argsbeforeoperationId:, clobbering overrides). (7/21) the slice grew: acting-principal context getters (getActingBrandUserUuid— session brand user, else token's configured actor;getActingPrincipalLabel—backfillToolExposure.test.ts— mount real route aggregators, run the production derivation per scope, pin the expected tool-name set); ApiToken factory note (unique on (brandUuid, name) → derive factory name from key prefix). Consider splitting to its own issue when picked up.ignoreFields/redactFieldsregistries and sharedhooks/shared/hookRows.ts. Taken over by the observe-channel workstream (template hookRows already pushed there) — coordinate before touching; do NOT port Zealot's sync fire-and-forget AppEvent persist (we route observe through BullMQ; CLAUDE.md §6 — divergence is a decision).parseUnifiedDiff+ 3 workflows + KB-drift gate are nearly Zealot-free; the knowledge base /.opencodeagents / corpus are not. Port = engine + workflows + template-authored taxonomy and fresh docs; start with an empty corpus. Decide if/when worth it.check-lockfile.sh(Zealot #1780, open): self-contained ~77-line lockfile-drift gate (bun install --dry-runagainst the pinned bun version) — the concrete answer to the ecosystem-wide stale-lockfile epidemic. We have no.github/workflows/by doctrine (deploy-via-init; CI is a parked want), so this sits in the parked-CI bucket: take the script whenever CI unparks.silent-success-failuresskill (Zealot #1457, S): the 8 failure modes are fully generic; every example is Zealot. Port the skeleton, rewrite examples against template models.runJobInlineForTestchain serialization (Zealot #1679, S, optional): AsyncLocalStorage 1-concurrency chaining so concurrent test enqueues run sequentially without nested-enqueue self-deadlock. Only needed if tests hit fan-out races.Email/COMM mirror track (carry-at-port items, not this issue's waves)
Rides the COMM tickets, listed here so they're not lost: #1657 composition contract + #1658 MJML slot grammar (both merged); (7/21) #1698 regions/parseBlocks/authoring (open, large — extension of the same track); (7/21) #1655
system.*reserved tokens (~40 lines engine:system.now/system.yearresolver allowlist, non-overridable, BCP-47localeinInterpolateOptions); #1689{{#each}}loop fixes per the 07-12 review.Reverse direction (template → Zealot, tracked here so it isn't lost)
getwith a segment regex that admits__proto__/constructor, and no function-value skip (functions render as source text). Ourpackages/email/src/render/interpolate.tshasUNSAFE_PATH_SEGMENTS+ the function skip. Open Zealot #1655 touches the same file but guards only its newsystem.*prefix. Fix shape: extend the guard to all prefixes — best delivered as a review comment on #1655.Scanned and ruled out (don't re-scan)
deletedAt: nullinjection — we landed first (3125e22f) with the fuller at-every-visit shape; Zealot cites it.interpolate.tsalready does lodash-get traversal AND carries theUNSAFE_PATH_SEGMENTSguard. Reverse-check done 7/21: Zealot's merged version LACKS the guard — see reverse-direction section above.withRetry,isWriteConflictError,unregisterDbHook, env-override seam (all tracked above).apps/lambdas, no counterpart here. #1668 — product schema. #1680 chat, #1655 admin UI (engine hunk tracked in email section), Devin/Macroscope/dependabot PRs — product/deps.migration-collation/no-foreign-keysrules — MySQL-specific; we're Postgres. CI fixture pattern already identical.a584db6edocker-PATH fix is the parallel workstream landing on Zealot main; ourcreate.sh/destroy.shalready carry the Postgres-adapted version.perBrandFlag/posthogClient— feature-flag provider adapter is a design decision, flagged not ported. (7/21) same ruling covers #1510'suseFeatureFlagdefaultValueenhancement.c051230etypecheck plumbing — we don't share the broken pattern:apps/api/tsconfig.jsonalready maps bare@template/db, every workspace hastypecheck, root aggregates viabun run '--filter=*' typecheck.4de700cdPDF-iframe sanitizer — no UGC-HTML surface exists here (zero DOMPurify/dangerouslySetInnerHTML hits); remember the pattern (host allowlist + forced sandbox viaADD_TAGShook) if we ever grow rich text. Related landmine if dompurify is ever added: Zealot pinned 3.3.1 because later versions sanitize silently wrong under happy-dom.e161b457webhook-breaker integration exemption — keys onintegrationSourceUuid(ownership concept we don't have) + their metrics counter; oursendWebhook.tsbreaker is otherwise the same. Product-conditioned.52f4d311/5eb29e12/479e9ab/566e328c(db.parallel, afterCommit drain, false-polymorphic AuditLog) — all template→Zealot ports; our versions are the origin and richer.5208a4a5delegate-caching fix — lives in their emulated createMany/updateMany paths; we ban those and resolve delegates fresh per call.fetchAllPages— we don't have the base util; adopting it is a from-scratch decision, not a backmerge. If ever adopted, carry #1510'smaxRows/onCappeddelta (d1502fd7).