PSA/ee/docs/extension-system/api-routing-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

159 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Extension API Routing Guide (Gateway → Runner)
This guide specifies the v2-only extension API gateway pattern used to route tenant requests to out-of-process extension handlers executed by the Runner service.
Key points:
- Route pattern: `/api/ext/[extensionId]/[[...path]]`
- Resolve tenant install → `{version_id, content_hash, config, provider grants, sealed secret envelope}` (manifest endpoint matching is advisory today)
- Proxy to Runner `POST /v1/execute` with strict header and size/time policies
- 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)
## Route Structure
Next.js app route:
```
server/src/app/api/ext/[extensionId]/[[...path]]/route.ts
```
Supports methods: GET, POST, PUT, PATCH, DELETE.
The URL conveys:
- `extensionId`: the registry or install identifier for the extension
- `path`: the arbitrary path that should match an endpoint in the manifest
Example requests:
```
/api/ext/com.alga.softwareone/agreements
/api/ext/com.alga.softwareone/agreements/agr-001
/api/ext/com.alga.softwareone/agreements/sync?force=true
```
## Request Pipeline (per request)
1) Resolve tenant context
- Derive `tenant_id` from auth/session
- Verify RBAC: the user can access this extension/endpoint
2) Resolve install/version
- Look up the tenants install for `extensionId`
- Determine the active `version_id` and `content_hash`
3) Resolve endpoint from manifest (advisory)
- Load manifest for the resolved version (cacheable)
- Match `{method, pathname}` to a manifest endpoint (`api.endpoints`). Today this is used for docs/UX; hard enforcement is tracked in [Plan A4](../plans/2025-11-12-extension-system-alignment-plan.md#workstream-a-%E2%80%94-gateway--registry).
4) Build Runner Execute request
- Normalize HTTP input (method, path, query, allowed headers, body_b64)
- Add context `{request_id, tenant_id, extension_id, version_id, content_hash}` (install_id propagation pending A1)
- Attach install metadata `{config, providers, secret_envelope}` and set limits `{timeout_ms}` from `EXT_GATEWAY_TIMEOUT_MS`
5) Call Runner `/v1/execute`
- `POST ${RUNNER_BASE_URL}/v1/execute`
- Authenticate with a shortlived service token
6) Map Runner response → NextResponse
- Apply header allowlist; enforce size/time limits
- Return `{status, headers, body}` from Runner
## Example (abridged)
```ts
import { NextRequest, NextResponse } from 'next/server';
import { loadInstallConfigCached } from '@ee/lib/extensions/lib/install-config-cache';
export async function handle(
req: NextRequest,
{ params }: { params: { extensionId: string; path: string[] } },
) {
const method = req.method.toUpperCase();
const requestId = req.headers.get('x-request-id') || crypto.randomUUID();
const pathname = '/' + (params.path || []).join('/');
const url = new URL(req.url);
const tenantId = await getTenantFromAuth(req);
await assertAccess(tenantId, method);
const install = await loadInstallConfigCached(tenantId, params.extensionId);
if (!install?.contentHash) return NextResponse.json({ error: 'extension_not_installed' }, { status: 404 });
const bodyBuf = method === 'GET' ? undefined : Buffer.from(await req.arrayBuffer());
const execReq = {
context: {
request_id: requestId,
tenant_id: tenantId,
extension_id: params.extensionId,
version_id: install.versionId,
content_hash: install.contentHash,
// install_id TODO(A1)
config: install.config,
},
http: {
method,
path: pathname,
query: Object.fromEntries(url.searchParams.entries()),
headers: filterRequestHeaders(req.headers, tenantId, params.extensionId, requestId, method),
body_b64: bodyBuf ? bodyBuf.toString('base64') : undefined,
},
limits: { timeout_ms: Number(process.env.EXT_GATEWAY_TIMEOUT_MS ?? '5000') },
providers: install.providers,
...(install.secretEnvelope ? { secret_envelope: install.secretEnvelope } : {}),
};
const runnerResp = await fetch(`${process.env.RUNNER_BASE_URL}/v1/execute`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-request-id': requestId,
'x-alga-tenant': tenantId,
'x-alga-extension': params.extensionId,
...(install.configVersion ? { 'x-ext-config-version': install.configVersion } : {}),
...(install.secretsVersion ? { 'x-ext-secrets-version': install.secretsVersion } : {}),
},
body: JSON.stringify(execReq),
signal: AbortSignal.timeout(Number(process.env.EXT_GATEWAY_TIMEOUT_MS ?? '5000')),
});
const payload = await runnerResp.json();
return new NextResponse(payload.body_b64 ? Buffer.from(payload.body_b64, 'base64') : undefined, {
status: payload.status ?? runnerResp.status,
headers: filterResponseHeaders(runnerResp.headers),
});
}
export { handle as GET, handle as POST, handle as PUT, handle as PATCH, handle as DELETE };
```
## Header Policy
Forward (allowlist):
- `x-request-id`, `accept`, `content-type`, `accept-encoding`, `user-agent`
- `x-alga-tenant` (gatewaygenerated), `x-alga-extension` (gatewaygenerated)
- `x-idempotency-key` (gatewaygenerated for nonGET)
Strip:
- Enduser `authorization` header (gateway authenticates user and uses a service token to the Runner)
Response allowlist:
- `content-type`, `cache-control` (safe), custom `x-ext-*` headers
- Disallow `set-cookie` and hopbyhop headers
## Limits and Timeouts
- Request/response body size caps (e.g., 510 MB)
- Default timeout: `EXT_GATEWAY_TIMEOUT_MS` (5s default), with safe perendpoint overrides
- Limited header propagation and standardized error mapping (e.g., 404/413/502/504)
## Testing
- Unittest manifest endpoint resolution and RBAC guards
- Integrationtest endtoend proxy behavior and error mapping
- Inject fake Runner responses to validate header/body handling and timeouts
## Related References
- Gateway 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)
- Install config helpers: [@ee/lib/extensions/installConfig](../../server/src/lib/extensions/installConfig.ts)
- Runner execution API: `POST /v1/execute` (see Runner responsibilities in [runner.md](runner.md))
- Registry v2 integration for resolution: [ExtensionRegistryServiceV2](../../server/src/lib/extensions/registry-v2.ts:48)