Skip to content

feat: tRPC v11 API layer + X-API-Version response header#1444

Merged
jaypatrick merged 11 commits intomainfrom
copilot/add-trpc-v1-api-versioning
Mar 28, 2026
Merged

feat: tRPC v11 API layer + X-API-Version response header#1444
jaypatrick merged 11 commits intomainfrom
copilot/add-trpc-v1-api-versioning

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 28, 2026

Adds a typed tRPC v11 API at /api/trpc/* alongside the existing REST endpoints, namespaced under v1.*. Every response now carries X-API-Version: v1. Also resolves merge conflicts with origin/main (Phase 4 domain route module split) and addresses review feedback to close ZTA gaps introduced by mounting tRPC outside the routes sub-app.

Description

Adds a typed tRPC v11 API at /api/trpc/* alongside the existing REST endpoints, namespaced under v1.*. Every response now carries X-API-Version: v1. Also resolves merge conflicts with origin/main (Phase 4 domain route module split) so the branch is fully up to date, and applies review-driven fixes to the tRPC layer's ZTA posture, rate-limiting, header reliability, type safety, and documentation accuracy.

Changes

worker/trpc/ (all new)

  • context.tsTrpcContext + createTrpcContext() from Hono context
  • init.tspublicProcedure, protectedProcedure (requires userId), adminProcedure (requires role === 'admin'), createCallerFactory
  • routers/v1/health.router.tsv1.health.get wraps handleHealth
  • routers/v1/compile.router.tsv1.compile.json mutation (protectedProcedure) wraps handleCompileJson; input uses a parser function instead of CompileRequestSchema as any to preserve full TypeScript type inference on both client and server
  • routers/v1/version.router.tsv1.version.get returns { version, apiVersion }
  • router.ts / handler.ts — top-level appRouter; fetchRequestHandler adapter with ZTA telemetry on UNAUTHORIZED/FORBIDDEN using the actual HTTP request method
  • client.tscreateTrpcClient(baseUrl, getToken?) typed client for Angular

worker/hono-app.ts

  • X-API-Version: v1 header is now set before await next() so error responses handled by app.onError also carry the header (previously post-next() placement could miss error paths)
  • Added app.use('/api/trpc/*', rateLimitMiddleware()) — tiered rate limiting (including rate_limit security telemetry) for all tRPC calls, matching REST write endpoints
  • Added app.use('/api/trpc/*', ...) ZTA access-gate middleware that calls checkUserApiAccess() (blocks banned/suspended users with auth_failure security event) and trackApiUsage() — mirrors the routes.use('*', ...) checks applied to every REST endpoint
  • Mounted app.all('/api/trpc/*', handleTrpcRequest) directly on app (before app.route('/api', routes)) — bypasses compress/logger middleware scoped to the routes sub-app
  • Merged with Phase 4 refactor from origin/main: inline route handlers replaced with domain module mounts; stale helper functions removed

worker/utils/synthetic-request.ts (new)

Shared utility extracted to avoid a circular import; tRPC's compile router imports buildSyntheticRequest directly from this module. Updated JSDoc to correctly reference buildSyntheticRequest(c, validatedBody) in worker/routes/shared.ts for Hono route handlers (removed stale reference to non-existent buildHonoRequest).

worker/routes/ (from origin/main Phase 4 merge)

Domain-scoped route modules (compile.routes.ts, rules.routes.ts, queue.routes.ts, configuration.routes.ts, admin.routes.ts, monitoring.routes.ts, api-keys.routes.ts, webhook.routes.ts, workflow.routes.ts, browser.routes.ts) and shared helpers in shared.ts/index.ts.

deno.json / package.json / pnpm-lock.yaml

  • Added @trpc/server@11.15.1 and @trpc/client@11.15.1 via deno add npm:@trpc/server npm:@trpc/client — packages are pinned in deno.lock and managed by Deno (not via manual imports map entries)
  • Updated pnpm-lock.yaml to include the new tRPC packages, fixing pnpm install --frozen-lockfile CI failures
  • Added trpc:types and check:trpc task aliases

Docs

  • docs/architecture/trpc.md — procedure catalogue, Mermaid flow, Angular client example (using correct CompileRequestSchema shape), ZTA notes updated to accurately describe the dedicated app.use('/api/trpc/*', ...) rate-limit and access-gate middleware
  • docs/architecture/hono-routing.md — added tRPC endpoint section and Phase 4 domain route module section

Tests

worker/trpc/trpc.test.ts — 8 unit tests via createCallerFactory (no HTTP): health, version, compile auth enforcement, protectedProcedure UNAUTHORIZED, adminProcedure FORBIDDEN.

Testing

  • Unit tests added/updated
  • Manual testing performed
  • CI passes

Zero Trust Architecture Checklist

Worker / Backend

  • Every handler verifies auth before executing business logic — protectedProcedure and adminProcedure enforce auth at the procedure level; global unified-auth middleware populates authContext before /api/trpc/* is reached; app.use('/api/trpc/*', ...) ZTA gate blocks banned/suspended users before handleTrpcRequest
  • CORS origin allowlist enforced (not *) on write/authenticated endpoints — tRPC inherits global cors() middleware already on app
  • All secrets accessed via Worker Secret bindings (not [vars]) — no new secrets introduced
  • All external inputs Zod-validated before use — v1.compile.json uses a typed parser function over CompileRequestSchema.safeParse() (throws TRPCError BAD_REQUEST on failure); tRPC validates all procedure inputs at the framework level
  • All D1 queries use parameterized .prepare().bind() (no string interpolation) — N/A, no new D1 queries
  • Security events emitted to Analytics Engine on auth failures — handleTrpcRequest onError hook emits trackSecurityEvent() on UNAUTHORIZED/FORBIDDEN; ZTA access-gate middleware emits auth_failure on banned/suspended users; rateLimitMiddleware() emits rate_limit telemetry on 429

Frontend / Angular

  • Protected routes have functional CanActivateFn auth guards — N/A
  • Auth tokens managed via Clerk SDK (not localStorage) — N/A
  • HTTP interceptor attaches ****** (no manual token passing) — createTrpcClient accepts a getToken callback; token attachment is in the httpBatchLink headers factory, not component code
  • API responses validated with Zod schemas before consumption — N/A (client.ts is a typed factory; validation is caller responsibility)
Original prompt

PR B — tRPC v1 + API Versioning (must be merged AFTER PR A)

This PR adds tRPC v1 on top of the domain-route-split from PR A (which adds worker/routes/). It also adds API versioning via the X-API-Version response header and a deno.json task alias.


Tech stack context

  • Runtime: Cloudflare Worker (Deno-compiled, V8 isolate)
  • HTTP framework: Hono (hono@^4.12.8) + @hono/zod-openapi@^1.2.2
  • Schema validation: zod (jsr:@zod/zod@^4.3.6)
  • Auth: Better Auth + Bearer token (session token from get-session response)
  • Angular frontend at frontend/ — the only current API consumer, but external clients will be added
  • No existing tRPC package in deno.json imports

Part 1: tRPC integration

New dependencies (add to deno.json imports)

"@trpc/server": "npm:@trpc/server@^11.0.0",
"@trpc/client": "npm:@trpc/client@^11.0.0"

New files to create

worker/trpc/context.ts

import type { Context } from 'hono';
import type { Env, IAuthContext } from '../types.ts';

export interface TrpcContext {
    env: Env;
    authContext: IAuthContext;
    requestId: string;
    ip: string;
}

export function createTrpcContext(c: Context<{ Bindings: Env; Variables: Variables }>): TrpcContext {
    return {
        env: c.env,
        authContext: c.get('authContext'),
        requestId: c.get('requestId') ?? crypto.randomUUID(),
        ip: c.get('ip') ?? '',
    };
}

(Import Variables from worker/hono-app.ts — or define a minimal local type alias that matches the fields used.)

worker/trpc/init.ts

import { initTRPC, TRPCError } from '@trpc/server';
import type { TrpcContext } from './context.ts';

const t = initTRPC.context<TrpcContext>().create();

export const router = t.router;
export const publicProcedure = t.procedure;

/** Procedure that requires an authenticated session (non-anonymous authContext). */
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
    if (!ctx.authContext.userId) {
        throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Authentication required.' });
    }
    return next({ ctx });
});

/** Procedure that requires admin role. */
export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
    if (ctx.authContext.role !== 'admin') {
        throw new TRPCError({ code: 'FORBIDDEN', message: 'Admin role required.' });
    }
    return next({ ctx });
});

worker/trpc/routers/v1/health.router.ts

Expose a health procedure that calls handleHealth(ctx.env) and returns the parsed JSON:

import { router, publicProcedure } from '../../init.ts';
import { handleHealth } from '../../../handlers/health.ts';

export const healthRouter = router({
    get: publicProcedure.query(async ({ ctx }) => {
        const res = await handleHealth(ctx.env);
        return res.json();
    }),
});

worker/trpc/routers/v1/compile.router.ts

Expose a compile.json mutation that wraps handleCompileJson. Use the existing CompileRequestSchema from src/configuration/schemas.ts as the input Zod schema. Return the raw JSON body from the handler response.

import { z } from 'zod';
import { router, protectedProcedure } from '../../init.ts';
import { CompileRequestSchema } from '../../../../src/configuration/schemas.ts';
import { handleCompileJson } from '../../../handlers/compile.ts';
import { buildSyntheticRequest } from '../../../hono-app.ts';  // re-export from hono-app.ts (see note below)

export const compileRouter = router({
    json: protectedProcedure
        .input(CompileRequestSchema)
        .mutation(async ({ input, ctx }) => {
            const req = buildSyntheticRequest(JSON.stringify(input));
            const res = await handleCompileJson(req, ctx.env, ctx.authContext);
            return res.json();
        }),
});

