Use when securing specific API endpoints in Bknd. Covers protecting custom HTTP triggers, plugin routes, auth middleware for Flows, checking permissions in custom endpoints, and role-based endpoint access.
npx claudepluginhub cameronapak/bknd-expert --plugin bknd-research-skillsThis skill uses the workspace's default tool permissions.
Secure specific API endpoints with authentication and authorization checks.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Secure specific API endpoints with authentication and authorization checks.
auth: { enabled: true })guard: { enabled: true })Note: Endpoint protection requires code mode. UI is read-only.
Bknd has several endpoint types to protect:
| Type | Path Pattern | How to Protect |
|---|---|---|
| Data API | /api/data/* | Guard permissions (automatic) |
| Auth API | /api/auth/* | Built-in protection |
| Media API | /api/media/* | Guard permissions (automatic) |
| HTTP Triggers | Custom paths | Manual auth check |
| Plugin Routes | Custom paths | Manual auth check |
Add authentication to a custom endpoint via FunctionTask:
import { serve } from "bknd/adapter/bun";
import { Flow, HttpTrigger, FunctionTask } from "bknd";
// Protected endpoint flow
const protectedFlow = new Flow("protected-endpoint", [
new FunctionTask({
name: "checkAuth",
handler: async (input, ctx) => {
// ctx.app gives access to modules
const authModule = ctx.app.modules.get("auth");
const user = await authModule.authenticator.getUserFromRequest(input);
if (!user) {
throw new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
// Pass user to next task
return { user, body: await input.json() };
},
}),
new FunctionTask({
name: "processRequest",
handler: async (input) => {
// input contains { user, body } from previous task
return {
message: `Hello ${input.user.email}`,
data: input.body,
};
},
}),
]);
protectedFlow.setTrigger(
new HttpTrigger({
path: "/api/custom/protected",
method: "POST",
respondWith: "processRequest",
})
);
serve({
connection: { url: "file:data.db" },
config: {
flows: {
flows: [protectedFlow],
},
},
});
Add auth check in plugin's onServerInit:
import { serve } from "bknd/adapter/bun";
import { createPlugin } from "bknd";
const protectedPlugin = createPlugin({
name: "protected-routes",
onServerInit: (server) => {
// Protected endpoint
server.post("/api/custom/data", async (c) => {
// Get app from context
const app = c.get("app");
const authModule = app.modules.get("auth");
// Resolve user from request
const user = await authModule.authenticator.getUserFromRequest(c.req.raw);
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
// Proceed with protected logic
const body = await c.req.json();
return c.json({
message: "Protected data",
user: user.email,
received: body,
});
});
// Public endpoint (no auth check)
server.get("/api/custom/public", (c) => {
return c.json({ message: "Public data" });
});
},
});
serve({
connection: { url: "file:data.db" },
plugins: [protectedPlugin],
});
Check user's role for specific permissions:
const roleProtectedPlugin = createPlugin({
name: "role-protected",
onServerInit: (server) => {
// Admin-only endpoint
server.delete("/api/admin/users/:id", async (c) => {
const app = c.get("app");
const authModule = app.modules.get("auth");
const user = await authModule.authenticator.getUserFromRequest(c.req.raw);
// Check authentication
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
// Check role
if (user.role !== "admin") {
return c.json({ error: "Forbidden: Admin role required" }, 403);
}
// Proceed with admin action
const userId = c.req.param("id");
// ... delete user logic
return c.json({ deleted: userId });
});
},
});
Use Guard for granular permission checks:
import { createPlugin, DataPermissions } from "bknd";
const guardProtectedPlugin = createPlugin({
name: "guard-protected",
onServerInit: (server) => {
server.post("/api/custom/sync", async (c) => {
const app = c.get("app");
const authModule = app.modules.get("auth");
const guard = authModule.guard;
const user = await authModule.authenticator.getUserFromRequest(c.req.raw);
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
// Check specific permission using Guard
try {
guard.granted(
DataPermissions.databaseSync, // Permission to check
{ role: user.role }, // User context
{} // Permission context
);
} catch (error) {
return c.json({
error: "Forbidden",
message: error.message,
}, 403);
}
// User has permission - proceed
return c.json({ status: "sync started" });
});
},
});
Check permissions for specific entity operations:
server.post("/api/custom/posts/batch", async (c) => {
const app = c.get("app");
const authModule = app.modules.get("auth");
const guard = authModule.guard;
const user = await authModule.authenticator.getUserFromRequest(c.req.raw);
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
// Check create permission for posts entity
try {
guard.granted(
DataPermissions.entityCreate,
{ role: user.role },
{ entity: "posts" } // Entity-specific context
);
} catch (error) {
return c.json({
error: "Cannot create posts",
message: error.message,
}, 403);
}
// Proceed with batch creation
const body = await c.req.json();
// ... create posts
return c.json({ created: body.length });
});
Create a helper for consistent auth checks:
// auth-middleware.ts
type AuthContext = {
user: any;
role: string;
};
export async function requireAuth(
c: any,
app: any
): Promise<AuthContext | Response> {
const authModule = app.modules.get("auth");
const user = await authModule.authenticator.getUserFromRequest(c.req.raw);
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
return { user, role: user.role };
}
export async function requireRole(
c: any,
app: any,
allowedRoles: string[]
): Promise<AuthContext | Response> {
const result = await requireAuth(c, app);
if (result instanceof Response) {
return result;
}
if (!allowedRoles.includes(result.role)) {
return c.json({
error: "Forbidden",
required: allowedRoles,
current: result.role,
}, 403);
}
return result;
}
// Usage in plugin
server.get("/api/reports/admin", async (c) => {
const app = c.get("app");
const auth = await requireRole(c, app, ["admin", "manager"]);
if (auth instanceof Response) return auth;
// auth.user available
return c.json({ reports: [] });
});
Create reusable auth task for Flows:
import { Flow, HttpTrigger, FunctionTask } from "bknd";
// Reusable auth task
const authTask = new FunctionTask({
name: "requireAuth",
handler: async (input, ctx) => {
const authModule = ctx.app.modules.get("auth");
const user = await authModule.authenticator.getUserFromRequest(input);
if (!user) {
throw new Response(
JSON.stringify({ error: "Unauthorized" }),
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
return { request: input, user };
},
});
// Reusable role check task
const requireAdmin = new FunctionTask({
name: "requireAdmin",
handler: async (input) => {
if (input.user.role !== "admin") {
throw new Response(
JSON.stringify({ error: "Admin required" }),
{ status: 403, headers: { "Content-Type": "application/json" } }
);
}
return input;
},
});
// Protected flow
const adminFlow = new Flow("admin-action", [
authTask,
requireAdmin,
new FunctionTask({
name: "performAction",
handler: async (input) => {
return { success: true, admin: input.user.email };
},
}),
]);
adminFlow.setTrigger(
new HttpTrigger({
path: "/api/admin/action",
method: "POST",
respondWith: "performAction",
})
);
server.get("/api/posts", async (c) => {
const app = c.get("app");
const authModule = app.modules.get("auth");
const api = app.getApi();
// Try to get user (may be null)
const user = await authModule.authenticator.getUserFromRequest(c.req.raw);
if (user) {
// Authenticated: show all posts including drafts
const posts = await api.data.readMany("posts", {
where: {
$or: [
{ status: "published" },
{ author_id: user.id },
],
},
});
return c.json(posts.data);
} else {
// Anonymous: show only published
const posts = await api.data.readMany("posts", {
where: { status: "published" },
});
return c.json(posts.data);
}
});
const rateLimits = new Map<string, { count: number; reset: number }>();
server.post("/api/expensive-operation", async (c) => {
const app = c.get("app");
const authModule = app.modules.get("auth");
const user = await authModule.authenticator.getUserFromRequest(c.req.raw);
if (!user) {
return c.json({ error: "Unauthorized" }, 401);
}
// Simple rate limiting by user
const key = `user:${user.id}`;
const now = Date.now();
const limit = rateLimits.get(key);
if (limit && limit.reset > now && limit.count >= 10) {
return c.json({
error: "Rate limit exceeded",
retryAfter: Math.ceil((limit.reset - now) / 1000),
}, 429);
}
// Update rate limit
if (!limit || limit.reset < now) {
rateLimits.set(key, { count: 1, reset: now + 60000 });
} else {
limit.count++;
}
// Proceed
return c.json({ result: "success" });
});
For service-to-service or external API access:
const API_KEYS = new Set([
process.env.SERVICE_API_KEY,
process.env.PARTNER_API_KEY,
]);
server.post("/api/webhook/external", async (c) => {
const apiKey = c.req.header("X-API-Key");
if (!apiKey || !API_KEYS.has(apiKey)) {
return c.json({ error: "Invalid API key" }, 401);
}
// Proceed with webhook handling
const body = await c.req.json();
return c.json({ received: true });
});
# Should return 401
curl -X POST http://localhost:7654/api/custom/protected \
-H "Content-Type: application/json" \
-d '{"test": "data"}'
# Login first
TOKEN=$(curl -s -X POST http://localhost:7654/api/auth/password/login \
-H "Content-Type: application/json" \
-d '{"email": "user@test.com", "password": "pass123"}' | jq -r '.token')
# Access protected endpoint
curl -X POST http://localhost:7654/api/custom/protected \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"test": "data"}'
# Login as non-admin
TOKEN=$(curl -s -X POST http://localhost:7654/api/auth/password/login \
-H "Content-Type: application/json" \
-d '{"email": "user@test.com", "password": "pass123"}' | jq -r '.token')
# Should return 403
curl -X DELETE http://localhost:7654/api/admin/users/1 \
-H "Authorization: Bearer $TOKEN"
# Login as admin
ADMIN_TOKEN=$(curl -s -X POST http://localhost:7654/api/auth/password/login \
-H "Content-Type: application/json" \
-d '{"email": "admin@test.com", "password": "admin123"}' | jq -r '.token')
# Should succeed
curl -X DELETE http://localhost:7654/api/admin/users/1 \
-H "Authorization: Bearer $ADMIN_TOKEN"
Problem: getUserFromRequest() returns null even with valid token
Fix: Ensure token is sent correctly:
// Header auth
fetch("/api/custom/protected", {
headers: { "Authorization": `Bearer ${token}` }
});
// OR cookie auth (if using cookies)
fetch("/api/custom/protected", {
credentials: "include" // Send cookies
});
Problem: authModule.guard is undefined
Fix: Ensure guard is enabled:
{
auth: {
enabled: true,
guard: { enabled: true }, // Required!
},
}
Problem: Guard throws unexpected error type
Fix: Catch specific exception:
import { GuardPermissionsException } from "bknd";
try {
guard.granted(permission, context, permContext);
} catch (error) {
if (error instanceof GuardPermissionsException) {
return c.json({ error: error.message }, 403);
}
throw error; // Re-throw unexpected errors
}
Problem: Preflight fails for Authorization header
Fix: Configure CORS:
serve({
// ...
config: {
server: {
cors: {
origin: ["http://localhost:3000"],
credentials: true,
allowHeaders: ["Authorization", "Content-Type"],
},
},
},
});
Problem: ctx.app undefined in FunctionTask
Fix: Access via execution context:
new FunctionTask({
name: "withApp",
handler: async (input, ctx) => {
// ctx.app is available in FunctionTask
const app = ctx.app;
// ...
},
});
DO:
DON'T: