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
172 changes: 150 additions & 22 deletions bun.lock

Large diffs are not rendered by default.

180 changes: 173 additions & 7 deletions docs/src/content/docs/(core)/guides/schema-validation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ description: Learn how to validate your authentication schema with Aura Auth's b

Aura Auth provides the `identity.schema` configuration option for type extension. This allows you to define a custom validation schema for user identity data, ensuring that the data conforms to a specific structure and set of fields. The schema is used to derive the `User` and `Session` types across the library. By default, Aura Auth accepts a flexible identity shape, but you can provide your own schema to enforce a specific structure.

Aura Auth supports `zod`, `valibot`, `arktype` and `typebox` schemas for validation. You can choose the one that best fits your needs, based on the constraints, limitations and features of each library.

For each library, Aura Auth provides a default `Identity` schema, which you can extend to create your own custom schema. This allows you to add additional fields or constraints to user identity data while leveraging built-in validation across all supported libraries, and built-in type inference where supported (zod, valibot, and arktype). The default `Identity` schema contains `sub`, `name`, `email`, and `image` fields, but you can extend it with any additional fields you need. TypeBox is supported for validation, but does not support direct type inference here.

<Tabs items={["zod", "valibot", "arktype", "typebox"]} groupId="schema-validation" persist>

<Tab value="zod">

```ts title="lib/auth.ts" lineNumbers
import { z } from "zod"
import { createAuth } from "@aura-stack/auth"
import { UserIdentity, type InferUser, type InferSession } from "@aura-stack/auth/identity"

const schema = UserIdentity.extend({
role: z.enum(["admin", "user"]),
permissions: z.array(z.string()),
})

export const auth = createAuth({
Expand All @@ -21,30 +28,189 @@ export const auth = createAuth({
schema,
},
})
```

export type User = InferUser<typeof auth>
export type Session = InferSession<typeof auth>
</Tab>

<Tab value="valibot">

```ts title="lib/auth.ts" lineNumbers
import * as valibot from "valibot"
import { createAuth } from "@aura-stack/auth"
import { UserIdentityValibot, type InferUser, type InferSession } from "@aura-stack/auth/identity"

const schema = valibot.object({
...UserIdentityValibot.entries,
role: valibot.enum(["admin", "user"]),
})

export const auth = createAuth({
oauth: [],
identity: {
schema,
},
})
```

**Or, if you prefer to infer the types from the schema:**
</Tab>

<Tab value="arktype">

```ts title="lib/auth.ts" lineNumbers
import { type } from "arktype"
import { createAuth } from "@aura-stack/auth"
import { UserIdentityArkType, type InferUser, type InferSession } from "@aura-stack/auth/identity"

const schema = UserIdentityArkType.and({
role: type.enumerated(["admin", "user"]),
})

export const auth = createAuth({
oauth: [],
identity: {
schema,
},
})
```

</Tab>

<Tab value="typebox">

```ts title="lib/auth.ts" lineNumbers
import { Type } from "typebox"
import { createAuth } from "@aura-stack/auth"
import { UserIdentityTypeBox } from "@aura-stack/auth/identity"

const schema = Type.Object({
...UserIdentityTypeBox.properties,
role: Type.Union([Type.Literal("admin"), Type.Literal("user")]),
})

export const auth = createAuth({
oauth: [],
identity: {
schema,
},
})
```

</Tab>

</Tabs>

Aura Auth provides utility types to infer the `User` and `Session` types from the identity schema. This allows you to have type safety and autocompletion when working with user identity data throughout your application. By using the provided utility types, you can ensure that your code is consistent with the defined schema and that you are accessing the correct fields on the user and session objects.

The `User` and `Session` types can be inferred from the auth configuration using the `InferUser` and `InferSession` utility types or by using the `UserFrom` and `SessionFrom` types directly from the schema.

<Tabs items={["zod", "valibot", "arktype", "typebox"]} groupId="schema-validation" persist>

<Tab value="zod">

```ts title="lib/auth.ts" lineNumbers
import { z } from "zod"
import { createAuth } from "@aura-stack/auth"
import { UserIdentity, type UserFrom, type SessionFrom } from "@aura-stack/auth/identity"
import { UserIdentity, type UserFrom, type SessionFrom, type InferUser, type InferSession } from "@aura-stack/auth/identity"

const schema = UserIdentity.extend({
role: z.enum(["admin", "user"]),
permissions: z.array(z.string()),
})

type User = UserFrom<typeof schema>
type Session = SessionFrom<typeof schema>
type _User = UserFrom<typeof schema>
type _Session = SessionFrom<typeof schema>

export const auth = createAuth({
oauth: [],
identity: {
schema,
},
})

export type User = InferUser<typeof auth>
export type Session = InferSession<typeof auth>
```

</Tab>

<Tab value="valibot">

```ts title="lib/auth.ts" lineNumbers
import * as valibot from "valibot"
import { createAuth } from "@aura-stack/auth"
import { UserIdentityValibot, type InferUser, type InferSession } from "@aura-stack/auth/identity"

const schema = valibot.object({
...UserIdentityValibot.entries,
role: valibot.enum(["admin", "user"]),
})

export const auth = createAuth({
oauth: [],
identity: {
schema,
},
})

export type User = InferUser<typeof auth>
export type Session = InferSession<typeof auth>
```

</Tab>

<Tab value="arktype">

```ts title="lib/auth.ts" lineNumbers
import { type } from "arktype"
import { createAuth } from "@aura-stack/auth"
import { UserIdentityArkType, type InferUser, type InferSession } from "@aura-stack/auth/identity"

const schema = UserIdentityArkType.and({
role: type.enumerated(["admin", "user"]),
})

export const auth = createAuth({
oauth: [],
identity: {
schema,
},
})

export type User = InferUser<typeof auth>
export type Session = InferSession<typeof auth>
```

