@lovelybunch/api 1.0.55 → 1.0.57

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.
@@ -0,0 +1,122 @@
1
+ import { AuthConfig, LocalAuthUser, AuthSession, ApiKey, UserRole } from '@lovelybunch/types';
2
+ export declare class AuthManager {
3
+ private authConfigPath;
4
+ private authConfig;
5
+ constructor(dataPath: string);
6
+ /**
7
+ * Check if auth is enabled
8
+ */
9
+ isAuthEnabled(): Promise<boolean>;
10
+ /**
11
+ * Load auth config from file
12
+ */
13
+ loadAuthConfig(): Promise<AuthConfig>;
14
+ /**
15
+ * Save auth config to file
16
+ */
17
+ saveAuthConfig(config: AuthConfig): Promise<void>;
18
+ /**
19
+ * Initialize auth config with defaults
20
+ */
21
+ initializeAuthConfig(adminEmail: string, adminName: string): Promise<AuthConfig>;
22
+ /**
23
+ * Find user by email
24
+ */
25
+ findUserByEmail(email: string): Promise<LocalAuthUser | null>;
26
+ /**
27
+ * Find user by ID
28
+ */
29
+ findUserById(userId: string): Promise<LocalAuthUser | null>;
30
+ /**
31
+ * Check if email is whitelisted
32
+ */
33
+ isEmailWhitelisted(email: string): Promise<boolean>;
34
+ /**
35
+ * Register a new user (must be whitelisted)
36
+ */
37
+ registerUser(email: string, password: string, name: string): Promise<LocalAuthUser>;
38
+ /**
39
+ * Verify user credentials
40
+ */
41
+ verifyCredentials(email: string, password: string): Promise<LocalAuthUser | null>;
42
+ /**
43
+ * Change user password
44
+ */
45
+ changePassword(userId: string, currentPassword: string, newPassword: string): Promise<void>;
46
+ /**
47
+ * Admin reset password (no current password required)
48
+ */
49
+ adminResetPassword(userId: string, newPassword: string): Promise<void>;
50
+ /**
51
+ * Update user's last login time
52
+ */
53
+ updateUserLastLogin(userId: string): Promise<void>;
54
+ /**
55
+ * Add a new whitelisted user
56
+ */
57
+ addWhitelistedUser(email: string, name: string, role?: UserRole): Promise<LocalAuthUser>;
58
+ /**
59
+ * Remove a user
60
+ */
61
+ removeUser(userId: string): Promise<void>;
62
+ /**
63
+ * Update user role
64
+ */
65
+ updateUserRole(userId: string, role: UserRole): Promise<void>;
66
+ /**
67
+ * Generate JWT token
68
+ */
69
+ generateToken(user: LocalAuthUser, provider?: string): Promise<string>;
70
+ /**
71
+ * Verify JWT token
72
+ */
73
+ verifyToken(token: string): Promise<AuthSession | null>;
74
+ /**
75
+ * Create API key
76
+ */
77
+ createApiKey(name: string, createdBy: string, scopes: string[], expiresIn?: string): Promise<{
78
+ apiKey: ApiKey;
79
+ rawKey: string;
80
+ }>;
81
+ /**
82
+ * Verify API key
83
+ */
84
+ verifyApiKey(rawKey: string): Promise<ApiKey | null>;
85
+ /**
86
+ * Update API key last used time
87
+ */
88
+ updateApiKeyLastUsed(apiKeyId: string): Promise<void>;
89
+ /**
90
+ * Delete API key
91
+ */
92
+ deleteApiKey(apiKeyId: string): Promise<void>;
93
+ /**
94
+ * List all API keys (without raw keys)
95
+ */
96
+ listApiKeys(): Promise<ApiKey[]>;
97
+ /**
98
+ * Generate a secure random ID
99
+ */
100
+ private generateId;
101
+ /**
102
+ * Generate a secure random secret
103
+ */
104
+ private generateSecret;
105
+ /**
106
+ * Generate a secure API key
107
+ */
108
+ private generateApiKey;
109
+ /**
110
+ * Parse expiry string to seconds
111
+ */
112
+ private parseExpiry;
113
+ /**
114
+ * Calculate expiry date from string
115
+ */
116
+ private calculateExpiry;
117
+ /**
118
+ * Clear cached config (useful for testing)
119
+ */
120
+ clearCache(): void;
121
+ }
122
+ export declare function getAuthManager(dataPath?: string): AuthManager;
@@ -0,0 +1,411 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import bcrypt from 'bcrypt';
4
+ import jwt from 'jsonwebtoken';
5
+ import crypto from 'crypto';
6
+ const SALT_ROUNDS = 10;
7
+ const DEFAULT_SESSION_EXPIRY = '7d';
8
+ const DEFAULT_COOKIE_NAME = 'nut-session';
9
+ export class AuthManager {
10
+ authConfigPath;
11
+ authConfig = null;
12
+ constructor(dataPath) {
13
+ this.authConfigPath = path.join(dataPath, '.nut', 'auth.json');
14
+ }
15
+ /**
16
+ * Check if auth is enabled
17
+ */
18
+ async isAuthEnabled() {
19
+ try {
20
+ const config = await this.loadAuthConfig();
21
+ return config.enabled;
22
+ }
23
+ catch (error) {
24
+ // If config doesn't exist or can't be loaded, auth is disabled
25
+ return false;
26
+ }
27
+ }
28
+ /**
29
+ * Load auth config from file
30
+ */
31
+ async loadAuthConfig() {
32
+ if (this.authConfig) {
33
+ return this.authConfig;
34
+ }
35
+ try {
36
+ const content = await fs.readFile(this.authConfigPath, 'utf-8');
37
+ this.authConfig = JSON.parse(content);
38
+ return this.authConfig;
39
+ }
40
+ catch (error) {
41
+ throw new Error('Auth config not found or invalid');
42
+ }
43
+ }
44
+ /**
45
+ * Save auth config to file
46
+ */
47
+ async saveAuthConfig(config) {
48
+ await fs.writeFile(this.authConfigPath, JSON.stringify(config, null, 2), 'utf-8');
49
+ this.authConfig = config;
50
+ }
51
+ /**
52
+ * Initialize auth config with defaults
53
+ */
54
+ async initializeAuthConfig(adminEmail, adminName) {
55
+ const config = {
56
+ version: '1.0',
57
+ enabled: false, // Start disabled, admin must enable
58
+ allowRegistration: true,
59
+ providers: {
60
+ local: {
61
+ enabled: true,
62
+ users: [
63
+ {
64
+ id: this.generateId(),
65
+ email: adminEmail,
66
+ name: adminName,
67
+ role: 'admin',
68
+ registered: false,
69
+ createdAt: new Date(),
70
+ updatedAt: new Date(),
71
+ },
72
+ ],
73
+ },
74
+ oauth: {},
75
+ },
76
+ session: {
77
+ secret: this.generateSecret(),
78
+ expiresIn: DEFAULT_SESSION_EXPIRY,
79
+ cookieName: DEFAULT_COOKIE_NAME,
80
+ secure: process.env.NODE_ENV === 'production',
81
+ },
82
+ apiKeys: [],
83
+ };
84
+ await this.saveAuthConfig(config);
85
+ return config;
86
+ }
87
+ /**
88
+ * Find user by email
89
+ */
90
+ async findUserByEmail(email) {
91
+ const config = await this.loadAuthConfig();
92
+ const user = config.providers.local.users.find((u) => u.email.toLowerCase() === email.toLowerCase());
93
+ return user || null;
94
+ }
95
+ /**
96
+ * Find user by ID
97
+ */
98
+ async findUserById(userId) {
99
+ const config = await this.loadAuthConfig();
100
+ const user = config.providers.local.users.find((u) => u.id === userId);
101
+ return user || null;
102
+ }
103
+ /**
104
+ * Check if email is whitelisted
105
+ */
106
+ async isEmailWhitelisted(email) {
107
+ const config = await this.loadAuthConfig();
108
+ return config.providers.local.users.some((u) => u.email.toLowerCase() === email.toLowerCase());
109
+ }
110
+ /**
111
+ * Register a new user (must be whitelisted)
112
+ */
113
+ async registerUser(email, password, name) {
114
+ const config = await this.loadAuthConfig();
115
+ if (!config.allowRegistration) {
116
+ throw new Error('Registration is disabled');
117
+ }
118
+ // Find the whitelisted user entry
119
+ const userIndex = config.providers.local.users.findIndex((u) => u.email.toLowerCase() === email.toLowerCase());
120
+ if (userIndex === -1) {
121
+ throw new Error('Email not whitelisted');
122
+ }
123
+ const user = config.providers.local.users[userIndex];
124
+ if (user.registered) {
125
+ throw new Error('User already registered');
126
+ }
127
+ // Hash password
128
+ const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
129
+ // Update user
130
+ user.name = name;
131
+ user.passwordHash = passwordHash;
132
+ user.registered = true;
133
+ user.updatedAt = new Date();
134
+ await this.saveAuthConfig(config);
135
+ return user;
136
+ }
137
+ /**
138
+ * Verify user credentials
139
+ */
140
+ async verifyCredentials(email, password) {
141
+ const user = await this.findUserByEmail(email);
142
+ if (!user || !user.registered || !user.passwordHash) {
143
+ return null;
144
+ }
145
+ const isValid = await bcrypt.compare(password, user.passwordHash);
146
+ if (!isValid) {
147
+ return null;
148
+ }
149
+ // Update last login
150
+ await this.updateUserLastLogin(user.id);
151
+ return user;
152
+ }
153
+ /**
154
+ * Change user password
155
+ */
156
+ async changePassword(userId, currentPassword, newPassword) {
157
+ const config = await this.loadAuthConfig();
158
+ const userIndex = config.providers.local.users.findIndex((u) => u.id === userId);
159
+ if (userIndex === -1) {
160
+ throw new Error('User not found');
161
+ }
162
+ const user = config.providers.local.users[userIndex];
163
+ if (!user.passwordHash) {
164
+ throw new Error('User has no password set');
165
+ }
166
+ // Verify current password
167
+ const isValid = await bcrypt.compare(currentPassword, user.passwordHash);
168
+ if (!isValid) {
169
+ throw new Error('Current password is incorrect');
170
+ }
171
+ // Hash and set new password
172
+ user.passwordHash = await bcrypt.hash(newPassword, SALT_ROUNDS);
173
+ user.updatedAt = new Date();
174
+ await this.saveAuthConfig(config);
175
+ }
176
+ /**
177
+ * Admin reset password (no current password required)
178
+ */
179
+ async adminResetPassword(userId, newPassword) {
180
+ const config = await this.loadAuthConfig();
181
+ const userIndex = config.providers.local.users.findIndex((u) => u.id === userId);
182
+ if (userIndex === -1) {
183
+ throw new Error('User not found');
184
+ }
185
+ const user = config.providers.local.users[userIndex];
186
+ user.passwordHash = await bcrypt.hash(newPassword, SALT_ROUNDS);
187
+ user.updatedAt = new Date();
188
+ await this.saveAuthConfig(config);
189
+ }
190
+ /**
191
+ * Update user's last login time
192
+ */
193
+ async updateUserLastLogin(userId) {
194
+ const config = await this.loadAuthConfig();
195
+ const user = config.providers.local.users.find((u) => u.id === userId);
196
+ if (user) {
197
+ user.lastLoginAt = new Date();
198
+ await this.saveAuthConfig(config);
199
+ }
200
+ }
201
+ /**
202
+ * Add a new whitelisted user
203
+ */
204
+ async addWhitelistedUser(email, name, role = 'viewer') {
205
+ const config = await this.loadAuthConfig();
206
+ // Check if user already exists
207
+ const existingUser = config.providers.local.users.find((u) => u.email.toLowerCase() === email.toLowerCase());
208
+ if (existingUser) {
209
+ throw new Error('User already exists');
210
+ }
211
+ const newUser = {
212
+ id: this.generateId(),
213
+ email,
214
+ name,
215
+ role,
216
+ registered: false,
217
+ createdAt: new Date(),
218
+ updatedAt: new Date(),
219
+ };
220
+ config.providers.local.users.push(newUser);
221
+ await this.saveAuthConfig(config);
222
+ return newUser;
223
+ }
224
+ /**
225
+ * Remove a user
226
+ */
227
+ async removeUser(userId) {
228
+ const config = await this.loadAuthConfig();
229
+ const userIndex = config.providers.local.users.findIndex((u) => u.id === userId);
230
+ if (userIndex === -1) {
231
+ throw new Error('User not found');
232
+ }
233
+ config.providers.local.users.splice(userIndex, 1);
234
+ await this.saveAuthConfig(config);
235
+ }
236
+ /**
237
+ * Update user role
238
+ */
239
+ async updateUserRole(userId, role) {
240
+ const config = await this.loadAuthConfig();
241
+ const user = config.providers.local.users.find((u) => u.id === userId);
242
+ if (!user) {
243
+ throw new Error('User not found');
244
+ }
245
+ user.role = role;
246
+ user.updatedAt = new Date();
247
+ await this.saveAuthConfig(config);
248
+ }
249
+ /**
250
+ * Generate JWT token
251
+ */
252
+ async generateToken(user, provider = 'local') {
253
+ const config = await this.loadAuthConfig();
254
+ const payload = {
255
+ userId: user.id,
256
+ email: user.email,
257
+ name: user.name,
258
+ role: user.role,
259
+ provider: provider,
260
+ iat: Math.floor(Date.now() / 1000),
261
+ exp: Math.floor(Date.now() / 1000) + this.parseExpiry(config.session.expiresIn),
262
+ };
263
+ return jwt.sign(payload, config.session.secret);
264
+ }
265
+ /**
266
+ * Verify JWT token
267
+ */
268
+ async verifyToken(token) {
269
+ try {
270
+ const config = await this.loadAuthConfig();
271
+ const decoded = jwt.verify(token, config.session.secret);
272
+ return decoded;
273
+ }
274
+ catch (error) {
275
+ return null;
276
+ }
277
+ }
278
+ /**
279
+ * Create API key
280
+ */
281
+ async createApiKey(name, createdBy, scopes, expiresIn) {
282
+ const config = await this.loadAuthConfig();
283
+ const rawKey = this.generateApiKey();
284
+ const hashedKey = await bcrypt.hash(rawKey, SALT_ROUNDS);
285
+ const apiKey = {
286
+ id: this.generateId(),
287
+ name,
288
+ key: hashedKey,
289
+ keyPreview: rawKey.slice(-4),
290
+ createdBy,
291
+ createdAt: new Date(),
292
+ expiresAt: expiresIn ? this.calculateExpiry(expiresIn) : undefined,
293
+ scopes,
294
+ };
295
+ config.apiKeys.push(apiKey);
296
+ await this.saveAuthConfig(config);
297
+ return { apiKey, rawKey };
298
+ }
299
+ /**
300
+ * Verify API key
301
+ */
302
+ async verifyApiKey(rawKey) {
303
+ const config = await this.loadAuthConfig();
304
+ for (const apiKey of config.apiKeys) {
305
+ const isValid = await bcrypt.compare(rawKey, apiKey.key);
306
+ if (isValid) {
307
+ // Check expiration
308
+ if (apiKey.expiresAt && new Date(apiKey.expiresAt) < new Date()) {
309
+ return null;
310
+ }
311
+ // Update last used
312
+ await this.updateApiKeyLastUsed(apiKey.id);
313
+ return apiKey;
314
+ }
315
+ }
316
+ return null;
317
+ }
318
+ /**
319
+ * Update API key last used time
320
+ */
321
+ async updateApiKeyLastUsed(apiKeyId) {
322
+ const config = await this.loadAuthConfig();
323
+ const apiKey = config.apiKeys.find((k) => k.id === apiKeyId);
324
+ if (apiKey) {
325
+ apiKey.lastUsedAt = new Date();
326
+ await this.saveAuthConfig(config);
327
+ }
328
+ }
329
+ /**
330
+ * Delete API key
331
+ */
332
+ async deleteApiKey(apiKeyId) {
333
+ const config = await this.loadAuthConfig();
334
+ const keyIndex = config.apiKeys.findIndex((k) => k.id === apiKeyId);
335
+ if (keyIndex === -1) {
336
+ throw new Error('API key not found');
337
+ }
338
+ config.apiKeys.splice(keyIndex, 1);
339
+ await this.saveAuthConfig(config);
340
+ }
341
+ /**
342
+ * List all API keys (without raw keys)
343
+ */
344
+ async listApiKeys() {
345
+ const config = await this.loadAuthConfig();
346
+ return config.apiKeys;
347
+ }
348
+ /**
349
+ * Generate a secure random ID
350
+ */
351
+ generateId() {
352
+ return crypto.randomBytes(16).toString('hex');
353
+ }
354
+ /**
355
+ * Generate a secure random secret
356
+ */
357
+ generateSecret() {
358
+ return crypto.randomBytes(32).toString('hex');
359
+ }
360
+ /**
361
+ * Generate a secure API key
362
+ */
363
+ generateApiKey() {
364
+ return `nut_${crypto.randomBytes(32).toString('hex')}`;
365
+ }
366
+ /**
367
+ * Parse expiry string to seconds
368
+ */
369
+ parseExpiry(expiry) {
370
+ const match = expiry.match(/^(\d+)([smhd])$/);
371
+ if (!match) {
372
+ return 7 * 24 * 60 * 60; // Default to 7 days
373
+ }
374
+ const value = parseInt(match[1]);
375
+ const unit = match[2];
376
+ switch (unit) {
377
+ case 's':
378
+ return value;
379
+ case 'm':
380
+ return value * 60;
381
+ case 'h':
382
+ return value * 60 * 60;
383
+ case 'd':
384
+ return value * 24 * 60 * 60;
385
+ default:
386
+ return 7 * 24 * 60 * 60;
387
+ }
388
+ }
389
+ /**
390
+ * Calculate expiry date from string
391
+ */
392
+ calculateExpiry(expiresIn) {
393
+ const seconds = this.parseExpiry(expiresIn);
394
+ return new Date(Date.now() + seconds * 1000);
395
+ }
396
+ /**
397
+ * Clear cached config (useful for testing)
398
+ */
399
+ clearCache() {
400
+ this.authConfig = null;
401
+ }
402
+ }
403
+ // Singleton instance
404
+ let authManagerInstance = null;
405
+ export function getAuthManager(dataPath) {
406
+ if (!authManagerInstance) {
407
+ const path = dataPath || process.env.GAIT_DATA_PATH || process.cwd();
408
+ authManagerInstance = new AuthManager(path);
409
+ }
410
+ return authManagerInstance;
411
+ }
@@ -0,0 +1,31 @@
1
+ import { Context, Next } from 'hono';
2
+ import { AuthSession } from '@lovelybunch/types';
3
+ /**
4
+ * Authentication middleware
5
+ * Checks for valid JWT token in cookie or Authorization header
6
+ */
7
+ export declare function authMiddleware(c: Context, next: Next): Promise<void | (Response & import("hono").TypedResponse<{
8
+ error: string;
9
+ message: string;
10
+ }, 401, "json">) | (Response & import("hono").TypedResponse<{
11
+ error: string;
12
+ message: string;
13
+ }, 403, "json">)>;
14
+ /**
15
+ * Helper to get current session from context
16
+ */
17
+ export declare function getSession(c: Context): AuthSession | null;
18
+ /**
19
+ * Helper to check if user is admin
20
+ */
21
+ export declare function isAdmin(c: Context): boolean;
22
+ /**
23
+ * Helper to require authentication (throws if not authenticated)
24
+ * Returns null if auth is disabled
25
+ */
26
+ export declare function requireAuth(c: Context): AuthSession | null;
27
+ /**
28
+ * Helper to require admin access (throws if not admin)
29
+ * Returns null if auth is disabled
30
+ */
31
+ export declare function requireAdmin(c: Context): AuthSession | null;
@@ -0,0 +1,142 @@
1
+ import { getCookie } from 'hono/cookie';
2
+ import { getAuthManager } from '../lib/auth/auth-manager.js';
3
+ // Public routes that don't require authentication
4
+ const PUBLIC_ROUTES = [
5
+ '/api/health',
6
+ '/api/v1/auth/status',
7
+ '/api/v1/auth/login',
8
+ '/api/v1/auth/register',
9
+ '/api/v1/auth/oauth',
10
+ '/api/v1/auth/callback',
11
+ ];
12
+ // Routes that require specific roles
13
+ const ADMIN_ROUTES = [
14
+ '/api/v1/auth-settings',
15
+ '/api/v1/config',
16
+ ];
17
+ /**
18
+ * Check if a route is public (doesn't require auth)
19
+ */
20
+ function isPublicRoute(path) {
21
+ return PUBLIC_ROUTES.some((route) => path.startsWith(route));
22
+ }
23
+ /**
24
+ * Check if a route requires admin access
25
+ */
26
+ function isAdminRoute(path) {
27
+ return ADMIN_ROUTES.some((route) => path.startsWith(route));
28
+ }
29
+ /**
30
+ * Authentication middleware
31
+ * Checks for valid JWT token in cookie or Authorization header
32
+ */
33
+ export async function authMiddleware(c, next) {
34
+ const authManager = getAuthManager();
35
+ // Check if auth is enabled
36
+ const authEnabled = await authManager.isAuthEnabled();
37
+ // Store auth status in context so helpers can check it
38
+ c.set('authEnabled', authEnabled);
39
+ if (!authEnabled) {
40
+ // Auth is disabled, allow all requests
41
+ return next();
42
+ }
43
+ const path = c.req.path;
44
+ // Allow public routes without authentication
45
+ if (isPublicRoute(path)) {
46
+ return next();
47
+ }
48
+ // Try to get token from cookie first
49
+ let token;
50
+ try {
51
+ const config = await authManager.loadAuthConfig();
52
+ token = getCookie(c, config.session.cookieName);
53
+ }
54
+ catch (error) {
55
+ // Config not available, try default cookie name
56
+ token = getCookie(c, 'nut-session');
57
+ }
58
+ // If no cookie, try Authorization header
59
+ if (!token) {
60
+ const authHeader = c.req.header('Authorization');
61
+ if (authHeader && authHeader.startsWith('Bearer ')) {
62
+ token = authHeader.substring(7);
63
+ }
64
+ }
65
+ // If no token, check for API key
66
+ if (!token) {
67
+ const apiKey = c.req.header('X-API-Key');
68
+ if (apiKey) {
69
+ const validApiKey = await authManager.verifyApiKey(apiKey);
70
+ if (validApiKey) {
71
+ // Store API key info in context
72
+ c.set('apiKey', validApiKey);
73
+ c.set('authType', 'apikey');
74
+ return next();
75
+ }
76
+ }
77
+ }
78
+ if (!token) {
79
+ return c.json({ error: 'Unauthorized', message: 'No authentication token provided' }, 401);
80
+ }
81
+ // Verify token
82
+ const session = await authManager.verifyToken(token);
83
+ if (!session) {
84
+ return c.json({ error: 'Unauthorized', message: 'Invalid or expired token' }, 401);
85
+ }
86
+ // Check if route requires admin access
87
+ if (isAdminRoute(path) && session.role !== 'admin') {
88
+ return c.json({ error: 'Forbidden', message: 'Admin access required' }, 403);
89
+ }
90
+ // Store session in context
91
+ c.set('session', session);
92
+ c.set('authType', 'session');
93
+ return next();
94
+ }
95
+ /**
96
+ * Helper to get current session from context
97
+ */
98
+ export function getSession(c) {
99
+ return c.get('session') || null;
100
+ }
101
+ /**
102
+ * Helper to check if user is admin
103
+ */
104
+ export function isAdmin(c) {
105
+ const session = getSession(c);
106
+ return session?.role === 'admin';
107
+ }
108
+ /**
109
+ * Helper to require authentication (throws if not authenticated)
110
+ * Returns null if auth is disabled
111
+ */
112
+ export function requireAuth(c) {
113
+ const authEnabled = c.get('authEnabled');
114
+ // If auth is disabled, return null (no authentication required)
115
+ if (authEnabled === false) {
116
+ return null;
117
+ }
118
+ const session = getSession(c);
119
+ if (!session) {
120
+ throw new Error('Authentication required');
121
+ }
122
+ return session;
123
+ }
124
+ /**
125
+ * Helper to require admin access (throws if not admin)
126
+ * Returns null if auth is disabled
127
+ */
128
+ export function requireAdmin(c) {
129
+ const authEnabled = c.get('authEnabled');
130
+ // If auth is disabled, return null (no admin check required)
131
+ if (authEnabled === false) {
132
+ return null;
133
+ }
134
+ const session = requireAuth(c);
135
+ if (!session) {
136
+ throw new Error('Authentication required');
137
+ }
138
+ if (session.role !== 'admin') {
139
+ throw new Error('Admin access required');
140
+ }
141
+ return session;
142
+ }
@@ -0,0 +1 @@
1
+ export { default } from './route.js';
@@ -0,0 +1 @@
1
+ export { default } from './route.js';