PSA/ee/docs/extension-system/development_guide.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

343 lines
14 KiB
Markdown

# Alga PSA Extension Development Guide (Runner + Iframe UI)
This guide describes how to build Enterprise Edition (v2) extensions for Alga PSA:
- Server-side handlers execute out-of-process in the Runner as Wasmtime **components** produced by `componentize-js`
- UI is rendered exclusively in sandboxed iframes
- Bundles are signed and content-addressed (sha256:...) and validated by the Registry
Core rules: See “Correctness Rules” in the README for the canonical list. Reference gateway scaffold: [server/src/app/api/ext/[extensionId]/[[...path]]/route.ts](../../../server/src/app/api/ext/%5BextensionId%5D/%5B%5B...path%5D%5D/route.ts)
## Prerequisites
- Node.js 18+
- Component toolchain:
- `@bytecodealliance/componentize-js` (`jco`) or another WIT-compatible pipeline
- `@alga-psa/extension-runtime` helpers for handlers/tests
- Familiarity with:
- TypeScript, React (for UI)
- Security best practices and least-privilege design
## SDK and CLI Tools
The Alga SDK provides CLI tools and libraries to streamline extension development:
### Installation
```bash
npm install -g @alga-psa/cli
# Or use npx: npx @alga-psa/cli <command>
```
### CLI Commands
| Command | Description |
|---------|-------------|
| `alga create extension <name>` | Scaffold a new extension project |
| `alga create component <name>` | Scaffold a WASM component project |
| `alga build` | Build the extension (compile TS → WASM if needed) |
| `alga pack` | Create `bundle.tar.zst` from built extension |
| `alga extension publish <dir>` | Build, pack, and publish to an Alga instance |
| `alga extension install <id>` | Install an extension from the registry |
| `alga extension uninstall <id>` | Uninstall an extension |
| `alga sign <bundle>` | Sign a bundle with cosign, x509, or pgp |
### SDK Packages
| Package | Purpose |
|---------|---------|
| `@alga-psa/cli` | Command-line tools for building and publishing |
| `@alga-psa/client-sdk` | Programmatic APIs for build/pack/publish |
| `@alga-psa/extension-runtime` | TypeScript types and helpers for WASM handlers |
| `@alga/extension-iframe-sdk` | Host communication APIs for iframe UIs |
## Project Layout (Example)
```
my-extension/
├── src/
│ ├── component/ # Component handler source (TS/JS + @alga-psa/extension-runtime)
│ │ ├── handler.ts
│ │ └── wit/ # Generated WIT/world bindings
│ ├── ui/ # Iframe app (Vite + React)
│ │ ├── index.html
│ │ └── src/
│ │ ├── main.tsx
│ │ └── components/
│ └── manifest.json # Manifest v2
├── dist/
│ ├── main.wasm # Componentized artifact produced by jco componentize
│ └── ui/ # Built iframe assets
├── SIGNATURE # Signature file (generated by CI)
├── package.json
└── README.md
```
## Step-by-step: Build + Package an Extension
### Quick Start with CLI
The fastest way to create and build an extension:
```bash
# Create a new extension project
alga create extension my-extension
cd my-extension
# Build (compiles TypeScript and creates WASM if needed)
alga build
# Create the bundle archive
alga pack
# Output: dist/bundle.tar.zst
# Publish to your Alga instance
alga extension publish . --api-key $ALGA_API_KEY --tenant $ALGA_TENANT_ID
```
### Manual Setup
1. **Scaffold directories**
- Create `component/` for your handler sources (e.g., `component/src/handler.ts`) and copy the platform WIT definitions into `component/wit/extension-runner.wit`.
- Add `ui/` for your static iframe app (any bundler). The host will serve files from `ui/dist` inside the bundle.
- Place `manifest.json` at the repo root alongside `package.json`.
2. **Author the Wasmtime endpoint**
- Install the toolchain: `npm install @alga-psa/extension-runtime @bytecodealliance/componentize-js`.
- Implement `handler` using the runtime helpers:
```ts
// component/src/handler.ts
import { Handler, jsonResponse } from '@alga-psa/extension-runtime';
export const handler: Handler = async (req, host) => {
const secret = await host.secrets.get('api_key');
const upstream = await host.http.fetch({
url: 'https://httpbin.org/get',
headers: { authorization: `Bearer ${secret}` },
});
return jsonResponse({ ok: true, upstream: upstream.status });
};
```
- Compile to JavaScript (e.g., `tsc` or Vite build) so `dist/js/handler.js` exists.
- Run `jco componentize` to produce the Wasmtime component:
`npx jco componentize dist/js/handler.js --wit ./wit/extension-runner.wit --world-name runner --out dist/main.wasm`
3. **Add static UI assets**
- Build your iframe application into `ui/dist` (for example, `npm run ui:build` producing `ui/dist/index.html`, JS/CSS chunks, images).
- Reference the entry file from the manifest via `"ui": { "type": "iframe", "entry": "ui/dist/index.html" }`.
4. **Create or update `manifest.json`**
- Include metadata, capabilities, and `assets` globs:
```json
{
"name": "com.example.basic",
"publisher": "Example Inc.",
"version": "1.0.0",
"runtime": "wasm-js@1",
"capabilities": ["cap:http.fetch", "cap:secrets.get", "cap:log.emit", "cap:ui.proxy"],
"ui": { "type": "iframe", "entry": "ui/dist/index.html" },
"assets": ["ui/dist/**/*"]
}
```
- Note: `api.endpoints` is optional advisory metadata for documentation/tooling. It is NOT required for the proxy pattern to work.
5. **Package the bundle**
**Using the CLI (recommended):**
```bash
alga pack --project .
# Creates dist/bundle.tar.zst with proper structure
```
**Manual packaging:**
- Layout your release directory:
```
dist/main.wasm
ui/dist/**/*
manifest.json
precompiled/ (optional cwasm artifacts)
```
- Create the canonical tarball (zstd recommended):
`tar --zstd -cf bundle.tar.zst manifest.json dist/main.wasm ui/dist`
- Compute the content hash: `shasum -a 256 bundle.tar.zst` → `sha256:...`
- Produce `SIGNATURE` (detached) with your publisher key per [security_signing.md](security_signing.md).
6. **Publish & install**
- Upload `bundle.tar.zst`, `manifest.json`, `SIGNATURE`, and metadata via your CI/registry flow.
- During tenant installation, operators provide install-scoped config and secrets that the gateway will pass to the runner.
With these steps, the Runner can fetch your component, serve the static UI, and execute the Wasmtime handler when `/api/ext/{extensionId}/...` is invoked.
## Manifest v2 (abridged)
See [Manifest Schema](manifest_schema.md) for full details.
```json
{
"name": "com.acme.my-extension",
"publisher": "Acme Inc.",
"version": "1.0.0",
"runtime": "wasm-js@1",
"capabilities": ["cap:http.fetch", "cap:storage.kv", "cap:secrets.get", "cap:ui.proxy"],
"ui": { "type": "iframe", "entry": "ui/index.html" },
"assets": ["ui/**/*"]
}
```
Key points:
- `ui.entry` points to your iframe HTML within the bundle.
- `cap:ui.proxy` enables the postMessage proxy pattern for UI→WASM handler communication.
- `capabilities` request access to host features (http, storage, secrets, ui proxy, logging) and are granted per tenant install.
- `api.endpoints` is optional advisory metadata for documentation/tooling—not required for the extension to function.
## Building Server Handlers (Componentized WASM)
- Write handlers in TypeScript (or JS) using `@alga-psa/extension-runtime`, then run `jco componentize` to produce `dist/main.wasm`.
- Optional: use other WIT-compatible languages (`wit-bindgen` for Rust/TinyGo) as long as the output conforms to the `alga:extension/runner` world.
- Use host APIs (capability-scoped) exposed via the generated bindings:
- `host.http.fetch`, `host.storage.*`, `host.secrets.get`, `host.uiProxy.callRoute`, `host.logging.*`.
- `host.invoicing.createManualInvoice` (requires `cap:invoice.manual.create`) for creating draft manual invoices.
Conceptual handler shape:
```ts
import { Handler, jsonResponse } from '@alga-psa/extension-runtime';
export const handler: Handler = async (request, host) => {
const items = await host.storage.list({
namespace: 'agreements',
limit: 50,
});
return jsonResponse(
{ data: items },
{ headers: { 'content-type': 'application/json' } }
);
};
```
Build example:
**Using the CLI:**
```bash
alga build --project .
# Automatically detects if WASM build is needed based on manifest
# Outputs: dist/main.wasm
```
**Using npm scripts:**
```bash
npm run build # compiles TS → JS
npm run build:component
# scripts should run:
# jco componentize dist/js/handler.js --wit ./wit/extension-runner.wit \
# --world-name runner --out dist/main.wasm
```
CI should output `dist/main.wasm` plus optional `precompiled/` artifacts referenced by the manifest.
## Building the Iframe UI
- Use the provided SDKs:
- `@alga/extension-iframe-sdk` for host communication (auth, theme, navigation, **proxy calls**)
- `@alga/ui-kit` for accessible, consistent components
- Iframe bootstrap and URL construction are handled by the host via:
- [buildExtUiSrc()](../../../server/src/lib/extensions/ui/iframeBridge.ts:38)
- [bootstrapIframe()](../../../server/src/lib/extensions/ui/iframeBridge.ts:45)
- UI assets are served by the Runner at `${RUNNER_PUBLIC_BASE}/ext-ui/{extensionId}/{content_hash}/[...]` with immutable caching
### Calling Your WASM Handler from the UI (postMessage Proxy Pattern)
**Important:** Extension UIs should NOT make direct `fetch()` calls to `/api/ext/` endpoints. Instead, use the **postMessage proxy pattern** which ensures:
- Authentication is handled by the host
- Secrets never reach the browser
- Consistent error handling and timeouts
Using the iframe SDK:
```ts
import { IframeBridge } from '@alga/extension-iframe-sdk';
const bridge = new IframeBridge();
await bridge.ready();
// Call your WASM handler via the proxy
const response = await bridge.uiProxy.call('/agreements');
const data = JSON.parse(new TextDecoder().decode(response));
```
Or using the lower-level postMessage API (see [client-portal-test sample](../../../ee/extensions/samples/client-portal-test/ui/main.js)):
```ts
// Send request to host
window.parent.postMessage({
alga: true,
version: '1',
type: 'apiproxy',
request_id: crypto.randomUUID(),
payload: { route: '/agreements' }
}, '*');
// Listen for response
window.addEventListener('message', (ev) => {
if (ev.data?.type === 'apiproxy_response') {
// Handle response...
}
});
```
The `cap:ui.proxy` capability is required in your manifest to use the proxy pattern.
## Signing and Packaging
- Produce a content-addressed bundle including:
- `manifest.json`
- WASM artifacts under `dist/`
- UI assets under `ui/`
- Optional precompiled artifacts (cwasm) under `precompiled/`
- Generate a SHA256 for the canonical bundle; sign with your publisher certificate; include `SIGNATURE`
- CI outputs an artifact ready for publish (e.g., `bundle.tar.zst`)
See [Security & Signing](security_signing.md).
## Publish and Install
- Publish the bundle and metadata to the Registry
- Admin installs a version for a tenant and grants capabilities
- The app server does not store or execute tenant code; bundles are verified and referenced via `content_hash`
## Calling Your Handlers (Gateway)
- Host exposes `/api/ext/[extensionId]/[[...path]]`.
- Gateway resolves install → version/content hash → config/providers/sealed secrets and calls Runner `POST /v1/execute` with that metadata.
- Header/time/size policies are enforced by the Gateway and Runner.
- Manifest endpoint lists are used for documentation/UX; strict enforcement is under active discussion (see [plan](../plans/2025-11-12-extension-system-alignment-plan.md)).
Reference route scaffold: [server/src/app/api/ext/[extensionId]/[[...path]]/route.ts](../../../server/src/app/api/ext/%5BextensionId%5D/%5B%5B...path%5D%5D/route.ts)
## Local Development Tips
For a detailed walkthrough of local development setup, build, install, and debugging workflows, see [Local Development Guide](local-development.md).
Quick reference:
- Use the pluggable runner backend to switch between Knative (`RUNNER_BACKEND=knative`) and the local Docker runner (`RUNNER_BACKEND=docker`).
- Start the runner container: `npm run runner:up` or `docker compose -f docker-compose.runner-dev.yml up` (stops with `npm run runner:down`).
- Launch the app with Docker backend defaults: `npm run dev:runner` (sets `RUNNER_DOCKER_HOST=http://localhost:${RUNNER_DOCKER_PORT:-8085}` and proxies UI assets through `/runner/*`).
- Install your extension locally: `node scripts/dev-install-extension.mjs ./path/to/extension`.
- Override runner URLs via `.env.runner` (see `.env.runner.example`) if you are using alternative S3 or registry endpoints.
- Keep UI bundles small; leverage code splitting where appropriate
- Use the iframe SDK's theme APIs to adapt to host styling
- Validate endpoint inputs/outputs and include clear error responses
## Best Practices
- Least privilege: request only necessary capabilities
- Input validation and resource bounds: avoid large responses; paginate/filter server-side
- Emit structured logs/metrics via Host API to support observability
- Prefer deterministic builds; maintain SBOMs; update dependencies proactively
## References
- [Local Development Guide](local-development.md) — Complete setup and workflow for developing locally with Docker Runner
- [Manifest Schema](manifest_schema.md)
- [API Routing Guide](api-routing-guide.md)
- [Security & Signing](security_signing.md)
- [Runner](runner.md)
- [Sample Extension](sample_template.md)
- Iframe bootstrap and src builder: [server/src/lib/extensions/ui/iframeBridge.ts](../../../server/src/lib/extensions/ui/iframeBridge.ts:38)