Files
Jay Brown 451da3d26d Merged in feature/service-accounts-1 (pull request #227)
Support for service accounts with static credentials

* feature complete

* tests and docs

* lint fix
2026-05-21 19:40:04 +00:00

31 KiB

UI Client Authentication Guide

This document provides technical instructions for frontend (UI) developers integrating with the Query Orchestration API. It covers how to authenticate requests, discover user permissions, handle errors, and configure CORS for cross-origin deployments.

Table of Contents

  1. Authentication Overview
  2. Sending Authenticated Requests
  3. Discovering User Permissions
  4. Error Handling
  5. Token Refresh Strategy
  6. CORS Configuration
  7. Security Best Practices
  8. Quick Reference

Authentication Overview

The Query Orchestration API uses JWT (JSON Web Token) authentication with tokens issued by AWS Cognito. The authentication flow assumes that the UI application handles user login directly with Cognito and obtains the necessary tokens.

Service-account callers (non-UI processes): queryAPI also accepts JWTs from a second Cognito user pool dedicated to service accounts. The flow is different (Cognito InitiateAuth with username/password instead of OAuth + PKCE + MFA) and the lifecycle is operator-driven via a CLI tool. See 13-two-pool-service-accounts.md for the full reference. Everything below in this guide describes the human-user (pool A) path.

Token Types

When a user authenticates with Cognito, your UI receives three tokens:

Token Purpose Typical Lifetime
access_token Used for API requests - Contains user identity and scopes 1 hour
id_token Contains user profile claims (email, name, etc.) 1 hour
refresh_token Used to obtain new access/id tokens without re-authentication 30 days

Important: The Query Orchestration API validates the access_token. Always use this token (not the id_token) in the Authorization header.

Authentication Flow

┌─────────────┐      ┌─────────────┐      ┌─────────────────────┐      ┌─────────────┐
│   UI App    │      │   Cognito   │      │  Query Orchestration│      │  Permit.io  │
│  (Frontend) │      │   (IDP)     │      │       API           │      │  (AuthZ)    │
└──────┬──────┘      └──────┬──────┘      └──────────┬──────────┘      └──────┬──────┘
       │                    │                        │                        │
       │  1. Login request  │                        │                        │
       │───────────────────>│                        │                        │
       │                    │                        │                        │
       │  2. Tokens         │                        │                        │
       │<───────────────────│                        │                        │
       │                    │                        │                        │
       │  3. API Request + Bearer Token              │                        │
       │────────────────────────────────────────────>│                        │
       │                    │                        │                        │
       │                    │                        │  4. Check Permission   │
       │                    │                        │───────────────────────>│
       │                    │                        │                        │
       │                    │                        │  5. Allowed/Denied     │
       │                    │                        │<───────────────────────│
       │                    │                        │                        │
       │  6. API Response                            │                        │
       │<────────────────────────────────────────────│                        │

Sending Authenticated Requests

Authorization Header Format

All authenticated API requests must include the JWT access token in the Authorization header using the Bearer scheme:

Authorization: Bearer <access_token>

Accepted Token Locations

The API accepts tokens from exactly two locations:

Location Format Notes
Authorization header Bearer <token> Recommended for cross-origin requests
auth_token cookie JWT string Used by cookie-based auth flow

Not supported:

  • Query parameters (e.g., ?token=...)
  • Other header names
  • Request body

If your deployment uses cookie-based authentication, the server uses these HTTP-only cookies:

Cookie Name Purpose
auth_token Access token (JWT)
refresh_token Refresh token for obtaining new access tokens

When cookies are present, the server middleware automatically copies the auth_token cookie value to the Authorization header internally. The server can also automatically refresh expired tokens using the refresh_token cookie.

Note: Cookie-based auth requires CORS configuration for cross-origin deployments. See CORS Configuration.

No CSRF Token Required

The API does not require CSRF tokens. JWT bearer token authentication provides protection against CSRF attacks because:

  • The token must be explicitly included in the Authorization header
  • Browsers do not automatically attach custom headers to cross-origin requests

JavaScript/TypeScript Examples

Using Fetch API

const API_BASE_URL = 'https://api.yourdomain.com';

async function makeAuthenticatedRequest(endpoint, options = {}) {
  const accessToken = getAccessToken(); // Retrieve from your token storage

  const response = await fetch(`${API_BASE_URL}${endpoint}`, {
    ...options,
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });

  if (!response.ok) {
    await handleErrorResponse(response);
  }

  return response.json();
}

// Example: Get client details
async function getClient(clientId) {
  return makeAuthenticatedRequest(`/client/${clientId}`);
}

// Example: Upload a document
async function uploadDocument(clientId, file, folderId = null) {
  const accessToken = getAccessToken();
  const formData = new FormData();
  formData.append('file', file);
  if (folderId) {
    formData.append('folderId', folderId);
  }

  const response = await fetch(`${API_BASE_URL}/client/${clientId}/document`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      // Note: Do NOT set Content-Type for FormData - browser sets it automatically
    },
    body: formData,
  });

  if (!response.ok) {
    await handleErrorResponse(response);
  }

  return response.json();
}

