@donkeylabs/cli 1.1.12 → 1.1.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/cli",
3
- "version": "1.1.12",
3
+ "version": "1.1.14",
4
4
  "type": "module",
5
5
  "description": "CLI for @donkeylabs/server - project scaffolding and code generation",
6
6
  "main": "./src/index.ts",
@@ -1,3 +1,44 @@
1
- DATABASE_URL=app.db
1
+ # =============================================================================
2
+ # DATABASE
3
+ # =============================================================================
4
+
5
+ # SQLite database path (relative to project root)
6
+ # Use ":memory:" for in-memory database during development
7
+ DATABASE_URL=":memory:"
8
+
9
+ # For production, use a file path:
10
+ # DATABASE_URL="./data/app.db"
11
+
12
+ # =============================================================================
13
+ # SERVER
14
+ # =============================================================================
15
+
16
+ # Port for the API server
2
17
  PORT=3000
18
+
19
+ # Node environment
3
20
  NODE_ENV=development
21
+
22
+ # =============================================================================
23
+ # AUTHENTICATION (if using auth plugin)
24
+ # =============================================================================
25
+
26
+ # JWT secret for signing tokens (generate with: openssl rand -base64 32)
27
+ # JWT_SECRET=your-secret-key-here
28
+
29
+ # =============================================================================
30
+ # EXTERNAL SERVICES (examples)
31
+ # =============================================================================
32
+
33
+ # Email service
34
+ # RESEND_API_KEY=re_xxxxxxxxxxxx
35
+
36
+ # Payment processing
37
+ # STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxx
38
+
39
+ # =============================================================================
40
+ # FEATURE FLAGS
41
+ # =============================================================================
42
+
43
+ # Enable debug logging
44
+ # DEBUG=true
@@ -1,3 +1,57 @@
1
- PORT=3000
2
- DATABASE_URL=app.db
1
+ # =============================================================================
2
+ # DATABASE
3
+ # =============================================================================
4
+
5
+ # SQLite database path (relative to project root)
6
+ # Use ":memory:" for in-memory database during development
7
+ DATABASE_URL=":memory:"
8
+
9
+ # For production, use a file path:
10
+ # DATABASE_URL="./data/app.db"
11
+
12
+ # =============================================================================
13
+ # SERVER
14
+ # =============================================================================
15
+
16
+ # Port for the API server (optional, defaults to 3000)
17
+ # PORT=3000
18
+
19
+ # Node environment
3
20
  NODE_ENV=development
21
+
22
+ # =============================================================================
23
+ # AUTHENTICATION (if using auth plugin)
24
+ # =============================================================================
25
+
26
+ # JWT secret for signing tokens (generate with: openssl rand -base64 32)
27
+ # JWT_SECRET=your-secret-key-here
28
+
29
+ # Session duration in seconds (default: 7 days)
30
+ # SESSION_DURATION=604800
31
+
32
+ # =============================================================================
33
+ # EXTERNAL SERVICES (examples)
34
+ # =============================================================================
35
+
36
+ # Email service (e.g., Resend, SendGrid)
37
+ # RESEND_API_KEY=re_xxxxxxxxxxxx
38
+
39
+ # Payment processing (e.g., Stripe)
40
+ # STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxx
41
+ # STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx
42
+
43
+ # File storage (e.g., S3, Cloudflare R2)
44
+ # S3_BUCKET=my-bucket
45
+ # S3_REGION=us-east-1
46
+ # S3_ACCESS_KEY=xxxxxxxxxxxx
47
+ # S3_SECRET_KEY=xxxxxxxxxxxx
48
+
49
+ # =============================================================================
50
+ # FEATURE FLAGS
51
+ # =============================================================================
52
+
53
+ # Enable debug logging
54
+ # DEBUG=true
55
+
56
+ # Enable SSE real-time updates
57
+ # ENABLE_SSE=true
@@ -5,8 +5,10 @@ import { BunSqliteDialect } from "kysely-bun-sqlite";
5
5
  import { Database } from "bun:sqlite";
