Roles y permisos sobre @core/auth: comodines, permisos sobre lo propio y concesiones sin escalada
Instalar
genpm add @core/rbacQué obtienes
- Código en src/lib/rbac/, 10 archivos. (20,1 kB)
- Reglas de IA en src/lib/rbac/AGENTS.md, más archivos de reglas para tu IDE.
- Resuelve @core/auth, @core/contracts, @core/db por ti.
README
Este paquete no tiene README.
Esto es exactamente lo que lee tu IA cuando trabaja en src/lib/rbac. No se añade nada más a su contexto.
@core/rbac — rules for AI agents
Purpose
Authorization on top of @core/auth users: roles stored in the database, permissions resource:action with wildcards
(posts:*, *) and own-only grants (posts:update:own), and a role hierarchy (rank) that prevents privilege
escalation. Default roles: owner, admin, editor, support, author. Not authentication, not multi-tenancy.
Map
index.ts— public API:can,assertCan,getGrants,grantRole,revokeRole,bootstrapOwner,defineRole,ensureDefaultRoles.permissions.ts— pure matchergrants()(no DB).defaults.ts— default roles and their permissions.schema.ts— tablesroles,user_roles. Depends on../dband../auth.adapters/hono.ts—requirePermission(perm)middleware.adapters/next.ts—requirePermission(user, perm),permissionResponse(user, perm).admin.ts—usersAdminResourcefor @core/admin (users with roles; grant/revoke actions withusers:manage).
Integration
- Generate and apply migrations (see
src/lib/db/AGENTS.md). - At startup or in a setup script:
await ensureDefaultRoles(). - Make the first user owner once, e.g. in the
onLoginhook of @core/auth:onLogin: async ({ user, isNewUser }) => { if (isNewUser) await bootstrapOwner(user.id); }(only the first call wins). - Hono:
app.use(sessionMiddleware); app.post('/api/posts/:id/publish', requirePermission('posts:publish'), handler). Next.js:const user = await getUser(await cookies()); await requirePermission(user, 'posts:publish'); - Own-only checks:
await assertCan(user.id, 'posts:update', { ownerId: post.authorId }). - Verify: a user without roles gets 403; the owner gets 200.
Conventions
- Permission names: lowercase
resource:action(orders:refund). Actions in use: read, create, update, delete, publish, manage. - Check permissions on the server for every write and every private read; hiding UI is not authorization.
- Load
getGrants(userId)once per request and pass it tocan()when checking several permissions. - Change roles only through
grantRole/revokeRole(they enforce the hierarchy and keep at least one owner).
Don't
- Don't insert into
user_rolesdirectly, and don't add an env flag or "god mode" that bypassescan(). - Don't expose
defineRoleto users without checkingusers:manageand that the new rank is below the actor's. - Don't trust a role or permission sent by the client.
# @core/rbac — rules for AI agents
## Purpose
Authorization on top of @core/auth users: roles stored in the database, permissions `resource:action` with wildcards
(`posts:*`, `*`) and own-only grants (`posts:update:own`), and a role hierarchy (`rank`) that prevents privilege
escalation. Default roles: owner, admin, editor, support, author. Not authentication, not multi-tenancy.
## Map
- `index.ts` — public API: `can`, `assertCan`, `getGrants`, `grantRole`, `revokeRole`, `bootstrapOwner`, `defineRole`, `ensureDefaultRoles`.
- `permissions.ts` — pure matcher `grants()` (no DB).
- `defaults.ts` — default roles and their permissions.
- `schema.ts` — tables `roles`, `user_roles`. Depends on `../db` and `../auth`.
- `adapters/hono.ts` — `requirePermission(perm)` middleware. `adapters/next.ts` — `requirePermission(user, perm)`, `permissionResponse(user, perm)`.
- `admin.ts` — `usersAdminResource` for @core/admin (users with roles; grant/revoke actions with `users:manage`).
## Integration
1. Generate and apply migrations (see `src/lib/db/AGENTS.md`).
2. At startup or in a setup script: `await ensureDefaultRoles()`.
3. Make the first user owner once, e.g. in the `onLogin` hook of @core/auth:
`onLogin: async ({ user, isNewUser }) => { if (isNewUser) await bootstrapOwner(user.id); }` (only the first call wins).
4. Hono: `app.use(sessionMiddleware); app.post('/api/posts/:id/publish', requirePermission('posts:publish'), handler)`.
Next.js: `const user = await getUser(await cookies()); await requirePermission(user, 'posts:publish');`
5. Own-only checks: `await assertCan(user.id, 'posts:update', { ownerId: post.authorId })`.
6. Verify: a user without roles gets 403; the owner gets 200.
## Conventions
- Permission names: lowercase `resource:action` (`orders:refund`). Actions in use: read, create, update, delete, publish, manage.
- Check permissions on the server for every write and every private read; hiding UI is not authorization.
- Load `getGrants(userId)` once per request and pass it to `can()` when checking several permissions.
- Change roles only through `grantRole`/`revokeRole` (they enforce the hierarchy and keep at least one owner).
## Don't
- Don't insert into `user_roles` directly, and don't add an env flag or "god mode" that bypasses `can()`.
- Don't expose `defineRole` to users without checking `users:manage` and that the new rank is below the actor's.
- Don't trust a role or permission sent by the client.
El árbol exacto que se inyectará, tras aplicar .genpmignore. Anclado a
// Recurso "Usuarios" para @core/admin: lista de usuarios con sus roles y acciones para asignar o retirar roles.
// Las reglas de escalada (rango, último owner) son las de grantRole/revokeRole: el panel no las duplica.
import { asc, count, eq, ilike, inArray, or } from 'drizzle-orm';
import { z } from 'zod';
import { users } from '../auth/schema.ts';
import type { AdminContext, AdminResource } from '../contracts/index.ts';
import { getDb } from '../db/index.ts';
import { grantRole, RbacError, revokeRole } from './rbac.ts';
import { roles, userRoles } from './schema.ts';
export type UserAdminRow = { id: string; email: string | null; name: string | null; roles: string[]; createdAt: Date };
async function need(ctx: AdminContext, permission: string) {
if (!(await ctx.can(permission))) throw new RbacError('forbidden', `forbidden: ${permission}`);
}
async function withRoles(rows: Array<{ id: string; email: string | null; name: string | null; createdAt: Date }>): Promise<UserAdminRow[]> {
if (!rows.length) return [];
const links = await getDb()
.select({ userId: userRoles.userId, key: roles.key })
.from(userRoles)
.innerJoin(roles, eq(userRoles.roleId, roles.id))
.where(
inArray(
userRoles.userId,
rows.map((r) => r.id),
),
);
return rows.map((r) => ({ ...r, roles: links.filter((l) => l.userId === r.id).map((l) => l.key).sort() }));
}
const RoleInput = z.object({ role: z.string().min(1).max(40) });
const pick = { id: users.id, email: users.email, name: users.name, createdAt: users.createdAt };
export const usersAdminResource: AdminResource<UserAdminRow> = {
name: 'users',
label: { singular: 'User', plural: 'Users' },
group: 'Settings',
fields: [
{ name: 'email', label: 'Email', type: 'email', readOnly: true, list: true },
{ name: 'name', label: 'Name', type: 'text', readOnly: true, list: true },
{ name: 'roles', label: 'Roles', type: 'text', readOnly: true, list: true, help: 'Users appear here after their first sign-in.' },
{ name: 'createdAt', label: 'Joined', type: 'datetime', readOnly: true },
],
input: z.never(),
title: (u) => u.email ?? u.name ?? u.id,
async list(q, ctx) {
await need(ctx, 'users:read');
const like = q.search ? `%${q.search.replace(/[%_\\]/g, (m) => `\\${m}`)}%` : null;
const where = like ? or(ilike(users.email, like), ilike(users.name, like)) : undefined;
const [total] = await getDb().select({ n: count() }).from(users).where(where);
const rows = await getDb()
.select(pick)
.from(users)
.where(where)
.orderBy(asc(users.createdAt), asc(users.id))
.limit(q.pageSize)
.offset((Math.max(q.page, 1) - 1) * q.pageSize);
return { rows: await withRoles(rows), total: total?.n ?? 0 };
},
async get(id, ctx) {
await need(ctx, 'users:read');
const rows = await getDb().select(pick).from(users).where(eq(users.id, id));
return (await withRoles(rows))[0] ?? null;
},
actions: [
{
name: 'grant-role',
label: 'Grant role',
permission: 'users:manage',
input: RoleInput,
async run(id, input, ctx) {
await grantRole(ctx.user.id, id, RoleInput.parse(input).role);
return (await usersAdminResource.get(id, ctx))!;
},
},
{
name: 'revoke-role',
label: 'Revoke role',
permission: 'users:manage',
confirm: true,
input: RoleInput,
async run(id, input, ctx) {
await revokeRole(ctx.user.id, id, RoleInput.parse(input).role);
return (await usersAdminResource.get(id, ctx))!;
},
},
],
};
Este paquete no declara servidores MCP.
| Versión | Commit | Publicado | Escaneo |
|---|---|---|---|
| 1.0.1 | decbadd | hace 7 horas | escaneo superado |
- npm
- ninguno
- propuesta
- GenPM propone el comando npm y solo lo ejecuta si dices que sí.
- Usado por (1)
- @core/admin ^1.0.0
- escaneo
- escaneo superado · 0 hallazgos
- commit
- v1.0.1 → decbadd748476defda5138e5dc02cd5b7086463b · verificado tras la descarga
- scripts
- Ninguno. GenPM nunca ejecuta código del paquete.
- licencia
- MIT
- Calidad
- 100/100
- Licencia reconocidacumplido
- AGENTS.md explica su propósitocumplido
- AGENTS.md tiene pasos de integracióncumplido
- AGENTS.md lista convenciones o prohibicionescumplido
- Incluye testscumplido
- Escaneo de seguridad superadocumplido
- Publicado en los últimos 6 mesescumplido
- Publicador verificadocumplido
- Resumen y palabras clavecumplido
- reporte
- ¿Ves algo raro?