Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/core/src/@types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,20 @@ export type AuthConfig<
logger?: boolean | Logger
/**
* Defines the session management strategy for Aura Auth. It determines how sessions are created, stored, and validated.
* @example
* // Stateless JWT strategy (default)
* session: {
* strategy: "jwt",
* jwt: {
* issuer: "https://auth.example.com",
* }
* }
*
* // Database-backed session strategy
* session: {
* strategy: "database",
* adapter: prismaAdapter({ client: prismaClient }),
* }
*/
session?: SessionConfig
/**
Expand Down
125 changes: 61 additions & 64 deletions packages/core/src/@types/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,60 +42,25 @@ import type { TypedJWTPayload } from "@aura-stack/jose"
import type { SessionWithUserEntity } from "@/@types/entities.ts"
import type { createCookieManager } from "@/session/cookie-manager.ts"

// #region API
/**
* Utility to merge the internal router global context (`ctx`) with per-function options.
* Used by implementation-level API functions in `src/api/*`.
*/
export type FunctionAPIContext<Options extends object = {}> = Prettify<
{
ctx: RouterGlobalContext
} & Options
>

// #region Config
export type InternalCookieStoreConfig = Record<CookieName, { name: string; attributes: CookieStrategyAttributes }>

/**
* Normalized internal logger with resolved level and structured log function.
*/
export interface InternalLogger {
level: LogLevel
log: typeof createLogEntry
}

/**
* Identity validation settings used when building session strategy and OAuth profile mapping.
* Controls the Zod schema and how unknown keys are handled on user objects.
*/
export interface InternalIdentityConfig<Schema extends SchemaTypes = typeof identitySchema> {
schema?: Schema
schemaAsPartial?: Schema
skipValidation?: boolean
unknownKeys?: "passthrough" | "strict" | "strip"
}

