@cl4im/node
v0.1.1
Published
Node.js client for the Identora authorizer (OIDC + RBAC)
Maintainers
Readme
@cl4im/node
Node.js client for the Identora authorizer — handles app authentication, JWT validation, permission checking and automatic operation sync.
Installation
npm install @cl4im/node
# With framework-specific adapter
npm install @cl4im/node express # Express
npm install @cl4im/node fastify # Fastify
npm install @cl4im/node @nestjs/common # NestJS
npm install @cl4im/node @apollo/server # Apollo ServerQuick start
Express
import express from 'express';
import { Cl4imClient, getRegistry } from '@cl4im/node';
import { cl4im } from '@cl4im/node/express';
const client = new Cl4imClient({
authorizerUrl: process.env.AUTHORIZER_URL!,
realmId: process.env.REALM_ID!,
appId: process.env.APP_ID!,
secret: process.env.APP_SECRET!,
});
const { middleware, guard } = cl4im(client);
const app = express();
app.use(express.json(), middleware);
app.get('/health', guard('health:check', 'public'), (_req, res) => res.json({ status: 'ok' }));
app.get('/tasks', guard('tasks:list', 'private'), (_req, res) => res.json(tasks));
app.post('/tasks', guard('tasks:create', 'protected'), (req, res) => { /* req.cl4imUser */ });
for (const op of getRegistry()) client.registerOperation(op);
await client.startup();
app.listen(3001);Fastify
import Fastify from 'fastify';
import { Cl4imClient, getRegistry } from '@cl4im/node';
import { cl4imFastifyPlugin } from '@cl4im/node/fastify';
const client = new Cl4imClient({ ... });
const fastify = Fastify();
await fastify.register(cl4imFastifyPlugin, { client });
fastify.get('/health',
{ preHandler: fastify.cl4imGuard('health:check', 'public') },
async () => ({ status: 'ok' }),
);
fastify.get('/tasks',
{ preHandler: fastify.cl4imGuard('tasks:list', 'private') },
async (req) => { /* req.cl4imUser */ },
);
for (const op of getRegistry()) client.registerOperation(op);
await client.startup();
await fastify.listen({ port: 3002 });NestJS
// app.module.ts
import { Module } from '@nestjs/common';
import { Cl4imModule } from '@cl4im/node/nest';
@Module({
imports: [
Cl4imModule.forRootAsync({
authorizerUrl: process.env.AUTHORIZER_URL!,
realmId: process.env.REALM_ID!,
appId: process.env.APP_ID!,
secret: process.env.APP_SECRET!,
}),
],
})
export class AppModule {}// tasks.controller.ts
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { Cl4imGuard, Cl4imUser, Operation } from '@cl4im/node/nest';
import type { TokenClaims } from '@cl4im/node';
@Controller('tasks')
@UseGuards(Cl4imGuard)
export class TasksController {
@Get()
@Operation('tasks:list', 'private')
list() { return tasks; }
@Post()
@Operation('tasks:create', 'protected')
create(@Cl4imUser() user: TokenClaims) {
// user.sub, user.groups
}
}Apollo Server
import { ApolloServer } from '@apollo/server';
import { Cl4imClient, getRegistry } from '@cl4im/node';
import { cl4imPlugin, createGuard, type Cl4imContext } from '@cl4im/node/apollo';
const client = new Cl4imClient({ ... });
const guard = createGuard(client);
const resolvers = {
Query: {
health: guard('health:check', 'public', () => 'ok'),
tasks: guard('tasks:list', 'private', (_p, _a, ctx: Cl4imContext) => {
const user = ctx.cl4imUser; // TokenClaims
return tasks;
}),
},
Mutation: {
createTask: guard('tasks:create', 'protected', (_p, { input }, ctx: Cl4imContext) => {
// ctx.cl4imUser is the authenticated user
}),
},
};
for (const op of getRegistry()) client.registerOperation(op);
await client.startup();
const server = new ApolloServer<Cl4imContext>({
typeDefs,
resolvers,
plugins: [cl4imPlugin(client)],
});Operation levels
| Level | Who can access |
|-------------|----------------|
| public | Everyone — no token required |
| private | Any authenticated user (token with at least one group) |
| protected | Only users whose groups intersect the operation's allowed_groups |
Method auto-detection
The method is inferred from the last segment of the operation id:
| Operation id suffix | Detected method |
|---------------------|-----------------|
| list, get, fetch, read | read |
| delete, remove, destroy | delete |
| stream, subscribe, watch, listen | stream |
| anything else | write |
You can always pass the method explicitly as the third argument to guard().
Configuration
| Parameter | Description |
|----------------|-------------|
| authorizerUrl | Base URL of Identora, including the API prefix (e.g. http://host/api) |
| realmId | Name or UUID of the realm (e.g. purp) |
| appId | UUID of the app registered in auth.apps |
| secret | Plain-text app secret — use env vars, never commit |
How it works
On startup(), the client:
- Authenticates — exchanges
appId+secretfor a short-lived app JWT - Fetches JWKS — caches the public keys for token validation
- Syncs operations — pushes the registered operation catalogue to the authorizer
- Fetches operations — retrieves the authorised operation list with group bindings
On each request, the adapter:
- Validates the Bearer token using the cached JWKS
- Checks
TokenClaims.groupsagainst the operation'sallowedGroups - Injects
cl4imUser(TokenClaims) into the request context
License
CC0 1.0 Universal — public domain.
