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

### Added

- Added `InferSignUp`, a utility type for inferring the sign-up payload from `createAuth().signUp.schema`. This type can be reused as the second generic parameter of `createAuthClient()` to ensure consistent typing between server and client authentication configurations. [#190](https://github.com/aura-stack-ts/auth/pull/190)

- Added a `signUp` client function accessible via `createAuthClient`, allowing interaction with the mounted `POST /signUp` endpoint. [#184](https://github.com/aura-stack-ts/auth/pull/184)

- Introduced an experimental `signUp` flow for both the API and endpoint definitions. The new action enables user account creation within the authentication system and provides customizable payload validation through the supported schema. To enable this feature, developers must configure the `signUp` option when calling `createAuth`. [#183](https://github.com/aura-stack-ts/auth/pull/183)

- Added support for a custom `userInfo` function in OAuth provider configuration, enabling callers to perform the user info request themselves. The `userInfo` option continues to accept either a URL string or an object with a `url` and optional request options (for example, custom headers). [#182](https://github.com/aura-stack-ts/auth/pull/182)

### Fixed

- Fixed type inference for authentication actions created with `createAuth()` and `createAuthClient()`. The `signUp.schema` configuration is now inferred correctly, improving type safety and reducing the need for manual type annotations. [#190](https://github.com/aura-stack-ts/auth/pull/190)

### Changed

- Refactored and standardized error handling across authentication flows. All authentication errors now extend the `AuraAuthError` base class, providing a consistent error model throughout the library. Error objects now expose structured metadata, including `type`, `code`, `message`, and `userMessage`. [#190](https://github.com/aura-stack-ts/auth/pull/190)

---

## [0.7.2] - 2026-06-05
Expand Down
54 changes: 53 additions & 1 deletion packages/core/src/@types/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Type } from "arktype"
import type { TProperties, TObject, TSchema } from "typebox"
import type { AuthInstance } from "@/@types/config.ts"
import type { Session, User } from "@/@types/session.ts"
import type { ZodObject, ZodRawShape, ZodTypeAny, infer as Infer } from "zod/v4"
import type { ZodObject, ZodRawShape, ZodTypeAny, infer as Infer, ZodOptional } from "zod/v4"
import type { Identities, IsArkType, IsZod, UserShapeTypeBox, UserShapeValibot } from "@/shared/identity.ts"
import type { ObjectSchema, BaseSchema, AnySchema as AnyValibotSchema, ObjectEntries, InferOutput } from "valibot"
import type { InferSchema } from "@aura-stack/router"

/** 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 @@ -80,6 +81,28 @@ export type FromShapeToObject<S> = S extends ZodRawShape
? S
: never

export type EditableToSchema<T> =
T extends EditableShape<infer S>
? ZodObject<S>
: T extends EditableShapeValibot<infer S>
? ObjectSchema<S, undefined>
: T extends EditableShapeTypebox<infer S>
? TObject<S>
: T extends EditableShapeArkType<any>
? T
: never

export type ReturnUpdateSessionShape<T> =
T extends EditableShape<infer S>
? ZodObject<{ user?: ZodObject<S>; expires?: ZodOptional<ZodTypeAny> }>
: T extends EditableShapeValibot<infer S>
Comment thread
halvaradop marked this conversation as resolved.
? ObjectSchema<{ user?: ObjectSchema<S, undefined>; expires?: BaseSchema<any, any, any> }, undefined>
: T extends EditableShapeArkType<any>
? Type<{ user?: T; expires?: Type<string> }>
: T extends EditableShapeTypebox<infer S>
? TObject<{ user?: TObject<S>; expires?: TSchema }>
: 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 Expand Up @@ -153,6 +176,35 @@ export type UserFrom<T extends ZodObject> = Prettify<ZodShapeToObject<InferZodSh
*/
export type SessionFrom<T extends ZodObject> = Wrap<Session<Wrap<UserFrom<T>>>>

/**
* Infers the sign-up data type from an {@link AuthInstance} config's `signUp.schema`. It supports
* Zod, Valibot and ArkType schemas.
*
* > For TypeBox its recommended to use the `Static` utility type directly to infer the schema.
*
* @example
* const auth = createAuth({
* oauth: [],
* signUp: {
* schema: z.object({
* username: z.string(),
* nickname: z.string(),
* password: z.string(),
* })
* }
* })
*
* type SignUp = InferSignUp<typeof auth>
*/
export type InferSignUp<Config extends AuthInstance> =
Config extends AuthInstance<infer _, infer SignUpSchema>
? Wrap<RemoveIndexSignature<InferSchema<SignUpSchema>>>
: Record<string, any>

export type RemoveIndexSignature<T> = {
[K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]
}

/**
* HTTP `Response` with `json()` typed to resolve to `Body` (defaults to `unknown`).
*/
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/actions/signUp/signUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { signUp } from "@/api/signUp.ts"
import { createEndpoint, createEndpointConfig } from "@aura-stack/router"
import { RedirectOptionsSchema } from "@/schemas.ts"
import type { SignUpConfig } from "@/@types/config.ts"
import type { Identities, SchemaTypes } from "@/shared/identity.ts"

const signUpConfig = (config: SignUpConfig<any, any>) => {
const signUpConfig = <Identity extends Identities, SignUpSchema extends SchemaTypes>(
config: SignUpConfig<Identity, SignUpSchema>
) => {
return createEndpointConfig({
schemas: {
body: config?.schema,
Expand All @@ -18,15 +21,18 @@ const signUpConfig = (config: SignUpConfig<any, any>) => {
*
* @returns The signed-up user's session
*/
export const signUpAction = (config: SignUpConfig<any, any>) => {
export const signUpAction = <Identity extends Identities, SignUpSchema extends SchemaTypes>(
config: SignUpConfig<Identity, SignUpSchema>
) => {
return createEndpoint(
"POST",
"/signUp",
async (ctx) => {
const payload = ctx.body
// @ts-ignore - Deep generic inference with router body type is currently too expensive.
const payload = ctx.body as any
const { toResponse } = await signUp({
ctx: ctx.context,
payload,
payload: payload,
request: ctx.request,
headers: ctx.request.headers,
redirect: ctx.searchParams.redirect,
Expand Down
12 changes: 5 additions & 7 deletions packages/core/src/actions/updateSession/updateSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { createEndpoint, createEndpointConfig } from "@aura-stack/router"
import { RedirectOptionsSchema } from "@/schemas.ts"
import { updateSession } from "@/api/updateSession.ts"
import { getFullSchema } from "@/validator/registry.ts"
import type { User } from "@/@types/session.ts"
import type { SchemaRegistryContext } from "@/@types/config.ts"
import type { Identities } from "@/shared/identity.ts"

export const config = (identity: SchemaRegistryContext) => {
export const config = <Identity extends Identities>(identity: SchemaRegistryContext) => {
return createEndpointConfig({
schemas: {
body: getFullSchema(identity.schemaRegistry.schemaAsPartial),
body: getFullSchema<Identity>(identity.schemaRegistry.schemaAsPartial),
Comment thread
halvaradop marked this conversation as resolved.
searchParams: RedirectOptionsSchema,
},
})
Expand All @@ -19,16 +19,14 @@ export const updateSessionAction = (identity: SchemaRegistryContext) => {
"PATCH",
"/session",
async (ctx) => {
const session = ctx.body
const { toResponse } = await updateSession({
ctx: ctx.context,
request: ctx.request,
headers: ctx.request.headers,
redirect: ctx.searchParams.redirect,
redirectTo: ctx.searchParams.redirectTo,
session: {
user: ctx.body?.user as User,
expires: ctx.body?.expires?.toISOString(),
},
session: session as any,
})
return toResponse()
},
Expand Down
5 changes: 1 addition & 4 deletions packages/core/src/api/createApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,13 @@ import type {
SignUpAPIOptions,
SignUpAPIReturn,
Wrap,
RemoveIndexSignature,
} from "@/@types/index.ts"
import type { ZodObject } from "zod"
import type { SchemaTypes } from "@/shared/identity.ts"

type InferSignUp<T> = Wrap<RemoveIndexSignature<InferSchema<T>>>

type RemoveIndexSignature<T> = {
[K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]
}

export const createAuthAPI = <DefaultUser extends User = User, SignUpSchema extends SchemaTypes = ZodObject<any>>(
ctx: GlobalContext
) => {
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ export const createAuthClient = <
const { redirectTo } = options ?? {}
const response = await client.post("/signIn/credentials", {
body: options.payload,
// @ts-ignore - Fix type here - go to @aura-stack/router.
searchParams: {
redirectTo,
redirect: false,
Expand Down Expand Up @@ -174,7 +173,6 @@ export const createAuthClient = <
const { redirectTo } = options ?? {}
// @ts-ignore
const response = await client.post("/signUp", {
// @ts-ignore - Fix type here - go to @aura-stack/router.
body: options.payload,
searchParams: {
redirectTo,
Expand Down Expand Up @@ -226,6 +224,7 @@ export const createAuthClient = <
body: {
// @ts-ignore - Fix type here - go to @aura-stack/router.
user,
// @ts-ignore - Fix type here - go to @aura-stack/router.
expires: session.expires ? new Date(session.expires) : undefined,
},
searchParams: {
Expand Down Expand Up @@ -267,7 +266,6 @@ export const createAuthClient = <
throw new AuraAuthError({ code: "CSRF_TOKEN_MISSING" })
}

// @ts-ignore - Fix type here - go to @aura-stack/router.
const response = await client.post("/signOut", {
searchParams: {
redirectTo: options?.redirectTo,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/createAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const createAuthInstance = <Identity extends Identities, SignUpSchema ext
signOutAction,
csrfTokenAction,
updateSessionAction(config.context.identity as SchemaRegistryContext),
signUpAction(config.context.signUp as SignUpConfig<Identity, SignUpSchema>),
signUpAction<Identity, SignUpSchema>(config.context.signUp as SignUpConfig<Identity, SignUpSchema>),
],
config
)
Expand Down
11 changes: 7 additions & 4 deletions packages/core/src/validator/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { z } from "zod/v4"
import * as valibot from "valibot"
import { type } from "arktype"
import { IsObject, Type as Typebox } from "typebox"
import { UserIdentity, type SchemaTypes } from "@/shared/identity.ts"
import { UserIdentity, type Identities, type SchemaTypes } from "@/shared/identity.ts"
import { isArkType, isValibotSchema, isZodSchema } from "@/shared/assert.ts"
import { AuraAuthError } from "@/shared/errors.ts"
import { createValidator } from "@aura-stack/router/validator"
import type { IdentityConfig } from "@/@types/config.ts"
import type { EditableToSchema, ReturnUpdateSessionShape } from "@/@types/utility.ts"

export const deriveSchema = <Schema extends SchemaTypes>(
schema: Schema,
Expand Down Expand Up @@ -116,7 +117,9 @@ export const deriveSchemaWithJWT = <Schema extends SchemaTypes>(schema: Schema):
throw new AuraAuthError({ code: "SCHEMA_UNSUPPORTED" })
}

export const getFullSchema = <Schema extends SchemaTypes>(schema: Schema): any => {
export const getFullSchema = <Identity extends Identities, Schema = EditableToSchema<Identity>>(
schema: Schema
): ReturnUpdateSessionShape<Schema> => {
if (isValibotSchema(schema)) {
// @ts-ignore Deep type instantiation with external schemas
return valibot.object({
Expand Down Expand Up @@ -145,13 +148,13 @@ export const getFullSchema = <Schema extends SchemaTypes>(schema: Schema): any =
return Typebox.Object({
user: schema,
expires: Typebox.Optional(Typebox.String()),
})
}) as unknown as ReturnUpdateSessionShape<Schema>
}
if (isZodSchema(schema)) {
return z.object({
user: schema,
expires: z.coerce.date().optional(),
})
}) as unknown as ReturnUpdateSessionShape<Schema>
}
throw new AuraAuthError({ code: "SCHEMA_UNSUPPORTED" })
}
Expand Down
Loading
Loading