@rimelight/auth 0.0.1

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.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # Rimelight Entertainment Workspace
2
+
3
+ ## Structure
4
+
5
+ ### Apps (`/packages`)
6
+
7
+ - **`rimelight.com`**: The main company website.
8
+ - **`starter.rimelight.com`**: Our standardized starter template for Astro websites.
9
+
10
+ ### Packages (`/packages`)
11
+
12
+ - **`@rimelight/auth`**: Authentication and authorization utilities for the Rimelight ecosystem.
13
+ - **`@rimelight/cli`**: The command line interface for managing Rimelight projects.
14
+ - **`@rimelight/cms`**: Enterprise content management, block rendering, and wiki engine.
15
+ - **`@rimelight/docs`**: Documentation components and utilities.
16
+ - **`@rimelight/i18n`**: Internationalization and localization tools.
17
+ - **`@rimelight/security`**: Astro security integration (CSP, SRI, and more).
18
+ - **`@rimelight/seo`**: SEO utilities including sitemap, robots, and meta components.
19
+ - **`@rimelight/ui`**: Our component library used in all our web projects.
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@rimelight/auth",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "description": "Rimelight Entertainment's Authentication Package",
6
+ "homepage": "https://rimelight.com/docs",
7
+ "bugs": {
8
+ "url": "https://github.com/Rimelight-Entertainment/rimelight/issues"
9
+ },
10
+ "license": "MIT",
11
+ "author": {
12
+ "name": "Rimelight Entertainment"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
17
+ },
18
+ "files": [
19
+ "src"
20
+ ],
21
+ "type": "module",
22
+ "exports": {
23
+ ".": "./src/index.ts",
24
+ "./server": "./src/server/index.ts",
25
+ "./client": "./src/client/index.ts",
26
+ "./permissions": "./src/permissions/index.ts",
27
+ "./schema/drizzle": "./src/schema/drizzle/index.ts",
28
+ "./plugins/reserved-usernames": "./src/plugins/reserved-usernames/index.ts",
29
+ "./plugins/discriminator-tag": "./src/plugins/discriminator-tag/index.ts",
30
+ "./plugins/synthetic-user": "./src/plugins/synthetic-user/index.ts",
31
+ "./plugins/construction-guest": "./src/plugins/construction-guest/index.ts"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "check": "pnpm audit --audit-level=moderate && vp check --fix"
38
+ },
39
+ "dependencies": {
40
+ "@better-auth/drizzle-adapter": "1.6.30",
41
+ "better-auth": "1.6.30",
42
+ "drizzle-orm": "0.45.2"
43
+ },
44
+ "devDependencies": {
45
+ "@rimelight/config": "workspace:*"
46
+ },
47
+ "peerDependencies": {
48
+ "better-auth": ">=1.0.0",
49
+ "drizzle-orm": ">=0.30.0",
50
+ "hono": ">=4.12.34"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "hono": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "engines": {
58
+ "node": ">=26.7.0"
59
+ },
60
+ "packageManager": "pnpm@11.22.0"
61
+ }
@@ -0,0 +1,3 @@
1
+ import { createAuthClient } from "better-auth/client"
2
+
3
+ export const createRimelightAuthClient = createAuthClient
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./server/index"
2
+ export * from "./permissions/index"
3
+ export * from "./plugins/reserved-usernames/index"
4
+ export * from "./plugins/synthetic-user/index"
5
+ export * from "./plugins/construction-guest/index"
6
+ export * from "./plugins/discriminator-tag/index"
@@ -0,0 +1,27 @@
1
+ import { createAccessControl } from "better-auth/plugins/access"
2
+ import {
3
+ defaultStatements,
4
+ ownerAc,
5
+ adminAc,
6
+ memberAc
7
+ } from "better-auth/plugins/organization/access"
8
+
9
+ export { createAccessControl, defaultStatements, ownerAc, adminAc, memberAc }
10
+
11
+ export interface CreatePermissionsOptions<TStatements extends Record<string, readonly string[]>> {
12
+ statements: TStatements
13
+ }
14
+
15
+ export function createPermissions<const TStatements extends Record<string, readonly string[]>>(
16
+ statements: TStatements
17
+ ) {
18
+ const mergedStatements = {
19
+ ...defaultStatements,
20
+ ...statements
21
+ }
22
+ const ac = createAccessControl(mergedStatements)
23
+ return {
24
+ ac,
25
+ statements: mergedStatements
26
+ }
27
+ }
@@ -0,0 +1,107 @@
1
+ import { getCookie, setCookie } from "hono/cookie"
2
+
3
+ export const CONSTRUCTION_GUEST_COOKIE = "rimelight-construction-guest"
4
+
5
+ export type GuestEnv = {
6
+ CONSTRUCTION_GUEST_EMAIL?: string
7
+ CONSTRUCTION_GUEST_PASSWORD?: string
8
+ CONSTRUCTION_GUEST_SECRET?: string
9
+ BETTER_AUTH_SECRET?: string
10
+ }
11
+
12
+ const getConfig = (env?: GuestEnv) => ({
13
+ email: env?.CONSTRUCTION_GUEST_EMAIL ?? process.env.CONSTRUCTION_GUEST_EMAIL,
14
+ password: env?.CONSTRUCTION_GUEST_PASSWORD ?? process.env.CONSTRUCTION_GUEST_PASSWORD,
15
+ secret:
16
+ env?.CONSTRUCTION_GUEST_SECRET ??
17
+ process.env.CONSTRUCTION_GUEST_SECRET ??
18
+ env?.BETTER_AUTH_SECRET ??
19
+ process.env.BETTER_AUTH_SECRET
20
+ })
21
+
22
+ const encode = (value: Uint8Array) =>
23
+ btoa(String.fromCharCode(...value))
24
+ .replace(/\+/g, "-")
25
+ .replace(/\//g, "_")
26
+ .replace(/=+$/, "")
27
+
28
+ const decode = (value: string) => {
29
+ const padded = value
30
+ .replace(/-/g, "+")
31
+ .replace(/_/g, "/")
32
+ .padEnd(Math.ceil(value.length / 4) * 4, "=")
33
+ return Uint8Array.from(atob(padded), (char) => char.charCodeAt(0))
34
+ }
35
+
36
+ const sign = async (payload: string, secret: string) => {
37
+ const key = await crypto.subtle.importKey(
38
+ "raw",
39
+ new TextEncoder().encode(secret),
40
+ { name: "HMAC", hash: "SHA-256" },
41
+ false,
42
+ ["sign", "verify"]
43
+ )
44
+ return {
45
+ key,
46
+ signature: await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload))
47
+ }
48
+ }
49
+
50
+ export const isConstructionGuest = async (c: any) => {
51
+ const token = getCookie(c, CONSTRUCTION_GUEST_COOKIE)
52
+ const secret = getConfig(c.env).secret
53
+ if (!token || !secret) return false
54
+
55
+ try {
56
+ const [encodedPayload, encodedSignature] = token.split(".")
57
+ if (!encodedPayload || !encodedSignature) return false
58
+ const payload = new TextDecoder().decode(decode(encodedPayload))
59
+ const { key } = await sign(payload, secret)
60
+ const valid = await crypto.subtle.verify(
61
+ "HMAC",
62
+ key,
63
+ decode(encodedSignature),
64
+ new TextEncoder().encode(payload)
65
+ )
66
+ return valid && JSON.parse(payload).expiresAt > Date.now()
67
+ } catch {
68
+ return false
69
+ }
70
+ }
71
+
72
+ export const signInConstructionGuest = async (
73
+ c: any,
74
+ email: string,
75
+ password: string,
76
+ rememberMe = true
77
+ ) => {
78
+ const config = getConfig(c.env)
79
+ if (
80
+ !config.email ||
81
+ !config.password ||
82
+ !config.secret ||
83
+ email !== config.email ||
84
+ password !== config.password
85
+ ) {
86
+ return false
87
+ }
88
+
89
+ const payload = JSON.stringify({
90
+ email: config.email,
91
+ expiresAt: Date.now() + 1000 * 60 * 60 * 24 * 7
92
+ })
93
+ const { signature } = await sign(payload, config.secret)
94
+ setCookie(
95
+ c,
96
+ CONSTRUCTION_GUEST_COOKIE,
97
+ encode(new TextEncoder().encode(payload)) + "." + encode(new Uint8Array(signature)),
98
+ {
99
+ httpOnly: true,
100
+ secure: true,
101
+ sameSite: "Lax",
102
+ path: "/",
103
+ ...(rememberMe ? { maxAge: 60 * 60 * 24 * 7 } : {})
104
+ }
105
+ )
106
+ return true
107
+ }
@@ -0,0 +1,55 @@
1
+ import { APIError } from "better-auth/api"
2
+ import { normalizeUsername, RESTRICTED_SET } from "../reserved-usernames"
3
+ import { eq, and, count } from "drizzle-orm"
4
+
5
+ export interface TagHookOptions {
6
+ userTable?: any
7
+ restrictedSet?: Set<string>
8
+ }
9
+
10
+ export function createTagAndUsernameHook(db: any, userTable: any, options?: TagHookOptions) {
11
+ const restricted = options?.restrictedSet ?? RESTRICTED_SET
12
+
13
+ return async (userData: any, _ctx: any) => {
14
+ // 1. Normalize the username and check against the restricted set
15
+ const normalizedInput = normalizeUsername(userData.name)
16
+ if (restricted.has(normalizedInput)) {
17
+ throw new APIError("BAD_REQUEST", {
18
+ message: "This username is reserved for official use."
19
+ })
20
+ }
21
+
22
+ // 2. Generate a unique 4-digit discriminator tag
23
+ let uniqueTag = "0000"
24
+ const tryFindTag = async (attemptsLeft: number): Promise<string> => {
25
+ if (attemptsLeft <= 0) return "0000"
26
+ const newTag = Math.floor(Math.random() * 10000)
27
+ .toString()
28
+ .padStart(4, "0")
29
+
30
+ const result = await db
31
+ .select({ count: count() })
32
+ .from(userTable)
33
+ .where(and(eq(userTable.name, userData.name), eq(userTable.tag, newTag)))
34
+
35
+ const tagCount = result[0]?.count ?? 0
36
+ if (tagCount === 0) return newTag
37
+ return tryFindTag(attemptsLeft - 1)
38
+ }
39
+
40
+ uniqueTag = await tryFindTag(50)
41
+
42
+ if (uniqueTag === "0000") {
43
+ throw new APIError("INTERNAL_SERVER_ERROR", {
44
+ message: "Failed to generate a unique tag for this username. Please try again."
45
+ })
46
+ }
47
+
48
+ return {
49
+ data: {
50
+ ...userData,
51
+ tag: uniqueTag
52
+ }
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,198 @@
1
+ const HOMOGLYPH_MAP: Record<string, string> = {
2
+ "0": "o",
3
+ "1": "i",
4
+ "3": "e",
5
+ "4": "a",
6
+ "5": "s",
7
+ "7": "t",
8
+ "8": "b",
9
+ "9": "g",
10
+ "@": "a",
11
+ "!": "i",
12
+ "$": "s",
13
+ "|": "i"
14
+ }
15
+
16
+ export const normalizeUsername = (input: string): string => {
17
+ return input
18
+ .toLowerCase()
19
+ .split("")
20
+ .map((char) => HOMOGLYPH_MAP[char] || char)
21
+ .join("")
22
+ .replace(/[^a-z0-9]/g, "")
23
+ }
24
+
25
+ export const STANDARD_RESTRICTED_GROUPS = {
26
+ STAFF_ROLES: [
27
+ "admin",
28
+ "administrator",
29
+ "moderator",
30
+ "mod",
31
+ "staff",
32
+ "support",
33
+ "help",
34
+ "official",
35
+ "verified",
36
+ "system",
37
+ "root",
38
+ "bot",
39
+ "security",
40
+ "community",
41
+ "manager",
42
+ "dev",
43
+ "developer",
44
+ "designer",
45
+ "gamemaster",
46
+ "gm",
47
+ "assistant",
48
+ "coordinator",
49
+ "representative",
50
+ "agent",
51
+ "supervisor",
52
+ "executive",
53
+ "ambassador",
54
+ "expert",
55
+ "specialist",
56
+ "advocate",
57
+ "internal",
58
+ "employee",
59
+ "associate",
60
+ "webmaster",
61
+ "sysop",
62
+ "operator",
63
+ "host",
64
+ "referee",
65
+ "council",
66
+ "ceo",
67
+ "founder",
68
+ "owner"
69
+ ],
70
+ LEGAL_FINANCIAL: [
71
+ "checkout",
72
+ "subscribe",
73
+ "subscription",
74
+ "premium",
75
+ "vip",
76
+ "store",
77
+ "shop",
78
+ "marketplace",
79
+ "wallet",
80
+ "refund",
81
+ "invoice",
82
+ "payout",
83
+ "rewards",
84
+ "prize",
85
+ "giveaway",
86
+ "claims",
87
+ "verification",
88
+ "billing",
89
+ "payment",
90
+ "sales",
91
+ "marketing",
92
+ "legal",
93
+ "compliance",
94
+ "privacy",
95
+ "tos",
96
+ "terms",
97
+ "copyright",
98
+ "trademark",
99
+ "dmca",
100
+ "abuse",
101
+ "report"
102
+ ],
103
+ TECHNICAL: [
104
+ "null",
105
+ "undefined",
106
+ "nan",
107
+ "none",
108
+ "everyone",
109
+ "all",
110
+ "guest",
111
+ "user",
112
+ "test",
113
+ "tester",
114
+ "account",
115
+ "api",
116
+ "webhook",
117
+ "index",
118
+ "config",
119
+ "settings",
120
+ "profile",
121
+ "auth",
122
+ "login",
123
+ "signup",
124
+ "signin",
125
+ "logout",
126
+ "signout",
127
+ "localhost",
128
+ "ftp",
129
+ "smtp",
130
+ "pop3",
131
+ "imap",
132
+ "dns",
133
+ "proxy",
134
+ "cdn",
135
+ "static",
136
+ "assets",
137
+ "media",
138
+ "upload",
139
+ "download",
140
+ "docs",
141
+ "manual",
142
+ "guide",
143
+ "tutorial",
144
+ "error",
145
+ "404",
146
+ "500",
147
+ "maintenance",
148
+ "update",
149
+ "patch",
150
+ "changelog",
151
+ "status",
152
+ "db",
153
+ "database",
154
+ "sql",
155
+ "query",
156
+ "health",
157
+ "ping",
158
+ "metrics",
159
+ "logs",
160
+ "no-reply",
161
+ "noreply",
162
+ "donotreply",
163
+ "security-alert",
164
+ "mail",
165
+ "email",
166
+ "postmaster"
167
+ ],
168
+ LEETSPEAK: [
169
+ "4dmin",
170
+ "@dmin",
171
+ "st4ff",
172
+ "m0d",
173
+ "m0derator",
174
+ "4dm1n",
175
+ "4dmin1strator",
176
+ "m0d3rator",
177
+ "st4f",
178
+ "5upport",
179
+ "0fficial",
180
+ "v3rified",
181
+ "5ystem",
182
+ "r00t",
183
+ "b0t",
184
+ "s3curity",
185
+ "d3v",
186
+ "d3veloper",
187
+ "@dministrator",
188
+ "adm1n",
189
+ "adm1nistrator"
190
+ ]
191
+ }
192
+
193
+ export const createRestrictedUsernameSet = (customBlacklist: string[] = []): Set<string> => {
194
+ const base = Object.values(STANDARD_RESTRICTED_GROUPS).flat().concat(customBlacklist)
195
+ return new Set(base.map((name) => normalizeUsername(name)))
196
+ }
197
+
198
+ export const RESTRICTED_SET = createRestrictedUsernameSet()
@@ -0,0 +1,50 @@
1
+ export interface CoreFields {
2
+ name: string
3
+ email: string
4
+ emailVerified: boolean | null
5
+ image: string | null
6
+ createdAt: Date
7
+ updatedAt: Date
8
+ }
9
+
10
+ export interface SyntheticUserData {
11
+ coreFields: CoreFields
12
+ additionalFields: Record<string, unknown>
13
+ id: string
14
+ }
15
+
16
+ export function createSyntheticUser({ coreFields, additionalFields, id }: SyntheticUserData) {
17
+ return {
18
+ ...coreFields,
19
+ role: "user",
20
+ banned: false,
21
+ banReason: null,
22
+ banExpires: null,
23
+ tag: typeof additionalFields.tag === "string" ? additionalFields.tag : "0000",
24
+ firstName: typeof additionalFields.firstName === "string" ? additionalFields.firstName : "",
25
+ lastName: typeof additionalFields.lastName === "string" ? additionalFields.lastName : "",
26
+ availability:
27
+ typeof additionalFields.availability === "string"
28
+ ? additionalFields.availability
29
+ : "available",
30
+ status: typeof additionalFields.status === "string" ? additionalFields.status : "",
31
+ publicKey:
32
+ typeof additionalFields.publicKey === "string" ? additionalFields.publicKey : undefined,
33
+ id
34
+ }
35
+ }
36
+
37
+ export function createSyntheticUserWrapper(userData: any) {
38
+ return createSyntheticUser({
39
+ coreFields: {
40
+ name: userData.name,
41
+ email: userData.email,
42
+ emailVerified: userData.emailVerified,
43
+ image: userData.image,
44
+ createdAt: userData.createdAt,
45
+ updatedAt: userData.createdAt
46
+ },
47
+ additionalFields: userData,
48
+ id: crypto.randomUUID()
49
+ })
50
+ }
@@ -0,0 +1,363 @@
1
+ import { relations, sql } from "drizzle-orm"
2
+ import {
3
+ type AnyPgColumn,
4
+ pgTable,
5
+ text,
6
+ uniqueIndex,
7
+ index,
8
+ boolean,
9
+ timestamp,
10
+ integer
11
+ } from "drizzle-orm/pg-core"
12
+
13
+ export type UserAvailability = "available" | "busy" | "away" | "offline" | (string & {})
14
+
15
+ // ============================================================================
16
+ // Core Auth Tables
17
+ // ============================================================================
18
+
19
+ export const user = pgTable(
20
+ "user",
21
+ {
22
+ id: text("id")
23
+ .$defaultFn(() => crypto.randomUUID())
24
+ .notNull()
25
+ .primaryKey(),
26
+ name: text("name").notNull(),
27
+ tag: text("tag").notNull().default("0000"),
28
+ email: text("email").notNull().unique(),
29
+ emailVerified: boolean("email_verified").default(false).notNull(),
30
+ image: text("image"),
31
+ firstName: text("first_name").notNull(),
32
+ lastName: text("last_name").notNull(),
33
+ availability: text("availability").$type<UserAvailability>().notNull().default("available"),
34
+ status: text("status"),
35
+ updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
36
+ createdAt: timestamp("created_at")
37
+ .$defaultFn(() => new Date())
38
+ .notNull(),
39
+ deletedAt: timestamp("deleted_at"),
40
+ role: text("role"),
41
+ banned: boolean("banned").default(false),
42
+ banReason: text("ban_reason"),
43
+ banExpires: timestamp("ban_expires"),
44
+ publicKey: text("public_key"),
45
+ encryptedPrivateKey: text("encrypted_private_key"),
46
+ derivationSalt: text("derivation_salt")
47
+ },
48
+ (table) => [uniqueIndex("user_name_tag_unique").on(table.name, table.tag)]
49
+ )
50
+
51
+ export const session = pgTable(
52
+ "session",
53
+ {
54
+ id: text("id")
55
+ .$defaultFn(() => crypto.randomUUID())
56
+ .notNull()
57
+ .primaryKey(),
58
+ expiresAt: timestamp("expires_at").notNull(),
59
+ token: text("token").notNull().unique(),
60
+ updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
61
+ createdAt: timestamp("created_at")
62
+ .$defaultFn(() => new Date())
63
+ .notNull(),
64
+ deletedAt: timestamp("deleted_at"),
65
+ ipAddress: text("ip_address"),
66
+ userAgent: text("user_agent"),
67
+ userId: text("user_id")
68
+ .notNull()
69
+ .references(() => user.id, { onDelete: "cascade" }),
70
+ impersonatedBy: text("impersonated_by"),
71
+ activeOrganizationId: text("active_organization_id"),
72
+ activeTeamId: text("active_team_id")
73
+ },
74
+ (table) => [index("session_userId_idx").on(table.userId)]
75
+ )
76
+
77
+ export const account = pgTable(
78
+ "account",
79
+ {
80
+ id: text("id")
81
+ .$defaultFn(() => crypto.randomUUID())
82
+ .notNull()
83
+ .primaryKey(),
84
+ accountId: text("account_id").notNull(),
85
+ providerId: text("provider_id").notNull(),
86
+ userId: text("user_id")
87
+ .notNull()
88
+ .references(() => user.id, { onDelete: "cascade" }),
89
+ accessToken: text("access_token"),
90
+ refreshToken: text("refresh_token"),
91
+ idToken: text("id_token"),
92
+ accessTokenExpiresAt: timestamp("access_token_expires_at"),
93
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
94
+ scope: text("scope"),
95
+ password: text("password"),
96
+ updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
97
+ createdAt: timestamp("created_at")
98
+ .$defaultFn(() => new Date())
99
+ .notNull(),
100
+ deletedAt: timestamp("deleted_at")
101
+ },
102
+ (table) => [index("account_userId_idx").on(table.userId)]
103
+ )
104
+
105
+ export const verification = pgTable(
106
+ "verification",
107
+ {
108
+ id: text("id")
109
+ .$defaultFn(() => crypto.randomUUID())
110
+ .notNull()
111
+ .primaryKey(),
112
+ identifier: text("identifier").notNull(),
113
+ value: text("value").notNull(),
114
+ expiresAt: timestamp("expires_at").notNull(),
115
+ updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
116
+ createdAt: timestamp("created_at")
117
+ .$defaultFn(() => new Date())
118
+ .notNull(),
119
+ deletedAt: timestamp("deleted_at")
120
+ },
121
+ (table) => [index("verification_identifier_idx").on(table.identifier)]
122
+ )
123
+
124
+ export const rateLimit = pgTable("rate_limit", {
125
+ id: text("id")
126
+ .$defaultFn(() => crypto.randomUUID())
127
+ .notNull()
128
+ .primaryKey(),
129
+ key: text("key"),
130
+ count: integer("count"),
131
+ lastRequest: integer("last_request")
132
+ })
133
+
134
+ // ============================================================================
135
+ // Organization Tables
136
+ // ============================================================================
137
+
138
+ export const organization = pgTable("organization", {
139
+ id: text("id")
140
+ .$defaultFn(() => crypto.randomUUID())
141
+ .notNull()
142
+ .primaryKey(),
143
+ name: text("name").notNull(),
144
+ slug: text("slug").notNull().unique(),
145
+ logo: text("logo"),
146
+ updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
147
+ createdAt: timestamp("created_at")
148
+ .$defaultFn(() => new Date())
149
+ .notNull(),
150
+ deletedAt: timestamp("deleted_at"),
151
+ metadata: text("metadata")
152
+ })
153
+
154
+ export const member = pgTable(
155
+ "member",
156
+ {
157
+ id: text("id")
158
+ .$defaultFn(() => crypto.randomUUID())
159
+ .notNull()
160
+ .primaryKey(),
161
+ organizationId: text("organization_id")
162
+ .notNull()
163
+ .references(() => organization.id, { onDelete: "cascade" }),
164
+ userId: text("user_id")
165
+ .notNull()
166
+ .references(() => user.id, { onDelete: "cascade" }),
167
+ role: text("role").default("member").notNull(),
168
+ updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
169
+ createdAt: timestamp("created_at")
170
+ .$defaultFn(() => new Date())
171
+ .notNull(),
172
+ deletedAt: timestamp("deleted_at")
173
+ },
174
+ (table) => [
175
+ index("member_organizationId_idx").on(table.organizationId),
176
+ index("member_userId_idx").on(table.userId)
177
+ ]
178
+ )
179
+
180
+ export const invitation = pgTable(
181
+ "invitation",
182
+ {
183
+ id: text("id")
184
+ .$defaultFn(() => crypto.randomUUID())
185
+ .notNull()
186
+ .primaryKey(),
187
+ organizationId: text("organization_id")
188
+ .notNull()
189
+ .references(() => organization.id, { onDelete: "cascade" }),
190
+ email: text("email").notNull(),
191
+ role: text("role"),
192
+ status: text("status").default("pending").notNull(),
193
+ expiresAt: timestamp("expires_at").notNull(),
194
+ updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
195
+ createdAt: timestamp("created_at")
196
+ .$defaultFn(() => new Date())
197
+ .notNull(),
198
+ deletedAt: timestamp("deleted_at"),
199
+ inviterId: text("inviter_id")
200
+ .notNull()
201
+ .references(() => user.id, { onDelete: "cascade" })
202
+ },
203
+ (table) => [
204
+ index("invitation_organizationId_idx").on(table.organizationId),
205
+ index("invitation_email_idx").on(table.email)
206
+ ]
207
+ )
208
+
209
+ export const team = pgTable(
210
+ "team",
211
+ {
212
+ id: text("id")
213
+ .$defaultFn(() => crypto.randomUUID())
214
+ .notNull()
215
+ .primaryKey(),
216
+ name: text("name").notNull(),
217
+ organizationId: text("organization_id")
218
+ .notNull()
219
+ .references(() => organization.id, { onDelete: "cascade" }),
220
+ parentId: text("parent_id").references((): AnyPgColumn => team.id, {
221
+ onDelete: "cascade"
222
+ }),
223
+ updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
224
+ createdAt: timestamp("created_at")
225
+ .$defaultFn(() => new Date())
226
+ .notNull(),
227
+ deletedAt: timestamp("deleted_at"),
228
+ metadata: text("metadata")
229
+ },
230
+ (table) => [index("team_organizationId_idx").on(table.organizationId)]
231
+ )
232
+
233
+ export const teamMember = pgTable("team_member", {
234
+ id: text("id")
235
+ .$defaultFn(() => crypto.randomUUID())
236
+ .notNull()
237
+ .primaryKey(),
238
+ teamId: text("team_id")
239
+ .notNull()
240
+ .references(() => team.id, { onDelete: "cascade" }),
241
+ userId: text("user_id")
242
+ .notNull()
243
+ .references(() => user.id, { onDelete: "cascade" }),
244
+ role: text("role").notNull(),
245
+ updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
246
+ createdAt: timestamp("created_at")
247
+ .$defaultFn(() => new Date())
248
+ .notNull(),
249
+ deletedAt: timestamp("deleted_at")
250
+ })
251
+
252
+ // ============================================================================
253
+ // Relations
254
+ // ============================================================================
255
+
256
+ export const userRelations = relations(user, ({ many }) => ({
257
+ sessions: many(session),
258
+ accounts: many(account),
259
+ members: many(member),
260
+ invitations: many(invitation),
261
+ teamMembers: many(teamMember)
262
+ }))
263
+
264
+ export const sessionRelations = relations(session, ({ one }) => ({
265
+ user: one(user, {
266
+ fields: [session.userId],
267
+ references: [user.id]
268
+ })
269
+ }))
270
+
271
+ export const accountRelations = relations(account, ({ one }) => ({
272
+ user: one(user, {
273
+ fields: [account.userId],
274
+ references: [user.id]
275
+ })
276
+ }))
277
+
278
+ export const organizationRelations = relations(organization, ({ many }) => ({
279
+ members: many(member),
280
+ invitations: many(invitation),
281
+ teams: many(team)
282
+ }))
283
+
284
+ export const memberRelations = relations(member, ({ one }) => ({
285
+ organization: one(organization, {
286
+ fields: [member.organizationId],
287
+ references: [organization.id]
288
+ }),
289
+ user: one(user, {
290
+ fields: [member.userId],
291
+ references: [user.id]
292
+ })
293
+ }))
294
+
295
+ export const invitationRelations = relations(invitation, ({ one }) => ({
296
+ organization: one(organization, {
297
+ fields: [invitation.organizationId],
298
+ references: [organization.id]
299
+ }),
300
+ user: one(user, {
301
+ fields: [invitation.inviterId],
302
+ references: [user.id]
303
+ })
304
+ }))
305
+
306
+ export const teamRelations = relations(team, ({ one, many }) => ({
307
+ organization: one(organization, {
308
+ fields: [team.organizationId],
309
+ references: [organization.id]
310
+ }),
311
+ parentTeam: one(team, {
312
+ fields: [team.parentId],
313
+ references: [team.id],
314
+ relationName: "subteams"
315
+ }),
316
+ subteams: many(team, { relationName: "subteams" }),
317
+ members: many(teamMember)
318
+ }))
319
+
320
+ export const teamMemberRelations = relations(teamMember, ({ one }) => ({
321
+ team: one(team, {
322
+ fields: [teamMember.teamId],
323
+ references: [team.id]
324
+ }),
325
+ user: one(user, {
326
+ fields: [teamMember.userId],
327
+ references: [user.id]
328
+ })
329
+ }))
330
+
331
+ // ============================================================================
332
+ // Roles & Permissions Tables
333
+ // ============================================================================
334
+
335
+ export const role = pgTable("role", {
336
+ id: text("id")
337
+ .$defaultFn(() => crypto.randomUUID())
338
+ .notNull()
339
+ .primaryKey(),
340
+ name: text("name").notNull().unique(),
341
+ displayName: text("display_name").notNull(),
342
+ description: text("description"),
343
+ permissions: text("permissions")
344
+ .$type<string[]>()
345
+ .notNull()
346
+ .default(sql`'[]'`),
347
+ updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
348
+ createdAt: timestamp("created_at")
349
+ .$defaultFn(() => new Date())
350
+ .notNull(),
351
+ deletedAt: timestamp("deleted_at")
352
+ })
353
+
354
+ export type User = typeof user.$inferSelect
355
+ export type Session = typeof session.$inferSelect
356
+ export type Account = typeof account.$inferSelect
357
+ export type Verification = typeof verification.$inferSelect
358
+ export type Organization = typeof organization.$inferSelect
359
+ export type Member = typeof member.$inferSelect
360
+ export type Invitation = typeof invitation.$inferSelect
361
+ export type Team = typeof team.$inferSelect
362
+ export type TeamMember = typeof teamMember.$inferSelect
363
+ export type Role = typeof role.$inferSelect
@@ -0,0 +1,148 @@
1
+ import type { BetterAuthOptions } from "better-auth"
2
+ import { betterAuth } from "better-auth"
3
+ import { createSyntheticUserWrapper } from "../plugins/synthetic-user"
4
+ import { createTagAndUsernameHook } from "../plugins/discriminator-tag"
5
+
6
+ export interface RimelightAuthFactoryOptions {
7
+ database: BetterAuthOptions["database"]
8
+ db?: any
9
+ userTable?: any
10
+ emailHandlers?: {
11
+ sendVerificationEmail?: (data: any, request?: Request) => Promise<void>
12
+ sendPasswordResetEmail?: (data: any, request?: Request) => Promise<void>
13
+ sendExistingUserSignUpNotification?: (existingUser: any) => Promise<void>
14
+ }
15
+ additionalUserFields?: Record<string, any>
16
+ plugins?: BetterAuthOptions["plugins"]
17
+ statements?: any
18
+ advanced?: BetterAuthOptions["advanced"]
19
+ session?: BetterAuthOptions["session"]
20
+ rateLimit?: BetterAuthOptions["rateLimit"]
21
+ customDatabaseHooks?: BetterAuthOptions["databaseHooks"]
22
+ enableTagGenerator?: boolean
23
+ restrictedUsernameBlacklist?: string[]
24
+ }
25
+
26
+ export function createRimelightAuth(options: RimelightAuthFactoryOptions) {
27
+ const enableTag = options.enableTagGenerator ?? (options.db && options.userTable)
28
+
29
+ const defaultUserHooks =
30
+ enableTag && options.db && options.userTable
31
+ ? {
32
+ create: {
33
+ before: createTagAndUsernameHook(options.db, options.userTable)
34
+ }
35
+ }
36
+ : undefined
37
+
38
+ const emailAndPasswordConfig: BetterAuthOptions["emailAndPassword"] = {
39
+ enabled: true,
40
+ autoSignIn: false,
41
+ requireEmailVerification: true,
42
+ minPasswordLength: 8,
43
+ maxPasswordLength: 128,
44
+ customSyntheticUser: createSyntheticUserWrapper
45
+ }
46
+
47
+ if (options.emailHandlers?.sendPasswordResetEmail) {
48
+ emailAndPasswordConfig.sendResetPassword = options.emailHandlers.sendPasswordResetEmail
49
+ }
50
+ if (options.emailHandlers?.sendExistingUserSignUpNotification) {
51
+ const handler = options.emailHandlers.sendExistingUserSignUpNotification
52
+ emailAndPasswordConfig.onExistingUserSignUp = async (existingUser: any) => {
53
+ await handler(existingUser)
54
+ }
55
+ }
56
+
57
+ const emailVerificationConfig: BetterAuthOptions["emailVerification"] = {
58
+ autoSignInAfterVerification: true
59
+ }
60
+
61
+ if (options.emailHandlers?.sendVerificationEmail) {
62
+ emailVerificationConfig.sendVerificationEmail = options.emailHandlers.sendVerificationEmail
63
+ }
64
+
65
+ return betterAuth({
66
+ database: options.database,
67
+ emailAndPassword: emailAndPasswordConfig,
68
+ emailVerification: emailVerificationConfig,
69
+ user: {
70
+ changeEmail: {
71
+ enabled: true
72
+ },
73
+ deleteUser: {
74
+ enabled: true
75
+ },
76
+ additionalFields: {
77
+ tag: {
78
+ type: "string",
79
+ required: false,
80
+ input: false
81
+ },
82
+ firstName: {
83
+ type: "string",
84
+ required: true,
85
+ default: "",
86
+ input: true
87
+ },
88
+ lastName: {
89
+ type: "string",
90
+ required: true,
91
+ default: "",
92
+ input: true
93
+ },
94
+ role: {
95
+ type: "string",
96
+ required: false,
97
+ default: "user",
98
+ input: false
99
+ },
100
+ availability: {
101
+ type: "string",
102
+ required: false,
103
+ default: "available"
104
+ },
105
+ status: {
106
+ type: "string",
107
+ required: false,
108
+ default: ""
109
+ },
110
+ publicKey: {
111
+ type: "string",
112
+ required: false,
113
+ input: true
114
+ },
115
+ ...options.additionalUserFields
116
+ }
117
+ },
118
+ session: {
119
+ expiresIn: 60 * 60 * 24 * 7,
120
+ updateAge: 60 * 60 * 24,
121
+ freshAge: 60 * 15,
122
+ cookieCache: {
123
+ enabled: true,
124
+ maxAge: 60 * 5
125
+ },
126
+ ...options.session
127
+ },
128
+ rateLimit: {
129
+ window: 10,
130
+ max: 100,
131
+ storage: "database",
132
+ modelName: "rateLimit",
133
+ ...options.rateLimit
134
+ },
135
+ plugins: options.plugins ?? [],
136
+ auth: options.statements ? { statements: options.statements } : undefined,
137
+ advanced: {
138
+ useSecureCookies: process.env.NODE_ENV === "production",
139
+ cookiePrefix: "auth",
140
+ database: {
141
+ generateId: () => crypto.randomUUID()
142
+ },
143
+ ...options.advanced
144
+ },
145
+ databaseHooks:
146
+ options.customDatabaseHooks ?? (defaultUserHooks ? { user: defaultUserHooks } : undefined)
147
+ })
148
+ }