From 37482733a66aae5f5095b94d910cf82a32e2ee67 Mon Sep 17 00:00:00 2001 From: Hernan Alvarado Date: Tue, 24 Mar 2026 22:01:24 -0500 Subject: [PATCH 1/2] chore(core): clean up core package --- packages/core/src/@types/config.ts | 249 ++++++++++ packages/core/src/@types/errors.ts | 55 +++ packages/core/src/@types/index.ts | 433 +----------------- packages/core/src/@types/oauth.ts | 78 ++++ packages/core/src/@types/session.ts | 43 +- .../core/src/actions/callback/access-token.ts | 4 +- .../core/src/actions/callback/callback.ts | 9 +- .../core/src/actions/callback/userinfo.ts | 10 +- .../core/src/actions/csrfToken/csrfToken.ts | 4 +- packages/core/src/actions/session/session.ts | 4 +- .../src/actions/signIn/authorization-url.ts | 4 +- .../core/src/actions/signIn/authorization.ts | 6 +- packages/core/src/actions/signOut/signOut.ts | 2 +- packages/core/src/api/createApi.ts | 2 +- packages/core/src/api/getSession.ts | 2 +- packages/core/src/api/index.ts | 4 + packages/core/src/api/signIn.ts | 4 +- packages/core/src/client/client.ts | 2 +- packages/core/src/cookie.ts | 2 +- packages/core/src/createAuth.ts | 9 +- packages/core/src/index.ts | 17 +- packages/core/src/jose.ts | 22 +- packages/core/src/{ => lib}/assert.ts | 58 +-- packages/core/src/{ => lib}/errors.ts | 0 .../src/{request.ts => lib/fetch-async.ts} | 0 packages/core/src/{ => lib}/headers.ts | 0 packages/core/src/{ => lib}/logger.ts | 4 +- packages/core/src/lib/utils.ts | 129 ++++++ packages/core/src/oauth/gitlab.ts | 2 +- packages/core/src/oauth/index.ts | 4 +- packages/core/src/oauth/notion.ts | 2 +- packages/core/src/{ => router}/context.ts | 4 +- packages/core/src/router/errorHandler.ts | 72 +++ packages/core/src/schemas.ts | 16 +- packages/core/src/{secure.ts => security.ts} | 17 +- packages/core/src/session/index.ts | 2 +- packages/core/src/session/manager/cookie.ts | 2 +- packages/core/src/session/manager/jose.ts | 2 +- .../core/src/session/strategies/stateless.ts | 6 +- packages/core/src/utils.ts | 182 -------- .../actions/callback/access-token.test.ts | 2 +- .../test/actions/callback/callback.test.ts | 4 +- .../test/actions/callback/userinfo.test.ts | 2 +- .../core/test/actions/session/session.test.ts | 4 +- .../core/test/actions/signOut/signOut.test.ts | 2 +- packages/core/test/assert.test.ts | 2 +- packages/core/test/context.test.ts | 2 +- packages/core/test/env.test.ts | 12 +- packages/core/test/jose.test.ts | 2 +- packages/core/test/request.test.ts | 2 +- packages/core/test/secure.test.ts | 2 +- packages/core/test/utils.test.ts | 64 --- 52 files changed, 755 insertions(+), 812 deletions(-) create mode 100644 packages/core/src/@types/config.ts create mode 100644 packages/core/src/@types/errors.ts create mode 100644 packages/core/src/@types/oauth.ts create mode 100644 packages/core/src/api/index.ts rename packages/core/src/{ => lib}/assert.ts (61%) rename packages/core/src/{ => lib}/errors.ts (100%) rename packages/core/src/{request.ts => lib/fetch-async.ts} (100%) rename packages/core/src/{ => lib}/headers.ts (100%) rename packages/core/src/{ => lib}/logger.ts (99%) create mode 100644 packages/core/src/lib/utils.ts rename packages/core/src/{ => router}/context.ts (94%) create mode 100644 packages/core/src/router/errorHandler.ts rename packages/core/src/{secure.ts => security.ts} (87%) delete mode 100644 packages/core/src/utils.ts delete mode 100644 packages/core/test/utils.test.ts diff --git a/packages/core/src/@types/config.ts b/packages/core/src/@types/config.ts new file mode 100644 index 00000000..34b417e7 --- /dev/null +++ b/packages/core/src/@types/config.ts @@ -0,0 +1,249 @@ +import { createJoseInstance } from "@/jose.ts" +import { createLogEntry } from "@/lib/logger.ts" +import { createAuthAPI } from "@/api/createApi.ts" +import type { Prettify } from "@/@types/utility.ts" +import type { BuiltInOAuthProvider } from "@/oauth/index.ts" +import type { OAuthProviderCredentials, OAuthProviderRecord } from "@/@types/oauth.ts" +import type { SerializeOptions } from "@aura-stack/router/cookie" +import type { JWTKey, SessionConfig, SessionStrategy, User } from "@/@types/session.ts" + +/** + * Main configuration interface for Aura Auth. + * This is the user-facing configuration object passed to `createAuth()`. + */ +export interface AuthConfig { + /** + * OAuth providers available in the authentication and authorization flows. It provides a type-inference + * for the OAuth providers that are supported by Aura Stack Auth; alternatively, you can provide a custom + * OAuth third-party authorization service by implementing the `OAuthProviderCredentials` interface. + * + * Built-in OAuth providers: + * oauth: ["github", "google"] + * + * Custom credentials via factory: + * oauth: [github({ clientId: "...", clientSecret: "..." })] + * + * Custom OAuth providers: + * oauth: [ + * { + * id: "oauth-providers", + * name: "OAuth", + * authorizeURL: "https://example.com/oauth/authorize", + * accessToken: "https://example.com/oauth/token", + * scope: "profile email", + * responseType: "code", + * userInfo: "https://example.com/oauth/userinfo", + * clientId: process.env.AURA_AUTH_PROVIDER_CLIENT_ID, + * clientSecret: process.env.AURA_AUTH_PROVIDER_CLIENT_SECRET, + * } + * ] + */ + oauth: (BuiltInOAuthProvider | OAuthProviderCredentials)[] + /** + * Cookie options defines the configuration for cookies used in Aura Auth. + * It includes a prefix for cookie names and flag options to determine + * the security and scope of the cookies. + * + * **⚠️ WARNING:** Ensure that the cookie options are configured correctly to + * maintain the security and integrity of the authentication process. `Aura Auth` + * is not responsible for misconfigured cookies that may lead to security vulnerabilities. + * + * - prefix: A string prefix to be added to all cookie names, by default "aura-stack". + * - flag options (This attributes help to define the security level of the cookies): + * - secure: Cookies use the __Secure- prefix and are only sent over HTTPS connections. + * - host: Cookies use the __Host- prefix and are only sent over HTTPS connections. + * - standard: Cookies can be sent over both HTTP and HTTPS connections. (default in development) + * + * @see https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#name-the-__secure-prefix + * @see https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#name-the-__host-prefix + */ + cookies?: Partial + /** + * Secret used to sign and verify JWT tokens for session and csrf protection. + * If not provided, it will load from the environment variable `AURA_AUTH_SECRET` or `AUTH_SECRET`, but if it + * doesn't exist, it will throw an error during the initialization of the Auth module. + */ + secret?: JWTKey + /** + * Base URL of the application, used to construct the incoming request's origin. + */ + baseURL?: string + /** + * Base path for all authentication routes. Default is `/auth`. + */ + basePath?: `/${string}` + /** + * Enable trusted proxy headers for scenarios where the application is behind a reverse proxy or load balancer. + * This setting allows Aura Auth to correctly interpret headers like `X-Forwarded-For` and `X-Forwarded-Proto` + * to determine the original client IP address and protocol. + * + * Default is `false`. Enable this option only if you are certain that your application is behind a trusted proxy. + * Misconfiguration can lead to security vulnerabilities, such as incorrect handling of secure cookies or + * inaccurate client IP logging. + * + * This value can also be set via environment variable as `AURA_AUTH_TRUSTED_PROXY_HEADERS` + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded + * @experimental + */ + trustedProxyHeaders?: boolean + /** + * Logger configuration for handling authentication-related logs and errors. It can be set to `true`, + * `DEBUG=true`, `LOG_LEVEL=debug`, or a custom logger. It implements the syslog format. + */ + logger?: boolean | Logger + /** + * Defines trusted origins for your application to prevent open redirect attacks. + * URLs from the Referer header, Origin header, request URL, and redirectTo option + * are validated against this list before redirecting. + * + * - **Exact URL**: `https://example.com` matches only that origin. + * - **Subdomain wildcard**: `https://*.example.com` matches `https://app.example.com`, `https://api.example.com`, etc. + * @example + * trustedOrigins: ["https://example.com", "https://*.example.com", "http://localhost:3000"] + * + * + * trustedOrigins: async (request) => { + * const origin = new URL(request.url).origin + * return [origin, "https://admin.example.com"] + * } + */ + trustedOrigins?: TrustedOrigin[] | ((request: Request) => Promise | TrustedOrigin[]) + /** + * Defines the session management strategy for Aura Auth. It determines how sessions are created, stored, and validated. + */ + session?: SessionConfig +} + +/** + * Cookie type with __Secure- prefix, must be Secure. + * @see https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#name-the-__secure-prefix + */ +export type SecureCookie = { strategy: "secure" } & Prettify> + +/** + * Cookie type with __Host- prefix, must be Secure, Path=/, no Domain attribute. + * @see https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#name-the-__host-prefix + */ +export type HostCookie = { strategy: "host" } & Prettify> + +/** + * Standard cookie type without security prefixes. + * Can be sent over both HTTP and HTTPS connections (default in development). + */ +export type StandardCookie = { strategy?: "standard" } & Prettify> + +/** + * Union type for cookie options based on the specified strategy. + * - `secure`: Cookies are only sent over HTTPS connections + * - `host`: Cookies use the __Host- prefix and are only sent over HTTPS connections + * - `standard`: Cookies can be sent over both HTTP and HTTPS connections (default in development) + */ +export type CookieStrategyAttributes = StandardCookie | SecureCookie | HostCookie + +/** + * Names of cookies used by Aura Auth for session management and OAuth flows. + * - `sessionToken`: User session JWT + * - `csrfToken`: CSRF protection token + * - `state`: OAuth state parameter for CSRF protection + * - `code_verifier`: PKCE code verifier for authorization code flow + * - `redirect_uri`: OAuth callback URI + * - `redirect_to`: Post-authentication redirect path + * - `nonce`: OpenID Connect nonce parameter + */ +export type CookieName = "sessionToken" | "csrfToken" | "state" | "codeVerifier" | "redirectTo" | "redirectURI" + +export type CookieStoreConfig = Record + +export interface CookieConfig { + /** + * Prefix to be added to all cookie names. By default "aura-stack". + */ + prefix?: string + overrides?: Partial +} + +/** + * A trusted origin URL or pattern. Supports: + * - Exact: `https://example.com` + * - Subdomain wildcard: `https://*.example.com` + */ +export type TrustedOrigin = string + +/** + * Log level for logger messages. + */ +export type LogLevel = "warn" | "error" | "debug" | "info" + +/** Defines the Severity between 0 to 7 */ +export type Severity = "emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug" + +/** + * @see https://datatracker.ietf.org/doc/html/rfc5424 + */ +export type SyslogOptions = { + facility: 4 | 10 + severity: Severity + timestamp?: string + hostname?: string + appName?: string + procId?: string + msgId: string + message: string + structuredData?: Record +} + +/** + * Logger function interface for structured logging. + * Called when errors or warnings occur during authentication flows. + */ +export interface Logger { + level?: LogLevel + log?: (args: SyslogOptions) => void +} + +export type AuthAPI = ReturnType +export type JoseInstance = ReturnType> + +export interface InternalLogger { + level: LogLevel + log: typeof createLogEntry +} + +export interface RouterGlobalContext { + oauth: OAuthProviderRecord + cookies: CookieStoreConfig + jose: JoseInstance + secret?: JWTKey + baseURL?: string + basePath: string + trustedProxyHeaders: boolean + trustedOrigins?: TrustedOrigin[] | ((request: Request) => Promise | TrustedOrigin[]) + logger?: InternalLogger + sessionStrategy: SessionStrategy +} + +/** + * Internal runtime configuration used within Aura Auth after initialization. + * All optional fields from AuthConfig are resolved to their default values. + */ +export type AuthRuntimeConfig = RouterGlobalContext + +export interface AuthInstance { + api: AuthAPI + jose: JoseInstance + handlers: { + GET: (request: Request) => Response | Promise + POST: (request: Request) => Response | Promise + ALL: (request: Request) => Response | Promise + } +} + +export type InternalContext = RouterGlobalContext & { + cookieConfig: { + secure: CookieStoreConfig + standard: CookieStoreConfig + } +} diff --git a/packages/core/src/@types/errors.ts b/packages/core/src/@types/errors.ts new file mode 100644 index 00000000..b62cbcfd --- /dev/null +++ b/packages/core/src/@types/errors.ts @@ -0,0 +1,55 @@ +import { z } from "zod/v4" +import { OAuthAccessTokenErrorResponse, OAuthAuthorizationErrorResponse } from "@/schemas.ts" + +export type APIErrorMap = Record + +/** + * Base OAuth error response structure. + */ +export interface OAuthError { + error: T + error_description?: string +} + +/** + * OAuth 2.0 Authorization Error Response Types + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 + */ +export type AuthorizationError = OAuthError["error"]> + +/** + * OAuth 2.0 Access Token Error Response Types + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + */ +export type AccessTokenError = OAuthError["error"]> + +/** + * OAuth 2.0 Token Revocation Error Response Types + * @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.2.1 + */ +export type TokenRevocationError = OAuthError<"invalid_session_token"> + +export type ErrorType = AuthorizationError["error"] | AccessTokenError["error"] | TokenRevocationError["error"] + +export type AuthInternalErrorCode = + | "INVALID_OAUTH_CONFIGURATION" + | "INVALID_JWT_TOKEN" + | "JOSE_INITIALIZATION_FAILED" + | "SESSION_STORE_NOT_INITIALIZED" + | "COOKIE_STORE_NOT_INITIALIZED" + | "COOKIE_PARSING_FAILED" + | "COOKIE_NOT_FOUND" + | "INVALID_ENVIRONMENT_CONFIGURATION" + | "INVALID_URL" + | "INVALID_SALT_SECRET_VALUE" + | "UNTRUSTED_ORIGIN" + | "INVALID_OAUTH_PROVIDER_CONFIGURATION" + | "DUPLICATED_OAUTH_PROVIDER_ID" + +export type AuthSecurityErrorCode = + | "INVALID_STATE" + | "MISMATCHING_STATE" + | "POTENTIAL_OPEN_REDIRECT_ATTACK_DETECTED" + | "CSRF_TOKEN_INVALID" + | "CSRF_TOKEN_MISSING" + | "SESSION_TOKEN_MISSING" diff --git a/packages/core/src/@types/index.ts b/packages/core/src/@types/index.ts index 5b57c054..eb85e2db 100644 --- a/packages/core/src/@types/index.ts +++ b/packages/core/src/@types/index.ts @@ -1,23 +1,23 @@ -import { z } from "zod" -import { createLogEntry } from "@/logger.ts" -import { OAuthAccessTokenErrorResponse, OAuthAuthorizationErrorResponse, OAuthEnvSchema } from "@/schemas.ts" -import { createJoseInstance, type JWTPayload } from "@/jose.ts" -import type { createAuthAPI } from "@/api/createApi.ts" +import { z } from "zod/v4" +import { OAuthEnvSchema } from "@/schemas.ts" +import type { JWTPayload } from "@/jose.ts" +import type { Prettify } from "@/@types/utility.ts" import type { ClientOptions } from "@aura-stack/router" import type { createAuthInstance } from "@/createAuth.ts" -import type { BuiltInOAuthProvider } from "@/oauth/index.ts" -import type { LiteralUnion, Prettify } from "@/@types/utility.ts" -import type { SerializeOptions } from "@aura-stack/router/cookie" -import type { JWTKey, Session, SessionConfig, SessionStrategy, User } from "@/@types/session.ts" -export type * from "./utility.ts" export type { BuiltInOAuthProvider } from "@/oauth/index.ts" -export type * from "@/@types/session.ts" export type { TypedJWTPayload } from "@aura-stack/jose" +export type * from "@/@types/config.ts" +export type * from "@/@types/errors.ts" +export type * from "@/@types/oauth.ts" +export type * from "@/@types/session.ts" +export type * from "@/@types/utility.ts" + /** * Standard JWT claims that are managed internally by the token system. * These fields are typically filtered out before returning user data. + * @deprecated */ export type JWTStandardClaims = Pick @@ -26,422 +26,11 @@ export type JWTStandardClaims = Pick - -export type ResponseType = LiteralUnion<"code" | "token" | "refresh_token" | "id_token"> - -/** - * Configuration for an OAuth provider without credentials. - * Use this type when defining provider metadata and endpoints. - */ -export interface OAuthProviderConfig, DefaultUser extends User = User> { - id: string - name: string - /** - * @deprecated - * use `authorize` instead of `authorizeURL` - */ - authorizeURL?: string - authorize: - | string - | { - url: string - params?: Partial & { responseType: ResponseType }> - } - accessToken: - | string - | { - url: string - headers?: Record - } - userInfo: - | string - | { - url: string - headers?: Record - method?: string - } - /** - * @deprecated - * use `authorize.params.scope` instead of `scope` - */ - scope?: string - /** - * @deprecated - * use `authorize.params.response_type` instead of `responseType` - */ - responseType?: ResponseType - profile?: (profile: Profile) => DefaultUser | Promise -} - -/** - * OAuth provider configuration with client credentials. - * Extends OAuthProviderConfig with clientId and clientSecret. - */ -export interface OAuthProviderCredentials< - Profile extends object = Record, - DefaultUser extends User = User, -> extends OAuthProviderConfig { - clientId?: string - clientSecret?: string -} - -/** - * Complete OAuth provider type combining configuration and credentials. - */ -export type OAuthProvider< - Profile extends object = Record, - DefaultUser extends User = User, -> = OAuthProviderCredentials - -/** - * Cookie type with __Secure- prefix, must be Secure. - * @see https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#name-the-__secure-prefix - */ -export type SecureCookie = { strategy: "secure" } & Prettify> - -/** - * Cookie type with __Host- prefix, must be Secure, Path=/, no Domain attribute. - * @see https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#name-the-__host-prefix - */ -export type HostCookie = { strategy: "host" } & Prettify> - -/** - * Standard cookie type without security prefixes. - * Can be sent over both HTTP and HTTPS connections (default in development). - */ -export type StandardCookie = { strategy?: "standard" } & Prettify> - -/** - * Union type for cookie options based on the specified strategy. - * - `secure`: Cookies are only sent over HTTPS connections - * - `host`: Cookies use the __Host- prefix and are only sent over HTTPS connections - * - `standard`: Cookies can be sent over both HTTP and HTTPS connections (default in development) - */ -export type CookieStrategyAttributes = StandardCookie | SecureCookie | HostCookie - -/** - * Names of cookies used by Aura Auth for session management and OAuth flows. - * - `sessionToken`: User session JWT - * - `csrfToken`: CSRF protection token - * - `state`: OAuth state parameter for CSRF protection - * - `code_verifier`: PKCE code verifier for authorization code flow - * - `redirect_uri`: OAuth callback URI - * - `redirect_to`: Post-authentication redirect path - * - `nonce`: OpenID Connect nonce parameter - */ -export type CookieName = "sessionToken" | "csrfToken" | "state" | "codeVerifier" | "redirectTo" | "redirectURI" - -export type CookieStoreConfig = Record - -export interface CookieConfig { - /** - * Prefix to be added to all cookie names. By default "aura-stack". - */ - prefix?: string - overrides?: Partial -} - -/** - * Main configuration interface for Aura Auth. - * This is the user-facing configuration object passed to `createAuth()`. - */ -export interface AuthConfig { - /** - * OAuth providers available in the authentication and authorization flows. It provides a type-inference - * for the OAuth providers that are supported by Aura Stack Auth; alternatively, you can provide a custom - * OAuth third-party authorization service by implementing the `OAuthProviderCredentials` interface. - * - * Built-in OAuth providers: - * oauth: ["github", "google"] - * - * Custom credentials via factory: - * oauth: [github({ clientId: "...", clientSecret: "..." })] - * - * Custom OAuth providers: - * oauth: [ - * { - * id: "oauth-providers", - * name: "OAuth", - * authorizeURL: "https://example.com/oauth/authorize", - * accessToken: "https://example.com/oauth/token", - * scope: "profile email", - * responseType: "code", - * userInfo: "https://example.com/oauth/userinfo", - * clientId: process.env.AURA_AUTH_PROVIDER_CLIENT_ID, - * clientSecret: process.env.AURA_AUTH_PROVIDER_CLIENT_SECRET, - * } - * ] - */ - oauth: (BuiltInOAuthProvider | OAuthProviderCredentials)[] - /** - * Cookie options defines the configuration for cookies used in Aura Auth. - * It includes a prefix for cookie names and flag options to determine - * the security and scope of the cookies. - * - * **⚠️ WARNING:** Ensure that the cookie options are configured correctly to - * maintain the security and integrity of the authentication process. `Aura Auth` - * is not responsible for misconfigured cookies that may lead to security vulnerabilities. - * - * - prefix: A string prefix to be added to all cookie names, by default "aura-stack". - * - flag options (This attributes help to define the security level of the cookies): - * - secure: Cookies use the __Secure- prefix and are only sent over HTTPS connections. - * - host: Cookies use the __Host- prefix and are only sent over HTTPS connections. - * - standard: Cookies can be sent over both HTTP and HTTPS connections. (default in development) - * - * @see https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#name-the-__secure-prefix - * @see https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#name-the-__host-prefix - */ - cookies?: Partial - /** - * Secret used to sign and verify JWT tokens for session and csrf protection. - * If not provided, it will load from the environment variable `AURA_AUTH_SECRET` or `AUTH_SECRET`, but if it - * doesn't exist, it will throw an error during the initialization of the Auth module. - */ - secret?: JWTKey - /** - * Base URL of the application, used to construct the incoming request's origin. - */ - baseURL?: string - /** - * Base path for all authentication routes. Default is `/auth`. - */ - basePath?: `/${string}` - /** - * Enable trusted proxy headers for scenarios where the application is behind a reverse proxy or load balancer. - * This setting allows Aura Auth to correctly interpret headers like `X-Forwarded-For` and `X-Forwarded-Proto` - * to determine the original client IP address and protocol. - * - * Default is `false`. Enable this option only if you are certain that your application is behind a trusted proxy. - * Misconfiguration can lead to security vulnerabilities, such as incorrect handling of secure cookies or - * inaccurate client IP logging. - * - * This value can also be set via environment variable as `AURA_AUTH_TRUSTED_PROXY_HEADERS` - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded - * @experimental - */ - trustedProxyHeaders?: boolean - /** - * Logger configuration for handling authentication-related logs and errors. It can be set to `true`, - * `DEBUG=true`, `LOG_LEVEL=debug`, or a custom logger. It implements the syslog format. - */ - logger?: boolean | Logger - /** - * Defines trusted origins for your application to prevent open redirect attacks. - * URLs from the Referer header, Origin header, request URL, and redirectTo option - * are validated against this list before redirecting. - * - * - **Exact URL**: `https://example.com` matches only that origin. - * - **Subdomain wildcard**: `https://*.example.com` matches `https://app.example.com`, `https://api.example.com`, etc. - * @example - * trustedOrigins: ["https://example.com", "https://*.example.com", "http://localhost:3000"] - * - * - * trustedOrigins: async (request) => { - * const origin = new URL(request.url).origin - * return [origin, "https://admin.example.com"] - * } - */ - trustedOrigins?: TrustedOrigin[] | ((request: Request) => Promise | TrustedOrigin[]) - /** - * Defines the session management strategy for Aura Auth. It determines how sessions are created, stored, and validated. - */ - session?: SessionConfig -} - -/** - * A trusted origin URL or pattern. Supports: - * - Exact: `https://example.com` - * - Subdomain wildcard: `https://*.example.com` - */ -export type TrustedOrigin = string - -export type JoseInstance = ReturnType> - -export type OAuthProviderRecord = Record< - LiteralUnion, - OAuthProviderCredentials -> - -export type InternalLogger = { - level: LogLevel - log: typeof createLogEntry -} - -export type SessionResponse = - | { session: Session; headers: Headers; authenticated: true } - | { session: null; headers: Headers; authenticated: false } - -export type GetSessionAPI = (options: { headers: HeadersInit }) => Promise - -export type AuthAPI = ReturnType - -export interface RouterGlobalContext { - oauth: OAuthProviderRecord - cookies: CookieStoreConfig - jose: JoseInstance - secret?: JWTKey - baseURL?: string - basePath: string - trustedProxyHeaders: boolean - trustedOrigins?: TrustedOrigin[] | ((request: Request) => Promise | TrustedOrigin[]) - logger?: InternalLogger - sessionStrategy: SessionStrategy -} - -/** - * Internal runtime configuration used within Aura Auth after initialization. - * All optional fields from AuthConfig are resolved to their default values. - */ -export type AuthRuntimeConfig = RouterGlobalContext - -export interface AuthInstance { - handlers: { - GET: (request: Request) => Response | Promise - POST: (request: Request) => Response | Promise - ALL: (request: Request) => Response | Promise - } - jose: JoseInstance - api: AuthAPI -} - -/** - * Base OAuth error response structure. - */ -export interface OAuthError { - error: T - error_description?: string -} - -/** - * OAuth 2.0 Authorization Error Response Types - * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 - */ -export type AuthorizationError = OAuthError["error"]> - -/** - * OAuth 2.0 Access Token Error Response Types - * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 - */ -export type AccessTokenError = OAuthError["error"]> - -/** - * OAuth 2.0 Token Revocation Error Response Types - * @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.2.1 - */ -export type TokenRevocationError = OAuthError<"invalid_session_token"> - -export type ErrorType = AuthorizationError["error"] | AccessTokenError["error"] | TokenRevocationError["error"] - -export type AuthInternalErrorCode = - | "INVALID_OAUTH_CONFIGURATION" - | "INVALID_JWT_TOKEN" - | "JOSE_INITIALIZATION_FAILED" - | "SESSION_STORE_NOT_INITIALIZED" - | "COOKIE_STORE_NOT_INITIALIZED" - | "COOKIE_PARSING_FAILED" - | "COOKIE_NOT_FOUND" - | "INVALID_ENVIRONMENT_CONFIGURATION" - | "INVALID_URL" - | "INVALID_SALT_SECRET_VALUE" - | "UNTRUSTED_ORIGIN" - | "INVALID_OAUTH_PROVIDER_CONFIGURATION" - | "DUPLICATED_OAUTH_PROVIDER_ID" - -export type AuthSecurityErrorCode = - | "INVALID_STATE" - | "MISMATCHING_STATE" - | "POTENTIAL_OPEN_REDIRECT_ATTACK_DETECTED" - | "CSRF_TOKEN_INVALID" - | "CSRF_TOKEN_MISSING" - | "SESSION_TOKEN_MISSING" - export type OAuthEnv = z.infer -export type APIErrorMap = Record - -/** - * Log level for logger messages. - */ -export type LogLevel = "warn" | "error" | "debug" | "info" - -/** Defines the Severity between 0 to 7 */ -export type Severity = "emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug" - -/** - * @see https://datatracker.ietf.org/doc/html/rfc5424 - */ -export type SyslogOptions = { - facility: 4 | 10 - severity: Severity - timestamp?: string - hostname?: string - appName?: string - procId?: string - msgId: string - message: string - structuredData?: Record -} - -/** - * Logger function interface for structured logging. - * Called when errors or warnings occur during authentication flows. - */ -export interface Logger { - level?: LogLevel - log?: (args: SyslogOptions) => void -} - export type AuthClient = ReturnType["handlers"] export type AuthClientOptions = Prettify< Omit & { baseURL?: string } > - -export interface SignInOptions { - redirect?: boolean - redirectTo?: string -} - -export interface SignOutOptions { - redirect?: boolean - redirectTo?: string -} - -export interface GetSessionAPIOptions { - headers: HeadersInit -} - -export interface SignOutAPIOptions { - headers: HeadersInit - redirectTo?: string - skipCSRFCheck?: boolean -} - -export interface SignInAPIOptions { - headers?: HeadersInit - redirect?: Redirect - redirectTo?: string - request?: Request -} - -export type FunctionAPIContext = { - ctx: RouterGlobalContext -} & Options - -export type SignInReturn = Redirect extends true - ? Response - : { redirect: false; signInURL: string } - -export type InternalContext = RouterGlobalContext & { - cookieConfig: { - secure: CookieStoreConfig - standard: CookieStoreConfig - } -} diff --git a/packages/core/src/@types/oauth.ts b/packages/core/src/@types/oauth.ts new file mode 100644 index 00000000..663aefd4 --- /dev/null +++ b/packages/core/src/@types/oauth.ts @@ -0,0 +1,78 @@ +import type { User } from "@/@types/session.ts" +import type { LiteralUnion } from "@/@types/utility.ts" +import type { BuiltInOAuthProvider } from "@/oauth/index.ts" + +export type AuthorizeParams = LiteralUnion< + "clientId" | "prompt" | "scope" | "responseMode" | "audience" | "loginHint" | "nonce" | "display" +> + +export type ResponseType = LiteralUnion<"code" | "token" | "refresh_token" | "id_token"> + +/** + * Configuration for an OAuth provider without credentials. + * Use this type when defining provider metadata and endpoints. + */ +export interface OAuthProviderConfig, DefaultUser extends User = User> { + id: string + name: string + /** + * @deprecated + * use `authorize` instead of `authorizeURL` + */ + authorizeURL?: string + authorize: + | string + | { + url: string + params?: Partial & { responseType: ResponseType }> + } + accessToken: + | string + | { + url: string + headers?: Record + } + userInfo: + | string + | { + url: string + headers?: Record + method?: string + } + /** + * @deprecated + * use `authorize.params.scope` instead of `scope` + */ + scope?: string + /** + * @deprecated + * use `authorize.params.response_type` instead of `responseType` + */ + responseType?: ResponseType + profile?: (profile: Profile) => DefaultUser | Promise +} + +/** + * OAuth provider configuration with client credentials. + * Extends OAuthProviderConfig with clientId and clientSecret. + */ +export interface OAuthProviderCredentials< + Profile extends object = Record, + DefaultUser extends User = User, +> extends OAuthProviderConfig { + clientId?: string + clientSecret?: string +} + +/** + * Complete OAuth provider type combining configuration and credentials. + */ +export type OAuthProvider< + Profile extends object = Record, + DefaultUser extends User = User, +> = OAuthProviderCredentials + +export type OAuthProviderRecord = Record< + LiteralUnion, + OAuthProviderCredentials +> diff --git a/packages/core/src/@types/session.ts b/packages/core/src/@types/session.ts index e11d873f..52f1bfe8 100644 --- a/packages/core/src/@types/session.ts +++ b/packages/core/src/@types/session.ts @@ -1,4 +1,4 @@ -import type { JoseInstance, CookieStoreConfig, InternalLogger } from "@/@types/index.ts" +import type { CookieStoreConfig, InternalLogger, JoseInstance, RouterGlobalContext } from "@/@types/config.ts" /** * Standardized user profile returned by OAuth providers after fetching user information @@ -214,3 +214,44 @@ export interface JWTStrategyOptions { logger?: InternalLogger cookies: () => CookieStoreConfig } + +export interface SignInOptions { + redirect?: boolean + redirectTo?: string +} + +export interface SignOutOptions { + redirect?: boolean + redirectTo?: string +} + +export interface GetSessionAPIOptions { + headers: HeadersInit +} + +export interface SignOutAPIOptions { + headers: HeadersInit + redirectTo?: string + skipCSRFCheck?: boolean +} + +export interface SignInAPIOptions { + headers?: HeadersInit + redirect?: Redirect + redirectTo?: string + request?: Request +} + +export type FunctionAPIContext = { + ctx: RouterGlobalContext +} & Options + +export type SignInReturn = Redirect extends true + ? Response + : { redirect: false; signInURL: string } + +export type SessionResponse = + | { session: Session; headers: Headers; authenticated: true } + | { session: null; headers: Headers; authenticated: false } + +export type GetSessionAPI = (options: { headers: HeadersInit }) => Promise diff --git a/packages/core/src/actions/callback/access-token.ts b/packages/core/src/actions/callback/access-token.ts index 0202b31d..f66a4052 100644 --- a/packages/core/src/actions/callback/access-token.ts +++ b/packages/core/src/actions/callback/access-token.ts @@ -1,5 +1,5 @@ -import { fetchAsync } from "@/request.ts" -import { AuthInternalError, OAuthProtocolError } from "@/errors.ts" +import { fetchAsync } from "@/lib/fetch-async.ts" +import { AuthInternalError, OAuthProtocolError } from "@/lib/errors.ts" import { OAuthAccessTokenErrorResponse, OAuthAccessTokenResponse } from "@/schemas.ts" import type { InternalLogger, OAuthProviderCredentials } from "@/@types/index.ts" diff --git a/packages/core/src/actions/callback/callback.ts b/packages/core/src/actions/callback/callback.ts index 3d10042b..8328615f 100644 --- a/packages/core/src/actions/callback/callback.ts +++ b/packages/core/src/actions/callback/callback.ts @@ -1,15 +1,16 @@ import { z } from "zod/v4" import { createEndpoint, createEndpointConfig, HeadersBuilder } from "@aura-stack/router" -import { createCSRF } from "@/secure.ts" -import { cacheControl } from "@/headers.ts" -import { isRelativeURL, isSameOrigin, isTrustedOrigin, timingSafeEqual } from "@/assert.ts" +import { createCSRF } from "@/security.ts" +import { cacheControl } from "@/lib/headers.ts" +import { isRelativeURL, isSameOrigin, isTrustedOrigin } from "@/lib/assert.ts" import { getUserInfo } from "@/actions/callback/userinfo.ts" import { OAuthAuthorizationErrorResponse } from "@/schemas.ts" -import { AuthSecurityError, OAuthProtocolError } from "@/errors.ts" +import { AuthSecurityError, OAuthProtocolError } from "@/lib/errors.ts" import { getOriginURL, getTrustedOrigins } from "@/actions/signIn/authorization.ts" import { createAccessToken } from "@/actions/callback/access-token.ts" import { getCookie, expiredCookieAttributes } from "@/cookie.ts" import type { OAuthProviderRecord } from "@/@types/index.ts" +import { timingSafeEqual } from "@/lib/utils.ts" const callbackConfig = (oauth: OAuthProviderRecord) => { return createEndpointConfig("/callback/:oauth", { diff --git a/packages/core/src/actions/callback/userinfo.ts b/packages/core/src/actions/callback/userinfo.ts index 5fbce377..b6ad4a47 100644 --- a/packages/core/src/actions/callback/userinfo.ts +++ b/packages/core/src/actions/callback/userinfo.ts @@ -1,8 +1,8 @@ -import { fetchAsync } from "@/request.ts" -import { generateSecure } from "@/secure.ts" -import { AURA_AUTH_VERSION } from "@/utils.ts" +import { fetchAsync } from "@/lib/fetch-async.ts" +import { createSecretValue } from "@/security.ts" +import { AURA_AUTH_VERSION } from "@/lib/utils.ts" import { OAuthErrorResponse } from "@/schemas.ts" -import { isNativeError, isOAuthProtocolError, OAuthProtocolError } from "@/errors.ts" +import { isNativeError, isOAuthProtocolError, OAuthProtocolError } from "@/lib/errors.ts" import type { InternalLogger, OAuthProviderCredentials, User } from "@/@types/index.ts" /** @@ -12,7 +12,7 @@ import type { InternalLogger, OAuthProviderCredentials, User } from "@/@types/in * @returns The standardized OAuth user profile */ const getDefaultUserInfo = (profile: Record): User => { - const sub = generateSecure(16) + const sub = createSecretValue(16) return { sub: profile?.id ?? profile?.sub ?? sub, diff --git a/packages/core/src/actions/csrfToken/csrfToken.ts b/packages/core/src/actions/csrfToken/csrfToken.ts index 6f4e514f..c1cb5bfe 100644 --- a/packages/core/src/actions/csrfToken/csrfToken.ts +++ b/packages/core/src/actions/csrfToken/csrfToken.ts @@ -1,6 +1,6 @@ import { createEndpoint } from "@aura-stack/router" -import { createCSRF } from "@/secure.ts" -import { secureApiHeaders } from "@/headers.ts" +import { createCSRF } from "@/security.ts" +import { secureApiHeaders } from "@/lib/headers.ts" import { setCookie, getCookie } from "@/cookie.ts" const getCSRFToken = (request: Request, cookieName: string) => { diff --git a/packages/core/src/actions/session/session.ts b/packages/core/src/actions/session/session.ts index 36974b5a..c71ac3ef 100644 --- a/packages/core/src/actions/session/session.ts +++ b/packages/core/src/actions/session/session.ts @@ -1,6 +1,6 @@ import { createEndpoint, HeadersBuilder } from "@aura-stack/router" -import { secureApiHeaders } from "@/headers.ts" -import { AuthInternalError } from "@/errors.ts" +import { secureApiHeaders } from "@/lib/headers.ts" +import { AuthInternalError } from "@/lib/errors.ts" import { getSession } from "@/api/getSession.ts" import { expiredCookieAttributes } from "@/cookie.ts" diff --git a/packages/core/src/actions/signIn/authorization-url.ts b/packages/core/src/actions/signIn/authorization-url.ts index 8a9929a2..c1a83796 100644 --- a/packages/core/src/actions/signIn/authorization-url.ts +++ b/packages/core/src/actions/signIn/authorization-url.ts @@ -1,7 +1,7 @@ import { OAuthProvider } from "@/@types/index.ts" -import { AuthInternalError } from "@/errors.ts" +import { AuthInternalError } from "@/lib/errors.ts" import { OAuthAuthorization } from "@/schemas.ts" -import { createPKCE, createSecretValue } from "@/secure.ts" +import { createPKCE, createSecretValue } from "@/security.ts" import type { GlobalContext } from "@aura-stack/router" export const setSearchParams = (url: URL, params: Record) => { diff --git a/packages/core/src/actions/signIn/authorization.ts b/packages/core/src/actions/signIn/authorization.ts index d8841888..f1ad4314 100644 --- a/packages/core/src/actions/signIn/authorization.ts +++ b/packages/core/src/actions/signIn/authorization.ts @@ -1,7 +1,7 @@ import { getEnv } from "@/env.ts" -import { AuthInternalError } from "@/errors.ts" -import { equals, extractPath } from "@/utils.ts" -import { isRelativeURL, isSameOrigin, isValidURL, isTrustedOrigin, patternToRegex } from "@/assert.ts" +import { AuthInternalError } from "@/lib/errors.ts" +import { equals, extractPath, patternToRegex } from "@/lib/utils.ts" +import { isRelativeURL, isSameOrigin, isValidURL, isTrustedOrigin } from "@/lib/assert.ts" import type { AuthConfig } from "@/@types/index.ts" import type { GlobalContext } from "@aura-stack/router" diff --git a/packages/core/src/actions/signOut/signOut.ts b/packages/core/src/actions/signOut/signOut.ts index 91c81f32..2902171b 100644 --- a/packages/core/src/actions/signOut/signOut.ts +++ b/packages/core/src/actions/signOut/signOut.ts @@ -1,6 +1,6 @@ import { z } from "zod/v4" import { createEndpoint, createEndpointConfig } from "@aura-stack/router" -import { getBaseURL } from "@/utils.ts" +import { getBaseURL } from "@/lib/utils.ts" import { signOut } from "@/api/signOut.ts" import { createRedirectTo } from "@/actions/signIn/authorization.ts" diff --git a/packages/core/src/api/createApi.ts b/packages/core/src/api/createApi.ts index c2a795bb..3cb2962b 100644 --- a/packages/core/src/api/createApi.ts +++ b/packages/core/src/api/createApi.ts @@ -1,6 +1,6 @@ import { signIn } from "@/api/signIn.ts" import { signOut } from "@/api/signOut.ts" -import { validateRedirectTo } from "@/utils.ts" +import { validateRedirectTo } from "@/lib/utils.ts" import { getSession } from "@/api/getSession.ts" import type { GlobalContext } from "@aura-stack/router" import type { diff --git a/packages/core/src/api/getSession.ts b/packages/core/src/api/getSession.ts index 5fe762ff..a308e361 100644 --- a/packages/core/src/api/getSession.ts +++ b/packages/core/src/api/getSession.ts @@ -1,4 +1,4 @@ -import { getErrorName } from "@/utils.ts" +import { getErrorName } from "@/lib/utils.ts" import type { FunctionAPIContext, GetSessionAPIOptions, SessionResponse } from "@/@types/index.ts" const unauthorized: SessionResponse = { session: null, headers: new Headers(), authenticated: false } diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts new file mode 100644 index 00000000..e63f7166 --- /dev/null +++ b/packages/core/src/api/index.ts @@ -0,0 +1,4 @@ +export { createAuthAPI } from "@/api/createApi.ts" +export { signIn } from "@/api/signIn.ts" +export { signOut } from "@/api/signOut.ts" +export { getSession } from "@/api/getSession.ts" diff --git a/packages/core/src/api/signIn.ts b/packages/core/src/api/signIn.ts index 8cccf90e..b7ebdfb7 100644 --- a/packages/core/src/api/signIn.ts +++ b/packages/core/src/api/signIn.ts @@ -1,5 +1,5 @@ -import { cacheControl } from "@/headers.ts" -import { AuthInternalError } from "@/errors.ts" +import { cacheControl } from "@/lib/headers.ts" +import { AuthInternalError } from "@/lib/errors.ts" import { HeadersBuilder } from "@aura-stack/router" import { createAuthorizationURL } from "@/actions/signIn/authorization-url.ts" import { createRedirectTo, createRedirectURI, createSignInURL, getBaseURL } from "@/actions/signIn/authorization.ts" diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index 97c51061..fcc465bc 100644 --- a/packages/core/src/client/client.ts +++ b/packages/core/src/client/client.ts @@ -1,4 +1,4 @@ -import { AuthClientError, isNativeError } from "@/errors.ts" +import { AuthClientError, isNativeError } from "@/lib/errors.ts" import { createClient as createClientAPI } from "@aura-stack/router" import type { Session, diff --git a/packages/core/src/cookie.ts b/packages/core/src/cookie.ts index 3d1b2f87..50103947 100644 --- a/packages/core/src/cookie.ts +++ b/packages/core/src/cookie.ts @@ -1,5 +1,5 @@ import { env } from "@/env.ts" -import { AuthInternalError } from "@/errors.ts" +import { AuthInternalError } from "@/lib/errors.ts" import { parse, parseSetCookie, serialize, type SerializeOptions } from "@aura-stack/router/cookie" import type { CookieStoreConfig, CookieConfig, InternalLogger } from "@/@types/index.ts" diff --git a/packages/core/src/createAuth.ts b/packages/core/src/createAuth.ts index 849d1d0f..e262a132 100644 --- a/packages/core/src/createAuth.ts +++ b/packages/core/src/createAuth.ts @@ -1,7 +1,8 @@ import { createRouter, type RouterConfig } from "@aura-stack/router" -import { createContext } from "@/context.ts" +import { useSecureCookies } from "@/lib/utils.ts" import { createAuthAPI } from "@/api/createApi.ts" -import { createErrorHandler, useSecureCookies } from "@/utils.ts" +import { createContext } from "@/router/context.ts" +import { createErrorHandler } from "@/router/errorHandler.ts" import { signInAction, callbackAction, sessionAction, signOutAction, csrfTokenAction } from "@/actions/index.ts" import type { AuthConfig, AuthInstance, User } from "@/@types/index.ts" @@ -43,7 +44,7 @@ const createInternalConfig = (authConfig?: Auth * }] * }) */ -export const createAuthInstance = (authConfig: AuthConfig) => { +export const createAuthInstance = (authConfig: AuthConfig) => { const config = createInternalConfig(authConfig) const router = createRouter( [signInAction(config.context.oauth), callbackAction(config.context.oauth), sessionAction, signOutAction, csrfTokenAction], @@ -57,7 +58,7 @@ export const createAuthInstance = (authConfig: } } -export const createAuth = (config: AuthConfig) => { +export const createAuth = (config: AuthConfig) => { const authInstance = createAuthInstance(config) as unknown as AuthInstance authInstance.handlers.ALL = async (request: Request) => { const method = request.method.toUpperCase() diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0e70c053..155258de 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,6 @@ export { createAuth } from "@/createAuth.ts" -export { createSyslogMessage } from "@/logger.ts" +export { createJoseInstance } from "@/jose.ts" +export { createSyslogMessage } from "@/lib/logger.ts" export { builtInOAuthProviders } from "@/oauth/index.ts" export { createAuthClient, createClient } from "@/client/index.ts" @@ -25,4 +26,18 @@ export type { GetSessionAPIOptions, SignInAPIOptions, SignOutAPIOptions, + JWTConfig, + JWTKey, + JWTMode, + JWTSigningAlgorithm, + JWTKeyAlgorithm, + JWTEncryptedMode, + JWTSignedMode, + JWTEncryptionAlgorithm, + JWTSealedMode, + JWTConfigBase, + JWTExpirationStrategy, + StatelessStrategyConfig, + SessionConfig, + SessionStrategy, } from "@/@types/index.ts" diff --git a/packages/core/src/jose.ts b/packages/core/src/jose.ts index c2a4335e..f21cb1c4 100644 --- a/packages/core/src/jose.ts +++ b/packages/core/src/jose.ts @@ -13,27 +13,11 @@ import { type JWEHeaderParameters, type JWTDecryptOptions, } from "@aura-stack/jose" -import { AuthInternalError, AuthSecurityError } from "@/errors.ts" export { base64url, type JWTPayload } from "@aura-stack/jose/jose" +import { AuthInternalError, AuthSecurityError } from "@/lib/errors.ts" +import { isEncryptedMode, isSealedMode, isSignedMode } from "@/lib/assert.ts" export { encoder, getRandomBytes, getSubtleCrypto } from "@aura-stack/jose/crypto" -import type { User, SessionConfig, JWTMode, JWTConfig, JWTKey } from "@/@types/index.ts" - -export const isSignedMode = (config?: SessionConfig): config is { jwt: Extract } => - getJWTMode(config) === "signed" - -export const isEncryptedMode = (config?: SessionConfig): config is { jwt: Extract } => - getJWTMode(config) === "encrypted" - -export const isSealedMode = (config?: SessionConfig): config is { jwt: Extract } => - getJWTMode(config) === "sealed" - -/** - * Extracts the JWT mode from a SessionConfig. - * Defaults to "sealed" when no mode is specified. - */ -const getJWTMode = (config?: SessionConfig): JWTMode => { - return config?.jwt?.mode ?? "sealed" -} +import type { User, SessionConfig, JWTKey } from "@/@types/index.ts" const getJWTConfig = (config?: SessionConfig) => { return config?.jwt diff --git a/packages/core/src/assert.ts b/packages/core/src/lib/assert.ts similarity index 61% rename from packages/core/src/assert.ts rename to packages/core/src/lib/assert.ts index d3c8dd3f..91bb05b2 100644 --- a/packages/core/src/assert.ts +++ b/packages/core/src/lib/assert.ts @@ -1,6 +1,5 @@ -import { equals } from "@/utils.ts" -import { encoder } from "@aura-stack/jose/crypto" -import type { JWTPayloadWithToken } from "@/@types/index.ts" +import { equals, patternToRegex } from "@/lib/utils.ts" +import type { JWTConfig, JWTMode, JWTPayloadWithToken, SessionConfig } from "@/@types/index.ts" export const isFalsy = (value: unknown): boolean => { return value === false || value === 0 || value === "" || value === null || value === undefined || Number.isNaN(value) @@ -72,35 +71,6 @@ export const isSameOrigin = (origin: string, expected: string): boolean => { return equals(originURL.origin, expectedURL.origin) } -/** - * Converts a trusted origin pattern to a regex for matching. - * Supports `*` as subdomain wildcard: `https://*.example.com` matches `https://app.example.com` - * @todo: add support to Custom URI Schemes (e.g. `myapp://*`). - */ -export const patternToRegex = (pattern: string): RegExp | null => { - try { - if (pattern.length > 2048) return null - - pattern = pattern.replace(/\\/g, "") - const match = pattern.match(/^(https?):\/\/([a-zA-Z0-9.*-]{1,253})(?::(\d{1,5}|\*))?(?:\/.*)?$/) - if (!match) return null - - const [, protocol, host, port] = match - const hasWildcard = host.includes("*") - if (hasWildcard && !host.startsWith("*.")) return null - if (hasWildcard && host.slice(2).includes("*")) return null - - const domain = hasWildcard ? host.slice(2) : host - const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") - const hostRegex = hasWildcard ? `[^.]+\\.${escapedDomain}` : escapedDomain - const portRegex = port === "*" ? ":\\d{1,5}" : port ? `:${port}` : "" - - return new RegExp(`^${protocol}:\\/\\/${hostRegex}${portRegex}$`) - } catch { - return null - } -} - /** * Checks if a URL matches any of the trusted origin patterns. * A URL is trusted if its origin matches any pattern (exact or wildcard). @@ -123,13 +93,19 @@ export const isTrustedOrigin = (url: string, trustedOrigins: string[]): boolean return false } -export const timingSafeEqual = (a: string, b: string): boolean => { - const bufferA = encoder.encode(a) - const bufferB = encoder.encode(b) - const len = Math.max(bufferA.length, bufferB.length) - let diff = 0 - for (let i = 0; i < len; i++) { - diff |= (bufferA[i] ?? 0) ^ (bufferB[i] ?? 0) - } - return diff === 0 && bufferA.length === bufferB.length +/** + * Extracts the JWT mode from a SessionConfig. + * Defaults to "sealed" when no mode is specified. + */ +const getJWTMode = (config?: SessionConfig): JWTMode => { + return config?.jwt?.mode ?? "sealed" } + +export const isSignedMode = (config?: SessionConfig): config is { jwt: Extract } => + getJWTMode(config) === "signed" + +export const isEncryptedMode = (config?: SessionConfig): config is { jwt: Extract } => + getJWTMode(config) === "encrypted" + +export const isSealedMode = (config?: SessionConfig): config is { jwt: Extract } => + getJWTMode(config) === "sealed" diff --git a/packages/core/src/errors.ts b/packages/core/src/lib/errors.ts similarity index 100% rename from packages/core/src/errors.ts rename to packages/core/src/lib/errors.ts diff --git a/packages/core/src/request.ts b/packages/core/src/lib/fetch-async.ts similarity index 100% rename from packages/core/src/request.ts rename to packages/core/src/lib/fetch-async.ts diff --git a/packages/core/src/headers.ts b/packages/core/src/lib/headers.ts similarity index 100% rename from packages/core/src/headers.ts rename to packages/core/src/lib/headers.ts diff --git a/packages/core/src/logger.ts b/packages/core/src/lib/logger.ts similarity index 99% rename from packages/core/src/logger.ts rename to packages/core/src/lib/logger.ts index 448f3858..e783d19c 100644 --- a/packages/core/src/logger.ts +++ b/packages/core/src/lib/logger.ts @@ -1,5 +1,5 @@ -import { getEnv, getEnvBoolean } from "./env.ts" -import { createStructuredData } from "./utils.ts" +import { getEnv, getEnvBoolean } from "@/env.ts" +import { createStructuredData } from "@/lib/utils.ts" import type { AuthConfig, InternalLogger, Logger, LogLevel, SyslogOptions } from "@/@types/index.ts" /** diff --git a/packages/core/src/lib/utils.ts b/packages/core/src/lib/utils.ts new file mode 100644 index 00000000..80317393 --- /dev/null +++ b/packages/core/src/lib/utils.ts @@ -0,0 +1,129 @@ +import { getEnv } from "@/env.ts" +import { encoder } from "@aura-stack/jose/crypto" +import { AuthInternalError } from "@/lib/errors.ts" +import { isRelativeURL, isValidURL } from "@/lib/assert.ts" +import type { ZodError } from "zod" +import type { APIErrorMap } from "@/@types/index.ts" + +export const AURA_AUTH_VERSION = "0.4.0" + +export const equals = (a: string | number | undefined | null, b: string | number | undefined | null) => { + if (a === null || b === null || a === undefined || b === undefined) return false + return a === b +} + +export const getBaseURL = (request: Request) => { + const url = new URL(request.url) + return `${url.origin}${url.pathname}` +} + +export const toISOString = (date: Date | string | number): string => { + return new Date(date).toISOString() +} + +export const useSecureCookies = (request: Request | Headers, trustedProxyHeaders: boolean): boolean => { + const headers = request instanceof Headers ? request : request.headers + const url = request instanceof Headers ? null : request.url + return trustedProxyHeaders + ? url?.startsWith("https://") || + headers.get("X-Forwarded-Proto") === "https" || + (headers.get("Forwarded")?.includes("proto=https") ?? false) + : (url?.startsWith("https://") ?? false) +} + +export const formatZodError = = Record>(error: ZodError): APIErrorMap => { + if (!error.issues || error.issues.length === 0) { + return {} + } + return error.issues.reduce((previous, issue) => { + const key = issue.path.join(".") + return { + ...previous, + [key]: { + code: issue.code, + message: issue.message, + }, + } + }, {}) +} + +export const extractPath = (url: string): string => { + const pathRegex = /^https?:\/\/[a-zA-Z0-9_\-.]+(:\d+)?(\/.*)$/ + const match = url.match(pathRegex) + return match && match[2] ? match[2] : "/" +} + +export const createStructuredData = (data: Record, sdID = "metadata"): string => { + const entries = Object.entries(data) + if (entries.length === 0) return `[${sdID}]` + const values = entries.map(([key, value]) => `${key}="${String(value).replace(/(["\\\]])/g, "\\$1")}"`).join(" ") + return `[${sdID} ${values}]` +} + +export const getErrorName = (error: unknown): string => { + if (error instanceof Error) { + return error.name + } + return typeof error === "string" ? error : "UnknownError" +} + +export const createBasicAuthHeader = (username: string, password: string): string => { + const getUsername = getEnv(username.toUpperCase()) ?? username + const getPassword = getEnv(password.toUpperCase()) ?? password + if (!getUsername || !getPassword) { + throw new AuthInternalError("INVALID_OAUTH_CONFIGURATION", "Missing client credentials for OAuth provider configuration.") + } + const credentials = `${getUsername}:${getPassword}` + return `Basic ${btoa(credentials)}` +} + +/** + * Validates and sanitizes redirect URLs to prevent open redirect attacks. + * Only relative URLs (starting with /) are allowed; absolute URLs are + * rejected and replaced with "/" to enforce same-origin redirects. + */ +export const validateRedirectTo = (url: string): string => { + if (!isRelativeURL(url) && !isValidURL(url)) return "/" + if (isRelativeURL(url)) return url + return "/" +} + +/** + * Converts a trusted origin pattern to a regex for matching. + * Supports `*` as subdomain wildcard: `https://*.example.com` matches `https://app.example.com` + * @todo: add support to Custom URI Schemes (e.g. `myapp://*`). + */ +export const patternToRegex = (pattern: string): RegExp | null => { + try { + if (pattern.length > 2048) return null + + pattern = pattern.replace(/\\/g, "") + const match = pattern.match(/^(https?):\/\/([a-zA-Z0-9.*-]{1,253})(?::(\d{1,5}|\*))?(?:\/.*)?$/) + if (!match) return null + + const [, protocol, host, port] = match + const hasWildcard = host.includes("*") + if (hasWildcard && !host.startsWith("*.")) return null + if (hasWildcard && host.slice(2).includes("*")) return null + + const domain = hasWildcard ? host.slice(2) : host + const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + const hostRegex = hasWildcard ? `[^.]+\\.${escapedDomain}` : escapedDomain + const portRegex = port === "*" ? ":\\d{1,5}" : port ? `:${port}` : "" + + return new RegExp(`^${protocol}:\\/\\/${hostRegex}${portRegex}$`) + } catch { + return null + } +} + +export const timingSafeEqual = (a: string, b: string): boolean => { + const bufferA = encoder.encode(a) + const bufferB = encoder.encode(b) + const len = Math.max(bufferA.length, bufferB.length) + let diff = 0 + for (let i = 0; i < len; i++) { + diff |= (bufferA[i] ?? 0) ^ (bufferB[i] ?? 0) + } + return diff === 0 && bufferA.length === bufferB.length +} diff --git a/packages/core/src/oauth/gitlab.ts b/packages/core/src/oauth/gitlab.ts index 614aaf60..6d51c165 100644 --- a/packages/core/src/oauth/gitlab.ts +++ b/packages/core/src/oauth/gitlab.ts @@ -81,7 +81,7 @@ export const gitlab = ( sub: profile.id.toString(), name: profile.name ?? profile.username, email: profile.email, - image: profile.avatar_url, + image: profile.avatar_url, }) as DefaultUser, ...options, } diff --git a/packages/core/src/oauth/index.ts b/packages/core/src/oauth/index.ts index 870e799f..878cf988 100644 --- a/packages/core/src/oauth/index.ts +++ b/packages/core/src/oauth/index.ts @@ -20,8 +20,8 @@ import { notion } from "./notion.ts" import { dropbox } from "./dropbox.ts" import { atlassian } from "./atlassian.ts" import { OAuthEnvSchema, OAuthProviderCredentialsSchema } from "@/schemas.ts" -import { AuthInternalError } from "@/errors.ts" -import { formatZodError } from "@/utils.ts" +import { AuthInternalError } from "@/lib/errors.ts" +import { formatZodError } from "@/lib/utils.ts" export * from "./github.ts" export * from "./bitbucket.ts" diff --git a/packages/core/src/oauth/notion.ts b/packages/core/src/oauth/notion.ts index 876740b9..76da05a1 100644 --- a/packages/core/src/oauth/notion.ts +++ b/packages/core/src/oauth/notion.ts @@ -1,4 +1,4 @@ -import { createBasicAuthHeader } from "@/utils.ts" +import { createBasicAuthHeader } from "@/lib/utils.ts" import type { OAuthProviderCredentials, User } from "@/@types/index.ts" export interface Person { diff --git a/packages/core/src/context.ts b/packages/core/src/router/context.ts similarity index 94% rename from packages/core/src/context.ts rename to packages/core/src/router/context.ts index 75cf7f3d..3fef82c3 100644 --- a/packages/core/src/context.ts +++ b/packages/core/src/router/context.ts @@ -1,5 +1,5 @@ import { createJoseInstance } from "@/jose.ts" -import { createProxyLogger } from "@/logger.ts" +import { createProxyLogger } from "@/lib/logger.ts" import { createCookieStore } from "@/cookie.ts" import { createSessionStrategy } from "@/session/index.ts" import { getEnv, getEnvArray, getEnvBoolean } from "@/env.ts" @@ -34,6 +34,6 @@ export const createContext = (config?: AuthConf jose, config: config?.session, logger: ctx.logger, - }) + }) as InternalContext["sessionStrategy"] return ctx } diff --git a/packages/core/src/router/errorHandler.ts b/packages/core/src/router/errorHandler.ts new file mode 100644 index 00000000..c69cf183 --- /dev/null +++ b/packages/core/src/router/errorHandler.ts @@ -0,0 +1,72 @@ +import { isAuthInternalError, isAuthSecurityError, isOAuthProtocolError } from "@/lib/errors.ts" +import { isInvalidZodSchemaError, isRouterError, type RouterConfig } from "@aura-stack/router" +import type { InternalLogger } from "@/@types/index.ts" + +export const createErrorHandler = (logger?: InternalLogger): RouterConfig["onError"] => { + return (error) => { + if (isRouterError(error)) { + const { message, status, statusText } = error + logger?.log("ROUTER_INTERNAL_ERROR") + return Response.json({ type: "ROUTER_ERROR", code: "ROUTER_INTERNAL_ERROR", message }, { status, statusText }) + } + if (isInvalidZodSchemaError(error)) { + logger?.log("INVALID_REQUEST") + return Response.json({ type: "ROUTER_ERROR", code: "INVALID_REQUEST", message: error.errors }, { status: 422 }) + } + if (isOAuthProtocolError(error)) { + const { error: errorCode, message, type, errorURI } = error + logger?.log("OAUTH_PROTOCOL_ERROR", { + structuredData: { + error: errorCode, + error_description: message, + error_uri: errorURI ?? "", + }, + }) + return Response.json( + { + type, + message: message, + }, + { status: 400 } + ) + } + if (isAuthInternalError(error)) { + const { type, code, message } = error + logger?.log("INVALID_OAUTH_CONFIGURATION", { + structuredData: { + error: code, + error_description: message, + }, + }) + return Response.json( + { + type, + message, + }, + { status: 400 } + ) + } + if (isAuthSecurityError(error)) { + const { type, code, message } = error + logger?.log("INVALID_OAUTH_CONFIGURATION", { + structuredData: { + error: code, + error_description: message, + }, + }) + return Response.json( + { + type, + code, + message, + }, + { status: 400 } + ) + } + logger?.log("SERVER_ERROR") + return Response.json( + { type: "SERVER_ERROR", code: "SERVER_ERROR", message: "An unexpected error occurred" }, + { status: 500 } + ) + } +} diff --git a/packages/core/src/schemas.ts b/packages/core/src/schemas.ts index 28f78d65..8305fffc 100644 --- a/packages/core/src/schemas.ts +++ b/packages/core/src/schemas.ts @@ -1,7 +1,7 @@ -import { object, string, enum as options, number, z, null as nullable, union, array } from "zod/v4" +import { object, string, enum as options, number, z, null as nullable, union, array, url } from "zod/v4" const AuthorizeConfigSchema = z.union([ - string().url(), + url(), object({ url: string().url(), params: object({ @@ -12,17 +12,17 @@ const AuthorizeConfigSchema = z.union([ ]) const AccessTokenConfigSchema = z.union([ - string().url(), + url(), object({ - url: string().url(), + url: url(), headers: z.record(string(), string()).optional(), }), ]) const UserInfoConfigSchema = z.union([ - string().url(), + url(), object({ - url: string().url(), + url: url(), headers: z.record(string(), string()).optional(), method: string().optional(), }), @@ -33,7 +33,7 @@ export const OAuthProviderCredentialsSchema = object({ name: string(), authorize: AuthorizeConfigSchema.optional(), /** @deprecated */ - authorizeURL: string().url().optional(), + authorizeURL: url().optional(), accessToken: AccessTokenConfigSchema, /** @deprecated */ scope: string().optional(), @@ -51,7 +51,7 @@ export const OAuthProviderCredentialsSchema = object({ export const OAuthProviderConfigSchema = object({ authorize: AuthorizeConfigSchema.optional(), /** @deprecated */ - authorizeURL: string().url().optional(), + authorizeURL: url().optional(), accessToken: AccessTokenConfigSchema, /** @deprecated */ scope: string().optional(), diff --git a/packages/core/src/secure.ts b/packages/core/src/security.ts similarity index 87% rename from packages/core/src/secure.ts rename to packages/core/src/security.ts index 13466fe7..85464d68 100644 --- a/packages/core/src/secure.ts +++ b/packages/core/src/security.ts @@ -1,14 +1,9 @@ -import { equals } from "@/utils.ts" -import { AuthSecurityError } from "@/errors.ts" -import { isJWTPayloadWithToken, timingSafeEqual } from "@/assert.ts" +import { AuthSecurityError } from "@/lib/errors.ts" +import { isJWTPayloadWithToken } from "@/lib/assert.ts" +import { equals, timingSafeEqual } from "@/lib/utils.ts" import { base64url, encoder, getRandomBytes, getSubtleCrypto } from "@/jose.ts" import type { AuthRuntimeConfig, JoseInstance, User } from "@/@types/index.ts" -/** @deprecated use `createSecretValue` instead */ -export const generateSecure = (length: number = 32) => { - return base64url.encode(getRandomBytes(length)) -} - export const createSecretValue = (length: number = 32) => { return base64url.encode(getRandomBytes(length)) } @@ -31,7 +26,7 @@ export const createHash = async (data: string) => { export const createPKCE = async (verifier?: string) => { // Base64url: n bytes → ceil(n * 4/3) chars. For 43-128 chars, need 32-96 bytes. const byteLength = verifier ? undefined : Math.floor(Math.random() * (96 - 32 + 1) + 32) - const codeVerifier = verifier ?? generateSecure(byteLength ?? 64) + const codeVerifier = verifier ?? createSecretValue(byteLength ?? 64) if (codeVerifier.length < 43 || codeVerifier.length > 128) { throw new AuthSecurityError("PKCE_VERIFIER_INVALID", "The code verifier must be between 43 and 128 characters in length.") } @@ -47,14 +42,14 @@ export const createPKCE = async (verifier?: string) => { */ export const createCSRF = async (jose: AuthRuntimeConfig["jose"], csrfCookie?: string) => { try { - const token = generateSecure(32) + const token = createSecretValue(32) if (csrfCookie) { await jose.verifyJWS(csrfCookie) return csrfCookie } return jose.signJWS({ token }) } catch { - const token = generateSecure(32) + const token = createSecretValue(32) return jose.signJWS({ token }) } } diff --git a/packages/core/src/session/index.ts b/packages/core/src/session/index.ts index 3f16e11b..c469deb7 100644 --- a/packages/core/src/session/index.ts +++ b/packages/core/src/session/index.ts @@ -1,4 +1,4 @@ -import { AuthInvalidConfigurationError } from "@/errors.ts" +import { AuthInvalidConfigurationError } from "@/lib/errors.ts" import { createStatelessStrategy } from "@/session/strategies/stateless.ts" import type { CreateSessionStrategyOptions, SessionStrategy, User } from "@/@types/session.ts" diff --git a/packages/core/src/session/manager/cookie.ts b/packages/core/src/session/manager/cookie.ts index 4d39d62a..3912ac0f 100644 --- a/packages/core/src/session/manager/cookie.ts +++ b/packages/core/src/session/manager/cookie.ts @@ -1,5 +1,5 @@ import { HeadersBuilder } from "@aura-stack/router" -import { secureApiHeaders } from "@/headers.ts" +import { secureApiHeaders } from "@/lib/headers.ts" import { expiredCookieAttributes, getCookie as getCookieByName } from "@/cookie.ts" import type { CookieStoreConfig } from "@/@types/index.ts" diff --git a/packages/core/src/session/manager/jose.ts b/packages/core/src/session/manager/jose.ts index c78b7d9d..b3ae48ce 100644 --- a/packages/core/src/session/manager/jose.ts +++ b/packages/core/src/session/manager/jose.ts @@ -1,4 +1,4 @@ -import { AuthInvalidConfigurationError } from "@/errors.ts" +import { AuthInvalidConfigurationError } from "@/lib/errors.ts" import type { TypedJWTPayload } from "@aura-stack/jose" import type { JoseInstance, User, JWTConfig } from "@/@types/index.ts" diff --git a/packages/core/src/session/strategies/stateless.ts b/packages/core/src/session/strategies/stateless.ts index 3193ab77..412d35a9 100644 --- a/packages/core/src/session/strategies/stateless.ts +++ b/packages/core/src/session/strategies/stateless.ts @@ -1,7 +1,7 @@ import { getCookie } from "@/cookie.ts" -import { verifyCSRF } from "@/secure.ts" -import { getErrorName } from "@/utils.ts" -import { AuthSecurityError } from "@/errors.ts" +import { verifyCSRF } from "@/security.ts" +import { getErrorName } from "@/lib/utils.ts" +import { AuthSecurityError } from "@/lib/errors.ts" import { createJoseManager } from "@/session/manager/jose.ts" import { createCookieManager } from "@/session/manager/cookie.ts" import type { Session, SessionStrategy, User, TypedJWTPayload, JWTStrategyOptions, GetSessionReturn } from "@/@types/index.ts" diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts deleted file mode 100644 index 25fe9735..00000000 --- a/packages/core/src/utils.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { isInvalidZodSchemaError, isRouterError, RouterConfig } from "@aura-stack/router" -import { AuthInternalError, isAuthInternalError, isAuthSecurityError, isOAuthProtocolError } from "@/errors.ts" -import type { ZodError } from "zod" -import type { APIErrorMap, InternalLogger } from "@/@types/index.ts" -import { getEnv } from "./env.ts" -import { isRelativeURL, isValidURL } from "./assert.ts" - -export const AURA_AUTH_VERSION = "0.4.0" - -export const toSnakeCase = (str: string) => { - return str - .replace(/([a-z0-9])([A-Z])/g, "$1_$2") - .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") - .toLowerCase() - .replace(/^_+/, "") -} - -export const toUpperCase = (str: string) => { - return str.toUpperCase() -} - -export const toCastCase = , Type extends "snake" | "upper">( - obj: Obj, - type: Type = "snake" as Type -) => { - return Object.entries(obj).reduce((previous, [key, value]) => { - const newKey = type === "snake" ? toSnakeCase(key) : toUpperCase(key) - return { ...previous, [newKey]: value } - }, {}) as Type extends "snake" - ? { [K in keyof Obj as `${string & K}`]: Obj[K] } - : { [K in keyof Obj as Uppercase]: Obj[K] } -} - -export const equals = (a: string | number | undefined | null, b: string | number | undefined | null) => { - if (a === null || b === null || a === undefined || b === undefined) return false - return a === b -} - -export const createErrorHandler = (logger?: InternalLogger): RouterConfig["onError"] => { - return (error) => { - if (isRouterError(error)) { - const { message, status, statusText } = error - logger?.log("ROUTER_INTERNAL_ERROR") - return Response.json({ type: "ROUTER_ERROR", code: "ROUTER_INTERNAL_ERROR", message }, { status, statusText }) - } - if (isInvalidZodSchemaError(error)) { - logger?.log("INVALID_REQUEST") - return Response.json({ type: "ROUTER_ERROR", code: "INVALID_REQUEST", message: error.errors }, { status: 422 }) - } - if (isOAuthProtocolError(error)) { - const { error: errorCode, message, type, errorURI } = error - logger?.log("OAUTH_PROTOCOL_ERROR", { - structuredData: { - error: errorCode, - error_description: message, - error_uri: errorURI ?? "", - }, - }) - return Response.json( - { - type, - message: message, - }, - { status: 400 } - ) - } - if (isAuthInternalError(error)) { - const { type, code, message } = error - logger?.log("INVALID_OAUTH_CONFIGURATION", { - structuredData: { - error: code, - error_description: message, - }, - }) - return Response.json( - { - type, - message, - }, - { status: 400 } - ) - } - if (isAuthSecurityError(error)) { - const { type, code, message } = error - logger?.log("INVALID_OAUTH_CONFIGURATION", { - structuredData: { - error: code, - error_description: message, - }, - }) - return Response.json( - { - type, - code, - message, - }, - { status: 400 } - ) - } - logger?.log("SERVER_ERROR") - return Response.json( - { type: "SERVER_ERROR", code: "SERVER_ERROR", message: "An unexpected error occurred" }, - { status: 500 } - ) - } -} - -export const getBaseURL = (request: Request) => { - const url = new URL(request.url) - return `${url.origin}${url.pathname}` -} - -export const toISOString = (date: Date | string | number): string => { - return new Date(date).toISOString() -} - -export const useSecureCookies = (request: Request | Headers, trustedProxyHeaders: boolean): boolean => { - const headers = request instanceof Headers ? request : request.headers - const url = request instanceof Headers ? null : request.url - return trustedProxyHeaders - ? url?.startsWith("https://") || - headers.get("X-Forwarded-Proto") === "https" || - (headers.get("Forwarded")?.includes("proto=https") ?? false) - : (url?.startsWith("https://") ?? false) -} - -export const formatZodError = = Record>(error: ZodError): APIErrorMap => { - if (!error.issues || error.issues.length === 0) { - return {} - } - return error.issues.reduce((previous, issue) => { - const key = issue.path.join(".") - return { - ...previous, - [key]: { - code: issue.code, - message: issue.message, - }, - } - }, {}) -} - -export const extractPath = (url: string): string => { - const pathRegex = /^https?:\/\/[a-zA-Z0-9_\-.]+(:\d+)?(\/.*)$/ - const match = url.match(pathRegex) - return match && match[2] ? match[2] : "/" -} - -export const createStructuredData = (data: Record, sdID = "metadata"): string => { - const entries = Object.entries(data) - if (entries.length === 0) return `[${sdID}]` - const values = entries.map(([key, value]) => `${key}="${String(value).replace(/(["\\\]])/g, "\\$1")}"`).join(" ") - return `[${sdID} ${values}]` -} - -export const getErrorName = (error: unknown): string => { - if (error instanceof Error) { - return error.name - } - return typeof error === "string" ? error : "UnknownError" -} - -export const createBasicAuthHeader = (username: string, password: string): string => { - const getUsername = getEnv(username.toUpperCase()) ?? username - const getPassword = getEnv(password.toUpperCase()) ?? password - if (!getUsername || !getPassword) { - throw new AuthInternalError("INVALID_OAUTH_CONFIGURATION", "Missing client credentials for OAuth provider configuration.") - } - const credentials = `${getUsername}:${getPassword}` - return `Basic ${btoa(credentials)}` -} - -/** - * Validates and sanitizes redirect URLs to prevent open redirect attacks. - * Only relative URLs (starting with /) are allowed; absolute URLs are - * rejected and replaced with "/" to enforce same-origin redirects. - */ -export const validateRedirectTo = (url: string): string => { - if (!isRelativeURL(url) && !isValidURL(url)) return "/" - if (isRelativeURL(url)) return url - return "/" -} diff --git a/packages/core/test/actions/callback/access-token.test.ts b/packages/core/test/actions/callback/access-token.test.ts index 349f9bbf..f7b6f8a5 100644 --- a/packages/core/test/actions/callback/access-token.test.ts +++ b/packages/core/test/actions/callback/access-token.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, vi } from "vitest" -import { createPKCE } from "@/secure.ts" +import { createPKCE } from "@/security.ts" import { oauthCustomService } from "@test/presets.ts" import { createAccessToken } from "@/actions/callback/access-token.ts" diff --git a/packages/core/test/actions/callback/callback.test.ts b/packages/core/test/actions/callback/callback.test.ts index 28b9e895..9b566fa4 100644 --- a/packages/core/test/actions/callback/callback.test.ts +++ b/packages/core/test/actions/callback/callback.test.ts @@ -1,8 +1,8 @@ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest" import { GET } from "@test/presets.ts" -import { createPKCE } from "@/secure.ts" +import { createPKCE } from "@/security.ts" import { setCookie, getSetCookie } from "@/cookie.ts" -import { AURA_AUTH_VERSION } from "@/utils.ts" +import { AURA_AUTH_VERSION } from "@/lib/utils.ts" beforeEach(() => { vi.stubEnv("BASE_URL", undefined) diff --git a/packages/core/test/actions/callback/userinfo.test.ts b/packages/core/test/actions/callback/userinfo.test.ts index 9f49ae25..37011dda 100644 --- a/packages/core/test/actions/callback/userinfo.test.ts +++ b/packages/core/test/actions/callback/userinfo.test.ts @@ -2,7 +2,7 @@ import { describe, test, expect, vi } from "vitest" import { getUserInfo } from "@/actions/callback/userinfo.ts" import { OAuthProviderConfig, OAuthProviderCredentials } from "@/@types/index.ts" import { oauthCustomService } from "@test/presets.ts" -import { AURA_AUTH_VERSION } from "@/utils.ts" +import { AURA_AUTH_VERSION } from "@/lib/utils.ts" describe("getUserInfo", () => { test("get user info", async () => { diff --git a/packages/core/test/actions/session/session.test.ts b/packages/core/test/actions/session/session.test.ts index 7cf183d9..b15fd3ea 100644 --- a/packages/core/test/actions/session/session.test.ts +++ b/packages/core/test/actions/session/session.test.ts @@ -1,8 +1,8 @@ import { describe, test, expect, vi, afterEach, beforeEach } from "vitest" -import { createPKCE } from "@/secure.ts" +import { createPKCE } from "@/security.ts" import { GET, jose, sessionPayload } from "@test/presets.ts" import { setCookie, getSetCookie, createCookieStore } from "@/cookie.ts" -import { AURA_AUTH_VERSION } from "@/utils.ts" +import { AURA_AUTH_VERSION } from "@/lib/utils.ts" beforeEach(() => { vi.stubEnv("BASE_URL", undefined) diff --git a/packages/core/test/actions/signOut/signOut.test.ts b/packages/core/test/actions/signOut/signOut.test.ts index 34795e66..bebc7b73 100644 --- a/packages/core/test/actions/signOut/signOut.test.ts +++ b/packages/core/test/actions/signOut/signOut.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest" -import { createCSRF } from "@/secure.ts" +import { createCSRF } from "@/security.ts" import { POST, jose, sessionPayload } from "@test/presets.ts" beforeEach(() => { diff --git a/packages/core/test/assert.test.ts b/packages/core/test/assert.test.ts index 7fabfc38..959522b0 100644 --- a/packages/core/test/assert.test.ts +++ b/packages/core/test/assert.test.ts @@ -1,4 +1,4 @@ -import { isRelativeURL, isValidURL, isTrustedOrigin } from "@/assert.ts" +import { isRelativeURL, isValidURL, isTrustedOrigin } from "@/lib/assert.ts" import { describe, test, expect } from "vitest" describe("isRelativeURL", () => { diff --git a/packages/core/test/context.test.ts b/packages/core/test/context.test.ts index d9a87cc0..8e3e60f6 100644 --- a/packages/core/test/context.test.ts +++ b/packages/core/test/context.test.ts @@ -1,5 +1,5 @@ import { describe, expect, vi, test } from "vitest" -import { createProxyLogger } from "@/logger.ts" +import { createProxyLogger } from "@/lib/logger.ts" import type { Logger } from "@/@types/index.ts" describe("createProxyLogger", () => { diff --git a/packages/core/test/env.test.ts b/packages/core/test/env.test.ts index 44b1d560..c2eeb6a7 100644 --- a/packages/core/test/env.test.ts +++ b/packages/core/test/env.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, test, vi } from "vitest" import { env } from "@/env.ts" -import { generateSecure } from "@/secure.ts" +import { createSecretValue } from "@/security.ts" describe("env", () => { afterEach(() => { @@ -8,21 +8,21 @@ describe("env", () => { }) test("read AURA_AUTH_SECRET from environment", () => { - const secret = generateSecure() + const secret = createSecretValue() vi.stubEnv("AURA_AUTH_SECRET", secret!) expect(env.AURA_AUTH_SECRET).toBe(secret) }) test("read multiple secrets from environment", () => { - const secret1 = generateSecure() - const secret2 = generateSecure() + const secret1 = createSecretValue() + const secret2 = createSecretValue() vi.stubEnv("AURA_AUTH_SECRET", `${secret1},${secret2}`) expect(env.AURA_AUTH_SECRET).toBe(`${secret1},${secret2}`) }) test("last stubbed value overwrites previous values", () => { - const secret1 = generateSecure() - const secret2 = generateSecure() + const secret1 = createSecretValue() + const secret2 = createSecretValue() vi.stubEnv("AURA_AUTH_SECRET", secret1) vi.stubEnv("AURA_AUTH_SECRET", secret2) expect(env.AURA_AUTH_SECRET).toBe(secret2) diff --git a/packages/core/test/jose.test.ts b/packages/core/test/jose.test.ts index 6c5d8e37..76ceca8e 100644 --- a/packages/core/test/jose.test.ts +++ b/packages/core/test/jose.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest" import { createJoseInstance } from "@/jose.ts" -import { createSecretValue } from "@/secure.ts" +import { createSecretValue } from "@/security.ts" const payload = { sub: "1234567890", diff --git a/packages/core/test/request.test.ts b/packages/core/test/request.test.ts index bbd1c8f0..d4afd325 100644 --- a/packages/core/test/request.test.ts +++ b/packages/core/test/request.test.ts @@ -1,4 +1,4 @@ -import { fetchAsync } from "@/request.ts" +import { fetchAsync } from "@/lib/fetch-async.ts" import { describe, expect, test, vi } from "vitest" describe("fetchAsync", () => { diff --git a/packages/core/test/secure.test.ts b/packages/core/test/secure.test.ts index b0b567b5..39cb766a 100644 --- a/packages/core/test/secure.test.ts +++ b/packages/core/test/secure.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "vitest" -import { createPKCE } from "@/secure.ts" +import { createPKCE } from "@/security.ts" describe("createPKCE", () => { test("generates a valid code verifier and code challenge", async () => { diff --git a/packages/core/test/utils.test.ts b/packages/core/test/utils.test.ts deleted file mode 100644 index 032f960a..00000000 --- a/packages/core/test/utils.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { describe, expect, test } from "vitest" -import { toCastCase, toSnakeCase } from "@/utils.ts" -import { OAuthAuthorizationErrorResponse } from "@/schemas.ts" - -describe("toSnakeCase", () => { - const testCases = [ - { - description: "converts camelCase to snake_case", - input: "camelCaseString", - expected: "camel_case_string", - }, - { - description: "converts PascalCase to snake_case", - input: "PascalCaseString", - expected: "pascal_case_string", - }, - { - description: "handles acronyms correctly", - input: "HTTPServerError", - expected: "http_server_error", - }, - { - description: "handles leading underscores", - input: "_LeadingUnderscore", - expected: "leading_underscore", - }, - { - description: "handles mixed case with numbers", - input: "version2UpdateAvailable", - expected: "version2_update_available", - }, - { - description: "returns empty string when input is empty", - input: "", - expected: "", - }, - { - description: "handles redirectURI", - input: "redirectURI", - expected: "redirect_uri", - }, - ] - - for (const { description, input, expected } of testCases) { - test(description, () => { - expect(toSnakeCase(input)).toBe(expected) - }) - } -}) - -describe("toUpperCase", () => { - test("converts string to uppercase", () => { - const entries = toCastCase(OAuthAuthorizationErrorResponse.shape.error.enum, "upper") - expect(entries).toEqual({ - INVALID_REQUEST: "invalid_request", - UNAUTHORIZED_CLIENT: "unauthorized_client", - ACCESS_DENIED: "access_denied", - UNSUPPORTED_RESPONSE_TYPE: "unsupported_response_type", - INVALID_SCOPE: "invalid_scope", - SERVER_ERROR: "server_error", - TEMPORARILY_UNAVAILABLE: "temporarily_unavailable", - }) - }) -}) From e9f6443e40115aee1d8cf7af21d0a7e713fb1db6 Mon Sep 17 00:00:00 2001 From: Hernan Alvarado Date: Wed, 25 Mar 2026 10:35:03 -0500 Subject: [PATCH 2/2] chore: rename from `lib` to `shared` --- packages/core/src/@types/config.ts | 4 +-- packages/core/src/@types/oauth.ts | 2 +- packages/core/src/@types/session.ts | 19 ++++++++---- .../core/src/actions/callback/access-token.ts | 4 +-- .../core/src/actions/callback/callback.ts | 10 +++---- .../core/src/actions/callback/userinfo.ts | 8 ++--- .../core/src/actions/csrfToken/csrfToken.ts | 4 +-- packages/core/src/actions/session/session.ts | 4 +-- .../src/actions/signIn/authorization-url.ts | 4 +-- .../core/src/actions/signIn/authorization.ts | 8 ++--- packages/core/src/actions/signOut/signOut.ts | 2 +- packages/core/src/api/createApi.ts | 2 +- packages/core/src/api/getSession.ts | 2 +- packages/core/src/api/signIn.ts | 4 +-- packages/core/src/client/client.ts | 2 +- packages/core/src/cookie.ts | 4 +-- packages/core/src/createAuth.ts | 6 ++-- packages/core/src/index.ts | 2 +- packages/core/src/jose.ts | 6 ++-- packages/core/src/oauth/index.ts | 16 ++++++++-- packages/core/src/oauth/notion.ts | 2 +- packages/core/src/oauth/twitch.ts | 2 +- packages/core/src/router/context.ts | 8 ++--- packages/core/src/router/errorHandler.ts | 6 ++-- packages/core/src/schemas.ts | 16 +++++----- .../{manager/cookie.ts => cookie-manager.ts} | 2 +- packages/core/src/session/index.ts | 26 ++--------------- .../{manager/jose.ts => jose-manager.ts} | 10 ++----- .../src/session/{strategies => }/stateless.ts | 10 +++---- packages/core/src/session/strategy.ts | 24 +++++++++++++++ packages/core/src/{lib => shared}/assert.ts | 2 +- packages/core/src/{ => shared}/env.ts | 0 packages/core/src/{lib => shared}/errors.ts | 0 .../core/src/{lib => shared}/fetch-async.ts | 0 packages/core/src/{lib => shared}/headers.ts | 0 packages/core/src/{lib => shared}/logger.ts | 17 +++++++++-- packages/core/src/{ => shared}/security.ts | 8 ++--- packages/core/src/{lib => shared}/utils.ts | 29 ++----------------- .../actions/callback/access-token.test.ts | 2 +- .../test/actions/callback/callback.test.ts | 4 +-- .../test/actions/callback/userinfo.test.ts | 2 +- .../core/test/actions/session/session.test.ts | 4 +-- .../core/test/actions/signOut/signOut.test.ts | 2 +- packages/core/test/assert.test.ts | 2 +- packages/core/test/context.test.ts | 2 +- packages/core/test/env.test.ts | 4 +-- packages/core/test/jose.test.ts | 2 +- packages/core/test/request.test.ts | 2 +- packages/core/test/secure.test.ts | 2 +- 49 files changed, 152 insertions(+), 151 deletions(-) rename packages/core/src/session/{manager/cookie.ts => cookie-manager.ts} (95%) rename packages/core/src/session/{manager/jose.ts => jose-manager.ts} (63%) rename packages/core/src/session/{strategies => }/stateless.ts (95%) create mode 100644 packages/core/src/session/strategy.ts rename packages/core/src/{lib => shared}/assert.ts (98%) rename packages/core/src/{ => shared}/env.ts (100%) rename packages/core/src/{lib => shared}/errors.ts (100%) rename packages/core/src/{lib => shared}/fetch-async.ts (100%) rename packages/core/src/{lib => shared}/headers.ts (100%) rename packages/core/src/{lib => shared}/logger.ts (95%) rename packages/core/src/{ => shared}/security.ts (95%) rename packages/core/src/{lib => shared}/utils.ts (74%) diff --git a/packages/core/src/@types/config.ts b/packages/core/src/@types/config.ts index 34b417e7..9409b2fb 100644 --- a/packages/core/src/@types/config.ts +++ b/packages/core/src/@types/config.ts @@ -1,5 +1,5 @@ import { createJoseInstance } from "@/jose.ts" -import { createLogEntry } from "@/lib/logger.ts" +import { createLogEntry } from "@/shared/logger.ts" import { createAuthAPI } from "@/api/createApi.ts" import type { Prettify } from "@/@types/utility.ts" import type { BuiltInOAuthProvider } from "@/oauth/index.ts" @@ -229,7 +229,7 @@ export interface RouterGlobalContext { * Internal runtime configuration used within Aura Auth after initialization. * All optional fields from AuthConfig are resolved to their default values. */ -export type AuthRuntimeConfig = RouterGlobalContext +export type AuthRuntimeConfig = RouterGlobalContext export interface AuthInstance { api: AuthAPI diff --git a/packages/core/src/@types/oauth.ts b/packages/core/src/@types/oauth.ts index 663aefd4..f37401fa 100644 --- a/packages/core/src/@types/oauth.ts +++ b/packages/core/src/@types/oauth.ts @@ -46,7 +46,7 @@ export interface OAuthProviderConfig DefaultUser | Promise diff --git a/packages/core/src/@types/session.ts b/packages/core/src/@types/session.ts index 52f1bfe8..e42f621f 100644 --- a/packages/core/src/@types/session.ts +++ b/packages/core/src/@types/session.ts @@ -1,3 +1,4 @@ +import type { TypedJWTPayload } from "@aura-stack/jose" import type { CookieStoreConfig, InternalLogger, JoseInstance, RouterGlobalContext } from "@/@types/config.ts" /** @@ -163,11 +164,9 @@ export type StatelessStrategyConfig = { */ export type SessionConfig = StatelessStrategyConfig -export interface GetSessionReturn { - session: Session | null - headers: Headers -} - +/** + * Abstraction layer for session management. + */ export interface SessionStrategy { /** * Read and validate the session from an incoming request. @@ -201,6 +200,11 @@ export interface SessionStrategy { destroySession(request: Headers, skipCSRFCheck?: boolean): Promise } +export interface GetSessionReturn { + session: Session | null + headers: Headers +} + export interface CreateSessionStrategyOptions { config?: SessionConfig jose: JoseInstance @@ -254,4 +258,7 @@ export type SessionResponse = | { session: Session; headers: Headers; authenticated: true } | { session: null; headers: Headers; authenticated: false } -export type GetSessionAPI = (options: { headers: HeadersInit }) => Promise +export type JWTManager = { + createToken(user: TypedJWTPayload>): Promise + verifyToken(token: string): Promise> +} diff --git a/packages/core/src/actions/callback/access-token.ts b/packages/core/src/actions/callback/access-token.ts index f66a4052..769c1244 100644 --- a/packages/core/src/actions/callback/access-token.ts +++ b/packages/core/src/actions/callback/access-token.ts @@ -1,5 +1,5 @@ -import { fetchAsync } from "@/lib/fetch-async.ts" -import { AuthInternalError, OAuthProtocolError } from "@/lib/errors.ts" +import { fetchAsync } from "@/shared/fetch-async.ts" +import { AuthInternalError, OAuthProtocolError } from "@/shared/errors.ts" import { OAuthAccessTokenErrorResponse, OAuthAccessTokenResponse } from "@/schemas.ts" import type { InternalLogger, OAuthProviderCredentials } from "@/@types/index.ts" diff --git a/packages/core/src/actions/callback/callback.ts b/packages/core/src/actions/callback/callback.ts index 8328615f..dab8a46d 100644 --- a/packages/core/src/actions/callback/callback.ts +++ b/packages/core/src/actions/callback/callback.ts @@ -1,16 +1,16 @@ import { z } from "zod/v4" import { createEndpoint, createEndpointConfig, HeadersBuilder } from "@aura-stack/router" -import { createCSRF } from "@/security.ts" -import { cacheControl } from "@/lib/headers.ts" -import { isRelativeURL, isSameOrigin, isTrustedOrigin } from "@/lib/assert.ts" +import { createCSRF } from "@/shared/security.ts" +import { cacheControl } from "@/shared/headers.ts" +import { isRelativeURL, isSameOrigin, isTrustedOrigin } from "@/shared/assert.ts" import { getUserInfo } from "@/actions/callback/userinfo.ts" import { OAuthAuthorizationErrorResponse } from "@/schemas.ts" -import { AuthSecurityError, OAuthProtocolError } from "@/lib/errors.ts" +import { AuthSecurityError, OAuthProtocolError } from "@/shared/errors.ts" import { getOriginURL, getTrustedOrigins } from "@/actions/signIn/authorization.ts" import { createAccessToken } from "@/actions/callback/access-token.ts" import { getCookie, expiredCookieAttributes } from "@/cookie.ts" import type { OAuthProviderRecord } from "@/@types/index.ts" -import { timingSafeEqual } from "@/lib/utils.ts" +import { timingSafeEqual } from "@/shared/utils.ts" const callbackConfig = (oauth: OAuthProviderRecord) => { return createEndpointConfig("/callback/:oauth", { diff --git a/packages/core/src/actions/callback/userinfo.ts b/packages/core/src/actions/callback/userinfo.ts index b6ad4a47..bac286c6 100644 --- a/packages/core/src/actions/callback/userinfo.ts +++ b/packages/core/src/actions/callback/userinfo.ts @@ -1,8 +1,8 @@ -import { fetchAsync } from "@/lib/fetch-async.ts" -import { createSecretValue } from "@/security.ts" -import { AURA_AUTH_VERSION } from "@/lib/utils.ts" +import { fetchAsync } from "@/shared/fetch-async.ts" +import { createSecretValue } from "@/shared/security.ts" +import { AURA_AUTH_VERSION } from "@/shared/utils.ts" import { OAuthErrorResponse } from "@/schemas.ts" -import { isNativeError, isOAuthProtocolError, OAuthProtocolError } from "@/lib/errors.ts" +import { isNativeError, isOAuthProtocolError, OAuthProtocolError } from "@/shared/errors.ts" import type { InternalLogger, OAuthProviderCredentials, User } from "@/@types/index.ts" /** diff --git a/packages/core/src/actions/csrfToken/csrfToken.ts b/packages/core/src/actions/csrfToken/csrfToken.ts index c1cb5bfe..5eb860a1 100644 --- a/packages/core/src/actions/csrfToken/csrfToken.ts +++ b/packages/core/src/actions/csrfToken/csrfToken.ts @@ -1,6 +1,6 @@ import { createEndpoint } from "@aura-stack/router" -import { createCSRF } from "@/security.ts" -import { secureApiHeaders } from "@/lib/headers.ts" +import { createCSRF } from "@/shared/security.ts" +import { secureApiHeaders } from "@/shared/headers.ts" import { setCookie, getCookie } from "@/cookie.ts" const getCSRFToken = (request: Request, cookieName: string) => { diff --git a/packages/core/src/actions/session/session.ts b/packages/core/src/actions/session/session.ts index c71ac3ef..41f9266d 100644 --- a/packages/core/src/actions/session/session.ts +++ b/packages/core/src/actions/session/session.ts @@ -1,6 +1,6 @@ import { createEndpoint, HeadersBuilder } from "@aura-stack/router" -import { secureApiHeaders } from "@/lib/headers.ts" -import { AuthInternalError } from "@/lib/errors.ts" +import { secureApiHeaders } from "@/shared/headers.ts" +import { AuthInternalError } from "@/shared/errors.ts" import { getSession } from "@/api/getSession.ts" import { expiredCookieAttributes } from "@/cookie.ts" diff --git a/packages/core/src/actions/signIn/authorization-url.ts b/packages/core/src/actions/signIn/authorization-url.ts index c1a83796..853aa64d 100644 --- a/packages/core/src/actions/signIn/authorization-url.ts +++ b/packages/core/src/actions/signIn/authorization-url.ts @@ -1,7 +1,7 @@ import { OAuthProvider } from "@/@types/index.ts" -import { AuthInternalError } from "@/lib/errors.ts" +import { AuthInternalError } from "@/shared/errors.ts" import { OAuthAuthorization } from "@/schemas.ts" -import { createPKCE, createSecretValue } from "@/security.ts" +import { createPKCE, createSecretValue } from "@/shared/security.ts" import type { GlobalContext } from "@aura-stack/router" export const setSearchParams = (url: URL, params: Record) => { diff --git a/packages/core/src/actions/signIn/authorization.ts b/packages/core/src/actions/signIn/authorization.ts index f1ad4314..57da3fe5 100644 --- a/packages/core/src/actions/signIn/authorization.ts +++ b/packages/core/src/actions/signIn/authorization.ts @@ -1,7 +1,7 @@ -import { getEnv } from "@/env.ts" -import { AuthInternalError } from "@/lib/errors.ts" -import { equals, extractPath, patternToRegex } from "@/lib/utils.ts" -import { isRelativeURL, isSameOrigin, isValidURL, isTrustedOrigin } from "@/lib/assert.ts" +import { getEnv } from "@/shared/env.ts" +import { AuthInternalError } from "@/shared/errors.ts" +import { equals, extractPath, patternToRegex } from "@/shared/utils.ts" +import { isRelativeURL, isSameOrigin, isValidURL, isTrustedOrigin } from "@/shared/assert.ts" import type { AuthConfig } from "@/@types/index.ts" import type { GlobalContext } from "@aura-stack/router" diff --git a/packages/core/src/actions/signOut/signOut.ts b/packages/core/src/actions/signOut/signOut.ts index 2902171b..b0b6eed5 100644 --- a/packages/core/src/actions/signOut/signOut.ts +++ b/packages/core/src/actions/signOut/signOut.ts @@ -1,6 +1,6 @@ import { z } from "zod/v4" import { createEndpoint, createEndpointConfig } from "@aura-stack/router" -import { getBaseURL } from "@/lib/utils.ts" +import { getBaseURL } from "@/shared/utils.ts" import { signOut } from "@/api/signOut.ts" import { createRedirectTo } from "@/actions/signIn/authorization.ts" diff --git a/packages/core/src/api/createApi.ts b/packages/core/src/api/createApi.ts index 3cb2962b..84f0f28c 100644 --- a/packages/core/src/api/createApi.ts +++ b/packages/core/src/api/createApi.ts @@ -1,7 +1,7 @@ import { signIn } from "@/api/signIn.ts" import { signOut } from "@/api/signOut.ts" -import { validateRedirectTo } from "@/lib/utils.ts" import { getSession } from "@/api/getSession.ts" +import { validateRedirectTo } from "@/shared/utils.ts" import type { GlobalContext } from "@aura-stack/router" import type { BuiltInOAuthProvider, diff --git a/packages/core/src/api/getSession.ts b/packages/core/src/api/getSession.ts index a308e361..a262e827 100644 --- a/packages/core/src/api/getSession.ts +++ b/packages/core/src/api/getSession.ts @@ -1,4 +1,4 @@ -import { getErrorName } from "@/lib/utils.ts" +import { getErrorName } from "@/shared/utils.ts" import type { FunctionAPIContext, GetSessionAPIOptions, SessionResponse } from "@/@types/index.ts" const unauthorized: SessionResponse = { session: null, headers: new Headers(), authenticated: false } diff --git a/packages/core/src/api/signIn.ts b/packages/core/src/api/signIn.ts index b7ebdfb7..1a120f6f 100644 --- a/packages/core/src/api/signIn.ts +++ b/packages/core/src/api/signIn.ts @@ -1,6 +1,6 @@ -import { cacheControl } from "@/lib/headers.ts" -import { AuthInternalError } from "@/lib/errors.ts" +import { cacheControl } from "@/shared/headers.ts" import { HeadersBuilder } from "@aura-stack/router" +import { AuthInternalError } from "@/shared/errors.ts" import { createAuthorizationURL } from "@/actions/signIn/authorization-url.ts" import { createRedirectTo, createRedirectURI, createSignInURL, getBaseURL } from "@/actions/signIn/authorization.ts" import type { BuiltInOAuthProvider, FunctionAPIContext, LiteralUnion, SignInAPIOptions, SignInReturn } from "@/@types/index.ts" diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index fcc465bc..c7ce3d5f 100644 --- a/packages/core/src/client/client.ts +++ b/packages/core/src/client/client.ts @@ -1,4 +1,4 @@ -import { AuthClientError, isNativeError } from "@/lib/errors.ts" +import { AuthClientError, isNativeError } from "@/shared/errors.ts" import { createClient as createClientAPI } from "@aura-stack/router" import type { Session, diff --git a/packages/core/src/cookie.ts b/packages/core/src/cookie.ts index 50103947..b355c870 100644 --- a/packages/core/src/cookie.ts +++ b/packages/core/src/cookie.ts @@ -1,5 +1,5 @@ -import { env } from "@/env.ts" -import { AuthInternalError } from "@/lib/errors.ts" +import { env } from "@/shared/env.ts" +import { AuthInternalError } from "@/shared/errors.ts" import { parse, parseSetCookie, serialize, type SerializeOptions } from "@aura-stack/router/cookie" import type { CookieStoreConfig, CookieConfig, InternalLogger } from "@/@types/index.ts" diff --git a/packages/core/src/createAuth.ts b/packages/core/src/createAuth.ts index e262a132..37ae0aec 100644 --- a/packages/core/src/createAuth.ts +++ b/packages/core/src/createAuth.ts @@ -1,12 +1,12 @@ import { createRouter, type RouterConfig } from "@aura-stack/router" -import { useSecureCookies } from "@/lib/utils.ts" import { createAuthAPI } from "@/api/createApi.ts" import { createContext } from "@/router/context.ts" +import { isSecureConnection } from "@/shared/utils.ts" import { createErrorHandler } from "@/router/errorHandler.ts" import { signInAction, callbackAction, sessionAction, signOutAction, csrfTokenAction } from "@/actions/index.ts" import type { AuthConfig, AuthInstance, User } from "@/@types/index.ts" -const createInternalConfig = (authConfig?: AuthConfig): RouterConfig => { +const createInternalConfig = (authConfig?: AuthConfig): RouterConfig => { const context = createContext(authConfig) return { basePath: authConfig?.basePath ?? "/auth", @@ -14,7 +14,7 @@ const createInternalConfig = (authConfig?: Auth context: context as unknown as RouterConfig["context"], use: [ (ctx) => { - const useSecure = useSecureCookies(ctx.request, ctx.context.trustedProxyHeaders) + const useSecure = isSecureConnection(ctx.request, ctx.context.trustedProxyHeaders) ctx.context.cookies = useSecure ? context.cookieConfig.secure : context.cookieConfig.standard return ctx }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 155258de..d95a3891 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,6 @@ export { createAuth } from "@/createAuth.ts" export { createJoseInstance } from "@/jose.ts" -export { createSyslogMessage } from "@/lib/logger.ts" +export { createSyslogMessage } from "@/shared/logger.ts" export { builtInOAuthProviders } from "@/oauth/index.ts" export { createAuthClient, createClient } from "@/client/index.ts" diff --git a/packages/core/src/jose.ts b/packages/core/src/jose.ts index f21cb1c4..9fba12ef 100644 --- a/packages/core/src/jose.ts +++ b/packages/core/src/jose.ts @@ -1,4 +1,4 @@ -import { getEnv } from "@/env.ts" +import { getEnv } from "@/shared/env.ts" import { createJWT, createJWS, @@ -14,8 +14,8 @@ import { type JWTDecryptOptions, } from "@aura-stack/jose" export { base64url, type JWTPayload } from "@aura-stack/jose/jose" -import { AuthInternalError, AuthSecurityError } from "@/lib/errors.ts" -import { isEncryptedMode, isSealedMode, isSignedMode } from "@/lib/assert.ts" +import { AuthInternalError, AuthSecurityError } from "@/shared/errors.ts" +import { isEncryptedMode, isSealedMode, isSignedMode } from "@/shared/assert.ts" export { encoder, getRandomBytes, getSubtleCrypto } from "@aura-stack/jose/crypto" import type { User, SessionConfig, JWTKey } from "@/@types/index.ts" diff --git a/packages/core/src/oauth/index.ts b/packages/core/src/oauth/index.ts index 878cf988..045ee2b9 100644 --- a/packages/core/src/oauth/index.ts +++ b/packages/core/src/oauth/index.ts @@ -4,7 +4,7 @@ * This modules re-exports OAuth providers available in Aura Auth to be used in the Auth instance configuration. */ import { type LiteralUnion, type OAuthProviderCredentials } from "@/@types/index.ts" -import { getEnv } from "@/env.ts" +import { getEnv } from "@/shared/env.ts" import { github } from "./github.ts" import { bitbucket } from "./bitbucket.ts" import { figma } from "./figma.ts" @@ -19,9 +19,9 @@ import { twitch } from "./twitch.ts" import { notion } from "./notion.ts" import { dropbox } from "./dropbox.ts" import { atlassian } from "./atlassian.ts" +import { formatZodError } from "@/shared/utils.ts" +import { AuthInternalError } from "@/shared/errors.ts" import { OAuthEnvSchema, OAuthProviderCredentialsSchema } from "@/schemas.ts" -import { AuthInternalError } from "@/lib/errors.ts" -import { formatZodError } from "@/lib/utils.ts" export * from "./github.ts" export * from "./bitbucket.ts" @@ -132,4 +132,14 @@ export const createBuiltInOAuthProviders = (oauth: (BuiltInOAuthProvider | OAuth }, {}) as Record, OAuthProviderCredentials> } +export const createBasicAuthHeader = (username: string, password: string): string => { + const getUsername = getEnv(username.toUpperCase()) ?? username + const getPassword = getEnv(password.toUpperCase()) ?? password + if (!getUsername || !getPassword) { + throw new AuthInternalError("INVALID_OAUTH_CONFIGURATION", "Missing client credentials for OAuth provider configuration.") + } + const credentials = `${getUsername}:${getPassword}` + return `Basic ${btoa(credentials)}` +} + export type BuiltInOAuthProvider = keyof typeof builtInOAuthProviders diff --git a/packages/core/src/oauth/notion.ts b/packages/core/src/oauth/notion.ts index 76da05a1..b367dcfc 100644 --- a/packages/core/src/oauth/notion.ts +++ b/packages/core/src/oauth/notion.ts @@ -1,4 +1,4 @@ -import { createBasicAuthHeader } from "@/lib/utils.ts" +import { createBasicAuthHeader } from "@/oauth/index.ts" import type { OAuthProviderCredentials, User } from "@/@types/index.ts" export interface Person { diff --git a/packages/core/src/oauth/twitch.ts b/packages/core/src/oauth/twitch.ts index cb3cd136..af1268af 100644 --- a/packages/core/src/oauth/twitch.ts +++ b/packages/core/src/oauth/twitch.ts @@ -1,4 +1,4 @@ -import { getEnv } from "@/env.ts" +import { getEnv } from "@/shared/env.ts" import type { OAuthProviderCredentials, User } from "@/@types/index.ts" /** diff --git a/packages/core/src/router/context.ts b/packages/core/src/router/context.ts index 3fef82c3..ccb3aeda 100644 --- a/packages/core/src/router/context.ts +++ b/packages/core/src/router/context.ts @@ -1,9 +1,9 @@ import { createJoseInstance } from "@/jose.ts" -import { createProxyLogger } from "@/lib/logger.ts" import { createCookieStore } from "@/cookie.ts" -import { createSessionStrategy } from "@/session/index.ts" -import { getEnv, getEnvArray, getEnvBoolean } from "@/env.ts" +import { createProxyLogger } from "@/shared/logger.ts" +import { createSessionStrategy } from "@/session/strategy.ts" import { createBuiltInOAuthProviders } from "@/oauth/index.ts" +import { getEnv, getEnvArray, getEnvBoolean } from "@/shared/env.ts" import type { AuthConfig, InternalContext, User } from "@/@types/index.ts" export const createContext = (config?: AuthConfig): InternalContext => { @@ -34,6 +34,6 @@ export const createContext = (config?: AuthConf jose, config: config?.session, logger: ctx.logger, - }) as InternalContext["sessionStrategy"] + }) return ctx } diff --git a/packages/core/src/router/errorHandler.ts b/packages/core/src/router/errorHandler.ts index c69cf183..9ad6ad47 100644 --- a/packages/core/src/router/errorHandler.ts +++ b/packages/core/src/router/errorHandler.ts @@ -1,5 +1,5 @@ -import { isAuthInternalError, isAuthSecurityError, isOAuthProtocolError } from "@/lib/errors.ts" import { isInvalidZodSchemaError, isRouterError, type RouterConfig } from "@aura-stack/router" +import { isAuthInternalError, isAuthSecurityError, isOAuthProtocolError } from "@/shared/errors.ts" import type { InternalLogger } from "@/@types/index.ts" export const createErrorHandler = (logger?: InternalLogger): RouterConfig["onError"] => { @@ -25,7 +25,7 @@ export const createErrorHandler = (logger?: InternalLogger): RouterConfig["onErr return Response.json( { type, - message: message, + message, }, { status: 400 } ) @@ -48,7 +48,7 @@ export const createErrorHandler = (logger?: InternalLogger): RouterConfig["onErr } if (isAuthSecurityError(error)) { const { type, code, message } = error - logger?.log("INVALID_OAUTH_CONFIGURATION", { + logger?.log("AUTH_SECURITY_ERROR", { structuredData: { error: code, error_description: message, diff --git a/packages/core/src/schemas.ts b/packages/core/src/schemas.ts index 8305fffc..28f78d65 100644 --- a/packages/core/src/schemas.ts +++ b/packages/core/src/schemas.ts @@ -1,7 +1,7 @@ -import { object, string, enum as options, number, z, null as nullable, union, array, url } from "zod/v4" +import { object, string, enum as options, number, z, null as nullable, union, array } from "zod/v4" const AuthorizeConfigSchema = z.union([ - url(), + string().url(), object({ url: string().url(), params: object({ @@ -12,17 +12,17 @@ const AuthorizeConfigSchema = z.union([ ]) const AccessTokenConfigSchema = z.union([ - url(), + string().url(), object({ - url: url(), + url: string().url(), headers: z.record(string(), string()).optional(), }), ]) const UserInfoConfigSchema = z.union([ - url(), + string().url(), object({ - url: url(), + url: string().url(), headers: z.record(string(), string()).optional(), method: string().optional(), }), @@ -33,7 +33,7 @@ export const OAuthProviderCredentialsSchema = object({ name: string(), authorize: AuthorizeConfigSchema.optional(), /** @deprecated */ - authorizeURL: url().optional(), + authorizeURL: string().url().optional(), accessToken: AccessTokenConfigSchema, /** @deprecated */ scope: string().optional(), @@ -51,7 +51,7 @@ export const OAuthProviderCredentialsSchema = object({ export const OAuthProviderConfigSchema = object({ authorize: AuthorizeConfigSchema.optional(), /** @deprecated */ - authorizeURL: url().optional(), + authorizeURL: string().url().optional(), accessToken: AccessTokenConfigSchema, /** @deprecated */ scope: string().optional(), diff --git a/packages/core/src/session/manager/cookie.ts b/packages/core/src/session/cookie-manager.ts similarity index 95% rename from packages/core/src/session/manager/cookie.ts rename to packages/core/src/session/cookie-manager.ts index 3912ac0f..aee5c3dc 100644 --- a/packages/core/src/session/manager/cookie.ts +++ b/packages/core/src/session/cookie-manager.ts @@ -1,5 +1,5 @@ import { HeadersBuilder } from "@aura-stack/router" -import { secureApiHeaders } from "@/lib/headers.ts" +import { secureApiHeaders } from "@/shared/headers.ts" import { expiredCookieAttributes, getCookie as getCookieByName } from "@/cookie.ts" import type { CookieStoreConfig } from "@/@types/index.ts" diff --git a/packages/core/src/session/index.ts b/packages/core/src/session/index.ts index c469deb7..46d7bf72 100644 --- a/packages/core/src/session/index.ts +++ b/packages/core/src/session/index.ts @@ -1,24 +1,2 @@ -import { AuthInvalidConfigurationError } from "@/lib/errors.ts" -import { createStatelessStrategy } from "@/session/strategies/stateless.ts" -import type { CreateSessionStrategyOptions, SessionStrategy, User } from "@/@types/session.ts" - -export const createSessionStrategy = ({ - config, - jose, - cookies, - logger, -}: CreateSessionStrategyOptions): SessionStrategy => { - const strategy = config?.strategy ?? "jwt" - - switch (strategy) { - case "jwt": - return createStatelessStrategy({ - jose, - config, - cookies, - logger, - }) - default: - throw new AuthInvalidConfigurationError(`[auth] unknown session strategy "${strategy}". Valid options are: "jwt".`) - } -} +export { createSessionStrategy } from "@/session/strategy.ts" +export { createJoseManager } from "@/session/jose-manager.ts" diff --git a/packages/core/src/session/manager/jose.ts b/packages/core/src/session/jose-manager.ts similarity index 63% rename from packages/core/src/session/manager/jose.ts rename to packages/core/src/session/jose-manager.ts index b3ae48ce..1139be12 100644 --- a/packages/core/src/session/manager/jose.ts +++ b/packages/core/src/session/jose-manager.ts @@ -1,11 +1,5 @@ -import { AuthInvalidConfigurationError } from "@/lib/errors.ts" -import type { TypedJWTPayload } from "@aura-stack/jose" -import type { JoseInstance, User, JWTConfig } from "@/@types/index.ts" - -export type JWTManager = { - createToken(user: TypedJWTPayload>): Promise - verifyToken(token: string): Promise> -} +import { AuthInvalidConfigurationError } from "@/shared/errors.ts" +import type { JoseInstance, User, JWTConfig, JWTManager } from "@/@types/index.ts" export const createJoseManager = ( config: JWTConfig | undefined, diff --git a/packages/core/src/session/strategies/stateless.ts b/packages/core/src/session/stateless.ts similarity index 95% rename from packages/core/src/session/strategies/stateless.ts rename to packages/core/src/session/stateless.ts index 412d35a9..ff495a06 100644 --- a/packages/core/src/session/strategies/stateless.ts +++ b/packages/core/src/session/stateless.ts @@ -1,9 +1,9 @@ import { getCookie } from "@/cookie.ts" -import { verifyCSRF } from "@/security.ts" -import { getErrorName } from "@/lib/utils.ts" -import { AuthSecurityError } from "@/lib/errors.ts" -import { createJoseManager } from "@/session/manager/jose.ts" -import { createCookieManager } from "@/session/manager/cookie.ts" +import { verifyCSRF } from "@/shared/security.ts" +import { getErrorName } from "@/shared/utils.ts" +import { AuthSecurityError } from "@/shared/errors.ts" +import { createJoseManager } from "@/session/jose-manager.ts" +import { createCookieManager } from "@/session/cookie-manager.ts" import type { Session, SessionStrategy, User, TypedJWTPayload, JWTStrategyOptions, GetSessionReturn } from "@/@types/index.ts" export const createStatelessStrategy = ({ diff --git a/packages/core/src/session/strategy.ts b/packages/core/src/session/strategy.ts new file mode 100644 index 00000000..266939d5 --- /dev/null +++ b/packages/core/src/session/strategy.ts @@ -0,0 +1,24 @@ +import { AuthInvalidConfigurationError } from "@/shared/errors.ts" +import { createStatelessStrategy } from "@/session/stateless.ts" +import type { CreateSessionStrategyOptions, SessionStrategy, User } from "@/@types/session.ts" + +export const createSessionStrategy = ({ + config, + jose, + cookies, + logger, +}: CreateSessionStrategyOptions): SessionStrategy => { + const strategy = config?.strategy ?? "jwt" + + switch (strategy) { + case "jwt": + return createStatelessStrategy({ + jose, + config, + cookies, + logger, + }) + default: + throw new AuthInvalidConfigurationError(`[auth] unknown session strategy "${strategy}". Valid options are: "jwt".`) + } +} diff --git a/packages/core/src/lib/assert.ts b/packages/core/src/shared/assert.ts similarity index 98% rename from packages/core/src/lib/assert.ts rename to packages/core/src/shared/assert.ts index 91bb05b2..d1890f05 100644 --- a/packages/core/src/lib/assert.ts +++ b/packages/core/src/shared/assert.ts @@ -1,4 +1,4 @@ -import { equals, patternToRegex } from "@/lib/utils.ts" +import { equals, patternToRegex } from "@/shared/utils.ts" import type { JWTConfig, JWTMode, JWTPayloadWithToken, SessionConfig } from "@/@types/index.ts" export const isFalsy = (value: unknown): boolean => { diff --git a/packages/core/src/env.ts b/packages/core/src/shared/env.ts similarity index 100% rename from packages/core/src/env.ts rename to packages/core/src/shared/env.ts diff --git a/packages/core/src/lib/errors.ts b/packages/core/src/shared/errors.ts similarity index 100% rename from packages/core/src/lib/errors.ts rename to packages/core/src/shared/errors.ts diff --git a/packages/core/src/lib/fetch-async.ts b/packages/core/src/shared/fetch-async.ts similarity index 100% rename from packages/core/src/lib/fetch-async.ts rename to packages/core/src/shared/fetch-async.ts diff --git a/packages/core/src/lib/headers.ts b/packages/core/src/shared/headers.ts similarity index 100% rename from packages/core/src/lib/headers.ts rename to packages/core/src/shared/headers.ts diff --git a/packages/core/src/lib/logger.ts b/packages/core/src/shared/logger.ts similarity index 95% rename from packages/core/src/lib/logger.ts rename to packages/core/src/shared/logger.ts index e783d19c..307c762a 100644 --- a/packages/core/src/lib/logger.ts +++ b/packages/core/src/shared/logger.ts @@ -1,5 +1,4 @@ -import { getEnv, getEnvBoolean } from "@/env.ts" -import { createStructuredData } from "@/lib/utils.ts" +import { getEnv, getEnvBoolean } from "@/shared/env.ts" import type { AuthConfig, InternalLogger, Logger, LogLevel, SyslogOptions } from "@/@types/index.ts" /** @@ -267,6 +266,12 @@ export const logMessages = { msgId: "SESSION_REFRESHED", message: "User session was refreshed with a new expiration time", }, + AUTH_SECURITY_ERROR: { + facility: 10, + severity: "error", + msgId: "AUTH_SECURITY_ERROR", + message: "An authentication security error occurred", + }, } as const export const createLogEntry = (key: T, overrides?: Partial): SyslogOptions => { @@ -309,6 +314,13 @@ const getSeverityLevel = (severity: string): number => { return severities[severity] ?? 6 } +export const createStructuredData = (data: Record, sdID = "metadata"): string => { + const entries = Object.entries(data) + if (entries.length === 0) return `[${sdID}]` + const values = entries.map(([key, value]) => `${key}="${String(value).replace(/(["\\\]])/g, "\\$1")}"`).join(" ") + return `[${sdID} ${values}]` +} + export const createSyslogMessage = (options: SyslogOptions): string => { const { timestamp, hostname, appName = "aura-auth", procId = "-", msgId, structuredData, message } = options const pri = (options.facility ?? 16) * 8 + getSeverityLevel(options.severity) @@ -340,7 +352,6 @@ export const createLogger = (logger?: Required): InternalLogger | undefi /** * Creates the logger instance based on the provided configuration and environment variables. * Priority: config.logger, LOG_LEVEL env, DEBUG env and defaults to undefined if logging is not enabled. - * */ export const createProxyLogger = (config?: AuthConfig) => { const level = getEnv("LOG_LEVEL") diff --git a/packages/core/src/security.ts b/packages/core/src/shared/security.ts similarity index 95% rename from packages/core/src/security.ts rename to packages/core/src/shared/security.ts index 85464d68..ce0c1ad4 100644 --- a/packages/core/src/security.ts +++ b/packages/core/src/shared/security.ts @@ -1,6 +1,6 @@ -import { AuthSecurityError } from "@/lib/errors.ts" -import { isJWTPayloadWithToken } from "@/lib/assert.ts" -import { equals, timingSafeEqual } from "@/lib/utils.ts" +import { AuthSecurityError } from "@/shared/errors.ts" +import { isJWTPayloadWithToken } from "@/shared/assert.ts" +import { equals, timingSafeEqual } from "@/shared/utils.ts" import { base64url, encoder, getRandomBytes, getSubtleCrypto } from "@/jose.ts" import type { AuthRuntimeConfig, JoseInstance, User } from "@/@types/index.ts" @@ -42,11 +42,11 @@ export const createPKCE = async (verifier?: string) => { */ export const createCSRF = async (jose: AuthRuntimeConfig["jose"], csrfCookie?: string) => { try { - const token = createSecretValue(32) if (csrfCookie) { await jose.verifyJWS(csrfCookie) return csrfCookie } + const token = createSecretValue(32) return jose.signJWS({ token }) } catch { const token = createSecretValue(32) diff --git a/packages/core/src/lib/utils.ts b/packages/core/src/shared/utils.ts similarity index 74% rename from packages/core/src/lib/utils.ts rename to packages/core/src/shared/utils.ts index 80317393..320324e3 100644 --- a/packages/core/src/lib/utils.ts +++ b/packages/core/src/shared/utils.ts @@ -1,11 +1,9 @@ -import { getEnv } from "@/env.ts" import { encoder } from "@aura-stack/jose/crypto" -import { AuthInternalError } from "@/lib/errors.ts" -import { isRelativeURL, isValidURL } from "@/lib/assert.ts" +import { isRelativeURL, isValidURL } from "@/shared/assert.ts" import type { ZodError } from "zod" import type { APIErrorMap } from "@/@types/index.ts" -export const AURA_AUTH_VERSION = "0.4.0" +export const AURA_AUTH_VERSION = "0.5.0" export const equals = (a: string | number | undefined | null, b: string | number | undefined | null) => { if (a === null || b === null || a === undefined || b === undefined) return false @@ -17,11 +15,7 @@ export const getBaseURL = (request: Request) => { return `${url.origin}${url.pathname}` } -export const toISOString = (date: Date | string | number): string => { - return new Date(date).toISOString() -} - -export const useSecureCookies = (request: Request | Headers, trustedProxyHeaders: boolean): boolean => { +export const isSecureConnection = (request: Request | Headers, trustedProxyHeaders: boolean): boolean => { const headers = request instanceof Headers ? request : request.headers const url = request instanceof Headers ? null : request.url return trustedProxyHeaders @@ -53,13 +47,6 @@ export const extractPath = (url: string): string => { return match && match[2] ? match[2] : "/" } -export const createStructuredData = (data: Record, sdID = "metadata"): string => { - const entries = Object.entries(data) - if (entries.length === 0) return `[${sdID}]` - const values = entries.map(([key, value]) => `${key}="${String(value).replace(/(["\\\]])/g, "\\$1")}"`).join(" ") - return `[${sdID} ${values}]` -} - export const getErrorName = (error: unknown): string => { if (error instanceof Error) { return error.name @@ -67,16 +54,6 @@ export const getErrorName = (error: unknown): string => { return typeof error === "string" ? error : "UnknownError" } -export const createBasicAuthHeader = (username: string, password: string): string => { - const getUsername = getEnv(username.toUpperCase()) ?? username - const getPassword = getEnv(password.toUpperCase()) ?? password - if (!getUsername || !getPassword) { - throw new AuthInternalError("INVALID_OAUTH_CONFIGURATION", "Missing client credentials for OAuth provider configuration.") - } - const credentials = `${getUsername}:${getPassword}` - return `Basic ${btoa(credentials)}` -} - /** * Validates and sanitizes redirect URLs to prevent open redirect attacks. * Only relative URLs (starting with /) are allowed; absolute URLs are diff --git a/packages/core/test/actions/callback/access-token.test.ts b/packages/core/test/actions/callback/access-token.test.ts index f7b6f8a5..8de87603 100644 --- a/packages/core/test/actions/callback/access-token.test.ts +++ b/packages/core/test/actions/callback/access-token.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, vi } from "vitest" -import { createPKCE } from "@/security.ts" +import { createPKCE } from "@/shared/security.ts" import { oauthCustomService } from "@test/presets.ts" import { createAccessToken } from "@/actions/callback/access-token.ts" diff --git a/packages/core/test/actions/callback/callback.test.ts b/packages/core/test/actions/callback/callback.test.ts index 9b566fa4..e1c236c7 100644 --- a/packages/core/test/actions/callback/callback.test.ts +++ b/packages/core/test/actions/callback/callback.test.ts @@ -1,8 +1,8 @@ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest" import { GET } from "@test/presets.ts" -import { createPKCE } from "@/security.ts" +import { createPKCE } from "@/shared/security.ts" import { setCookie, getSetCookie } from "@/cookie.ts" -import { AURA_AUTH_VERSION } from "@/lib/utils.ts" +import { AURA_AUTH_VERSION } from "@/shared/utils.ts" beforeEach(() => { vi.stubEnv("BASE_URL", undefined) diff --git a/packages/core/test/actions/callback/userinfo.test.ts b/packages/core/test/actions/callback/userinfo.test.ts index 37011dda..913ad23b 100644 --- a/packages/core/test/actions/callback/userinfo.test.ts +++ b/packages/core/test/actions/callback/userinfo.test.ts @@ -2,7 +2,7 @@ import { describe, test, expect, vi } from "vitest" import { getUserInfo } from "@/actions/callback/userinfo.ts" import { OAuthProviderConfig, OAuthProviderCredentials } from "@/@types/index.ts" import { oauthCustomService } from "@test/presets.ts" -import { AURA_AUTH_VERSION } from "@/lib/utils.ts" +import { AURA_AUTH_VERSION } from "@/shared/utils.ts" describe("getUserInfo", () => { test("get user info", async () => { diff --git a/packages/core/test/actions/session/session.test.ts b/packages/core/test/actions/session/session.test.ts index b15fd3ea..2291d30e 100644 --- a/packages/core/test/actions/session/session.test.ts +++ b/packages/core/test/actions/session/session.test.ts @@ -1,8 +1,8 @@ import { describe, test, expect, vi, afterEach, beforeEach } from "vitest" -import { createPKCE } from "@/security.ts" +import { createPKCE } from "@/shared/security.ts" +import { AURA_AUTH_VERSION } from "@/shared/utils.ts" import { GET, jose, sessionPayload } from "@test/presets.ts" import { setCookie, getSetCookie, createCookieStore } from "@/cookie.ts" -import { AURA_AUTH_VERSION } from "@/lib/utils.ts" beforeEach(() => { vi.stubEnv("BASE_URL", undefined) diff --git a/packages/core/test/actions/signOut/signOut.test.ts b/packages/core/test/actions/signOut/signOut.test.ts index bebc7b73..cdb4001a 100644 --- a/packages/core/test/actions/signOut/signOut.test.ts +++ b/packages/core/test/actions/signOut/signOut.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest" -import { createCSRF } from "@/security.ts" +import { createCSRF } from "@/shared/security.ts" import { POST, jose, sessionPayload } from "@test/presets.ts" beforeEach(() => { diff --git a/packages/core/test/assert.test.ts b/packages/core/test/assert.test.ts index 959522b0..541bb318 100644 --- a/packages/core/test/assert.test.ts +++ b/packages/core/test/assert.test.ts @@ -1,5 +1,5 @@ -import { isRelativeURL, isValidURL, isTrustedOrigin } from "@/lib/assert.ts" import { describe, test, expect } from "vitest" +import { isRelativeURL, isValidURL, isTrustedOrigin } from "@/shared/assert.ts" describe("isRelativeURL", () => { const testCases = [ diff --git a/packages/core/test/context.test.ts b/packages/core/test/context.test.ts index 8e3e60f6..65ab9c25 100644 --- a/packages/core/test/context.test.ts +++ b/packages/core/test/context.test.ts @@ -1,5 +1,5 @@ import { describe, expect, vi, test } from "vitest" -import { createProxyLogger } from "@/lib/logger.ts" +import { createProxyLogger } from "@/shared/logger.ts" import type { Logger } from "@/@types/index.ts" describe("createProxyLogger", () => { diff --git a/packages/core/test/env.test.ts b/packages/core/test/env.test.ts index c2eeb6a7..6dfe6876 100644 --- a/packages/core/test/env.test.ts +++ b/packages/core/test/env.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, test, vi } from "vitest" -import { env } from "@/env.ts" -import { createSecretValue } from "@/security.ts" +import { env } from "@/shared/env.ts" +import { createSecretValue } from "@/shared/security.ts" describe("env", () => { afterEach(() => { diff --git a/packages/core/test/jose.test.ts b/packages/core/test/jose.test.ts index 76ceca8e..14e7ddda 100644 --- a/packages/core/test/jose.test.ts +++ b/packages/core/test/jose.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest" import { createJoseInstance } from "@/jose.ts" -import { createSecretValue } from "@/security.ts" +import { createSecretValue } from "@/shared/security.ts" const payload = { sub: "1234567890", diff --git a/packages/core/test/request.test.ts b/packages/core/test/request.test.ts index d4afd325..7e4d413d 100644 --- a/packages/core/test/request.test.ts +++ b/packages/core/test/request.test.ts @@ -1,4 +1,4 @@ -import { fetchAsync } from "@/lib/fetch-async.ts" +import { fetchAsync } from "@/shared/fetch-async.ts" import { describe, expect, test, vi } from "vitest" describe("fetchAsync", () => { diff --git a/packages/core/test/secure.test.ts b/packages/core/test/secure.test.ts index 39cb766a..faf41e54 100644 --- a/packages/core/test/secure.test.ts +++ b/packages/core/test/secure.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "vitest" -import { createPKCE } from "@/security.ts" +import { createPKCE } from "@/shared/security.ts" describe("createPKCE", () => { test("generates a valid code verifier and code challenge", async () => {