PSA/sdk/docs/guides/user-host-api.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

6.9 KiB

User Host API Guide

This guide explains how to use the cap:user.read capability to access current user information in your extension.

Overview

The User Host API allows extensions to retrieve information about the currently authenticated user making the request. This is useful for:

  • Personalization: Customize responses based on the user's identity
  • Audit logging: Track which user triggered an action
  • Authorization: Make decisions based on user type (MSP vs client)
  • Multi-tenant awareness: Access tenant and client context

Prerequisites

The cap:user.read capability is included in the default capabilities for all extensions, so you don't need to explicitly declare it. However, if you're customizing capabilities, ensure it's included:

{
  "capabilities": ["cap:user.read", "cap:log.emit"]
}

API Reference

UserHost Interface

interface UserHost {
  getUser(): Promise<UserData>;
}

Types

interface UserData {
  /** Tenant ID the user belongs to */
  tenantId: string;
  /** Client/company name */
  clientName: string;
  /** Unique user identifier */
  userId: string;
  /** User's email address */
  userEmail: string;
  /** User's display name */
  userName: string;
  /** User type: "internal" (MSP staff) or "client" (client portal user) */
  userType: string;
  /** For client portal users, the client_id they are associated with */
  clientId?: string;
  /** Optional additional attributes provided by the host */
  additionalFields?: Record<string, string>;
}

type UserError = 'not-available' | 'not-allowed';

Usage Examples

Basic Usage

try {
  const user = await host.user.getUser();
  console.log(`Request from: ${user.userName} (${user.userEmail})`);
  console.log(`User type: ${user.userType}`);
} catch (err) {
  // User info not available (e.g., service-to-service call)
  console.log('No user context available');
}

Conditional Logic Based on User Type

const user = await host.user.getUser();

if (user.userType === 'internal') {
  // MSP staff - show full data
  return jsonResponse({
    data: allRecords,
    canEdit: true
  });
} else {
  // Client portal user - show filtered data
  return jsonResponse({
    data: filteredRecords,
    canEdit: false
  });
}

Including User Info in Responses

export async function handler(request: ExecuteRequest, host: HostBindings): Promise<ExecuteResponse> {
  let user = null;
  try {
    user = await host.user.getUser();
  } catch {
    // Continue without user info
  }

  return jsonResponse({
    ok: true,
    message: 'Hello from extension',
    user: user ? {
      name: user.userName,
      email: user.userEmail,
      type: user.userType,
    } : null,
  });
}

Setting Up Host Bindings

To use the User API (or any host capability), your extension needs a wrapper that imports the WIT functions. Create an index.ts that builds the HostBindings object:

// src/index.ts
import { handler as userHandler } from './handler-impl.js';
import { ExecuteRequest, ExecuteResponse, HostBindings, normalizeUserData } from '@alga-psa/extension-runtime';

// Import WIT functions (these are resolved at runtime by jco)
// @ts-ignore
import { getUser } from 'alga:extension/user-v2';
// @ts-ignore
import { logInfo, logWarn, logError } from 'alga:extension/logging';
// @ts-ignore
import { getContext } from 'alga:extension/context';
// ... other imports as needed

// Build the HostBindings object
const host: HostBindings = {
  context: {
    get: async () => getContext(),
  },
  logging: {
    info: async (msg: string) => logInfo(msg),
    warn: async (msg: string) => logWarn(msg),
    error: async (msg: string) => logError(msg),
  },
  user: {
    getUser: async () => normalizeUserData(await getUser()),
  },
  // ... other bindings
};

// Export WIT-compatible handler
export async function handler(request: ExecuteRequest): Promise<ExecuteResponse> {
  return userHandler(request, host);
}

Build Configuration

When using WIT imports, your build must mark them as external. Add a custom build script to package.json:

{
  "scripts": {
    "build:backend": "esbuild src/index.ts --bundle --format=esm --platform=neutral --outfile=dist/js/index.js --external:alga:extension/secrets --external:alga:extension/http --external:alga:extension/storage --external:alga:extension/logging --external:alga:extension/ui-proxy --external:alga:extension/context --external:alga:extension/user --external:alga:extension/user-v2",
    "build:component": "jco componentize dist/js/index.js --wit ./wit/extension-runner.wit --world-name runner --disable all --out dist/main.wasm",
    "build": "npm run build:backend && npm run build:component"
  },
  "devDependencies": {
    "@bytecodealliance/jco": "^1.8.0",
    "esbuild": "^0.20.0"
  }
}

WIT Definition

Ensure your wit/extension-runner.wit includes the user interface (v2):

record user-data-v2 {
    tenant-id: string,
    client-name: string,
    user-id: string,
    user-email: string,
    user-name: string,
    user-type: string,
    client-id: option<string>,
    additional-fields: list<tuple<string, string>>,
}

enum user-error {
    not-available,
    not-allowed,
}

interface user-v2 {
    get-user: func() -> result<user-data-v2, user-error>;
}

world runner {
    // ... other imports
    import user-v2;

    export handler: func(request: execute-request) -> execute-response;
}

Error Handling

The getUser() function can throw errors in these cases:

Error Cause
not-available No user session (e.g., service-to-service call, scheduled task)
not-allowed The cap:user.read capability was not granted

Always wrap getUser() in a try-catch:

let userName = 'Anonymous';
try {
  const user = await host.user.getUser();
  userName = user.userName;
} catch (err) {
  await host.logging.info('No user context available for this request');
}

When User Info Is Not Available

User information is extracted from the session. It will be null or throw not-available in these scenarios:

  • Scheduled task invocations: Tasks run by the scheduler don't have a user session
  • Service-to-service calls: API calls using x-alga-tenant header instead of session
  • Webhook callbacks: External systems calling your extension endpoints

Design your extension to handle both authenticated and unauthenticated contexts gracefully.

Security Notes

  • User data is read-only; extensions cannot modify user information
  • The cap:user.read capability is granted by default but can be revoked
  • User IDs are tenant-scoped and may differ across tenants
  • Do not expose sensitive user information to client-side code

See Also