Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
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-tenantheader 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.readcapability 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