Authentication
Forepost uses Clerk on a custom domain (clerk.forepost.ai). Every API request from the SPA carries a Clerk-issued JWT:
- Verified server-side using the JWKS public keys from Clerk
- Algorithm pinned to
RS256 issclaim must match Forepost’s configured issuerazp(authorized party) claim must behttps://app.forepost.ai, no other Clerk application’s tokens are acceptedexpandnbfclaims enforced- JWKS keys cached for 1 hour, then re-fetched (so revoked keys cannot be honoured indefinitely)
reason field, iss_mismatch, expired, bad_signature, azp_not_allowed, etc. This makes deploys debuggable without leaking internal state.
Authorization
There’s currently one role: the workspace owner. A user can only read or write their own workspace, subscription, briefs, and history, keyed by Clerk user ID. There is no team or admin role yet.Email ownership
Subscribing the Daily Brief to an email address requires that address to be verified on your Clerk account. Forepost queries Clerk’s Backend API to confirm before saving. This stops the system being used to send mail to addresses you don’t own.Rate limiting
Per-user, sliding-hour windowed counters in D1:| Endpoint | Limit |
|---|---|
AI proxy (POST /) | 60 / hour |
POST /brief/send | 5 / hour |
POST /brief/send-digest | 5 / hour |
Transport security
- All traffic is HTTPS only.
- HSTS is set with
max-age=63072000; includeSubDomains; preload. - TLS 1.3 with modern ciphers (managed by Cloudflare and Vercel).
Browser security headers
The SPA atapp.forepost.ai ships with:
- Content-Security-Policy: strict allowlist;
default-src 'self'; explicitscript-srcandscript-src-elem;frame-ancestors 'none'(clickjack-proof). - X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: camera, microphone, geolocation, FLoC all disabled.
Input validation
Every write endpoint validates incoming data before any database write or upstream API call:- Workspace JSON: allowlisted keys, type checks per field, 16 KB total cap, 50-agent maximum
- Brief subscription: email regex + 254 char cap, IANA timezone, hour 0-23
- AI proxy: system prompt and message content size caps, max-token cap of 1024
Email integrity
Outbound mail is sent via Resend with full DNS alignment onforepost.ai:
- SPF:
v=spf1 include:amazonses.com ~all - DKIM. Resend-managed, signed with
d=forepost.ai - DMARC: published; currently
p=nonewith reporting (rua=); will tighten toquarantinethenrejectafter a clean reporting period
List-Unsubscribe header for one-click opt-out.
Unsubscribe tokens
Unsubscribe links are HMAC-SHA-256 signed with a server-side secret. Forging or replaying a token to disable someone else’s subscription is computationally infeasible. Verification is constant-time.CORS
The API is locked toapp.forepost.ai (and local-dev origins). Cross-site browser calls from any other origin are rejected.
Cross-app token isolation
Even if a token were issued by Clerk for some other application sharing the same Clerk instance, ourazp allowlist rejects it. There’s no way for a Clerk JWT not minted for Forepost to authenticate against the worker.
What’s not yet in place
- SOC 2 Type II. Planned. The controls outlined here are the substrate; the audit confirms them externally.
- SAML SSO for Enterprise. On the roadmap; Clerk supports it natively.
- Bug bounty. Not yet stood up. For now, security disclosures go to security@forepost.ai.
OWASP Top 10 (2021) — stance
The Forepost Worker has been spot-audited against each category. Summary below; details on request.| Category | Stance |
|---|---|
| A01 Broken Access Control | Every endpoint resolves the caller via Clerk JWT → user_id, then keys all reads/writes on that user_id. Admin endpoints additionally gated by an ADMIN_USER_IDS env-var allowlist. Workspace members get scoped read access via the membership table, never cross-workspace. |
| A02 Cryptographic Failures | JWT verification with RS256 + JWKS rotation (1-hour cache); HMAC-SHA-256 on unsubscribe tokens with constant-time verification; helpdesk OAuth secrets encrypted with AES-GCM at rest; HTTPS only on every surface. |
| A03 Injection | All D1 queries use prepared statements with bind() — no string-concatenated SQL anywhere. HTML in outgoing email rendered via a dedicated escapeHtml. Slack webhook URLs validated against a strict regex (hooks.slack.com/services/... only) on both save and send. |
| A04 Insecure Design | Honest-no-data principle prevents fabricated outputs. Email-ownership check (against Clerk’s verified addresses) before subscribing. Per-user rate limits per endpoint scope. Workspace JSON capped at 16 KB; agent count capped at 50. |
| A05 Security Misconfiguration | CORS strictly allowlisted to app.forepost.ai. Strict CSP, X-Frame DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy locked down. Generic 500 returned on uncaught errors — no stack traces in responses. |
| A06 Vulnerable + Outdated Components | Dependabot enabled (npm + GitHub Actions); weekly minor/patch grouped PR, monthly Action bumps. The worker’s runtime dependencies are minimal (Cloudflare runtime + Anthropic SDK pinned via package-lock). |
| A07 Authentication Failures | Clerk handles authentication. Forepost verifies iss, azp (cross-app token isolation), exp, nbf on every request. No password storage of any kind. |
| A08 Software + Data Integrity | Production worker deploys exclusively via GitHub Actions on merge to main, using a scoped Cloudflare API token. npm ci enforces package-lock hashes. No arbitrary code execution paths. |
| A09 Logging + Monitoring | All admin write actions append to admin_audit_log in D1. Cloudflare Workers observability is enabled for traffic + error metrics. Gap: no on-call alerting on error-rate spikes yet — operational rather than security, but flagged. |
| A10 Server-Side Request Forgery | The worker makes outbound requests to a small set of hardcoded destinations (Anthropic, Clerk, Resend, helpdesk OAuth endpoints, JWKS). The only user-controlled outbound URL is the Slack webhook, host-restricted to hooks.slack.com. No fetch(userControlledUrl) anywhere. |