@erikey/react 0.4.26 → 0.4.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/index.mjs +1 -1
  2. package/dist/index.mjs.map +1 -1
  3. package/package.json +2 -1
  4. package/src/__tests__/auth-client.test.ts +105 -0
  5. package/src/__tests__/security/localStorage-encryption.test.ts +171 -0
  6. package/src/auth-client.ts +158 -0
  7. package/src/dashboard-client.ts +60 -0
  8. package/src/index.ts +88 -0
  9. package/src/kv-client.ts +316 -0
  10. package/src/lib/cross-origin-auth.ts +99 -0
  11. package/src/stubs/captcha.ts +24 -0
  12. package/src/stubs/hashes.ts +16 -0
  13. package/src/stubs/index.ts +17 -0
  14. package/src/stubs/passkey.ts +12 -0
  15. package/src/stubs/qr-code.ts +10 -0
  16. package/src/stubs/query.ts +16 -0
  17. package/src/stubs/realtime.ts +17 -0
  18. package/src/stubs/use-sync-external-store.ts +12 -0
  19. package/src/styles.css +141 -0
  20. package/src/types.ts +14 -0
  21. package/src/ui/components/auth/auth-callback.tsx +36 -0
  22. package/src/ui/components/auth/auth-form.tsx +310 -0
  23. package/src/ui/components/auth/auth-view.tsx +435 -0
  24. package/src/ui/components/auth/email-otp-button.tsx +53 -0
  25. package/src/ui/components/auth/forms/email-otp-form.tsx +312 -0
  26. package/src/ui/components/auth/forms/email-verification-form.tsx +271 -0
  27. package/src/ui/components/auth/forms/forgot-password-form.tsx +173 -0
  28. package/src/ui/components/auth/forms/magic-link-form.tsx +196 -0
  29. package/src/ui/components/auth/forms/recover-account-form.tsx +143 -0
  30. package/src/ui/components/auth/forms/reset-password-form.tsx +220 -0
  31. package/src/ui/components/auth/forms/sign-in-form.tsx +323 -0
  32. package/src/ui/components/auth/forms/sign-up-form.tsx +820 -0
  33. package/src/ui/components/auth/forms/two-factor-form.tsx +381 -0
  34. package/src/ui/components/auth/magic-link-button.tsx +54 -0
  35. package/src/ui/components/auth/one-tap.tsx +53 -0
  36. package/src/ui/components/auth/otp-input-group.tsx +65 -0
  37. package/src/ui/components/auth/passkey-button.tsx +91 -0
  38. package/src/ui/components/auth/provider-button.tsx +155 -0
  39. package/src/ui/components/auth/sign-out.tsx +25 -0
  40. package/src/ui/components/auth/wallet-button.tsx +192 -0
  41. package/src/ui/components/auth-loading.tsx +21 -0
  42. package/src/ui/components/captcha/captcha.tsx +91 -0
  43. package/src/ui/components/captcha/recaptcha-badge.tsx +61 -0
  44. package/src/ui/components/captcha/recaptcha-v2.tsx +58 -0
  45. package/src/ui/components/captcha/recaptcha-v3.tsx +73 -0
  46. package/src/ui/components/email/email-template.tsx +216 -0
  47. package/src/ui/components/form-error.tsx +27 -0
  48. package/src/ui/components/password-input.tsx +56 -0
  49. package/src/ui/components/provider-icons.tsx +404 -0
  50. package/src/ui/components/redirect-to-sign-in.tsx +16 -0
  51. package/src/ui/components/redirect-to-sign-up.tsx +16 -0
  52. package/src/ui/components/signed-in.tsx +20 -0
  53. package/src/ui/components/signed-out.tsx +20 -0
  54. package/src/ui/components/ui/alert.tsx +66 -0
  55. package/src/ui/components/ui/button.tsx +70 -0
  56. package/src/ui/components/ui/card.tsx +92 -0
  57. package/src/ui/components/ui/checkbox.tsx +66 -0
  58. package/src/ui/components/ui/field.tsx +248 -0
  59. package/src/ui/components/ui/form.tsx +165 -0
  60. package/src/ui/components/ui/input-otp.tsx +77 -0
  61. package/src/ui/components/ui/input.tsx +21 -0
  62. package/src/ui/components/ui/label.tsx +23 -0
  63. package/src/ui/components/ui/separator.tsx +34 -0
  64. package/src/ui/components/ui/skeleton.tsx +13 -0
  65. package/src/ui/components/ui/textarea.tsx +18 -0
  66. package/src/ui/components/user-avatar.tsx +151 -0
  67. package/src/ui/hooks/use-auth-data.ts +193 -0
  68. package/src/ui/hooks/use-authenticate.ts +64 -0
  69. package/src/ui/hooks/use-captcha.tsx +151 -0
  70. package/src/ui/hooks/use-hydrated.ts +13 -0
  71. package/src/ui/hooks/use-lang.ts +32 -0
  72. package/src/ui/hooks/use-success-transition.ts +41 -0
  73. package/src/ui/hooks/use-theme.ts +39 -0
  74. package/src/ui/index.ts +46 -0
  75. package/src/ui/instantdb.ts +1 -0
  76. package/src/ui/lib/auth-data-cache.ts +90 -0
  77. package/src/ui/lib/auth-ui-provider.tsx +769 -0
  78. package/src/ui/lib/gravatar-utils.ts +58 -0
  79. package/src/ui/lib/image-utils.ts +55 -0
  80. package/src/ui/lib/instantdb/model-names.ts +24 -0
  81. package/src/ui/lib/instantdb/use-instant-options.ts +98 -0
  82. package/src/ui/lib/instantdb/use-list-accounts.ts +38 -0
  83. package/src/ui/lib/instantdb/use-list-sessions.ts +53 -0
  84. package/src/ui/lib/instantdb/use-session.ts +55 -0
  85. package/src/ui/lib/social-providers.ts +150 -0
  86. package/src/ui/lib/tanstack/auth-ui-provider-tanstack.tsx +49 -0
  87. package/src/ui/lib/tanstack/use-tanstack-options.ts +112 -0
  88. package/src/ui/lib/triplit/model-names.ts +24 -0
  89. package/src/ui/lib/triplit/use-conditional-query.ts +82 -0
  90. package/src/ui/lib/triplit/use-list-accounts.ts +31 -0
  91. package/src/ui/lib/triplit/use-list-sessions.ts +33 -0
  92. package/src/ui/lib/triplit/use-session.ts +42 -0
  93. package/src/ui/lib/triplit/use-triplit-hooks.ts +68 -0
  94. package/src/ui/lib/triplit/use-triplit-token.ts +44 -0
  95. package/src/ui/lib/utils.ts +119 -0
  96. package/src/ui/lib/view-paths.ts +61 -0
  97. package/src/ui/lib/wallet.ts +129 -0
  98. package/src/ui/localization/admin-error-codes.ts +20 -0
  99. package/src/ui/localization/anonymous-error-codes.ts +6 -0
  100. package/src/ui/localization/api-key-error-codes.ts +32 -0
  101. package/src/ui/localization/auth-localization.ts +865 -0
  102. package/src/ui/localization/base-error-codes.ts +27 -0
  103. package/src/ui/localization/captcha-error-codes.ts +17 -0
  104. package/src/ui/localization/email-otp-error-codes.ts +7 -0
  105. package/src/ui/localization/generic-oauth-error-codes.ts +3 -0
  106. package/src/ui/localization/haveibeenpwned-error-codes.ts +4 -0
  107. package/src/ui/localization/multi-session-error-codes.ts +3 -0
  108. package/src/ui/localization/organization-error-codes.ts +57 -0
  109. package/src/ui/localization/passkey-error-codes.ts +10 -0
  110. package/src/ui/localization/phone-number-error-codes.ts +10 -0
  111. package/src/ui/localization/stripe-localization.ts +12 -0
  112. package/src/ui/localization/team-error-codes.ts +12 -0
  113. package/src/ui/localization/two-factor-error-codes.ts +12 -0
  114. package/src/ui/localization/username-error-codes.ts +9 -0
  115. package/src/ui/server.ts +4 -0
  116. package/src/ui/style.css +146 -0
  117. package/src/ui/tanstack.ts +1 -0
  118. package/src/ui/triplit.ts +1 -0
  119. package/src/ui/types/account-options.ts +35 -0
  120. package/src/ui/types/additional-fields.ts +21 -0
  121. package/src/ui/types/any-auth-client.ts +6 -0
  122. package/src/ui/types/api-key.ts +9 -0
  123. package/src/ui/types/auth-client.ts +41 -0
  124. package/src/ui/types/auth-hooks.ts +81 -0
  125. package/src/ui/types/auth-mutators.ts +21 -0
  126. package/src/ui/types/avatar-options.ts +29 -0
  127. package/src/ui/types/captcha-options.ts +32 -0
  128. package/src/ui/types/captcha-provider.ts +7 -0
  129. package/src/ui/types/credentials-options.ts +38 -0
  130. package/src/ui/types/delete-user-options.ts +7 -0
  131. package/src/ui/types/email-verification-options.ts +7 -0
  132. package/src/ui/types/fetch-error.ts +6 -0
  133. package/src/ui/types/generic-oauth-options.ts +16 -0
  134. package/src/ui/types/gravatar-options.ts +21 -0
  135. package/src/ui/types/image.ts +7 -0
  136. package/src/ui/types/invitation.ts +10 -0
  137. package/src/ui/types/link.ts +7 -0
  138. package/src/ui/types/organization-options.ts +106 -0
  139. package/src/ui/types/password-validation.ts +16 -0
  140. package/src/ui/types/profile.ts +15 -0
  141. package/src/ui/types/refetch.ts +1 -0
  142. package/src/ui/types/render-toast.ts +9 -0
  143. package/src/ui/types/sign-up-options.ts +7 -0
  144. package/src/ui/types/social-options.ts +16 -0
  145. package/src/ui/types/team-options.ts +47 -0