Using Axios

import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.yourdomain.com',
});

// Add auth token to every request
apiClient.interceptors.request.use((config) => {
  const accessToken = getAccessToken();
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});

// Handle 401 errors globally
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      // Token expired or invalid - attempt refresh or redirect to login
      const refreshed = await attemptTokenRefresh();
      if (refreshed) {
        // Retry the original request with new token
        error.config.headers.Authorization = `Bearer ${getAccessToken()}`;
        return apiClient.request(error.config);
      }
      // Refresh failed - redirect to login
      redirectToLogin();
    }
    return Promise.reject(error);
  }
);

// Example usage
const client = await apiClient.get('/client/AAA');
const documents = await apiClient.get('/client/AAA/document');

Using React Query

import { useQuery, useMutation, QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient();

// Fetch wrapper with auth
async function authFetch<T>(endpoint: string, options?: RequestInit): Promise<T> {
  const accessToken = getAccessToken();

  const response = await fetch(`${API_BASE_URL}${endpoint}`, {
    ...options,
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
      ...options?.headers,
    },
  });

  if (response.status === 401) {
    // Trigger re-authentication
    throw new AuthenticationError('Token expired');
  }

  if (!response.ok) {
    throw new ApiError(response.status, await response.json());
  }

  return response.json();
}

// Hook for fetching client data
function useClient(clientId: string) {
  return useQuery({
    queryKey: ['client', clientId],
    queryFn: () => authFetch<Client>(`/client/${clientId}`),
    retry: (failureCount, error) => {
      // Don't retry on auth errors
      if (error instanceof AuthenticationError) return false;
      return failureCount < 3;
    },
  });
}

// Hook for fetching user identity/permissions
function useIdentity() {
  return useQuery({
    queryKey: ['identity'],
    queryFn: () => authFetch<IdentityResponse>('/identity'),
    staleTime: 5 * 60 * 1000, // Cache for 5 minutes
  });
}

Discovering User Permissions

The /identity Endpoint

After authentication, use the /identity endpoint to discover what the current user is authorized to do. This endpoint returns the user's roles and associated permissions from Permit.io.

Endpoint: GET /identity

Authentication: Required (Bearer token)

Response (200 OK):

{
  "roles": [
    {
      "key": "auditor",
      "name": "Auditor",
      "description": "Read-only access to view documents and exports",
      "permissions": [
        "client:get",
        "document:get",
        "folders:get",
        "export:get"
      ]
    },
    {
      "key": "editor",
      "name": "Editor",
      "description": "Can create and modify documents",
      "permissions": [
        "client:get",
        "client:post",
        "document:get",
        "document:post",
        "folders:get",
        "folders:post",
        "folders:patch"
      ]
    }
  ]
}

Permission Format

Permissions follow the format resource:action:

Resource Actions Description
client get, post, patch, delete Client management (delete: super_admin only)
clients get List all clients
document get, post, delete Document operations (delete: super_admin only)
folders get, post, patch, delete Folder management (delete: super_admin only)
export get, post Export operations
admin get, post, patch, delete User and EULA administration
eula get, post EULA status and agreement
super-admin get, post, patch, delete Schema management (CRUD for client_metadata_schemas; super_admin only for writes)
custom-metadata get, post Custom document metadata read/write (all roles have some access; auditor read-only)

