Skip to content

Checking access...

Cross-Suite Authentication Architecture

Story: FAS-6.2 - Verify Cross-Suite SSO Last Updated: 2026-02-10

Overview

The Hello World Co-Op platform uses a cookie-based Single Sign-On (SSO) architecture that allows users to authenticate once and access all deployed suites without re-authenticating. Authentication is centralized through the oracle-bridge service, which proxies requests to the auth-service canister on the Internet Computer and manages HttpOnly cookies for secure token storage.

Architecture Diagram

                          Browser (*.helloworlddao.com)
    ┌─────────────────────────────────────────────────────────────────┐
    │                                                                 │
    │  ┌──────────┐  ┌───────────┐  ┌──────────┐  ┌──────────────┐  │
    │  │dao-suite │  │think-tank │  │governance│  │otter-camp   │  │
    │  │          │  │ -suite    │  │  -suite  │  │  -suite     │  │
    │  └────┬─────┘  └─────┬─────┘  └────┬─────┘  └──────┬──────┘  │
    │       │              │              │               │          │
    │       └──────────────┴──────┬───────┴───────────────┘          │
    │                             │                                   │
    │                   @hello-world-co-op/auth                      │
    │                   (shared auth package)                         │
    │                             │                                   │
    │                  credentials: 'include'                         │
    │                  + X-CSRF-Token header                          │
    │                                                                 │
    ├─────────────────────────────┼───────────────────────────────────┤
    │  Cookies (Domain: .helloworlddao.com)                          │
    │  ┌──────────────────────────────────────────────────────────┐  │
    │  │ access_token  (HttpOnly, Secure, Path=/)                │  │
    │  │ refresh_token (HttpOnly, Secure, Path=/api/auth)        │  │
    │  │ csrf_token    (non-HttpOnly, Secure, Path=/)            │  │
    │  └──────────────────────────────────────────────────────────┘  │
    └─────────────────────────────┼───────────────────────────────────┘
                                  │ HTTPS

                      ┌───────────────────────┐
                      │    oracle-bridge       │
                      │    (Node.js/Express)   │
                      │                        │
                      │  /api/auth/login         │
                      │  /api/auth/wallet-login  │
                      │  /api/auth/refresh       │
                      │  /api/auth/logout        │
                      │  /api/auth/session       │
                      │  /api/auth/csrf-token    │
                      └───────────┬────────────┘
                                  │ IC Agent

                      ┌───────────────────────┐
                      │   auth-service         │
                      │   (Rust canister)      │
                      │                        │
                      │  login_email_password  │
                      │  refresh_tokens        │
                      │  logout                │
                      └────────────────────────┘

Authentication Flow

1. Login Flow

User (browser)                oracle-bridge              auth-service (IC)
      │                             │                           │
      │  POST /api/auth/login       │                           │
      │  {email, password,          │                           │
      │   device_fingerprint}       │                           │
      │  credentials: 'include'     │                           │
      ├────────────────────────────►│                           │
      │                             │  login_email_password()   │
      │                             ├──────────────────────────►│
      │                             │                           │
      │                             │  {access_token,           │
      │                             │   refresh_token,          │
      │                             │   access_expires_at,      │
      │                             │   user_id}                │
      │                             │◄──────────────────────────┤
      │                             │                           │
      │  Set-Cookie: access_token   │                           │
      │  Set-Cookie: refresh_token  │                           │
      │  Set-Cookie: csrf_token     │                           │
      │  Body: {user_id,            │                           │
      │   access_expires_at}        │                           │
      │◄────────────────────────────┤                           │
      │                             │                           │
      │  Store session metadata     │                           │
      │  in memory (expiry time)    │                           │

1b. Wallet Login Flow (BL-058)

Wallet login supports Plug, NFID, Ledger, and Trezor. The flow varies by wallet type but converges at the oracle-bridge endpoint.

User (browser)                oracle-bridge              identity-gateway (IC)
      │                             │                           │
      │  WalletAdapter.connect()    │                           │
      │  (browser-side SDK)         │                           │
      │                             │                           │
      │  POST /api/auth/wallet-login│                           │
      │  {wallet_type,              │                           │
      │   delegation_chain,         │                           │
      │   public_key?,              │                           │
      │   device_fingerprint}       │                           │
      │  credentials: 'include'     │                           │
      ├────────────────────────────►│                           │
      │                             │  validate_session()       │
      │                             ├──────────────────────────►│
      │                             │                           │
      │                             │  {principal, verified}    │
      │                             │◄──────────────────────────┤
      │                             │                           │
      │                             │  get_user_by_ii_principal │
      │                             │  (or create_user_ii if    │
      │                             │   new principal)          │
      │                             │                           │
      │  Set-Cookie: access_token   │                           │
      │  Set-Cookie: refresh_token  │                           │
      │  Set-Cookie: csrf_token     │                           │
      │  Body: {user_id,            │                           │
      │   wallet_type}              │                           │
      │◄────────────────────────────┤                           │

Wallet adapter types:

Walletdelegation_chainpublic_keyAuth method
PlugDelegation from extensionN/AExtension API
NFIDDelegation from popupDER-stripped bytesNFID SDK
IIDelegation from II windowN/A@dfinity/auth-client
LedgerN/ARaw secp256k1 (64 hex)WebUSB + BIP-44
TrezorN/ARaw secp256k1 (64 hex)Trezor Connect

Software wallets (Plug, NFID, II) provide delegation chains. Hardware wallets (Ledger, Trezor) provide raw public keys — the identity-gateway derives the principal from the key.

Frontend integration:

The @hello-world-co-op/auth package (v0.7.0+) provides:

  • WalletAdapter interface — standard connect/disconnect/getLoginPayload contract
  • registerWalletAdapter() — register adapters at app init
  • WalletSelector React component — renders wallet buttons with availability detection
  • useWalletLogin() hook — handles the connect → POST → redirect flow
typescript
import {
  registerWalletAdapter,
  PlugWalletAdapter,
  NFIDWalletAdapter,
  LedgerWalletAdapter,
  TrezorWalletAdapter,
  WalletSelector,
} from '@hello-world-co-op/auth';

// Register at app init
registerWalletAdapter(new PlugWalletAdapter());
registerWalletAdapter(new NFIDWalletAdapter());
registerWalletAdapter(new LedgerWalletAdapter());
registerWalletAdapter(new TrezorWalletAdapter());

// Render on login page
<WalletSelector onSuccess={() => navigate('/dashboard')} />

Wallet linking (membership canister):

The membership canister supports linking multiple wallet principals to one membership record via link_wallet(primary, linked). This allows a user to log in with any linked wallet and resolve to the same membership status. Linking is controller-only (not user-callable).

2. Session Check Flow (Cross-Suite Navigation)

When a user navigates from one suite to another (e.g., dao-suite to think-tank-suite):

User navigates to think-tank-suite

      │  AuthProvider mounts
      │  Calls checkSession()

      │  GET /api/auth/session
      │  credentials: 'include'
      │  (browser auto-attaches access_token cookie)
      ├──────────────────────────────►  oracle-bridge
      │                                  │
      │                                  │  Check access_token cookie
      │                                  │  exists and is valid
      │                                  │
      │  {authenticated: true}           │
      │◄──────────────────────────────────┤

      │  Update in-memory state:
      │  isAuthenticated = true
      │  Schedule token refresh

3. Token Refresh Flow

5 min before access token expiry:

      │  POST /api/auth/refresh
      │  credentials: 'include'
      │  X-CSRF-Token: <csrf_token from cookie>
      │  {device_fingerprint, timezone}
      ├──────────────────────────────►  oracle-bridge
      │                                  │
      │                                  │  Read refresh_token from cookie
      │                                  │  Call auth-service.refresh_tokens()
      │                                  │
      │  Set-Cookie: access_token (new)  │
      │  Set-Cookie: refresh_token (new) │
      │  Set-Cookie: csrf_token (rotated)│
      │  {access_expires_at (new)}       │
      │◄──────────────────────────────────┤

      │  Update in-memory expiry
      │  Schedule next refresh

4. Logout Flow

User clicks Logout on any suite:

      │  POST /api/auth/logout
      │  credentials: 'include'
      │  X-CSRF-Token: <csrf_token>
      ├──────────────────────────────►  oracle-bridge
      │                                  │
      │                                  │  Clear all auth cookies:
      │                                  │  - access_token
      │                                  │  - refresh_token
      │                                  │  - csrf_token
      │                                  │
      │  Set-Cookie: (cleared)           │
      │◄──────────────────────────────────┤

      │  Clear in-memory state
      │  Clear localStorage remnants

      │  When user visits other suite:
      │  checkSession() → authenticated: false
      │  → Redirect to /login
