/** * Authentication for the agent server: JSON Web Tokens, API keys, sessions, and a trusted proxy. * * Each builds the `authenticate` hook of `createAgentServer()`, and each returns the same * `Principal`: a subject, a tenant, roles, and scopes. That principal is what scopes threads, runs, * budgets, and the store to a tenant, and what reaches every graph node, tool, and middleware of the * run. The application issues its own tokens with `createJwtSigner()`, keeps its own keys with * `createApiKeyStore()`, and signs browsers in with `createSessions()`. Everything is built on Web * Crypto, so there is no dependency and it runs wherever `fetch` does. */ import type { webcrypto } from 'node:crypto'; import type { Principal } from '../types/principal.js'; import type { ServerStateStore } from '../types/server.js'; type JsonWebKey = webcrypto.JsonWebKey; type CryptoKey = webcrypto.CryptoKey; /** The `authenticate` hook of `createAgentServer()`. */ export type AuthHook = (request: Request) => Promise | Principal | Response | undefined; /** The signature algorithms a token may use. `none` is never accepted. */ export type JwtAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'RS256' | 'RS384' | 'RS512' | 'PS256' | 'PS384' | 'PS512' | 'ES256' | 'ES384' | 'ES512' | 'EdDSA'; /** A token's claims, the registered ones typed. */ export interface JwtPayload { /** The issuer. */ iss?: string; /** The subject: who the token is about. */ sub?: string; /** The audience: who the token is for. */ aud?: string | string[]; /** Expiry, in seconds since the epoch. */ exp?: number; /** Not valid before, in seconds since the epoch. */ nbf?: number; /** Issued at, in seconds since the epoch. */ iat?: number; /** The token's id. */ jti?: string; /** Every other claim. */ [claim: string]: unknown; } /** A token's header. */ export interface JwtHeader { /** The signature algorithm. */ alg: string; /** The id of the key that signed it, for choosing one from a key set. */ kid?: string; /** The token type, usually `JWT`. */ typ?: string; /** Any other header field. */ [field: string]: unknown; } /** A verified token. */ export interface VerifiedJwt { /** Its header. */ header: JwtHeader; /** Its claims, each checked. */ payload: JwtPayload; } /** Why a token was refused. */ export type JwtErrorCode = 'JWT_MALFORMED' | 'JWT_ALGORITHM' | 'JWT_KEY' | 'JWT_SIGNATURE' | 'JWT_EXPIRED' | 'JWT_NOT_YET_VALID' | 'JWT_TOO_OLD' | 'JWT_ISSUER' | 'JWT_AUDIENCE' | 'JWT_CLAIM'; /** Thrown when a token is refused: malformed, signed wrongly, expired, or for someone else. */ export declare class JwtError extends Error { /** Why. */ readonly code: JwtErrorCode; constructor( /** Why. */ code: JwtErrorCode, message: string); } /** Options for `createJwtVerifier()`. Give it one key source: `secret`, `publicKey`, or `jwks`. */ export interface JwtVerifyOptions { /** A shared secret, for HS256, HS384, and HS512. */ secret?: string | Uint8Array; /** One public key: PEM (`-----BEGIN PUBLIC KEY-----`), a JWK, or a `CryptoKey`. */ publicKey?: string | JsonWebKey | CryptoKey; /** * The identity provider's key set: its URL, or the keys themselves. Fetched keys are cached, and * a token naming a key not yet seen fetches the set again, so a provider's key rotation needs no * restart. */ jwks?: string | URL | { keys: JsonWebKey[]; }; /** * The algorithms accepted. Defaults to HS256, HS384, and HS512 for a secret and to every * asymmetric algorithm for a public key or a key set. A shared secret is never accepted for an * asymmetric algorithm, nor a public key for HMAC, so one cannot be passed off as the other. */ algorithms?: readonly JwtAlgorithm[]; /** The issuer a token must name: one, or any of several. */ issuer?: string | readonly string[]; /** * The audience a token must be for: one, or any of several. Required with `jwks`, where a * provider signs tokens for every application it serves; `false` accepts any audience, * deliberately. */ audience?: string | readonly string[] | false; /** Seconds of clock difference tolerated on `exp`, `nbf`, and `iat`. Defaults to 60. */ clockToleranceSec?: number; /** The oldest a token may be, from its `iat`, in seconds. */ maxAgeSec?: number; /** Requires an `exp` claim. Defaults to true: a token that never expires is refused. */ requireExpiry?: boolean; /** Claims a token must carry, besides `exp`. */ requiredClaims?: readonly string[]; /** How long a fetched key set is kept, in milliseconds. Defaults to 10 minutes. */ jwksCacheMs?: number; /** * The least time between two fetches for keys not yet seen, in milliseconds, so tokens naming keys * that do not exist cannot make the server hammer the provider. Defaults to 30 seconds. */ jwksCooldownMs?: number; /** Fetches the key set. Defaults to the global `fetch`. */ fetch?: (input: string | URL, init?: RequestInit) => Promise; /** Replaces the clock, in milliseconds since the epoch, for tests. */ now?: () => number; } /** A reusable verifier: it keeps imported keys and the fetched key set between tokens. */ export interface JwtVerifier { /** Verifies a token: its form, its signature, and its claims. Throws `JwtError` when it is refused. */ verify(token: string): Promise; } /** * Builds a verifier for tokens from one issuer. * * ```ts * const verifier = createJwtVerifier({ * jwks: 'https://login.example.com/.well-known/jwks.json', * issuer: 'https://login.example.com/', * audience: 'agents-api', * }); * const { payload } = await verifier.verify(token); * ``` */ export declare function createJwtVerifier(options: JwtVerifyOptions): JwtVerifier; /** Which claims a principal is read from. A dot reaches into a nested claim: `realm_access.roles`. */ export interface JwtClaimNames { /** The subject. Defaults to `sub`. */ subject?: string; /** The tenant. Defaults to `tenant_id`. */ tenant?: string; /** The roles, as a list or a space-separated string. Defaults to `roles`. */ roles?: string; /** The scopes, as a space-separated string or a list. Defaults to `scope`, then `scp`. */ scopes?: string; } /** Options for `jwtAuth()`. */ export interface JwtAuthOptions extends JwtVerifyOptions { /** Reads the token from a request. Defaults to `Authorization: Bearer `; a cookie works too. */ token?: (request: Request) => string | undefined; /** Which claims the principal is read from. */ claims?: JwtClaimNames; /** * Builds the principal from the verified claims instead, for a mapping of your own. Returning * nothing refuses the request with 403: the token is valid, but not for this server. */ principal?: (payload: JwtPayload, request: Request) => Principal | undefined | Promise; } /** * Authenticates requests by a JSON Web Token: an identity provider's, through its key set, or your * own, signed with a shared secret. * * A request with no token is passed over, so another hook in `anyAuth()` can try. A token that is * refused answers 401 with a `WWW-Authenticate` challenge naming why, and a valid one becomes a * `Principal`: its subject, tenant, roles, and scopes, with `method: 'jwt'` and the verified claims. * * ```ts * createAgentServer({ * assistants, * authenticate: jwtAuth({ * jwks: 'https://login.example.com/.well-known/jwks.json', * issuer: 'https://login.example.com/', * audience: 'agents-api', * claims: { tenant: 'org_id', roles: 'realm_access.roles' }, * }), * }); * ``` */ export declare function jwtAuth(options: JwtAuthOptions): AuthHook; /** Options for `apiKeyAuth()`. Give it `keys`, `lookup`, or both. */ export interface ApiKeyAuthOptions { /** * Keys by their SHA-256 hash, from `hashApiKey()`, each with the principal it stands for. The keys * themselves are never written into configuration. */ keys?: Readonly>; /** Looks a key up by its hash, in a database or a secret store, when `keys` does not have it. */ lookup?: (hash: string, request: Request) => Principal | undefined | Promise; /** The header the key is read from. Defaults to `x-api-key`, then `Authorization: Bearer`. */ header?: string; /** * Only a credential with this prefix is taken for a key, such as `nxk_`, so a token in the same * header is left to another hook. Without it, anything shaped like a JWT is left alone. */ prefix?: string; } /** The SHA-256 of an API key, as hex: what `apiKeyAuth({ keys })` is configured with. */ export declare function hashApiKey(key: string): Promise; /** * Authenticates requests by an API key, for services and scripts rather than people. * * Keys are compared by their hash, so configuration and the database hold hashes only. A request * with no key is passed over; an unknown key answers 401. The principal is the one the key maps to, * with `method: 'api-key'`. * * ```ts * apiKeyAuth({ keys: { [await hashApiKey(process.env.CI_KEY)]: { userId: 'ci', tenantId: 'acme', scopes: ['write'] } } }) * ``` */ export declare function apiKeyAuth(options: ApiKeyAuthOptions): AuthHook; /** The headers a proxy names the caller in. */ export interface TrustedProxyHeaders { /** The user. Defaults to `x-forwarded-user`. */ user?: string; /** The tenant. Defaults to `x-forwarded-tenant`. */ tenant?: string; /** The roles. Defaults to `x-forwarded-roles`. */ roles?: string; /** The scopes. Defaults to `x-forwarded-scopes`. */ scopes?: string; } /** Options for `trustedProxyAuth()`. Give it `secret`, `trust`, or both. */ export interface TrustedProxyAuthOptions { /** A header the proxy sets to a shared secret, which proves the request came through it. */ secret?: { header: string; value: string; }; /** Decides whether a request came through the proxy, by a check of your own. */ trust?: (request: Request) => boolean | Promise; /** The headers the caller is named in. */ headers?: TrustedProxyHeaders; /** What separates several roles or scopes in one header. Defaults to a comma; spaces are trimmed. */ separator?: string; } /** * Takes the caller from headers an authenticating proxy sets: an identity-aware proxy, an API * gateway, a service mesh. * * Headers are only as trustworthy as the path a request took, so this refuses to exist without a * proof that the proxy sent the request: a shared secret it adds, a check of your own, or both. A * request without that proof, or without a user, is passed over. The principal carries * `method: 'proxy'`. */ export declare function trustedProxyAuth(options: TrustedProxyAuthOptions): AuthHook; /** Options for `createJwtSigner()`. Give it one key: `secret`, `privateKey`, or `keyPair`. */ export interface JwtSignerOptions { /** A shared secret, for HS256, HS384, and HS512, which `jwtAuth({ secret })` verifies with. At least 32 bytes. */ secret?: string | Uint8Array; /** A private key: PEM (`-----BEGIN PRIVATE KEY-----`, PKCS #8), or a private JWK, as `generateJwtKey()` makes one. */ privateKey?: string | JsonWebKey; /** A Web Crypto key pair, as `crypto.subtle.generateKey()` makes one, its public key extractable. */ keyPair?: { privateKey: CryptoKey; publicKey: CryptoKey; }; /** * The algorithm. Defaults to HS256 for a secret, and to the key's own for a JWK or a key pair: its * `alg`, or RS256 for RSA, ES256, ES384, or ES512 by curve, and EdDSA for Ed25519. A PEM key names it. */ algorithm?: JwtAlgorithm; /** The key's id, in each token's header and the key set. Defaults to the public key's RFC 7638 thumbprint; a secret has none. */ kid?: string; /** The issuer every token names. */ issuer?: string; /** The audience every token is for, unless one is issued for another. */ audience?: string | readonly string[]; /** How long a token lasts, in seconds, unless one is issued for longer or shorter. Defaults to an hour. */ expiresInSec?: number; /** Replaces the clock, in milliseconds since the epoch, for tests. */ now?: () => number; } /** What one token is issued with, over the signer's own. */ export interface JwtSignOptions { /** How long it lasts, in seconds. */ expiresInSec?: number; /** Who it is for. */ audience?: string | readonly string[]; /** Seconds from now before which it is not valid. */ notBeforeSec?: number; } /** Issues tokens with one key, which `jwtAuth()`, `createJwtVerifier()`, and any other verifier read. */ export interface JwtSigner { /** The algorithm it signs with. */ readonly algorithm: JwtAlgorithm; /** * Signs claims into a token: the signer's issuer and audience, `iat`, `exp`, and a `jti` of its own, * then the claims given, which win. */ sign(claims?: JwtPayload, options?: JwtSignOptions): Promise; /** * A token for a principal: its user as `sub`, its tenant, roles, and scopes, under the claim names * `jwtAuth()` reads them from, so the same `claims` names read it back. */ issue(principal: Principal, options?: JwtSignOptions & { claims?: JwtClaimNames; }): Promise; /** The public key, as a JWK with its `kid`, `alg`, and `use`, for a key set; none for a secret. */ publicKey(): Promise; } /** * Builds a signer, to issue tokens of your own: for a service, after a sign-in, or for a test. * * ```ts * const signer = createJwtSigner({ privateKey: JSON.parse(process.env.JWT_KEY ?? ''), issuer: 'https://agents.example.com' }); * const token = await signer.issue({ userId: 'ana', tenantId: 'acme', scopes: ['read'] }, { audience: 'agents-api' }); * ``` */ export declare function createJwtSigner(options: JwtSignerOptions): JwtSigner; /** * A new private key for `createJwtSigner()`, as a JWK with its `kid`, `alg`, and `use`: keep it in a * secret store, and the signer publishes its public half. Defaults to ES256. */ export declare function generateJwtKey(algorithm?: Exclude): Promise; /** * Serves signers' public keys as a JSON Web Key Set, at the URL `jwtAuth({ jwks })` and other services * read. Give it the signer in use and those whose tokens may still be live, so a key rotation never * refuses a token signed before it. * * ```ts * if (new URL(request.url).pathname === '/.well-known/jwks.json') return jwks(request); * ``` */ export declare function jwksHandler(...signers: JwtSigner[]): (request: Request) => Promise; /** An API key as it is kept: its hash, never the key, with the principal it stands for. */ export interface ApiKeyRecord { /** Its id, which names it in a list and to `revoke()` and `rotate()`. Not a secret. */ id: string; /** The key's SHA-256, as `hashApiKey()` gives it. */ hash: string; /** The key's first characters, its prefix and four more, to recognise it by. */ start: string; /** Who the key stands for. */ principal: Principal; /** What it is for, in words. */ label?: string; /** When it was issued, in milliseconds since the epoch. */ createdAt: number; /** When it stops working, in milliseconds since the epoch. */ expiresAt?: number; /** When it was last used, to the store's `touchEveryMs`. */ lastUsedAt?: number; /** When it was revoked. */ revokedAt?: number; /** The id of the key that replaced it, when it was rotated. */ replacedBy?: string; } /** A key just issued: the key itself, shown this once, and its record. */ export interface IssuedApiKey { /** The key, to hand to whoever will use it. It is never stored, so it cannot be shown again. */ key: string; /** Its record. */ record: ApiKeyRecord; } /** What a key is issued with. */ export interface ApiKeyIssueOptions { /** What it is for, in words. */ label?: string; /** How long it works, in seconds. Without it, until it is revoked. */ expiresInSec?: number; } /** Options for `createApiKeyStore()`. */ export interface ApiKeyStoreOptions { /** * Where keys are kept: the server's state store, so every replica reads the same keys. Defaults to * a `MemoryServerStore`, which one process alone reads. */ store?: ServerStateStore; /** What every key begins with, so it is recognised in a log or by a secret scanner. Defaults to `nxk_`. */ prefix?: string; /** The least time between two writes of a key's `lastUsedAt`, in milliseconds. Defaults to a minute. */ touchEveryMs?: number; /** Replaces the clock, in milliseconds since the epoch, for tests. */ now?: () => number; } /** API keys issued, rotated, and revoked by the application, which `apiKeyAuth()` reads by their hash. */ export interface ApiKeyStore { /** Issues a key for a principal. */ issue(principal: Principal, options?: ApiKeyIssueOptions): Promise; /** * Replaces a key with a new one for the same principal and label. The old one works for `graceSec` * more, 0 by default, so what uses it can move to the new one first. */ rotate(id: string, options?: ApiKeyIssueOptions & { graceSec?: number; }): Promise; /** Revokes a key at once. Resolves whether there was one to revoke. */ revoke(id: string): Promise; /** A key's record. */ get(id: string): Promise; /** Records, newest first, a tenant's alone when `tenantId` is given; `limit` read, 1,000 by default. */ list(options?: { tenantId?: string; limit?: number; }): Promise; /** The principal a key stands for, by its hash, while it works: `apiKeyAuth({ lookup })`. */ lookup(hash: string): Promise; /** `apiKeyAuth()` over this store, taking only keys with its prefix. */ auth(options?: Pick): AuthHook; } /** * API keys the application issues and revokes itself, kept as hashes in the server's state store. * * ```ts * const keys = createApiKeyStore({ store: fromStore(redisStore) }); * const { key } = await keys.issue({ userId: 'ci', tenantId: 'acme', scopes: ['write'] }, { label: 'CI' }); * createAgentServer({ assistants, state: fromStore(redisStore), authenticate: anyAuth(jwtAuth({ ... }), keys.auth()) }); * ``` */ export declare function createApiKeyStore(options?: ApiKeyStoreOptions): ApiKeyStore; /** How the session cookie is written. */ export interface SessionCookieOptions { /** Its name. Defaults to `__Host-nexus_session`, which a browser binds to this host alone, or `nexus_session` where that cannot be used. */ name?: string; /** The domain it is sent to, besides this host. Defaults to none: this host alone. */ domain?: string; /** The path it is sent for. Defaults to `/`. */ path?: string; /** Sent over HTTPS alone. Defaults to true; false for plain HTTP on a developer's machine. */ secure?: boolean; /** Whether a browser sends it from another site. Defaults to `Lax`; `None` needs `secure`. */ sameSite?: 'Strict' | 'Lax' | 'None'; } /** Options for `createSessions()`. */ export interface SessionOptions { /** Where sessions are kept: the server's state store, so every replica reads them. Defaults to a `MemoryServerStore`. */ store?: ServerStateStore; /** How the cookie is written. */ cookie?: SessionCookieOptions; /** The longest a session lasts from its start, in seconds. Defaults to 8 hours. */ maxAgeSec?: number; /** How long a session lasts unused, in seconds. Defaults to the longest it lasts. */ idleSec?: number; /** * Origins a request that changes something may come from, besides the request URL's own: the * application's public origin, when a proxy in front makes the server see another. */ origins?: readonly string[]; /** Replaces the clock, in milliseconds since the epoch, for tests. */ now?: () => number; } /** A session as it is kept: the hash of its secret, never the secret, with whom it is for. */ export interface SessionRecord { /** Its id, which names it to `revoke()`. Not a secret. */ id: string; /** The SHA-256 of the secret the cookie carries. */ hash: string; /** Who signed in. */ principal: Principal; /** When it started, in milliseconds since the epoch. */ createdAt: number; /** When it ends, however much it is used. */ expiresAt: number; /** When it was last used, to the minute. */ lastSeenAt?: number; } /** A session just started. */ export interface StartedSession { /** The value of a `Set-Cookie` header, for the response to the sign-in. */ cookie: string; /** Its record. */ record: SessionRecord; } /** Browser sessions: a cookie after a sign-in of the application's own, and the hook that reads it. */ export interface Sessions { /** Starts a session for a principal: set the cookie it returns on the sign-in's response. */ start(principal: Principal): Promise; /** Ends the request's session, and returns the `Set-Cookie` value that clears its cookie. */ end(request: Request): Promise; /** Ends a session by its id, wherever its cookie is. Resolves whether there was one. */ revoke(id: string): Promise; /** Live sessions, newest first, a user's or a tenant's alone when one is given; `limit` read, 1,000 by default. */ list(options?: { userId?: string; tenantId?: string; limit?: number; }): Promise; /** Removes sessions that have ended, up to 10,000 at a time, and resolves how many. Call it now and then, from a cron job. */ sweep(): Promise; /** * The `authenticate` hook. A request with no cookie is passed over; an ended or forged one answers * 401 and clears the cookie; a request that changes something, from a page of another origin, * answers 403. The principal carries `method: 'session'` and its id as the `sid` claim. */ readonly auth: AuthHook; } /** * Browser sessions after a sign-in of the application's own: a password, a magic link, or an identity * provider's redirect. The cookie carries a random secret, the store its hash, so a copy of the store * signs no one in; it is `HttpOnly`, `Secure`, and `SameSite=Lax` by default, and a request that * changes something must come from the application's own origin. * * ```ts * const sessions = createSessions({ store: fromStore(redisStore), origins: ['https://agents.example.com'] }); * // In the sign-in route, once the user is known: * const { cookie } = await sessions.start({ userId: user.id, tenantId: user.orgId, scopes: ['read', 'write'] }); * return new Response(null, { status: 303, headers: { location: '/', 'set-cookie': cookie } }); * ``` */ export declare function createSessions(options?: SessionOptions): Sessions; /** * Tries hooks in order and takes the first principal: tokens for people and keys for services on * one server. When none accepts the request, the first refusal any of them gave is the answer, so a * caller learns why their credential was refused. * * ```ts * authenticate: anyAuth(jwtAuth({ ... }), apiKeyAuth({ ... })) * ``` */ export declare function anyAuth(...hooks: AuthHook[]): AuthHook; export {};