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
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",
"arktype": "^2.2.0",
"valibot": "^1.3.1",
"zod": "catalog:zod-v4"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/@types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ConfigSchema, FromShapeToObject, Prettify } from "@/@types/utility
import type { OAuthProviderCredentials, OAuthProviderRecord } from "@/@types/oauth.ts"
import type { JWTKey, SessionConfig, SessionStrategy, User } from "@/@types/session.ts"
import { ObjectSchema } from "valibot"
import { Type } from "arktype"

/**
* Main configuration interface for Aura Auth.
Expand Down Expand Up @@ -284,7 +285,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> | ObjectSchema<any, undefined> = typeof UserIdentity> {
export interface IdentityConfig<Schema extends ZodObject<any> | ObjectSchema<any, undefined | Type<any>> = typeof UserIdentity> {
schema?: Schema
skipValidation?: boolean
unknownKeys?: "passthrough" | "strict" | "strip"
Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/@types/utility.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Type } from "arktype"
import type { AuthInstance } from "@/@types/config.ts"
import type { Session, User, UserShape } from "@/@types/session.ts"
import type { Session, User } from "@/@types/session.ts"
import type { ZodObject, ZodRawShape, ZodTypeAny, infer as Infer } from "zod/v4"
import type { Identities, IsArkType, IsZod, UserShapeValibot } from "@/shared/identity.ts"
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 Down Expand Up @@ -30,15 +31,21 @@ export type EditableShapeValibot<T extends ObjectEntries> = {
: BaseSchema<any, any, any>
}

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

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

export type ArktypeShapeToObject<S extends Type> = S extends Type<infer Shape> ? Wrap<Merge<Shape, User>> : never

export type EditableShapeArkType<T extends Type> = T extends Type<infer Shape> ? Type<{ [K in keyof Shape]: any }> : never

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

Expand All @@ -52,7 +59,9 @@ export type FromShapeToObject<S> = S extends ZodRawShape
? ZodShapeToObject<S>
: S extends ObjectEntries
? ValibotShapeToObject<S>
: never
: S extends Type
? ArktypeShapeToObject<S>
: never

/** Recursively makes every property required. */
export type DeepRequired<T> = {
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/actions/signIn/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { Identities } from "@/shared/identity.ts"
/**
* Resolves trusted origins from config (array or function).
*/
export const getTrustedOrigins = async (request: Request, trustedOrigins: AuthConfig<Identities>["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
4 changes: 2 additions & 2 deletions packages/core/src/createAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
csrfTokenAction,
updateSessionAction,
} from "@/actions/index.ts"
import { Identities } from "@/shared/identity.ts"
import type { AuthConfig, AuthInstance, EditableShape, FromShapeToObject, UserShape } from "@/@types/index.ts"
import type { EditableShape, Identities, UserShape } from "@/shared/identity.ts"
import type { AuthConfig, AuthInstance, FromShapeToObject } from "@/@types/index.ts"

const createInternalConfig = <Identity extends Identities>(config?: AuthConfig<Identity>): RouterConfig => {
const context = createContext<Identity>(config)
Expand Down
69 changes: 48 additions & 21 deletions packages/core/src/schema-registry.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
import { isZodSchema } from "@/shared/assert.ts"
import { isArkType, isValibotSchema, isZodSchema } from "@/shared/assert.ts"
import { formatZodError } from "@/shared/utils.ts"
import { UserIdentity } from "@/shared/identity.ts"
import { AuthValidationError } from "@/shared/errors.ts"
import { strictObject, objectWithRest, transform, pipe, unknown, safeParseAsync, partial, type ObjectSchema } from "valibot"
import type { Type } from "arktype"
import type { ZodObject } from "zod/v4"
import type { IdentityConfig } from "@/@types/config.ts"

export const stripUnknownKeys = <T extends ZodObject<any> | ObjectSchema<any, undefined>>(
export const stripUnknownKeys = <T extends ZodObject<any> | ObjectSchema<any, undefined> | Type>(
schema: T,
unknownKeys: "strip" | "passthrough" | "strict"
): any => {
switch (unknownKeys) {
case "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
})
)
: isValibotSchema(schema)
? 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
})
)
: isArkType(schema)
? schema.onUndeclaredKey("delete")
: undefined
case "passthrough":
return isZodSchema(schema)
? schema.loose()
: objectWithRest((schema as ObjectSchema<any, undefined>).entries, unknown())
: isValibotSchema(schema)
? objectWithRest((schema as ObjectSchema<any, undefined>).entries, unknown())
: isArkType(schema)
? schema.onUndeclaredKey("ignore")
: undefined
case "strict":
return isZodSchema(schema) ? schema.strict() : strictObject((schema as ObjectSchema<any, undefined>).entries)
return isZodSchema(schema)
? schema.strict()
: isValibotSchema(schema)
? strictObject((schema as ObjectSchema<any, undefined>).entries)
: isArkType(schema)
? schema.onUndeclaredKey("reject")
: undefined
default:
throw new AuthValidationError(
"INVALID_IDENTITY_VALIDATION_FAILED",
Expand All @@ -38,22 +53,34 @@ export const stripUnknownKeys = <T extends ZodObject<any> | ObjectSchema<any, un
}
}

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

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 parsed: any = isZod
? await schema.safeParseAsync(data)
: isValibotSchema(schema)
? await safeParseAsync(schema as any, data)
: isArkType(schema)
? schema(data)
: undefined
if ((!isArkType(schema) && !parsed.success) || (isArkType(schema) && !schema?.allows(data))) {
const details = JSON.stringify(isZod ? formatZodError(parsed.error) : {}, null, 2)
throw new AuthValidationError("INVALID_IDENTITY_VALIDATION_FAILED", details, {
cause: isZod ? parsed.error : undefined,
cause: isZod ? parsed.error : isArkType(schema) ? parsed.errors : undefined,
})
}
return isZod ? parsed.data : parsed.output
return isZod ? parsed.data : isValibotSchema(schema) ? parsed.output : isArkType(schema) ? parsed : undefined
}