Using Permissions in UI

interface IdentityRole {
  key: string;
  name: string;
  description?: string;
  permissions: string[];
}

interface IdentityResponse {
  roles: IdentityRole[];
}

// Fetch and cache user permissions
let cachedPermissions: Set<string> | null = null;

async function loadUserPermissions(): Promise<Set<string>> {
  if (cachedPermissions) {
    return cachedPermissions;
  }

  const response = await authFetch<IdentityResponse>('/identity');

  // Flatten all permissions from all roles
  const permissions = new Set<string>();
  for (const role of response.roles) {
    for (const permission of role.permissions) {
      permissions.add(permission);
    }
  }

  cachedPermissions = permissions;
  return permissions;
}

// Check if user has a specific permission
async function hasPermission(resource: string, action: string): Promise<boolean> {
  const permissions = await loadUserPermissions();
  return permissions.has(`${resource}:${action}`);
}

// Check if user can perform an action
async function canUploadDocuments(): Promise<boolean> {
  return hasPermission('client', 'post'); // Upload uses POST /client/{id}/document
}

async function canManageUsers(): Promise<boolean> {
  return hasPermission('admin', 'post');
}

// React hook example
function usePermission(resource: string, action: string) {
  const { data: identity, isLoading } = useIdentity();

  const hasPermission = useMemo(() => {
    if (!identity) return false;
    const permission = `${resource}:${action}`;
    return identity.roles.some(role =>
      role.permissions.includes(permission)
    );
  }, [identity, resource, action]);

  return { hasPermission, isLoading };
}

// Usage in component
function UploadButton() {
  const { hasPermission, isLoading } = usePermission('client', 'post');

  if (isLoading) return <Spinner />;
  if (!hasPermission) return null; // Hide button if not authorized

  return <Button onClick={handleUpload}>Upload Document</Button>;
}

Role-Based UI Rendering

// Define UI feature permissions
const UI_FEATURES = {
  viewDocuments: ['client:get', 'document:get'],
  uploadDocuments: ['client:post'],
  manageClients: ['client:post', 'client:patch'],
  deleteClients: ['client:delete'],
  deleteDocuments: ['document:delete'],
  deleteFolders: ['folders:delete'],
  viewExports: ['export:get'],
  createExports: ['export:post'],
  adminPanel: ['admin:get'],
} as const;

function canAccessFeature(
  userPermissions: Set<string>,
  feature: keyof typeof UI_FEATURES
): boolean {
  const requiredPermissions = UI_FEATURES[feature];
  return requiredPermissions.every(p => userPermissions.has(p));
}

Error Handling

HTTP Status Codes

Status Meaning UI Action
200 Success Process response
201 Created Resource created successfully
204 No Content Operation successful (no body)
400 Bad Request Show validation error to user
401 Unauthorized Token invalid/expired - refresh or re-login
403 Forbidden User lacks permission - show access denied
404 Not Found Resource doesn't exist
409 Conflict Operation cannot proceed in current state (e.g., folder has documents)
429 Too Many Requests Rate limited - implement backoff
500 Server Error Show generic error, retry later
502 Bad Gateway Authorization service unavailable

Error Response Format

Error responses typically include a message field, but the structure varies by endpoint and error type:

// Standard error format
{
  "message": "Description of what went wrong"
}

// Authentication error format (401)
{
  "error": "Authorization required",
  "message": "This endpoint requires authentication. Please obtain a valid JWT token.",
  "login_url": "/login"
}

// Authorization error format (403)
{
  "error": "Access denied"
}

Recommended error handling: Check for both error and message fields when parsing error responses.

Environment Access Denied (403)

When the server is configured with COGNITO_ENVIRONMENT_ID, an additional access control check runs after JWT validation. Users whose Cognito custom:environment_id attribute does not match the server's configured value will receive a 403 Forbidden response with the message "environment access denied". This applies to all authenticated endpoints.

Exceptions:

  • Users with the super_admin role bypass environment checks entirely.
  • Users with no custom:environment_id attribute (legacy/untagged users) are allowed through for backward compatibility.
  • If the server has no COGNITO_ENVIRONMENT_ID set, this check is skipped entirely.

