diff --git a/README.md b/README.md index b64d1c4..46d159b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ Tabi Mascot

+> Tabi (旅) means "journey" in Japanese. The shortest path between two points is +> a straight line. Tabi stays out of your way—no abstractions to fight, no magic +> to reverse-engineer, no surprises in production. Every build should be a +> stress free journey. + ## Quick Start ```bash diff --git a/docs/adr/001-deno-runtime.md b/docs/adr/001-deno-runtime.md new file mode 100644 index 0000000..7374b28 --- /dev/null +++ b/docs/adr/001-deno-runtime.md @@ -0,0 +1,48 @@ +# ADR-001: Use Deno as the Single Runtime + +**Status:** Accepted + +## Context + +Modern web frameworks can target multiple JavaScript runtimes: + +- **Node.js** - Mature ecosystem, widespread adoption, CommonJS/ESM complexity +- **Deno** - Native TypeScript, secure by default, modern standards +- **Bun** - Fast startup, Node.js compatible, newer/less stable +- **Runtime-agnostic** - Maximum compatibility, lowest common denominator + features + +Each approach has trade-offs in ecosystem access, security model, developer +experience, and maintenance burden. + +## Decision + +Tabi targets Deno exclusively, requiring version 2.5.6 or higher. + +We will not maintain Node.js compatibility layers or abstract runtime-specific +APIs behind adapters. + +## Consequences + +### Benefits + +- Native TypeScript execution without build step or configuration +- Built-in security permissions model (network, filesystem, env access must be + explicit) +- Standard library (`@std/*`) for common utilities without third-party + dependencies +- Modern web standards (Fetch API, Web Crypto, Streams) as first-class citizens +- Single lockfile format and dependency resolution strategy +- Publish to JSR with native TypeScript support + +### Drawbacks + +- Smaller ecosystem than Node.js +- Some npm packages may not work without compatibility layer +- Users must install Deno (less ubiquitous than Node.js) +- Deployment targets limited to Deno-supporting platforms + +### Neutral + +- Version pinning via `.dvmrc` for team consistency +- Deno Deploy as natural deployment target (but not required) diff --git a/docs/adr/002-minimal-philosophy.md b/docs/adr/002-minimal-philosophy.md new file mode 100644 index 0000000..2e61124 --- /dev/null +++ b/docs/adr/002-minimal-philosophy.md @@ -0,0 +1,76 @@ +# ADR-002: Minimal Framework Philosophy + +**Status:** Accepted + +## Context + +Web frameworks exist on a spectrum: + +**Batteries-included** (Rails, Django, Nest.js): + +- Prescriptive structure and conventions +- Built-in ORM, auth, sessions, templating +- Faster initial development, steeper learning curve +- Harder to deviate from conventions + +**Minimal** (Express, Fastify, Hono): + +- Core routing and middleware only +- Bring your own everything else +- More setup, more flexibility +- Explicit over implicit + +Tabi needs to choose where on this spectrum to position itself. + +## Decision + +Tabi follows a minimal, explicit, no-magic philosophy. + +Core principles: + +- **No implicit behavior** - Every feature must be explicitly enabled +- **No hidden state** - Request/response flow is predictable and traceable +- **No framework lock-in** - Standard interfaces over proprietary abstractions +- **Composition over inheritance** - Build applications by composing small + pieces + +The framework provides: + +- Request/response handling +- Routing +- Middleware composition +- Core utilities (cookies, validation, compression) + +The framework does not provide: + +- Database/ORM layer +- Authentication/authorization +- Session management +- Templating engine +- Background jobs +- Email sending + +## Consequences + +### Benefits + +- Smaller bundle size (only include what you use) +- Easier to understand (less magic to learn) +- Predictable behavior (explicit composition) +- Flexible architecture (no prescribed patterns) +- Easier to test (fewer hidden dependencies) +- Security benefits (smaller attack surface) + +### Drawbacks + +- More initial setup for common features +- Must choose and integrate third-party solutions +- Less guidance for application structure +- Potential for inconsistency across applications + +### Implications + +- Documentation must clearly show composition patterns +- Example applications should demonstrate common setups +- Middleware should be independently useful +- Each feature should have single, clear responsibility diff --git a/docs/adr/003-monorepo-composable-modules.md b/docs/adr/003-monorepo-composable-modules.md new file mode 100644 index 0000000..fb5d769 --- /dev/null +++ b/docs/adr/003-monorepo-composable-modules.md @@ -0,0 +1,75 @@ +# ADR-003: Monorepo with Independently Composable Modules + +**Status:** Accepted + +## Context + +The framework consists of multiple features: core routing, validation, various +middleware (CORS, CSRF, compression, etc.), and utilities. These can be +organized as: + +**Single monolithic package:** + +- One import, one version +- All features always available +- Larger bundle if tree-shaking fails + +**Separate packages with independent versions:** + +- Import only what you need +- Version compatibility matrix complexity +- More release management overhead + +**Single package with submodule exports:** + +- One version to track +- Import specific modules +- Tree-shaking friendly +- Simpler release process + +## Decision + +Organize as a monorepo with a single JSR package (`@tabirun/app`) that exposes +submodule exports, each independently importable. + +```typescript +// Import only what you need +import { TabiApp } from "@tabirun/app/app"; +import { cors } from "@tabirun/app/cors"; +import { rateLimit } from "@tabirun/app/rate-limit"; +``` + +Each module: + +- Has its own directory with `mod.ts` as public API +- Contains colocated tests +- Has independent README documentation +- Exports only public types and functions + +## Consequences + +### Benefits + +- Single version number for entire framework +- Simpler dependency management for users +- Tree-shaking removes unused modules +- Easier cross-module testing +- Consistent release cycle +- No version compatibility matrix + +### Drawbacks + +- Breaking change in any module bumps entire package version +- Cannot patch individual modules independently +- All modules must maintain same quality bar + +### Module Structure + +``` +module-name/ +├── mod.ts # Public exports only +├── middleware.ts # Implementation +├── types.ts # Types (if needed) +├── README.md # Usage documentation +└── tests/ # Module tests +``` diff --git a/docs/adr/004-standard-schema-validation.md b/docs/adr/004-standard-schema-validation.md new file mode 100644 index 0000000..865554b --- /dev/null +++ b/docs/adr/004-standard-schema-validation.md @@ -0,0 +1,74 @@ +# ADR-004: Standard Schema for Validation Library Agnosticism + +**Status:** Accepted + +## Context + +Runtime validation at API boundaries is essential for security and type safety. +Popular validation libraries include: + +- **Zod** - TypeScript-first, excellent inference, large bundle +- **Valibot** - Smaller bundle, modular design +- **ArkType** - Fast runtime, complex syntax +- **Yup** - Mature, widespread adoption +- **Joi** - Battle-tested, verbose API + +Each has different APIs, performance characteristics, and bundle sizes. Coupling +the framework to one library forces that choice on all users. + +The `@standard-schema/spec` initiative defines a common interface that +validation libraries can implement, enabling interoperability. + +## Decision + +Use the Standard Schema interface (`@standard-schema/spec`) for all validation +integration. Provide Zod as the reference implementation in documentation and +examples. + +```typescript +import type { StandardSchemaV1 } from "@standard-schema/spec"; + +interface ValidatorConfig { + json?: StandardSchemaV1; + query?: StandardSchemaV1; + params?: StandardSchemaV1; + form?: StandardSchemaV1; +} +``` + +Users can use any Standard Schema-compatible library: + +```typescript +// With Zod +import { z } from "zod"; +const schema = z.object({ name: z.string() }); + +// With Valibot +import * as v from "valibot"; +const schema = v.object({ name: v.string() }); + +// Both work with Tabi's validator +const { validate, valid } = validator({ json: schema }); +``` + +## Consequences + +### Benefits + +- No lock-in to specific validation library +- Users choose based on their preferences (bundle size, API, performance) +- Middleware remains library-agnostic +- Future libraries automatically compatible if they implement Standard Schema + +### Drawbacks + +- Slightly more complex type signatures +- Must document which libraries support Standard Schema +- Error message format varies by library +- Testing should cover multiple schema implementations + +### Implementation Notes + +- Validator middleware accepts any `StandardSchemaV1` compliant schema +- Type inference works through Standard Schema's `InferOutput` type +- Error reporting can be customized via `onError` option diff --git a/docs/adr/005-linear-router-default.md b/docs/adr/005-linear-router-default.md new file mode 100644 index 0000000..6c6b4f0 --- /dev/null +++ b/docs/adr/005-linear-router-default.md @@ -0,0 +1,80 @@ +# ADR-005: Linear Router as Default Implementation + +**Status:** Accepted + +## Context + +Route matching algorithms have different trade-offs: + +**Linear scan O(n):** + +- Simple implementation +- Predictable matching order +- Performance degrades with route count +- Easy to debug + +**Radix tree O(k) where k = path length:** + +- Fast for large route sets +- Complex implementation +- Matching order less intuitive +- Harder to debug edge cases + +**Compiled regex:** + +- Very fast after compilation +- Complex to implement correctly +- Startup cost for compilation + +For a minimal framework targeting small-to-medium applications, simplicity and +predictability may outweigh raw performance. + +## Decision + +Implement `LinearRouter` as the default router, with a pluggable `TabiRouter` +interface allowing alternative implementations. + +```typescript +interface TabiRouter { + add(method: string, path: string, handler: TabiHandler): void; + match(method: string, path: string): RouteMatch | null; +} +``` + +Route matching priority: + +1. Static routes (exact match) +2. Parameterized routes (`:id`) +3. Wildcard routes (`*`) + +Within each category, first registered wins. + +## Consequences + +### Benefits + +- Predictable matching (registration order within priority tiers) +- Simple debugging (linear search, clear priority rules) +- Easy to implement path traversal protection +- Sufficient performance for typical applications (<100 routes) +- Clear mental model for users + +### Drawbacks + +- O(n) matching per request +- Not suitable for applications with thousands of routes +- May need optimization for high-traffic scenarios + +### Future Options + +- `RadixRouter` can be added as alternative implementation +- Users can implement custom routers via `TabiRouter` interface +- Router choice doesn't affect application code (same API) + +### Security + +Linear router enables straightforward path traversal protection: + +- Decode URL parameters +- Block `../`, `..\\`, and encoded variants +- Return 400 Bad Request on traversal attempts diff --git a/docs/adr/006-lazy-response-materialization.md b/docs/adr/006-lazy-response-materialization.md new file mode 100644 index 0000000..0710037 --- /dev/null +++ b/docs/adr/006-lazy-response-materialization.md @@ -0,0 +1,80 @@ +# ADR-006: Lazy Response Materialization + +**Status:** Accepted + +## Context + +Response construction can be handled two ways: + +**Eager construction:** + +- Create `Response` object immediately when handler sets response +- Simple mental model +- Difficult for middleware to modify response after it's set +- Multiple `Response` allocations if middleware transforms response + +**Lazy construction:** + +- Buffer response state (headers, body, status) throughout request lifecycle +- Construct final `Response` object once at the end +- Middleware can modify response at any point +- Single allocation + +## Decision + +`TabiResponse` buffers response state until `finalize()` is called, which +constructs the actual `Response` object. + +```typescript +class TabiResponse { + // Buffer state + status(code: number): this; + header(name: string, value: string): this; + body(content: BodyInit): this; + + // Construct final Response + finalize(): Response; +} + +// Usage in handlers +c.json({ data: "value" }); // Buffers, doesn't create Response yet +c.header("X-Custom", "value"); // Can still modify + +// Framework calls finalize() after all middleware complete +``` + +## Consequences + +### Benefits + +- Middleware can modify response after handler sets it +- Headers can be added/removed throughout request lifecycle +- Single `Response` construction reduces allocations +- Enables response transformation patterns (compression, caching headers) +- Cleaner separation between "building response" and "sending response" + +### Drawbacks + +- Cannot stream response body until finalized +- Must remember that response isn't sent until request completes +- Slightly more complex response internals + +### Patterns Enabled + +```typescript +// Compression middleware can check content-type before compressing +app.use(async (c, next) => { + await next(); + if (shouldCompress(c.res)) { + c.res.body(compress(c.res.getBody())); + } +}); + +// Cache-control middleware can add headers based on response +app.use(async (c, next) => { + await next(); + if (c.res.getStatus() === 200) { + c.res.header("Cache-Control", "max-age=3600"); + } +}); +``` diff --git a/docs/adr/007-memoized-request-body.md b/docs/adr/007-memoized-request-body.md new file mode 100644 index 0000000..5a922ba --- /dev/null +++ b/docs/adr/007-memoized-request-body.md @@ -0,0 +1,76 @@ +# ADR-007: Memoized Request Body Parsing + +**Status:** Accepted + +## Context + +The Fetch API's `Request` body can only be consumed once: + +```typescript +const json = await request.json(); // Works +const text = await request.text(); // Throws: body already consumed +``` + +In a middleware architecture, multiple middleware may need to read the request +body: + +- Validation middleware reads JSON to validate +- Logging middleware reads body for audit +- Handler reads body to process + +Options: + +1. **Clone request** - Memory overhead, complexity +2. **Parse once, pass through context** - Coupling between middleware +3. **Memoize parsed results** - Parse on first access, cache result + +## Decision + +Memoize parsed request body per format. First access parses and caches; +subsequent accesses return cached result. + +```typescript +class TabiRequest { + private jsonCache?: unknown; + private textCache?: string; + // ... other formats + + async json(): Promise { + if (this.jsonCache === undefined) { + this.jsonCache = await this.raw.json(); + } + return this.jsonCache as T; + } +} +``` + +Supported formats: + +- `json()` - Parse as JSON +- `text()` - Parse as string +- `formData()` - Parse as FormData +- `arrayBuffer()` - Parse as ArrayBuffer +- `blob()` - Parse as Blob + +## Consequences + +### Benefits + +- Multiple middleware can read same body +- No "body already consumed" errors +- Transparent to middleware authors +- Consistent parsing across request lifecycle + +### Drawbacks + +- Memory overhead for cached parsed bodies +- First accessor determines parse success/failure +- Cannot re-parse with different options (e.g., different JSON reviver) +- Large bodies stay in memory for request duration + +### Behavior Notes + +- Parsing happens lazily (only when accessed) +- Parse errors throw on first access, cached for subsequent access +- Different formats are independent (can call both `json()` and `text()`) +- Original `Request` body is consumed on first parse of any format diff --git a/docs/adr/008-csrf-fetch-metadata.md b/docs/adr/008-csrf-fetch-metadata.md new file mode 100644 index 0000000..c2a6596 --- /dev/null +++ b/docs/adr/008-csrf-fetch-metadata.md @@ -0,0 +1,80 @@ +# ADR-008: CSRF Protection via Fetch Metadata Headers + +**Status:** Accepted + +## Context + +Cross-Site Request Forgery (CSRF) attacks trick authenticated users into +submitting malicious requests. Traditional protections include: + +**Synchronizer token pattern:** + +- Server generates token, embeds in forms +- Server validates token on submission +- Requires server-side token storage +- Session affinity or shared storage needed + +**Double-submit cookie:** + +- Token in cookie and request body/header +- Stateless but vulnerable to subdomain attacks +- Cookie scope issues + +**Fetch Metadata headers:** + +- Browser sends `Sec-Fetch-Site`, `Sec-Fetch-Mode`, `Sec-Fetch-Dest` +- Cannot be set by JavaScript (browser-enforced) +- Stateless, no token management +- Modern browsers only (Safari 16.4+, Chrome 76+, Firefox 90+) + +## Decision + +Implement CSRF protection using Fetch Metadata headers (`Sec-Fetch-Site`) with +`Origin` header as fallback for older browsers. + +```typescript +// Protection logic +if (isStateChangingRequest(method) && hasFormContentType(contentType)) { + const site = headers.get("Sec-Fetch-Site"); + + if (site && site !== "same-origin" && site !== "same-site") { + return forbidden(); + } + + // Fallback: check Origin header + if (!site) { + const origin = headers.get("Origin"); + if (origin && !isSameOrigin(origin, requestUrl)) { + return forbidden(); + } + } +} +``` + +## Consequences + +### Benefits + +- No server-side token storage +- No token synchronization between server instances +- Browser-enforced (cannot be spoofed by JavaScript) +- Simpler implementation than token-based approaches +- No token in forms or AJAX headers + +### Drawbacks + +- Older browsers fall back to Origin check (less reliable) +- Only protects requests with form content types +- Cross-origin APIs require explicit CORS configuration +- Cannot protect GET requests (by design - GET should be safe) + +### Protected Scenarios + +- `POST`/`PUT`/`DELETE` with `application/x-www-form-urlencoded` +- `POST`/`PUT`/`DELETE` with `multipart/form-data` + +### Not Protected (by design) + +- `GET` requests (should be idempotent) +- JSON API requests (require explicit `Content-Type`, mitigates CSRF) +- Requests from same origin/site diff --git a/docs/adr/009-signed-cookies-hmac.md b/docs/adr/009-signed-cookies-hmac.md new file mode 100644 index 0000000..fefc25b --- /dev/null +++ b/docs/adr/009-signed-cookies-hmac.md @@ -0,0 +1,83 @@ +# ADR-009: Signed Cookies with HMAC-SHA256 + +**Status:** Accepted + +## Context + +Cookies storing user data (preferences, session IDs, tokens) are vulnerable to +tampering. A malicious user can modify cookie values to impersonate others or +escalate privileges. + +Protection options: + +**Server-side sessions:** + +- Cookie contains only session ID +- All data stored server-side +- Requires shared storage for horizontal scaling + +**Encrypted cookies:** + +- Data encrypted, unreadable by client +- Key management complexity +- Larger cookies due to encryption overhead + +**Signed cookies:** + +- Data readable but tamper-evident +- HMAC signature validates integrity +- Stateless, no server storage needed + +## Decision + +Support signed cookies using HMAC-SHA256 with constant-time signature +comparison. + +Format: `s:value.signature` + +```typescript +// Setting a signed cookie +setCookie(c, "userId", "12345", { + secret: COOKIE_SECRET, + httpOnly: true, + secure: true, + sameSite: "Strict", +}); +// Result: userId=s:12345.HmacSignatureHere + +// Reading validates signature automatically +const userId = getCookie(c, "userId", { secret: COOKIE_SECRET }); +// Returns "12345" if valid, undefined if tampered +``` + +## Consequences + +### Benefits + +- Detects tampering without server-side state +- Web Crypto API provides native HMAC-SHA256 +- Constant-time comparison prevents timing attacks +- Cookie value remains readable for debugging +- Stateless - works with any number of server instances + +### Drawbacks + +- Secret key management required +- Signed cookies are larger than unsigned (~44 chars for signature) +- Key rotation requires supporting multiple keys temporarily +- Value is visible (not encrypted) - don't store sensitive data + +### Security Requirements + +- Secret must be cryptographically random, minimum 32 bytes +- Secret must not be committed to version control +- Use constant-time comparison to prevent timing attacks +- Always combine with `HttpOnly`, `Secure`, `SameSite` attributes + +### Key Rotation + +When rotating secrets: + +1. Accept signatures from both old and new keys +2. Sign new cookies with new key only +3. After cookie max-age expires, remove old key diff --git a/docs/adr/010-test-against-running-server.md b/docs/adr/010-test-against-running-server.md new file mode 100644 index 0000000..aced23c --- /dev/null +++ b/docs/adr/010-test-against-running-server.md @@ -0,0 +1,91 @@ +# ADR-010: Test Against Running Server + +**Status:** Accepted + +## Context + +Framework tests can be structured as: + +**Unit tests with mocks:** + +- Mock request/response objects +- Test functions in isolation +- Fast execution +- May miss integration issues + +**Integration tests against real server:** + +- Start actual HTTP server +- Make real network requests +- Slower but higher confidence +- Tests match production behavior + +**Hybrid approach:** + +- Unit tests for pure functions +- Integration tests for HTTP behavior + +## Decision + +All Tabi tests use `TabiTestServer` to run actual HTTP requests against a real +server instance. + +```typescript +describe("routing - parameters", () => { + let server: TabiTestServer; + + beforeAll(() => { + const app = new TabiApp(); + app.get("/users/:id", (c) => { + return c.json({ id: c.req.param("id") }); + }); + + server = new TabiTestServer(app); + server.start(); // Random port + }); + + it("extracts route parameters", async () => { + const res = await fetch(server.url("/users/123")); + const json = await res.json(); + expect(json.id).toBe("123"); + }); + + afterAll(async () => { + await server.stop(); + }); +}); +``` + +## Consequences + +### Benefits + +- Tests verify actual behavior, not mocked behavior +- Catches integration issues (header handling, encoding, content negotiation) +- Test execution path matches production +- No complex mocking infrastructure +- Tests serve as executable documentation + +### Drawbacks + +- Slower than unit tests (network overhead) +- Requires port management (use port 0 for random assignment) +- Tests are inherently integration-level +- Harder to test internal edge cases +- Parallel test execution needs care (port conflicts) + +### Test Infrastructure + +`TabiTestServer` provides: + +- Automatic random port assignment +- URL builder for requests +- Clean start/stop lifecycle +- Works with BDD-style tests (describe/it/beforeAll/afterAll) + +### Test Philosophy + +- Test behavior, not implementation +- One test file per feature +- Realistic test data (no "foo", "bar", "test123") +- Mock only at true system boundaries (if ever) diff --git a/docs/adr/011-context-based-state.md b/docs/adr/011-context-based-state.md new file mode 100644 index 0000000..e38b018 --- /dev/null +++ b/docs/adr/011-context-based-state.md @@ -0,0 +1,101 @@ +# ADR-011: Context-Based Request State + +**Status:** Accepted + +## Context + +Middleware needs to share data through the request lifecycle: + +- Authentication middleware sets user info +- Validation middleware sets parsed/validated input +- Request ID middleware sets correlation ID +- Handlers read data set by earlier middleware + +Options: + +**Mutate request object:** + +- Simple but loses type safety +- Risk of property collisions +- Difficult to track what's set where + +**Dependency injection:** + +- Type-safe but complex setup +- Requires container configuration +- Over-engineering for simple cases + +**Context object with get/set:** + +- Explicit storage and retrieval +- Can be type-safe with generics +- Clear ownership of data + +## Decision + +Use `TabiContext` with `set(key, value)` / `get(key)` for request-scoped +state. + +```typescript +// Generic context storage +app.use(async (c, next) => { + c.set("requestTime", Date.now()); + await next(); +}); + +app.get("/", (c) => { + const time = c.get("requestTime"); + return c.json({ time }); +}); +``` + +For validated input, provide type-safe accessor pattern: + +```typescript +const { validate, valid } = validator({ + json: z.object({ name: z.string() }), + query: z.object({ page: z.coerce.number() }), +}); + +app.post("/users", validate, (c) => { + const { json, query } = valid(c); // Fully typed + // json.name is string, query.page is number +}); +``` + +## Consequences + +### Benefits + +- Simple API for ad-hoc state sharing +- No global state - scoped to request +- Validator pattern provides full type safety +- UUID-based internal keys prevent collisions +- Explicit data flow (set before get) + +### Drawbacks + +- Generic `get()` requires explicit type annotation +- Key management is application responsibility +- No compile-time guarantee that key was set +- Validator setup adds complexity for type safety + +### Patterns + +```typescript +// Constants for keys prevent typos +const USER_KEY = "auth:user"; +c.set(USER_KEY, user); +const user = c.get(USER_KEY); + +// Middleware can check if value exists +if (c.get("auth:user")) { + // Authenticated +} +``` + +### Type Safety Spectrum + +1. **Weakest:** `c.get("key")` - returns `unknown` +2. **Medium:** `c.get("key")` - typed but not verified +3. **Strongest:** `valid(c).json` - compile-time type from schema diff --git a/docs/adr/012-composable-security-middleware.md b/docs/adr/012-composable-security-middleware.md new file mode 100644 index 0000000..7caa456 --- /dev/null +++ b/docs/adr/012-composable-security-middleware.md @@ -0,0 +1,95 @@ +# ADR-012: Composable Security Middleware + +**Status:** Accepted + +## Context + +Security features can be packaged as: + +**Monolithic security layer:** + +- Single "security" middleware with all features +- One configuration object +- All-or-nothing approach +- Simpler to enable "secure by default" + +**Composable security middleware:** + +- Each feature is independent middleware +- Applications compose what they need +- More configuration, more control +- Explicit security posture + +## Decision + +Each security feature is a separate, independently usable middleware. + +```typescript +import { securityHeaders } from "@tabirun/app/security-headers"; +import { csrf } from "@tabirun/app/csrf"; +import { cors } from "@tabirun/app/cors"; +import { rateLimit } from "@tabirun/app/rate-limit"; +import { bodySize } from "@tabirun/app/body-size"; + +const app = new TabiApp(); + +// Compose security middleware explicitly +app.use(securityHeaders({ referrerPolicy: "strict-origin" })); +app.use(csrf()); +app.use(cors({ origin: "https://example.com" })); +app.use(rateLimit({ max: 100, windowMs: 60_000 })); +app.use(bodySize({ maxBytes: 1024 * 1024 })); // 1MB +``` + +## Consequences + +### Benefits + +- Applications include only needed protections +- Each middleware has clear, testable responsibility +- Configuration is explicit per feature +- Can compose in custom order +- Easier to understand individual security measures +- Can disable specific protections when needed (e.g., CSRF for API routes) + +### Drawbacks + +- More setup for comprehensive security +- Easy to forget a middleware (no "secure by default") +- Must understand each security measure +- Documentation must show recommended composition + +### Security Modules + +| Module | Purpose | +| ------------------ | ------------------------------------- | +| `security-headers` | CSP, HSTS, X-Frame-Options, etc. | +| `csrf` | Cross-Site Request Forgery protection | +| `cors` | Cross-Origin Resource Sharing | +| `csp` | Content Security Policy (detailed) | +| `rate-limit` | Request rate limiting | +| `body-size` | Request body size limits | + +### Recommended Composition + +For typical web applications: + +```typescript +// Apply to all routes +app.use(securityHeaders()); +app.use(bodySize()); +app.use(rateLimit()); + +// Apply to browser-facing routes +app.use("/app/*", csrf()); + +// Apply to API routes +app.use("/api/*", cors({ origin: ALLOWED_ORIGINS })); +``` + +### Documentation Requirements + +- Clear explanation of what each middleware protects against +- Recommended defaults for common scenarios +- Examples showing composition patterns +- Warnings about common misconfigurations diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..5312e6c --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,63 @@ +# Architecture Decision Records + +This directory contains Architecture Decision Records (ADRs) for Tabi. + +## What is an ADR? + +An ADR captures an architecturally significant decision along with its context +and consequences. ADRs are immutable once accepted; superseding decisions create +new ADRs that reference the original. + +## ADR Status + +- **Proposed** - Under discussion +- **Accepted** - Decision made and in effect +- **Deprecated** - No longer applies but kept for historical context +- **Superseded** - Replaced by a newer ADR (link to replacement) + +## Index + +| ADR | Title | Status | +| -------------------------------------------- | -------------------------------------------------- | -------- | +| [001](001-deno-runtime.md) | Use Deno as the Single Runtime | Accepted | +| [002](002-minimal-philosophy.md) | Minimal Framework Philosophy | Accepted | +| [003](003-monorepo-composable-modules.md) | Monorepo with Independently Composable Modules | Accepted | +| [004](004-standard-schema-validation.md) | Standard Schema for Validation Library Agnosticism | Accepted | +| [005](005-linear-router-default.md) | Linear Router as Default Implementation | Accepted | +| [006](006-lazy-response-materialization.md) | Lazy Response Materialization | Accepted | +| [007](007-memoized-request-body.md) | Memoized Request Body Parsing | Accepted | +| [008](008-csrf-fetch-metadata.md) | CSRF Protection via Fetch Metadata Headers | Accepted | +| [009](009-signed-cookies-hmac.md) | Signed Cookies with HMAC-SHA256 | Accepted | +| [010](010-test-against-running-server.md) | Test Against Running Server | Accepted | +| [011](011-context-based-state.md) | Context-Based Request State | Accepted | +| [012](012-composable-security-middleware.md) | Composable Security Middleware | Accepted | + +## Creating a New ADR + +1. Copy the template below +2. Use the next sequential number +3. Write in past tense for context, present tense for decision +4. Submit PR for review + +### Template + +```markdown +# ADR-NNN: Title + +**Status:** Proposed | Accepted | Deprecated | Superseded by +[ADR-NNN](NNN-title.md) + +**Date:** YYYY-MM-DD + +## Context + +What is the issue that we're seeing that is motivating this decision or change? + +## Decision + +What is the change that we're proposing and/or doing? + +## Consequences + +What becomes easier or more difficult to do because of this change? +```