Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"fmt:check": "deno fmt --check",
"check": "deno task check:src && deno task check:worker",
"check:slow-types": "deno publish --dry-run",
"check:drift": "git diff --quiet docs/api/cloudflare-schema.yaml docs/postman/postman-collection.json docs/postman/postman-environment.json || (printf '\\n Generated files are out of date. Run: deno task schema:generate\\n Then: git add docs/api/cloudflare-schema.yaml docs/postman/postman-collection.json docs/postman/postman-environment.json\\n Finally: commit the updated files (for example: git commit -m \"chore: update generated schemas\")\\n' && exit 1)",
"check:drift": "git diff --quiet docs/api/cloudflare-schema.yaml docs/postman/postman-collection.json docs/postman/postman-environment.json || (printf '\\n\u274c Generated files are out of date. Run: deno task schema:generate\\n Then: git add docs/api/cloudflare-schema.yaml docs/postman/postman-collection.json docs/postman/postman-environment.json\\n Finally: commit the updated files (for example: git commit -m \"chore: update generated schemas\")\\n' && exit 1)",
"cache": "deno cache src/index.ts",
"openapi:validate": "deno run --allow-read --allow-net scripts/validate-openapi.ts",
"openapi:docs": "deno run --allow-read --allow-write --allow-net scripts/generate-docs.ts",
Expand All @@ -48,7 +48,7 @@
"generate:schema": "deno task schema:cloudflare && deno run --allow-read --allow-write --allow-env scripts/generate-openapi-schema.ts",
"preflight": "deno task fmt:check && deno task lint && deno task check && deno task openapi:validate && deno task schema:generate && deno task check:drift",
"preflight:full": "deno task preflight && deno task test && deno task check:slow-types",
"setup": "deno run --allow-read --allow-write scripts/setup-env.ts && deno task db:generate && deno task setup:hooks && echo ' Setup complete! Edit .env.local and .dev.vars with your credentials, then run: deno task wrangler:dev'",
"setup": "deno run --allow-read --allow-write scripts/setup-env.ts && deno task db:generate && deno task setup:hooks && echo '\u2705 Setup complete! Edit .env.local and .dev.vars with your credentials, then run: deno task wrangler:dev'",
"setup:hooks": "deno run --allow-read --allow-write --allow-run scripts/setup-hooks.ts",
"test:contract": "SKIP_CONTRACT_TESTS=false deno test --allow-read --allow-write --allow-net --allow-env worker/openapi-contract.test.ts",
"migrate:validate": "deno run --allow-read scripts/validate-migrations.ts",
Expand Down Expand Up @@ -91,7 +91,9 @@
"lighthouse:run": "pnpm exec lhci autorun",
"lighthouse:install": "pnpm add --global @lhci/cli",
"check:src": "deno check src/index.ts src/cli.ts",
"check:worker": "deno check worker/worker.ts worker/tail.ts"
"check:worker": "deno check worker/worker.ts worker/tail.ts",
"trpc:types": "deno check worker/trpc/router.ts",
"check:trpc": "deno check worker/trpc/router.ts worker/trpc/client.ts"
},
"imports": {
"@/": "./src/",
Expand Down
77 changes: 58 additions & 19 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions docs/architecture/hono-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,26 @@ async (c) => {

---

## tRPC endpoint

The tRPC v11 handler is mounted directly on the top-level `app` at `/api/trpc/*`:

```typescript
// Mounted BEFORE app.route('/api', routes) so the routes sub-app
// (with compress + logger middleware) never wraps tRPC responses.
app.all('/api/trpc/*', (c) => handleTrpcRequest(c));
```

This placement ensures:

- The global middleware chain (timing, request metadata, auth, CORS, secure headers)
**does** run before tRPC requests — `authContext` is already populated.
- The `compress()` and `logger()` middleware scoped to the `routes` sub-app **do not**
wrap tRPC responses.

See [`docs/architecture/trpc.md`](./trpc.md) for the full tRPC procedure catalogue,
client usage, and ZTA notes.

---

## Phase 4 — Domain Route Modules (complete)
Expand Down Expand Up @@ -274,3 +294,4 @@ were needed.

Run manually with: `deno task lint:routes`


88 changes: 88 additions & 0 deletions docs/architecture/trpc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# tRPC API Layer

## Overview

The adblock-compiler Worker exposes a typed tRPC v11 API alongside the existing REST endpoints.
All tRPC procedures are available at `/api/trpc/*`.

## Versioning

Procedures are namespaced by version: `v1.*`. The `v1` namespace is stable.
Breaking changes (removed procedures, changed input shapes) will be introduced under `v2`
without removing `v1`.

## Procedure catalogue

### v1.health.get (query, public)

Returns the same payload as `GET /api/health`.

### v1.compile.json (mutation, authenticated)

Accepts a `CompileRequestSchema` body. Returns the compiled ruleset JSON.

### v1.version.get (query, public)

Returns `{ version, apiVersion }`.

## Angular client

```typescript
import { createTrpcClient } from './trpc-client';

const client = createTrpcClient(environment.apiBaseUrl, () => authService.getToken());
const result = await client.v1.compile.json.mutate({
configuration: {
sources: [
{
url: 'https://example.com/easylist.txt',
},
],
},
preFetchedContent: {},
});
```

## Adding a new procedure

1. Create (or extend) a router file in `worker/trpc/routers/v1/`.
2. Add it to `worker/trpc/routers/v1/index.ts`.
3. No changes to `hono-app.ts` required — the tRPC handler is already mounted.

## Mount point

The tRPC handler is mounted directly on the top-level `app` (not the `routes` sub-app)
so that the `compress` and `logger` middleware scoped to business routes do not wrap
tRPC responses. See [`hono-routing.md`](./hono-routing.md#trpc-endpoint) for details.

```mermaid
flowchart TD
R[Incoming Request] --> M1[Global middleware\ntiming · metadata · auth · CORS]
M1 --> TRPC{path starts with\n/api/trpc?}
TRPC -->|yes| TH[handleTrpcRequest\nfetchRequestHandler]
TH --> PROC{procedure auth?}
PROC -->|publicProcedure| HANDLER[Execute handler]
PROC -->|protectedProcedure| AUTH_CHECK{userId non-null?}
AUTH_CHECK -->|no| UNAUTH[TRPCError UNAUTHORIZED\n+ security telemetry]
AUTH_CHECK -->|yes| HANDLER
PROC -->|adminProcedure| ADMIN_CHECK{role === admin?}
ADMIN_CHECK -->|no| FORB[TRPCError FORBIDDEN\n+ security telemetry]
ADMIN_CHECK -->|yes| HANDLER
HANDLER --> RESP[Response]
TRPC -->|no| REST[REST routes sub-app]
```

## ZTA notes

- `protectedProcedure` enforces non-anonymous auth (returns `UNAUTHORIZED` if
`authContext.userId` is null).
- `adminProcedure` additionally enforces `role === 'admin'` (returns `FORBIDDEN`).
- Auth failures emit `AnalyticsService.trackSecurityEvent()` via the `onError` hook
in `worker/trpc/handler.ts`.
- `/api/trpc/*` has its own `app.use('/api/trpc/*', rateLimitMiddleware())` middleware
applied directly on `app` before `handleTrpcRequest`, enforcing tiered rate limits
(including `rate_limit` security telemetry) for all tRPC calls.
- `/api/trpc/*` has its own ZTA access-gate middleware that calls `checkUserApiAccess()`
(blocks banned/suspended users) and `trackApiUsage()` (billing/analytics) —
matching the same checks applied to REST routes by `routes.use('*', ...)`.
- CORS is inherited from the global `cors()` middleware already in place on `app`.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"@prisma/adapter-pg": "^7.5.0",
"@prisma/client": "^7.5.0",
"@sentry/cloudflare": "10.43.0",
"@trpc/client": "^11.15.1",
"@trpc/server": "^11.15.1",
"better-auth": "^1.5.6",
"cloudflare": "^5.2.0",
"hono": "^4.12.8",
Expand Down
28 changes: 28 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading