UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

394 lines 18.4 kB
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