diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..de0a007 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,67 @@ +name: Deploy VitePress Docs + +on: + push: + branches: [main, feature/docs-site] + paths: + - 'docs/**' + - '.github/workflows/deploy-docs.yml' + workflow_dispatch: # Allow manual triggers + +# Sets permissions for GitHub Pages deployment +permissions: + contents: read + pages: write + id-token: write + +# Prevent concurrent deployments +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # For lastUpdated feature + + - name: Retrieve Node.js version + id: node-version + run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ steps.node-version.outputs.node-version }} + cache: 'npm' + cache-dependency-path: docs/package-lock.json + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Install dependencies + working-directory: docs + run: npm ci + + - name: Build with VitePress + working-directory: docs + run: npm run build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..57a09c3 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +node_modules +.vitepress/dist +.vitepress/cache diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 0000000..dc5a33a --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,70 @@ +import { defineConfig } from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "Trusted Server", + description: "Privacy-preserving edge computing for ad serving and synthetic ID generation", + base: "/trusted-server", + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: 'Home', link: '/' }, + { text: 'Guide', link: '/guide/getting-started' }, + ], + + sidebar: [ + { + text: 'Introduction', + items: [ + { text: 'What is Trusted Server?', link: '/guide/what-is-trusted-server' }, + { text: 'Getting Started', link: '/guide/getting-started' } + ] + }, + { + text: 'Core Concepts', + items: [ + { text: 'Synthetic IDs', link: '/guide/synthetic-ids' }, + { text: 'GDPR Compliance', link: '/guide/gdpr-compliance' }, + { text: 'Ad Serving', link: '/guide/ad-serving' }, + { text: 'First-Party Proxy', link: '/guide/first-party-proxy' }, + { text: 'Creative Processing', link: '/guide/creative-processing' }, + { text: 'Integrations Overview', link: '/guide/integrations-overview' } + ] + }, + { + text: 'Security', + items: [ + { text: 'Request Signing', link: '/guide/request-signing' }, + { text: 'Key Rotation', link: '/guide/key-rotation' } + ] + }, + { + text: 'Development', + items: [ + { text: 'Architecture', link: '/guide/architecture' }, + { text: 'Configuration', link: '/guide/configuration' }, + { text: 'Configuration Reference', link: '/guide/configuration-reference' }, + { text: 'Testing', link: '/guide/testing' }, + { text: 'Integration Guide', link: '/guide/integration-guide' } + ] + }, + { + text: 'Reference', + items: [ + { text: 'API Reference', link: '/guide/api-reference' }, + { text: 'Environment Variables', link: '/guide/environment-variables' }, + { text: 'Error Reference', link: '/guide/error-reference' } + ] + } + ], + + socialLinks: [ + { icon: 'github', link: 'https://github.com/IABTechLab/trusted-server' } + ], + + footer: { + message: 'Released under the Apache License 2.0.', + copyright: 'Copyright © 2018-present IAB Technology Laboratory' + } + } +}) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b25785a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,163 @@ +# Trusted Server Documentation + +VitePress documentation site for Trusted Server. + +## Local Development + +### Prerequisites + +- Node.js 24.10.0 (or version specified in `.tool-versions`) +- npm + +### Setup + +```bash +# Install dependencies +npm install + +# Start dev server (available at localhost:5173) +npm run dev +``` + +### Build + +```bash +# Build for production +npm run build + +# Preview production build +npm run preview +``` + +## GitHub Pages Deployment + +The documentation is automatically deployed to GitHub Pages when changes are pushed to the `main` branch. + +### Setup GitHub Pages + +1. Go to repository **Settings** → **Pages** +2. Under **Source**, select **GitHub Actions** +3. The workflow in `.github/workflows/deploy-docs.yml` will automatically deploy on push to `main` + +### Custom Domain Setup + +1. **Update CNAME file**: Edit `docs/public/CNAME` with your domain: + ``` + docs.yourdomain.com + ``` + +2. **Configure DNS**: Add DNS records at your domain provider: + + **Option A - CNAME Record** (recommended for subdomains): + ``` + Type: CNAME + Name: docs + Value: iabtechlab.github.io + ``` + + **Option B - A Records** (for apex domains): + ``` + Type: A + Name: @ + Value: 185.199.108.153 + Value: 185.199.109.153 + Value: 185.199.110.153 + Value: 185.199.111.153 + ``` + +3. **Verify in GitHub**: + - Go to **Settings** → **Pages** + - Enter your custom domain + - Wait for DNS check to pass + - Enable "Enforce HTTPS" + +### Workflow Details + +**Trigger**: +- Push to `main` branch (only when `docs/**` changes) +- Manual trigger via Actions tab + +**Build Process**: +1. Checkout repository with full history (for `lastUpdated` feature) +2. Setup Node.js (version from `.tool-versions`) +3. Install dependencies (`npm ci`) +4. Build VitePress site (`npm run build`) +5. Upload build artifact +6. Deploy to GitHub Pages + +**Permissions Required**: +- `contents: read` - Read repository +- `pages: write` - Deploy to Pages +- `id-token: write` - OIDC token for deployment + +## Troubleshooting + +### Build Fails in GitHub Actions + +**Check**: +- Node.js version matches `.tool-versions` +- All dependencies in `package.json` are correct +- Build succeeds locally (`npm run build`) + +**View Logs**: +1. Go to **Actions** tab in GitHub +2. Click on failed workflow run +3. Review build logs + +### Custom Domain Not Working + +**Check**: +- DNS records propagated (use `dig docs.yourdomain.com`) +- CNAME file exists in `docs/public/CNAME` +- Custom domain verified in GitHub Pages settings +- HTTPS enforced (may take up to 24 hours) + +**DNS Verification**: +```bash +# Check CNAME record +dig docs.yourdomain.com CNAME + +# Check A records (for apex domain) +dig yourdomain.com A +``` + +### 404 Errors + +**Check**: +- VitePress `base` config (should not be set for custom domains) +- Links use correct paths (start with `/`) +- Build output in `docs/.vitepress/dist` is correct + +## Project Structure + +``` +docs/ +├── .vitepress/ +│ ├── config.mts # VitePress configuration +│ └── dist/ # Build output (gitignored) +├── guide/ # Documentation pages +│ ├── getting-started.md +│ ├── configuration.md +│ └── ... +├── public/ # Static assets +│ └── CNAME # Custom domain file +├── index.md # Homepage +├── package.json # Dependencies +└── README.md # This file +``` + +## Contributing + +When adding new documentation: + +1. Create `.md` files in `docs/guide/` +2. Update sidebar in `docs/.vitepress/config.mts` +3. Test locally with `npm run dev` +4. Build and verify with `npm run build && npm run preview` +5. Commit and push to trigger deployment + +## Links + +- **Production**: (Configure your custom domain) +- **GitHub Repo**: https://github.com/IABTechLab/trusted-server +- **VitePress Docs**: https://vitepress.dev diff --git a/docs/guide/ad-serving.md b/docs/guide/ad-serving.md new file mode 100644 index 0000000..6a05ed6 --- /dev/null +++ b/docs/guide/ad-serving.md @@ -0,0 +1,115 @@ +# Ad Serving + +Learn how Trusted Server handles privacy-compliant ad serving. + +## Overview + +Trusted Server provides edge-based ad serving with built-in GDPR compliance and real-time bidding support. + +## Supported Integrations + +### Equativ + +Primary ad server integration with support for: +- Direct ad requests +- Creative proxying +- Click tracking +- Impression tracking + +### Prebid + +Real-time bidding integration: +- Header bidding support +- Bid caching +- Timeout management +- Winner selection + +## Ad Request Flow + +1. Request validation +2. GDPR consent check +3. Synthetic ID generation (if consented) +4. Ad server request +5. Response processing +6. Creative delivery + +## Configuration + +Configure ad servers in `trusted-server.toml`: + +```toml +[ad_servers.equativ] +endpoint = "https://ad-server.example.com" +timeout_ms = 1000 +enabled = true + +[prebid] +timeout_ms = 1500 +cache_ttl = 300 +``` + +## Creative Handling + +### Proxy Mode + +Creatives can be proxied through Trusted Server for: +- Security scanning +- Content modification +- Click tracking injection +- GDPR compliance + +### Direct Mode + +Creatives served directly from ad server: +- Lower latency +- Reduced edge load +- Less control over content + +## Tracking + +### Impression Tracking + +```javascript +// Placeholder example +trustedServer.trackImpression({ + adId: 'ad-123', + syntheticId: 'synthetic-xyz', + consent: true +}); +``` + +### Click Tracking + +Click tracking with privacy preservation: +- No PII in URLs +- Synthetic ID only (with consent) +- Encrypted parameters + +## Performance + +### Edge Caching + +- Bid responses cached at edge +- Creative assets cached +- Configuration cached +- Reduced origin requests + +### Timeouts + +Configurable timeouts for: +- Ad server requests +- Prebid auctions +- Creative fetching + +## Best Practices + +1. Set appropriate timeouts for your use case +2. Enable caching for frequently requested ads +3. Monitor ad server response times +4. Use proxy mode for security-sensitive content +5. Implement fallback ads + +## Next Steps + +- Review [Architecture](/guide/architecture) +- Configure [Testing](/guide/testing) diff --git a/docs/guide/api-reference.md b/docs/guide/api-reference.md new file mode 100644 index 0000000..b10d345 --- /dev/null +++ b/docs/guide/api-reference.md @@ -0,0 +1,581 @@ +# API Reference + +Quick reference for all Trusted Server HTTP endpoints. + +## Endpoint Categories + +- [First-Party Endpoints](#first-party-endpoints) - Core ad serving and proxying +- [Request Signing](#request-signing-endpoints) - Cryptographic signing and key management +- [TSJS Library](#tsjs-library-endpoint) - JavaScript library serving +- [Integration Endpoints](#integration-endpoints) - Third-party service proxying + +--- + +## First-Party Endpoints + +### GET /first-party/ad + +Server-side ad rendering endpoint. Returns complete HTML for a single ad slot. + +**Query Parameters:** +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `slot` | string | Yes | Ad slot identifier (matches ad unit code) | +| `w` | integer | Yes | Ad width in pixels | +| `h` | integer | Yes | Ad height in pixels | + +**Response:** +- **Content-Type:** `text/html; charset=utf-8` +- **Body:** Complete HTML creative with first-party proxying applied + +**Example:** +```bash +curl "https://edge.example.com/first-party/ad?slot=header-banner&w=728&h=90" +``` + +**Response Headers:** +- `X-Synthetic-Trusted-Server` - Stable synthetic ID +- `X-Synthetic-Fresh` - One-time fresh ID + +**Use Cases:** +- Server-side ad rendering +- Direct iframe embedding +- First-party ad delivery + +--- + +### POST /third-party/ad + +Client-side auction endpoint for TSJS library. + +**Request Body:** +```json +{ + "adUnits": [ + { + "code": "header-banner", + "mediaTypes": { + "banner": { + "sizes": [[728, 90], [970, 250]] + } + } + } + ], + "config": { + "debug": false + } +} +``` + +**Response:** +```json +{ + "seatbid": [ + { + "bid": [ + { + "impid": "header-banner", + "adm": "...", + "price": 1.50, + "w": 728, + "h": 90 + } + ] + } + ] +} +``` + +**Example:** +```bash +curl -X POST https://edge.example.com/third-party/ad \ + -H "Content-Type: application/json" \ + -d '{"adUnits":[{"code":"banner","mediaTypes":{"banner":{"sizes":[[300,250]]}}}]}' +``` + +--- + +### GET /first-party/proxy + +Unified proxy for resources referenced by creatives (images, scripts, CSS, etc.). + +**Query Parameters:** +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `tsurl` | string | Yes | Target URL without query parameters (base URL) | +| `tstoken` | string | Yes | Base64 URL-safe SHA-256 digest of encrypted full target URL | +| `*` | any | No | Original target URL query parameters (preserved as-is) | + +**Response:** +- **Content-Type:** Mirrors upstream or inferred from content +- **Body:** Proxied resource content + - HTML responses: Rewritten with creative processor + - Image responses: Proxied with content-type inference + - Other: Passed through + +**Behavior:** +- Validates `tstoken` against reconstructed URL +- Follows redirects (301/302/303/307/308, max 4 hops) +- Injects synthetic ID as `synthetic_id` query parameter +- Logs 1×1 pixel impressions + +**Example:** +```bash +# Original URL: https://ad.doubleclick.net/pixel?id=123&type=view +# Signed proxy URL: +curl "https://edge.example.com/first-party/proxy?tsurl=https://ad.doubleclick.net/pixel&id=123&type=view&tstoken=abc123xyz..." +``` + +**Error Responses:** +- `400 Bad Request` - Missing or invalid `tstoken` +- `403 Forbidden` - Token validation failed +- `500 Internal Server Error` - Upstream fetch failed + +--- + +### GET /first-party/click + +Click tracking redirect endpoint. + +**Query Parameters:** +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `tsurl` | string | Yes | Target redirect URL without query parameters | +| `tstoken` | string | Yes | Base64 URL-safe SHA-256 digest of encrypted full target URL | +| `*` | any | No | Original target URL query parameters | + +**Response:** +- **Status:** `302 Found` +- **Location:** Reconstructed target URL with synthetic ID injected + +**Behavior:** +- Validates `tstoken` against reconstructed URL +- Injects `synthetic_id` query parameter +- Logs click metadata (tsurl, referer, user agent) +- Does not proxy content (redirect only) + +**Example:** +```bash +curl -I "https://edge.example.com/first-party/click?tsurl=https://advertiser.com/landing&campaign=123&tstoken=xyz..." +# → 302 Location: https://advertiser.com/landing?campaign=123&synthetic_id=abc123 +``` + +--- + +### GET/POST /first-party/sign + +URL signing endpoint. Returns signed first-party proxy URL for a given target URL. + +**Request Methods:** GET or POST + +**GET Request:** +```bash +curl "https://edge.example.com/first-party/sign?url=https://external.com/pixel.gif" +``` + +**POST Request:** +```bash +curl -X POST https://edge.example.com/first-party/sign \ + -H "Content-Type: application/json" \ + -d '{"url":"https://external.com/pixel.gif"}' +``` + +**Response:** +```json +{ + "signed_url": "https://edge.example.com/first-party/proxy?tsurl=https://external.com/pixel.gif&tstoken=abc123..." +} +``` + +**Use Cases:** +- TSJS creative runtime (image/iframe proxying) +- Dynamic URL signing in client-side code +- Testing proxy URL generation + +--- + +### POST /first-party/proxy-rebuild + +URL mutation recovery endpoint. Rebuilds signed proxy URL after creative JavaScript modifies query parameters. + +**Request Body:** +```json +{ + "tsclick": "https://edge.example.com/first-party/click?tsurl=https://advertiser.com&campaign=123&tstoken=original...", + "add": { + "utm_source": "banner" + }, + "del": ["old_param"] +} +``` + +**Response:** +```json +{ + "url": "https://edge.example.com/first-party/click?tsurl=https://advertiser.com&campaign=123&utm_source=banner&tstoken=new..." +} +``` + +**Use Cases:** +- TSJS click guard (automatic URL repair) +- Handling creative JavaScript that modifies tracking URLs + +--- + +## Request Signing Endpoints + +### GET /.well-known/ts.jwks.json + +Returns active public keys in JWKS (JSON Web Key Set) format for signature verification. + +**Response:** +```json +{ + "keys": [ + { + "kty": "OKP", + "crv": "Ed25519", + "kid": "ts-2025-01-A", + "use": "sig", + "x": "UVTi04QLrIuB7jXpVfHjUTVN5aIdcbPNr50umTtN8pw" + } + ] +} +``` + +**Example:** +```bash +curl https://edge.example.com/.well-known/ts.jwks.json +``` + +**Use Cases:** +- Signature verification by downstream systems +- Key rotation validation +- Integration testing + +--- + +### POST /verify-signature + +Verifies a signature against a payload and key ID. + +**Request Body:** +```json +{ + "payload": "base64-encoded-data", + "signature": "base64-signature", + "kid": "ts-2025-01-A" +} +``` + +**Response (Success):** +```json +{ + "verified": true, + "kid": "ts-2025-01-A", + "message": "Signature verified successfully" +} +``` + +**Response (Failure):** +```json +{ + "verified": false, + "kid": "ts-2025-01-A", + "message": "Invalid signature" +} +``` + +**Example:** +```bash +curl -X POST https://edge.example.com/verify-signature \ + -H "Content-Type: application/json" \ + -d '{"payload":"SGVsbG8gV29ybGQ=","signature":"abc123...","kid":"ts-2025-01-A"}' +``` + +--- + +### POST /admin/keys/rotate + +Generates and activates a new signing key. + +**Authentication:** Requires basic auth (configured via `handlers` in `trusted-server.toml`) + +**Request Body (Optional):** +```json +{ + "kid": "custom-key-id" +} +``` + +If omitted, auto-generates date-based ID (e.g., `ts-2025-01-15-A`). + +**Response:** +```json +{ + "new_kid": "ts-2025-01-15-A", + "previous_kid": "ts-2025-01-14-A", + "active_kids": ["ts-2025-01-15-A", "ts-2025-01-14-A"], + "message": "Key rotation successful" +} +``` + +**Example:** +```bash +curl -X POST https://edge.example.com/admin/keys/rotate \ + -u admin:password \ + -H "Content-Type: application/json" +``` + +**Behavior:** +- Keeps both new and previous key active +- Updates `current-kid` to new key +- Preserves old key for graceful transition + +See [Key Rotation Guide](./key-rotation.md) for workflow details. + +--- + +### POST /admin/keys/deactivate + +Deactivates or deletes a signing key. + +**Authentication:** Requires basic auth + +**Request Body:** +```json +{ + "kid": "ts-2025-01-14-A", + "delete": false +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `kid` | string | Yes | Key ID to deactivate | +| `delete` | boolean | No | If true, permanently removes key (default: false) | + +**Response:** +```json +{ + "kid": "ts-2025-01-14-A", + "active_kids": ["ts-2025-01-15-A"], + "message": "Key deactivated successfully" +} +``` + +**Example:** +```bash +curl -X POST https://edge.example.com/admin/keys/deactivate \ + -u admin:password \ + -H "Content-Type: application/json" \ + -d '{"kid":"ts-2025-01-14-A","delete":true}' +``` + +--- + +## TSJS Library Endpoint + +### GET /static/tsjs=`` + +Serves the TSJS (Trusted Server JavaScript) library. + +**Path Pattern:** `/static/tsjs=?v=` + +**Supported Filenames:** +- `tsjs-unified.js` +- `tsjs-unified.min.js` + +**Query Parameters:** +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `v` | string | No | Cache-busting hash (SHA256 of bundle contents) | + +**Response:** +- **Content-Type:** `application/javascript; charset=utf-8` +- **Body:** TSJS bundle (IIFE format) +- **Headers:** ETag for caching + +**Example:** +```html + +``` + +**Module Selection:** +Controlled at build time via `TSJS_MODULES` environment variable: +```bash +TSJS_MODULES=creative,ext,permutive cargo build +``` + +See [Configuration](./configuration.md) for TSJS build options. + +--- + +## Integration Endpoints + +### Prebid Integration + +#### GET /first-party/ad +See [First-Party Endpoints](#get-first-party-ad) above. + +#### POST /third-party/ad +See [First-Party Endpoints](#post-third-party-ad) above. + +#### GET /prebid.js (Optional) +Returns empty JavaScript to override Prebid.js when `script_handler` is configured. + +**Configuration:** +```toml +[integrations.prebid] +script_handler = "/prebid.js" +``` + +**Response:** +- **Content-Type:** `application/javascript; charset=utf-8` +- **Body:** `// Prebid.js override by Trusted Server` +- **Cache:** `immutable, max-age=31536000` + +--- + +### Permutive Integration + +#### GET /integrations/permutive/sdk +Serves Permutive SDK from first-party domain. + +**Response:** +- **Content-Type:** `application/javascript; charset=utf-8` +- **Body:** Permutive SDK fetched from `{organization_id}.edge.permutive.app/{workspace_id}-web.js` +- **Cache:** 1 hour (configurable via `cache_ttl_seconds`) + +#### GET/POST /integrations/permutive/api/* +Proxies to `api.permutive.com`. + +**Example:** +```bash +curl https://edge.example.com/integrations/permutive/api/settings +# → Proxies to https://api.permutive.com/settings +``` + +#### GET/POST /integrations/permutive/secure-signal/* +Proxies to `secure-signals.permutive.app`. + +#### GET/POST /integrations/permutive/events/* +Proxies to `events.permutive.app` for event tracking. + +#### GET/POST /integrations/permutive/sync/* +Proxies to `sync.permutive.com` for ID synchronization. + +#### GET /integrations/permutive/cdn/* +Proxies to `cdn.permutive.com` for static assets. + +--- + +### Testlight Integration + +#### POST /integrations/testlight/auction +Testing auction endpoint with synthetic ID injection. + +**Request Body:** +```json +{ + "user": { + "id": null + }, + "imp": [ + { "id": "slot-1" } + ] +} +``` + +**Response:** +Proxies to configured endpoint with `user.id` populated with synthetic ID. + +**Response Headers:** +- `X-Synthetic-Trusted-Server` - Stable synthetic ID +- `X-Synthetic-Fresh` - One-time fresh ID + +--- + +## Error Responses + +All endpoints use consistent error response format: + +```json +{ + "error": "Error type", + "message": "Detailed error description", + "details": { + "field": "Additional context" + } +} +``` + +**Common HTTP Status Codes:** +| Code | Meaning | Common Causes | +|------|---------|---------------| +| 400 | Bad Request | Missing required parameters, invalid JSON | +| 401 | Unauthorized | Missing or invalid basic auth credentials | +| 403 | Forbidden | Invalid token signature, disabled integration | +| 404 | Not Found | Unknown endpoint, missing resource | +| 500 | Internal Server Error | Upstream service failure, configuration error | +| 502 | Bad Gateway | Backend service unavailable | +| 504 | Gateway Timeout | Backend service timeout exceeded | + +--- + +## Authentication + +### Basic Authentication + +Endpoints under protected paths require HTTP Basic Authentication: + +**Configuration:** +```toml +[[handlers]] +path = "^/admin" +username = "admin" +password = "secure-password" +``` + +**Usage:** +```bash +curl -u admin:secure-password https://edge.example.com/admin/keys/rotate +``` + +**Protected Endpoints:** +- `/admin/keys/rotate` +- `/admin/keys/deactivate` +- Any paths matching configured `handlers` patterns + +--- + +## Rate Limiting + +Trusted Server relies on Fastly's built-in rate limiting. Configure in Fastly dashboard: + +**Recommended Limits:** +- Public endpoints: 1000 req/min per IP +- Admin endpoints: 10 req/min per IP +- TSJS serving: 10000 req/min (highly cacheable) + +--- + +## CORS + +CORS headers are not enabled by default. Configure `response_headers` for cross-origin requests: + +```toml +[response_headers] +Access-Control-Allow-Origin = "*" +Access-Control-Allow-Methods = "GET, POST, OPTIONS" +Access-Control-Allow-Headers = "Content-Type, Authorization" +``` + +--- + +## Next Steps + +- Explore [Integrations Overview](./integrations-overview.md) +- Learn about [Configuration](./configuration.md) +- Review [Error Reference](./error-reference.md) +- Understand [Environment Variables](./environment-variables.md) diff --git a/docs/guide/architecture.md b/docs/guide/architecture.md new file mode 100644 index 0000000..822b302 --- /dev/null +++ b/docs/guide/architecture.md @@ -0,0 +1,122 @@ +# Architecture + +Understanding the architecture of Trusted Server. + +## High-Level Overview + +Trusted Server is built as a Rust-based edge computing application that runs on Fastly Compute platform. + +``` +┌─────────────┐ +│ Browser │ +└──────┬──────┘ + │ + ▼ +┌─────────────────────┐ +│ Trusted Server │ +│ (Fastly Edge) │ +│ ┌───────────────┐ │ +│ │ GDPR Check │ │ +│ │ Synthetic IDs │ │ +│ │ Ad Serving │ │ +│ └───────────────┘ │ +└──────┬──────────────┘ + │ + ▼ +┌─────────────────────┐ +│ Ad Servers │ +│ KV Stores │ +│ External APIs │ +└─────────────────────┘ +``` + +## Core Components + +### trusted-server-common + +Core library containing shared functionality: +- Synthetic ID generation +- Cookie handling +- HTTP abstractions +- GDPR consent management +- Ad server integrations + +### trusted-server-fastly + +Fastly-specific implementation: +- Main application entry point +- Fastly SDK integration +- Request/response handling +- KV store access + +## Design Patterns + +### RequestWrapper Trait + +Abstracts HTTP request handling to support different backends: + +```rust +// Placeholder example +pub trait RequestWrapper { + fn get_header(&self, name: &str) -> Option; + fn get_cookie(&self, name: &str) -> Option; + // ... +} +``` + +### Settings-Driven Configuration + +External configuration via `trusted-server.toml` allows deployment-time customization without code changes. + +### Privacy-First Design + +All tracking operations require explicit GDPR consent checks before execution. + +## Data Flow + +1. **Request Ingress** - Request arrives at Fastly edge +2. **Consent Validation** - GDPR consent checked +3. **ID Generation** - Synthetic ID generated (if consented) +4. **Ad Request** - Backend ad server called +5. **Response Processing** - Creative processed and modified +6. **Response Egress** - Response sent to browser + +## Storage + +### Fastly KV Store + +Used for: +- Counter storage +- Domain mappings +- Configuration cache +- Synthetic ID state + +### No User Data Persistence + +User data is not persisted in storage - only processed in-flight at the edge. + +## Performance Characteristics + +- **Low Latency** - Edge execution near users +- **High Throughput** - Parallel request processing +- **Global Distribution** - Fastly's global network +- **Caching** - Aggressive edge caching + +## Security + +- **HMAC-based IDs** - Cryptographically secure identifiers +- **No PII Storage** - Privacy by design +- **Request Signing** - Optional request authentication +- **Content Security** - Creative scanning and modification + +## WebAssembly Target + +Compiled to `wasm32-wasip1` for Fastly Compute: +- Sandboxed execution +- Fast cold starts +- Efficient resource usage + +## Next Steps + +- Learn about [Configuration](/guide/configuration) +- Set up [Testing](/guide/testing) diff --git a/docs/guide/configuration-reference.md b/docs/guide/configuration-reference.md new file mode 100644 index 0000000..c820ca7 --- /dev/null +++ b/docs/guide/configuration-reference.md @@ -0,0 +1,855 @@ +# Configuration Reference + +Complete reference for all configuration options in Trusted Server. + +## Overview + +Trusted Server uses a TOML-based configuration system with environment variable overrides. Configuration is loaded from: + +1. **`trusted-server.toml`** - Base configuration file +2. **Environment Variables** - Runtime overrides with `TRUSTED_SERVER__` prefix + +### Quick Example + +```toml +# trusted-server.toml +[publisher] +domain = "publisher.com" +cookie_domain = ".publisher.com" +origin_url = "https://origin.publisher.com" +proxy_secret = "your-secure-secret-here" + +[synthetic] +counter_store = "counter_store" +opid_store = "opid_store" +secret_key = "your-hmac-secret-here" +template = "{{ client_ip }}:{{ user_agent }}" +``` + +## Environment Variable Overrides + +### Format + +``` +TRUSTED_SERVER__SECTION__SUBSECTION__FIELD +``` + +**Rules**: +- Prefix: `TRUSTED_SERVER` +- Separator: `__` (double underscore) +- Case: UPPERCASE +- Sections: Match TOML hierarchy + +### Examples + +**Simple Field**: +```bash +TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.com +``` + +**Nested Field**: +```bash +TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL=https://prebid.example/auction +``` + +**Array Field (JSON)**: +```bash +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS='["kargo","rubicon"]' +``` + +**Array Field (Indexed)**: +```bash +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS__0=kargo +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS__1=rubicon +``` + +**Array Field (Comma-Separated)**: +```bash +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS=kargo,rubicon,appnexus +``` + +## Publisher Configuration + +Core publisher settings for domain, origin, and proxy configuration. + +### `[publisher]` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `domain` | String | Yes | Publisher's domain name | +| `cookie_domain` | String | Yes | Domain for setting cookies (typically with leading dot) | +| `origin_url` | String | Yes | Full URL of publisher origin server | +| `proxy_secret` | String | Yes | Secret key for encrypting/signing proxy URLs | + +**Example**: +```toml +[publisher] +domain = "publisher.com" +cookie_domain = ".publisher.com" # Includes subdomains +origin_url = "https://origin.publisher.com" +proxy_secret = "change-me-to-secure-random-value" +``` + +**Environment Override**: +```bash +TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.com +TRUSTED_SERVER__PUBLISHER__COOKIE_DOMAIN=.publisher.com +TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://origin.publisher.com +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=your-secret-here +``` + +### Field Details + +#### `domain` + +**Purpose**: Primary domain for the publisher. + +**Usage**: +- Displayed in synthetic ID generation +- Used in template variables (`publisher_domain`) +- Part of request context + +**Format**: Hostname without protocol or path +- ✅ `publisher.com` +- ✅ `www.publisher.com` +- ❌ `https://publisher.com` +- ❌ `publisher.com/path` + +#### `cookie_domain` + +**Purpose**: Domain scope for synthetic ID cookies. + +**Usage**: +- Set on `synthetic_id` cookie +- Controls cookie sharing across subdomains + +**Format**: Domain with optional leading dot +- `.publisher.com` - Shares across all subdomains +- `publisher.com` - Exact domain only + +**Best Practice**: Use leading dot (`.publisher.com`) for subdomain sharing. + +#### `origin_url` + +**Purpose**: Backend origin server URL for publisher content. + +**Usage**: +- Fallback proxy target for non-integration requests +- HTML processing rewrites origin URLs to request host +- Base for relative URL resolution + +**Format**: Full URL with protocol +- ✅ `https://origin.publisher.com` +- ✅ `https://origin.publisher.com:8080` +- ✅ `http://192.168.1.1:9000` +- ❌ `origin.publisher.com` (missing protocol) + +**Port Handling**: Includes port if non-standard (not 80/443). + +#### `proxy_secret` + +**Purpose**: Secret key for HMAC-SHA256 signing of proxy URLs. + +**Security**: +- Keep confidential and secure +- Rotate periodically (90 days recommended) +- Use cryptographically random values (32+ bytes) +- Never commit to version control + +**Generation**: +```bash +# Generate secure random secret +openssl rand -base64 32 +``` + +**Usage**: +- Signs `/first-party/proxy` URLs +- Signs `/first-party/click` URLs +- Validates incoming proxy requests +- Prevents URL tampering + +::: danger Security Warning +Changing `proxy_secret` invalidates all existing signed URLs. Plan rotations carefully and use graceful transition periods. +::: + +## Synthetic ID Configuration + +Settings for generating privacy-preserving synthetic identifiers. + +### `[synthetic]` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `counter_store` | String | Yes | Fastly KV store name for counters | +| `opid_store` | String | Yes | Fastly KV store name for publisher ID mappings | +| `secret_key` | String | Yes | HMAC secret for ID generation | +| `template` | String | Yes | Handlebars template for ID composition | + +**Example**: +```toml +[synthetic] +counter_store = "counter_store" +opid_store = "opid_store" +secret_key = "your-secure-hmac-secret" +template = "{{ client_ip }}:{{ user_agent }}:{{ first_party_id }}" +``` + +**Environment Override**: +```bash +TRUSTED_SERVER__SYNTHETIC__COUNTER_STORE=counter_store +TRUSTED_SERVER__SYNTHETIC__OPID_STORE=opid_store +TRUSTED_SERVER__SYNTHETIC__SECRET_KEY=your-secret +TRUSTED_SERVER__SYNTHETIC__TEMPLATE="{{ client_ip }}:{{ user_agent }}" +``` + +### Field Details + +#### `counter_store` + +**Purpose**: Fastly KV store for synthetic ID counters. + +**Usage**: +- Stores incrementing counters per domain +- Ensures ID uniqueness +- Accessed via Fastly KV Store API + +**Setup**: +```bash +# Create KV store +fastly kv-store create --name=counter_store +``` + +**Data Format**: +```json +{ + "publisher.com": 12345, + "another.com": 67890 +} +``` + +#### `opid_store` + +**Purpose**: Fastly KV store for publisher-provided ID mappings. + +**Usage**: +- Maps publisher IDs to synthetic IDs +- Enables first-party ID integration +- Optional (used if publisher provides IDs) + +**Setup**: +```bash +# Create KV store +fastly kv-store create --name=opid_store +``` + +**Data Format**: +```json +{ + "publisher-id-123": "synthetic-abc", + "publisher-id-456": "synthetic-def" +} +``` + +#### `secret_key` + +**Purpose**: HMAC secret for deterministic ID generation. + +**Security**: +- Minimum 8 bytes (validation enforced) +- Cannot be `"secret-key"` (reserved/invalid) +- Rotate periodically for security +- Store securely (environment variable recommended) + +**Generation**: +```bash +# Generate secure random key +openssl rand -hex 32 +``` + +**Validation**: Application startup fails if: +- Empty string +- Exactly `"secret-key"` (default placeholder) +- Less than 1 character + +#### `template` + +**Purpose**: Handlebars template defining ID composition. + +**Available Variables**: + +| Variable | Description | Example | +|----------|-------------|---------| +| `client_ip` | Client IP address | `192.168.1.1` | +| `user_agent` | User-Agent header | `Mozilla/5.0...` | +| `first_party_id` | Publisher-provided ID | `user-123` | +| `auth_user_id` | Authenticated user ID | `auth-456` | +| `publisher_domain` | Publisher domain | `publisher.com` | +| `accept_language` | Accept-Language header | `en-US,en;q=0.9` | + +**Template Examples**: + +**Simple (IP + UA)**: +```toml +template = "{{ client_ip }}:{{ user_agent }}" +``` + +**With First-Party ID**: +```toml +template = "{{ first_party_id }}:{{ client_ip }}" +``` + +**Comprehensive**: +```toml +template = "{{ client_ip }}:{{ user_agent }}:{{ first_party_id }}:{{ auth_user_id }}:{{ publisher_domain }}:{{ accept_language }}" +``` + +**Validation**: Must be non-empty string. + +::: tip Template Design +Choose template variables based on your privacy and uniqueness requirements: +- **More variables** = More unique IDs, less privacy +- **Fewer variables** = More privacy, potential collisions +- **Include `first_party_id`** for publisher ID integration +::: + +## Response Headers + +Custom headers added to all responses. + +### `[response_headers]` + +**Purpose**: Add custom HTTP headers to every response. + +**Format**: Key-value pairs + +**Example**: +```toml +[response_headers] +X-Custom-Header = "custom value" +X-Publisher-ID = "pub-12345" +X-Environment = "production" +Cache-Control = "public, max-age=3600" +``` + +**Environment Override**: +```bash +TRUSTED_SERVER__RESPONSE_HEADERS__X_CUSTOM_HEADER="custom value" +``` + +**Use Cases**: +- Custom tracking headers +- Cache control overrides +- Debugging identifiers +- CORS headers (if needed) + +::: warning Header Precedence +Custom headers may be overwritten by application logic. Standard headers (`Content-Type`, `Content-Length`) are controlled by the application. +::: + +## Request Signing + +Configuration for Ed25519 request signing and JWKS management. + +### `[request_signing]` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `enabled` | Boolean | No (default: false) | Enable request signing features | +| `config_store_id` | String | If enabled | Fastly Config Store ID for JWKS | +| `secret_store_id` | String | If enabled | Fastly Secret Store ID for private keys | + +**Example**: +```toml +[request_signing] +enabled = true +config_store_id = "01GXXX" # From Fastly dashboard +secret_store_id = "01GYYY" # From Fastly dashboard +``` + +**Environment Override**: +```bash +TRUSTED_SERVER__REQUEST_SIGNING__ENABLED=true +TRUSTED_SERVER__REQUEST_SIGNING__CONFIG_STORE_ID=01GXXX +TRUSTED_SERVER__REQUEST_SIGNING__SECRET_STORE_ID=01GYYY +``` + +### Store Setup + +**Config Store** (for public keys): +```bash +# Create store +fastly config-store create --name=jwks_store + +# Get store ID +fastly config-store list +``` + +**Secret Store** (for private keys): +```bash +# Create store +fastly secret-store create --name=signing_keys + +# Get store ID +fastly secret-store list +``` + +**Link to Service** (`fastly.toml`): +```toml +[setup.config_stores.jwks_store] + +[setup.secret_stores.signing_keys] +``` + +See [Request Signing](/guide/request-signing) and [Key Rotation](/guide/key-rotation) for usage. + +## Basic Authentication Handlers + +Path-based HTTP Basic Authentication. + +### `[[handlers]]` + +**Purpose**: Protect specific paths with username/password authentication. + +**Format**: Array of handler objects + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `path` | String (Regex) | Yes | Regular expression matching paths | +| `username` | String | Yes | HTTP Basic Auth username | +| `password` | String | Yes | HTTP Basic Auth password | + +**Example**: +```toml +# Single handler +[[handlers]] +path = "^/admin" +username = "admin" +password = "secure-password" + +# Multiple handlers +[[handlers]] +path = "^/secure" +username = "user1" +password = "pass1" + +[[handlers]] +path = "^/api/private" +username = "api-user" +password = "api-pass" +``` + +**Environment Override**: +```bash +# Handler 0 +TRUSTED_SERVER__HANDLERS__0__PATH="^/admin" +TRUSTED_SERVER__HANDLERS__0__USERNAME="admin" +TRUSTED_SERVER__HANDLERS__0__PASSWORD="secure-password" + +# Handler 1 +TRUSTED_SERVER__HANDLERS__1__PATH="^/api/private" +TRUSTED_SERVER__HANDLERS__1__USERNAME="api-user" +TRUSTED_SERVER__HANDLERS__1__PASSWORD="api-pass" +``` + +### Path Patterns + +**Regex Syntax**: Standard Rust regex patterns + +**Examples**: + +```toml +# Exact path +path = "^/admin$" # Only /admin + +# Prefix match +path = "^/admin" # /admin, /admin/users, /admin/settings + +# Multiple paths +path = "^/(admin|secure|private)" + +# File extension +path = "\\.pdf$" # All PDF files + +# Complex pattern +path = "^/api/v[0-9]+/private" # /api/v1/private, /api/v2/private +``` + +**Validation**: Application startup fails if regex is invalid. + +### Security Considerations + +**Password Storage**: +- Stored in plain text in config +- Use environment variables in production +- Rotate passwords regularly +- Consider using Fastly Secret Store + +**Limitations**: +- HTTP Basic Auth (not OAuth/JWT) +- Single username/password per path +- No role-based access control +- No rate limiting (add at edge) + +::: warning Production Use +For production, store credentials in environment variables: +```bash +TRUSTED_SERVER__HANDLERS__0__PASSWORD=$(cat /run/secrets/admin_password) +``` +::: + +## URL Rewrite Configuration + +Control which domains are excluded from first-party rewriting. + +### `[rewrite]` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `exclude_domains` | Array[String] | No (default: []) | Domains to skip rewriting | + +**Example**: +```toml +[rewrite] +exclude_domains = [ + "*.cdn.trusted-partner.com", # Wildcard + "first-party.publisher.com", # Exact match + "localhost", # Development +] +``` + +**Environment Override**: +```bash +# JSON array +TRUSTED_SERVER__REWRITE__EXCLUDE_DOMAINS='["*.cdn.example.com","localhost"]' + +# Indexed +TRUSTED_SERVER__REWRITE__EXCLUDE_DOMAINS__0="*.cdn.example.com" +TRUSTED_SERVER__REWRITE__EXCLUDE_DOMAINS__1="localhost" + +# Comma-separated +TRUSTED_SERVER__REWRITE__EXCLUDE_DOMAINS="*.cdn.example.com,localhost" +``` + +### Pattern Matching + +**Wildcard Patterns** (`*`): +```toml +"*.cdn.example.com" +``` +Matches: +- ✅ `assets.cdn.example.com` +- ✅ `images.cdn.example.com` +- ✅ `cdn.example.com` (base domain) +- ❌ `cdn.example.com.evil.com` (different domain) + +**Exact Patterns** (no `*`): +```toml +"api.example.com" +``` +Matches: +- ✅ `api.example.com` +- ❌ `www.api.example.com` +- ❌ `api.example.com.evil.com` + +### Use Cases + +**Trusted Partners**: +```toml +exclude_domains = ["*.approved-cdn.com"] +``` + +**First-Party Resources**: +```toml +exclude_domains = ["assets.publisher.com", "static.publisher.com"] +``` + +**Development**: +```toml +exclude_domains = ["localhost", "127.0.0.1", "*.local"] +``` + +**Performance** (already first-party): +```toml +exclude_domains = ["*.publisher.com"] # Skip unnecessary proxying +``` + +See [Creative Processing](/guide/creative-processing#exclude-domains) for details. + +## Integration Configurations + +Settings for built-in integrations (Prebid, Next.js, Permutive, Testlight). + +### Common Fields + +All integrations support: + +| Field | Type | Description | +|-------|------|-------------| +| `enabled` | Boolean | Enable/disable integration (default: false) | + +### Prebid Integration + +**Section**: `[integrations.prebid]` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `enabled` | Boolean | `false` | Enable Prebid integration | +| `server_url` | String | Required | Prebid Server endpoint URL | +| `timeout_ms` | Integer | `1000` | Request timeout in milliseconds | +| `bidders` | Array[String] | `[]` | List of enabled bidders | +| `auto_configure` | Boolean | `false` | Auto-inject Prebid.js shim | +| `debug` | Boolean | `false` | Enable debug logging | +| `script_handler` | String | Optional | Custom script endpoint path | + +**Example**: +```toml +[integrations.prebid] +enabled = true +server_url = "https://prebid-server.example/openrtb2/auction" +timeout_ms = 1200 +bidders = ["kargo", "rubicon", "appnexus", "openx"] +auto_configure = true +debug = false +``` + +**Environment Override**: +```bash +TRUSTED_SERVER__INTEGRATIONS__PREBID__ENABLED=true +TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL=https://prebid.example/auction +TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1200 +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS=kargo,rubicon,appnexus +TRUSTED_SERVER__INTEGRATIONS__PREBID__AUTO_CONFIGURE=true +TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG=false +``` + +### Next.js Integration + +**Section**: `[integrations.nextjs]` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `enabled` | Boolean | `false` | Enable Next.js integration | +| `rewrite_attributes` | Array[String] | `["href","link","url"]` | Attributes to rewrite | + +**Example**: +```toml +[integrations.nextjs] +enabled = true +rewrite_attributes = ["href", "link", "url", "src"] +``` + +**Environment Override**: +```bash +TRUSTED_SERVER__INTEGRATIONS__NEXTJS__ENABLED=true +TRUSTED_SERVER__INTEGRATIONS__NEXTJS__REWRITE_ATTRIBUTES=href,link,url,src +``` + +### Permutive Integration + +**Section**: `[integrations.permutive]` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `enabled` | Boolean | `false` | Enable Permutive integration | +| `organization_id` | String | Required | Permutive organization ID | +| `workspace_id` | String | Required | Permutive workspace ID | +| `project_id` | String | Required | Permutive project ID | +| `api_endpoint` | String | `https://api.permutive.com` | Permutive API URL | +| `secure_signals_endpoint` | String | `https://secure-signals.permutive.app` | Secure signals URL | +| `cache_ttl_seconds` | Integer | `3600` | Cache TTL in seconds | +| `rewrite_sdk` | Boolean | `true` | Rewrite Permutive SDK references | + +**Example**: +```toml +[integrations.permutive] +enabled = true +organization_id = "org-12345" +workspace_id = "ws-67890" +project_id = "proj-abcde" +api_endpoint = "https://api.permutive.com" +secure_signals_endpoint = "https://secure-signals.permutive.app" +cache_ttl_seconds = 7200 +rewrite_sdk = true +``` + +### Testlight Integration + +**Section**: `[integrations.testlight]` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `enabled` | Boolean | `false` | Enable Testlight integration | +| `endpoint` | String | Required | Testlight auction endpoint | +| `timeout_ms` | Integer | `1000` | Request timeout in milliseconds | +| `rewrite_scripts` | Boolean | `true` | Rewrite Testlight script references | + +**Example**: +```toml +[integrations.testlight] +enabled = true +endpoint = "https://testlight.example/openrtb2/auction" +timeout_ms = 1500 +rewrite_scripts = true +``` + +## Validation + +### Automatic Validation + +Configuration is validated at startup: + +**Publisher Validation**: +- All fields non-empty +- `origin_url` is valid URL + +**Synthetic Validation**: +- `secret_key` ≥ 1 character +- `secret_key` ≠ `"secret-key"` +- `template` non-empty + +**Handler Validation**: +- `path` is valid regex +- `username` non-empty +- `password` non-empty + +**Integration Validation**: +- Each integration implements `Validate` trait +- Custom rules per integration + +### Validation Errors + +**Startup Failure** if: +- Required fields missing +- Invalid data types +- Regex compilation fails +- Secret key is default value +- Integration config fails validation + +**Error Format**: +``` +Configuration error: Integration 'prebid' configuration failed validation: +server_url: must not be empty +``` + +## Best Practices + +### Configuration Management + +**Development**: +```toml +# trusted-server.dev.toml +[publisher] +domain = "localhost" +origin_url = "http://localhost:3000" +proxy_secret = "dev-secret" +``` + +**Staging**: +```bash +# .env.staging +TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://staging.publisher.com +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=$(cat /run/secrets/proxy_secret_staging) +``` + +**Production**: +```bash +# All secrets from environment +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=$(cat /run/secrets/proxy_secret) +TRUSTED_SERVER__SYNTHETIC__SECRET_KEY=$(cat /run/secrets/synthetic_secret) +TRUSTED_SERVER__HANDLERS__0__PASSWORD=$(cat /run/secrets/admin_password) +``` + +### Secret Management + +**Do**: +✅ Use environment variables for secrets +✅ Rotate secrets periodically +✅ Generate cryptographically random values +✅ Store in secure secret management (Fastly Secret Store, Vault) +✅ Use different secrets per environment + +**Don't**: +❌ Commit secrets to version control +❌ Use default/placeholder values +❌ Share secrets across environments +❌ Log secret values +❌ Expose in error messages + +### File Organization + +**Recommended Structure**: +``` +trusted-server.toml # Base config +trusted-server.dev.toml # Development overrides +.env.development # Dev environment vars +.env.staging # Staging environment vars +.env.production # Production environment vars (not in git) +.env.example # Example template (in git) +``` + +**.gitignore**: +``` +.env.production +.env.staging +.env.local +*.secret +``` + +## Troubleshooting + +### Common Issues + +**"Failed to build configuration"**: +- Check TOML syntax (trailing commas, quotes) +- Verify all required fields present +- Check environment variable format + +**"Secret key is not valid"**: +- Cannot use `"secret-key"` (placeholder) +- Must be non-empty +- Change to secure random value + +**"Invalid regex"**: +- Handler `path` must be valid regex +- Test pattern: `echo "^/admin" | grep -E "^/admin"` +- Escape special characters: `\.`, `\$`, etc. + +**"Integration configuration could not be parsed"**: +- Check JSON syntax in env vars +- Verify indexed arrays (0, 1, 2...) +- Check field names match exactly + +**Environment Variables Not Applied**: +- Verify prefix: `TRUSTED_SERVER__` +- Check separator: `__` (double underscore) +- Confirm variable is exported: `echo $VARIABLE_NAME` +- Try explicit string: `VARIABLE='value'` not `VARIABLE=value` + +### Debug Configuration + +**Print Loaded Config** (test only): +```rust +use trusted_server_common::settings::Settings; + +let settings = Settings::new()?; +println!("{:#?}", settings); +``` + +**Check Environment**: +```bash +# List all TRUSTED_SERVER variables +env | grep TRUSTED_SERVER +``` + +**Validate TOML**: +```bash +# Use any TOML validator +cat trusted-server.toml | npx toml-cli validate +``` + +## Next Steps + +- Learn about [First-Party Proxy](/guide/first-party-proxy) for URL proxying +- Set up [Request Signing](/guide/request-signing) for secure API calls +- Configure [Creative Processing](/guide/creative-processing) rewrites +- Explore [Integration Guide](/guide/integration-guide) for custom integrations diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md new file mode 100644 index 0000000..962e440 --- /dev/null +++ b/docs/guide/configuration.md @@ -0,0 +1,505 @@ +# Configuration + +Learn how to configure Trusted Server for your deployment. + +## Overview + +Trusted Server uses a flexible configuration system based on: + +1. **TOML Files** - `trusted-server.toml` for base configuration +2. **Environment Variables** - Runtime overrides with `TRUSTED_SERVER__` prefix +3. **Fastly Stores** - KV/Config/Secret stores for runtime data + +::: tip Complete Reference +See [Configuration Reference](/guide/configuration-reference) for detailed documentation of all configuration options. +::: + +## Quick Start + +### Basic Configuration + +Create `trusted-server.toml` in your project root: + +```toml +[publisher] +domain = "publisher.com" +cookie_domain = ".publisher.com" +origin_url = "https://origin.publisher.com" +proxy_secret = "your-secure-secret-here" + +[synthetic] +counter_store = "counter_store" +opid_store = "opid_store" +secret_key = "your-hmac-secret" +template = "{{ client_ip }}:{{ user_agent }}" +``` + +### Environment Overrides + +Override any setting with environment variables: + +```bash +# Publisher settings +export TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.com +export TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://origin.publisher.com + +# Synthetic ID settings +export TRUSTED_SERVER__SYNTHETIC__SECRET_KEY=your-secret +export TRUSTED_SERVER__SYNTHETIC__TEMPLATE="{{ client_ip }}:{{ user_agent }}" +``` + +## Configuration Files + +### `trusted-server.toml` + +Main application configuration file. + +**Location**: Project root directory + +**Format**: TOML (Tom's Obvious, Minimal Language) + +**Sections**: +- `[publisher]` - Publisher domain and origin settings +- `[synthetic]` - Synthetic ID generation +- `[request_signing]` - Request signing and JWKS +- `[response_headers]` - Custom response headers +- `[rewrite]` - URL rewriting rules +- `[[handlers]]` - Basic auth handlers +- `[integrations.*]` - Integration configs (Prebid, Next.js, etc.) + +**Example**: +```toml +[publisher] +domain = "publisher.com" +cookie_domain = ".publisher.com" +origin_url = "https://origin.publisher.com" +proxy_secret = "change-me-to-secure-value" + +[synthetic] +counter_store = "counter_store" +opid_store = "opid_store" +secret_key = "your-hmac-secret-key" +template = "{{ client_ip }}:{{ user_agent }}:{{ first_party_id }}" + +[response_headers] +X-Publisher-ID = "pub-12345" +X-Environment = "production" + +[request_signing] +enabled = false +config_store_id = "" +secret_store_id = "" + +[integrations.prebid] +enabled = true +server_url = "https://prebid-server.com/openrtb2/auction" +timeout_ms = 1200 +bidders = ["kargo", "rubicon", "appnexus"] +auto_configure = false +``` + +### `fastly.toml` + +Fastly Compute service configuration. + +**Purpose**: Build settings, local development, store links + +**Example**: +```toml +manifest_version = 2 +name = "trusted-server" +description = "Privacy-preserving ad serving" +authors = ["Your Team"] +language = "rust" + +[local_server] + [local_server.kv_stores.counter_store] + file = "test-data/counter_store.json" + + [local_server.kv_stores.opid_store] + file = "test-data/opid_store.json" + +[setup] + [setup.config_stores.jwks_store] + [setup.secret_stores.signing_keys] +``` + +### `.env.*` Files + +Environment-specific variable files. + +**`.env.dev`** - Local development: +```bash +TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=http://localhost:3000 +TRUSTED_SERVER__SYNTHETIC__SECRET_KEY=dev-secret +LOG_LEVEL=debug +``` + +**`.env.staging`** - Staging environment: +```bash +TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://staging.publisher.com +TRUSTED_SERVER__SYNTHETIC__SECRET_KEY=$(cat /run/secrets/synthetic_key_staging) +``` + +**`.env.production`** - Production (secrets from secure store): +```bash +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=$(cat /run/secrets/proxy_secret) +TRUSTED_SERVER__SYNTHETIC__SECRET_KEY=$(cat /run/secrets/synthetic_secret) +TRUSTED_SERVER__REQUEST_SIGNING__ENABLED=true +``` + +## Environment Variables + +### Format + +``` +TRUSTED_SERVER__SECTION__FIELD=value +``` + +**Rules**: +- Prefix: `TRUSTED_SERVER` +- Separator: `__` (double underscore) +- Case: UPPERCASE +- Nested: Use additional `__` for each level + +### Examples + +**Simple Field**: +```bash +TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.com +``` + +**Array (JSON)**: +```bash +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS='["kargo","rubicon"]' +``` + +**Array (Indexed)**: +```bash +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS__0=kargo +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS__1=rubicon +``` + +**Array (Comma-Separated)**: +```bash +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS=kargo,rubicon,appnexus +``` + +## Key Configuration Sections + +### Publisher Settings + +Core settings for your publisher domain and origin. + +```toml +[publisher] +domain = "publisher.com" +cookie_domain = ".publisher.com" +origin_url = "https://origin.publisher.com" +proxy_secret = "secure-random-secret" +``` + +**Key Fields**: +- `domain` - Your publisher domain +- `cookie_domain` - Domain for synthetic ID cookies (use `.domain.com` for subdomains) +- `origin_url` - Backend origin server URL +- `proxy_secret` - Secret for signing proxy URLs (HMAC-SHA256) + +::: warning Security +Generate `proxy_secret` with cryptographically random values: +```bash +openssl rand -base64 32 +``` +::: + +### Synthetic IDs + +Configure privacy-preserving ID generation. + +```toml +[synthetic] +counter_store = "counter_store" +opid_store = "opid_store" +secret_key = "your-hmac-secret" +template = "{{ client_ip }}:{{ user_agent }}:{{ first_party_id }}" +``` + +**Template Variables**: +- `client_ip` - Client IP address +- `user_agent` - User-Agent header +- `first_party_id` - Publisher-provided ID +- `auth_user_id` - Authenticated user ID +- `publisher_domain` - Publisher domain +- `accept_language` - Accept-Language header + +See [Synthetic IDs](/guide/synthetic-ids) for details. + +### Request Signing + +Enable Ed25519 request signing and JWKS management. + +```toml +[request_signing] +enabled = true +config_store_id = "01GXXX" # From Fastly dashboard +secret_store_id = "01GYYY" # From Fastly dashboard +``` + +**Setup**: +1. Create Fastly Config Store for JWKS +2. Create Fastly Secret Store for private keys +3. Copy store IDs to configuration +4. Enable request signing + +See [Request Signing](/guide/request-signing) and [Key Rotation](/guide/key-rotation) for setup. + +### Integrations + +Configure built-in integrations. + +**Prebid**: +```toml +[integrations.prebid] +enabled = true +server_url = "https://prebid-server.com/openrtb2/auction" +timeout_ms = 1200 +bidders = ["kargo", "rubicon", "appnexus"] +auto_configure = false +``` + +**Next.js**: +```toml +[integrations.nextjs] +enabled = true +rewrite_attributes = ["href", "link", "url"] +``` + +**Permutive**: +```toml +[integrations.permutive] +enabled = true +organization_id = "org-12345" +workspace_id = "ws-67890" +project_id = "proj-abcde" +``` + +See [Integration Guide](/guide/integration-guide) for custom integrations. + +## Fastly Store Setup + +### KV Stores + +Create stores for synthetic ID data: + +```bash +# Create counter store +fastly kv-store create --name=counter_store + +# Create publisher ID mapping store +fastly kv-store create --name=opid_store +``` + +**Link to Service** (`fastly.toml`): +```toml +[local_server.kv_stores.counter_store] + file = "test-data/counter_store.json" + +[local_server.kv_stores.opid_store] + file = "test-data/opid_store.json" +``` + +### Config Stores + +For JWKS public keys: + +```bash +# Create config store +fastly config-store create --name=jwks_store + +# Get store ID +fastly config-store list +``` + +### Secret Stores + +For private signing keys: + +```bash +# Create secret store +fastly secret-store create --name=signing_keys + +# Get store ID +fastly secret-store list +``` + +## Validation + +### Automatic Validation + +Configuration is validated at application startup: + +**Checks**: +- Required fields present +- Data types correct +- Regex patterns valid +- Secret keys not default values +- Integration configs valid + +**Failure Behavior**: Application exits with error message. + +### Manual Validation + +Validate before deployment: + +```bash +# Test TOML syntax +fastly compute validate + +# Test with local server +fastly compute serve +``` + +## Secrets Management + +### Best Practices + +**Development**: +```bash +# Use simple secrets for local dev +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=dev-secret +``` + +**Staging/Production**: +```bash +# Load from secure sources +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=$(cat /run/secrets/proxy_secret) +TRUSTED_SERVER__SYNTHETIC__SECRET_KEY=$(vault kv get -field=value secret/synthetic_key) +``` + +**Do**: +✅ Use environment variables for secrets +✅ Generate cryptographically random values +✅ Rotate secrets periodically +✅ Store in Fastly Secret Store or Vault +✅ Use different secrets per environment + +**Don't**: +❌ Commit secrets to version control +❌ Use default placeholder values +❌ Share secrets across environments +❌ Log secret values + +### `.gitignore` + +Protect secret files: + +``` +.env.production +.env.staging +.env.local +*.secret +trusted-server.production.toml +``` + +## Configuration Patterns + +### Multi-Environment Setup + +**Directory Structure**: +``` +project/ +├── trusted-server.toml # Base config +├── trusted-server.dev.toml # Development overrides +├── .env.development # Dev environment vars +├── .env.staging # Staging environment vars +├── .env.production # Production (not in git) +├── .env.example # Template (in git) +└── .gitignore +``` + +**Base Config** (`trusted-server.toml`): +```toml +# Shared across all environments +[synthetic] +template = "{{ client_ip }}:{{ user_agent }}" + +[integrations.prebid] +timeout_ms = 1200 +bidders = ["kargo", "rubicon"] +``` + +**Environment Overrides**: +```bash +# Development +export TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=http://localhost:3000 + +# Staging +export TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://staging.publisher.com + +# Production +export TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://origin.publisher.com +``` + +### Dynamic Configuration + +Use environment variables for runtime changes: + +```bash +# Enable/disable features +TRUSTED_SERVER__REQUEST_SIGNING__ENABLED=true + +# Adjust timeouts +TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1500 + +# Update endpoints +TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL=https://new-prebid.com/auction +``` + +## Troubleshooting + +### Common Issues + +**"Failed to build configuration"**: +- Check TOML syntax (commas, quotes, brackets) +- Verify all required fields present +- Check environment variable format + +**"Secret key is not valid"**: +- Cannot use `"secret-key"` placeholder +- Must be non-empty +- Change to secure random value + +**"Invalid regex"**: +- Handler `path` must be valid regex +- Escape special characters: `\.`, `\$` +- Test with: `echo "pattern" | grep -E "pattern"` + +**Environment variables not applied**: +- Verify prefix: `TRUSTED_SERVER__` +- Check separator: `__` (double underscore) +- Confirm exported: `echo $VAR_NAME` + +### Debug Commands + +**Check environment**: +```bash +env | grep TRUSTED_SERVER +``` + +**Validate TOML**: +```bash +cat trusted-server.toml | npx toml-cli validate +``` + +**Test local server**: +```bash +fastly compute serve --verbose +``` + +## Next Steps + +- See [Configuration Reference](/guide/configuration-reference) for complete options +- Set up [Request Signing](/guide/request-signing) for secure API calls +- Configure [First-Party Proxy](/guide/first-party-proxy) for URL proxying +- Learn about [Integration Guide](/guide/integration-guide) for custom integrations +- Review [Testing](/guide/testing) for validation strategies diff --git a/docs/guide/creative-processing.md b/docs/guide/creative-processing.md new file mode 100644 index 0000000..f7bca18 --- /dev/null +++ b/docs/guide/creative-processing.md @@ -0,0 +1,817 @@ +# Creative Processing + +Learn how Trusted Server automatically rewrites ad creative HTML and CSS to route all resources through first-party domains. + +## Overview + +Creative processing transforms third-party ad creatives by rewriting URLs to go through your first-party domain. This provides: + +- **Privacy Control** - All resources load through your domain +- **First-Party Context** - Cookies and storage use your domain +- **Synthetic ID Integration** - Automatic ID forwarding to trackers +- **Security** - Validated, signed URLs prevent tampering +- **GDPR Compliance** - Controlled data sharing + +## How It Works + +``` +┌──────────────────────────────────────────────────────┐ +│ Original Creative HTML │ +│ │ +│ + + + +``` + +::: warning Nested Iframes +If the iframe content itself contains HTML, it will be processed recursively. Each level of nesting gets its own URL rewriting pass. +::: + +### Links + +**Elements**: `` + +**Attributes**: +- `href` - Link target (stylesheets, preload, prefetch) +- `imagesrcset` - Responsive images in link preload + +**Conditions**: Only rewritten when `rel` attribute matches: +- `stylesheet` +- `preload` +- `prefetch` + +**Example**: +```html + + + + + + + +``` + +### Anchors (Click Tracking) + +**Elements**: ``, `` + +**Attributes**: +- `href` - Link destination + +**Rewrite Mode**: Uses `/first-party/click` for direct redirects + +**Example**: +```html + +Buy Now + + +Buy Now +``` + +::: tip Click vs Proxy +Anchors (``) use `/first-party/click` for 302 redirects, avoiding content downloads. Other elements use `/first-party/proxy` to fetch and potentially rewrite content. +::: + +### SVG Elements + +**Elements**: SVG ``, `` + +**Attributes**: +- `href` - SVG 2.0 syntax +- `xlink:href` - SVG 1.1 legacy syntax + +**Example**: +```html + + + + + + + + + + + +``` + +### Inline Styles + +**Attributes**: `style` attribute on any element + +**Patterns**: Rewrites `url(...)` values in CSS + +**Example**: +```html + +
+ + +
+``` + +### Style Blocks + +**Elements**: ` + + + +``` + +## URL Detection Rules + +### Absolute URLs + +**Pattern**: Starts with `http://` or `https://` + +**Rewritten**: ✅ Yes + +**Examples**: +``` +✅ https://cdn.example.com/image.png +✅ http://tracker.example.com/pixel.gif +❌ /relative/path.jpg (relative) +❌ ../images/logo.png (relative) +``` + +### Protocol-Relative URLs + +**Pattern**: Starts with `//` + +**Rewritten**: ✅ Yes (normalized to `https://`) + +**Examples**: +``` +Original: //cdn.example.com/script.js +Normalized: https://cdn.example.com/script.js +Rewritten: /first-party/proxy?tsurl=https://cdn.example.com/script.js&tstoken=sig +``` + +### Relative URLs + +**Patterns**: +- Starts with `/` (absolute path) +- Starts with `./` or `../` (relative path) +- No scheme prefix (relative) + +**Rewritten**: ❌ No + +**Examples**: +``` +❌ /assets/image.png +❌ ./local.jpg +❌ ../parent/file.css +❌ image.png +``` + +::: info Why Skip Relative URLs? +Relative URLs already point to your domain (publisher origin). Rewriting them would create unnecessary proxy loops and break functionality. +::: + +### Non-Network Schemes + +**Skipped Schemes**: +- `data:` - Data URIs (inline content) +- `javascript:` - JavaScript execution +- `mailto:` - Email links +- `tel:` - Phone numbers +- `blob:` - Blob objects +- `about:` - Browser internal pages + +**Rewritten**: ❌ No + +**Examples**: +``` +❌ data:image/png;base64,iVBORw0KGgo... +❌ javascript:void(0) +❌ mailto:contact@example.com +❌ tel:+1234567890 +❌ blob:https://example.com/uuid +❌ about:blank +``` + +## Srcset Processing + +### Srcset Syntax + +Srcset attributes contain comma-separated candidates with optional descriptors: + +**Format**: `url descriptor, url descriptor, ...` + +**Descriptors**: +- `1x`, `2x`, `3x` - Pixel density +- `100w`, `200w` - Width in pixels + +### Parsing Rules + +**Robust Comma Handling**: +- Splits on commas with or without spaces +- Preserves `data:` URIs (doesn't split on internal commas) +- Handles irregular spacing + +**Example**: +```html + +srcset="url1.jpg 1x, url2.jpg 2x" +srcset="url1.jpg 1x,url2.jpg 2x" +srcset="url1.jpg 1x , url2.jpg 2x" +``` + +### Descriptor Preservation + +Descriptors are preserved exactly as written: + +```html + + + + + +``` + +### Mixed URL Types + +Srcset can mix absolute and relative URLs: + +```html + + + + + +``` + +## CSS URL Rewriting + +### url() Syntax Variations + +CSS `url()` values support multiple quote styles: + +```css +/* No quotes */ +url(https://example.com/image.png) + +/* Single quotes */ +url('https://example.com/image.png') + +/* Double quotes */ +url("https://example.com/image.png") + +/* With spaces */ +url( "https://example.com/image.png" ) +``` + +**All are rewritten correctly**, preserving the original quote style. + +### CSS Properties + +Common properties with `url()` values: + +```css +/* Background images */ +background: url(https://cdn.com/bg.jpg); +background-image: url(https://cdn.com/pattern.png); + +/* Borders */ +border-image: url(https://cdn.com/border.svg); + +/* List styles */ +list-style-image: url(https://cdn.com/bullet.png); + +/* Cursors */ +cursor: url(https://cdn.com/cursor.cur), pointer; + +/* Masks */ +mask-image: url(https://cdn.com/mask.svg); + +/* Filters */ +filter: url(https://cdn.com/filter.svg#blur); +``` + +**All `url()` occurrences are rewritten** regardless of property. + +### Multiple url() Values + +Properties can have multiple `url()` values: + +```css +/* Original */ +.element { + background: + url(https://cdn.com/top.png) top, + url(https://cdn.com/bottom.png) bottom; +} + +/* Rewritten */ +.element { + background: + url(/first-party/proxy?tsurl=https://cdn.com/top.png&tstoken=sig1) top, + url(/first-party/proxy?tsurl=https://cdn.com/bottom.png&tstoken=sig2) bottom; +} +``` + +### @import Rules + +CSS `@import` with URLs: + +```css +/* Original */ +@import url(https://fonts.googleapis.com/css?family=Roboto); + +/* Rewritten */ +@import url(/first-party/proxy?tsurl=https://fonts.googleapis.com/css?family=Roboto&tstoken=sig); +``` + +## Exclude Domains + +### Configuration + +Prevent specific domains from being rewritten: + +```toml +[rewrite] +exclude_domains = [ + "*.cdn.trusted-partner.com", # Wildcard pattern + "first-party.example.com", # Exact match + "localhost", # Development +] +``` + +### Pattern Matching + +**Wildcard Patterns**: `*` matches any subdomain + +``` +Pattern: *.cdn.example.com +Matches: + ✅ assets.cdn.example.com + ✅ images.cdn.example.com + ❌ cdn.example.com (no subdomain) + ❌ cdn.example.com.evil.com (different domain) +``` + +**Exact Patterns**: No `*` requires exact host match + +``` +Pattern: api.example.com +Matches: + ✅ api.example.com + ❌ www.api.example.com + ❌ api.example.com.evil.com +``` + +### Use Cases + +**Trusted Partners**: +```toml +exclude_domains = ["*.trusted-cdn.com"] +``` +Skip rewriting for partners already providing first-party scripts. + +**Development**: +```toml +exclude_domains = ["localhost", "127.0.0.1"] +``` +Avoid proxying local development servers. + +**Same-Origin Resources**: +```toml +exclude_domains = ["assets.publisher.com"] +``` +Skip resources already on your domain. + +## Integration Hooks + +### Attribute Rewriters + +Integrations can override attribute rewriting: + +**Example**: Next.js integration rewrites origin URLs + +```rust +impl IntegrationAttributeRewriter for NextJsIntegration { + fn rewrite(&self, attr_name: &str, attr_value: &str, ctx: &Context) + -> AttributeRewriteAction + { + if attr_name == "href" && attr_value.contains(&ctx.origin_host) { + let rewritten = attr_value.replace(&ctx.origin_host, &ctx.request_host); + return AttributeRewriteAction::replace(rewritten); + } + AttributeRewriteAction::keep() + } +} +``` + +**Actions**: +- `keep()` - Leave attribute unchanged +- `replace(value)` - Change attribute value +- `remove_element()` - Delete entire element + +### Script Rewriters + +Integrations can modify ` +``` + +**Timing**: Injected **once per HTML response** before any other scripts. + +### Integration Bundles + +Integrations can request additional bundles: + +```rust +IntegrationRegistration::builder("my_integration") + .with_asset("my_integration") // Requests tsjs-my_integration.min.js + .build() +``` + +**Result**: +```html + + + + + +``` + +### Bundle Types + +Available bundles (from `crates/js/lib/src/integrations/`): + +- `tsjs-core.min.js` - Core API (always included) +- `tsjs-ext.min.js` - Extensions (Prebid integration) +- `tsjs-creative.min.js` - Creative tracking utilities +- `tsjs-permutive.min.js` - Permutive integration +- `tsjs-testlight.min.js` - Testlight integration + +## Performance Optimization + +### Streaming Processing + +HTML is processed in **chunks** (default 8192 bytes): + +**Benefits**: +- Low memory footprint +- Handles large creatives +- Incremental output +- Fast first byte + +**Trade-offs**: +- Cannot access full DOM +- Element-by-element processing +- No look-ahead + +### Compression + +**Buffered Mode** (with rewriting): +``` +Origin Response (gzipped) + ↓ Decompress +Processing + ↓ Rewrite URLs +Response (uncompressed) + ↓ Fastly edge can re-compress +Client +``` + +**Streaming Mode** (no rewriting): +``` +Origin Response (gzipped) + ↓ Passthrough +Client (stays gzipped) +``` + +Use streaming for binary/large files to preserve compression. + +### Caching + +Rewritten creatives can be cached: + +**Cache Key Components**: +- Original creative URL +- Publisher domain +- Integration configuration + +**Headers to Set**: +```http +Cache-Control: public, max-age=3600 +Vary: Accept-Encoding +``` + +## Debugging + +### Logging + +Enable debug logging for rewrite operations: + +```rust +log::debug!("creative: rewriting {} -> {}", original_url, proxy_url); +log::debug!("creative: excluded domain {}", url); +log::debug!("creative: skipped non-network scheme {}", url); +``` + +### Testing Rewrites + +**Manual Testing**: +1. Save original creative HTML to file +2. Pass through rewrite function +3. Compare output + +```rust +let original = ""; +let rewritten = rewrite_creative_html(original, &settings); +assert!(rewritten.contains("/first-party/proxy")); +``` + +**Integration Tests**: +```rust +#[test] +fn test_image_src_rewrite() { + let html = r#""#; + let result = rewrite_creative_html(html, &test_settings()); + assert!(result.contains("/first-party/proxy?tsurl=")); + assert!(result.contains("&tstoken=")); +} +``` + +### Common Issues + +**Relative URLs Not Working**: +- Ensure origin response includes proper `` tag +- Or convert to absolute URLs before rewriting + +**Data URIs Being Rewritten**: +- Should be automatically skipped +- Check for malformed `data:` scheme + +**Srcset Parsing Errors**: +- Verify comma-separated format +- Check for unclosed quotes + +## Security Considerations + +### URL Validation + +All rewritten URLs are validated: + +1. **Scheme Check**: Only `http://` and `https://` +2. **Signature**: HMAC-SHA256 token required +3. **Expiration**: Optional `tsexp` timestamp +4. **Exclusion List**: Configurable domain blacklist + +### Content Security Policy + +Recommended CSP headers for rewritten creatives: + +```http +Content-Security-Policy: + default-src 'self'; + img-src 'self' /first-party/proxy; + script-src 'self' /static/tsjs; + style-src 'self' 'unsafe-inline'; + frame-src 'self' /first-party/proxy; +``` + +### Injection Prevention + +**Automatic Protection**: +- URLs are properly encoded +- No raw user input in rewrites +- Signature prevents tampering + +**Manual Checks**: +- Validate origin creative sources +- Sanitize user-generated content +- Monitor for suspicious patterns + +## Best Practices + +### Configuration + +✅ **Do**: +- Use strong `proxy_secret` (32+ bytes random) +- Exclude trusted first-party domains +- Set appropriate cache headers +- Test rewrites before production + +❌ **Don't**: +- Hardcode secrets in source +- Rewrite same-origin URLs unnecessarily +- Skip signature validation +- Disable TSJS injection without reason + +### Performance + +✅ **Do**: +- Use streaming for large/binary responses +- Enable compression at edge +- Cache rewritten creatives +- Monitor rewrite latency + +❌ **Don't**: +- Buffer entire response unnecessarily +- Rewrite on every request (cache!) +- Process non-HTML/CSS with rewriter +- Chain multiple rewrites + +### Monitoring + +Track these metrics: + +- **Rewrite operations** - Count of rewrites per request +- **Excluded domains** - Frequency of exclusions +- **Processing time** - Latency added by rewriting +- **Cache hit rate** - Effectiveness of caching +- **TSJS injection** - Verify library loads + +## Next Steps + +- Learn about [First-Party Proxy](/guide/first-party-proxy) for URL handling +- Review [Integration Guide](/guide/integration-guide) for custom rewriters +- Set up [Configuration](/guide/configuration) for your creatives +- Explore [Synthetic IDs](/guide/synthetic-ids) for identity management diff --git a/docs/guide/environment-variables.md b/docs/guide/environment-variables.md new file mode 100644 index 0000000..82a11f9 --- /dev/null +++ b/docs/guide/environment-variables.md @@ -0,0 +1,664 @@ +# Environment Variables Reference + +Complete guide to configuring Trusted Server using environment variables. + +## Overview + +Trusted Server can be configured using: +1. **TOML file** (`trusted-server.toml`) - Default configuration +2. **Environment variables** - Override TOML settings at runtime + +Environment variables are useful for: +- Secrets management (never commit passwords/keys to git) +- Environment-specific settings (dev/staging/prod) +- CI/CD pipelines +- Container deployments +- Testing different configurations + +--- + +## Variable Naming Pattern + +All environment variables follow this pattern: + +``` +TRUSTED_SERVER__{SECTION}__{SUBSECTION}__{FIELD} +``` + +**Rules:** +- Double underscores (`__`) separate levels +- All uppercase +- Hyphens in TOML keys become underscores +- Array indices use double underscores with numbers + +**Example:** +```toml +# TOML: trusted-server.toml +[publisher] +domain = "example.com" + +# Environment variable: +TRUSTED_SERVER__PUBLISHER__DOMAIN="example.com" +``` + +--- + +## Publisher Configuration + +### Required Settings + +```bash +# Publisher domain (required) +TRUSTED_SERVER__PUBLISHER__DOMAIN="publisher.com" + +# Origin server URL (required) +TRUSTED_SERVER__PUBLISHER__ORIGIN_URL="https://origin.publisher.com" + +# Cookie domain (required) +TRUSTED_SERVER__PUBLISHER__COOKIE_DOMAIN=".publisher.com" + +# Proxy secret for URL signing (required) +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET="change-me-to-random-string-min-32-chars" +``` + +**TOML Equivalent:** +```toml +[publisher] +domain = "publisher.com" +origin_url = "https://origin.publisher.com" +cookie_domain = ".publisher.com" +proxy_secret = "change-me-to-random-string-min-32-chars" +``` + +--- + +## Synthetic ID Configuration + +```bash +# KV store names +TRUSTED_SERVER__SYNTHETIC__COUNTER_STORE="counter_store" +TRUSTED_SERVER__SYNTHETIC__OPID_STORE="opid_store" + +# Secret key for HMAC (required, min 16 chars) +TRUSTED_SERVER__SYNTHETIC__SECRET_KEY="your-secret-key-min-16-chars" + +# Template (comma-separated for array) +TRUSTED_SERVER__SYNTHETIC__TEMPLATE="{{ client_ip }}:{{ user_agent }}:{{ first_party_id }}" +``` + +**Template Variables:** +- `client_ip` - Client IP address +- `user_agent` - User agent string +- `first_party_id` - Publisher's first-party cookie +- `auth_user_id` - Authenticated user ID +- `publisher_domain` - Publisher domain +- `accept_language` - Accept-Language header + +**TOML Equivalent:** +```toml +[synthetic] +counter_store = "counter_store" +opid_store = "opid_store" +secret_key = "your-secret-key-min-16-chars" +template = "{{ client_ip }}:{{ user_agent }}:{{ first_party_id }}" +``` + +--- + +## Request Signing Configuration + +```bash +# Enable request signing +TRUSTED_SERVER__REQUEST_SIGNING__ENABLED=true + +# Fastly Config Store ID +TRUSTED_SERVER__REQUEST_SIGNING__CONFIG_STORE_ID="your-config-store-id" + +# Fastly Secret Store ID +TRUSTED_SERVER__REQUEST_SIGNING__SECRET_STORE_ID="your-secret-store-id" +``` + +**TOML Equivalent:** +```toml +[request_signing] +enabled = true +config_store_id = "your-config-store-id" +secret_store_id = "your-secret-store-id" +``` + +--- + +## Response Headers + +Custom headers to include in all responses: + +```bash +# Single header +TRUSTED_SERVER__RESPONSE_HEADERS__X_CUSTOM_HEADER="custom value" + +# Multiple headers (use separate variables) +TRUSTED_SERVER__RESPONSE_HEADERS__X_FRAME_OPTIONS="SAMEORIGIN" +TRUSTED_SERVER__RESPONSE_HEADERS__X_CONTENT_TYPE_OPTIONS="nosniff" +TRUSTED_SERVER__RESPONSE_HEADERS__STRICT_TRANSPORT_SECURITY="max-age=31536000" +``` + +**TOML Equivalent:** +```toml +[response_headers] +X-Custom-Header = "custom value" +X-Frame-Options = "SAMEORIGIN" +X-Content-Type-Options = "nosniff" +Strict-Transport-Security = "max-age=31536000" +``` + +--- + +## Handler Authentication + +Basic auth for specific paths: + +```bash +# Handler #1 +TRUSTED_SERVER__HANDLERS__0__PATH="^/admin" +TRUSTED_SERVER__HANDLERS__0__USERNAME="admin" +TRUSTED_SERVER__HANDLERS__0__PASSWORD="secure-password" + +# Handler #2 +TRUSTED_SERVER__HANDLERS__1__PATH="^/secure" +TRUSTED_SERVER__HANDLERS__1__USERNAME="user" +TRUSTED_SERVER__HANDLERS__1__PASSWORD="another-password" +``` + +**Pattern:** Use `__0__`, `__1__`, etc. for array indices. + +**TOML Equivalent:** +```toml +[[handlers]] +path = "^/admin" +username = "admin" +password = "secure-password" + +[[handlers]] +path = "^/secure" +username = "user" +password = "another-password" +``` + +--- + +## Prebid Integration + +```bash +# Enable integration +TRUSTED_SERVER__INTEGRATIONS__PREBID__ENABLED=true + +# Prebid Server URL (required) +TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL="https://prebid-server.example.com" + +# Request timeout in milliseconds +TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1000 + +# Bidders (comma-separated) +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS="appnexus,rubicon,openx" + +# Auto-remove Prebid.js scripts +TRUSTED_SERVER__INTEGRATIONS__PREBID__AUTO_CONFIGURE=true + +# Enable debug logging +TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG=false + +# Optional: Script handler path +TRUSTED_SERVER__INTEGRATIONS__PREBID__SCRIPT_HANDLER="/prebid.js" +``` + +**TOML Equivalent:** +```toml +[integrations.prebid] +enabled = true +server_url = "https://prebid-server.example.com" +timeout_ms = 1000 +bidders = ["appnexus", "rubicon", "openx"] +auto_configure = true +debug = false +script_handler = "/prebid.js" +``` + +--- + +## Next.js Integration + +```bash +# Enable integration +TRUSTED_SERVER__INTEGRATIONS__NEXTJS__ENABLED=true + +# Attributes to rewrite (comma-separated) +TRUSTED_SERVER__INTEGRATIONS__NEXTJS__REWRITE_ATTRIBUTES="href,link,url,src" +``` + +**TOML Equivalent:** +```toml +[integrations.nextjs] +enabled = true +rewrite_attributes = ["href", "link", "url", "src"] +``` + +--- + +## Permutive Integration + +```bash +# Enable integration +TRUSTED_SERVER__INTEGRATIONS__PERMUTIVE__ENABLED=true + +# Permutive organization ID (required) +TRUSTED_SERVER__INTEGRATIONS__PERMUTIVE__ORGANIZATION_ID="myorg" + +# Permutive workspace ID (required) +TRUSTED_SERVER__INTEGRATIONS__PERMUTIVE__WORKSPACE_ID="workspace-12345" + +# Optional project ID +TRUSTED_SERVER__INTEGRATIONS__PERMUTIVE__PROJECT_ID="project-789" + +# API endpoints +TRUSTED_SERVER__INTEGRATIONS__PERMUTIVE__API_ENDPOINT="https://api.permutive.com" +TRUSTED_SERVER__INTEGRATIONS__PERMUTIVE__SECURE_SIGNALS_ENDPOINT="https://secure-signals.permutive.app" + +# SDK cache TTL in seconds +TRUSTED_SERVER__INTEGRATIONS__PERMUTIVE__CACHE_TTL_SECONDS=3600 + +# Auto-rewrite SDK URLs in HTML +TRUSTED_SERVER__INTEGRATIONS__PERMUTIVE__REWRITE_SDK=true +``` + +**TOML Equivalent:** +```toml +[integrations.permutive] +enabled = true +organization_id = "myorg" +workspace_id = "workspace-12345" +project_id = "project-789" +api_endpoint = "https://api.permutive.com" +secure_signals_endpoint = "https://secure-signals.permutive.app" +cache_ttl_seconds = 3600 +rewrite_sdk = true +``` + +--- + +## Testlight Integration + +```bash +# Enable integration +TRUSTED_SERVER__INTEGRATIONS__TESTLIGHT__ENABLED=true + +# Upstream endpoint (required) +TRUSTED_SERVER__INTEGRATIONS__TESTLIGHT__ENDPOINT="https://testlight-server.example.com" + +# Request timeout in milliseconds +TRUSTED_SERVER__INTEGRATIONS__TESTLIGHT__TIMEOUT_MS=1000 + +# Script replacement URL +TRUSTED_SERVER__INTEGRATIONS__TESTLIGHT__SHIM_SRC="/static/tsjs-unified.js" + +# Auto-rewrite testlight.js scripts +TRUSTED_SERVER__INTEGRATIONS__TESTLIGHT__REWRITE_SCRIPTS=false +``` + +**TOML Equivalent:** +```toml +[integrations.testlight] +enabled = true +endpoint = "https://testlight-server.example.com" +timeout_ms = 1000 +shim_src = "/static/tsjs-unified.js" +rewrite_scripts = false +``` + +--- + +## Rewrite Configuration + +Exclude domains from first-party rewriting: + +```bash +# Comma-separated list of domains/patterns +TRUSTED_SERVER__REWRITE__EXCLUDE_DOMAINS="*.edgecompute.app,localhost:*,*.internal.com" +``` + +**TOML Equivalent:** +```toml +[rewrite] +exclude_domains = [ + "*.edgecompute.app", + "localhost:*", + "*.internal.com" +] +``` + +**Patterns:** +- Exact match: `example.com` +- Wildcard subdomain: `*.example.com` +- Wildcard port: `localhost:*` +- Full wildcard: `*` + +--- + +## Data Type Formats + +### Strings +```bash +# Simple string +TRUSTED_SERVER__PUBLISHER__DOMAIN="example.com" + +# String with spaces (quote in shell) +TRUSTED_SERVER__RESPONSE_HEADERS__X_CUSTOM="value with spaces" +``` + +### Booleans +```bash +# Accepted values: true, false (lowercase) +TRUSTED_SERVER__INTEGRATIONS__PREBID__ENABLED=true +TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG=false +``` + +### Numbers +```bash +# Integers (no quotes) +TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1000 +TRUSTED_SERVER__INTEGRATIONS__PERMUTIVE__CACHE_TTL_SECONDS=3600 +``` + +### Arrays (Comma-Separated) +```bash +# Comma-separated values (no spaces) +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS="appnexus,rubicon,openx" + +# With spaces requires quotes +TRUSTED_SERVER__INTEGRATIONS__NEXTJS__REWRITE_ATTRIBUTES="href,link,url" +``` + +### Arrays (Indexed) +```bash +# Use indices for complex arrays +TRUSTED_SERVER__HANDLERS__0__PATH="^/admin" +TRUSTED_SERVER__HANDLERS__0__USERNAME="admin" +TRUSTED_SERVER__HANDLERS__1__PATH="^/secure" +TRUSTED_SERVER__HANDLERS__1__USERNAME="user" +``` + +### Nested Objects +```bash +# Use double underscores for nesting +TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL="https://server.com" +# ^section ^subsection ^field +``` + +--- + +## Common Patterns + +### Development Environment +```bash +# .env.development +TRUSTED_SERVER__PUBLISHER__DOMAIN="localhost:7676" +TRUSTED_SERVER__PUBLISHER__ORIGIN_URL="http://localhost:8080" +TRUSTED_SERVER__PUBLISHER__COOKIE_DOMAIN="localhost" +TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG=true +TRUSTED_SERVER__REQUEST_SIGNING__ENABLED=false +``` + +### Production Environment +```bash +# .env.production +TRUSTED_SERVER__PUBLISHER__DOMAIN="publisher.com" +TRUSTED_SERVER__PUBLISHER__ORIGIN_URL="https://origin.publisher.com" +TRUSTED_SERVER__PUBLISHER__COOKIE_DOMAIN=".publisher.com" +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET="${PROXY_SECRET}" # From secrets manager +TRUSTED_SERVER__SYNTHETIC__SECRET_KEY="${SYNTHETIC_KEY}" +TRUSTED_SERVER__REQUEST_SIGNING__ENABLED=true +TRUSTED_SERVER__REQUEST_SIGNING__CONFIG_STORE_ID="${CONFIG_STORE_ID}" +TRUSTED_SERVER__REQUEST_SIGNING__SECRET_STORE_ID="${SECRET_STORE_ID}" +``` + +### CI/CD Pipeline +```bash +# GitHub Actions example +env: + TRUSTED_SERVER__PUBLISHER__DOMAIN: ${{ vars.PUBLISHER_DOMAIN }} + TRUSTED_SERVER__PUBLISHER__PROXY_SECRET: ${{ secrets.PROXY_SECRET }} + TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL: ${{ vars.PREBID_URL }} + TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS: "appnexus,rubicon" +``` + +--- + +## Secrets Management + +### Never Commit Secrets +```bash +# ❌ WRONG - Don't commit to git +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET="my-secret-key" + +# ✅ CORRECT - Use environment-specific injection +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET="${PROXY_SECRET}" +``` + +### Use Secrets Managers +```bash +# AWS Secrets Manager +export PROXY_SECRET=$(aws secretsmanager get-secret-value --secret-id trusted-server/proxy-secret --query SecretString --output text) +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET="${PROXY_SECRET}" + +# HashiCorp Vault +export PROXY_SECRET=$(vault kv get -field=value secret/trusted-server/proxy-secret) +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET="${PROXY_SECRET}" + +# Fastly Secret Store (for keys) +# Managed via Fastly dashboard - not environment variables +``` + +### Rotate Secrets Regularly +```bash +# Generate strong secrets +openssl rand -base64 32 # 32-byte random string + +# Update environment and redeploy +``` + +--- + +## Loading Environment Variables + +### Local Development + +**Using .env file:** +```bash +# Create .env file +cat > .env <&1 | grep -i "settings" + +# Or in Fastly logs +fastly log-tail | grep -i "settings" +``` + +### Verify Override + +```toml +# trusted-server.toml +[publisher] +domain = "default.com" +``` + +```bash +# Override with environment variable +TRUSTED_SERVER__PUBLISHER__DOMAIN="override.com" cargo build + +# Should log: "domain: override.com" +``` + +### Test Locally + +```bash +# Start with environment variable +TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG=true fastly compute serve + +# Verify in logs +# Should see debug output from Prebid integration +``` + +--- + +## Troubleshooting + +### Variable Not Taking Effect + +**Cause:** Environment variable set after build + +**Solution:** Set before building: +```bash +export TRUSTED_SERVER__PUBLISHER__DOMAIN="example.com" +cargo build +``` + +### Parse Error + +**Cause:** Wrong data type format + +```bash +# ❌ Wrong +TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS="1000" # String + +# ✅ Correct +TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1000 # Number +``` + +### Array Not Working + +**Cause:** Incorrect array format + +```bash +# ❌ Wrong +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS='["appnexus", "rubicon"]' + +# ✅ Correct +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS="appnexus,rubicon" +``` + +### Variable Name Typo + +**Cause:** Incorrect casing or separator + +```bash +# ❌ Wrong +TRUSTED_SERVER_PUBLISHER_DOMAIN="example.com" # Single underscore +trusted_server__publisher__domain="example.com" # Lowercase + +# ✅ Correct +TRUSTED_SERVER__PUBLISHER__DOMAIN="example.com" # Double underscore, uppercase +``` + +--- + +## Security Best Practices + +1. **Never commit secrets** to version control +2. **Use environment-specific files** (.env.development, .env.production) +3. **Restrict access** to production environment variables +4. **Rotate secrets** regularly (every 90 days minimum) +5. **Use strong secrets** (minimum 32 characters, random) +6. **Audit access** to secrets management systems +7. **Encrypt at rest** (use secrets managers with encryption) +8. **Log carefully** (don't log secret values) + +--- + +## Complete Example + +```bash +#!/bin/bash +# Environment configuration for production + +# Required: Publisher Settings +export TRUSTED_SERVER__PUBLISHER__DOMAIN="publisher.com" +export TRUSTED_SERVER__PUBLISHER__ORIGIN_URL="https://origin.publisher.com" +export TRUSTED_SERVER__PUBLISHER__COOKIE_DOMAIN=".publisher.com" +export TRUSTED_SERVER__PUBLISHER__PROXY_SECRET="${PROXY_SECRET}" # From secrets manager + +# Required: Synthetic ID +export TRUSTED_SERVER__SYNTHETIC__COUNTER_STORE="counter_store" +export TRUSTED_SERVER__SYNTHETIC__OPID_STORE="opid_store" +export TRUSTED_SERVER__SYNTHETIC__SECRET_KEY="${SYNTHETIC_KEY}" # From secrets manager +export TRUSTED_SERVER__SYNTHETIC__TEMPLATE="{{ client_ip }}:{{ user_agent }}" + +# Optional: Request Signing +export TRUSTED_SERVER__REQUEST_SIGNING__ENABLED=true +export TRUSTED_SERVER__REQUEST_SIGNING__CONFIG_STORE_ID="config-store-123" +export TRUSTED_SERVER__REQUEST_SIGNING__SECRET_STORE_ID="secret-store-456" + +# Optional: Prebid Integration +export TRUSTED_SERVER__INTEGRATIONS__PREBID__ENABLED=true +export TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL="https://prebid-server.com" +export TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=2000 +export TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS="appnexus,rubicon,openx" +export TRUSTED_SERVER__INTEGRATIONS__PREBID__AUTO_CONFIGURE=true + +# Optional: Security Headers +export TRUSTED_SERVER__RESPONSE_HEADERS__STRICT_TRANSPORT_SECURITY="max-age=31536000" +export TRUSTED_SERVER__RESPONSE_HEADERS__X_CONTENT_TYPE_OPTIONS="nosniff" +export TRUSTED_SERVER__RESPONSE_HEADERS__X_FRAME_OPTIONS="SAMEORIGIN" + +# Build and deploy +cargo build --release --target wasm32-wasip1 +fastly compute publish +``` + +--- + +## Next Steps + +- Review [Configuration Reference](./configuration-reference.md) +- Understand [Error Reference](./error-reference.md) +- Explore [API Reference](./api-reference.md) +- Learn about [Request Signing](./request-signing.md) diff --git a/docs/guide/error-reference.md b/docs/guide/error-reference.md new file mode 100644 index 0000000..a7c9c43 --- /dev/null +++ b/docs/guide/error-reference.md @@ -0,0 +1,665 @@ +# Error Reference + +Common errors, their causes, and solutions when working with Trusted Server. + +## Table of Contents + +- [Configuration Errors](#configuration-errors) +- [Runtime Errors](#runtime-errors) +- [Integration Errors](#integration-errors) +- [Request Signing Errors](#request-signing-errors) +- [Build & Deployment Errors](#build--deployment-errors) + +--- + +## Configuration Errors + +### Failed to load settings + +**Error Message:** +``` +Failed to load settings: ParseError +``` + +**Cause:** Invalid TOML syntax in `trusted-server.toml` + +**Solution:** +1. Validate TOML syntax using an online validator +2. Check for missing quotes around strings +3. Ensure array syntax uses square brackets: `["item1", "item2"]` +4. Verify section headers use brackets: `[section]` + +**Example Fix:** +```toml +# ❌ Wrong +[publisher] +domain = test-publisher.com # Missing quotes + +# ✅ Correct +[publisher] +domain = "test-publisher.com" +``` + +--- + +### Missing required configuration + +**Error Message:** +``` +Missing required field: publisher.domain +``` + +**Cause:** Required configuration field not provided + +**Solution:** Add the missing field to `trusted-server.toml`: + +```toml +[publisher] +domain = "your-publisher-domain.com" +origin_url = "https://origin.your-publisher-domain.com" +proxy_secret = "change-me-to-random-string" +``` + +**Required Fields:** +- `publisher.domain` +- `publisher.origin_url` +- `publisher.proxy_secret` +- `synthetic.secret_key` + +--- + +### Invalid URL format + +**Error Message:** +``` +Invalid URL in integrations.prebid.server_url +``` + +**Cause:** Malformed URL in configuration + +**Solution:** Ensure URLs are well-formed with scheme: + +```toml +# ❌ Wrong +[integrations.prebid] +server_url = "prebid-server.example.com" + +# ✅ Correct +[integrations.prebid] +server_url = "https://prebid-server.example.com" +``` + +--- + +### Environment variable override failed + +**Error Message:** +``` +Failed to parse environment variable: TRUSTED_SERVER__PUBLISHER__DOMAIN +``` + +**Cause:** Environment variable format doesn't match expected type + +**Solution:** Use correct format for the field type: + +```bash +# For strings +TRUSTED_SERVER__PUBLISHER__DOMAIN="example.com" + +# For numbers +TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1000 + +# For booleans +TRUSTED_SERVER__INTEGRATIONS__PREBID__ENABLED=true + +# For arrays (comma-separated) +TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS="appnexus,rubicon" +``` + +See [Environment Variables Reference](./environment-variables.md) for complete patterns. + +--- + +## Runtime Errors + +### Synthetic ID generation failed + +**Error Message:** +``` +Failed to generate synthetic ID: KV store not available +``` + +**Cause:** KV store (counter_store or opid_store) not configured in Fastly + +**Solution:** +1. Create KV stores in Fastly dashboard +2. Link them to your Compute service +3. Update `trusted-server.toml`: + +```toml +[synthetic] +counter_store = "counter_store" # Must match Fastly KV store name +opid_store = "opid_store" +``` + +4. For local development, configure in `fastly.toml`: + +```toml +[local_server.kv_stores] + [[local_server.kv_stores.counter_store]] + key = "placeholder" + data = "placeholder" +``` + +--- + +### Backend not found + +**Error Message:** +``` +Backend not found: prebid-server +``` + +**Cause:** Dynamic backend creation failed or backend not configured + +**Solution:** + +For integrations using dynamic backends (Prebid, Testlight): +- Ensure the integration is enabled +- Verify the URL is accessible from Fastly edge +- Check Fastly service limits (backend count) + +For static backends, configure in Fastly dashboard: +1. Go to Origins → Hosts +2. Add backend with name matching configuration +3. Redeploy service + +--- + +### Token validation failed + +**Error Message:** +``` +Invalid tstoken signature +``` + +**Cause:** URL was modified after signing, or proxy_secret mismatch + +**Solution:** +1. Verify `publisher.proxy_secret` matches across environments +2. Don't manually modify signed URLs +3. Use `/first-party/sign` endpoint to generate new signatures +4. Check for URL encoding issues + +**Debug:** +```bash +# Test URL signing +curl "https://edge.example.com/first-party/sign?url=https://external.com/pixel.gif" +``` + +--- + +### Request timeout + +**Error Message:** +``` +Upstream request timeout after 1000ms +``` + +**Cause:** Upstream service (Prebid Server, Permutive, etc.) didn't respond in time + +**Solution:** +1. Increase timeout in configuration: + +```toml +[integrations.prebid] +timeout_ms = 2000 # Increase from default 1000ms +``` + +2. Verify upstream service is responsive: +```bash +curl -w "%{time_total}\n" https://upstream-service.example.com +``` + +3. Check Fastly backend timeout settings + +--- + +### Cookie domain mismatch + +**Error Message:** +``` +Warning: Cookie not set due to domain mismatch +``` + +**Cause:** `publisher.cookie_domain` doesn't match request domain + +**Solution:** +```toml +[publisher] +domain = "example.com" +cookie_domain = ".example.com" # Leading dot for subdomains +``` + +**Rules:** +- Use leading dot (`.example.com`) to cover subdomains +- Cookie domain must be parent of request domain +- Don't include protocol or port + +--- + +## Integration Errors + +### Prebid Server error + +**Error Message:** +``` +Prebid Server returned 400: Invalid OpenRTB request +``` + +**Cause:** OpenRTB transformation produced invalid request + +**Solution:** +1. Enable debug mode: +```toml +[integrations.prebid] +debug = true +``` + +2. Check logs for request/response details +3. Verify bidders are supported by your Prebid Server +4. Ensure ad unit format is correct: + +```javascript +{ + "code": "banner-1", + "mediaTypes": { + "banner": { + "sizes": [[300, 250], [728, 90]] + } + } +} +``` + +--- + +### Next.js rewriting not working + +**Error Message:** +``` +Next.js links still pointing to origin domain +``` + +**Cause:** Rewrite attributes don't match JSON keys in Next.js data + +**Solution:** +1. Inspect Next.js data payload in `