The environment check result is cached per user for 30 minutes. If a user's environment assignment changes, the new assignment takes effect after the cache expires.

Handling Authentication Errors (401)

// Helper to extract error message from varying response formats
function getErrorMessage(body: any, fallback: string): string {
  return body.message || body.error || fallback;
}

async function handleErrorResponse(response: Response) {
  const body = await response.json().catch(() => ({}));

  switch (response.status) {
    case 401:
      // Token expired or invalid
      console.log('Authentication required:', getErrorMessage(body, 'Authentication required'));

      // Attempt to refresh the token (only if using header-based auth)
      const refreshed = await attemptTokenRefresh();
      if (!refreshed) {
        // Clear stored tokens
        clearTokens();
        // Redirect to login
        redirectToLogin();
      }
      throw new AuthenticationError(getErrorMessage(body, 'Authentication required'));

    case 403:
      // User authenticated but not authorized
      console.log('Access denied:', getErrorMessage(body, 'Access denied'));
      throw new AuthorizationError(getErrorMessage(body, 'Access denied'));

    case 429:
      // Rate limited - check RateLimit header
      const rateLimitInfo = response.headers.get('RateLimit');
      console.log('Rate limited:', rateLimitInfo);
      throw new RateLimitError(getErrorMessage(body, 'Rate limit exceeded'), rateLimitInfo);

    default:
      throw new ApiError(response.status, getErrorMessage(body, 'Request failed'));
  }
}

Rate Limiting

The API includes rate limiting. Monitor the RateLimit response header:

RateLimit: 100;window=60

This indicates 100 requests allowed per 60-second window.

// Implement exponential backoff for rate limits
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status === 429) {
      const retryAfter = response.headers.get('Retry-After');
      const waitTime = retryAfter
        ? parseInt(retryAfter) * 1000
        : Math.pow(2, attempt) * 1000;

      console.log(`Rate limited. Retrying in ${waitTime}ms`);
      await sleep(waitTime);
      continue;
    }

    return response;
  }

  throw new Error('Max retries exceeded');
}

Token Refresh Strategy

Server-Side vs Client-Side Refresh

The refresh strategy depends on how your UI sends authentication:

Authentication Method Who Handles Refresh Notes
Authorization header UI must refresh Server has no access to refresh token
Cookie-based Server auto-refreshes Server reads refresh_token cookie

If using Authorization: Bearer header (recommended for cross-origin):

  • The UI must store and manage the refresh token
  • The UI must call Cognito directly to refresh tokens
  • The server cannot refresh for you (it doesn't have your refresh token)

If using cookie-based authentication:

  • The server automatically refreshes expired tokens
  • New tokens are returned as updated cookies
  • No action required from the UI

When to Refresh (Header-Based Auth)

The access token has a limited lifetime (typically 1 hour). Implement proactive refresh to avoid authentication errors:

  1. Proactive refresh: Check token expiration before each request
  2. Reactive refresh: Refresh when receiving a 401 response

Checking Token Expiration

JWT tokens contain an exp claim with the expiration timestamp:

function isTokenExpired(token: string, bufferSeconds = 60): boolean {
  try {
    // Decode JWT payload (base64)
    const payload = JSON.parse(atob(token.split('.')[1]));
    const expiresAt = payload.exp * 1000; // Convert to milliseconds
    const now = Date.now();

    // Consider expired if within buffer period
    return now >= (expiresAt - bufferSeconds * 1000);
  } catch {
    return true; // Treat decode errors as expired
  }
}

function getAccessToken(): string | null {
  const token = localStorage.getItem('access_token');

  if (!token || isTokenExpired(token)) {
    return null;
  }

  return token;
}

Refreshing Tokens with Cognito

Use the Cognito SDK or direct API call to refresh tokens:

import {
  CognitoIdentityProviderClient,
  InitiateAuthCommand
} from '@aws-sdk/client-cognito-identity-provider';

const cognitoClient = new CognitoIdentityProviderClient({
  region: 'us-east-2', // Your Cognito region
});

async function refreshTokens(): Promise<boolean> {
  const refreshToken = localStorage.getItem('refresh_token');

  if (!refreshToken) {
    return false;
  }

  try {
    const command = new InitiateAuthCommand({
      AuthFlow: 'REFRESH_TOKEN_AUTH',
      ClientId: 'your-cognito-client-id',
      AuthParameters: {
        REFRESH_TOKEN: refreshToken,
      },
    });

    const response = await cognitoClient.send(command);

    if (response.AuthenticationResult) {
      // Store new tokens
      localStorage.setItem('access_token', response.AuthenticationResult.AccessToken!);
      localStorage.setItem('id_token', response.AuthenticationResult.IdToken!);
      // Note: Refresh token may not be returned - keep existing one

      return true;
    }

    return false;
  } catch (error) {
    console.error('Token refresh failed:', error);
    return false;
  }
}

Request Interceptor with Auto-Refresh

async function authFetchWithRefresh<T>(
  endpoint: string,
  options?: RequestInit
): Promise<T> {
  // Check if token needs refresh before request
  let accessToken = localStorage.getItem('access_token');

  if (!accessToken || isTokenExpired(accessToken)) {
    const refreshed = await refreshTokens();
    if (!refreshed) {
      throw new AuthenticationError('Session expired. Please log in again.');
    }
    accessToken = localStorage.getItem('access_token');
  }

  const response = await fetch(`${API_BASE_URL}${endpoint}`, {
    ...options,
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
      ...options?.headers,
    },
  });

  // Handle 401 in case token expired between check and request
  if (response.status === 401) {
    const refreshed = await refreshTokens();
    if (refreshed) {
      // Retry with new token
      return authFetchWithRefresh(endpoint, options);
    }
    throw new AuthenticationError('Session expired. Please log in again.');
  }

  if (!response.ok) {
    throw new ApiError(response.status, await response.json());
  }

  return response.json();
}

