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
131 lines
3.1 KiB
TypeScript
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;
|
|
}
|