Skip to content
Merged
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
2 changes: 2 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- Added experimental support for `Valibot` schema validation in `createAuth`; it now supports both Zod and Valibot schemas to extend the default User fields. [#160](https://github.com/aura-stack-ts/auth/pull/160)

- Extended asymmetric cryptography support to accept JWK (JSON Web Key) format keys in addition to `CryptoKeyPair` across JOSE functions exposed by `createAuth.jose`, including the dedicated `signJWS`, `verifyJWS`, `encryptJWE`, `decryptJWE`, `encodeJWT`, and `decodeJWT` functions. [#159](https://github.com/aura-stack-ts/auth/pull/159)

- Added support for asymmetric cryptography using `public/private` key pairs via `CryptoKeyPair` across JOSE functions exposed by `createAuth.jose`, including the dedicated `signJWS`, `verifyJWS`, `encryptJWE`, `decryptJWE`, `encodeJWT`, and `decodeJWT` functions. [#157](https://github.com/aura-stack-ts/auth/pull/157)
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"dependencies": {
"@aura-stack/jose": "workspace:*",
"@aura-stack/router": "^0.6.0",
"valibot": "^1.3.1",
"zod": "catalog:zod-v4"
},
"devDependencies": {
Expand Down
23 changes: 11 additions & 12 deletions packages/core/src/@types/config.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { createJoseInstance } from "@/jose.ts"
import { createAuthAPI } from "@/api/createApi.ts"
import { createLogEntry } from "@/shared/logger.ts"
import { UserIdentity } from "@/shared/identity.ts"
import { Identities, UserIdentity } from "@/shared/identity.ts"
import type { ZodObject } from "zod/v4"
import type { BuiltInOAuthProvider } from "@/oauth/index.ts"
import type { SerializeOptions } from "@aura-stack/router/cookie"
import type { EditableShape, Prettify, ZodShapeToObject } from "@/@types/utility.ts"
import type { ConfigSchema, FromShapeToObject, Prettify } from "@/@types/utility.ts"
import type { OAuthProviderCredentials, OAuthProviderRecord } from "@/@types/oauth.ts"
import type { JWTKey, SessionConfig, SessionStrategy, User, UserShape } from "@/@types/session.ts"
import type { JWTKey, SessionConfig, SessionStrategy, User } from "@/@types/session.ts"
import { ObjectSchema } from "valibot"

/**
* Main configuration interface for Aura Auth.
* This is the user-facing configuration object passed to `createAuth()`.
*/
export interface AuthConfig<Identity extends EditableShape<UserShape> = EditableShape<UserShape>> {
export interface AuthConfig<Identity extends Identities> {
/**
* 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
Expand Down Expand Up @@ -45,7 +46,7 @@ export interface AuthConfig<Identity extends EditableShape<UserShape> = Editable
* ```
*/
// @todo: add type inference for built-in providers
oauth: (BuiltInOAuthProvider | OAuthProviderCredentials<any, ZodShapeToObject<Identity>>)[]
oauth: (BuiltInOAuthProvider | OAuthProviderCredentials<any, FromShapeToObject<Identity>>)[]
/**
* Cookie options defines the configuration for cookies used in Aura Auth.
* It includes a prefix for cookie names and flag options to determine
Expand Down Expand Up @@ -167,7 +168,7 @@ export interface AuthConfig<Identity extends EditableShape<UserShape> = Editable
*/
identity?: Partial<{
skipValidation: boolean
schema: ZodObject<Identity>
schema: ConfigSchema<Identity>
unknownKeys: "passthrough" | "strict" | "strip"
}>
/**
Expand Down Expand Up @@ -283,7 +284,7 @@ export interface InternalLogger {
* 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 IdentityConfig<Schema extends ZodObject<any> = typeof UserIdentity> {
export interface IdentityConfig<Schema extends ZodObject<any> | ObjectSchema<any, undefined> = typeof UserIdentity> {
schema?: Schema
skipValidation?: boolean
unknownKeys?: "passthrough" | "strict" | "strip"
Expand Down Expand Up @@ -317,7 +318,7 @@ export interface CredentialsProviderContext<T> {
/**
* Interface for the credentials provider.
*/
export interface CredentialsProvider<Identity extends EditableShape<UserShape> = EditableShape<UserShape>> {
export interface CredentialsProvider<Identity extends Identities> {
hash?: (password: string, salt?: string, iterations?: number) => Promise<string>
verify?: (password: string, hashedPassword: string) => Promise<boolean>
/**
Expand All @@ -326,7 +327,7 @@ export interface CredentialsProvider<Identity extends EditableShape<UserShape> =
*/
authorize: (
ctx: CredentialsProviderContext<CredentialsPayload>
) => Promise<ZodShapeToObject<Identity> | null> | ZodShapeToObject<Identity> | null
) => Promise<FromShapeToObject<Identity> | null> | FromShapeToObject<Identity> | null
}

/**
Expand Down Expand Up @@ -375,9 +376,7 @@ export interface AuthInstance<DefaultUser extends User = User> {
/**
* Extended context used inside the library with both secure and standard cookie materializations.
*/
export type InternalContext<Identity extends EditableShape<UserShape>> = RouterGlobalContext<
ZodShapeToObject<Identity> & User
> & {
export type InternalContext<Identity extends Identities> = RouterGlobalContext<FromShapeToObject<Identity> & User> & {
cookieConfig: {
secure: CookieStoreConfig
standard: CookieStoreConfig
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/@types/session.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { infer as Infer } from "zod/v4/core"
import type { TypedJWTPayload } from "@aura-stack/jose"
import type { UserIdentity, UserShape } from "@/shared/identity.ts"
import type { DeepPartial, EditableShape, Prettify, ZodShapeToObject } from "@/@types/utility.ts"
import type { Identities, UserIdentity } from "@/shared/identity.ts"
import type { DeepPartial, FromShapeToObject, Prettify } from "@/@types/utility.ts"
import type { CookieStoreConfig, IdentityConfig, InternalLogger, JoseInstance } from "@/@types/config.ts"
import type { JWK } from "@aura-stack/jose/jose"

Expand Down Expand Up @@ -226,9 +226,9 @@ export interface SessionStrategy<DefaultUser extends User = User> {
}

/** Inputs for constructing a session strategy implementation for a given identity schema. */
export interface CreateSessionStrategyOptions<Identity extends EditableShape<UserShape>> {
export interface CreateSessionStrategyOptions<Identity extends Identities> {
config?: SessionConfig
jose: JoseInstance<ZodShapeToObject<Identity> & User>
jose: JoseInstance<FromShapeToObject<Identity> & User>
cookies: () => CookieStoreConfig
logger?: InternalLogger
identity: IdentityConfig
Expand Down
29 changes: 28 additions & 1 deletion packages/core/src/@types/utility.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { AuthInstance } from "@/@types/config.ts"
import type { Session, User } from "@/@types/session.ts"
import type { Session, User, UserShape } from "@/@types/session.ts"
import type { ZodObject, ZodRawShape, ZodTypeAny, infer as Infer } from "zod/v4"
import type { ObjectSchema, BaseSchema, AnySchema as AnyValibotSchema, ObjectEntries, InferOutput } from "valibot"
import { UserShapeValibot } from "@/shared/identity.ts"

/** Expands intersection types into a single flat object type for readable editor hints. */
export type Prettify<T> = { [K in keyof T]: T[K] }
Expand All @@ -18,6 +20,25 @@ export type EditableShape<T extends ZodRawShape> = {
[K in keyof T]: T[K] extends ZodObject<infer Inner extends ZodRawShape> ? ZodObject<EditableShape<Inner>> : ZodTypeAny
}

export type EditableShapeZod<T extends ZodRawShape> = EditableShape<T>

type AnyShape = Record<string, AnyValibotSchema>

export type EditableShapeValibot<T extends ObjectEntries> = {
[K in keyof T]: T[K] extends ObjectSchema<infer Inner extends AnyShape, undefined>
? ObjectSchema<EditableShapeValibot<Inner>, undefined>
: BaseSchema<any, any, any>
}

export type ConfigSchema<T extends EditableShape<UserShape> | EditableShapeValibot<UserShapeValibot>> =
T extends EditableShape<UserShape>
? ZodObject<T & ZodRawShape>
: T extends EditableShapeValibot<UserShapeValibot>
? ObjectSchema<T & ObjectEntries, undefined>
: never

export type ValibotShapeToObject<S extends ObjectEntries> = Merge<InferOutput<ObjectSchema<S, undefined>>, User>

/** Merges type `B` over `A`, replacing overlapping keys with `B`. */
export type Merge<A, B> = Omit<A, keyof B> & B

Expand All @@ -27,6 +48,12 @@ export type Merge<A, B> = Omit<A, keyof B> & B
*/
export type ZodShapeToObject<S extends ZodRawShape = ZodRawShape> = Merge<Infer<ZodObject<S>>, User>

export type FromShapeToObject<S> = S extends ZodRawShape
? ZodShapeToObject<S>
: S extends ObjectEntries
? ValibotShapeToObject<S>
: never

/** Recursively makes every property required. */
export type DeepRequired<T> = {
[K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K]
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/actions/signIn/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ 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"
import { Identities } from "@/shared/identity.ts"

/**
* Resolves trusted origins from config (array or function).
*/
export const getTrustedOrigins = async (request: Request, trustedOrigins: AuthConfig["trustedOrigins"]): Promise<string[]> => {
export const getTrustedOrigins = async (request: Request, trustedOrigins: AuthConfig<Identities>["trustedOrigins"]): Promise<string[]> => {
if (!trustedOrigins) return []
const raw = typeof trustedOrigins === "function" ? await trustedOrigins(request) : trustedOrigins
return Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : []
Expand Down
12 changes: 8 additions & 4 deletions packages/core/src/actions/updateSession/updateSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { createEndpoint, createEndpointConfig } from "@aura-stack/router"
import { updateSession } from "@/api/updateSession.ts"
import type { User } from "@/@types/session.ts"
import type { IdentityConfig } from "@/@types/config.ts"
import { UserIdentity } from "@/shared/identity.ts"

export const config = (identity: IdentityConfig) => {
export const config = (_identity: IdentityConfig) => {
return createEndpointConfig({
schemas: {
body: z.object({
user: identity.schema?.partial().optional(),
/**
* @todo add support for valibot schemas in the body as well, currently only Zod is supported
*/
user: UserIdentity.partial().optional(),
expires: z.coerce.date().optional(),
}),
},
Expand All @@ -24,8 +28,8 @@ export const updateSessionAction = (identity: IdentityConfig) => {
ctx: ctx.context,
headers: ctx.request.headers,
session: {
user: ctx.body.user as User,
expires: ctx.body.expires?.toISOString(),
user: ctx.body?.user as User,
expires: ctx.body?.expires?.toISOString(),
},
})
return toResponse()
Expand Down
11 changes: 6 additions & 5 deletions packages/core/src/createAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import {
csrfTokenAction,
updateSessionAction,
} from "@/actions/index.ts"
import type { AuthConfig, AuthInstance, EditableShape, ZodShapeToObject, UserShape } from "@/@types/index.ts"
import { Identities } from "@/shared/identity.ts"
import type { AuthConfig, AuthInstance, EditableShape, FromShapeToObject, UserShape } from "@/@types/index.ts"

const createInternalConfig = <Identity extends EditableShape<UserShape>>(config?: AuthConfig<Identity>): RouterConfig => {
const createInternalConfig = <Identity extends Identities>(config?: AuthConfig<Identity>): RouterConfig => {
const context = createContext<Identity>(config)
return {
basePath: config?.basePath ?? "/auth",
Expand All @@ -30,7 +31,7 @@ const createInternalConfig = <Identity extends EditableShape<UserShape>>(config?
}
}

export const createAuthInstance = <Identity extends EditableShape<UserShape>>(authConfig: AuthConfig<Identity>) => {
export const createAuthInstance = <Identity extends Identities>(authConfig: AuthConfig<Identity>) => {
const config = createInternalConfig<Identity>(authConfig)
const router = createRouter(
[
Expand Down Expand Up @@ -75,8 +76,8 @@ export const createAuthInstance = <Identity extends EditableShape<UserShape>>(au
* }]
* })
*/
export const createAuth = <Identity extends EditableShape<UserShape>>(config: AuthConfig<Identity>) => {
const authInstance = createAuthInstance<Identity>(config) as unknown as AuthInstance<ZodShapeToObject<Identity>>
export const createAuth = <Identity extends Identities = EditableShape<UserShape>>(config: AuthConfig<Identity>) => {
const authInstance = createAuthInstance<Identity>(config) as unknown as AuthInstance<FromShapeToObject<Identity>>
authInstance.handlers.ALL = async (request: Request) => {
const method = request.method.toUpperCase()
const methodHandlers = {
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/router/context.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { createJoseInstance } from "@/jose.ts"
import { createCookieStore } from "@/cookie.ts"
import { UserIdentity } from "@/shared/identity.ts"
import { Identities, UserIdentity } from "@/shared/identity.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, EditableShape, InternalContext, ZodShapeToObject, UserShape } from "@/@types/index.ts"
import type { AuthConfig, InternalContext, FromShapeToObject } from "@/@types/index.ts"

export const createContext = <Identity extends EditableShape<UserShape>>(config?: AuthConfig<Identity>) => {
export const createContext = <Identity extends Identities>(config?: AuthConfig<Identity>) => {
const trustedProxyHeadersEnv = getEnv("TRUSTED_PROXY_HEADERS")
const useProxyHeaders =
trustedProxyHeadersEnv === undefined ? (config?.trustedProxyHeaders ?? false) : getEnvBoolean("TRUSTED_PROXY_HEADERS")
Expand All @@ -16,7 +16,7 @@ export const createContext = <Identity extends EditableShape<UserShape>>(config?
const cookieOverrides = config?.cookies?.overrides ?? {}
const secureCookieStore = createCookieStore(true, cookiePrefix, cookieOverrides, logger)
const standardCookieStore = createCookieStore(false, cookiePrefix, cookieOverrides, logger)
const jose = createJoseInstance<ZodShapeToObject<Identity>>(config?.secret, config?.session)
const jose = createJoseInstance<FromShapeToObject<Identity>>(config?.secret, config?.session)

const ctx = {
oauth: createBuiltInOAuthProviders(config?.oauth),
Expand Down
61 changes: 44 additions & 17 deletions packages/core/src/schema-registry.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,71 @@
import { isZodSchema } from "@/shared/assert.ts"
import { formatZodError } from "@/shared/utils.ts"
import { UserIdentity } from "@/shared/identity.ts"
import { infer as Infer, type ZodObject } from "zod/v4"
import { AuthValidationError } from "@/shared/errors.ts"
import { strictObject, objectWithRest, transform, pipe, unknown, safeParseAsync, partial, type ObjectSchema } from "valibot"
import type { ZodObject } from "zod/v4"
import type { IdentityConfig } from "@/@types/config.ts"

export const stripUnknownKeys = <T extends ZodObject<any>>(schema: T, unknownKeys: "strip" | "passthrough" | "strict") => {
export const stripUnknownKeys = <T extends ZodObject<any> | ObjectSchema<any, undefined>>(
schema: T,
unknownKeys: "strip" | "passthrough" | "strict"
): any => {
switch (unknownKeys) {
case "strip":
return schema.strip()
return isZodSchema(schema)
? schema.strip()
: pipe(
objectWithRest((schema as ObjectSchema<any, undefined>).entries, unknown()),
transform((input) => {
const result: any = {}
for (const key in (schema as ObjectSchema<any, undefined>).entries) {
if (key in input) result[key] = input[key]
}
return result
})
)
case "passthrough":
return schema.loose()
return isZodSchema(schema)
? schema.loose()
: objectWithRest((schema as ObjectSchema<any, undefined>).entries, unknown())
case "strict":
return schema.strict()
return isZodSchema(schema) ? schema.strict() : strictObject((schema as ObjectSchema<any, undefined>).entries)
default:
throw new AuthValidationError(
"INVALID_IDENTITY_VALIDATION_FAILED",
`Invalid unknownKeys configuration: ${unknownKeys}. Valid options are: "strip", "passthrough", "strict".`
)
}
}

export const createSchemaRegistry = <Identity extends ZodObject<any>>(config: IdentityConfig<Identity>) => {
export const createSchemaRegistry = <Identity extends ZodObject<any> | ObjectSchema<any, any>>(
config: IdentityConfig<Identity>
) => {
const schema = stripUnknownKeys(config.schema ?? UserIdentity, config.unknownKeys ?? "strip")
const partialSchema = schema.partial()
const partialSchema = isZodSchema(schema) ? schema.partial() : partial(schema)

const parse = async <T = Infer<typeof schema>>(data: unknown = {}) => {
const parsed = await schema.safeParseAsync(data)
const parse = async (data: unknown = {}) => {
const isZod = isZodSchema(schema)
const parsed: any = isZod ? await schema.safeParseAsync(data) : await safeParseAsync(schema as any, data)
if (!parsed.success) {
const details = JSON.stringify(formatZodError(parsed.error), null, 2)
const details = JSON.stringify(isZod ? formatZodError(parsed.error) : {}, null, 2)
throw new AuthValidationError("INVALID_IDENTITY_VALIDATION_FAILED", details, {
cause: parsed.error,
cause: isZod ? parsed.error : undefined,
})
}
return parsed.data as T
return isZod ? parsed.data : parsed.output
}

const parseAsPartial = async <T = Partial<Infer<typeof schema>>>(data: unknown = {}) => {
const parsed = await partialSchema.safeParseAsync(data)
const parseAsPartial = async (data: unknown = {}) => {
const isZod = isZodSchema(partialSchema)
const parsed: any = isZod ? await partialSchema.safeParseAsync(data) : await safeParseAsync(partialSchema as any, data)
if (!parsed.success) {
const details = JSON.stringify(formatZodError(parsed.error), null, 2)
const details = JSON.stringify(isZod ? formatZodError(parsed.error) : {}, null, 2)
throw new AuthValidationError("INVALID_IDENTITY_VALIDATION_FAILED", details, {
cause: parsed.error,
cause: isZod ? parsed.error : undefined,
})
}
return parsed.data as T
return isZod ? parsed.data : parsed.output
}

return { parse, parseAsPartial }
Expand Down
Loading
Loading