Server Utilities¶
@sentinel-auth/js/server provides JWT verification, permission checks, and RBAC action checks for Node.js and Edge runtimes.
verifyToken¶
Verify a Sentinel JWT against a JWKS endpoint. Uses jose (Edge-compatible).
const payload = await verifyToken(token, {
jwksUrl: 'http://localhost:9003/.well-known/jwks.json',
})
const user = payloadToUser(payload)
// { userId, email, name, workspaceId, workspaceSlug, workspaceRole, groups }
| Option | Type | Default | Description |
|---|---|---|---|
jwksUrl |
string |
required | Sentinel JWKS endpoint |
audience |
string |
"sentinel:access" |
Expected aud claim |
issuer |
string |
-- | Expected iss claim |
JWKS keys are fetched and cached automatically.
PermissionClient¶
Zanzibar-style permission checks. Mirrors the Python SDK.
const permissions = new PermissionClient(
'http://localhost:9003', 'my-service', 'sk_my_service_key',
)
Which token?
The token argument on every PermissionClient and RoleClient method must be a Sentinel-signed token: the access token in proxy mode, or the authz token (the X-Authz-Token request header) in AuthZ mode. In AuthZ mode the Authorization header carries the IdP token — Sentinel can't decode it, so passing it fails every call with a 401.
can(token, resourceType, resourceId, action) -- single permission check.
check(token, checks) -- batch check.
const results = await permissions.check(token, [
{ service_name: 'my-service', resource_type: 'document', resource_id: docId, action: 'view' },
{ service_name: 'my-service', resource_type: 'document', resource_id: docId, action: 'edit' },
])
registerResource(request) -- register a resource (service key, no JWT needed).
await permissions.registerResource({
service_name: 'my-service', resource_type: 'document', resource_id: docId,
workspace_id: workspaceId, owner_id: userId, visibility: 'workspace',
})
share(token, resourceType, resourceId, share) -- grant access.
await permissions.share(token, 'document', docId, {
grantee_type: 'user', grantee_id: targetUserId, permission: 'edit',
})
accessible(token, resourceType, action, workspaceId, limit?) -- list accessible resource IDs.
const result = await permissions.accessible(token, 'document', 'view', workspaceId)
// { resource_ids: ['doc1', 'doc2'], has_full_access: false }
RoleClient¶
RBAC action checks. Mirrors the Python SDK.
registerActions(actions) -- register at startup (service key).
await roles.registerActions([
{ action: 'notes:create', description: 'Create notes' },
{ action: 'notes:export', description: 'Export notes' },
])
checkAction(token, action, workspaceId) -- check single action.
getUserActions(token, workspaceId) -- list permitted actions.
Express example¶
import express from 'express'
import { verifyToken, payloadToUser, PermissionClient } from '@sentinel-auth/js/server'
const app = express()
const permissions = new PermissionClient('http://localhost:9003', 'my-service', process.env.SERVICE_KEY)
async function authenticate(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) return res.status(401).json({ error: 'Unauthorized' })
try {
req.user = payloadToUser(await verifyToken(token, {
jwksUrl: 'http://localhost:9003/.well-known/jwks.json',
}))
req.token = token
next()
} catch {
res.status(401).json({ error: 'Invalid token' })
}
}
app.get('/api/documents/:id', authenticate, async (req, res) => {
const allowed = await permissions.can(req.token, 'document', req.params.id, 'view')
if (!allowed) return res.status(403).json({ error: 'Forbidden' })
res.json(await getDocument(req.params.id))
})
Realm m2m (server only)¶
For realm members, @sentinel-auth/js/server adds the no-user m2m primitives for Flow B. These are server-entry only — they hold the service key and must never reach a browser. (@sentinel-auth/react deliberately has no m2m surface.)
The examples below point at http://sentinel-internal:9010 — the unpublished internal listener, not the public :9003 URL browser flows use.
fetchWhoami¶
Self-discover this service's shared scope (standalone → effective_scope === service_name, realm: null).
const who = await fetchWhoami({ sentinelUrl: 'http://sentinel-internal:9010', serviceKey: process.env.SERVICE_KEY })
// { service_name, effective_scope, realm: { slug, name } | null }
M2mTokenClient (mint — sender)¶
Mints and caches m2m tokens for outbound system calls; re-mints only past ~80% of the TTL.
const m2m = new M2mTokenClient('http://sentinel-internal:9010', process.env.SERVICE_KEY)
const token = await m2m.getToken()
await fetch('http://app-b.internal/internal/reindex', {
headers: { Authorization: `Bearer ${token}` },
})
verifyM2mToken (accept — receiver)¶
Verifies an inbound m2m token and returns a SystemAuth. Throws on any failure (bad signature, wrong realm, wrong type, expired).
const sys = await verifyM2mToken(token, {
jwksUrl: 'http://sentinel-internal:9010/.well-known/jwks.json',
effectiveScope: 'acme-suite', // the token's svc must equal this
serviceName: 'reports', // optional — checked against aud_target when set
})
sys.caller // minting member (server-stamped)
sys.svc // realm slug
sys.actions // string[] — granted actions (["*"] = full realm trust)
sys.can('search:reindex') // true if actions includes "*" or the action
| Option | Type | Description |
|---|---|---|
jwksUrl |
string |
JWKS endpoint of the Sentinel that signs m2m tokens |
effectiveScope |
string |
this service's realm slug; the token's svc must equal it |
serviceName |
string? |
checked against the token's aud_target when set |
issuer |
string? |
expected iss claim |
Next.js apps get the same three helpers re-exported from @sentinel-auth/nextjs/server. See Realms for the trust model and API → Realms for the wire format.