@north-light/identity — verify directory-issued tokens and evaluate crouter scopes
@north-light/identity is the token contract shared by crouter's runtime, @north-light/sdk and @north-light/plugin. It does two things: it verifies the signed JWTs that a Northlight directory issues (access tokens, ID tokens, outbound provider tokens and grant-sync notices) against the directory's published keys, and it parses and compares scope strings such as crtr:llm or crtr:memory:read:user:work/. It is ESM-only, depends only on jose, and has no side effects on import.
It is a verifier, not an identity provider. It never issues, signs or stores tokens, and it does not run a login flow; the directory does that. Most applications do not import it directly: the SDK's OAuth helpers and the plugin kit's auth option call it for you. Use it when you write your own provider or service that receives these tokens.
Docs · Plugin authentication · Apps with many users · crouter repository
Install
npm install @north-light/identity
Verify a token
Every verifier takes { token, jwks, issuer, audience } and returns the typed claims or throws. jwks is either a static JWKS object or a JwksCache.
import { JwksCache, verifyCallToken } from '@north-light/identity';
const issuer = 'https://directory.example.com';
const jwks = new JwksCache(issuer, fetch, `${issuer}/.well-known/jwks.json`);
export async function authorize(request: Request) {
const token = request.headers.get('authorization')?.replace(/^Bearer /, '') ?? '';
const verified = await verifyCallToken({
token,
jwks,
issuer,
audience: 'https://provider.example.com/crtr', // exactly the URL the provider registered
});
return (verified.scope ?? '').split(' '); // the provider's `<provider>:<group>` strings the caller holds
}
JwksCache keeps the last good key set for one issuer. When a token names a kid it has not seen, it refetches, at most once every 30 seconds, and concurrent requests share that fetch. A failed refresh leaves the previous keys in place. If you omit the third argument, it discovers jwks_uri from <issuer>/.well-known/openid-configuration.
| Function | Checks | Returns |
|---|---|---|
verifyCallToken | typ: at+jwt, ES256 or RS256 with a kid, signature, iss, aud (a string equal to audience), exp, iat; sub is user:<id>; client_id non-empty; scope a string when present; the act chain (at most 8 links; each sub a runtime:, app: or user: principal; grant and rev only on an app: link, and grant needs an integer rev ≥ 1, else MissingRevClaimError). A token with no act is refused | CallToken (sub, aud, chain presenter first, scope, and the verified claims) |
verifyIdToken | ES256, signature, iss, aud, exp; sub is user:<id> | IdTokenClaims (email* when email was granted; name, given_name, family_name when profile was) |
verifySyncNotice | ES256 with a kid, purpose: grant_sync, lifetime of at most five minutes, 30 s clock tolerance | SyncNoticeClaims (jti, iat, exp) |
verifyCallToken is the one verifier for peer, app access, outbound provider and run-notice tokens; the older verifyAccessToken, verifyMachineToken and verifyOutboundToken are removed and tokens in their legacy shapes (no act) are refused. verifyCallToken authorizes nothing: the caller decides authority from the returned chain.
aud must match exactly; a trailing-slash difference is refused. The act claim of an outbound token is for audit only: nothing in this package authorizes on it.
Parse and compare scopes
import { parseScope, covers } from '@north-light/identity';
parseScope('crtr:files:rw:user:reports'); // returns the string, typed as Scope
parseScope('crtr:files:rw:user:../etc'); // throws TypeError: Unsupported scope
covers('crtr:memory:rw:user', 'crtr:memory:read:user:work'); // true: rw covers read, no path covers any path
covers('crtr:memory:read:user', 'crtr:memory:rw:user'); // false
covers('crtr:files:rw:user:.', 'crtr:files:read:user:a/b'); // true: `.` is the whole directory
The grammar parseScope accepts:
| Scope | Meaning |
|---|---|
crtr:llm, crtr:act, crtr:schedule | Model calls and starting runs; acting on nodes, files and bash; scheduling crons |
crtr:memory:read:user[:path], crtr:memory:rw:user[:path], crtr:memory:manage:user | The person's documents, optionally limited to a path; manage covers both access levels |
crtr:memory:read_private:user | Read-only access to public and private documents; no path; covered by manage, covers only itself |
crtr:memory:read|rw:app:<id>[:profile:<profile>[:path]] | An app's documents |
crtr:files:read|rw:user:<dir>, crtr:files:read|rw:app:<id>:<dir>, crtr:files:share | Host files a run may mount, and sharing them |
crtr:conversations:read:app:<id>, crtr:objects:read:app:<id> | Reading another app's conversations or node objects |
crtr:peer:<id> | A peer grant |
<provider>:<group> | A capability provider's own scope, such as the ones in an outbound token's scope |
Paths are relative and refuse .., :, backslashes, whitespace and empty segments. A memory path ending in / covers that directory and everything below it; a file path names a directory and covers it and everything below, with . meaning the whole root. Passing grantee as the second argument to parseScope also refuses an app: holder other than the grantee. ENFORCED is the set of scopes crtr sys connect --scopes accepts when it mints a token. What each scope allows is in Scopes and trust.
Related
@north-light/sdk: uses this package for ID-token verification and scope validation in its OAuth helpers.@north-light/plugin: verifies the directory's outbound tokens for providers registered in the directory.- Main repository: the
crtrCLI, thecrtrddaemon,.
License
Proprietary. Copyright (c) 2026 Northlight Systems. All rights reserved. See LICENSE.