6
6
  import { demoPlugin } from "./plugins/demo";
7
7
  import { workflowDemoPlugin } from "./plugins/workflow-demo";
8
+ import { authPlugin } from "./plugins/auth";
8
9
  import demoRoutes from "./routes/demo";
9
10
  import { exampleRouter } from "./routes/example";
11
+ import { authRouter } from "./routes/auth";
10
12
 
11
13
  // Simple in-memory database
12
14
  const db = new Kysely<{}>({
@@ -23,10 +25,12 @@ export const server = new AppServer({
23
25
  });
24
26
 
25
27
  // Register plugins
28
+ server.registerPlugin(authPlugin); // Auth first - other plugins may depend on it
26
29
  server.registerPlugin(demoPlugin);
27
30
  server.registerPlugin(workflowDemoPlugin);
28
31
 
29
32
  // Register routes
33
+ server.use(authRouter);
30
34
  server.use(demoRoutes);
31
35
  server.use(exampleRouter);
32
36
 
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Auth Plugin - User authentication with sessions
3
+ *
4
+ * Provides:
5
+ * - User registration and login
6
+ * - Password hashing with bcrypt
7
+ * - Session-based authentication
8
+ * - Auth middleware for protected routes
9
+ */
10
+
11
+ import { createPlugin, createMiddleware } from "@donkeylabs/server";
12
+ import type { ColumnType } from "kysely";
13
+
14
+ // =============================================================================
15
+ // DATABASE SCHEMA TYPES
16
+ // =============================================================================
17
+
18
+ interface UsersTable {
19
+ id: string;
20
+ email: string;
21
+ password_hash: string;
22
+ name: string | null;
23
+ created_at: ColumnType<string, string | undefined, never>;
24
+ updated_at: string;
25
+ }
26
+
27
+ interface SessionsTable {
28
+ id: string;
29
+ user_id: string;
30
+ expires_at: string;
31
+ created_at: ColumnType<string, string | undefined, never>;
32
+ }
33
+
34
+ interface AuthSchema {
35
+ users: UsersTable;
36
+ sessions: SessionsTable;
37
+ }
38
+
39
+ // =============================================================================
40
+ // TYPES
41
+ // =============================================================================
42
+
43
+ export interface AuthUser {
44
+ id: string;
45
+ email: string;
46
+ name: string | null;
47
+ }
48
+
49
+ export interface Session {
50
+ id: string;
51
+ userId: string;
52
+ expiresAt: Date;
53
+ }
54
+
55
+ // =============================================================================
56
+ // PLUGIN DEFINITION
57
+ // =============================================================================
58
+
59
+ export const authPlugin = createPlugin
60
+ .withSchema<AuthSchema>()
61
+ .define({
62
+ name: "auth",
63
+
64
+ // Custom errors for auth failures
65
+ customErrors: {
66
+ InvalidCredentials: {
67
+ status: 401,
68
+ code: "INVALID_CREDENTIALS",
69
+ message: "Invalid email or password",
70
+ },
71
+ EmailAlreadyExists: {
72
+ status: 409,
73
+ code: "EMAIL_EXISTS",
74
+ message: "An account with this email already exists",
75
+ },
76
+ SessionExpired: {
77
+ status: 401,
78
+ code: "SESSION_EXPIRED",
79
+ message: "Your session has expired. Please log in again.",
80
+ },
81
+ },
82
+
83
+ service: async (ctx) => {
84
+ const SESSION_DURATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
85
+
86
+ return {
87
+ /**
88
+ * Register a new user
89
+ */
90
+ register: async (data: {
91
+ email: string;
92
+ password: string;
93
+ name?: string;
94
+ }): Promise<{ user: AuthUser; sessionId: string }> => {
95
+ const { email, password, name } = data;
96
+
97
+ // Check if email already exists
98
+ const existing = await ctx.db
99
+ .selectFrom("users")
100
+ .where("email", "=", email.toLowerCase())
101
+ .selectAll()
102
+ .executeTakeFirst();
103
+
104
+ if (existing) {
105
+ throw ctx.errors.EmailAlreadyExists();
106
+ }
107
+
108
+ // Hash password
109
+ const passwordHash = await Bun.password.hash(password, {
110
+ algorithm: "bcrypt",
111
+ cost: 10,
112
+ });
113
+
114
+ const userId = crypto.randomUUID();
115
+ const now = new Date().toISOString();
116
+
117
+ // Create user
118
+ await ctx.db
119
+ .insertInto("users")
120
+ .values({
121
+ id: userId,
122
+ email: email.toLowerCase(),
123
+ password_hash: passwordHash,
124
+ name: name || null,
125
+ created_at: now,
126
+ updated_at: now,
127
+ })
128
+ .execute();
129
+
130
+ // Create session
131
+ const sessionId = crypto.randomUUID();
132
+ const expiresAt = new Date(Date.now() + SESSION_DURATION_MS);
133
+
134
+ await ctx.db
135
+ .insertInto("sessions")
136
+ .values({
137
+ id: sessionId,
138
+ user_id: userId,
139
+ expires_at: expiresAt.toISOString(),
140
+ created_at: now,
141
+ })
142
+ .execute();
143
+
144
+ ctx.core.logger.info("User registered", { userId, email });
145
+
146
+ return {
147
+ user: { id: userId, email: email.toLowerCase(), name: name || null },
148
+ sessionId,
149
+ };
150
+ },
151
+
152
+ /**
153
+ * Login with email and password
154
+ */
155
+ login: async (data: {
156
+ email: string;
157
+ password: string;
158
+ }): Promise<{ user: AuthUser; sessionId: string }> => {
159
+ const { email, password } = data;
160
+
161
+ // Find user
162
+ const user = await ctx.db
163
+ .selectFrom("users")
164
+ .where("email", "=", email.toLowerCase())
165
+ .selectAll()
166
+ .executeTakeFirst();
167
+
168
+ if (!user) {
169
+ throw ctx.errors.InvalidCredentials();
170
+ }
171
+
172
+ // Verify password
173
+ const valid = await Bun.password.verify(password, user.password_hash);
174
+ if (!valid) {
175
+ throw ctx.errors.InvalidCredentials();
176
+ }
177
+
178
+ // Create session
179
+ const sessionId = crypto.randomUUID();
180
+ const expiresAt = new Date(Date.now() + SESSION_DURATION_MS);
181
+
182
+ await ctx.db
183
+ .insertInto("sessions")
184
+ .values({
185
+ id: sessionId,
186
+ user_id: user.id,
187
+ expires_at: expiresAt.toISOString(),
188
+ created_at: new Date().toISOString(),
189
+ })
190
+ .execute();
191
+
192
+ ctx.core.logger.info("User logged in", { userId: user.id });
193
+
194
+ return {
195
+ user: { id: user.id, email: user.email, name: user.name },
196
+ sessionId,
197
+ };
198
+ },
199
+
200
+ /**
201
+ * Logout - invalidate session
202
+ */
203
+ logout: async (sessionId: string): Promise<void> => {
204
+ await ctx.db
205
+ .deleteFrom("sessions")
206
+ .where("id", "=", sessionId)
207
+ .execute();
208
+ },
209
+
210
+ /**
211
+ * Validate session and get user
212
+ */
213
+ validateSession: async (sessionId: string): Promise<AuthUser | null> => {
214
+ const session = await ctx.db
215
+ .selectFrom("sessions")
216
+ .where("id", "=", sessionId)
217
+ .selectAll()
218
+ .executeTakeFirst();
219
+
220
+ if (!session) {
221
+ return null;
222
+ }
223
+
224
+ // Check if expired
225
+ if (new Date(session.expires_at) < new Date()) {
226
+ // Clean up expired session
227
+ await ctx.db
228
+ .deleteFrom("sessions")
229
+ .where("id", "=", sessionId)
230
+ .execute();
231
+ return null;
232
+ }
233
+
234
+ // Get user
235
+ const user = await ctx.db
236
+ .selectFrom("users")
237
+ .where("id", "=", session.user_id)
238
+ .selectAll()
239
+ .executeTakeFirst();
240
+
241
+ if (!user) {
242
+ return null;
243
+ }
244
+
245
+ return {
246
+ id: user.id,
247
+ email: user.email,
248
+ name: user.name,
249
+ };
250
+ },
251
+
252
+ /**
253
+ * Get user by ID
254
+ */
255
+ getUserById: async (userId: string): Promise<AuthUser | null> => {
256
+ const user = await ctx.db
257
+ .selectFrom("users")
258
+ .where("id", "=", userId)
259
+ .selectAll()
260
+ .executeTakeFirst();
261
+
262
+ if (!user) {
263
+ return null;
264
+ }
265
+
266
+ return {
267
+ id: user.id,
268
+ email: user.email,
269
+ name: user.name,
270
+ };
271
+ },
272
+
273
+ /**
274
+ * Update user profile
275
+ */
276
+ updateProfile: async (
277
+ userId: string,
278
+ data: { name?: string; email?: string }
279
+ ): Promise<AuthUser> => {
280
+ const updates: Record<string, string> = {
281
+ updated_at: new Date().toISOString(),
282
+ };
283
+
284
+ if (data.name !== undefined) {
285
+ updates.name = data.name;
286
+ }
287
+
288
+ if (data.email !== undefined) {
289
+ // Check if email is taken by another user
290
+ const existing = await ctx.db
291
+ .selectFrom("users")
292
+ .where("email", "=", data.email.toLowerCase())
293
+ .where("id", "!=", userId)
294
+ .selectAll()
295
+ .executeTakeFirst();
296
+
297
+ if (existing) {
298
+ throw ctx.errors.EmailAlreadyExists();
299
+ }
300
+
301
+ updates.email = data.email.toLowerCase();
302
+ }
303
+
304
+ await ctx.db
305
+ .updateTable("users")
306
+ .set(updates)
307
+ .where("id", "=", userId)
308
+ .execute();
309
+
310
+ const user = await ctx.db
311
+ .selectFrom("users")
312
+ .where("id", "=", userId)
313
+ .selectAll()
314
+ .executeTakeFirstOrThrow();
315
+
316
+ return {
317
+ id: user.id,
318
+ email: user.email,
319
+ name: user.name,
320
+ };
321
+ },
322
+
323
+ /**
324
+ * Delete all expired sessions (cleanup job)
325
+ */
326
+ cleanupExpiredSessions: async (): Promise<number> => {
327
+ const result = await ctx.db
328
+ .deleteFrom("sessions")
329
+ .where("expires_at", "<", new Date().toISOString())
330
+ .executeTakeFirst();
331
+
332
+ const count = Number(result.numDeletedRows);
333
+ if (count > 0) {
334
+ ctx.core.logger.info("Cleaned up expired sessions", { count });
335
+ }
336
+ return count;
337
+ },
338
+ };
339
+ },
340
+
341
+ /**
342
+ * Auth middleware - validates session from cookie/header
343
+ */
344
+ middleware: (ctx, service) => ({
345
+ /**
346
+ * Require authentication middleware
347
+ * Sets ctx.user if valid session found
348
+ * Returns 401 if required and no valid session
349
+ */
350
+ auth: createMiddleware<{ required?: boolean }>(
351
+ async (req, reqCtx, next, config) => {
352
+ // Get session ID from cookie or Authorization header
353
+ const cookies = req.headers.get("cookie") || "";
354
+ const cookieMatch = cookies.match(/session=([^;]+)/);
355
+ const headerToken = req.headers.get("authorization")?.replace("Bearer ", "");
356
+
357
+ const sessionId = cookieMatch?.[1] || headerToken;
358
+
359
+ if (sessionId) {
360
+ const user = await service.validateSession(sessionId);
361
+ if (user) {
362
+ // Set user on request context
363
+ (reqCtx as any).user = user;
364
+ (reqCtx as any).sessionId = sessionId;
365
+ }
366
+ }
367
+
368
+ // If auth is required and no user, return 401
369
+ if (config?.required && !(reqCtx as any).user) {
370
+ return Response.json(
371
+ { error: "Unauthorized", code: "UNAUTHORIZED" },
372
+ { status: 401 }
373
+ );
374
+ }
375
+
376
+ return next();
377
+ }
378
+ ),
379
+ }),
380
+
381
+ /**
382
+ * Initialize cleanup cron job
383
+ */
384
+ init: async (ctx, service) => {
385
+ // Clean up expired sessions daily at 3am
386
+ ctx.core.cron.schedule(
387
+ "0 3 * * *",
388
+ async () => {
389
+ await service.cleanupExpiredSessions();
390
+ },
391
+ { name: "auth-session-cleanup" }
392
+ );
393
+
394
+ ctx.core.logger.info("Auth plugin initialized");
395
+ },
396
+ });
@@ -0,0 +1,25 @@
1
+ import type { Kysely } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ await db.schema
5
+ .createTable("users")
6
+ .ifNotExists()
7
+ .addColumn("id", "text", (col) => col.primaryKey())
8
+ .addColumn("email", "text", (col) => col.notNull().unique())
9
+ .addColumn("password_hash", "text", (col) => col.notNull())
10
+ .addColumn("name", "text")
11
+ .addColumn("created_at", "text", (col) => col.notNull().defaultTo("CURRENT_TIMESTAMP"))
12
+ .addColumn("updated_at", "text", (col) => col.notNull())
13
+ .execute();
14
+
15
+ await db.schema
16
+ .createIndex("idx_users_email")
17
+ .ifNotExists()
18
+ .on("users")
19
+ .column("email")
20
+ .execute();
21
+ }
22
+
23
+ export async function down(db: Kysely<any>): Promise<void> {
24
+ await db.schema.dropTable("users").ifExists().execute();
25
+ }
@@ -0,0 +1,32 @@
1
+ import type { Kysely } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ await db.schema
5
+ .createTable("sessions")
6
+ .ifNotExists()
7
+ .addColumn("id", "text", (col) => col.primaryKey())
8
+ .addColumn("user_id", "text", (col) =>
9
+ col.notNull().references("users.id").onDelete("cascade")
10
+ )
11
+ .addColumn("expires_at", "text", (col) => col.notNull())
12
+ .addColumn("created_at", "text", (col) => col.notNull().defaultTo("CURRENT_TIMESTAMP"))
13
+ .execute();
14
+
15
+ await db.schema
16
+ .createIndex("idx_sessions_user_id")
17
+ .ifNotExists()
18
+ .on("sessions")
19
+ .column("user_id")
20
+ .execute();
21
+
22
+ await db.schema
23
+ .createIndex("idx_sessions_expires_at")
24
+ .ifNotExists()
25
+ .on("sessions")
26
+ .column("expires_at")
27
+ .execute();
28
+ }
29
+
30
+ export async function down(db: Kysely<any>): Promise<void> {
31
+ await db.schema.dropTable("sessions").ifExists().execute();
32
+ }
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+
3
+ // =============================================================================
4
+ // INPUT SCHEMAS
5
+ // =============================================================================
6
+
7
+ export const registerSchema = z.object({
8
+ email: z.string().email("Invalid email address"),
9
+ password: z.string().min(8, "Password must be at least 8 characters"),
10
+ name: z.string().min(1).optional(),
11
+ });
12
+
13
+ export const loginSchema = z.object({
14
+ email: z.string().email("Invalid email address"),
15
+ password: z.string().min(1, "Password is required"),
16
+ });
17
+
18
+ export const updateProfileSchema = z.object({
19
+ name: z.string().min(1).optional(),
20
+ email: z.string().email().optional(),
21
+ });
22
+
23
+ // =============================================================================
24
+ // OUTPUT SCHEMAS
25
+ // =============================================================================
26
+
27
+ export const userSchema = z.object({
28
+ id: z.string(),
29
+ email: z.string(),
30
+ name: z.string().nullable(),
31
+ });
32
+
33
+ export const authResponseSchema = z.object({
34
+ user: userSchema,
35
+ sessionId: z.string(),
36
+ });
37
+
38
+ export const meResponseSchema = userSchema.nullable();
39
+
40
+ export const logoutResponseSchema = z.object({
41
+ success: z.boolean(),
42
+ });
43
+
44
+ // =============================================================================
45
+ // DERIVED TYPES
46
+ // =============================================================================
47
+
48
+ export type RegisterInput = z.infer<typeof registerSchema>;
49
+ export type LoginInput = z.infer<typeof loginSchema>;
50
+ export type UpdateProfileInput = z.infer<typeof updateProfileSchema>;
51
+ export type User = z.infer<typeof userSchema>;
52
+ export type AuthResponse = z.infer<typeof authResponseSchema>;
@@ -0,0 +1,20 @@
1
+ import type { Handler, Routes, AppContext } from "$server/api";
2
+
3
+ /**
4
+ * Login Handler - Authenticate user and create session
5
+ */
6
+ export class LoginHandler implements Handler<Routes.Auth.Login> {
7
+ constructor(private ctx: AppContext) {}
8
+
9
+ async handle(input: Routes.Auth.Login.Input): Promise<Routes.Auth.Login.Output> {
10
+ const result = await this.ctx.plugins.auth.login({
11
+ email: input.email,
12
+ password: input.password,
13
+ });
14
+
15
+ return {
16
+ user: result.user,
17
+ sessionId: result.sessionId,
18
+ };
19
+ }
20
+ }
@@ -0,0 +1,19 @@
1
+ import type { Handler, Routes, AppContext } from "$server/api";
2
+
3
+ /**
4
+ * Logout Handler - Invalidate current session
5
+ */
6
+ export class LogoutHandler implements Handler<Routes.Auth.Logout> {
7
+ constructor(private ctx: AppContext) {}
8
+
9
+ async handle(_input: Routes.Auth.Logout.Input): Promise<Routes.Auth.Logout.Output> {
10
+ // Get session ID from request context (set by auth middleware)
11
+ const sessionId = (this.ctx as any).sessionId;
12
+
13
+ if (sessionId) {
14
+ await this.ctx.plugins.auth.logout(sessionId);
15
+ }
16
+
17
+ return { success: true };
18
+ }
19
+ }
@@ -0,0 +1,23 @@
1
+ import type { Handler, Routes, AppContext } from "$server/api";
2
+
3
+ /**
4
+ * Me Handler - Get current authenticated user
5
+ */
6
+ export class MeHandler implements Handler<Routes.Auth.Me> {
7
+ constructor(private ctx: AppContext) {}
8
+
9
+ async handle(_input: Routes.Auth.Me.Input): Promise<Routes.Auth.Me.Output> {
10
+ // Get user from request context (set by auth middleware)
11
+ const user = (this.ctx as any).user;
12
+
13
+ if (!user) {
14
+ return null;
15
+ }
16
+
17
+ return {
18
+ id: user.id,
19
+ email: user.email,
20
+ name: user.name,
21
+ };
22
+ }
23
+ }
@@ -0,0 +1,21 @@
1
+ import type { Handler, Routes, AppContext } from "$server/api";
2
+
3
+ /**
4
+ * Register Handler - Create a new user account
5
+ */
6
+ export class RegisterHandler implements Handler<Routes.Auth.Register> {
7
+ constructor(private ctx: AppContext) {}
8
+
9
+ async handle(input: Routes.Auth.Register.Input): Promise<Routes.Auth.Register.Output> {
10
+ const result = await this.ctx.plugins.auth.register({
11
+ email: input.email,
12
+ password: input.password,
13
+ name: input.name,
14
+ });
15
+
16
+ return {
17
+ user: result.user,
18
+ sessionId: result.sessionId,
19
+ };
20
+ }
21
+ }
@@ -0,0 +1,24 @@
1
+ import type { Handler, Routes, AppContext } from "$server/api";
2
+
3
+ /**
4
+ * Update Profile Handler - Update current user's profile
5
+ */
6
+ export class UpdateProfileHandler implements Handler<Routes.Auth.UpdateProfile> {
7
+ constructor(private ctx: AppContext) {}
8
+
9
+ async handle(input: Routes.Auth.UpdateProfile.Input): Promise<Routes.Auth.UpdateProfile.Output> {
10
+ // Get user from request context (set by auth middleware)
11
+ const user = (this.ctx as any).user;
12
+
13
+ if (!user) {
14
+ throw this.ctx.errors.Unauthorized("Not authenticated");
15
+ }
16
+
17
+ const updated = await this.ctx.plugins.auth.updateProfile(user.id, {
18
+ name: input.name,
19
+ email: input.email,
20
+ });
21
+
22
+ return updated;
23
+ }
24
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Auth Router - Authentication endpoints
3
+ *
4
+ * Provides:
5
+ * - auth.register - Create new account
6
+ * - auth.login - Login and get session
7
+ * - auth.logout - Invalidate session (requires auth)
8
+ * - auth.me - Get current user (optional auth)
9
+ * - auth.updateProfile - Update profile (requires auth)
10
+ */
11
+
12
+ import { createRouter } from "@donkeylabs/server";
13
+ import { z } from "zod";
14
+ import {
15
+ registerSchema,
16
+ loginSchema,
17
+ updateProfileSchema,
18
+ authResponseSchema,
19
+ userSchema,
20
+ logoutResponseSchema,
21
+ } from "./auth.schemas";
22
+ import { RegisterHandler } from "./handlers/register.handler";
23
+ import { LoginHandler } from "./handlers/login.handler";
24
+ import { LogoutHandler } from "./handlers/logout.handler";
25
+ import { MeHandler } from "./handlers/me.handler";
26
+ import { UpdateProfileHandler } from "./handlers/update-profile.handler";
27
+
28
+ export const authRouter = createRouter("auth")
29
+
30
+ // Public routes
31
+ .route("register").typed({
32
+ input: registerSchema,
33
+ output: authResponseSchema,
34
+ handle: RegisterHandler,
35
+ })
36
+
37
+ .route("login").typed({
38
+ input: loginSchema,
39
+ output: authResponseSchema,
40
+ handle: LoginHandler,
41
+ })
42
+
43
+ // Optional auth - returns user if logged in, null otherwise
44
+ .middleware.auth({ required: false })
45
+ .route("me").typed({
46
+ input: z.object({}),
47
+ output: userSchema.nullable(),
48
+ handle: MeHandler,
49
+ })
50
+
51
+ // Protected routes - require authentication
52
+ .middleware.auth({ required: true })
53
+ .route("logout").typed({
54
+ input: z.object({}),
55
+ output: logoutResponseSchema,
56
+ handle: LogoutHandler,
57
+ })
58
+
59
+ .route("updateProfile").typed({
60
+ input: updateProfileSchema,
61
+ output: userSchema,
62
+ handle: UpdateProfileHandler,
63
+ });