create-m5kdev 0.33.0 → 0.34.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 (147) hide show
  1. package/dist/src/catalog.cjs +21 -1
  2. package/dist/src/catalog.cjs.map +1 -1
  3. package/dist/src/catalog.d.cts +3 -1
  4. package/dist/src/create.cjs +6 -2
  5. package/dist/src/create.cjs.map +1 -1
  6. package/dist/src/doctor.cjs.map +1 -1
  7. package/dist/src/fs.cjs +10 -5
  8. package/dist/src/fs.cjs.map +1 -1
  9. package/dist/src/prompts.cjs +32 -0
  10. package/dist/src/prompts.cjs.map +1 -1
  11. package/dist/src/prompts.d.cts +7 -1
  12. package/dist/src/template.cjs +23 -4
  13. package/dist/src/template.cjs.map +1 -1
  14. package/dist/src/template.d.cts +17 -3
  15. package/dist/src/types.d.cts +11 -4
  16. package/package.json +4 -1
  17. package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +1 -1
  18. package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +3 -3
  19. package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +3 -2
  20. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +1 -1
  21. package/templates/minimal-app/.dockerignore.tpl +31 -0
  22. package/templates/minimal-app/.gitignore.tpl +1 -0
  23. package/templates/minimal-app/AGENTS.md.tpl +13 -10
  24. package/templates/minimal-app/CLAUDE.md.tpl +132 -131
  25. package/templates/minimal-app/README.md.tpl +50 -19
  26. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -23
  27. package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +51 -39
  28. package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -38
  29. package/templates/minimal-app/apps/e2e/servers.ts.tpl +136 -112
  30. package/templates/minimal-app/apps/e2e/tests/conversation.spec.ts.tpl +22 -0
  31. package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -47
  32. package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -73
  33. package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -517
  34. package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -94
  35. package/templates/minimal-app/apps/e2e/tests/files.spec.ts.tpl +35 -0
  36. package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -289
  37. package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -516
  38. package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -107
  39. package/templates/minimal-app/apps/e2e/tests/workflow.spec.ts.tpl +26 -0
  40. package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -10
  41. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  42. package/templates/minimal-app/apps/email/src/emails/trialEndingEmail.tsx.tpl +40 -0
  43. package/templates/minimal-app/apps/email/src/index.ts.tpl +7 -0
  44. package/templates/minimal-app/apps/email/src/resources.ts.tpl +72 -65
  45. package/templates/minimal-app/apps/expo/app.json.tpl +36 -36
  46. package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -8
  47. package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -16
  48. package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -31
  49. package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -151
  50. package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -53
  51. package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -8
  52. package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -71
  53. package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -19
  54. package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -85
  55. package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -40
  56. package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -75
  57. package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -193
  58. package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -34
  59. package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -37
  60. package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -10
  61. package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -22
  62. package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -9
  63. package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -11
  64. package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -20
  65. package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -11
  66. package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -1
  67. package/templates/minimal-app/apps/landing/.env.production.example.tpl +7 -0
  68. package/templates/minimal-app/apps/landing/Dockerfile.tpl +39 -0
  69. package/templates/minimal-app/apps/landing/fly.toml.tpl +20 -0
  70. package/templates/minimal-app/apps/landing/index.html.tpl +35 -0
  71. package/templates/minimal-app/apps/landing/package.json.tpl +34 -0
  72. package/templates/minimal-app/apps/landing/scripts/fly-deploy.mjs.tpl +75 -0
  73. package/templates/minimal-app/apps/landing/scripts/fly-secrets.mjs.tpl +31 -0
  74. package/templates/minimal-app/apps/landing/src/App.tsx.tpl +12 -0
  75. package/templates/minimal-app/apps/landing/src/LandingPage.tsx.tpl +37 -0
  76. package/templates/minimal-app/apps/landing/src/index.css.tpl +24 -0
  77. package/templates/minimal-app/apps/landing/src/main.tsx.tpl +16 -0
  78. package/templates/minimal-app/apps/landing/src/vite-env.d.ts.tpl +11 -0
  79. package/templates/minimal-app/apps/landing/tsconfig.json.tpl +12 -0
  80. package/templates/minimal-app/apps/landing/vite.config.ts.tpl +25 -0
  81. package/templates/minimal-app/apps/server/AGENTS.md.tpl +54 -46
  82. package/templates/minimal-app/apps/server/{drizzle/seed.e2e.ts.tpl → db.e2e.ts.tpl} +57 -40
  83. package/templates/minimal-app/apps/server/{drizzle/seed.ts.tpl → db.ts.tpl} +27 -20
  84. package/templates/minimal-app/apps/server/drizzle/0003_silly_the_liberteens.sql +27 -0
  85. package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +56 -166
  86. package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +1796 -1910
  87. package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +1818 -1932
  88. package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +2137 -0
  89. package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +8 -1
  90. package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +3 -33
  91. package/templates/minimal-app/apps/server/package.json.tpl +5 -3
  92. package/templates/minimal-app/apps/server/src/app.ts.tpl +36 -0
  93. package/templates/minimal-app/apps/server/src/modules/conversation/conversation.mastra.ts.tpl +26 -0
  94. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.module.ts.tpl +47 -0
  95. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl +43 -0
  96. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.trpc.ts.tpl +16 -0
  97. package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +28 -28
  98. package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +28 -28
  99. package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +54 -54
  100. package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +67 -67
  101. package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +116 -116
  102. package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +37 -37
  103. package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +279 -279
  104. package/templates/minimal-app/apps/server/src/schema.ts.tpl +31 -20
  105. package/templates/minimal-app/apps/server/src/types.ts.tpl +12 -12
  106. package/templates/minimal-app/apps/server/tsconfig.json.tpl +1 -1
  107. package/templates/minimal-app/apps/server/tsup.config.ts.tpl +23 -9
  108. package/templates/minimal-app/apps/shared/.env.example.tpl +5 -0
  109. package/templates/minimal-app/apps/shared/.env.production.example.tpl +23 -0
  110. package/templates/minimal-app/apps/shared/.env.tpl +5 -0
  111. package/templates/minimal-app/apps/shared/Dockerfile.tpl +48 -0
  112. package/templates/minimal-app/apps/shared/fly.toml.tpl +24 -0
  113. package/templates/minimal-app/apps/shared/package.json.tpl +25 -24
  114. package/templates/minimal-app/apps/shared/scripts/fly-deploy.mjs.tpl +75 -0
  115. package/templates/minimal-app/apps/shared/scripts/fly-secrets.mjs.tpl +31 -0
  116. package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -28
  117. package/templates/minimal-app/apps/shared/src/modules/conversation/conversation.constants.ts.tpl +1 -0
  118. package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
  119. package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
  120. package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -25
  121. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -53
  122. package/templates/minimal-app/apps/webapp/public/push-sw.js.tpl +34 -34
  123. package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +46 -44
  124. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +96 -65
  125. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +114 -93
  126. package/templates/minimal-app/apps/webapp/src/index.css.tpl +15 -15
  127. package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -26
  128. package/templates/minimal-app/apps/webapp/src/modules/conversation/ConversationRoute.tsx.tpl +36 -0
  129. package/templates/minimal-app/apps/webapp/src/modules/files/FilesRoute.tsx.tpl +88 -0
  130. package/templates/minimal-app/apps/webapp/src/modules/files/hooks/useFilesRoute.ts.tpl +50 -0
  131. package/templates/minimal-app/apps/webapp/src/modules/workflows/WorkflowsRoute.tsx.tpl +97 -0
  132. package/templates/minimal-app/apps/webapp/src/modules/workflows/hooks/useWorkflowsRoute.ts.tpl +64 -0
  133. package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -13
  134. package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +10 -10
  135. package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -1
  136. package/templates/minimal-app/apps/webapp/translations/en/app.json.tpl +7 -7
  137. package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +193 -135
  138. package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +17 -17
  139. package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -31
  140. package/templates/minimal-app/biome.json.tpl +17 -12
  141. package/templates/minimal-app/package.json.tpl +6 -2
  142. package/templates/minimal-app/pnpm-workspace.yaml.tpl +14 -7
  143. package/templates/minimal-app/template.manifest.json +92 -2
  144. package/templates/minimal-app/apps/server/drizzle/db.ts.tpl +0 -26
  145. package/templates/minimal-app/apps/server/drizzle/guard.ts.tpl +0 -36
  146. package/templates/minimal-app/apps/server/drizzle/reset.ts.tpl +0 -40
  147. package/templates/minimal-app/apps/server/drizzle/sync.ts.tpl +0 -16
