Routing & Security

Security Guards (XyGuard)

XyGuard is the high-performance declarative guard registry of XyPriss. It enables centralized, standardized security enforcement for routes and route groups with native TypeScript type-safety.

Architecture and Core Concepts

Unlike traditional middleware, guards rely on a static global registry (XyGuard) and a standardized return-type protocol. They run before handler initialization and are fully inspectable in the route registry.

XyGuard (static registry)

resolvers: Map<string, GuardResolver>

createGuardMiddleware()

helpers.ts
guards: RouteGuard[] (Array syntax)
Executes inline guard functions sequentially
guards: BuiltInGuards (Object syntax)
Invokes XyGuard.get(key) resolvers with parameters

Route Execution Handler

registry.ts
Executes the main handler ONLY if all guard checks return true.

Guard Signatures

XyGuard resolvers support two distinct signature patterns based on whether they accept configuration options.

1. Direct / Boolean Guard Signature

Used for simple guards (e.g. authenticated: true or inline functions). Receives (req, res) or (req, ctx).

typescript
import { XyPrisRequest, XyPrisResponse } from "xypriss";

export const authGuard = async (req: XyPrisRequest, res: XyPrisResponse) => {
    if (!req.session?.userId) {
        return "Unauthorized: Bearer token or session missing";
    }
    return true;
};

2. Configurable Guard Signature (with Options)

Used when parameters are passed (e.g. roles: ["admin"]). Receives (req, options, ctx).

typescript
import { XyPrisRequest, XyGuardContext } from "xypriss";

export const rolesGuard = async (
    req: XyPrisRequest,
    requiredRoles: string[],
    ctx: XyGuardContext
) => {
    const userRole = req.user?.role;
    if (!userRole || !requiredRoles.includes(userRole)) {
        return "Forbidden: Insufficient roles";
    }
    return true;
};

XyGuard.define() — Registering Resolvers

Register your guard logic globally using XyGuard.define() during application startup before defining routes.

typescript
import { XyGuard, XyPrisRequest, XyPrisResponse, XyGuardContext } from "xypriss";

// 1. Authentication guard
XyGuard.define("authenticated", async (req: XyPrisRequest, res: XyPrisResponse) => {
    const token = req.headers["authorization"]?.split(" ")[1];
    if (!token) return "Missing authorization header";
    try {
        (req as any).user = await verifyJwt(token);
        return true;
    } catch {
        return "Invalid or expired token";
    }
});

// 2. Role-based guard (receives options parameter)
XyGuard.define("roles", (req: XyPrisRequest, requiredRoles: string[], ctx: XyGuardContext) => {
    const userRole = (req as any).user?.role;
    if (!userRole || !requiredRoles.includes(userRole)) {
        return `Role '${userRole}' is insufficient. Required: ${requiredRoles.join(", ")}`;
    }
    return true;
});

// 3. Permission guard
XyGuard.define("permissions", (req: XyPrisRequest, required: string[]) => {
    const userPerms = (req as any).user?.permissions || [];
    const missing = required.filter((p) => !userPerms.includes(p));
    return missing.length === 0 ? true : `Missing permissions: ${missing.join(", ")}`;
});

// 4. Custom guard — IP Whitelist
XyGuard.define("ipWhitelist", (req: XyPrisRequest) => {
    const allowedIps = ["127.0.0.1", "192.168.1.100"];
    return allowedIps.includes(req.ip ?? "") ? true : `IP ${req.ip} is not allowed`;
});

Applying Guards

Declarative Object Syntax (Recommended)

Pass guard configurations in the guards object of route options.

typescript
import { router } from "./router";

// Single guard
router.get("/profile", { guards: { authenticated: true } }, async (req, res) => {
    res.json({ user: (req as any).user });
});

// Multi-guard combination
router.post(
    "/admin/users",
    {
        guards: {
            authenticated: true,
            roles: ["admin", "super-admin"],
            ipWhitelist: true,
        },
    },
    async (req, res) => {
        res.status(201).json({ message: "User created successfully" });
    },
);

Inline Array Syntax

Alternatively, pass an array of inline guard functions:

typescript
router.get("/dashboard", { guards: [authGuard, checkSubscriptionGuard] }, (req, res) => {
    res.json({ status: "active" });
});

Group-Level Guards & Inheritance

Guards applied to a route group via router.group() automatically apply to all child routes inside that group. Guards cascade in hierarchical order:

Router LevelGroup LevelRoute Level
typescript
router.group(
    {
        prefix: "/admin",
        guards: {
            authenticated: true,
            roles: ["admin"],
        },
    },
    (adminRouter) => {
        // Inherits 'authenticated' and 'roles: ["admin"]'
        adminRouter.get("/dashboard", dashboardHandler);

        // Inherits parent group guards AND adds 'permissions: ["reports:export"]'
        adminRouter.get(
            "/reports/export",
            { guards: { permissions: ["reports:export"] } },
            exportHandler,
        );
    },
);
Strict Inheritance Policy
Child routes cannot bypass or disable parent group guards. Security policies are strictly enforced top-down.

Return Protocol & HTTP Behavior

The table below details how return values from resolvers translate directly into HTTP responses:

Return ValueGuard Type / ContextHTTP StatusJSON Response Body
true / voidAllAccess GrantedRequest continues to handler
falseauthenticated401 Unauthorized{ success: false, error: "Unauthorized: Authentication required" }
falseroles403 Forbidden{ success: false, error: "Forbidden: Insufficient roles" }
falsepermissions / Custom / Array403 Forbidden{ success: false, error: "Forbidden: Access denied" }
"custom error"authenticated / Custom / Array401 Unauthorized{ success: false, error: "custom error" }
"custom error"roles / permissions403 Forbidden{ success: false, error: "custom error" }
throw ErrorAll500 Server Error{ success: false, error: "Internal Server Error during guard check" }
Recommendation
Prefer returning a custom error string over false to provide clear, actionable feedback to API clients.

v1 → v2 Migration (Removal of `custom`)

In XyPriss v2, the legacy custom property inside the guards object has been removed and is now typed as never.

Legacy Syntax (v1 — Deprecated / Removed)

typescript
// NO LONGER WORKS IN V2
router.get("/data", {
    guards: {
        authenticated: true,
        custom: [myInlineGuard], // Type error: 'custom' is typed as 'never'
    },
}, handler);

V2 Recommended Migration Options

typescript
// Option A: Use array for inline guards
router.get("/data", { guards: [myInlineGuard] }, handler);

// Option B: Register named guard via XyGuard.define
XyGuard.define("myGuard", myInlineGuard);
router.get("/data", {
    guards: { authenticated: true, myGuard: true },
}, handler);

TypeScript Auto-completion via Declaration Merging

Augment the CustomGuards interface in your project to enable native IDE autocompletion for custom guard keys:

typescript
// src/types/xypriss.d.ts
declare module "xypriss" {
    interface CustomGuards {
        ipWhitelist?: boolean;
        plan?: "free" | "starter" | "premium" | "enterprise";
        apiKey?: boolean;
    }
}

// IDE now validates custom keys and values:
router.get(
    "/premium-feature",
    {
        guards: {
            authenticated: true,
            plan: "premium", // Auto-completed & type-checked!
        },
    },
    handler,
);

Advanced Practical Patterns

1. Async Database Verification Guard

typescript
XyGuard.define("subscriptionActive", async (req) => {
    const user = (req as any).user;
    if (!user) return false;

    const sub = await db.subscriptions.findOne({
        userId: user.id,
        status: "active",
    });

    if (!sub) return "Subscription expired or invalid";
    (req as any).subscription = sub;
    return true;
});

2. Daily User Quota Guard

typescript
XyGuard.define("dailyExportQuota", async (req) => {
    const user = (req as any).user;
    const key = `quota:${user.id}:${new Date().toISOString().slice(0, 10)}`;
    
    const count = await redis.incr(key);
    const limit = user.plan === "premium" ? 1000 : 50;

    if (count > limit) {
        return `Daily quota reached (${limit} exports/day)`;
    }
    return true;
});

3. Feature Flag / Beta Guard

typescript
XyGuard.define("betaTesterOnly", (req) => {
    const user = (req as any).user;
    if (user?.isBetaTester) return true;
    return "This feature is restricted to beta testers";
});

4. Pre-Handler Request Validation

typescript
XyGuard.define("requireJsonBody", (req) => {
    const ct = req.headers["content-type"] ?? "";
    if (!ct.includes("application/json")) {
        return "Content-Type: application/json is required";
    }
    if (!req.body) return "Request body cannot be empty";
    return true;
});

Guards vs Middleware

MiddlewareGuards (XyGuard)
DeclarationImperative (app.use)Declarative (inline or via XyGuard.define)
Inspection VisibilityNoYes (visible in route inspection)
Return ProtocolManually call next()Standardized (true / false / "string")
Execution TimingDuring request processing chainBefore main handler initializes
Advanced Route Features

Explore Go-native rate limiting, multi-window XTRS shields, response caching, and lifecycle hooks.