Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@ packages/**/src/*.d.ts
packages/**/src/*.d.ts.map

tsconfig.tsbuildinfo
*.tsbuildinfo
923 changes: 812 additions & 111 deletions README.md

Large diffs are not rendered by default.

61 changes: 50 additions & 11 deletions apps/gateway-server/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import express, { type RequestHandler } from "express";
import bodyParser from "body-parser";

import { runWithGatewayContext } from "@gatewaystack/request-context";
import { identifiabl } from "@gatewaystack/identifiabl";
import { transformabl } from "@gatewaystack/transformabl";
import {
protectedResourceRouter,
requireScope,
} from "@gatewaystack/validatabl";
import { limitabl } from "@gatewaystack/limitabl";
import { toolGatewayRouter } from "@gatewaystack/proxyabl";
import {
createProxyablRouter,
configFromEnv as proxyablConfigFromEnv,
} from "@gatewaystack/proxyabl";
import { explicablRouter } from "@gatewaystack/explicabl";

import { testEchoRoutes } from "./routes/testEcho";

import rateLimit from "express-rate-limit";

function trimTrailingSlashes(input: string): string {
let out = input;
while (out.endsWith("/")) {
out = out.slice(0, -1);
}
return out;
}

export function buildApp(env: NodeJS.ProcessEnv) {
// -----------------------------
// Boot & configuration
Expand Down Expand Up @@ -69,6 +81,21 @@ export function buildApp(env: NodeJS.ProcessEnv) {
const app = express();
app.use(bodyParser.json({ limit: "2mb" }));

// 🔹 NEW: create a GatewayContext for every incoming request
app.use((req, _res, next) => {
runWithGatewayContext(
{
request: {
method: req.method,
path: req.path,
ip: (req as any).ip,
userAgent: req.get("user-agent") ?? undefined,
},
},
() => next()
);
});

// Simple root health check (extra; explicabl has richer health)
app.get("/", (_req, res) => res.status(200).json({ ok: true }));

Expand Down Expand Up @@ -96,25 +123,17 @@ export function buildApp(env: NodeJS.ProcessEnv) {
// Layer 3: Mount validatabl’s PRM router early for public metadata
// validatabl is added as added via requireScope() in the protected area below
// -----------------------------
// app.use(
// protectedResourceRouter({
// issuer: OAUTH_ISSUER.replace(/\/+$/, ""),
// audience: OAUTH_AUDIENCE,
// scopes: OAUTH_SCOPES,
// }) as unknown as RequestHandler
// );

app.use(
"/prm",
prmLimiter,
protectedResourceRouter({
issuer: OAUTH_ISSUER.replace(/\/+$/, ""),
issuer: trimTrailingSlashes(OAUTH_ISSUER),
audience: OAUTH_AUDIENCE,
scopes: OAUTH_SCOPES,
}) as unknown as RequestHandler
);


// -----------------------------
// Layer 4: limitabl (per-identity rate limiting)
// -----------------------------
Expand Down Expand Up @@ -146,7 +165,27 @@ export function buildApp(env: NodeJS.ProcessEnv) {
// -----------------------------
// Layer 5: proxyabl (tool / MCP gateway)
// -----------------------------
app.use(toolGatewayRouter as unknown as RequestHandler);
const proxyablConfig = proxyablConfigFromEnv(env);

// Rate limiter for /tools (satisfies CodeQL, reasonable defaults)
const toolsRateLimiter = rateLimit({
windowMs,
max: limit * 10, // more generous than per-user limitabl
standardHeaders: true,
legacyHeaders: false,
});

// /tools pipeline:
// GatewayContext (already set globally)
// → toolsRateLimiter
// → identifiabl (JWT → ctx.identity)
// → proxyabl router (scopes + routing)
app.use(
"/tools",
toolsRateLimiter,
identifiablMiddleware,
Comment thread Fixed
createProxyablRouter(proxyablConfig) as unknown as RequestHandler
);

// -----------------------------
// Layer 6: explicabl (health, logs, webhooks)
Expand Down
7 changes: 4 additions & 3 deletions apps/gateway-server/src/routes/testEcho.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import express from "express";
import type { Request, Response, NextFunction } from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";
import rateLimit from "express-rate-limit";
import rateLimit, { ipKeyGenerator } from "express-rate-limit";

// Read allowlist from env the same way your handler does (or import your util if you have one)
function parseAllowlist(env: NodeJS.ProcessEnv): Record<string, string[]> {
Expand All @@ -18,9 +18,10 @@ function parseAllowlist(env: NodeJS.ProcessEnv): Record<string, string[]> {
function keyFromReq(req: any): string {
const xf = req.get?.("x-forwarded-for");
if (typeof xf === "string" && xf.length > 0) {
return xf.split(",")[0].trim();
const ip = xf.split(",")[0].trim();
return ipKeyGenerator(ip);
}
return req.ip || "unknown";
return ipKeyGenerator(req.ip);
}

function createTestEchoLimiter(env: NodeJS.ProcessEnv): express.RequestHandler {
Expand Down
154 changes: 154 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
## error handling

gatewaystack modules use a **fail-safe strategy** based on the security/availability tradeoff:

| module | on failure | reason |
|--------|-----------|--------|
| **identifiabl** | deny (fail closed) | no anonymous requests allowed |
| **transformabl** | continue (fail open) | allow untransformed content with warning |
| **validatabl** | deny (fail closed) | safety first — block when policy engine fails |
| **limitabl (pre-flight)** | continue (fail open) | favor availability over limit enforcement |
| **proxyabl** | fallback → error | try alternative providers, then return error |
| **limitabl (accounting)** | log error | accounting failure shouldn't block response delivery |
| **explicabl** | log error | audit failure logged but doesn't block request |

**circuit breakers**: modules that make external calls (jwks validation, policy stores, redis) implement circuit breakers with configurable thresholds.

**degraded mode**: when upstream dependencies fail, gatewaystack can operate in degraded mode with reduced governance guarantees (configurable per environment).

## inputs

- oidc tokens / apps sdk identity tokens
- user context (user_id, org_id, roles, scopes)
- model request (messages, params, attachments)
- configured policies
- routing rules
- quota & budget settings

## outputs

- authenticated, user-bound requests
- policy decisions (allow, deny, modify)
- routing decisions (provider/model/tool)
- enforced limits (rate, cost, budgets)
- full audit logs and runtime metadata

## shared requestcontext

all modules operate on a shared `RequestContext` object that flows through the pipeline. this context carries identity, content, metadata, and decisions from each module.

> 📘 **full type definitions and integration examples**: see [`docs/reference/interfaces.md`](https://github.com/davidcrowe/gatewaystack/blob/main/docs/reference/interfaces.md) in the repo

**core types** (simplified view):
```ts
// shared across modules
export type Identity = {
user_id: string;
org_id?: string;
tenant?: string;
roles: string[];
scopes: string[];
};

export type ContentMetadata = {
contains_pii?: boolean;
classification?: string[]; // ["sensitive", "financial"]
risk_score?: number; // 0–1
topics?: string[];
// extensible bag
[key: string]: unknown;
};

export type ModelRequest = {
model: string; // "gpt-4", "smart-model"
tools?: string[];
max_tokens?: number;
messages: any[];
attachments?: any[];
};

export type PolicyDecision = {
effect: 'allow' | 'deny' | 'modify';
reasons: string[];
modifications?: Partial<ModelRequest>;
};

export type LimitsDecision = {
effect: 'ok' | 'throttle' | 'deny' | 'fallback' | 'degrade';
reasons: string[];
constraints?: {
max_tokens?: number;
max_cost?: number;
};
};

export type RoutingDecision = {
provider: string; // "openai", "azure-openai"
model: string; // concrete provider model
region?: string;
alias?: string; // "smart-model"
};

export type UsageRecord = {
input_tokens: number;
output_tokens: number;
total_cost: number;
latency_ms: number;
};

export type RequestContext = {
request_id: string;
trace_id?: string;

identity?: Identity;
content?: {
messages: any[];
attachments?: any[];
};
metadata?: ContentMetadata;
modelRequest: ModelRequest;

policyDecision?: PolicyDecision;
limitsDecision?: LimitsDecision;
routingDecision?: RoutingDecision;
usage?: UsageRecord;

// arbitrary module extensions
[key: string]: unknown;
};
```

## module boundaries

**identifiabl**
- **input**: `RequestContext` with `modelRequest` + http auth header
- **reads**: authorization header, jwks endpoint
- **writes**: `identity`, `trace_id`, identity headers (`x-user-id`, `x-org-id`, etc.)

**transformabl**
- **input**: `RequestContext` with `identity`, `content`
- **reads**: `content` (messages, attachments)
- **writes**: `metadata` (pii flags, classification, risk score), redacted `content`, transformation logs

**validatabl**
- **input**: `identity`, `content`, `metadata`, `modelRequest`
- **reads**: all above + policy definitions
- **writes**: `policyDecision` (allow/deny/modify + reasons)

**limitabl** (two-phase)
- **pre-flight phase**:
- **reads**: `identity`, `modelRequest`, `policyDecision`
- **writes**: `limitsDecision` (constraints, budget availability)
- **accounting phase**:
- **reads**: provider response (tokens, cost)
- **writes**: `usage`, updates quota/budget stores, emits usage events

**proxyabl**
- **input**: `modelRequest`, `identity`, `metadata`, `policyDecision`, `limitsDecision`
- **reads**: routing rules, provider configurations, secrets
- **writes**: `routingDecision`, normalized provider response

**explicabl**
- **input**: full `RequestContext` + events from all modules
- **reads**: everything (for audit trail construction)
- **writes**: external logs and traces only (no modification of request context)
Empty file added docs/auth0/auth0-setup.md
Empty file.
2 changes: 1 addition & 1 deletion docs/conformance.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"allowlist": "pass",
"expiry": "pass"
},
"timestamp": "2025-11-26T21:38:28.617Z"
"timestamp": "2025-12-06T05:25:44.281Z"
}
Empty file added docs/idp-requirements.md
Empty file.
Loading