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

6.3 KiB
Raw Permalink Blame History

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

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
  1. Resolve install/version
  • Look up the tenants install for extensionId
  • Determine the active version_id and content_hash
  1. 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.
  1. 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
  1. Call Runner /v1/execute
  • POST ${RUNNER_BASE_URL}/v1/execute
  • Authenticate with a shortlived service token
  1. Map Runner response → NextResponse
  • Apply header allowlist; enforce size/time limits
  • Return {status, headers, body} from Runner

Example (abridged)

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