@@ -1,279 +1,279 @@
1
- import {
2
- BaseModule,
3
- type ModuleExpressContext,
4
- type TableMap,
5
- } from "@m5kdev/backend/modules/base/base.module";
6
- import { and, desc, eq, isNotNull } from "drizzle-orm";
7
- import type { Request, Response } from "express";
8
- import { v4 as uuidv4 } from "uuid";
9
-
10
- type TestHarnessTables = TableMap;
11
-
12
- function normalizedEmail(value: unknown) {
13
- return typeof value === "string" ? value.trim().toLowerCase() : "";
14
- }
15
-
16
- function normalizedText(value: unknown, fallback: string) {
17
- return typeof value === "string" && value.trim() ? value.trim() : fallback;
18
- }
19
-
20
- function slugPart(value: string) {
21
- return value
22
- .trim()
23
- .toLowerCase()
24
- .replace(/[^a-z0-9]+/g, "-")
25
- .replace(/^-+|-+$/g, "")
26
- .slice(0, 48);
27
- }
28
-
29
- function sendJson(res: Response, status: number, body: unknown) {
30
- res
31
- .status(status)
32
- .type("application/json")
33
- .send(JSON.stringify(body, null, 2));
34
- }
35
-
36
- export class TestHarnessModule extends BaseModule<
37
- Record<string, never>,
38
- TestHarnessTables,
39
- Record<string, never>,
40
- Record<string, never>,
41
- never
42
- > {
43
- readonly id = "auth-e2e-test-harness";
44
- override readonly dbDependsOn = ["auth"] as const;
45
-
46
- override express({ db, infra }: ModuleExpressContext<Record<string, never>>) {
47
- infra.express.get("/__auth-e2e/user.json", async (req: Request, res: Response) => {
48
- const email = normalizedEmail(req.query.email);
49
- if (!email) {
50
- sendJson(res, 400, { error: "email is required" });
51
- return;
52
- }
53
-
54
- const [user] = await db.orm
55
- .select({
56
- id: db.schema.users.id,
57
- name: db.schema.users.name,
58
- email: db.schema.users.email,
59
- emailVerified: db.schema.users.emailVerified,
60
- role: db.schema.users.role,
61
- banned: db.schema.users.banned,
62
- })
63
- .from(db.schema.users)
64
- .where(eq(db.schema.users.email, email))
65
- .limit(1);
66
-
67
- if (!user) {
68
- sendJson(res, 404, { error: "user not found" });
69
- return;
70
- }
71
-
72
- const organizations = await db.orm
73
- .select({
74
- id: db.schema.organizations.id,
75
- name: db.schema.organizations.name,
76
- slug: db.schema.organizations.slug,
77
- type: db.schema.organizations.type,
78
- parentId: db.schema.organizations.parentId,
79
- role: db.schema.members.role,
80
- })
81
- .from(db.schema.members)
82
- .innerJoin(
83
- db.schema.organizations,
84
- eq(db.schema.members.organizationId, db.schema.organizations.id)
85
- )
86
- .where(eq(db.schema.members.userId, user.id));
87
-
88
- const [credentialAccount] = await db.orm
89
- .select({ id: db.schema.accounts.id })
90
- .from(db.schema.accounts)
91
- .where(
92
- and(
93
- eq(db.schema.accounts.userId, user.id),
94
- eq(db.schema.accounts.providerId, "credential"),
95
- isNotNull(db.schema.accounts.password)
96
- )
97
- )
98
- .limit(1);
99
-
100
- const accountClaims = await db.orm
101
- .select({
102
- id: db.schema.waitlist.id,
103
- status: db.schema.waitlist.status,
104
- claimUserId: db.schema.waitlist.claimUserId,
105
- claimedEmail: db.schema.waitlist.claimedEmail,
106
- claimedAt: db.schema.waitlist.claimedAt,
107
- expiresAt: db.schema.waitlist.expiresAt,
108
- })
109
- .from(db.schema.waitlist)
110
- .where(
111
- and(
112
- eq(db.schema.waitlist.type, "ACCOUNT_CLAIM"),
113
- eq(db.schema.waitlist.claimUserId, user.id)
114
- )
115
- )
116
- .orderBy(desc(db.schema.waitlist.createdAt));
117
-
118
- const [latestSession] = await db.orm
119
- .select({
120
- id: db.schema.sessions.id,
121
- userId: db.schema.sessions.userId,
122
- activeOrganizationId: db.schema.sessions.activeOrganizationId,
123
- activeOrganizationRole: db.schema.sessions.activeOrganizationRole,
124
- activeOrganizationMemberId: db.schema.sessions.activeOrganizationMemberId,
125
- activeOrganizationType: db.schema.sessions.activeOrganizationType,
126
- activeTeamId: db.schema.sessions.activeTeamId,
127
- impersonatedBy: db.schema.sessions.impersonatedBy,
128
- createdAt: db.schema.sessions.createdAt,
129
- })
130
- .from(db.schema.sessions)
131
- .where(eq(db.schema.sessions.userId, user.id))
132
- .orderBy(desc(db.schema.sessions.createdAt))
133
- .limit(1);
134
-
135
- sendJson(res, 200, {
136
- user,
137
- organizations,
138
- credentialAccount: Boolean(credentialAccount),
139
- accountClaims,
140
- pendingAccountClaims: accountClaims.filter((claim) => claim.status === "INVITED").length,
141
- acceptedAccountClaims: accountClaims.filter((claim) => claim.status === "ACCEPTED").length,
142
- latestSession: latestSession ?? null,
143
- });
144
- });
145
-
146
- infra.express.post(
147
- "/__auth-e2e/provisioned-claim-user",
148
- async (req: Request, res: Response) => {
149
- const email = normalizedEmail((req.body as { email?: unknown }).email);
150
- const expiresInHours =
151
- typeof (req.body as { expiresInHours?: unknown }).expiresInHours === "number"
152
- ? (req.body as { expiresInHours: number }).expiresInHours
153
- : 24 * 14;
154
- const createClaim = (req.body as { createClaim?: unknown }).createClaim === true;
155
-
156
- if (!email.endsWith("@provisioned.auth-e2e.local")) {
157
- sendJson(res, 400, { error: "email must use the provisioned auth-e2e domain" });
158
- return;
159
- }
160
-
161
- const [existing] = await db.orm
162
- .select({ id: db.schema.users.id })
163
- .from(db.schema.users)
164
- .where(eq(db.schema.users.email, email))
165
- .limit(1);
166
-
167
- if (existing) {
168
- sendJson(res, 409, { error: "user already exists" });
169
- return;
170
- }
171
-
172
- const now = new Date();
173
- const userId = uuidv4();
174
- const organizationId = uuidv4();
175
- const teamId = uuidv4();
176
- const claimId = createClaim ? uuidv4() : null;
177
-
178
- await db.orm.transaction(async (tx) => {
179
- await tx.insert(db.schema.users).values({
180
- id: userId,
181
- name: `Provisioned ${email}`,
182
- email,
183
- emailVerified: false,
184
- role: "user",
185
- createdAt: now,
186
- updatedAt: now,
187
- });
188
-
189
- await tx.insert(db.schema.organizations).values({
190
- id: organizationId,
191
- name: `Provisioned ${userId.slice(0, 8)}`,
192
- slug: `provisioned-${userId.slice(0, 8)}`,
193
- type: "organization",
194
- createdAt: now,
195
- });
196
-
197
- await tx.insert(db.schema.members).values({
198
- id: uuidv4(),
199
- organizationId,
200
- userId,
201
- role: "owner",
202
- createdAt: now,
203
- });
204
-
205
- await tx.insert(db.schema.teams).values({
206
- id: teamId,
207
- name: "Editorial",
208
- organizationId,
209
- createdAt: now,
210
- updatedAt: now,
211
- });
212
-
213
- await tx.insert(db.schema.teamMembers).values({
214
- id: uuidv4(),
215
- teamId,
216
- userId,
217
- role: "owner",
218
- createdAt: now,
219
- });
220
-
221
- if (claimId) {
222
- await tx.insert(db.schema.waitlist).values({
223
- id: claimId,
224
- type: "ACCOUNT_CLAIM",
225
- status: "INVITED",
226
- claimUserId: userId,
227
- code: uuidv4(),
228
- expiresAt: new Date(Date.now() + expiresInHours * 60 * 60 * 1000),
229
- createdAt: now,
230
- updatedAt: now,
231
- });
232
- }
233
- });
234
-
235
- sendJson(res, 201, {
236
- user: { id: userId, email },
237
- organization: { id: organizationId },
238
- claim: claimId ? { id: claimId } : null,
239
- });
240
- }
241
- );
242
-
243
- infra.express.post("/__auth-e2e/organizations", async (req: Request, res: Response) => {
244
- const body = req.body as { count?: unknown; prefix?: unknown };
245
- const count = typeof body.count === "number" ? Math.trunc(body.count) : 12;
246
- const prefix = normalizedText(body.prefix, "Harness Organization");
247
-
248
- if (count < 1 || count > 50) {
249
- sendJson(res, 400, { error: "count must be between 1 and 50" });
250
- return;
251
- }
252
-
253
- const now = Date.now();
254
- const slugPrefix = slugPart(prefix) || "organization";
255
- const organizations = Array.from({ length: count }, (_, index) => {
256
- const id = uuidv4();
257
- const displayIndex = String(index + 1).padStart(2, "0");
258
-
259
- return {
260
- id,
261
- name: `${prefix} ${displayIndex}`,
262
- slug: `${slugPrefix}-${displayIndex}-${id.slice(0, 8)}`,
263
- type: "organization",
264
- createdAt: new Date(now + index),
265
- };
266
- });
267
-
268
- await db.orm.insert(db.schema.organizations).values(organizations);
269
-
270
- sendJson(res, 201, {
271
- organizations: organizations.map((organization) => ({
272
- id: organization.id,
273
- name: organization.name,
274
- slug: organization.slug,
275
- })),
276
- });
277
- });
278
- }
279
- }
1
+ import {
2
+ BaseModule,
3
+ type ModuleExpressContext,
4
+ type TableMap,
5
+ } from "@m5kdev/backend/modules/base/base.module";
6
+ import { and, desc, eq, isNotNull } from "drizzle-orm";
7
+ import type { Request, Response } from "express";
8
+ import { v4 as uuidv4 } from "uuid";
9
+
10
+ type TestHarnessTables = TableMap;
11
+
12
+ function normalizedEmail(value: unknown) {
13
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
14
+ }
15
+
16
+ function normalizedText(value: unknown, fallback: string) {
17
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
18
+ }
19
+
20
+ function slugPart(value: string) {
21
+ return value
22
+ .trim()
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9]+/g, "-")
25
+ .replace(/^-+|-+$/g, "")
26
+ .slice(0, 48);
27
+ }
28
+
29
+ function sendJson(res: Response, status: number, body: unknown) {
30
+ res
31
+ .status(status)
32
+ .type("application/json")
33
+ .send(JSON.stringify(body, null, 2));
34
+ }
35
+
36
+ export class TestHarnessModule extends BaseModule<
37
+ Record<string, never>,
38
+ TestHarnessTables,
39
+ Record<string, never>,
40
+ Record<string, never>,
41
+ never
42
+ > {
43
+ readonly id = "auth-e2e-test-harness";
44
+ override readonly dbDependsOn = ["auth"] as const;
45
+
46
+ override express({ db, infra }: ModuleExpressContext<Record<string, never>>) {
47
+ infra.express.get("/__auth-e2e/user.json", async (req: Request, res: Response) => {
48
+ const email = normalizedEmail(req.query.email);
49
+ if (!email) {
50
+ sendJson(res, 400, { error: "email is required" });
51
+ return;
52
+ }
53
+
54
+ const [user] = await db.orm
55
+ .select({
56
+ id: db.schema.users.id,
57
+ name: db.schema.users.name,
58
+ email: db.schema.users.email,
59
+ emailVerified: db.schema.users.emailVerified,
60
+ role: db.schema.users.role,
61
+ banned: db.schema.users.banned,
62
+ })
63
+ .from(db.schema.users)
64
+ .where(eq(db.schema.users.email, email))
65
+ .limit(1);
66
+
67
+ if (!user) {
68
+ sendJson(res, 404, { error: "user not found" });
69
+ return;
70
+ }
71
+
72
+ const organizations = await db.orm
73
+ .select({
74
+ id: db.schema.organizations.id,
75
+ name: db.schema.organizations.name,
76
+ slug: db.schema.organizations.slug,
77
+ type: db.schema.organizations.type,
78
+ parentId: db.schema.organizations.parentId,
79
+ role: db.schema.members.role,
80
+ })
81
+ .from(db.schema.members)
82
+ .innerJoin(
83
+ db.schema.organizations,
84
+ eq(db.schema.members.organizationId, db.schema.organizations.id)
85
+ )
86
+ .where(eq(db.schema.members.userId, user.id));
87
+
88
+ const [credentialAccount] = await db.orm
89
+ .select({ id: db.schema.accounts.id })
90
+ .from(db.schema.accounts)
91
+ .where(
92
+ and(
93
+ eq(db.schema.accounts.userId, user.id),
94
+ eq(db.schema.accounts.providerId, "credential"),
95
+ isNotNull(db.schema.accounts.password)
96
+ )
97
+ )
98
+ .limit(1);
99
+
100
+ const accountClaims = await db.orm
101
+ .select({
102
+ id: db.schema.waitlist.id,
103
+ status: db.schema.waitlist.status,
104
+ claimUserId: db.schema.waitlist.claimUserId,
105
+ claimedEmail: db.schema.waitlist.claimedEmail,
106
+ claimedAt: db.schema.waitlist.claimedAt,
107
+ expiresAt: db.schema.waitlist.expiresAt,
108
+ })
109
+ .from(db.schema.waitlist)
110
+ .where(
111
+ and(
112
+ eq(db.schema.waitlist.type, "ACCOUNT_CLAIM"),
113
+ eq(db.schema.waitlist.claimUserId, user.id)
114
+ )
115
+ )
116
+ .orderBy(desc(db.schema.waitlist.createdAt));
117
+
118
+ const [latestSession] = await db.orm
119
+ .select({
120
+ id: db.schema.sessions.id,
121
+ userId: db.schema.sessions.userId,
122
+ activeOrganizationId: db.schema.sessions.activeOrganizationId,
123
+ activeOrganizationRole: db.schema.sessions.activeOrganizationRole,
124
+ activeOrganizationMemberId: db.schema.sessions.activeOrganizationMemberId,
125
+ activeOrganizationType: db.schema.sessions.activeOrganizationType,
126
+ activeTeamId: db.schema.sessions.activeTeamId,
127
+ impersonatedBy: db.schema.sessions.impersonatedBy,
128
+ createdAt: db.schema.sessions.createdAt,
129
+ })
130
+ .from(db.schema.sessions)
131
+ .where(eq(db.schema.sessions.userId, user.id))
132
+ .orderBy(desc(db.schema.sessions.createdAt))
133
+ .limit(1);
134
+
135
+ sendJson(res, 200, {
136
+ user,
137
+ organizations,
138
+ credentialAccount: Boolean(credentialAccount),
139
+ accountClaims,
140
+ pendingAccountClaims: accountClaims.filter((claim) => claim.status === "INVITED").length,
141
+ acceptedAccountClaims: accountClaims.filter((claim) => claim.status === "ACCEPTED").length,
142
+ latestSession: latestSession ?? null,
143
+ });
144
+ });
145
+
146
+ infra.express.post(
147
+ "/__auth-e2e/provisioned-claim-user",
148
+ async (req: Request, res: Response) => {
149
+ const email = normalizedEmail((req.body as { email?: unknown }).email);
150
+ const expiresInHours =
151
+ typeof (req.body as { expiresInHours?: unknown }).expiresInHours === "number"
152
+ ? (req.body as { expiresInHours: number }).expiresInHours
153
+ : 24 * 14;
154
+ const createClaim = (req.body as { createClaim?: unknown }).createClaim === true;
155
+
156
+ if (!email.endsWith("@provisioned.auth-e2e.local")) {
157
+ sendJson(res, 400, { error: "email must use the provisioned auth-e2e domain" });
158
+ return;
159
+ }
160
+
161
+ const [existing] = await db.orm
162
+ .select({ id: db.schema.users.id })
163
+ .from(db.schema.users)
164
+ .where(eq(db.schema.users.email, email))
165
+ .limit(1);
166
+
167
+ if (existing) {
168
+ sendJson(res, 409, { error: "user already exists" });
169
+ return;
170
+ }
171
+
172
+ const now = new Date();
173
+ const userId = uuidv4();
174
+ const organizationId = uuidv4();
175
+ const teamId = uuidv4();
176
+ const claimId = createClaim ? uuidv4() : null;
177
+
178
+ await db.orm.transaction(async (tx) => {
179
+ await tx.insert(db.schema.users).values({
180
+ id: userId,
181
+ name: `Provisioned ${email}`,
182
+ email,
183
+ emailVerified: false,
184
+ role: "user",
185
+ createdAt: now,
186
+ updatedAt: now,
187
+ });
188
+
189
+ await tx.insert(db.schema.organizations).values({
190
+ id: organizationId,
191
+ name: `Provisioned ${userId.slice(0, 8)}`,
192
+ slug: `provisioned-${userId.slice(0, 8)}`,
193
+ type: "organization",
194
+ createdAt: now,
195
+ });
196
+
197
+ await tx.insert(db.schema.members).values({
198
+ id: uuidv4(),
199
+ organizationId,
200
+ userId,
201
+ role: "owner",
202
+ createdAt: now,
203
+ });
204
+
205
+ await tx.insert(db.schema.teams).values({
206
+ id: teamId,
207
+ name: "Editorial",
208
+ organizationId,
209
+ createdAt: now,
210
+ updatedAt: now,
211
+ });
212
+
213
+ await tx.insert(db.schema.teamMembers).values({
214
+ id: uuidv4(),
215
+ teamId,
216
+ userId,
217
+ role: "owner",
218
+ createdAt: now,
219
+ });
220
+
221
+ if (claimId) {
222
+ await tx.insert(db.schema.waitlist).values({
223
+ id: claimId,
224
+ type: "ACCOUNT_CLAIM",
225
+ status: "INVITED",
226
+ claimUserId: userId,
227
+ code: uuidv4(),
228
+ expiresAt: new Date(Date.now() + expiresInHours * 60 * 60 * 1000),
229
+ createdAt: now,
230
+ updatedAt: now,
231
+ });
232
+ }
233
+ });
234
+
235
+ sendJson(res, 201, {
236
+ user: { id: userId, email },
237
+ organization: { id: organizationId },
238
+ claim: claimId ? { id: claimId } : null,
239
+ });
240
+ }
241
+ );
242
+
243
+ infra.express.post("/__auth-e2e/organizations", async (req: Request, res: Response) => {
244
+ const body = req.body as { count?: unknown; prefix?: unknown };
245
+ const count = typeof body.count === "number" ? Math.trunc(body.count) : 12;
246
+ const prefix = normalizedText(body.prefix, "Harness Organization");
247
+
248
+ if (count < 1 || count > 50) {
249
+ sendJson(res, 400, { error: "count must be between 1 and 50" });
250
+ return;
251
+ }
252
+
253
+ const now = Date.now();
254
+ const slugPrefix = slugPart(prefix) || "organization";
255
+ const organizations = Array.from({ length: count }, (_, index) => {
256
+ const id = uuidv4();
257
+ const displayIndex = String(index + 1).padStart(2, "0");
258
+
259
+ return {
260
+ id,
261
+ name: `${prefix} ${displayIndex}`,
262
+ slug: `${slugPrefix}-${displayIndex}-${id.slice(0, 8)}`,
263
+ type: "organization",
264
+ createdAt: new Date(now + index),
265
+ };
266
+ });
267
+
268
+ await db.orm.insert(db.schema.organizations).values(organizations);
269
+
270
+ sendJson(res, 201, {
271
+ organizations: organizations.map((organization) => ({
272
+ id: organization.id,
273
+ name: organization.name,
274
+ slug: organization.slug,
275
+ })),
276
+ });
277
+ });
278
+ }
279
+ }
@@ -1,20 +1,31 @@
1
- export {
2
- accountClaimMagicLinks,
3
- accounts,
4
- apikeys,
5
- invitations,
6
- members,
7
- organizations,
8
- sessions,
9
- teamMembers,
10
- teams,
11
- users,
12
- verifications,
13
- waitlist,
14
- } from "@m5kdev/backend/modules/auth/auth.db";
15
- export {
16
- notificationDevices,
17
- notificationSendLogs,
18
- } from "@m5kdev/backend/modules/notification/notification.db";
19
- export { workflows } from "@m5kdev/backend/modules/workflow/workflow.db";
20
- export { posts } from "./modules/posts/posts.db";
1
+ // biome-ignore-all assist/source/organizeImports: feature-gated table exports stay in marker blocks
2
+ export {
3
+ accountClaimMagicLinks,
4
+ accounts,
5
+ apikeys,
6
+ invitations,
7
+ members,
8
+ organizations,
9
+ sessions,
10
+ teamMembers,
11
+ teams,
12
+ users,
13
+ verifications,
14
+ waitlist,
15
+ } from "@m5kdev/backend/modules/auth/auth.db";
16
+ // m5k:ai:start
17
+ export { aiUsage, chats } from "@m5kdev/backend/modules/ai/ai.db";
18
+ // m5k:ai:end
19
+ // m5k:files:start
20
+ export { files } from "@m5kdev/backend/modules/file/file.db";
21
+ // m5k:files:end
22
+ // m5k:notifications:start
23
+ export {
24
+ notificationDevices,
25
+ notificationSendLogs,
26
+ } from "@m5kdev/backend/modules/notification/notification.db";
27
+ // m5k:notifications:end
28
+ // m5k:workflows:start
29
+ export { workflows } from "@m5kdev/backend/modules/workflow/workflow.db";
30
+ // m5k:workflows:end
31
+ export { posts } from "./modules/posts/posts.db";
@@ -1,12 +1,12 @@
1
- import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
2
- import type { builtBackendApp } from "./app";
3
-
4
- export type AppRouter = typeof builtBackendApp.trpc.router;
5
- export type RouterInputs = inferRouterInputs<AppRouter>;
6
- export type RouterOutputs = inferRouterOutputs<AppRouter>;
7
-
8
- export type Orm = typeof builtBackendApp.db.orm;
9
- export type Schema = typeof builtBackendApp.db.schema;
10
-
11
- export type Session = NonNullable<typeof builtBackendApp.auth>["instance"]["$Infer"]["Session"];
12
- export type User = Session["user"];
1
+ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
2
+ import type { builtBackendApp } from "./app";
3
+
4
+ export type AppRouter = typeof builtBackendApp.trpc.router;
5
+ export type RouterInputs = inferRouterInputs<AppRouter>;
6
+ export type RouterOutputs = inferRouterOutputs<AppRouter>;
7
+
8
+ export type Orm = typeof builtBackendApp.db.orm;
9
+ export type Schema = typeof builtBackendApp.db.schema;
10
+
11
+ export type Session = NonNullable<typeof builtBackendApp.auth>["instance"]["$Infer"]["Session"];
12
+ export type User = Session["user"];
@@ -9,5 +9,5 @@
9
9
  "rootDir": "."
10
10
  },
11
11
  "exclude": ["node_modules"],
12
- "include": ["src/**/*.ts", "drizzle/**/*.ts", "drizzle.config.ts", "tsup.config.ts"]
12
+ "include": ["src/**/*.ts", "db.ts", "db.e2e.ts", "drizzle/**/*.ts", "drizzle.config.ts", "tsup.config.ts"]
13
13
  }