@m5kdev/backend 0.1.0

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 (113) hide show
  1. package/LICENSE +621 -0
  2. package/README.md +22 -0
  3. package/package.json +205 -0
  4. package/src/lib/posthog.ts +5 -0
  5. package/src/lib/sentry.ts +8 -0
  6. package/src/modules/access/access.repository.ts +36 -0
  7. package/src/modules/access/access.service.ts +81 -0
  8. package/src/modules/access/access.test.ts +216 -0
  9. package/src/modules/access/access.utils.ts +46 -0
  10. package/src/modules/ai/ai.db.ts +38 -0
  11. package/src/modules/ai/ai.prompt.ts +47 -0
  12. package/src/modules/ai/ai.repository.ts +53 -0
  13. package/src/modules/ai/ai.router.ts +148 -0
  14. package/src/modules/ai/ai.service.ts +310 -0
  15. package/src/modules/ai/ai.trpc.ts +22 -0
  16. package/src/modules/ai/ideogram/ideogram.constants.ts +170 -0
  17. package/src/modules/ai/ideogram/ideogram.dto.ts +64 -0
  18. package/src/modules/ai/ideogram/ideogram.prompt.ts +858 -0
  19. package/src/modules/ai/ideogram/ideogram.repository.ts +39 -0
  20. package/src/modules/ai/ideogram/ideogram.service.ts +14 -0
  21. package/src/modules/auth/auth.db.ts +224 -0
  22. package/src/modules/auth/auth.dto.ts +47 -0
  23. package/src/modules/auth/auth.lib.ts +349 -0
  24. package/src/modules/auth/auth.middleware.ts +62 -0
  25. package/src/modules/auth/auth.repository.ts +672 -0
  26. package/src/modules/auth/auth.service.ts +261 -0
  27. package/src/modules/auth/auth.trpc.ts +208 -0
  28. package/src/modules/auth/auth.utils.ts +117 -0
  29. package/src/modules/base/base.abstract.ts +62 -0
  30. package/src/modules/base/base.dto.ts +206 -0
  31. package/src/modules/base/base.grants.test.ts +861 -0
  32. package/src/modules/base/base.grants.ts +199 -0
  33. package/src/modules/base/base.repository.ts +433 -0
  34. package/src/modules/base/base.service.ts +154 -0
  35. package/src/modules/base/base.types.ts +7 -0
  36. package/src/modules/billing/billing.db.ts +27 -0
  37. package/src/modules/billing/billing.repository.ts +328 -0
  38. package/src/modules/billing/billing.router.ts +77 -0
  39. package/src/modules/billing/billing.service.ts +177 -0
  40. package/src/modules/billing/billing.trpc.ts +17 -0
  41. package/src/modules/clay/clay.repository.ts +29 -0
  42. package/src/modules/clay/clay.service.ts +61 -0
  43. package/src/modules/connect/connect.db.ts +32 -0
  44. package/src/modules/connect/connect.dto.ts +44 -0
  45. package/src/modules/connect/connect.linkedin.ts +70 -0
  46. package/src/modules/connect/connect.oauth.ts +288 -0
  47. package/src/modules/connect/connect.repository.ts +65 -0
  48. package/src/modules/connect/connect.router.ts +76 -0
  49. package/src/modules/connect/connect.service.ts +171 -0
  50. package/src/modules/connect/connect.trpc.ts +26 -0
  51. package/src/modules/connect/connect.types.ts +27 -0
  52. package/src/modules/crypto/crypto.db.ts +15 -0
  53. package/src/modules/crypto/crypto.repository.ts +13 -0
  54. package/src/modules/crypto/crypto.service.ts +57 -0
  55. package/src/modules/email/email.service.ts +222 -0
  56. package/src/modules/file/file.repository.ts +95 -0
  57. package/src/modules/file/file.router.ts +108 -0
  58. package/src/modules/file/file.service.ts +186 -0
  59. package/src/modules/recurrence/recurrence.db.ts +79 -0
  60. package/src/modules/recurrence/recurrence.repository.ts +70 -0
  61. package/src/modules/recurrence/recurrence.service.ts +105 -0
  62. package/src/modules/recurrence/recurrence.trpc.ts +82 -0
  63. package/src/modules/social/social.dto.ts +22 -0
  64. package/src/modules/social/social.linkedin.test.ts +277 -0
  65. package/src/modules/social/social.linkedin.ts +593 -0
  66. package/src/modules/social/social.service.ts +112 -0
  67. package/src/modules/social/social.types.ts +43 -0
  68. package/src/modules/tag/tag.db.ts +41 -0
  69. package/src/modules/tag/tag.dto.ts +18 -0
  70. package/src/modules/tag/tag.repository.ts +222 -0
  71. package/src/modules/tag/tag.service.ts +48 -0
  72. package/src/modules/tag/tag.trpc.ts +62 -0
  73. package/src/modules/uploads/0581796b-8845-420d-bd95-cd7de79f6d37.webm +0 -0
  74. package/src/modules/uploads/33b1e649-6727-4bd0-94d0-a0b363646865.webm +0 -0
  75. package/src/modules/uploads/49a8c4c0-54d7-4c94-bef4-c93c029f9ed0.webm +0 -0
  76. package/src/modules/uploads/50e31e38-a2f0-47ca-8b7d-2d7fcad9267d.webm +0 -0
  77. package/src/modules/uploads/72ac8cf9-c3a7-4cd8-8a78-6d8e137a4c7e.webm +0 -0
  78. package/src/modules/uploads/75293649-d966-46cd-a675-67518958ae9c.png +0 -0
  79. package/src/modules/uploads/88b7b867-ce15-4891-bf73-81305a7de1f7.wav +0 -0
  80. package/src/modules/uploads/a5d6fee8-6a59-42c6-9d4a-ac8a3c5e7245.webm +0 -0
  81. package/src/modules/uploads/c13a9785-ca5a-4983-af30-b338ed76d370.webm +0 -0
  82. package/src/modules/uploads/caa1a5a7-71ba-4381-902d-7e2cafdf6dcb.webm +0 -0
  83. package/src/modules/uploads/cbeb0b81-374d-445b-914b-40ace7c8e031.webm +0 -0
  84. package/src/modules/uploads/d626aa82-b10f-493f-aee7-87bfb3361dfc.webm +0 -0
  85. package/src/modules/uploads/d7de4c16-de0c-495d-9612-e72260a6ecca.png +0 -0
  86. package/src/modules/uploads/e532e38a-6421-400e-8a5f-8e7bc8ce411b.wav +0 -0
  87. package/src/modules/uploads/e86ec867-6adf-4c51-84e0-00b0836625e8.webm +0 -0
  88. package/src/modules/utils/applyPagination.ts +13 -0
  89. package/src/modules/utils/applySorting.ts +21 -0
  90. package/src/modules/utils/getConditionsFromFilters.ts +216 -0
  91. package/src/modules/video/video.service.ts +89 -0
  92. package/src/modules/webhook/webhook.constants.ts +9 -0
  93. package/src/modules/webhook/webhook.db.ts +15 -0
  94. package/src/modules/webhook/webhook.dto.ts +9 -0
  95. package/src/modules/webhook/webhook.repository.ts +68 -0
  96. package/src/modules/webhook/webhook.router.ts +29 -0
  97. package/src/modules/webhook/webhook.service.ts +78 -0
  98. package/src/modules/workflow/workflow.db.ts +29 -0
  99. package/src/modules/workflow/workflow.repository.ts +171 -0
  100. package/src/modules/workflow/workflow.service.ts +56 -0
  101. package/src/modules/workflow/workflow.trpc.ts +26 -0
  102. package/src/modules/workflow/workflow.types.ts +30 -0
  103. package/src/modules/workflow/workflow.utils.ts +259 -0
  104. package/src/test/stubs/utils.ts +2 -0
  105. package/src/trpc/context.ts +21 -0
  106. package/src/trpc/index.ts +3 -0
  107. package/src/trpc/procedures.ts +43 -0
  108. package/src/trpc/utils.ts +20 -0
  109. package/src/types.ts +22 -0
  110. package/src/utils/errors.ts +148 -0
  111. package/src/utils/logger.ts +8 -0
  112. package/src/utils/posthog.ts +43 -0
  113. package/src/utils/types.ts +5 -0