CORS Configuration

Overview

Since the UI and API are hosted on different domains, Cross-Origin Resource Sharing (CORS) must be configured. The API uses Echo framework's CORS middleware with the following default configuration:

  • Allowed Origins: * (all origins)
  • Allowed Methods: GET, HEAD, PUT, PATCH, POST, DELETE
  • Allowed Headers: All headers
  • Credentials: NOT enabled by default (AllowCredentials: false)

Important: The default CORS configuration does not support credentials (cookies). This means:

  • Cross-origin requests with credentials: 'include' will fail
  • Cookie-based authentication will not work cross-origin without server configuration changes
  • Use Authorization: Bearer header for cross-origin authentication (recommended)

UI Configuration for Cross-Origin Requests

For cross-origin requests, use the Authorization header (not cookies):

// Recommended: Use Authorization header for cross-origin requests
const response = await fetch(`${API_BASE_URL}/client/AAA`, {
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
});

Cookie-based authentication cross-origin requires server-side CORS configuration changes:

  1. AllowCredentials: true must be set on the server
  2. AllowOrigins must list specific origins (cannot use wildcard *)
  3. UI must use credentials: 'include' in fetch calls
// Cookie-based auth (ONLY works if server is configured for credentials)
const response = await fetch(`${API_BASE_URL}/client/AAA`, {
  method: 'GET',
  credentials: 'include', // Sends cookies cross-origin
  headers: {
    'Content-Type': 'application/json',
    // Authorization header optional if using cookies
  },
});

Preflight Requests

For non-simple requests (e.g., with custom headers like Authorization), the browser sends a preflight OPTIONS request. The API automatically handles these.

Preflight triggers include:

  • Custom headers (e.g., Authorization)
  • Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain
  • HTTP methods other than GET, HEAD, POST

Production CORS Configuration

For production deployments with specific UI domains, the API CORS configuration can be customized. Contact the backend team to configure:

// Example production CORS config (server-side)
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
    AllowOrigins: []string{
        "https://app.yourdomain.com",
        "https://admin.yourdomain.com",
    },
    AllowMethods: []string{
        http.MethodGet,
        http.MethodHead,
        http.MethodPut,
        http.MethodPatch,
        http.MethodPost,
        http.MethodDelete,
    },
    AllowHeaders: []string{
        "Authorization",
        "Content-Type",
        "Accept",
        "X-Requested-With",
    },
    AllowCredentials: true,
    MaxAge:           86400, // Preflight cache: 24 hours
}))

Troubleshooting CORS Issues