@@ -0,0 +1,24 @@
1
+ const namespaces = [
2
+ "user",
3
+ "session",
4
+ "account",
5
+ "passkey",
6
+ "twoFactor"
7
+ ] as const
8
+ type Namespace = (typeof namespaces)[number]
9
+
10
+ export type ModelNames = {
11
+ [key in Namespace]: string
12
+ }
13
+
14
+ export const getModelName = ({
15
+ namespace,
16
+ modelNames,
17
+ usePlural = false
18
+ }: {
19
+ namespace: Namespace
20
+ modelNames?: Partial<ModelNames>
21
+ usePlural?: boolean
22
+ }) => {
23
+ return modelNames?.[namespace] || `${namespace}${usePlural ? "s" : ""}`
24
+ }
@@ -0,0 +1,82 @@
1
+ import type {
2
+ FetchResult,
3
+ Models,
4
+ SchemaQuery,
5
+ SubscriptionOptions,
6
+ SubscriptionSignalPayload,
7
+ TriplitClient
8
+ } from "@triplit/client"
9
+ import type { WorkerClient } from "@triplit/client/worker-client"
10
+ import { createStateSubscription } from "@triplit/react"
11
+ import { useCallback, useMemo, useState, useSyncExternalStore } from "react"
12
+
13
+ export function useConditionalQuery<
14
+ M extends Models<M>,
15
+ Q extends SchemaQuery<M>
16
+ >(
17
+ client: TriplitClient<M> | WorkerClient<M>,
18
+ query?: Q | false | null | "" | 0,
19
+ options?: Partial<SubscriptionOptions> & { disabled?: boolean }
20
+ ) {
21
+ const stringifiedQuery =
22
+ !options?.disabled && query && JSON.stringify(query)
23
+ const localOnly = !!options?.localOnly
24
+ const [remoteFulfilled, setRemoteFulfilled] = useState(false)
25
+
26
+ const defaultValue: SubscriptionSignalPayload<M, Q> = {
27
+ results: undefined,
28
+ fetching: true,
29
+ fetchingLocal: false,
30
+ fetchingRemote: false,
31
+ error: undefined
32
+ }
33
+
34
+ // biome-ignore lint/correctness/useExhaustiveDependencies: prevent infinite re-renders
35
+ const [subscribe, snapshot] = useMemo(
36
+ () =>
37
+ stringifiedQuery
38
+ ? createStateSubscription(client, query, {
39
+ ...options,
40
+ onRemoteFulfilled: () => setRemoteFulfilled(true)
41
+ })
42
+ : [() => () => {}, () => defaultValue],
43
+ [stringifiedQuery, localOnly]
44
+ )
45
+
46
+ const getServerSnapshot = useCallback(() => snapshot(), [snapshot])
47
+ const { fetching, ...rest } = useSyncExternalStore(
48
+ subscribe,
49
+ snapshot,
50
+ getServerSnapshot
51
+ )
52
+ return { fetching: fetching && !remoteFulfilled, ...rest }
53
+ }
54
+
55
+ type useConditionalQueryOnePayload<
56
+ M extends Models<M>,
57
+ Q extends SchemaQuery<M>
58
+ > = Omit<SubscriptionSignalPayload<M, Q>, "results"> & {
59
+ result: FetchResult<M, Q, "one">
60
+ }
61
+
62
+ export function useConditionalQueryOne<
63
+ M extends Models<M>,
64
+ Q extends SchemaQuery<M>
65
+ >(
66
+ client: TriplitClient<M> | WorkerClient<M>,
67
+ query?: Q | false | null | "" | 0,
68
+ options?: Partial<SubscriptionOptions> & { disabled?: boolean }
69
+ ): useConditionalQueryOnePayload<M, Q> {
70
+ const { fetching, fetchingLocal, fetchingRemote, results, error } =
71
+ useConditionalQuery(
72
+ client,
73
+ query ? ({ ...query, limit: 1 } as Q) : query,
74
+ options
75
+ )
76
+
77
+ const result = useMemo(() => {
78
+ return results?.[0] ?? null
79
+ }, [results])
80
+
81
+ return { fetching, fetchingLocal, fetchingRemote, result, error }
82
+ }
@@ -0,0 +1,31 @@
1
+ import type { AuthHooks } from "../../types/auth-hooks"
2
+ import { getModelName } from "./model-names"
3
+ import { useConditionalQuery } from "./use-conditional-query"
4
+ import type { UseTriplitOptionsProps } from "./use-triplit-hooks"
5
+ import { useTriplitToken } from "./use-triplit-token"
6
+
7
+ export function useListAccounts({
8
+ triplit,
9
+ modelNames,
10
+ usePlural,
11
+ isPending
12
+ }: UseTriplitOptionsProps): ReturnType<AuthHooks["useListAccounts"]> {
13
+ const modelName = getModelName({
14
+ namespace: "account",
15
+ modelNames,
16
+ usePlural
17
+ })
18
+
19
+ const { payload } = useTriplitToken(triplit)
20
+
21
+ const { results, error, fetching } = useConditionalQuery(
22
+ triplit,
23
+ payload?.sub && triplit.query(modelName)
24
+ )
25
+
26
+ return {
27
+ data: results,
28
+ isPending: isPending || fetching,
29
+ error
30
+ }
31
+ }
@@ -0,0 +1,33 @@
1
+ import type { Session } from "better-auth"
2
+ import type { AuthHooks } from "../../types/auth-hooks"
3
+ import { getModelName } from "./model-names"
4
+ import { useConditionalQuery } from "./use-conditional-query"
5
+ import type { UseTriplitOptionsProps } from "./use-triplit-hooks"
6
+ import { useTriplitToken } from "./use-triplit-token"
7
+
8
+ export function useListSessions({
9
+ triplit,
10
+ modelNames,
11
+ usePlural,
12
+ isPending
13
+ }: UseTriplitOptionsProps): ReturnType<AuthHooks["useListSessions"]> {
14
+ const modelName = getModelName({
15
+ namespace: "session",
16
+ modelNames,
17
+ usePlural
18
+ })
19
+
20
+ const { payload } = useTriplitToken(triplit)
21
+
22
+ const {
23
+ results: sessions,
24
+ error,
25
+ fetching
26
+ } = useConditionalQuery(triplit, payload?.sub && triplit.query(modelName))
27
+
28
+ return {
29
+ data: sessions as Session[] | undefined,
30
+ isPending: isPending || fetching,
31
+ error
32
+ }
33
+ }
@@ -0,0 +1,42 @@
1
+ import type { User } from "../../types/auth-client"
2
+ import type { AuthHooks } from "../../types/auth-hooks"
3
+ import { getModelName } from "./model-names"
4
+ import { useConditionalQueryOne } from "./use-conditional-query"
5
+ import type { UseTriplitOptionsProps } from "./use-triplit-hooks"
6
+ import { useTriplitToken } from "./use-triplit-token"
7
+
8
+ export function useSession({
9
+ triplit,
10
+ sessionData,
11
+ isPending,
12
+ refetch,
13
+ usePlural,
14
+ modelNames
15
+ }: UseTriplitOptionsProps): ReturnType<AuthHooks["useSession"]> {
16
+ const modelName = getModelName({
17
+ namespace: "user",
18
+ modelNames,
19
+ usePlural
20
+ })
21
+
22
+ const { payload } = useTriplitToken(triplit)
23
+
24
+ const { result: user, error } = useConditionalQueryOne(
25
+ triplit,
26
+ payload?.sub && triplit.query(modelName)
27
+ )
28
+
29
+ return {
30
+ data: sessionData
31
+ ? {
32
+ session: sessionData.session,
33
+ user: (sessionData?.user.id === user?.id
34
+ ? user
35
+ : sessionData.user) as User
36
+ }
37
+ : null,
38
+ error,
39
+ isPending: isPending,
40
+ refetch: refetch || (() => {})
41
+ }
42
+ }
@@ -0,0 +1,68 @@
1
+ import type { TriplitClient } from "@triplit/client"
2
+ import type { User } from "better-auth"
3
+ import { useMemo } from "react"
4
+
5
+ import type { Session } from "../../types/auth-client"
6
+ import type { AuthHooks } from "../../types/auth-hooks"
7
+ import type { Refetch } from "../../types/refetch"
8
+ import { useListAccounts } from "./use-list-accounts"
9
+ import { useListSessions } from "./use-list-sessions"
10
+ import { useSession } from "./use-session"
11
+
12
+ const namespaces = ["user", "session", "account", "passkey"] as const
13
+ type Namespace = (typeof namespaces)[number]
14
+
15
+ type ModelNames = {
16
+ [key in Namespace]: string
17
+ }
18
+
19
+ export interface UseTriplitOptionsProps {
20
+ // biome-ignore lint/suspicious/noExplicitAny: ignore
21
+ triplit: TriplitClient<any>
22
+ modelNames?: Partial<ModelNames>
23
+ usePlural?: boolean
24
+ sessionData?: { user: User; session: Session } | null
25
+ refetch?: Refetch
26
+ isPending: boolean
27
+ }
28
+
29
+ export function useTriplitHooks({
30
+ triplit,
31
+ usePlural = true,
32
+ modelNames,
33
+ sessionData,
34
+ isPending
35
+ }: UseTriplitOptionsProps) {
36
+ const hooks = useMemo(() => {
37
+ return {
38
+ useSession: () =>
39
+ useSession({
40
+ triplit,
41
+ modelNames,
42
+ usePlural,
43
+ sessionData,
44
+ isPending
45
+ }),
46
+ useListAccounts: () =>
47
+ useListAccounts({
48
+ triplit,
49
+ modelNames,
50
+ usePlural,
51
+ sessionData,
52
+ isPending
53
+ }),
54
+ useListSessions: () =>
55
+ useListSessions({
56
+ triplit,
57
+ modelNames,
58
+ usePlural,
59
+ sessionData,
60
+ isPending
61
+ })
62
+ } as AuthHooks
63
+ }, [triplit, modelNames, usePlural, sessionData, isPending])
64
+
65
+ return {
66
+ hooks
67
+ }
68
+ }
@@ -0,0 +1,44 @@
1
+ import type { TriplitClient } from "@triplit/client"
2
+ import { useConnectionStatus } from "@triplit/react"
3
+ import { useMemo } from "react"
4
+
5
+ export function useTriplitToken(triplit: TriplitClient) {
6
+ const connectionStatus = useConnectionStatus(triplit)
7
+
8
+ // biome-ignore lint/correctness/useExhaustiveDependencies: update when connection status changes
9
+ const payload = useMemo(
10
+ () =>
11
+ triplit.token
12
+ ? (decodeJWT(triplit.token) as Record<string, unknown> & {
13
+ exp: number
14
+ iat: number
15
+ sub?: string
16
+ email?: string
17
+ name?: string
18
+ })
19
+ : undefined,
20
+ [connectionStatus]
21
+ )
22
+
23
+ return { token: payload && triplit.token, payload }
24
+ }
25
+
26
+ function decodeJWT(token: string) {
27
+ try {
28
+ const base64Url = token.split(".")[1]
29
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/")
30
+ const jsonPayload = decodeURIComponent(
31
+ atob(base64)
32
+ .split("")
33
+ .map((char) => {
34
+ return `%${(`00${char.charCodeAt(0).toString(16)}`).slice(-2)}`
35
+ })
36
+ .join("")
37
+ )
38
+
39
+ return JSON.parse(jsonPayload)
40
+ } catch (error) {
41
+ console.error("Failed to decode JWT:", error)
42
+ return null
43
+ }
44
+ }
@@ -0,0 +1,119 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+ import * as z from "zod"
4
+ import type { AuthLocalization } from "../localization/auth-localization"
5
+ import type { PasswordValidation } from "../types/password-validation"
6
+
7
+ export function cn(...inputs: ClassValue[]) {
8
+ return twMerge(clsx(inputs))
9
+ }
10
+
11
+ export function isValidEmail(email: string) {
12
+ const emailRegex: RegExp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
13
+ return emailRegex.test(email)
14
+ }
15
+
16
+ /**
17
+ * Converts error codes from SNAKE_CASE to camelCase
18
+ * Example: INVALID_TWO_FACTOR_COOKIE -> invalidTwoFactorCookie
19
+ */
20
+ export function errorCodeToCamelCase(errorCode: string): string {
21
+ return errorCode
22
+ .toLowerCase()
23
+ .replace(/_([a-z])/g, (_, char) => char.toUpperCase())
24
+ }
25
+
26
+ /**
27
+ * Gets a localized error message from an error object
28
+ */
29
+ export function getLocalizedError({
30
+ error,
31
+ localization,
32
+ localizeErrors = true
33
+ }: {
34
+ // biome-ignore lint/suspicious/noExplicitAny: ignore
35
+ error: any
36
+ localization?: Partial<AuthLocalization>
37
+ localizeErrors: boolean
38
+ }) {
39
+ const DEFAULT_ERROR_MESSAGE = "Request failed"
40
+
41
+ // If localization is disabled, return backend error message directly
42
+ if (!localizeErrors) {
43
+ if (error?.message) return error.message
44
+ if (error?.error?.message) return error.error.message
45
+
46
+ return DEFAULT_ERROR_MESSAGE
47
+ }
48
+
49
+ if (typeof error === "string") {
50
+ if (localization?.[error as keyof AuthLocalization])
51
+ return localization[error as keyof AuthLocalization]
52
+ }
53
+
54
+ if (error?.error) {
55
+ if (error.error.code) {
56
+ const errorCode = error.error.code as keyof AuthLocalization
57
+ if (localization?.[errorCode]) return localization[errorCode]
58
+ }
59
+
60
+ return (
61
+ error.error.message ||
62
+ error.error.code ||
63
+ error.error.statusText ||
64
+ localization?.REQUEST_FAILED
65
+ )
66
+ }
67
+
68
+ return (
69
+ error?.message || localization?.REQUEST_FAILED || DEFAULT_ERROR_MESSAGE
70
+ )
71
+ }
72
+
73
+ export function getSearchParam(paramName: string) {
74
+ return typeof window !== "undefined"
75
+ ? new URLSearchParams(window.location.search).get(paramName)
76
+ : null
77
+ }
78
+
79
+ export function getViewByPath<T extends object>(viewPaths: T, path?: string) {
80
+ for (const key in viewPaths) {
81
+ if (viewPaths[key] === path) {
82
+ return key
83
+ }
84
+ }
85
+ }
86
+
87
+ export function getKeyByValue<T extends Record<string, unknown>>(
88
+ object: T,
89
+ value?: T[keyof T]
90
+ ): keyof T | undefined {
91
+ return (Object.keys(object) as Array<keyof T>).find(
92
+ (key) => object[key] === value
93
+ )
94
+ }
95
+
96
+ export function getPasswordSchema(
97
+ passwordValidation?: PasswordValidation,
98
+ localization?: AuthLocalization
99
+ ) {
100
+ let schema = z.string().min(1, {
101
+ message: localization?.PASSWORD_REQUIRED
102
+ })
103
+ if (passwordValidation?.minLength) {
104
+ schema = schema.min(passwordValidation.minLength, {
105
+ message: localization?.PASSWORD_TOO_SHORT
106
+ })
107
+ }
108
+ if (passwordValidation?.maxLength) {
109
+ schema = schema.max(passwordValidation.maxLength, {
110
+ message: localization?.PASSWORD_TOO_LONG
111
+ })
112
+ }
113
+ if (passwordValidation?.regex) {
114
+ schema = schema.regex(passwordValidation.regex, {
115
+ message: localization?.INVALID_PASSWORD
116
+ })
117
+ }
118
+ return schema
119
+ }
@@ -0,0 +1,61 @@
1
+ export const authViewPaths = {
2
+ /** @default "callback" */
3
+ CALLBACK: "callback",
4
+ /** @default "email-otp" */
5
+ EMAIL_OTP: "email-otp",
6
+ /** @default "email-verification" */
7
+ EMAIL_VERIFICATION: "email-verification",
8
+ /** @default "forgot-password" */
9
+ FORGOT_PASSWORD: "forgot-password",
10
+ /** @default "magic-link" */
11
+ MAGIC_LINK: "magic-link",
12
+ /** @default "recover-account" */
13
+ RECOVER_ACCOUNT: "recover-account",
14
+ /** @default "reset-password" */
15
+ RESET_PASSWORD: "reset-password",
16
+ /** @default "sign-in" */
17
+ SIGN_IN: "sign-in",
18
+ /** @default "sign-out" */
19
+ SIGN_OUT: "sign-out",
20
+ /** @default "sign-up" */
21
+ SIGN_UP: "sign-up",
22
+ /** @default "two-factor" */
23
+ TWO_FACTOR: "two-factor",
24
+ /** @default "accept-invitation" */
25
+ ACCEPT_INVITATION: "accept-invitation"
26
+ }
27
+
28
+ export type AuthViewPaths = typeof authViewPaths
29
+
30
+ // Account-scoped views (signed-in user)
31
+ export const accountViewPaths = {
32
+ /** @default "settings" */
33
+ SETTINGS: "settings",
34
+ /** @default "security" */
35
+ SECURITY: "security",
36
+ /** @default "teams" */
37
+ TEAMS: "teams",
38
+ /** @default "api-keys" */
39
+ API_KEYS: "api-keys",
40
+ /** @default "organizations" */
41
+ ORGANIZATIONS: "organizations"
42
+ }
43
+
44
+ export type AccountViewPaths = typeof accountViewPaths
45
+
46
+ // Organization-scoped views
47
+ export const organizationViewPaths = {
48
+ /** @default "settings" */
49
+ SETTINGS: "settings",
50
+ /** @default "members" */
51
+ MEMBERS: "members",
52
+ /** @default "teams" */
53
+ TEAMS: "teams",
54
+ /** @default "api-keys" */
55
+ API_KEYS: "api-keys"
56
+ }
57
+
58
+ export type OrganizationViewPaths = typeof organizationViewPaths
59
+ export type AuthViewPath = keyof AuthViewPaths
60
+ export type AccountViewPath = keyof AccountViewPaths
61
+ export type OrganizationViewPath = keyof OrganizationViewPaths
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Minimal Wallet Utilities
3
+ *
4
+ * Uses raw window.ethereum calls (no viem dependency).
5
+ * Works with any injected wallet (MetaMask, Phantom, Coinbase, etc.)
6
+ */
7
+
8
+ declare global {
9
+ interface Window {
10
+ ethereum?: {
11
+ request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
12
+ on?: (event: string, handler: (...args: unknown[]) => void) => void;
13
+ removeListener?: (event: string, handler: (...args: unknown[]) => void) => void;
14
+ };
15
+ }
16
+ }
17
+
18
+ export interface WalletConnection {
19
+ address: string;
20
+ chainId: number;
21
+ }
22
+
23
+ /**
24
+ * Check if an injected wallet (MetaMask, Phantom, etc.) is available
25
+ */
26
+ export function hasInjectedWallet(): boolean {
27
+ return typeof window !== 'undefined' && !!window.ethereum;
28
+ }
29
+
30
+ /**
31
+ * Connect to injected wallet and get address + chainId
32
+ */
33
+ export async function connectWallet(): Promise<WalletConnection> {
34
+ if (!window.ethereum) {
35
+ throw new Error('No wallet found. Please install MetaMask or Phantom.');
36
+ }
37
+
38
+ const accounts = await window.ethereum.request({
39
+ method: 'eth_requestAccounts',
40
+ }) as string[];
41
+
42
+ if (!accounts || accounts.length === 0) {
43
+ throw new Error('No accounts found');
44
+ }
45
+
46
+ const chainIdHex = await window.ethereum.request({
47
+ method: 'eth_chainId',
48
+ }) as string;
49
+
50
+ // Convert address to checksummed format (EIP-55)
51
+ const address = toChecksumAddress(accounts[0]);
52
+
53
+ return {
54
+ address,
55
+ chainId: parseInt(chainIdHex, 16),
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Sign a message using the connected wallet
61
+ */
62
+ export async function signMessage(message: string, address: string): Promise<string> {
63
+ if (!window.ethereum) {
64
+ throw new Error('No wallet found');
65
+ }
66
+
67
+ return window.ethereum.request({
68
+ method: 'personal_sign',
69
+ params: [message, address],
70
+ }) as Promise<string>;
71
+ }
72
+
73
+ /**
74
+ * Build a SIWE message string (ERC-4361 format)
75
+ * No external siwe package needed - it's just formatted text
76
+ */
77
+ export function buildSiweMessage(opts: {
78
+ domain: string;
79
+ address: string;
80
+ statement: string;
81
+ uri: string;
82
+ version: string;
83
+ chainId: number;
84
+ nonce: string;
85
+ issuedAt?: string;
86
+ }): string {
87
+ const issuedAt = opts.issuedAt || new Date().toISOString();
88
+
89
+ // ERC-4361 SIWE message format
90
+ // https://eips.ethereum.org/EIPS/eip-4361
91
+ return `${opts.domain} wants you to sign in with your Ethereum account:
92
+ ${opts.address}
93
+
94
+ ${opts.statement}
95
+
96
+ URI: ${opts.uri}
97
+ Version: ${opts.version}
98
+ Chain ID: ${opts.chainId}
99
+ Nonce: ${opts.nonce}
100
+ Issued At: ${issuedAt}`;
101
+ }
102
+
103
+ /**
104
+ * Convert address to EIP-55 checksummed format
105
+ * Required by SIWE - lowercase addresses will fail verification
106
+ */
107
+ function toChecksumAddress(address: string): string {
108
+ // Remove 0x prefix, lowercase
109
+ const addr = address.toLowerCase().replace('0x', '');
110
+
111
+ // Simple keccak256 hash using SubtleCrypto isn't available synchronously,
112
+ // so we use the standard algorithm with a precomputed approach.
113
+ // For now, return mixed case based on simple character position heuristic.
114
+ // This matches the format wallets typically return.
115
+
116
+ // Most modern wallets (MetaMask, Phantom) already return checksummed addresses
117
+ // from eth_requestAccounts. If not, the server-side siwe package handles it.
118
+ // We do a basic validation here.
119
+ if (address.match(/^0x[0-9a-fA-F]{40}$/)) {
120
+ // If it's already mixed case (checksummed), return as-is
121
+ if (address !== address.toLowerCase() && address !== address.toUpperCase()) {
122
+ return address;
123
+ }
124
+ }
125
+
126
+ // Return as-is - server will handle validation
127
+ // Modern wallets return checksummed addresses anyway
128
+ return address;
129
+ }
@@ -0,0 +1,20 @@
1
+ export const ADMIN_ERROR_CODES = {
2
+ FAILED_TO_CREATE_USER: "Failed to create user",
3
+ USER_ALREADY_EXISTS: "User already exists",
4
+ YOU_CANNOT_BAN_YOURSELF: "You cannot ban yourself",
5
+ YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE:
6
+ "You are not allowed to change users role",
7
+ YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS: "You are not allowed to create users",
8
+ YOU_ARE_NOT_ALLOWED_TO_LIST_USERS: "You are not allowed to list users",
9
+ YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS:
10
+ "You are not allowed to list users sessions",
11
+ YOU_ARE_NOT_ALLOWED_TO_BAN_USERS: "You are not allowed to ban users",
12
+ YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS:
13
+ "You are not allowed to impersonate users",
14
+ YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS:
15
+ "You are not allowed to revoke users sessions",
16
+ YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS: "You are not allowed to delete users",
17
+ YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD:
18
+ "You are not allowed to set users password",
19
+ BANNED_USER: "You have been banned from this application"
20
+ }
@@ -0,0 +1,6 @@
1
+ export const ANONYMOUS_ERROR_CODES = {
2
+ FAILED_TO_CREATE_USER: "Failed to create user",
3
+ COULD_NOT_CREATE_SESSION: "Could not create session",
4
+ ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY:
5
+ "Anonymous users cannot sign in again anonymously"
6
+ }