/**
* Runtime context passed into auth actions and API handlers: OAuth map, cookies, JWT, session strategy, trusted origins, etc.
* This is the fully resolved configuration surface after `createAuth` initializes defaults.
*/
export interface RouterGlobalContext<DefaultUser extends User = User, SignUpSchema extends SchemaTypes = ZodObject<any>> {
oauth: OAuthProviderRecord<DefaultUser>
credentials?: CredentialsConfig<any>
cookies: InternalCookieStoreConfig
jose: JoseInstance<DefaultUser>
cookies: InternalCookieConfig
jose: InternalJoseConfig<DefaultUser>
secret?: JWTKey
baseURL?: string
basePath: RoutePattern
trustedProxyHeaders: boolean | TrustedProxyHeadersSource[]
trustedOrigins?: TrustedOrigin[] | ((request: Request) => Promise<TrustedOrigin[]> | TrustedOrigin[])
logger?: InternalLogger
sessionStrategy: SessionStrategy<DefaultUser>
session: InternalSessionConfig<DefaultUser>
identity: SchemaRegistryContext
signUp?: SignUpConfig<DefaultUser, SignUpSchema>
jwtManager: JWTManager<DefaultUser>
rateLimiters: InferRules<Required<RateLimiterConfig>>
sessionConfig: SessionConfig
emailVerification?: EmailVerificationConfig
passwordReset?: PasswordResetConfig
emailChange?: EmailChangeConfig
Expand All @@ -108,37 +73,22 @@ export interface SchemaRegistryContext {
unknownKeys: "passthrough" | "strict" | "strip"
}

export type InternalSessionConfig<DefaultUser extends User, Config extends SessionConfig = SessionConfig> = {
config: Config
strategy: SessionStrategy<DefaultUser>
}

export type InternalContext<Identity extends Identities, SignUpSchema extends SchemaTypes> = RouterGlobalContext<
FromShapeToObject<Identity>,
SignUpSchema
> & {
cookieConfig: {
trusted: InternalCookieStoreConfig
untrusted: InternalCookieStoreConfig
}
}

export type InternalContextForStateful = Omit<InternalContext<any, any>, "sessionConfig"> & {
sessionConfig: StatefulStrategyConfig
}

export interface CreateSessionStrategyOptions<Identity extends Identities> {
ctx: InternalContext<Identity, any>
cookies: () => InternalCookieStoreConfig
}
>

export interface InternalSessionContext<Ctx> {
ctx: Ctx
cookies: () => InternalCookieStoreConfig
cookieManager: CookieManager
export type InternalContextForStateful = RouterGlobalContext<any, any> & {
session: InternalSessionConfig<User, StatefulStrategyConfig>
}

export type InternalStatefulContext = InternalSessionContext<InternalContextForStateful>

export type InternalStatelessContext = InternalSessionContext<InternalContextForStateless>

export type InternalContextForStateless = Omit<InternalContext<any, any>, "sessionConfig"> & {
sessionConfig: StatelessStrategyConfig
export type InternalContextForStateless = RouterGlobalContext<any, any> & {
session: InternalSessionConfig<User, StatelessStrategyConfig>
}

export type InternalExpirationResult =
Expand Down Expand Up @@ -173,11 +123,58 @@ export interface OnCreateUserContext<Schema extends SchemaTypes> {
}

/** Minimal token issue/verify surface used by session code paths. */
export type JWTManager<DefaultUser extends User = User> = {
export type JoseManager<DefaultUser extends User = User> = {
createToken(user: TypedJWTPayload<Partial<DefaultUser>>): Promise<string>
verifyToken(token: string): Promise<TypedJWTPayload<DefaultUser>>
}

export interface InternalCookieConfig {
store: InternalCookieStoreConfig
precomputed: {
trusted: InternalCookieStoreConfig
untrusted: InternalCookieStoreConfig
}
manager: CookieManager
}

export interface InternalJoseConfig<DefaultUser extends User = User> {
instance: JoseInstance<DefaultUser>
manager: JoseManager<DefaultUser>
}

// #region API
/**
* Utility to merge the internal router global context (`ctx`) with per-function options.
* Used by implementation-level API functions in `src/api/*`.
*/
export type FunctionAPIContext<Options extends object = {}> = Prettify<
{
ctx: RouterGlobalContext
} & Options
>

// #region Config
export type InternalCookieStoreConfig = Record<CookieName, { name: string; attributes: CookieStrategyAttributes }>

/**
* Normalized internal logger with resolved level and structured log function.
*/
export interface InternalLogger {
level: LogLevel
log: typeof createLogEntry
}

/**
* Identity validation settings used when building session strategy and OAuth profile mapping.
* Controls the Zod schema and how unknown keys are handled on user objects.
*/
export interface InternalIdentityConfig<Schema extends SchemaTypes = typeof identitySchema> {
schema?: Schema
schemaAsPartial?: Schema
skipValidation?: boolean
unknownKeys?: "passthrough" | "strict" | "strip"
}

// #region Session
export interface AsymmetricKeyPairFromEnv {
publicKey: string
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/actions/callback/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const callbackAction = (oauth: OAuthProviderRecord) => {
searchParams: { code, state },
context,
} = ctx
return await context.sessionStrategy.oauthCallback({
return await context.session.strategy.oauthCallback({
oauthId,
request,
params: { code, state },
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/actions/csrfToken/csrf-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ export const csrfTokenAction = createEndpoint(
request,
context: { jose, cookies, logger },
} = ctx
const token = getOptionalCookie(request, cookies.csrfToken.name)
const token = getOptionalCookie(request, cookies.store.csrfToken.name)
logger?.log("CSRF_TOKEN_REQUESTED", { structuredData: { has_token: Boolean(token) } })
const csrfToken = await createCSRF(jose, token)
const csrfToken = await createCSRF(jose.instance, token)
logger?.log("CSRF_TOKEN_ISSUED", { structuredData: { issued: Boolean(csrfToken) } })

const headers = new Headers(secureApiHeaders)
headers.append("Set-Cookie", setCookie(cookies.csrfToken.name, csrfToken, cookies.csrfToken.attributes))
headers.append("Set-Cookie", setCookie(cookies.store.csrfToken.name, csrfToken, cookies.store.csrfToken.attributes))
return ctx.json({ csrfToken }, { headers })
},
config
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/api/cancel-email-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const cancelEmailChange = async ({
skipCSRFCheck = false,
doubleSubmitToken = undefined,
}: FunctionAPIContext<CancelEmailChangeAPIOptions>): Promise<CancelEmailChangeAPIReturn> => {
const { logger, sessionStrategy } = ctx
const { logger, session } = ctx
try {
logger?.log("CANCEL_EMAIL_CHANGE_INITIATED", {
structuredData: {
Expand All @@ -36,7 +36,7 @@ export const cancelEmailChange = async ({
return rateLimit
}

await sessionStrategy.cancelEmailChange({
await session.strategy.cancelEmailChange({
token: payload?.token,
userId: undefined,
})
Expand All @@ -51,7 +51,7 @@ export const cancelEmailChange = async ({
return rateLimit
}

await sessionStrategy.cancelEmailChange({
await session.strategy.cancelEmailChange({
token: undefined,
userId: userId!,
})
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/api/change-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const changeEmail = async ({
doubleSubmitToken = undefined,
skipCSRFCheck = false,
}: FunctionAPIContext<ChangeEmailAPIOptions>): Promise<ChangeEmailAPIReturn> => {
const { logger, sessionStrategy, emailChange } = ctx
const { logger, session, emailChange } = ctx
if (!emailChange?.sendEmailChangeVerification) {
throw new AuraAuthError({ code: "CHANGE_EMAIL_NOT_IMPLEMENTED" })
}
Expand All @@ -39,7 +39,7 @@ export const changeEmail = async ({
if (rateLimit) {
return rateLimit
}
const { email, confirmToken, confirmURL, cancelToken, cancelURL } = await sessionStrategy.changeEmail({
const { email, confirmToken, confirmURL, cancelToken, cancelURL } = await session.strategy.changeEmail({
origin,
userId: userId!,
currentPassword: payload.currentPassword,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/api/change-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const changePassword = async ({
skipCSRFCheck = false,
doubleSubmitToken = undefined,
}: FunctionAPIContext<ChangePasswordAPIOptions>): Promise<ChangePasswordAPIReturn> => {
const { logger, sessionStrategy } = ctx
const { logger, session } = ctx
try {
logger?.log("CHANGE_PASSWORD_INITIATED", {
structuredData: {
Expand All @@ -37,7 +37,7 @@ export const changePassword = async ({
return rateLimit
}

await sessionStrategy.changePassword({
await session.strategy.changePassword({
userId: userId!,
currentPassword: payload.currentPassword,
newPassword: payload.newPassword,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/api/forgot-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const forgotPassword = async (
redirect: redirectInit = true,
}: FunctionAPIContext<ForgotPasswordAPIOptions>
): Promise<ForgotPasswordAPIReturn> => {
const { logger, sessionStrategy, passwordReset } = ctx
const { logger, session, passwordReset } = ctx
try {
logger?.log("FORGOT_PASSWORD_INITIATED")
const { request, rateLimit, origin } = await createValidation(
Expand All @@ -27,7 +27,7 @@ export const forgotPassword = async (
if (rateLimit) {
return rateLimit as ForgotPasswordAPIReturn
}
const { token, url } = await sessionStrategy.forgotPassword({ email, origin })
const { token, url } = await session.strategy.forgotPassword({ email, origin })
await passwordReset?.sendPasswordResetEmail?.({ email, token, url })

const builder = new HeadersBuilder(secureApiHeaders)
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/api/get-provider-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const getProviderTokens = async (
if (rateLimit) {
return rateLimit as unknown as GetProviderTokensAPIReturn
}
const getTokens = await ctx.sessionStrategy.getProviderTokens({
const getTokens = await ctx.session.strategy.getProviderTokens({
oauthId,
request,
provider,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/api/get-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const getSession = async <DefaultUser extends User = User>({
headers: headersInit,
}: FunctionAPIContext<GetSessionAPIOptions>): Promise<GetSessionAPIReturn<DefaultUser>> => {
const headers = new HeadersBuilder(secureApiHeaders)
.setCookie(ctx.cookies.sessionToken.name, "", getExpiredCookie(ctx.cookies.sessionToken.attributes))
.setCookie(ctx.cookies.store.sessionToken.name, "", getExpiredCookie(ctx.cookies.store.sessionToken.attributes))
.toHeaders()
const unauthorizedError = {
code: "GET_SESSION_FAILED",
Expand All @@ -25,7 +25,7 @@ export const getSession = async <DefaultUser extends User = User>({
toResponse: () => Response.json({ success: false, session: null }, { status: 401, headers }),
}
try {
const { session, headers } = await ctx.sessionStrategy.getSession({
const { session, headers } = await ctx.session.strategy.getSession({
headers: toStandardizedHeaders(headersInit),
})
if (!session) return unauthorized
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/api/is-provider-connected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const isProviderConnected = async (
return rateLimit
}

const connected = await ctx.sessionStrategy.isProviderConnected({
const connected = await ctx.session.strategy.isProviderConnected({
oauthId,
headers,
sessionByToken: session!,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/api/refresh-userInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const refreshUserInfo = async <DefaultUser extends User = User>(
structuredData: { provider: oauth, userId: userInfo.sub },
})

const { session, headers: newHeaders } = await ctx.sessionStrategy.refreshUserInfo({
const { session, headers: newHeaders } = await ctx.session.strategy.refreshUserInfo({
headers,
user: userInfo,
})
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/api/reset-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const resetPassword = async ({
redirectTo,
redirect: redirectInit = true,
}: FunctionAPIContext<ResetPasswordAPIOptions>): Promise<ResetPasswordAPIReturn> => {
const { logger, sessionStrategy } = ctx
const { logger, session } = ctx
try {
logger?.log("RESET_PASSWORD_INITIATED")
const { request, rateLimit } = await createValidation(
Expand All @@ -26,7 +26,7 @@ export const resetPassword = async ({
return rateLimit as ResetPasswordAPIReturn
}

await sessionStrategy.resetPassword(payload)
await session.strategy.resetPassword(payload)

const builder = new HeadersBuilder(secureApiHeaders)
const { redirect, redirectURL } = await resolveApiRedirect(ctx, request, redirectInit, redirectTo, builder)
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/api/revoke-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const revokeToken = async (
return rateLimit as RevokeTokenAPIReturn
}

const revokeHeaders = await ctx.sessionStrategy.revokeToken({
const revokeHeaders = await ctx.session.strategy.revokeToken({
oauthId,
provider,
headers,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/api/send-magic-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const sendMagicLink = async (
doubleSubmitToken = undefined,
}: FunctionAPIContext<SendMagicLinkAPIOptions>
): Promise<SendMagicLinkAPIReturn> => {
const { logger, sessionStrategy, magicLinks } = ctx
const { logger, session, magicLinks } = ctx
if (!magicLinks?.sendMagicLinkEmail) {
throw new AuraAuthError({ code: "MAGIC_LINK_SEND_EMAIL_NOT_IMPLEMENTED" })
}
Expand All @@ -40,7 +40,7 @@ export const sendMagicLink = async (
if (rateLimit) {
return rateLimit as SendMagicLinkAPIReturn
}
const { token, url } = await sessionStrategy.sendMagicLink({
const { token, url } = await session.strategy.sendMagicLink({
email,
origin,
allowSignUp: magicLinks?.allowSignUp ?? false,
Expand Down
Loading
Loading