const parseAsPartial = async (data: unknown = {}) => {
Expand Down
8 changes: 2 additions & 6 deletions packages/core/src/session/stateless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,11 @@ export const createStatelessStrategy = <DefaultUser extends User = User>({

const expiresAt = updateExpires({ exp })
if (!expiresAt) {
const userSession = identity.skipValidation
? session.user
: await schema.parse(session.user)
const userSession = identity.skipValidation ? session.user : await schema.parse(session.user)
return { session: { expires: session.expires, user: userSession }, headers }
}

const newSessionPayload = identity.skipValidation
? session.user
: await schema.parse(session.user)
const newSessionPayload = identity.skipValidation ? session.user : await schema.parse(session.user)
const newSession = { user: newSessionPayload, expires: expiresAt.toISOString() }

const issuedAt = strategy === "absolute" ? _iat : Math.floor(Date.now() / 1000)
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/shared/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
import type { JWK } from "@aura-stack/jose/jose"
import { BaseSchema } from "valibot"
import { ZodObject, ZodTypeAny } from "zod"
import { Type } from "arktype"

export const isFalsy = (value: unknown): boolean => {
return value === false || value === 0 || value === "" || value === null || value === undefined || Number.isNaN(value)
Expand Down Expand Up @@ -197,3 +198,7 @@ export const isZodSchema = (value: unknown): value is ZodObject<any> => {
export const isZodEntries = (value: unknown): value is Record<string, ZodTypeAny> => {
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every(isZodSchema)
}

export const isArkType = (value: unknown): value is Type<{}, {}> => {
return typeof value === "function" && value !== null && "allows" in value && "assert" in value
}
50 changes: 36 additions & 14 deletions packages/core/src/shared/identity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { z } from "zod/v4"
import type { EditableShape, EditableShapeValibot } from "@/@types/utility.ts"
import * as valibot from "valibot"
import { isValibotEntries } from "./assert.ts"
import { type } from "arktype"
import { isArkType, isValibotEntries, isZodEntries } from "@/shared/assert.ts"
import type { EditableShape, EditableShapeArkType, EditableShapeValibot } from "@/@types/utility.ts"

export type {
InferUser,
Expand All @@ -17,7 +18,7 @@ export const UserIdentity = z.object({
sub: z.string(),
name: z.string().nullable().optional(),
image: z.string().nullable().optional(),
email: z.string().email().nullable().optional(),
email: z.email().nullable().optional(),
})

export const UserIdentityValibot = valibot.object({
Expand All @@ -27,23 +28,44 @@ export const UserIdentityValibot = valibot.object({
email: valibot.optional(valibot.nullable(valibot.pipe(valibot.string(), valibot.email()))),
})

export type UserShape = (typeof UserIdentity)["shape"]
export const UserIdentityArkType = type({
sub: "string",
name: "string | null?",
image: "string | null?",
email: "string.email | null?",
})

export type UserShape = typeof UserIdentity.shape
export type UserShapeValibot = typeof UserIdentityValibot.entries
export type UserShapeArkType = typeof UserIdentityArkType

export type IsArkType<T extends Identities> = T extends EditableShapeArkType<UserShapeArkType> ? true : false
export type IsZod<T extends Identities> = T extends EditableShape<UserShape> ? true : false
export type IsValibot<T extends Identities> = T extends EditableShapeValibot<UserShapeValibot> ? true : false

export type Identities = EditableShape<UserShape> | EditableShapeValibot<UserShapeValibot>
export type Identities =
| EditableShape<UserShape>
| EditableShapeValibot<UserShapeValibot>
| EditableShapeArkType<UserShapeArkType>

type ReturnShapeType<S> =
S extends EditableShape<UserShape>
? z.ZodObject<S>
: S extends EditableShapeValibot<UserShapeValibot>
? valibot.ObjectSchema<S, undefined>
: never
type ReturnShapeType<T> =
T extends EditableShape<UserShape>
? z.ZodObject<T>
: T extends EditableShapeValibot<UserShapeValibot>
? valibot.ObjectSchema<T, undefined>
: T extends EditableShapeArkType<UserShapeArkType>
? T
: never

export const createIdentity = <S extends EditableShape<UserShape> | EditableShapeValibot<UserShapeValibot>>(
shape: S
): ReturnShapeType<S> => {
export const createIdentity = <S extends Identities>(shape: S): ReturnShapeType<S> => {
if (isArkType(shape)) {
return shape as unknown as ReturnShapeType<S>
}
if (isValibotEntries(shape)) {
return valibot.object(shape) as unknown as ReturnShapeType<S>
}
if (isZodEntries(shape)) {
return z.object(shape) as unknown as ReturnShapeType<S>
}
return z.object(shape) as unknown as ReturnShapeType<S>
}
Loading
Loading