@@ -0,0 +1,39 @@
1
+ import { ok } from "neverthrow";
2
+ import type {
3
+ IdeogramV3GenerateInput,
4
+ IdeogramV3GenerateOutput,
5
+ } from "#modules/ai/ideogram/ideogram.dto";
6
+ import type { ServerResultAsync } from "#modules/base/base.dto";
7
+ import { BaseExternaRepository } from "#modules/base/base.repository";
8
+
9
+ export class IdeogramRepository extends BaseExternaRepository {
10
+ async generate(input: IdeogramV3GenerateInput): ServerResultAsync<IdeogramV3GenerateOutput> {
11
+ if (!process.env.IDEOGRAM_API_KEY)
12
+ return this.error("INTERNAL_SERVER_ERROR", "IDEOGRAM_API_KEY is not set");
13
+ return this.throwableAsync(async () => {
14
+ const formData = new FormData();
15
+ formData.append("prompt", input.prompt);
16
+ if (input.seed) formData.append("seed", input.seed.toString());
17
+ if (input.resolution) formData.append("resolution", input.resolution);
18
+ if (input.rendering_speed) formData.append("rendering_speed", input.rendering_speed);
19
+ if (input.magic_prompt) formData.append("magic_prompt", input.magic_prompt);
20
+ if (input.negative_prompt) formData.append("negative_prompt", input.negative_prompt);
21
+ if (input.num_images) formData.append("num_images", input.num_images.toString());
22
+ if (input.color_palette)
23
+ formData.append("color_palette", JSON.stringify(input.color_palette));
24
+ if (input.style_codes) formData.append("style_codes", JSON.stringify(input.style_codes));
25
+ if (input.aspect_ratio) formData.append("aspect_ratio", input.aspect_ratio);
26
+ if (input.style_type) formData.append("style_type", input.style_type);
27
+ if (input.style_preset) formData.append("style_preset", input.style_preset);
28
+ //TODO: Add file support
29
+
30
+ const response = await fetch("https://api.ideogram.ai/v1/ideogram-v3/generate", {
31
+ method: "POST",
32
+ headers: { "Api-Key": process.env.IDEOGRAM_API_KEY! },
33
+ body: formData,
34
+ });
35
+ const data = await response.json();
36
+ return ok(data as IdeogramV3GenerateOutput);
37
+ });
38
+ }
39
+ }
@@ -0,0 +1,14 @@
1
+ import type {
2
+ IdeogramV3GenerateInput,
3
+ IdeogramV3GenerateOutput,
4
+ } from "#modules/ai/ideogram/ideogram.dto";
5
+ import type { IdeogramRepository } from "#modules/ai/ideogram/ideogram.repository";
6
+ import type { ServerResultAsync } from "#modules/base/base.dto";
7
+ import { BaseService } from "#modules/base/base.service";
8
+
9
+ export class IdeogramService extends BaseService<{ ideogram: IdeogramRepository }, never> {
10
+ async generate(input: IdeogramV3GenerateInput): ServerResultAsync<IdeogramV3GenerateOutput> {
11
+ const result = await this.repository.ideogram.generate(input);
12
+ return result;
13
+ }
14
+ }
@@ -0,0 +1,224 @@
1
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
+ import { v4 as uuidv4 } from "uuid";
3
+
4
+ export const users = sqliteTable("users", {
5
+ id: text("id").primaryKey().$default(uuidv4),
6
+ name: text("name").notNull(),
7
+ email: text("email").notNull().unique(),
8
+ emailVerified: integer("email_verified", { mode: "boolean" }).notNull(),
9
+ image: text("image"),
10
+ createdAt: integer("created_at", { mode: "timestamp" })
11
+ .notNull()
12
+ .$default(() => new Date()),
13
+ updatedAt: integer("updated_at", { mode: "timestamp" })
14
+ .notNull()
15
+ .$default(() => new Date()),
16
+ role: text("role"),
17
+ banned: integer("banned", { mode: "boolean" }),
18
+ banReason: text("ban_reason"),
19
+ banExpires: integer("ban_expires", { mode: "timestamp" }),
20
+ stripeCustomerId: text("stripe_customer_id").unique(),
21
+ paymentCustomerId: text("payment_customer_id").unique(),
22
+ paymentPlanTier: text("payment_plan_tier"),
23
+ paymentPlanExpiresAt: integer("payment_plan_expires_at", {
24
+ mode: "timestamp",
25
+ }),
26
+ preferences: text("preferences"),
27
+ metadata: text("metadata", { mode: "json" })
28
+ .notNull()
29
+ .default({})
30
+ .$type<Record<string, unknown>>(),
31
+ onboarding: integer("onboarding"),
32
+ flags: text("flags"),
33
+ });
34
+
35
+ export const sessions = sqliteTable("sessions", {
36
+ id: text("id").primaryKey().$default(uuidv4),
37
+ expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
38
+ token: text("token").notNull().unique(),
39
+ createdAt: integer("created_at", { mode: "timestamp" })
40
+ .notNull()
41
+ .$default(() => new Date()),
42
+ updatedAt: integer("updated_at", { mode: "timestamp" })
43
+ .notNull()
44
+ .$default(() => new Date()),
45
+ ipAddress: text("ip_address"),
46
+ userAgent: text("user_agent"),
47
+ userId: text("user_id")
48
+ .notNull()
49
+ .references(() => users.id, { onDelete: "cascade" }),
50
+ impersonatedBy: text("impersonated_by"),
51
+ activeOrganizationId: text("active_organization_id"),
52
+ activeOrganizationRole: text("active_organization_role"),
53
+ activeTeamId: text("active_team_id"),
54
+ activeTeamRole: text("active_team_role"),
55
+ });
56
+
57
+ export const accounts = sqliteTable("accounts", {
58
+ id: text("id").primaryKey().$default(uuidv4),
59
+ accountId: text("account_id").notNull(),
60
+ providerId: text("provider_id").notNull(),
61
+ userId: text("user_id")
62
+ .notNull()
63
+ .references(() => users.id, { onDelete: "cascade" }),
64
+ accessToken: text("access_token"),
65
+ refreshToken: text("refresh_token"),
66
+ idToken: text("id_token"),
67
+ accessTokenExpiresAt: integer("access_token_expires_at", {
68
+ mode: "timestamp",
69
+ }),
70
+ refreshTokenExpiresAt: integer("refresh_token_expires_at", {
71
+ mode: "timestamp",
72
+ }),
73
+ scope: text("scope"),
74
+ password: text("password"),
75
+ createdAt: integer("created_at", { mode: "timestamp" })
76
+ .notNull()
77
+ .$default(() => new Date()),
78
+ updatedAt: integer("updated_at", { mode: "timestamp" })
79
+ .notNull()
80
+ .$default(() => new Date()),
81
+ });
82
+
83
+ export const verifications = sqliteTable("verifications", {
84
+ id: text("id").primaryKey().$default(uuidv4),
85
+ identifier: text("identifier").notNull(),
86
+ value: text("value").notNull(),
87
+ expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
88
+ createdAt: integer("created_at", { mode: "timestamp" }).$default(() => new Date()),
89
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$default(() => new Date()),
90
+ });
91
+
92
+ export const organizations = sqliteTable("organizations", {
93
+ id: text("id").primaryKey().$default(uuidv4),
94
+ name: text("name").notNull(),
95
+ slug: text("slug").unique(),
96
+ logo: text("logo"),
97
+ createdAt: integer("created_at", { mode: "timestamp" })
98
+ .notNull()
99
+ .$default(() => new Date()),
100
+ metadata: text("metadata"),
101
+ });
102
+
103
+ export const members = sqliteTable("members", {
104
+ id: text("id").primaryKey().$default(uuidv4),
105
+ organizationId: text("organization_id")
106
+ .notNull()
107
+ .references(() => organizations.id),
108
+ userId: text("user_id")
109
+ .notNull()
110
+ .references(() => users.id),
111
+ role: text("role").notNull(),
112
+ createdAt: integer("created_at", { mode: "timestamp" })
113
+ .notNull()
114
+ .$default(() => new Date()),
115
+ });
116
+
117
+ export const teams = sqliteTable("teams", {
118
+ id: text("id").primaryKey().$default(uuidv4),
119
+ name: text("name").notNull(),
120
+ organizationId: text("organization_id")
121
+ .notNull()
122
+ .references(() => organizations.id),
123
+ createdAt: integer("created_at", { mode: "timestamp" })
124
+ .notNull()
125
+ .$default(() => new Date()),
126
+ updatedAt: integer("updated_at", { mode: "timestamp" }),
127
+ });
128
+
129
+ export const teamMembers = sqliteTable("teammembers", {
130
+ id: text("id").primaryKey().$default(uuidv4),
131
+ teamId: text("team_id")
132
+ .notNull()
133
+ .references(() => teams.id),
134
+ userId: text("user_id")
135
+ .notNull()
136
+ .references(() => users.id),
137
+ role: text("role").notNull(),
138
+ createdAt: integer("created_at", { mode: "timestamp" })
139
+ .notNull()
140
+ .$default(() => new Date()),
141
+ });
142
+
143
+ export const invitations = sqliteTable("invitations", {
144
+ id: text("id").primaryKey().$default(uuidv4),
145
+ organizationId: text("organization_id")
146
+ .notNull()
147
+ .references(() => organizations.id),
148
+ teamId: text("team_id").references(() => teams.id),
149
+ email: text("email").notNull(),
150
+ role: text("role"),
151
+ status: text("status").notNull(),
152
+ createdAt: integer("created_at", { mode: "timestamp" })
153
+ .notNull()
154
+ .$default(() => new Date()),
155
+ expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
156
+ inviterId: text("inviter_id")
157
+ .notNull()
158
+ .references(() => users.id),
159
+ });
160
+
161
+ export const apikeys = sqliteTable("apikeys", {
162
+ id: text("id").primaryKey().$default(uuidv4),
163
+ name: text("name"),
164
+ start: text("start"),
165
+ prefix: text("prefix"),
166
+ key: text("key").notNull(),
167
+ userId: text("user_id")
168
+ .notNull()
169
+ .references(() => users.id),
170
+ refillInterval: integer("refill_interval", { mode: "number" }),
171
+ refillAmount: integer("refill_amount", { mode: "number" }),
172
+ lastRefillAt: integer("last_refill_at", { mode: "timestamp" }),
173
+ enabled: integer("enabled", { mode: "boolean" }).notNull(),
174
+ rateLimitEnabled: integer("rate_limit_enabled", { mode: "boolean" }).notNull(),
175
+ rateLimitTimeWindow: integer("rate_limit_time_window", { mode: "number" }),
176
+ rateLimitMax: integer("rate_limit_max", { mode: "number" }),
177
+ requestCount: integer("request_count", { mode: "number" }).notNull(),
178
+ remaining: integer("remaining", { mode: "number" }),
179
+ lastRequest: integer("last_request", { mode: "timestamp" }),
180
+ expiresAt: integer("expires_at", { mode: "timestamp" }),
181
+ createdAt: integer("created_at", { mode: "timestamp" })
182
+ .notNull()
183
+ .$default(() => new Date()),
184
+ updatedAt: integer("updated_at", { mode: "timestamp" })
185
+ .notNull()
186
+ .$default(() => new Date()),
187
+ permissions: text("permissions"),
188
+ metadata: text("metadata"),
189
+ });
190
+
191
+ export const waitlist = sqliteTable("waitlist", {
192
+ id: text("id").primaryKey().$default(uuidv4),
193
+ name: text("name"),
194
+ email: text("email"),
195
+ createdAt: integer("created_at", { mode: "timestamp" })
196
+ .notNull()
197
+ .$default(() => new Date()),
198
+ updatedAt: integer("updated_at", { mode: "timestamp" }),
199
+ status: text("status").notNull().default("WAITLIST"),
200
+ type: text("type").notNull().default("WAITLIST"),
201
+ code: text("code"),
202
+ expiresAt: integer("expires_at", { mode: "timestamp" }),
203
+ userId: text("user_id").references(() => users.id),
204
+ claimUserId: text("claim_user_id").references(() => users.id),
205
+ claimedAt: integer("claimed_at", { mode: "timestamp" }),
206
+ claimedEmail: text("claimed_email"),
207
+ });
208
+
209
+ export const accountClaimMagicLinks = sqliteTable("account_claim_magic_links", {
210
+ id: text("id").primaryKey().$default(uuidv4),
211
+ claimId: text("claim_id")
212
+ .notNull()
213
+ .references(() => waitlist.id, { onDelete: "cascade" }),
214
+ userId: text("user_id")
215
+ .notNull()
216
+ .references(() => users.id, { onDelete: "cascade" }),
217
+ email: text("email").notNull(),
218
+ token: text("token").notNull(),
219
+ url: text("url").notNull(),
220
+ expiresAt: integer("expires_at", { mode: "timestamp" }),
221
+ createdAt: integer("created_at", { mode: "timestamp" })
222
+ .notNull()
223
+ .$default(() => new Date()),
224
+ });
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+
3
+ export const waitlistSchema = z.object({
4
+ id: z.string(),
5
+ name: z.string().nullable(),
6
+ email: z.string().nullable(),
7
+ createdAt: z.date(),
8
+ updatedAt: z.date().nullable(),
9
+ status: z.string(),
10
+ code: z.string().nullable(),
11
+ expiresAt: z.date().nullable(),
12
+ });
13
+
14
+ export const waitlistOutputSchema = waitlistSchema.omit({ code: true, expiresAt: true });
15
+ export type WaitlistOutput = z.infer<typeof waitlistOutputSchema>;
16
+ export type Waitlist = z.infer<typeof waitlistSchema>;
17
+
18
+ export const accountClaimSchema = z.object({
19
+ id: z.string(),
20
+ claimUserId: z.string().nullable(),
21
+ code: z.string().nullable(),
22
+ status: z.string(),
23
+ expiresAt: z.date().nullable(),
24
+ claimedAt: z.date().nullable(),
25
+ claimedEmail: z.string().nullable(),
26
+ createdAt: z.date(),
27
+ updatedAt: z.date().nullable(),
28
+ });
29
+
30
+ export const accountClaimOutputSchema = accountClaimSchema.omit({ code: true });
31
+ export type AccountClaim = z.infer<typeof accountClaimSchema>;
32
+ export type AccountClaimOutput = z.infer<typeof accountClaimOutputSchema>;
33
+
34
+ export const accountClaimMagicLinkSchema = z.object({
35
+ id: z.string(),
36
+ claimId: z.string(),
37
+ userId: z.string(),
38
+ email: z.string(),
39
+ token: z.string(),
40
+ url: z.string(),
41
+ expiresAt: z.date().nullable(),
42
+ createdAt: z.date(),
43
+ });
44
+
45
+ export const accountClaimMagicLinkOutputSchema = accountClaimMagicLinkSchema.omit({ token: true });
46
+ export type AccountClaimMagicLink = z.infer<typeof accountClaimMagicLinkSchema>;
47
+ export type AccountClaimMagicLinkOutput = z.infer<typeof accountClaimMagicLinkOutputSchema>;
@@ -0,0 +1,349 @@
1
+ import { type BetterAuthOptions, betterAuth } from "better-auth";
2
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
3
+ import { getOAuthState } from "better-auth/api";
4
+ import { admin, apiKey, lastLoginMethod, magicLink, organization } from "better-auth/plugins";
5
+ import { and, desc, eq, gte, type InferSelectModel } from "drizzle-orm";
6
+ import type { LibSQLDatabase } from "drizzle-orm/libsql";
7
+ import * as auth from "#modules/auth/auth.db";
8
+ import { createOrganizationAndTeam, getActiveOrganizationAndTeam } from "#modules/auth/auth.utils";
9
+ import type { BillingService } from "#modules/billing/billing.service";
10
+ import type { EmailService } from "#modules/email/email.service";
11
+ import { logger as rootLogger } from "#utils/logger";
12
+ import { posthogCapture } from "#utils/posthog";
13
+
14
+ const schema = { ...auth };
15
+ type Schema = typeof schema;
16
+ export type Orm = LibSQLDatabase<Schema>;
17
+
18
+ export type User = InferSelectModel<typeof auth.users>;
19
+ export type Session = InferSelectModel<typeof auth.sessions>;
20
+ export type Context = { session: Session; user: User };
21
+
22
+ export type BetterAuth = ReturnType<typeof betterAuth>;
23
+
24
+ type CreateBetterAuthParams<
25
+ O extends Orm,
26
+ S extends Schema,
27
+ E extends EmailService,
28
+ B extends BillingService,
29
+ > = {
30
+ orm: O;
31
+ schema: S;
32
+ services: {
33
+ email?: E;
34
+ billing?: B;
35
+ };
36
+ hooks?: {
37
+ onError?: (error: unknown) => void;
38
+ afterCreateUser?: (
39
+ user: Pick<User, "id" | "email" | "emailVerified" | "name" | "createdAt" | "updatedAt">
40
+ ) => Promise<void>;
41
+ };
42
+ options?: BetterAuthOptions;
43
+ config?: {
44
+ waitlist: boolean;
45
+ provisionedAccountEmailDomain?: string;
46
+ };
47
+ };
48
+
49
+ export function createBetterAuth<
50
+ O extends Orm,
51
+ S extends Schema,
52
+ E extends EmailService,
53
+ B extends BillingService,
54
+ >({ orm, schema, services, hooks, options, config }: CreateBetterAuthParams<O, S, E, B>) {
55
+ const { email: emailService, billing: billingService } = services;
56
+ const { waitlist = false, provisionedAccountEmailDomain } = config ?? {};
57
+ const normalizedProvisionedAccountEmailDomain = provisionedAccountEmailDomain
58
+ ? provisionedAccountEmailDomain.toLowerCase().replace(/^@/, "")
59
+ : null;
60
+
61
+ const logger = rootLogger.child({ layer: "betterAuth" });
62
+
63
+ const getWaitlistInvitationCode = async (ctx?: { headers?: Headers | null } | null) => {
64
+ let code = ctx?.headers?.get("waitlist-invitation-code");
65
+ if (code) return code;
66
+ const oauthState = await getOAuthState();
67
+ if (oauthState) {
68
+ code = oauthState.waitlistInvitationCode;
69
+ }
70
+ return code;
71
+ };
72
+
73
+ const isProvisionedAccountEmail = (email: string) => {
74
+ if (!normalizedProvisionedAccountEmailDomain) return false;
75
+ return email.toLowerCase().endsWith(`@${normalizedProvisionedAccountEmailDomain}`);
76
+ };
77
+
78
+ return betterAuth({
79
+ ...options,
80
+ baseURL: process.env.VITE_SERVER_URL!,
81
+ session: {
82
+ expiresIn: 60 * 60 * 24 * 7,
83
+ updateAge: 60 * 60 * 24,
84
+ cookieCache: {
85
+ enabled: true,
86
+ maxAge: 60 * 5,
87
+ },
88
+ additionalFields: {
89
+ activeOrganizationRole: {
90
+ type: "string",
91
+ required: false,
92
+ defaultValue: null,
93
+ },
94
+ activeTeamRole: {
95
+ type: "string",
96
+ required: false,
97
+ defaultValue: null,
98
+ },
99
+ },
100
+ },
101
+ user: {
102
+ deleteUser: {
103
+ enabled: true,
104
+ sendDeleteAccountVerification: async ({ user, url }) => {
105
+ const result = await emailService?.sendDeleteAccountVerification(user.email, url);
106
+ if (result?.isErr()) {
107
+ logger.error(result.error);
108
+ hooks?.onError?.(result.error);
109
+ throw result.error;
110
+ }
111
+ },
112
+ },
113
+ additionalFields: {
114
+ onboarding: {
115
+ type: "number",
116
+ required: false,
117
+ defaultValue: null,
118
+ },
119
+ preferences: {
120
+ type: "string",
121
+ required: false,
122
+ defaultValue: null,
123
+ },
124
+ flags: {
125
+ type: "string",
126
+ required: false,
127
+ defaultValue: null,
128
+ },
129
+ stripeCustomerId: {
130
+ type: "string",
131
+ required: false,
132
+ defaultValue: null,
133
+ input: false,
134
+ },
135
+ paymentCustomerId: {
136
+ type: "string",
137
+ required: false,
138
+ defaultValue: null,
139
+ input: false,
140
+ },
141
+ paymentPlanTier: {
142
+ type: "string",
143
+ required: false,
144
+ defaultValue: null,
145
+ input: false,
146
+ },
147
+ paymentPlanExpiresAt: {
148
+ type: "number",
149
+ required: false,
150
+ defaultValue: null,
151
+ input: false,
152
+ },
153
+ },
154
+ },
155
+ database: drizzleAdapter(orm, {
156
+ provider: "sqlite",
157
+ schema,
158
+ usePlural: true,
159
+ }),
160
+ emailAndPassword: {
161
+ enabled: true,
162
+ requireEmailVerification: !waitlist,
163
+ sendResetPassword: async ({ user, url }) => {
164
+ const result = await emailService?.sendResetPassword(user.email, url);
165
+ if (result?.isErr()) {
166
+ logger.error(result.error);
167
+ hooks?.onError?.(result.error);
168
+ throw result.error;
169
+ }
170
+ },
171
+ },
172
+ emailVerification: {
173
+ sendVerificationEmail: async ({ user, url }) => {
174
+ const result = await emailService?.sendVerification(user.email, url);
175
+ if (result?.isErr()) {
176
+ logger.error(result.error);
177
+ hooks?.onError?.(result.error);
178
+ throw result.error;
179
+ }
180
+ },
181
+ },
182
+ plugins: [
183
+ ...(options?.plugins ?? []),
184
+ magicLink({
185
+ disableSignUp: true,
186
+ sendMagicLink: async ({ email, url, token }) => {
187
+ const [user] = await orm
188
+ .select({ id: schema.users.id })
189
+ .from(schema.users)
190
+ .where(eq(schema.users.email, email.toLowerCase()))
191
+ .limit(1);
192
+
193
+ if (!user) return;
194
+
195
+ const [claim] = await orm
196
+ .select({ id: schema.waitlist.id })
197
+ .from(schema.waitlist)
198
+ .where(
199
+ and(
200
+ eq(schema.waitlist.type, "ACCOUNT_CLAIM"),
201
+ eq(schema.waitlist.claimUserId, user.id),
202
+ eq(schema.waitlist.status, "INVITED"),
203
+ gte(schema.waitlist.expiresAt, new Date())
204
+ )
205
+ )
206
+ .orderBy(desc(schema.waitlist.createdAt))
207
+ .limit(1);
208
+ if (claim) {
209
+ await orm.insert(schema.accountClaimMagicLinks).values({
210
+ claimId: claim.id,
211
+ userId: user.id,
212
+ email: email.toLowerCase(),
213
+ token,
214
+ url,
215
+ expiresAt: new Date(Date.now() + 1000 * 60 * 5),
216
+ });
217
+ }
218
+ },
219
+ }),
220
+ admin(),
221
+ lastLoginMethod(),
222
+ organization({
223
+ allowUserToCreateOrganization: false,
224
+ teams: {
225
+ enabled: true,
226
+ allowRemovingAllTeams: false,
227
+ },
228
+ sendInvitationEmail: async (data) => {
229
+ const invitationUrl = `${process.env.VITE_APP_URL}/organization/accept-invitation?id=${data.id}`;
230
+ const inviterName = data.inviter.user.name || data.inviter.user.email;
231
+ const result = await emailService?.sendOrganizationInvite(
232
+ data.email,
233
+ data.organization.name,
234
+ inviterName,
235
+ data.role,
236
+ invitationUrl
237
+ );
238
+ if (result?.isErr()) {
239
+ logger.error(result.error);
240
+ hooks?.onError?.(result.error);
241
+ throw result.error;
242
+ }
243
+ },
244
+ schema: {
245
+ team: {
246
+ modelName: "team",
247
+ },
248
+ teamMember: {
249
+ modelName: "teamMember",
250
+ additionalFields: {
251
+ role: {
252
+ type: "string",
253
+ required: true,
254
+ },
255
+ },
256
+ },
257
+ member: {
258
+ modelName: "member",
259
+ },
260
+ invitation: {
261
+ modelName: "invitation",
262
+ },
263
+ organization: {
264
+ modelName: "organization",
265
+ },
266
+ },
267
+ }),
268
+ apiKey(),
269
+ ],
270
+ trustedOrigins: [process.env.VITE_APP_URL!, process.env.VITE_SERVER_URL!],
271
+
272
+ databaseHooks: {
273
+ user: {
274
+ create: {
275
+ before: async (_user, ctx) => {
276
+ if (waitlist) {
277
+ const waitlistCode = await getWaitlistInvitationCode(ctx);
278
+ if (waitlistCode) {
279
+ const [waitlistInvitation] = await orm
280
+ .select()
281
+ .from(schema.waitlist)
282
+ .where(
283
+ and(
284
+ eq(schema.waitlist.code, waitlistCode),
285
+ eq(schema.waitlist.type, "WAITLIST"),
286
+ eq(schema.waitlist.status, "INVITED"),
287
+ gte(schema.waitlist.expiresAt, new Date())
288
+ )
289
+ )
290
+ .limit(1);
291
+
292
+ if (!waitlistInvitation) {
293
+ return false;
294
+ }
295
+ await orm
296
+ .update(schema.waitlist)
297
+ .set({
298
+ status: "ACCEPTED",
299
+ updatedAt: new Date(),
300
+ })
301
+ .where(eq(schema.waitlist.id, waitlistInvitation.id));
302
+ return;
303
+ }
304
+ }
305
+
306
+ if (waitlist) {
307
+ return false;
308
+ }
309
+ return;
310
+ },
311
+ after: async (user) => {
312
+ await createOrganizationAndTeam(orm, schema, user);
313
+ if (!isProvisionedAccountEmail(user.email)) {
314
+ await billingService?.createUserHook({ user });
315
+ }
316
+ posthogCapture({
317
+ distinctId: user.id,
318
+ event: "user_created",
319
+ properties: {
320
+ email: user.email,
321
+ name: user.name,
322
+ role: user.role,
323
+ image: user.image,
324
+ },
325
+ });
326
+ if (hooks?.afterCreateUser) await hooks.afterCreateUser(user);
327
+ },
328
+ },
329
+ },
330
+ session: {
331
+ create: {
332
+ before: async (session) => {
333
+ const { organizationId, teamId, organizationRole, teamRole } =
334
+ await getActiveOrganizationAndTeam(orm, schema, session.userId);
335
+ return {
336
+ data: {
337
+ ...session,
338
+ activeOrganizationId: organizationId,
339
+ activeTeamId: teamId,
340
+ activeOrganizationRole: organizationRole,
341
+ activeTeamRole: teamRole,
342
+ },
343
+ };
344
+ },
345
+ },
346
+ },
347
+ },
348
+ });
349
+ }