Guards Guide
Overview
Guards are the first line of defense in NitroStack's tool request pipeline. They act as the bouncer at the door, determining whether an incoming tool invocation should proceed or be denied immediately.
If a guard returns false (or throws an error), the tool execution pipeline halts—no middleware executes, no interceptors run, and your handler code is never invoked.
The Request Pipeline Order
Guards run before all other tool execution stages:
Client Request (tools/call)
│
▼
┌───────────────────┐ Returns false
│ Guards Check │────────────────────────► Rejection / Exception Filter
└─────────┬─────────┘
│ Returns true
▼
┌───────────────────┐
│ Middleware │ (Logging, Tracing, Headers)
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Interceptors │ (Around-invoke logic: before)
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Pipes │ (Input normalization & validation)
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Tool Handler │ (Business logic)
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Interceptors │ (Around-invoke logic: after)
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Exception Filters │ (Error transformation, isError: true)
└─────────┬─────────┘
│
▼
Client Response
The Guard Interface
Custom guards implement the Guard interface exported from @nitrostack/core:
import { ExecutionContext } from '@nitrostack/core';
export interface Guard {
canActivate(context: ExecutionContext): boolean | Promise<boolean>;
}
- Return
true: The request is authorized and continues through the pipeline. - Return
false: The request is denied immediately withError('Access denied by guard')and routed through any configured@UseFilters. - Throw an Exception: Rejects the request with an error, routed through any configured
@UseFilters.
Implementing a Guard
Here is a practical example of a write guard protecting mutations (such as adding or deleting books) in the BookShelf catalog:
// src/modules/library/guards/library-write.guard.ts
import { Guard, ExecutionContext, Injectable } from '@nitrostack/core';
@Injectable()
export class LibraryWriteGuard implements Guard {
async canActivate(context: ExecutionContext): Promise<boolean> {
const expectedToken = process.env.LIBRARY_ADMIN_TOKEN;
// Development convenience: if no token is configured, allow writes
if (!expectedToken) {
context.logger.warn('LIBRARY_ADMIN_TOKEN unset — allowing writes for local development');
return true;
}
// Inspect request metadata passed by client or gateway
const meta = context.metadata as Record<string, unknown> | undefined;
const providedToken = meta?.adminToken ?? meta?.libraryAdminToken;
if (typeof providedToken === 'string' && providedToken === expectedToken) {
// Attach identity details to context for downstream handlers
context.auth = {
authenticated: true,
subject: 'admin-user',
scopes: ['write', 'admin'],
claims: { role: 'admin' },
};
return true;
}
context.logger.warn('Write guard denied tool execution: missing or invalid admin token');
return false;
}
}
Applying Guards
Apply guards to tool methods using the @UseGuards() decorator:
Method-Level Application
Protect specific sensitive actions while leaving read operations public:
import { ToolDecorator as Tool, UseGuards, ExecutionContext, z } from '@nitrostack/core';
import { LibraryWriteGuard } from './guards/library-write.guard.js';
export class LibraryTools {
// Public tool: no guard required
@Tool({
name: 'list_books',
description: 'List all books in the catalog',
inputSchema: z.object({}),
})
async listBooks(_input: unknown, ctx: ExecutionContext) {
return { books: this.catalogService.findAll() };
}
// Protected tool: guarded by LibraryWriteGuard
@UseGuards(LibraryWriteGuard)
@Tool({
name: 'delete_book',
description: 'Permanently remove a book from the catalog',
inputSchema: z.object({ bookId: z.string() }),
})
async deleteBook(input: { bookId: string }, ctx: ExecutionContext) {
ctx.logger.info('Deleting book', { id: input.bookId, auth: ctx.auth });
return this.catalogService.delete(input.bookId);
}
}
Multiple guards can be chained:
@UseGuards(AuthGuard, ScopeGuard)
@Tool({ name: 'purge_catalog', ... })
async purgeCatalog(...) {}
Guards execute sequentially in the order passed to @UseGuards(). If any guard returns false, execution terminates immediately.
Authentication Guards
NitroStack supports authentication guards to protect sensitive operations:
| Guard | Origin | Description | Typical Use Case |
|---|---|---|---|
OAuthGuard | @nitrostack/core (Built-in) | Validates OAuth 2.1 access tokens via JWT verification or RFC 7662 token introspection. | User-facing apps, OpenAI Apps SDK, enterprise IdPs |
ApiKeyGuard | Application Guard / Starter Template | Validates API keys against environment variables (API_KEY_1, API_KEY_2) or headers (x-api-key). | Service-to-service, CI bots, internal microservices |
JwtGuard | Application Guard / Starter Template | Verifies signed JSON Web Tokens using shared secrets or public keys with issuer and audience validation. | Microservices, API gateways, first-party authentication |
Using OAuthGuard
OAuthGuard is exported directly by @nitrostack/core:
import { ToolDecorator as Tool, UseGuards, OAuthGuard, ExecutionContext, z } from '@nitrostack/core';
export class SecureTools {
@UseGuards(OAuthGuard)
@Tool({
name: 'sync_database',
description: 'Sync database records (requires OAuth 2.1 authentication)',
inputSchema: z.object({ target: z.string() }),
})
async syncDatabase(input: { target: string }, ctx: ExecutionContext) {
return { status: 'synced', user: ctx.auth?.subject };
}
}
Using Custom API Key / JWT Guards
For API Key or JWT authentication, implement the guard in your project's guards/ directory (or use templates scaffolded by nitrostack-cli init):
// src/modules/secure/secure.tools.ts
import { ToolDecorator as Tool, UseGuards, ExecutionContext, z } from '@nitrostack/core';
import { ApiKeyGuard } from '../../guards/apikey.guard.js';
export class AdminTools {
@UseGuards(ApiKeyGuard)
@Tool({
name: 'purge_catalog',
description: 'Purge catalog items',
inputSchema: z.object({ confirm: z.boolean() }),
})
async purgeCatalog(input: { confirm: boolean }, ctx: ExecutionContext) {
return { purged: true };
}
}
Best Practices
- Fail Closed: In production environments, deny requests by default if security configuration is missing.
- Keep Guards Fast: Guards run synchronously in the critical path before any handlers. Avoid expensive database queries; prefer cryptographic token verification or memory-cached lookups.
- Attach Identity Context: When authorization succeeds, populate
context.auth(e.g.subject,scopes,claims) so downstream tools know who made the request. - Targeted Protection: Leave public query tools (e.g. search, list, catalog) unguarded to reduce friction for AI models, while guarding state mutations.
Related Documentation
- Authentication Overview — In-depth guide to OAuth 2.1, JWT, and API Keys
- Middleware Guide — Cross-cutting request processing
- Interceptors Guide — Around-invoke response transforms