SDK security

Protect tokens, validate redirects, isolate private caches, and respect API-enforced gates.

The publishable board key is public. Board-user bearer and refresh tokens are credentials. Admin API credentials do not belong in the SDK at all.

Know which value you are handling

ValueSafe in browser code?Storage rule
pk_… publishable board keyYesConfiguration or public environment variable
Board-password grantTreat as credentialhttpOnly cookie for SSR; board-scoped storage in a browser app
Board-user access tokenNo for SSR; unavoidable in browser-owned authhttpOnly cookie for SSR or deliberately selected browser storage
Board-user refresh tokenNohttpOnly cookie for SSR; never render into HTML
Operator or Admin API keyNoServer-only operator integration—never pass to @cavuno/board

The SDK defaults to memory storage in browsers and nostore on servers. Choose local or session storage only after accepting that any script executing on the page can read those tokens.

Validate return paths

OAuth, login, checkout, and password pages commonly accept a return path. Do not redirect directly to untrusted input. The server helper accepts same-origin paths and rejects schemes, protocol-relative paths, and URL-parser bypasses.

ts
import { safeRedirectPath } from '@cavuno/board/server';
const destination = safeRedirectPath(searchParams.get('returnTo'), '/account');
return Response.redirect(new URL(destination, request.url));

Validate externally supplied checkout URLs against the exact Cavuno or payment-provider origins your flow expects. Do not broaden the allowlist because a URL contains a trusted hostname as a substring.

Handle API gates as authority

Feature flags improve the UI, but they are not authorization. The Board API decides whether a candidate, employer, subscriber, or password grant may read or mutate a resource. Render private data only after that request succeeds, and handle 401, 403, plan_upgrade_required, and surface-specific error codes explicitly.

ts
import { isForbidden, isUnauthorized } from '@cavuno/board';
try {
return await board.me.savedJobs.list(undefined, {
cache: 'no-store',
headers: { authorization: `Bearer ${accessToken}` },
});
} catch (error) {
if (isUnauthorized(error)) return { state: 'signed_out' as const };
if (isForbidden(error)) return { state: 'not_allowed' as const };
throw error;
}

Log safely

Record the BoardApiError.code, HTTP status, and requestId. These values let support trace a request without exposing credentials. Redact authorization headers, cookies, email bodies, resumes, application data, and checkout tokens.

Verify the boundary

  1. Inspect the deployed JavaScript and source maps for operator keys, refresh tokens, and private environment values.
  2. Submit https://attacker.example and //attacker.example as return paths and confirm both fall back to the allowed local path.
  3. Request one private route as two users through the production cache and confirm identities and response bodies never cross.
  4. Trigger a typed API failure and confirm logs contain its code and request ID but no authorization or cookie value.
  5. Rotate a board password or revoke a board-user session and confirm the next request returns to the correct gate without a retry loop.

Production notes

  • Use CSP and normal frontend XSS defenses.
  • Review every cache that can receive an Authorization header.
  • Rate-limit application-owned proxy endpoints.
  • Scope persistent browser storage and server cookies by board when one origin serves multiple boards.
  • Treat successful logout as server revocation plus local cleanup. If revocation fails, the SDK deliberately retains stored tokens so you can retry instead of forgetting a still-live refresh token.

Continue with Run doctor to verify deployed read and SEO boundaries.