alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
394 lines • 18.4 kB
TypeScript
import { Alepha, Static } from "alepha";
import { IssuerPrimitive, JwtProvider, UserAccount } from "alepha/security";
import { CryptoProvider } from "alepha/crypto";
import { DateTimeProvider } from "alepha/datetime";
//#region ../../src/api/oauth/entities/oauthClientEntity.d.ts
/**
* A registered OAuth 2.1 client application.
*
* Rows are created by Dynamic Client Registration (RFC 7591) when an MCP
* client (e.g. Claude) first connects. `source` records who created the
* client; for DCR it is always `"dcr"` and `createdByUserId` is null until
* a user completes an authorization.
*/
declare const oauthClientEntity: import("alepha/orm").EntityPrimitive<import("zod").ZodObject<{
id: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_PRIMARY_KEY>, typeof import("alepha/orm").PG_DEFAULT>;
createdAt: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_CREATED_AT>, typeof import("alepha/orm").PG_DEFAULT>;
updatedAt: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_UPDATED_AT>, typeof import("alepha/orm").PG_DEFAULT>;
clientId: import("zod").ZodString;
clientName: import("zod").ZodString;
redirectUris: import("alepha/orm").PgAttr<import("zod").ZodArray<import("zod").ZodString>, typeof import("alepha/orm").PG_DEFAULT>;
scopes: import("alepha/orm").PgAttr<import("zod").ZodArray<import("zod").ZodString>, typeof import("alepha/orm").PG_DEFAULT>;
realm: import("zod").ZodString;
type: import("alepha/orm").PgAttr<import("zod").ZodEnum<{
confidential: "confidential";
public: "public";
}>, typeof import("alepha/orm").PG_DEFAULT>;
trusted: import("alepha/orm").PgAttr<import("zod").ZodBoolean, typeof import("alepha/orm").PG_DEFAULT>;
clientSecretHash: import("zod").ZodOptional<import("zod").ZodString>;
source: import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_DEFAULT>;
createdByUserId: import("zod").ZodOptional<import("zod").ZodString>;
lastUsedAt: import("zod").ZodOptional<import("zod").ZodString>;
revokedAt: import("zod").ZodOptional<import("zod").ZodString>;
}, import("zod/v4/core").$strip>>;
type OAuthClientEntity = Static<typeof oauthClientEntity.schema>;
//#endregion
//#region ../../src/api/oauth/services/OAuthClientService.d.ts
/**
* The user projection a realm's `loadUser` hands to the OAuth module —
* a `UserAccount` plus the OIDC profile extras the id_token mints
* (`email_verified`; `firstName`/`lastName` map to `given_name`/`family_name`).
*/
interface OidcIssuerUser extends UserAccount {
emailVerified?: boolean;
}
interface RegisterClientOptions {
realm: string;
/**
* Explicit client id (OIDC clients registered by Platform). When omitted,
* a `mcp_<uuid>` id is generated (Dynamic Client Registration).
*/
clientId?: string;
clientName?: string;
redirectUris: string[];
scopes?: string[];
/**
* `confidential` clients require a `secret` (stored hashed) and authenticate
* at the token endpoint. Defaults to `public` (PKCE only).
*/
type?: "public" | "confidential";
/** First-party client — skip the consent screen (see the entity field). */
trusted?: boolean;
/** Raw secret for a confidential client; stored as a scrypt hash. */
secret?: string;
source?: "dcr" | "user" | "admin";
createdByUserId?: string;
}
/**
* Core OAuth 2.1 service backing the authorization server.
*
* Responsibilities:
* - Client registration (RFC 7591 Dynamic Client Registration) and lookup,
* with exact-match redirect_uri validation.
* - Stateless PKCE authorization codes: minting short-lived signed JWTs that
* carry the grant, and verifying/consuming them (replay, expiry, client and
* redirect_uri checks, S256 PKCE).
* - Realm issuer registry: realms register an issuer + user loader so the
* token endpoint can mint access tokens without depending on realm wiring.
*/
declare class OAuthClientService {
protected readonly alepha: Alepha;
protected readonly dateTime: DateTimeProvider;
protected readonly log: import("alepha/logger").Logger;
protected readonly repo: import("alepha/orm").Repository<import("zod").ZodObject<{
id: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_PRIMARY_KEY>, typeof import("alepha/orm").PG_DEFAULT>;
createdAt: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_CREATED_AT>, typeof import("alepha/orm").PG_DEFAULT>;
updatedAt: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_UPDATED_AT>, typeof import("alepha/orm").PG_DEFAULT>;
clientId: import("zod").ZodString;
clientName: import("zod").ZodString;
redirectUris: import("alepha/orm").PgAttr<import("zod").ZodArray<import("zod").ZodString>, typeof import("alepha/orm").PG_DEFAULT>;
scopes: import("alepha/orm").PgAttr<import("zod").ZodArray<import("zod").ZodString>, typeof import("alepha/orm").PG_DEFAULT>;
realm: import("zod").ZodString;
type: import("alepha/orm").PgAttr<import("zod").ZodEnum<{
confidential: "confidential";
public: "public";
}>, typeof import("alepha/orm").PG_DEFAULT>;
trusted: import("alepha/orm").PgAttr<import("zod").ZodBoolean, typeof import("alepha/orm").PG_DEFAULT>;
clientSecretHash: import("zod").ZodOptional<import("zod").ZodString>;
source: import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_DEFAULT>;
createdByUserId: import("zod").ZodOptional<import("zod").ZodString>;
lastUsedAt: import("zod").ZodOptional<import("zod").ZodString>;
revokedAt: import("zod").ZodOptional<import("zod").ZodString>;
}, import("zod/v4/core").$strip>>;
protected readonly jwt: JwtProvider;
protected readonly crypto: CryptoProvider;
/**
* Codes already redeemed in this process. Single-use enforcement only
* needs to cover the ~60s code lifetime, so a bounded in-memory set is
* sufficient even on serverless — an expired code fails JWT verification
* regardless.
*/
protected readonly usedCodes: Set<string>;
/**
* Registry of realm issuers used to mint access tokens. Populated by
* `$realm` (via `registerIssuer`) so the OAuth module does not depend on the
* realm wiring directly.
*/
protected readonly issuers: Map<string, {
issuer: IssuerPrimitive;
loadUser: (userId: string) => Promise<OidcIssuerUser>;
}>;
/**
* Register a realm issuer and a user loader. Called by `$realm` so the
* OAuth token endpoint can mint access tokens for that realm.
*/
registerIssuer(realm: string, issuer: IssuerPrimitive, loadUser: (userId: string) => Promise<OidcIssuerUser>): void;
/**
* Mint an access token for a consumed authorization-code grant, using the
* issuer registered for `realm`. Throws if the realm has no issuer.
*/
issueAccessToken(realm: string, grant: {
userId: string;
scopes: string[];
resource?: string;
clientId?: string;
}): Promise<{
access_token: string;
expires_in?: number;
refresh_token?: string;
}>;
/**
* Mint an OIDC `id_token` for a consumed grant, signed by the realm's issuer
* key (asymmetric when configured). Claims: iss, sub, aud (client_id), exp,
* iat, nonce (when present), plus the standard profile claims the loader
* provides: email, email_verified, name, given_name, family_name,
* preferred_username, picture.
*/
issueIdToken(realm: string, params: {
userId: string;
clientId: string;
issuer: string;
nonce?: string;
}): Promise<string>;
/**
* Exchange a refresh token for a fresh access token (OAuth 2.1
* `refresh_token` grant), using the issuer registered for `realm`. Lets an
* MCP client stay connected for the refresh token's full lifetime without
* re-running the authorization flow. Throws if the realm has no issuer or
* the refresh token is invalid/expired.
*/
refreshAccessToken(realm: string, refreshToken: string): Promise<{
access_token: string;
expires_in?: number;
refresh_token?: string;
/** Subject of the refreshed session — lets the token endpoint re-mint an
* OIDC `id_token` for relying parties that forward it as the Bearer. */
userId: string;
}>;
/**
* Register a new OAuth client. Used by the RFC 7591 DCR endpoint and,
* later, by user/admin UIs (via the `source` field).
*/
register(options: RegisterClientOptions): Promise<OAuthClientEntity>;
/**
* Idempotently align a registered client's redirect_uri allowlist with the
* given list. No-op when the client is unknown or the list already matches —
* safe to call from post-deploy seeders (allowlist changes ship as code, and
* `register` alone would leave existing rows stale).
*/
updateRedirectUris(clientId: string, redirectUris: string[]): Promise<void>;
/**
* Verify a confidential client's secret against its stored scrypt hash.
* Returns false for unknown/revoked/public (no-hash) clients.
*/
verifySecret(clientId: string, secret: string): Promise<boolean>;
/**
* Validate a registered redirect_uri. https (or http://localhost) only, and
* at most a single `*` which must live inside the host (see
* `redirectUriMatches` for the matching rule).
*/
protected assertValidRedirectUri(uri: string): void;
/**
* Look up a client by its public `clientId`. Returns null if unknown.
*/
findByClientId(clientId: string): Promise<OAuthClientEntity | null>;
/**
* Narrow the scopes a client asks for to those it is actually registered
* for. The requested scope string is attacker-controlled, so it must never
* be trusted verbatim — a client registered for `["mcp"]` that asks for
* `mcp admin` must only be granted `mcp`. When nothing (usable) is
* requested, the client's full registered set is the default grant.
* Requested order is preserved and duplicates are dropped.
*/
intersectScopes(requested: string[] | undefined, allowed: string[]): string[];
/**
* Redirect_uri check. A registered pattern is matched byte-exact unless it
* contains a single `*`, which matches exactly ONE host label (no dots).
* E.g. `https://*.alepha.club/auth/callback` matches
* `https://b14.alepha.club/auth/callback` but NOT `https://alepha.club/...`
* nor `https://a.b.alepha.club/...`.
*/
isRedirectUriAllowed(client: OAuthClientEntity, redirectUri: string): boolean;
protected redirectUriMatches(pattern: string, candidate: string): boolean;
/**
* Mint a stateless authorization code: a short-lived signed JWT
* (`typ: "oauth_code"`) carrying the grant. No server-side code storage.
*/
createAuthorizationCode(realm: string, grant: {
userId: string;
clientId: string;
redirectUri: string;
codeChallenge: string;
scopes: string[];
resource?: string;
nonce?: string;
}): Promise<string>;
/**
* Verify and atomically consume an authorization code. Throws on expiry,
* replay, client/redirect mismatch, or PKCE failure.
*/
consumeAuthorizationCode(realm: string, code: string, check: {
clientId: string;
redirectUri: string;
codeVerifier: string;
}): Promise<{
userId: string;
scopes: string[];
resource?: string;
nonce?: string;
}>;
}
//#endregion
//#region ../../src/api/oauth/controllers/OAuthController.d.ts
/**
* Configuration for the OAuth authorization server.
* `realm` is the issuer realm whose JWTs are minted as access tokens;
* `resource` is the path of the protected MCP endpoint;
* `loginPath` is the app-level login page unauthenticated users are
* redirected to from the authorize endpoint.
*/
declare const oauthOptions: import("alepha").Atom<import("zod").ZodObject<{
realm: import("zod").ZodString;
resource: import("zod").ZodString;
loginPath: import("zod").ZodString;
}, import("zod/v4/core").$strip>, "alepha.api.oauth.options">;
/**
* OAuth 2.1 authorization server endpoints: discovery metadata and
* RFC 7591 dynamic client registration. Authorize/token routes are added
* separately.
*/
declare class OAuthController {
protected readonly log: import("alepha/logger").Logger;
protected readonly options: Readonly<{
realm: string;
resource: string;
loginPath: string;
}>;
protected readonly clients: OAuthClientService;
protected readonly jwt: JwtProvider;
/**
* Absolute origin of the current request, e.g. https://app.com.
*/
protected baseUrl(url: URL): string;
metadata: import("alepha/server").RoutePrimitive<import("alepha/server").RequestConfigSchema>;
protectedResource: import("alepha/server").RoutePrimitive<import("alepha/server").RequestConfigSchema>;
openidConfiguration: import("alepha/server").RoutePrimitive<import("alepha/server").RequestConfigSchema>;
jwks: import("alepha/server").RoutePrimitive<import("alepha/server").RequestConfigSchema>;
register: import("alepha/server").RoutePrimitive<{
body: import("zod").ZodObject<{
client_name: import("zod").ZodOptional<import("zod").ZodString>;
redirect_uris: import("zod").ZodArray<import("zod").ZodString>;
scope: import("zod").ZodOptional<import("zod").ZodString>;
grant_types: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
token_endpoint_auth_method: import("zod").ZodOptional<import("zod").ZodString>;
}, import("zod/v4/core").$strip>;
}>;
/**
* GET /oauth/authorize — OAuth 2.1 authorization request. If the user
* has no session, redirect to the realm login page with a return URL.
* If authenticated, render the consent screen.
*/
authorize: import("alepha/server").RoutePrimitive<{
query: import("zod").ZodObject<{
response_type: import("zod").ZodString;
client_id: import("zod").ZodString;
redirect_uri: import("zod").ZodString;
code_challenge: import("zod").ZodString;
code_challenge_method: import("zod").ZodString;
scope: import("zod").ZodOptional<import("zod").ZodString>;
state: import("zod").ZodOptional<import("zod").ZodString>;
resource: import("zod").ZodOptional<import("zod").ZodString>;
prompt: import("zod").ZodOptional<import("zod").ZodString>;
nonce: import("zod").ZodOptional<import("zod").ZodString>;
}, import("zod/v4/core").$strip>;
}>;
/**
* POST /oauth/authorize — consent decision. On "allow", mint an
* authorization code and redirect back to the client's redirect_uri.
*
* CSRF: this route carries no CSRF token and relies solely on the session
* cookie to identify the user. This is a deliberate MVP/MCP tradeoff — a
* forged consent submit can still only issue an authorization code to an
* already-registered client, and that code is bound by PKCE, so the
* attacker cannot redeem it without the matching code_verifier.
*/
authorizeDecision: import("alepha/server").RoutePrimitive<{
body: import("zod").ZodObject<{
decision: import("zod").ZodString;
response_type: import("zod").ZodString;
client_id: import("zod").ZodString;
redirect_uri: import("zod").ZodString;
code_challenge: import("zod").ZodString;
code_challenge_method: import("zod").ZodString;
scope: import("zod").ZodOptional<import("zod").ZodString>;
state: import("zod").ZodOptional<import("zod").ZodString>;
resource: import("zod").ZodOptional<import("zod").ZodString>;
nonce: import("zod").ZodOptional<import("zod").ZodString>;
}, import("zod/v4/core").$strip>;
}>;
/**
* POST /oauth/token — supports the `authorization_code` grant (verifies
* PKCE, mints an access token via the realm issuer) and the
* `refresh_token` grant (exchanges a refresh token for a fresh access
* token, so a client stays connected without re-running the flow).
*/
token: import("alepha/server").RoutePrimitive<{
body: import("zod").ZodObject<{
grant_type: import("zod").ZodOptional<import("zod").ZodString>;
code: import("zod").ZodOptional<import("zod").ZodString>;
client_id: import("zod").ZodOptional<import("zod").ZodString>;
redirect_uri: import("zod").ZodOptional<import("zod").ZodString>;
code_verifier: import("zod").ZodOptional<import("zod").ZodString>;
refresh_token: import("zod").ZodOptional<import("zod").ZodString>;
client_secret: import("zod").ZodOptional<import("zod").ZodString>;
}, import("zod/v4/core").$strip>;
}>;
}
//#endregion
//#region ../../src/api/oauth/helpers/oidcMetadata.d.ts
/**
* OpenID Connect Discovery 1.0 — provider metadata document
* (`/.well-known/openid-configuration`). `baseUrl` is the absolute origin of
* the OIDC provider (e.g. https://alepha.club).
*/
declare const buildOpenIdConfiguration: (baseUrl: string) => {
issuer: string;
authorization_endpoint: string;
token_endpoint: string;
jwks_uri: string;
registration_endpoint: string;
response_types_supported: string[];
grant_types_supported: string[];
subject_types_supported: string[];
id_token_signing_alg_values_supported: string[];
code_challenge_methods_supported: string[];
token_endpoint_auth_methods_supported: string[];
scopes_supported: string[];
};
//#endregion
//#region ../../src/api/oauth/index.d.ts
/**
* OAuth 2.1 authorization server module for MCP.
*
* **Features:**
* - OAuth 2.1 authorization code flow with PKCE (RFC 7636)
* - Dynamic Client Registration (RFC 7591)
* - Authorization server metadata discovery (RFC 8414)
* - Stateless authorization codes (short-lived signed JWTs)
* - Single-use code enforcement
*
* **Integration:**
* Register the module and configure the realm + protected resource path:
*
* ```ts
* const app = Alepha.create()
* .with(AlephaOAuth)
* .set(oauthOptions, { realm: "users", resource: "/mcp" });
* ```
*
* @module alepha.api.oauth
*/
declare const AlephaOAuth: import("alepha").Service<import("alepha").Module>;
//#endregion
export { AlephaOAuth, type OAuthClientEntity, OAuthClientService, OAuthController, type RegisterClientOptions, buildOpenIdConfiguration, oauthClientEntity, oauthOptions };
//# sourceMappingURL=index.d.ts.map