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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
343 lines
14 KiB
Markdown
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)
|