CookieHttpOnlySecureSameSiteDomainPathMax Age
access_tokenYesYes (prod)Configurable (default: strict).helloworlddao.com/15 min (matches token TTL)
refresh_tokenYesYes (prod)Configurable (default: strict).helloworlddao.com/api/auth7 days (matches token TTL)
csrf_tokenNoYes (prod)Configurable (default: strict).helloworlddao.com/24 hours
  • HttpOnly: Prevents JavaScript access to the cookie. Access and refresh tokens are HttpOnly to prevent XSS attacks from stealing tokens. The CSRF token is non-HttpOnly because the frontend must read it to include in request headers.

  • Secure: Ensures cookies are only sent over HTTPS. Always enabled in production. Can be disabled for local HTTP development.

  • SameSite: Controls when cookies are sent with cross-origin requests.

    • strict (default): Most secure; cookies only sent for same-site requests. May block cookies on direct cross-subdomain navigation from external links.
    • lax: Cookies sent for top-level navigations and same-site requests. Recommended for cross-subdomain SSO because it allows cookies when users navigate between subdomains.
    • none: Cookies sent for all requests. Requires Secure flag. Only use if suites are on different domains.
  • Domain: Set to .helloworlddao.com to allow cookie sharing across all subdomains (e.g., www.helloworlddao.com, staging.helloworlddao.com). When not set, cookies are scoped to the exact origin domain and cross-suite SSO will not work.

  • Path: The refresh token is restricted to /api/auth to minimize exposure. Access and CSRF tokens are available on all paths.

Environment Variables

VariableDescriptionDefaultProduction Value
AUTH_COOKIE_DOMAINCookie domain for cross-subdomain sharingnull (current domain).helloworlddao.com
AUTH_COOKIE_SECUREUse HTTPS-only cookiestrue in production, false in devtrue
AUTH_COOKIE_SAME_SITESameSite attributestrictlax (recommended for SSO)

Configuration Source

Cookie configuration is managed in oracle-bridge/src/config/cookies.ts:

typescript
export function getAuthCookieConfig(): AuthCookieConfig {
  const domain = process.env.AUTH_COOKIE_DOMAIN || null;
  const secure = process.env.AUTH_COOKIE_SECURE !== undefined
    ? process.env.AUTH_COOKIE_SECURE === 'true'
    : isProduction;
  // SameSite defaults to 'strict', supports 'lax' and 'none'
  const sameSite = process.env.AUTH_COOKIE_SAME_SITE || 'strict';
  return { domain, secure, sameSite };
}

Production Warnings

The cookie config module logs warnings at startup if:

  • Running in production without the Secure flag
  • AUTH_COOKIE_DOMAIN is not set (cookies won't work cross-subdomain)
  • SameSite=none is set without Secure flag

Token Storage Strategy

In-Memory Session Metadata (Client-Side)

The @hello-world-co-op/auth package stores only metadata in memory:

  • sessionId: User ID for display purposes
  • accessExpiresAt: Timestamp for proactive refresh timing

No tokens are stored in JavaScript-accessible storage. The actual access token and refresh token are in HttpOnly cookies managed by the server.

On page refresh, in-memory state is lost. The AuthProvider calls checkSession() on mount to re-establish session state from cookies.

Token Manager (tokenManager.ts)

typescript
class TokenManager {
  private sessionId: string | null = null;
  private accessExpiresAt: number | null = null;

  setSessionMetadata(sessionId: string, expiresAt: number): void { ... }
  isSessionExpired(): boolean {
    // 30-second buffer for network latency
    return Date.now() >= this.accessExpiresAt - 30_000;
  }
}

Why Not localStorage?

Tokens stored in localStorage are vulnerable to XSS attacks. Any injected script can read localStorage and exfiltrate tokens. HttpOnly cookies are inaccessible to JavaScript, making them immune to XSS token theft.

CSRF Protection

The platform uses the double-submit cookie pattern for CSRF protection:

  1. On login/refresh, the server sets a CSRF token in a non-HttpOnly cookie
  2. The frontend reads this cookie via document.cookie
  3. For state-changing requests (POST, PUT, PATCH, DELETE), the frontend includes the token as an X-CSRF-Token header
  4. The server validates that the cookie value matches the header value using constant-time comparison

Why This Works

An attacker cannot forge cross-site requests because:

  • They cannot read the CSRF cookie from another origin (same-origin policy)
  • They cannot set custom headers on cross-origin requests without CORS
  • Without the cookie value, they cannot set the X-CSRF-Token header

Client-Side CSRF Handling (csrf.ts)

typescript
// Read CSRF token from cookies
export function getCSRFToken(): string | null {
  const cookies = document.cookie.split(';');
  for (const cookie of cookies) {
    const [name, value] = cookie.trim().split('=');
    if (name === 'csrf_token') return decodeURIComponent(value);
  }
  return null;
}

// Fetch wrapper that auto-includes CSRF header
export async function csrfFetch(url: string, options: RequestInit): Promise<Response> {
  const headers = new Headers(options.headers);
  const token = getCSRFToken();
  if (token) headers.set('X-CSRF-Token', token);
  return fetch(url, { ...options, headers, credentials: 'include' });
}

CSRF Token Lifecycle

  • Generated on successful login
  • Rotated on each token refresh (prevents replay with stale tokens)
  • Cleared on logout
  • Fetched via GET /api/auth/csrf-token if missing (e.g., after page refresh before first state-changing request)

Session Management

Session Lifecycle

EventAccess Token (15 min)Refresh Token (7 days)In-Memory State
LoginSet in cookieSet in cookieisAuthenticated: true
Page refreshCookie persistsCookie persistsLost, restored via checkSession()
Suite navigationAuto-attached by browserAuto-attached on /api/auth pathsNew AuthProvider calls checkSession()
Token refresh (5 min before expiry)New cookie setNew cookie setaccessExpiresAt updated
LogoutCookie clearedCookie clearedisAuthenticated: false
Inactivity (30 min)N/AN/AAuto-logout triggered

Proactive Token Refresh

The AuthProvider schedules a refresh 5 minutes before the access token expires:

typescript
const scheduleRefresh = (expiresAt: number) => {
  const refreshIn = expiresAt - Date.now() - tokenRefreshBufferMs; // 5 min buffer
  setTimeout(async () => {
    const result = await refreshService(apiBaseUrl);
    if (result.success) scheduleRefresh(result.accessExpiresAt);
    else handleSessionExpired();
  }, refreshIn);
};

Refresh Queue (Mutex Pattern)

The TokenRefreshQueue prevents multiple concurrent refresh requests:

typescript
class TokenRefreshQueue<T> {
  private refreshPromise: Promise<QueuedRefreshResult<T>> | null = null;

  async refresh(refreshFn: () => Promise<T>): Promise<QueuedRefreshResult<T>> {
    if (this.refreshPromise) return this.refreshPromise; // Re-use in-flight request
    this.refreshPromise = this.doRefresh(refreshFn);
    return this.refreshPromise;
  }
}

Inactivity Timeout

The AuthProvider tracks user activity (mouse, keyboard, scroll, touch) and logs the user out after 30 minutes of inactivity:

  • Activity events are throttled (1 event per second)
  • The inactivity timer is reset on each activity
  • On timeout, logout() is called and the user is redirected to login

credentials: 'include'

All fetch requests to the oracle-bridge use credentials: 'include':

typescript
// Login (plain fetch)
fetch(url, { method: 'POST', credentials: 'include', ... });

// Session check
fetch(url, { method: 'GET', credentials: 'include' });

// Refresh and logout (via csrfFetch wrapper)
export async function csrfFetch(url, options) {
  return fetch(url, { ...options, credentials: 'include' });
}

This is required for the browser to:

  • Send cookies with cross-origin requests
  • Accept Set-Cookie headers from the server

The server must respond with Access-Control-Allow-Credentials: true in its CORS configuration.

Internet Identity Integration

Internet Identity (II) authentication uses a different flow from email/password:

  1. User initiates II authentication via @dfinity/auth-client
  2. II returns a delegation chain (not a password)
  3. The delegation is verified by the auth-service canister
  4. On success, the same cookie-based session is established

The cookie-based session management is provider-agnostic. Once authenticated through any provider (EmailPassword, II, Google, etc.), the session uses the same HttpOnly cookie mechanism.

Suite-by-Suite Auth Implementation

@hello-world-co-op/auth Package (Shared)

The canonical auth implementation. Suites should consume this package for:

  • AuthProvider - React context provider with session management
  • ProtectedRoute - Route guard with login redirect
  • useAuth() - Hook for accessing auth state
  • authServiceClient - Direct API client for login/logout/refresh/checkSession

think-tank-suite

Pattern: AuthProviderBridge (nanostores to @hello-world-co-op/auth context)

The think-tank-suite has a dual-mode auth system:

  • authStore (nanostores): Manages auth state internally, supports both cookie-based (authCookieClient) and legacy localStorage modes via isCookieAuthEnabled() feature flag
  • AuthProviderBridge: Wraps the nanostore state to provide AuthContextValue compatible with @hello-world-co-op/auth
  • Cookie mode: Enabled by default in production (VITE_USE_COOKIE_AUTH=true). Uses credentials: 'include' and CSRF protection
  • ProtectedRoute: Uses nanostores directly ($isAuthenticated, $isLoading)

Status: Fully compatible with cross-suite SSO when cookie mode is enabled.

governance-suite

Pattern: Same as think-tank-suite (nanostores + authCookieClient)

  • Uses its own authCookieClient.ts (copy of dao-suite pattern)
  • Has authStore with isCookieAuthEnabled() feature flag
  • ProtectedRoute uses nanostores directly
  • credentials: 'include' in all fetch calls

Status: Fully compatible with cross-suite SSO when cookie mode is enabled.

dao-suite

Pattern: Dual-mode (legacy localStorage + cookie-based client)

  • Legacy auth (utils/auth.ts): Stores tokens in localStorage - this is a known gap flagged as FE-DAO-1 (CRITICAL) with TODO to migrate
  • Cookie client (services/authCookieClient.ts): Newer cookie-based implementation using credentials: 'include' and CSRF
  • ProtectedRoute: Currently uses the legacy localStorage isAuthenticated() and getRefreshToken() from utils/auth.ts
  • authHelpers.ts: Reads user_data from localStorage

Gap: The ProtectedRoute component still uses the legacy localStorage-based auth, not the cookie client. This means dao-suite may not participate in cross-suite SSO until fully migrated to cookie auth. The cookie client exists but is not yet the default code path for route protection.

marketing-suite

Pattern: No authentication (public-only)

  • No auth imports found in source code
  • All pages are publicly accessible
  • No AuthProvider or ProtectedRoute

Status: N/A - no auth required.

otter-camp-suite

Pattern: Standalone localStorage reader

  • authHelpers.ts reads user_data from localStorage for simple auth checks
  • No dependency on @hello-world-co-op/auth
  • No cookie-based auth integration
  • Minimal auth footprint (just checks if user_data exists with an access token)

Gap: Does not use cookie-based auth. If cross-suite SSO is needed for the game, it would need to be integrated with the shared auth package or at minimum use the cookie session check.

Identified Gaps

Severity: Medium Impact: Developers may not configure cookie domain, breaking SSO in staging/production Location: /home/coby/git/oracle-bridge/.env.exampleFix: Add AUTH_COOKIE_DOMAIN, AUTH_COOKIE_SECURE, and AUTH_COOKIE_SAME_SITE to .env.example

Gap 2: dao-suite ProtectedRoute Uses Legacy localStorage Auth

Severity: Medium (known, tracked as FE-DAO-1) Impact: dao-suite route protection does not participate in cross-suite SSO Location: /home/coby/git/dao-suite/src/components/ProtectedRoute.tsxMitigation: The authCookieClient.ts exists in dao-suite and is ready for migration. The ProtectedRoute needs to be updated to use cookie-based session checks.

Gap 3: otter-camp-suite Uses Standalone localStorage Auth

Severity: Low (game may not need full SSO) Impact: Game suite cannot detect cross-suite sessions Location: /home/coby/git/otter-camp-suite/src/utils/authHelpers.tsMitigation: If SSO is needed for the game, integrate @hello-world-co-op/auth or add cookie-based session check.

Gap 4: SameSite Default is 'strict'

Severity: Low-Medium Impact: Cross-subdomain navigation from external links may not carry cookies with SameSite=strict. Users clicking a link to dao.helloworlddao.com from an email would not have their cookies sent, requiring re-login. Recommendation: Use AUTH_COOKIE_SAME_SITE=lax in production for better SSO UX.

Troubleshooting Guide

Issue 1: Cookies Not Shared Across Suites

Symptoms: User logs in on one suite but appears unauthenticated on another suite.

Diagnosis:

  1. Open browser DevTools > Application > Cookies
  2. Check the domain of auth cookies
  3. If domain shows exact hostname (e.g., www.helloworlddao.com) instead of .helloworlddao.com, cookies are not shared

Fix: Set AUTH_COOKIE_DOMAIN=.helloworlddao.com in oracle-bridge environment variables.

Issue 2: Session Lost on Page Refresh

Symptoms: User is authenticated, refreshes the page, and appears logged out momentarily.

Diagnosis: This is expected behavior. In-memory session metadata is lost on refresh. The AuthProvider calls checkSession() on mount to restore state from cookies.

Fix: Ensure isLoading state is handled in the UI (show loading indicator during session restoration). The ProtectedRoute component handles this automatically.

Issue 3: CORS Errors on Auth Requests

Symptoms: Console shows Access to fetch at '...' has been blocked by CORS policy.

Diagnosis:

  1. Check oracle-bridge CORS_ORIGINS includes the suite's origin
  2. Check that Access-Control-Allow-Credentials: true is in the response
  3. Verify credentials: 'include' is set in the fetch call

Fix:

  • Add the suite's origin to CORS_ORIGINS in oracle-bridge config
  • Ensure the CORS middleware sends Access-Control-Allow-Credentials: true

Issue 4: SameSite=Strict Blocks Cookies on Navigation

Symptoms: User is authenticated but loses session when clicking a link from an email or external site to a suite.

Diagnosis: SameSite=strict prevents cookies from being sent on top-level navigations initiated from a different site.

Fix: Set AUTH_COOKIE_SAME_SITE=lax in oracle-bridge. Lax allows cookies on top-level GET navigations while still preventing CSRF on POST requests.

Issue 5: Secure Flag Requires HTTPS

Symptoms: Cookies are not set in local development using http://localhost.

Diagnosis: The Secure flag requires HTTPS. In development, AUTH_COOKIE_SECURE defaults to false, but if manually set to true, cookies will not be set over HTTP.

Fix: For local development, ensure AUTH_COOKIE_SECURE is not set to true, or use HTTPS with a self-signed certificate.

Issue 6: CSRF Token Missing

Symptoms: State-changing requests (POST/PUT/DELETE) fail with 403 Forbidden - CSRF token required.

Diagnosis:

  1. Check if csrf_token cookie exists in DevTools
  2. Check if X-CSRF-Token header is included in the request
  3. CSRF cookie may be missing after initial page load (before login)

Fix: The csrfFetch() wrapper calls ensureCSRFToken() which fetches a token from GET /api/auth/csrf-token if none exists. Ensure all state-changing requests use csrfFetch() instead of plain fetch().

Issue 7: Token Refresh Race Condition

Symptoms: Multiple 401 errors in quick succession, or concurrent refresh requests.

Diagnosis: Multiple parts of the application detect an expired token and try to refresh simultaneously.

Fix: This is handled by the TokenRefreshQueue mutex. Only the first refresh request executes; subsequent callers receive the same promise result. Verify the refresh queue is being used (via authServiceClient.refreshTokens(), not direct fetch calls).

Debugging with Browser DevTools

Application Tab > Cookies:

  • Verify access_token, refresh_token, csrf_token are present
  • Check Domain is .helloworlddao.com (not exact hostname)
  • Verify HttpOnly flag on access_token and refresh_token
  • Check Secure and SameSite attributes

Network Tab:

  • Filter by /api/auth to see auth requests
  • Check Cookie request header includes auth cookies
  • Check Set-Cookie response headers for cookie attributes
  • Verify credentials: include in request details

Console Tab:

  • Auth service logs session initialization
  • Token refresh attempts with success/failure
  • Cookie configuration warnings at oracle-bridge startup
  • CSRF token read/fetch events

Configuration Reference

Auth Package (@hello-world-co-op/auth) Config

typescript
const config: AuthConfig = {
  apiBaseUrl: 'https://oracle.helloworlddao.com', // oracle-bridge URL
  inactivityTimeoutMs: 30 * 60 * 1000,            // 30 minutes
  tokenRefreshBufferMs: 5 * 60 * 1000,            // 5 minutes before expiry
  onSessionExpired: (returnUrl) => { ... },        // Custom handler
};

Oracle-Bridge Environment Variables

bash
# Auth cookie configuration
AUTH_COOKIE_DOMAIN=.helloworlddao.com    # Required for cross-suite SSO
AUTH_COOKIE_SECURE=true                   # Always true in production
AUTH_COOKIE_SAME_SITE=lax                 # 'lax' recommended for SSO

# CORS (must include all suite origins)
CORS_ORIGINS=https://helloworlddao.com,https://www.helloworlddao.com,https://staging.helloworlddao.com

# Auth service canister
AUTH_SERVICE_CANISTER_ID=<canister-id>
IC_HOST=https://ic0.app

Suite Environment Variables

bash
# Oracle-bridge URL for auth API calls
VITE_ORACLE_BRIDGE_URL=https://oracle.helloworlddao.com

# Cookie auth mode (think-tank-suite, governance-suite)
VITE_USE_COOKIE_AUTH=true    # Default: true in production

Hello World Co-Op DAO