Note: buildSyntheticRequest is currently a private helper in hono-app.ts. Export it from hono-app.ts so the tRPC router can import it.

worker/trpc/routers/v1/version.router.ts

import { router, publicProcedure } from '../../init.ts';

export const versionRouter = router({
    get: publicProcedure.query(({ ctx }) => ({
        version: ctx.env.COMPILER_VERSION ?? 'unknown',
        apiVersion: 'v1',
    })),
});

worker/trpc/routers/v1/index.ts — barrel

import { router } from '../../init.ts';
import { healthRouter } from './health.router.ts';
import { compileRouter } from './compile.router.ts';
import { versionRouter } from './version.router.ts';

export const v1Router = router({
    health: healthRouter,
    compile: compileRouter,
    version: versionRouter,
});

export type V1Router = typeof v1Router;

worker/trpc/router.ts — top-level versioned router

import { router } from './init.ts';
import { v1Router } from './routers/v1/index.ts';

export const appRouter = router({
    v1: v1Router,
});

export type AppRouter = typ...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Send tasks to Copilot coding agent from [Slack](https://gh.io/cca-slack-docs) and [Teams](https://gh.io/cca-teams-docs) to turn conversations into code. Copilot posts an update in your thread when it's finished.

@jaypatrick jaypatrick added enhancement New feature or request ux Issues with the UX labels Mar 28, 2026
@jaypatrick jaypatrick added this to the beta milestone Mar 28, 2026
Copilot AI changed the title [WIP] Add tRPC v1 and API versioning support feat: tRPC v11 API layer + X-API-Version response header Mar 28, 2026
Copilot AI requested a review from jaypatrick March 28, 2026 04:11
@jaypatrick jaypatrick requested a review from Copilot March 28, 2026 04:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a typed tRPC v11 API layer on the Worker (mounted at /api/trpc/*, namespaced under v1.*) alongside existing REST endpoints, and adds a global X-API-Version: v1 response header.

Changes:

  • Added a new worker/trpc/ module (context, router/procedures, fetch adapter handler, typed client) with initial v1 routers (health/compile/version).
  • Mounted the tRPC handler in worker/hono-app.ts and added a global API version response header middleware.
  • Added docs and Deno tasks/deps for type-checking the new tRPC layer, plus unit tests via createCallerFactory.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
worker/utils/synthetic-request.ts New helper to construct minimal JSON POST Request objects for calling legacy handlers outside Hono.
worker/trpc/trpc.test.ts New unit tests covering v1 procedures and auth enforcement.
worker/trpc/routers/v1/version.router.ts Adds v1.version.get procedure returning Worker/app API version info.
worker/trpc/routers/v1/index.ts Assembles the v1 router namespace (health/compile/version).
worker/trpc/routers/v1/health.router.ts Adds v1.health.get procedure wrapping existing health handler.
worker/trpc/routers/v1/compile.router.ts Adds v1.compile.json protected mutation wrapping handleCompileJson.
worker/trpc/router.ts Defines the top-level appRouter with a v1 namespace.
worker/trpc/init.ts Initializes tRPC and defines publicProcedure, protectedProcedure, and adminProcedure.
worker/trpc/handler.ts Adds a Hono-compatible fetch adapter handler for /api/trpc/* with security telemetry hooks.
worker/trpc/context.ts Defines TrpcContext and createTrpcContext() from Hono context variables.
worker/trpc/client.ts Adds a typed tRPC client factory using httpBatchLink.
worker/hono-app.ts Adds global X-API-Version header middleware, mounts tRPC handler, and refactors synthetic request helper naming/re-export.
docs/architecture/trpc.md New architecture doc for tRPC procedures, usage, and ZTA notes.
docs/architecture/hono-routing.md Documents the tRPC mount location and middleware implications.
deno.lock Locks new npm deps for @trpc/server/@trpc/client (and transitive changes).
deno.json Adds tRPC deps and task aliases for tRPC type-checking.

jaypatrick and others added 4 commits March 28, 2026 00:41
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@jaypatrick
Copy link
Copy Markdown
Owner

@copilot Ensure you are importing Trpc via deno if possible deno add npm:@trpc/server npm:@trpc/client if it is compatible with our tech stack

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot Ensure you are importing Trpc via deno if possible deno add npm:@trpc/server npm:@trpc/client if it is compatible with our tech stack

Done in dd2852d. Ran deno add npm:@trpc/server npm:@trpc/client which resolved both packages to 11.15.1 and pinned them in deno.lock + node_modules — the idiomatic Deno 2 approach. The manual imports map entries were removed; bare specifier imports (@trpc/server, @trpc/client) continue to work via the nodeModulesDir: "auto" setting already in deno.json. All 1202 tests still pass.

@jaypatrick
Copy link
Copy Markdown
Owner

@copilot CI error
Run pnpm install --frozen-lockfile
Scope: all 3 workspace projects
 ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with /package.json

Note that in CI environments this setting is true by default. If you still need to run install in such cases, use "pnpm install --no-frozen-lockfile"

Failure reason:
specifiers in the lockfile don't match specifiers in package.json:

  • 2 dependencies were added: @trpc/client@^11.15.1, @trpc/server@^11.15.1

Error: Process completed with exit code 1.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot CI error
Run pnpm install --frozen-lockfile
Scope: all 3 workspace projects
...

Fixed in ab330a4. deno add had added @trpc/client and @trpc/server to package.json but didn't regenerate pnpm-lock.yaml, causing the --frozen-lockfile failure. Ran pnpm install to sync the lockfile; pnpm install --frozen-lockfile now succeeds.

@socket-security
Copy link
Copy Markdown

socket-security bot commented Mar 28, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​trpc/​client@​11.15.11001009498100
Added@​trpc/​server@​11.15.11001009998100

View full report

@jaypatrick
Copy link
Copy Markdown
Owner

@copilot please fix the merge conflicts.

…pi-versioning

# Conflicts:
#	docs/architecture/hono-routing.md
#	worker/hono-app.ts

Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot please fix the merge conflicts.

Resolved in 134fcc1. Merged origin/main (Phase 4 domain route module split) into the branch. Conflicts were in worker/hono-app.ts (inline route handlers replaced with domain module mounts; stale helper functions removed; tRPC mount + X-API-Version middleware preserved) and docs/architecture/hono-routing.md (tRPC endpoint section + Phase 4 section both kept). All 1197 tests pass.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 18 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

@jaypatrick
Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

…order, type inference, docs

Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/5fbd6aa3-5013-460b-8d40-82db88073b51

Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot apply changes based on the comments in this thread

Applied all 6 review comments in 3b4de55:

  • X-API-Version header (hono-app.ts:191): moved c.header('X-API-Version', 'v1') to before await next() so error responses also carry the header.
  • Rate-limiting for tRPC (hono-app.ts:570): added app.use('/api/trpc/*', rateLimitMiddleware()) — same tiered rate limiting (with rate_limit telemetry) as REST write endpoints.
  • ZTA access gate + usage tracking (hono-app.ts:570): added app.use('/api/trpc/*', ...) that calls checkUserApiAccess() (blocks banned/suspended users) and trackApiUsage() — mirrors the routes.use('*', ...) checks on REST endpoints.
  • synthetic-request.ts comment (utils/synthetic-request.ts:9-10): updated to point to buildSyntheticRequest(c, validatedBody) in worker/routes/shared.ts instead of the non-existent buildHonoRequest.
  • Docs (trpc.md:82-83): updated ZTA notes to accurately describe the dedicated rate-limit and access-gate middleware now applied to /api/trpc/*.
  • CompileRequestSchema as any (compile.router.ts:14-20): replaced with a parser function (input: unknown): z.infer<typeof CompileRequestSchema> => { ... } that uses safeParse and throws TRPCError({ code: 'BAD_REQUEST' }) on failure — avoids the module-identity cast and preserves full type inference.

@jaypatrick jaypatrick marked this pull request as ready for review March 28, 2026 07:29
Copilot AI review requested due to automatic review settings March 28, 2026 07:29
@jaypatrick jaypatrick merged commit 4175e63 into main Mar 28, 2026
28 checks passed
@jaypatrick jaypatrick deleted the copilot/add-trpc-v1-api-versioning branch March 28, 2026 07:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 18 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +584 to +596

const accessDenied = await checkUserApiAccess(authContext, c.env);
if (accessDenied) {
analytics.trackSecurityEvent({
eventType: 'auth_failure',
path: c.req.path,
method: c.req.method,
clientIpHash: AnalyticsService.hashIp(ip),
reason: 'api_disabled',
});
return accessDenied;
}
c.executionCtx.waitUntil(trackApiUsage(authContext, c.req.path, c.req.method, c.env));
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tRPC access-gate middleware uses c.req.path for security telemetry and trackApiUsage(), but the REST equivalent normalizes paths via routesPath(c) (strips the /api prefix). This will make analytics/usage buckets inconsistent (e.g. REST stores /compile while tRPC stores /api/trpc/...). Consider using routesPath(c) (or a similar normalization) for both the trackSecurityEvent.path field and the trackApiUsage() call so dashboards/quotas group consistently.

Suggested change
const accessDenied = await checkUserApiAccess(authContext, c.env);
if (accessDenied) {
analytics.trackSecurityEvent({
eventType: 'auth_failure',
path: c.req.path,
method: c.req.method,
clientIpHash: AnalyticsService.hashIp(ip),
reason: 'api_disabled',
});
return accessDenied;
}
c.executionCtx.waitUntil(trackApiUsage(authContext, c.req.path, c.req.method, c.env));
const path = routesPath(c);
const accessDenied = await checkUserApiAccess(authContext, c.env);
if (accessDenied) {
analytics.trackSecurityEvent({
eventType: 'auth_failure',
path,
method: c.req.method,
clientIpHash: AnalyticsService.hashIp(ip),
reason: 'api_disabled',
});
return accessDenied;
}
c.executionCtx.waitUntil(trackApiUsage(authContext, path, c.req.method, c.env));

Copilot uses AI. Check for mistakes.
@jaypatrick jaypatrick restored the copilot/add-trpc-v1-api-versioning branch March 28, 2026 07:41
@jaypatrick
Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request ux Issues with the UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants