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

131 lines
3.1 KiB
TypeScript

/**
* Shared gateway utilities for the extension proxy.
*/
export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
export interface ApiEndpointDef {
method: Method;
path: string;
handler: string;
}
export interface ManifestV2 {
api: {
endpoints: ApiEndpointDef[];
};
}
export function pathnameFromParts(parts: string[]): string {
return '/' + parts.filter(Boolean).join('/');
}
function trimAndSplit(path: string): string[] {
if (!path) return [];
const trimmed = path.trim();
if (trimmed === '' || trimmed === '/') return [];
return trimmed.replace(/^\/+|\/+$/g, '').split('/');
}
export function matchEndpoint(
endpoints: ApiEndpointDef[] | undefined,
method: Method,
pathname: string,
): { handler: string } | null {
if (!endpoints || endpoints.length === 0) return null;
const reqSegs = trimAndSplit(pathname);
for (const endpoint of endpoints) {
if (endpoint.method !== method) continue;
const patSegs = trimAndSplit(endpoint.path);
if (patSegs.length !== reqSegs.length) continue;
let matched = true;
for (let i = 0; i < patSegs.length; i += 1) {
const pat = patSegs[i];
const req = reqSegs[i];
if (pat.startsWith(':')) {
if (!req.length) {
matched = false;
break;
}
} else if (pat !== req) {
matched = false;
break;
}
}
if (matched) {
return { handler: endpoint.handler };
}
}
return null;
}
const REQUEST_HEADER_ALLOWLIST = new Set([
'accept',
'content-type',
'accept-encoding',
'user-agent',
'x-request-id',
'x-alga-tenant',
'x-alga-extension',
'x-idempotency-key',
]);
const RESPONSE_HEADER_ALLOWLIST = new Set([
'content-type',
'cache-control',
'x-ext-request-id',
'x-ext-warning',
]);
export function filterRequestHeaders(
reqHeaders: Headers,
tenantId: string,
extensionId: string,
requestId: string,
method: Method,
): Record<string, string> {
const out: Record<string, string> = {};
for (const [key, value] of reqHeaders.entries()) {
const lower = key.toLowerCase();
if (lower === 'authorization') continue;
if (REQUEST_HEADER_ALLOWLIST.has(lower)) {
out[lower] = value;
}
}
out['x-request-id'] = requestId;
out['x-alga-tenant'] = tenantId;
out['x-alga-extension'] = extensionId;
if (method !== 'GET') {
out['x-idempotency-key'] = out['x-idempotency-key'] ?? crypto.randomUUID();
}
return out;
}
export function filterResponseHeaders(
headers: Record<string, string | string[] | undefined> | undefined,
): Record<string, string> {
const result: Record<string, string> = {};
if (!headers) return result;
for (const [key, value] of Object.entries(headers)) {
const lower = key.toLowerCase();
if (!RESPONSE_HEADER_ALLOWLIST.has(lower)) continue;
if (Array.isArray(value)) {
result[lower] = value.join(', ');
} else if (typeof value === 'string') {
result[lower] = value;
}
}
return result;
}
export function getTimeoutMs(): number {
const raw = process.env.EXT_GATEWAY_TIMEOUT_MS;
const parsed = raw ? Number(raw) : 30000;
return Number.isFinite(parsed) && parsed > 0 ? parsed : 30000;
}