Gateway Security & Authentication Plan
Current State (Vulnerabilities)
The pfix.us gateway has zero authentication or rate limiting:
- Gateway β API is fully open.
POST /api/gateway/convertand/api/gateway/bulkaccept any caller. Anyone who discoversapi-pdf.theaccessible.orgcan invoke conversions directly, bypassing the gateway entirely. - No credit deduction. Gateway conversions call
recordCost()withuserId: undefinedβ costs are tracked in the ledger but never charged to a userβs credit balance. - No rate limiting. No IP-based, session-based, or token-based throttling exists on any gateway path. A single client can trigger unlimited parallel conversions.
- No tenant context. The gateway Worker sends no
X-Tenant-IDheader, so all gateway conversions are attributed to thedefaulttenant. - Cache is free to populate. Any URL can be submitted, filling R2 storage with cached conversions at no cost to the requester.
Design Principle: No Anonymous Access
All gateway conversions require an authenticated user. There is no free/anonymous tier. Unauthenticated requests to pfix.us are redirected to a sign-in page. This eliminates the entire class of anonymous abuse vectors and ensures every conversion is tied to a paying account with credits.
Cached content is public. Once a document has been converted and cached in R2, any subsequent request for the same URL + flags combination is served directly from cache without authentication. Auth is only required to trigger a new conversion. This means the first authenticated user who converts a document effectively pays for all future viewers β a reasonable tradeoff since cache hits cost nothing.
Architecture Overview
βββββββββββββββββββββββββββββββββββββββββββ β End User (Browser) β ββββββββ¬βββββββββββββββββββ¬ββββββββββββββββ β β Unauthenticatedβ β Authenticated β sign-in wall β β (JWT cookie or API key) βΌ βΌ βββββββββββββββββββββββββββββββββββββββββββ β Gateway Worker (pfix.us) β β β β 1. Parse URL + flags β β 2. Check R2 cache β serve if hit β β 3. Require auth (cookie or API key) β β 4. Rate limit (per-user KV counter) β β 5. POST to API with auth context β β 6. Serve interstitial β ββββββββββββββββ¬βββββββββββββββββββββββββββ β X-Gateway-Secret header X-Gateway-User / X-Gateway-Tier headers β βΌ βββββββββββββββββββββββββββββββββββββββββββ β API Worker (api-pdf.theaccessible.org) β β β β 1. Verify X-Gateway-Secret β β 2. Check credit balance β β 3. Enforce spend limits β β 4. Run conversion pipeline β β 5. Deduct credits post-conversion β β 6. Record cost in ledger β βββββββββββββββββββββββββββββββββββββββββββ1. Gateway-to-API Shared Secret
Problem: The /api/gateway/* endpoints are public. Anyone can call them directly.
Solution: A static shared secret verified on every gateway-originated request.
Implementation
Gateway Worker (workers/gateway/src/index.ts):
// Add to GatewayEnv:GATEWAY_API_SECRET: string;
// When calling the API:const apiResponse = await fetch(`${c.env.API_BASE_URL}/api/gateway/convert`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Gateway-Secret': c.env.GATEWAY_API_SECRET, }, body: JSON.stringify({ url: sourceUrl, flags, cacheKey }),});API Worker (workers/api/src/routes/gateway.ts):
// New middleware applied to all gateway routes:function requireGatewaySecret(c, next) { const secret = c.req.header('X-Gateway-Secret'); if (!secret || secret !== c.env.GATEWAY_API_SECRET) { return error(c, 'UNAUTHORIZED', 'Invalid gateway secret', 401); } return next();}
gateway.use('*', requireGatewaySecret);Secret provisioning:
# Generate a 64-char hex secret:openssl rand -hex 32
# Set on both workers:echo "SECRET" | npx wrangler secret put GATEWAY_API_SECRET --env production # gateway workerecho "SECRET" | npx wrangler secret put GATEWAY_API_SECRET --env production # api worker
# Also set in docker .env for Node serversFiles to modify:
workers/gateway/src/index.tsβ add header to API callsworkers/gateway/wrangler.tomlβ declare the secret bindingworkers/api/src/routes/gateway.tsβ add verification middlewareworkers/api/src/types/env.tsβ addGATEWAY_API_SECRETtoEnv
2. User Authentication (Required)
Every gateway conversion requires authentication. Two methods are supported:
| Method | Use Case | Rate Limit | Credit Cost |
|---|---|---|---|
| JWT Cookie | Browser users logged in via pdf.anglin.com | 50 conversions/hour per user | Credits deducted |
| API Key | Programmatic access, browser extensions, bulk | Per-key configurable (default 100/hour) | Credits deducted |
Unauthenticated requests receive a sign-in page instead of the interstitial.
2a. Unauthenticated Request Flow
When a user visits pfix.us/example.com/doc.pdf without auth:
- Gateway Worker checks for
__pfix_sessioncookie orAuthorization: Bearer pdfx_...header - Neither found β do not start conversion
- Serve a branded sign-in page:
ββββββββββββββββββββββββββββββββββββββββββββ ββ π Sign in to view this document ββ ββ TheAccessible.org converts PDFs to ββ WCAG-compliant accessible HTML. ββ ββ βββββββββββββββββββββββββ ββ β Sign In β β links to ββ βββββββββββββββββββββββββ pdf.anglin ββ .com/login ββ βββββββββββββββββββββββββ ββ β Create Account β ββ βββββββββββββββββββββββββ ββ ββ Already have an API key? ββ Use it with the browser extension ββ or pass it as a Bearer token. ββ ββ βββββββββββββββββββββββββ ββ Can't wait? ββ View original PDF β ββββββββββββββββββββββββββββββββββββββββββββThe sign-in link includes a redirect parameter: https://pdf.anglin.com/login?redirect=https://pfix.us/{original-path} so the user returns to the gateway URL after authenticating.
2b. Authenticated Access (JWT Cookie)
Logged-in users from the main pdf.anglin.com app can use the gateway with their existing account and credits.
How it works:
- User logs in at
pdf.anglin.com(Supabase Auth β JWT) - A cross-domain auth flow sets a
__pfix_sessioncookie onpfix.us:- Frontend calls
POST /api/gateway/auth/sessionwith the Supabase JWT - API validates the JWT and returns a signed session token (short-lived, 1 hour)
- Gateway Worker stores this as an HttpOnly cookie on
pfix.us
- Frontend calls
- On subsequent gateway requests, the Gateway Worker reads the cookie and passes
X-Gateway-User: {userId}andX-Gateway-Tier: authenticatedto the API - API checks credits, enforces spend limits, deducts after conversion
Session token format: A JWT signed with a separate GATEWAY_SESSION_SECRET, containing:
{ "sub": "user-uuid", "tenantId": "default", "iat": 1709740800, "exp": 1709744400}New endpoints:
POST /api/gateway/auth/sessionβ exchange Supabase JWT for gateway session tokenDELETE /api/gateway/auth/sessionβ revoke (clear cookie)GET /api/gateway/auth/statusβ check if user has a valid session (returns user email + credit balance)
Files to create/modify:
- NEW
workers/api/src/routes/gateway-auth.tsβ route file for session management workers/gateway/src/index.tsβ cookie reading logic, header injection, sign-in page rendering- NEW
workers/gateway/src/sign-in-page.tsβ branded sign-in page HTML
2c. API Key Access (Programmatic)
For integrations, browser extensions, and the bulk endpoint.
Key format: pdfx_live_{base62_random_32chars} (e.g., pdfx_live_a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6)
Database schema (Supabase migration):
CREATE TABLE api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, tenant_id TEXT NOT NULL DEFAULT 'default', name TEXT NOT NULL, -- user-provided label key_prefix TEXT NOT NULL, -- first 8 chars for display: "pdfx_liv" key_hash TEXT NOT NULL UNIQUE, -- SHA-256 of full key scopes TEXT[] NOT NULL DEFAULT '{gateway}', -- 'gateway', 'bulk', 'convert' rate_limit_per_hour INT DEFAULT 100, last_used_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, -- null = no expiry revoked_at TIMESTAMPTZ, -- null = active created_at TIMESTAMPTZ NOT NULL DEFAULT now());
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash) WHERE revoked_at IS NULL;CREATE INDEX idx_api_keys_user ON api_keys(user_id);Key lifecycle endpoints (protected by requireAuth β user must be logged in to manage keys):
POST /api/keysβ generate a new key (returns the full key ONCE; only the hash is stored)GET /api/keysβ list userβs keys (shows prefix + name + last_used, never the full key)DELETE /api/keys/:idβ revoke a keyPATCH /api/keys/:idβ update name, rate limit, expiry
Validation middleware (requireApiKey):
async function requireApiKey(c, next) { const authHeader = c.req.header('Authorization'); if (!authHeader?.startsWith('Bearer pdfx_')) return error(c, 'UNAUTHORIZED', 'API key required', 401);
const key = authHeader.slice(7); const keyHash = await sha256(key);
// Lookup in Supabase (cached in KV for 5 min) const apiKey = await lookupApiKey(c.env, keyHash); if (!apiKey || apiKey.revoked_at || (apiKey.expires_at && new Date(apiKey.expires_at) < new Date())) { return error(c, 'UNAUTHORIZED', 'Invalid or expired API key', 401); }
// Check key-specific rate limit const rateLimitKey = `ratelimit:apikey:${apiKey.id}:${hourBucket()}`; const count = await incrementKV(c.env.KV_RATE_LIMIT, rateLimitKey, 3600); if (count > apiKey.rate_limit_per_hour) { return error(c, 'RATE_LIMITED', 'API key rate limit exceeded', 429); }
// Touch last_used_at (fire-and-forget) updateLastUsed(c.env, apiKey.id);
c.set('userId', apiKey.user_id); c.set('email', apiKey.email); c.set('tenantId', apiKey.tenant_id); return next();}How API keys reach the gateway:
For browser-extension or programmatic use of pfix.us:
GET https://pfix.us/example.com/doc.pdfAuthorization: Bearer pdfx_live_a1B2c3D4...The Gateway Worker reads the Authorization header and forwards it to the API as X-Gateway-Api-Key: pdfx_live_.... The APIβs gateway middleware validates it using requireApiKey logic.
For the bulk endpoint:
POST https://api-pdf.theaccessible.org/api/gateway/bulkAuthorization: Bearer pdfx_live_a1B2c3D4...Content-Type: application/json{"urls": ["https://example.com/a.pdf", "https://example.com/b.pdf"]}Files to create/modify:
- NEW
workers/api/src/routes/api-keys.tsβ CRUD endpoints for key management - NEW
workers/api/src/middleware/api-key-auth.tsβrequireApiKeymiddleware - NEW
workers/api/src/services/api-keys.tsβ key generation, hashing, lookup, caching workers/gateway/src/index.tsβ forward Authorization header to API- Supabase migration for
api_keystable
3. Credit Integration for Gateway
Problem: Gateway conversions incur real AI cost (~$0.01-0.50 per document) but charge no one.
Solution: Every gateway conversion checks and deducts credits, identical to the upload pipeline.
Credit Check and Deduction
Modification to runGatewayConversion() in workers/api/src/routes/gateway.ts:
The gateway conversion function receives userId from the gateway middleware (resolved from JWT cookie or API key). Since there is no anonymous tier, userId is always present.
// After PDF fetch and classification, before cascade starts:const pageCount = classification.totalPages;const creditCheck = await checkCredits(env, userId, pageCount);if (!creditCheck.hasCredits) { throw new GatewayAuthError('INSUFFICIENT_CREDITS', `Need ${pageCount} credits but have ${creditCheck.balance}`);}
const spendCheck = await checkSpendLimits(env, userId, pageCount);if (!spendCheck.allowed) { throw new GatewayAuthError('SPEND_LIMIT_EXCEEDED', spendCheck.reason);}
// After successful conversion (final assembly done):const actualPages = completedPages.size;await deductCredits(env, userId, actualPages, `Gateway conversion: ${sourceUrl} (${actualPages} pages)`);Error Handling
A new GatewayAuthError class distinguishes credit/auth failures from conversion failures. The error type is stored in the job status so the interstitial can render the appropriate UI:
class GatewayAuthError extends Error { constructor(public code: 'INSUFFICIENT_CREDITS' | 'SPEND_LIMIT_EXCEEDED' | 'UNAUTHORIZED', message: string) { super(message); this.name = 'GatewayAuthError'; }}In the interstitial:
INSUFFICIENT_CREDITSβ βYou need more credits to convert this document. [Purchase Credits]βSPEND_LIMIT_EXCEEDEDβ βYouβve reached your spend limit. [Manage Limits]β
Cost Attribution
Update recordCost() calls to include the authenticated user:
recordCost(env, { userId, operationType: 'gateway-conversion', metadata: { tier: authMethod, // 'authenticated' | 'api-key' sourceUrl, totalPages, // ... existing fields },});Files to modify:
workers/api/src/routes/gateway.tsβ add credit check/deduct flow, accept userId parameterworkers/api/src/services/credits.tsβ (already exists, just import and use)
4. Rate Limiting
Implementation: KV-Based Sliding Window
All rate limiting uses Cloudflare KV with TTL-based expiration. This is simple and works across all Workers instances (KV is globally distributed).
Rate limit helper (new utility):
interface RateLimitResult { allowed: boolean; remaining: number; resetAt: number; // Unix timestamp}
async function checkRateLimit( kv: KVNamespace, key: string, limit: number, windowSeconds: number,): Promise<RateLimitResult> { const bucket = Math.floor(Date.now() / 1000 / windowSeconds); const kvKey = `${key}:${bucket}`; const current = parseInt(await kv.get(kvKey) || '0', 10);
if (current >= limit) { return { allowed: false, remaining: 0, resetAt: (bucket + 1) * windowSeconds, }; }
// Increment (non-atomic, but acceptable for rate limiting) await kv.put(kvKey, String(current + 1), { expirationTtl: windowSeconds * 2 }); return { allowed: true, remaining: limit - current - 1, resetAt: (bucket + 1) * windowSeconds, };}Rate Limits by Auth Method
| Method | Scope | Limit | Window | KV Key Pattern |
|---|---|---|---|---|
| JWT Cookie | User | 50 conversions | 1 hour | rl:user:{userId}:{hourBucket} |
| JWT Cookie | User | 500 conversions | 24 hours | rl:user-day:{userId}:{dayBucket} |
| API Key | Key | Configurable (default 100) | 1 hour | rl:key:{keyId}:{hourBucket} |
Rate limiting also serves as a safety net against compromised accounts or runaway scripts.
Response headers (set on all gateway responses):
X-RateLimit-Limit: 50X-RateLimit-Remaining: 47X-RateLimit-Reset: 1709744400429 response: When rate limited, the interstitial shows a friendly message with the reset time. For API key users, a JSON 429 response is returned.
Rate Limiting Location
Rate limiting runs in the Gateway Worker (before calling the API), because:
- Itβs the public entry point β stops abuse before any AI cost is incurred
- KV reads are fast (~10ms) and free on the Workers free plan
- The API Worker only receives pre-validated requests
Files to create/modify:
- NEW
workers/gateway/src/rate-limiter.ts workers/gateway/src/index.tsβ apply rate limiting before API callworkers/gateway/wrangler.tomlβ bindKV_RATE_LIMITnamespace to gateway
5. Tenant Propagation
Problem: Gateway conversions all fall back to the default tenant.
Solution: The Gateway Worker resolves tenant from the request context and passes it through.
Resolution Logic (in Gateway Worker)
- Custom domain: If the gateway is white-labeled (e.g.,
accessible.company.com), resolve tenant from the hostname via a KV lookup (tenant-domain:{hostname}βtenantId) - API key: If the request has an API key, the keyβs
tenant_idis used - Session cookie: The gateway session JWT includes
tenantId - Default: Fall back to
default
Header passed to API: X-Gateway-Tenant: {tenantId}
API Worker reads this header in the gateway middleware and sets c.set('tenantId', ...).
Files to modify:
workers/gateway/src/index.tsβ tenant resolution + header injectionworkers/api/src/routes/gateway.tsβ read tenant header in middleware
6. Interstitial Auth UI
The interstitial page shows the userβs auth state and credit info.
Authenticated User View (Normal)
ββββββββββββββββββββββββββββββββββββββββββββ [spinner] Preparing your accessible ββ document ββ ββ Converting to WCAG-compliant HTML ββ [standard] ββ ββ Signed in as [email protected] ββ Credits: 1,247 remaining ββ Converting page 5 of 47 ββββββββββββββββββββββββββββββββββββββββββββInsufficient Credits View
ββββββββββββββββββββββββββββββββββββββββββββ β Insufficient credits ββ ββ This document has 47 pages. ββ You have 12 credits remaining. ββ ββ βββββββββββββββββββββββββ ββ β Purchase Credits β ββ βββββββββββββββββββββββββ ββ ββ Can't wait? ββ View original PDF β ββββββββββββββββββββββββββββββββββββββββββββRate Limited View
ββββββββββββββββββββββββββββββββββββββββββββ β± Rate limit reached ββ ββ You've hit the hourly conversion ββ limit. Try again in 23 minutes. ββ ββ Can't wait? ββ View original PDF β ββββββββββββββββββββββββββββββββββββββββββββThe status endpoint (GET /api/gateway/status/:conversionId) is extended to return:
{ "data": { "status": "converting", "progress": 45, "phase": "Converting page 5 of 47", "creditBalance": 1247 }}Files to modify:
workers/gateway/src/interstitial.tsβ add auth state display, credit balance, error CTAsworkers/api/src/routes/gateway.tsβ return user info + credit balance in status response
7. Abuse Prevention
URL Validation
Not all URLs should be convertible. Block:
- Private/internal IPs (
10.x,192.168.x,127.x,169.254.x,::1,fc00::) - Localhost and link-local addresses
- Non-PDF URLs (already enforced via content-type check)
- URLs longer than 2048 characters
- Known malicious domains (optional blocklist)
function isAllowedUrl(url: string): { allowed: boolean; reason?: string } { const parsed = new URL(url);
// Block private IPs if (isPrivateIP(parsed.hostname)) return { allowed: false, reason: 'Private IP addresses not allowed' };
// Block non-HTTP(S) if (!['http:', 'https:'].includes(parsed.protocol)) return { allowed: false, reason: 'Only HTTP/HTTPS allowed' };
// Block overly long URLs if (url.length > 2048) return { allowed: false, reason: 'URL too long' };
return { allowed: true };}SSRF Prevention
The safeFetch() function in workers/api/src/services/url-fetcher.ts should validate the resolved IP (not just the hostname) to prevent DNS rebinding:
- After DNS resolution, before connecting, verify the IP is not private
- Use
fetch()with Cloudflareβs built-in protections where available
Logging for Abuse Detection
All gateway requests should log:
{ "event": "gateway_request", "sourceUrl": "https://example.com/doc.pdf", "userId": "user-uuid", "authMethod": "jwt-cookie", "rateLimitRemaining": 47, "cached": false, "timestamp": "2026-03-06T12:00:00Z"}Feed these into Loki. Alert on:
- Single user >100 conversions/day (possible script abuse)
- Single API key >1000 conversions/day
- Repeated 429s from the same user (brute-force attempt)
- Credit check failures (may indicate stolen credentials being tested)
8. Migration Plan (Implementation Order)
Phase 1: Gateway Secret (Day 1) β Critical
Close the open API endpoint. No user-facing changes.
- Generate shared secret
- Add
GATEWAY_API_SECRETto both workers - Add verification middleware to API gateway routes
- Add header injection to Gateway Worker
- Deploy both workers
- Verify: direct API calls without secret return 401
Phase 2: Authenticated Gateway Sessions (Day 1-3) β Critical
Require auth for all conversions.
- Create
gateway-auth.tsroutes (session exchange, status, revoke) - Add
GATEWAY_SESSION_SECRETto both workers - Add cookie reading + JWT validation to Gateway Worker
- Create
sign-in-page.tsfor unauthenticated users - Pass
X-Gateway-User+X-Gateway-Tier: authenticatedheaders - Add sign-in redirect flow (
pdf.anglin.com/login?redirect=pfix.us/...) - Deploy and test
Phase 3: Credit Integration (Day 3-4) β Critical
Charge for conversions.
- Import
checkCredits,checkSpendLimits,deductCreditsinto gateway route - Add credit check before conversion starts
- Add credit deduction after successful conversion
- Add
GatewayAuthErrorclass for credit/spend errors - Update interstitial with credit balance display and insufficient-credits UI
- Update status endpoint to return
userEmailandcreditBalance - Deploy and test
Phase 4: Rate Limiting (Day 4-5)
Prevent excessive usage even from authenticated users.
- Create
rate-limiter.tsutility - Bind
KV_RATE_LIMITto Gateway Worker - Add per-user rate limit check before API call in Gateway Worker
- Add
X-RateLimit-*response headers - Add 429 handling to interstitial
- Deploy and test
Phase 5: API Key System (Day 5-7)
Enable programmatic access.
- Create Supabase migration for
api_keystable - Create
api-keys.tsservice (generate, hash, lookup, cache) - Create
api-key-auth.tsmiddleware - Create
api-keys.tsroutes (CRUD) - Add API key forwarding to Gateway Worker
- Apply
requireApiKeyto/api/gateway/bulk - Add API key management UI to
pdf.anglin.comsettings - Deploy and test
Phase 6: Tenant Propagation (Day 7-8)
Attribute gateway usage to correct tenants.
- Add tenant resolution logic to Gateway Worker
- Pass
X-Gateway-Tenantheader - Read tenant header in API gateway middleware
- Test with white-labeled domains
Phase 7: Abuse Prevention Hardening (Day 8-10)
Polish security posture.
- Add URL validation (private IP blocking, SSRF prevention)
- Add structured logging for all gateway requests
- Set up Grafana alerts for abuse patterns
- Security audit of the complete auth flow
9. Environment Variables Summary
| Variable | Worker | Purpose |
|---|---|---|
GATEWAY_API_SECRET | Both | Shared secret for gateway β API auth |
GATEWAY_SESSION_SECRET | Both | JWT signing key for gateway session cookies |
KV_RATE_LIMIT | Gateway | KV namespace for rate limit counters |
10. Database Migrations Required
-- Phase 5: API KeysCREATE TABLE api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, tenant_id TEXT NOT NULL DEFAULT 'default', name TEXT NOT NULL, key_prefix TEXT NOT NULL, key_hash TEXT NOT NULL UNIQUE, scopes TEXT[] NOT NULL DEFAULT '{gateway}', rate_limit_per_hour INT DEFAULT 100, last_used_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now());
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash) WHERE revoked_at IS NULL;CREATE INDEX idx_api_keys_user ON api_keys(user_id);
-- Optional: gateway usage analyticsCREATE TABLE gateway_usage ( id BIGSERIAL PRIMARY KEY, user_id UUID NOT NULL REFERENCES auth.users(id), api_key_id UUID REFERENCES api_keys(id), source_url TEXT NOT NULL, auth_method TEXT NOT NULL, -- 'jwt-cookie' | 'api-key' pages_converted INT, credits_deducted INT, cost_usd NUMERIC(10,6), cached BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT now());
CREATE INDEX idx_gateway_usage_user ON gateway_usage(user_id, created_at);CREATE INDEX idx_gateway_usage_key ON gateway_usage(api_key_id, created_at);11. Testing Checklist
- Direct API call to
/api/gateway/convertwithout secret β 401 - Gateway Worker call with secret β 200/202
- Unauthenticated browser request β sign-in page (not interstitial)
- Cached URL without auth β served from R2 (cache hits are public)
- Uncached URL without auth β sign-in page
- JWT cookie auth β conversion starts, credits checked
- Authenticated: credit deducted after conversion
- Authenticated: insufficient credits β 402 with βPurchase creditsβ CTA
- Authenticated: spend limit exceeded β 429 with explanation
- Authenticated: 51st conversion in 1 hour β 429
- API key: valid key β conversion succeeds, credits deducted
- API key: revoked key β 401
- API key: expired key β 401
- API key: rate limit exceeded β 429
- Bulk endpoint without API key β 401
- Private IP in source URL β rejected
- Tenant resolved correctly from custom domain
- Grafana alerts fire on abuse patterns
- Sign-in redirect round-trip works (pdf.anglin.com β pfix.us)
- Session cookie expires after 1 hour, user redirected to sign in again
- Credit balance shown in interstitial during conversion
12. Files Summary
| File | Change |
|---|---|
NEW workers/gateway/src/sign-in-page.ts | Branded sign-in page for unauthenticated users |
NEW workers/gateway/src/rate-limiter.ts | KV-based rate limit utility |
NEW workers/gateway/src/url-validator.ts | URL validation (private IP blocking, etc.) |
NEW workers/api/src/routes/gateway-auth.ts | Session exchange, status, revoke endpoints |
NEW workers/api/src/routes/api-keys.ts | API key CRUD endpoints |
NEW workers/api/src/middleware/api-key-auth.ts | requireApiKey middleware |
NEW workers/api/src/services/api-keys.ts | Key generation, hashing, lookup, KV caching |
workers/gateway/src/index.ts | Auth check, cookie reading, header injection, sign-in redirect |
workers/gateway/src/interstitial.ts | Auth state display, credit balance, error CTAs |
workers/gateway/wrangler.toml | Add GATEWAY_API_SECRET, KV_RATE_LIMIT bindings |
workers/api/src/routes/gateway.ts | Gateway secret middleware, credit check/deduct, userId param |
workers/api/src/types/env.ts | Add GATEWAY_API_SECRET, GATEWAY_SESSION_SECRET to Env |
| Supabase migration | api_keys table, gateway_usage table |