From 12be75e6a6e5e170512cfd4fa111c0af433fbb0f Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 21 Apr 2026 18:30:47 +0200 Subject: [PATCH] fix(broadcast): track route paths + shared-branding tsconfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes surfaced by the end-to-end smoke test. 1. broadcast-track.ts: inner route paths double-prefixed. Routes were declared as '/track/open/:token' etc, then mounted at '/api/v1/track', yielding '/api/v1/track/track/open/:token' — every tracking endpoint returned 404. Dropped the redundant '/track/' prefix so the full path is now '/api/v1/track/{open,click,unsubscribe}/:token' as the orchestrator + client both expect. Verified with live curl: - /track/open/BAD → 200 image/gif 42 bytes (graceful no-signal) - /track/click/?url missing → 400 missing url - /track/click?url=javascript: → 400 bad url - /track/click?url=https://ok + bad token → 302 graceful - /track/unsubscribe/BAD GET → 400 HTML - /track/unsubscribe/BAD POST → 400 (RFC 8058) 2. shared-branding/tsconfig.json: allowImportingTsExtensions missing. shared-types/src/index.ts uses explicit .ts imports (intentional, for Tailwind's module resolver); any downstream tsconfig without allowImportingTsExtensions emits 8 errors. shared-auth already had this fix — shared-branding gets the same treatment. noEmit:true is set, so no rewrite flag needed. Verified: shared-branding pnpm check → 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/shared-branding/tsconfig.json | 1 + services/mana-mail/src/routes/broadcast-track.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/shared-branding/tsconfig.json b/packages/shared-branding/tsconfig.json index bf3185bbb..8b1b48626 100644 --- a/packages/shared-branding/tsconfig.json +++ b/packages/shared-branding/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", + "allowImportingTsExtensions": true, "lib": ["ES2022", "DOM", "DOM.Iterable"], "strict": true, "esModuleInterop": true, diff --git a/services/mana-mail/src/routes/broadcast-track.ts b/services/mana-mail/src/routes/broadcast-track.ts index 6212434c0..3104acdcb 100644 --- a/services/mana-mail/src/routes/broadcast-track.ts +++ b/services/mana-mail/src/routes/broadcast-track.ts @@ -57,7 +57,7 @@ export function createBroadcastTrackRoutes(db: Database, trackingSecret: string, * GET /track/open/:token — 1×1 pixel. Always returns the pixel even * on bad tokens so there's no signal to whoever's probing. */ - app.get('/track/open/:token', async (c) => { + app.get('/open/:token', async (c) => { const token = c.req.param('token'); const payload = verifyToken(token, trackingSecret); if (!payload) return pixelResponse(); @@ -86,7 +86,7 @@ export function createBroadcastTrackRoutes(db: Database, trackingSecret: string, * graceful-fall-through on verification failure so a broken token * doesn't strand the recipient on a dead page. */ - app.get('/track/click/:token', async (c) => { + app.get('/click/:token', async (c) => { const token = c.req.param('token'); const targetUrl = c.req.query('url'); if (!targetUrl) return c.text('missing url', 400); @@ -121,7 +121,7 @@ export function createBroadcastTrackRoutes(db: Database, trackingSecret: string, * so a plain anchor link works for older clients — but we still * persist the unsubscribe on GET because the user actively clicked. */ - app.get('/track/unsubscribe/:token', async (c) => { + app.get('/unsubscribe/:token', async (c) => { const token = c.req.param('token'); const payload = verifyToken(token, trackingSecret); if (!payload) { @@ -160,7 +160,7 @@ export function createBroadcastTrackRoutes(db: Database, trackingSecret: string, * Same effect as GET but returns 204 so the client doesn't show a * page (Gmail/Apple-Mail's native button calls this). */ - app.post('/track/unsubscribe/:token', async (c) => { + app.post('/unsubscribe/:token', async (c) => { const token = c.req.param('token'); const payload = verifyToken(token, trackingSecret); if (!payload) return c.text('', 400);