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

251 lines
6.9 KiB
Markdown

# 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:
```json
{
"capabilities": ["cap:user.read", "cap:log.emit"]
}
```
## API Reference
### UserHost Interface
```typescript
interface UserHost {
getUser(): Promise<UserData>;
}
```
### Types
```typescript
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
```typescript
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
```typescript
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
```typescript
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:
```typescript
// 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`:
```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):
```wit
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:
```typescript
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
- [Scheduler Host API Guide](./scheduler-host-api.md)
- [Sample Client Portal Extension](../../../ee/extensions/samples/client-portal-test/)
- [Extension Runtime Reference](../../extension-runtime/)