Error Cause Solution
No 'Access-Control-Allow-Origin' header Origin not allowed Add UI origin to server config
Preflight response is not successful OPTIONS blocked Ensure server handles OPTIONS
Credentials flag is true, but... Wildcard origin with credentials Use specific origins, not *
Request header not allowed Custom header blocked Add header to AllowHeaders

Security Best Practices

Token Storage

Storage Option Security Recommendation
localStorage Vulnerable to XSS Use with caution
sessionStorage Cleared on tab close, vulnerable to XSS Better than localStorage
Memory (variable) Lost on refresh, most secure Best for high-security apps
HTTP-only cookies Protected from XSS Requires server-side support

Recommended approach for SPAs:

// Store tokens in memory with sessionStorage backup
class TokenManager {
  private accessToken: string | null = null;
  private idToken: string | null = null;
  private refreshToken: string | null = null;

  constructor() {
    // Restore from sessionStorage on page load
    this.accessToken = sessionStorage.getItem('access_token');
    this.idToken = sessionStorage.getItem('id_token');
    this.refreshToken = sessionStorage.getItem('refresh_token');
  }

  setTokens(tokens: { accessToken: string; idToken: string; refreshToken: string }) {
    this.accessToken = tokens.accessToken;
    this.idToken = tokens.idToken;
    this.refreshToken = tokens.refreshToken;

    // Backup to sessionStorage (cleared on tab close)
    sessionStorage.setItem('access_token', tokens.accessToken);
    sessionStorage.setItem('id_token', tokens.idToken);
    sessionStorage.setItem('refresh_token', tokens.refreshToken);
  }

  getAccessToken(): string | null {
    return this.accessToken;
  }

  clearTokens() {
    this.accessToken = null;
    this.idToken = null;
    this.refreshToken = null;
    sessionStorage.clear();
  }
}

export const tokenManager = new TokenManager();

Content Security Policy

Configure CSP headers in your UI application to prevent XSS:

<meta http-equiv="Content-Security-Policy"
      content="default-src 'self';
               connect-src 'self' https://api.yourdomain.com https://cognito-idp.us-east-2.amazonaws.com;
               script-src 'self';
               style-src 'self' 'unsafe-inline';">

Additional Security Measures

  1. Always use HTTPS for both UI and API
  2. Validate token on the client before making requests
  3. Implement logout that clears all stored tokens
  4. Set reasonable token lifetimes in Cognito configuration
  5. Monitor for suspicious activity via API logs

Quick Reference

Required Headers

Authorization: Bearer <access_token>
Content-Type: application/json

Common Endpoints

Endpoint Method Description
/identity GET Get user roles and permissions
/health GET API health check (no auth required)
/clients GET List all clients
/client/{id} GET Get client details
/client/{id}/document GET List client documents
/client/{id}/document POST Upload document (multipart/form-data)
/client/{id}/document/batch POST Upload ZIP batch
/document/{id} GET Get document details
/client/{id} DELETE Hard-delete client (super_admin, requires confirm=true)
/document/{id} DELETE Hard-delete document (super_admin)
/folders/{folderId} DELETE Hard-delete folder tree (super_admin)
/clients/{clientId}/folders GET List folders
/eula GET Get current EULA (no auth required)
/eula/status GET Check user's EULA agreement status
/eula/agree POST Record EULA agreement

Public Endpoints (No Authentication Required)

Endpoint Description
/health Health check and version info
/swagger/* API documentation
/metrics Prometheus metrics
/eula Current EULA version (public)

TypeScript Type Definitions

// Identity types
interface IdentityRole {
  key: string;
  name: string;
  description?: string;
  permissions: string[];
}

interface IdentityResponse {
  roles: IdentityRole[];
}

// Error types
interface ErrorResponse {
  message: string;
}

// Client types
interface Client {
  client_id: string;
  name: string;
  can_sync: boolean;
}

// Document types
interface Document {
  id: string;
  client_id: string;
  hash: string;
  filename?: string;
  folder_id?: string;
  fields?: Record<string, any>;
}

Support

For API issues or questions:

  • Check the Swagger documentation at /swagger/index.html
  • Review API logs for detailed error information
  • Contact the backend team for CORS or authentication configuration changes