</Tab>

<Tab value="typebox">

<Callout type="warning">
Aura Auth does not currently support inferring types directly from the TypeBox schema. To work around this, you can manually use
`Static` type from TypeBox to define the `User` and `Session` types based on the structure of your schema. This is a temporary
limitation by the expensive nature of TypeBox's type inference.
</Callout>

```ts title="lib/auth.ts" lineNumbers
import { Type, type Static } from "typebox"
import { createAuth } from "@aura-stack/auth"
import { UserIdentityTypeBox } from "@aura-stack/auth/identity"

const schema = Type.Object({
...UserIdentityTypeBox.properties,
role: Type.Union([Type.Literal("admin"), Type.Literal("user")]),
})

export type User = Static<typeof schema>
export type Session = Static<typeof schema>

export const auth = createAuth({
oauth: [],
identity: {
schema,
},
})
```

</Tab>

</Tabs>
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@
"vitest": {
"vitest": "4.1.4",
"@vitest/coverage-v8": "4.1.4"
},
"valibot": {
"valibot": "^1.4.0"
},
"testing-library": {
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1"
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"license": "MIT",
"dependencies": {
"@aura-stack/jose": "workspace:*",
"@aura-stack/router": "^0.7.0",
"@aura-stack/router": "^0.7.2",
"arktype": "^2.2.0",
"typebox": "^1.1.38",
"valibot": "catalog:valibot",
Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/@types/utility.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Type } from "arktype"
import type { TProperties, TObject, Static, TSchema } from "typebox"
import type { TProperties, TObject, TSchema } from "typebox"
Comment thread
halvaradop marked this conversation as resolved.
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"
Expand Down Expand Up @@ -36,6 +36,10 @@ export type EditableShapeTypebox<T extends TProperties> = {
[K in keyof T]: T[K] extends TObject ? Wrap<EditableShapeTypebox<T[K]["properties"]>> : TSchema
}

export type EditableUser = {
[K in keyof User]: any
}

export type ConfigSchema<T extends Identities> =
IsZod<T> extends true
? ZodObject<T & ZodRawShape>
Expand All @@ -51,7 +55,7 @@ export type ValibotShapeToObject<S extends ObjectEntries> = Merge<InferOutput<Ob

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

export type TypeboxShapeToObject<S extends TProperties> = S extends TProperties ? Wrap<Merge<Static<TObject<S>>, User>> : never
export type TypeboxShapeToObject<S> = Wrap<Merge<S, User>>
Comment thread
halvaradop marked this conversation as resolved.

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

Expand All @@ -72,7 +76,9 @@ export type FromShapeToObject<S> = S extends ZodRawShape
? ArktypeShapeToObject<S>
: S extends TProperties
? TypeboxShapeToObject<S>
: never
: S extends User
? S
: never
Comment thread
halvaradop marked this conversation as resolved.

/** Recursively makes every property required. */
export type DeepRequired<T> = {
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/shared/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import * as valibot from "valibot"
import { Type as Typebox } from "typebox"
import { type, type Type } from "arktype"
import { isArkType, isTypeboxEntries, isValibotEntries, isZodEntries } from "@/shared/assert.ts"
import type { EditableShape, EditableShapeArkType, EditableShapeTypebox, EditableShapeValibot } from "@/@types/utility.ts"
import type {
EditableShape,
EditableShapeArkType,
EditableShapeTypebox,
EditableShapeValibot,
EditableUser,
} from "@/@types/utility.ts"

export type {
InferUser,
Expand Down Expand Up @@ -63,6 +69,7 @@ export type Identities =
| EditableShapeValibot<UserShapeValibot>
| EditableShapeArkType<UserShapeArkType>
| EditableShapeTypebox<UserShapeTypeBox>
| EditableUser
Comment thread
halvaradop marked this conversation as resolved.

type ReturnShapeType<T> =
T extends EditableShape<UserShape>
Expand All @@ -73,7 +80,9 @@ type ReturnShapeType<T> =
? T
: T extends EditableShapeTypebox<UserShapeTypeBox>
? Typebox.TObject<T>
: never
: T extends EditableUser
? z.ZodObject<T>
: never

export const createIdentity = <S extends Identities>(shape: S): ReturnShapeType<S> => {
if (isArkType(shape)) {
Expand Down
16 changes: 13 additions & 3 deletions packages/core/test/identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,24 @@ describe("createIdentity", () => {
}),
})

const auth = createAuth({
type ExpectedIdentity = Static<typeof Schema>
createAuth({
oauth: [],
identity: {
schema: Schema,
},
})
type ExpectedIdentity = Static<typeof Schema>
expectTypeOf<ExpectedIdentity>().toEqualTypeOf<InferUser<typeof auth>>()
expectTypeOf<ExpectedIdentity>().toEqualTypeOf<{
sub: string
name?: string | null
email?: string | null
image?: string | null
role: string
user: {
id: string
name: string
}
}>()
})
})

Expand Down
4 changes: 2 additions & 2 deletions packages/core/test/types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe("createAuth", () => {
Partial<
{
sub: string
role?: string | undefined
role?: Typebox.TOptional<Typebox.TString>
name?: string | null | undefined
image?: string | null | undefined
email?: string | null | undefined
Expand Down Expand Up @@ -226,7 +226,7 @@ describe("createAuth", () => {
).toEqualTypeOf<
(options: GetSessionAPIOptions) => Promise<
GetSessionAPIReturn<{
role?: string | undefined
role: Typebox.TOptional<Typebox.TString>
sub: string
name?: string | null | undefined
image?: string | null | undefined
Expand Down
Loading
Loading