@mmmbuto/nexuscrew 0.2.1 → 0.2.3

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 (52) hide show
  1. package/README.md +122 -67
  2. package/bin/nexuscrew.js +194 -470
  3. package/frontend/dist/assets/{index-BG1N7bSL.js → index-BsldfYnr.js} +1704 -1711
  4. package/frontend/dist/assets/index-DcQGg3dp.css +1 -0
  5. package/frontend/dist/index.html +3 -11
  6. package/lib/server/routes/hosts.js +22 -12
  7. package/lib/server/routes/launchers.js +12 -0
  8. package/lib/server/routes/models.js +16 -63
  9. package/lib/server/routes/runtime.js +10 -0
  10. package/lib/server/routes/send.js +97 -258
  11. package/lib/server/routes/sessions.js +13 -42
  12. package/lib/server/routes/status.js +18 -22
  13. package/lib/server/routes/tmux.js +88 -0
  14. package/lib/server/server.js +60 -81
  15. package/lib/services/engine-discovery.js +192 -445
  16. package/lib/services/log-watcher.js +22 -40
  17. package/lib/services/session-store.js +88 -62
  18. package/lib/services/tmux-manager.js +111 -173
  19. package/package.json +6 -10
  20. package/frontend/dist/apple-touch-icon.png +0 -0
  21. package/frontend/dist/assets/index-CiAtinNP.css +0 -1
  22. package/frontend/dist/favicon.svg +0 -20
  23. package/frontend/dist/icon-192.png +0 -0
  24. package/frontend/dist/icon-512.png +0 -0
  25. package/frontend/dist/site.webmanifest +0 -29
  26. package/lib/config/hosts.js +0 -67
  27. package/lib/config/manager.js +0 -379
  28. package/lib/config/models.js +0 -408
  29. package/lib/server/db/adapter.js +0 -274
  30. package/lib/server/db/drivers/sql-js.js +0 -75
  31. package/lib/server/db/migrate.js +0 -174
  32. package/lib/server/db/migrations/001_base_schema.sql +0 -70
  33. package/lib/server/middleware/auth.js +0 -134
  34. package/lib/server/middleware/rate-limit.js +0 -63
  35. package/lib/server/models/User.js +0 -128
  36. package/lib/server/routes/auth.js +0 -168
  37. package/lib/server/routes/keys.js +0 -28
  38. package/lib/server/routes/runtimes.js +0 -34
  39. package/lib/server/routes/speech.js +0 -46
  40. package/lib/server/routes/upload.js +0 -135
  41. package/lib/server/routes/wake-lock.js +0 -95
  42. package/lib/server/routes/workspaces.js +0 -101
  43. package/lib/server/services/attachment-manager.js +0 -57
  44. package/lib/server/services/context-bridge.js +0 -425
  45. package/lib/server/services/runtime-manager.js +0 -462
  46. package/lib/server/services/speech-manager.js +0 -76
  47. package/lib/server/services/summary-generator.js +0 -309
  48. package/lib/server/services/workspace-manager.js +0 -79
  49. package/lib/services/remote-pane-watcher.js +0 -155
  50. package/lib/setup/postinstall.js +0 -38
  51. package/lib/utils/paths.js +0 -124
  52. package/lib/utils/termux.js +0 -182
@@ -1,75 +0,0 @@
1
- const initSqlJs = require('sql.js');
2
- const fs = require('fs');
3
-
4
- class SqlJsDriver {
5
- constructor(dbPath) {
6
- this.dbPath = dbPath;
7
- this.db = null;
8
- this.saveInterval = null;
9
- }
10
-
11
- async init() {
12
- const SQL = await initSqlJs();
13
-
14
- if (fs.existsSync(this.dbPath)) {
15
- const buffer = fs.readFileSync(this.dbPath);
16
- this.db = new SQL.Database(buffer);
17
- } else {
18
- this.db = new SQL.Database();
19
- }
20
-
21
- // Auto-save every 5 seconds
22
- this.saveInterval = setInterval(() => this.save(), 5000);
23
- }
24
-
25
- save() {
26
- if (!this.db) return;
27
- const data = this.db.export();
28
- const buffer = Buffer.from(data);
29
- fs.writeFileSync(this.dbPath, buffer);
30
- }
31
-
32
- exec(sql) {
33
- return this.db.run(sql);
34
- }
35
-
36
- prepare(sql) {
37
- const stmt = this.db.prepare(sql);
38
- return {
39
- run: (...params) => {
40
- stmt.bind(params);
41
- stmt.step();
42
- stmt.reset();
43
- this.save();
44
- },
45
- get: (...params) => {
46
- stmt.bind(params);
47
- const result = stmt.step() ? stmt.getAsObject() : null;
48
- stmt.reset();
49
- return result;
50
- },
51
- all: (...params) => {
52
- stmt.bind(params);
53
- const results = [];
54
- while (stmt.step()) {
55
- results.push(stmt.getAsObject());
56
- }
57
- stmt.reset();
58
- return results;
59
- }
60
- };
61
- }
62
-
63
- close() {
64
- if (this.db) {
65
- this.save();
66
- this.db.close();
67
- }
68
- if (this.saveInterval) {
69
- clearInterval(this.saveInterval);
70
- this.saveInterval = null;
71
- }
72
- }
73
- }
74
-
75
- module.exports = SqlJsDriver;
@@ -1,174 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { initDb, getDb, saveDb } = require('./adapter');
4
-
5
- const MIGRATIONS_DIR = path.join(__dirname, 'migrations');
6
-
7
- /**
8
- * Get list of migration files in order
9
- */
10
- function getMigrationFiles() {
11
- const files = fs.readdirSync(MIGRATIONS_DIR)
12
- .filter(f => f.endsWith('.sql'))
13
- .sort(); // Alphabetical order ensures numeric order (001_, 002_, etc.)
14
- return files;
15
- }
16
-
17
- /**
18
- * Normalize and split SQL into executable statements.
19
- * Keeps CREATE/INSERT blocks while stripping comment-only lines.
20
- */
21
- function parseSqlStatements(sql) {
22
- return sql
23
- .replace(/\r\n/g, '\n')
24
- .split(';')
25
- .map(stmt => stmt.trim())
26
- .map(stmt => {
27
- const lines = stmt
28
- .split('\n')
29
- .map(line => line.trim())
30
- .filter(line => line.length > 0 && !line.startsWith('--'));
31
- return lines.join('\n').trim();
32
- })
33
- .filter(stmt => stmt.length > 0);
34
- }
35
-
36
- /**
37
- * Get applied migrations from DB
38
- */
39
- function getAppliedMigrations(db) {
40
- try {
41
- // Ensure migrations table exists
42
- db.exec(`
43
- CREATE TABLE IF NOT EXISTS _migrations (
44
- id INTEGER PRIMARY KEY AUTOINCREMENT,
45
- name TEXT UNIQUE NOT NULL,
46
- applied_at INTEGER NOT NULL
47
- )
48
- `);
49
-
50
- const stmt = db.prepare('SELECT name FROM _migrations ORDER BY id');
51
- return stmt.all().map(row => row.name);
52
- } catch (error) {
53
- console.error('[Migration] Error getting applied migrations:', error.message);
54
- return [];
55
- }
56
- }
57
-
58
- /**
59
- * Mark migration as applied
60
- */
61
- function markMigrationApplied(db, name) {
62
- const stmt = db.prepare('INSERT INTO _migrations (name, applied_at) VALUES (?, ?)');
63
- stmt.run(name, Date.now());
64
- }
65
-
66
- /**
67
- * Run a single migration file
68
- */
69
- function runMigrationFile(db, filename) {
70
- const filePath = path.join(MIGRATIONS_DIR, filename);
71
- const migrationSql = fs.readFileSync(filePath, 'utf8');
72
- const statements = parseSqlStatements(migrationSql);
73
-
74
- console.log(`[Migration] Running ${filename} (${statements.length} statements)`);
75
-
76
- for (let i = 0; i < statements.length; i++) {
77
- const stmt = statements[i];
78
-
79
- if (/^SELECT/i.test(stmt)) {
80
- // Skip verification SELECT statements
81
- continue;
82
- }
83
-
84
- try {
85
- db.exec(stmt);
86
- } catch (error) {
87
- // Handle "duplicate column" errors gracefully
88
- if (error.message.includes('duplicate column')) {
89
- console.log(`[Migration] Column already exists (skipping): ${error.message}`);
90
- continue;
91
- }
92
- console.error(`[Migration] Failed at statement ${i + 1}: ${error.message}`);
93
- console.error('Statement preview:', stmt.substring(0, 200));
94
- throw error;
95
- }
96
- }
97
-
98
- markMigrationApplied(db, filename);
99
- console.log(`[Migration] ✅ ${filename} applied`);
100
- }
101
-
102
- /**
103
- * Run database migrations.
104
- * @param {Object} options
105
- * @param {boolean} options.skipInit - skip initDb (adapter already initialized)
106
- */
107
- async function runMigrations(options = {}) {
108
- const { skipInit = false } = options;
109
-
110
- console.log('[Migration] Starting database migration...');
111
-
112
- if (!skipInit && !getDb()) {
113
- await initDb({ skipMigrationCheck: true });
114
- }
115
-
116
- const db = getDb();
117
-
118
- if (!db) {
119
- throw new Error('[Migration] Database instance not initialized');
120
- }
121
-
122
- const migrationFiles = getMigrationFiles();
123
- const appliedMigrations = getAppliedMigrations(db);
124
-
125
- console.log(`[Migration] Found ${migrationFiles.length} migration files`);
126
- console.log(`[Migration] Already applied: ${appliedMigrations.length}`);
127
-
128
- const pendingMigrations = migrationFiles.filter(f => !appliedMigrations.includes(f));
129
-
130
- if (pendingMigrations.length === 0) {
131
- console.log('[Migration] No pending migrations');
132
- return;
133
- }
134
-
135
- console.log(`[Migration] Pending: ${pendingMigrations.join(', ')}`);
136
-
137
- for (const file of pendingMigrations) {
138
- runMigrationFile(db, file);
139
- }
140
-
141
- saveDb();
142
-
143
- console.log('[Migration] All migrations completed');
144
-
145
- // Verification counts
146
- try {
147
- const verifyStmt = db.prepare(`
148
- SELECT 'conversations' as table_name, COUNT(*) as count FROM conversations
149
- UNION ALL
150
- SELECT 'messages' as table_name, COUNT(*) as count FROM messages
151
- `);
152
-
153
- const counts = verifyStmt.all();
154
- console.log('[Migration] Verification:');
155
- counts.forEach(row => console.log(` ${row.table_name}: ${row.count} rows`));
156
- } catch (error) {
157
- console.error('[Migration] Verification failed:', error.message);
158
- }
159
- }
160
-
161
- // Run if called directly
162
- if (require.main === module) {
163
- runMigrations()
164
- .then(() => {
165
- console.log('[Migration] Complete');
166
- process.exit(0);
167
- })
168
- .catch(error => {
169
- console.error('[Migration] Failed:', error);
170
- process.exit(1);
171
- });
172
- }
173
-
174
- module.exports = { runMigrations };
@@ -1,70 +0,0 @@
1
- -- NexusCrew Base Schema Migration
2
- -- This migration creates the initial database schema for NexusCrew
3
-
4
- -- Conversations table (NexusCrew + auth schema)
5
- CREATE TABLE IF NOT EXISTS conversations (
6
- id TEXT PRIMARY KEY,
7
- title TEXT DEFAULT 'New Chat',
8
- engine TEXT NOT NULL DEFAULT 'claude',
9
- model TEXT,
10
- tmux_window TEXT,
11
- log_path TEXT,
12
- workspace TEXT DEFAULT '',
13
- host TEXT DEFAULT 'local',
14
- status TEXT DEFAULT 'active',
15
- pinned INTEGER DEFAULT 0,
16
- metadata TEXT,
17
- created_at INTEGER DEFAULT (unixepoch()),
18
- updated_at INTEGER DEFAULT (unixepoch())
19
- );
20
-
21
- -- Messages table
22
- CREATE TABLE IF NOT EXISTS messages (
23
- id TEXT PRIMARY KEY,
24
- conversation_id TEXT NOT NULL,
25
- role TEXT NOT NULL,
26
- content TEXT DEFAULT '',
27
- engine TEXT,
28
- model TEXT,
29
- tokens_in INTEGER DEFAULT 0,
30
- tokens_out INTEGER DEFAULT 0,
31
- created_at INTEGER DEFAULT (unixepoch()),
32
- FOREIGN KEY (conversation_id) REFERENCES conversations(id)
33
- );
34
-
35
- -- Users table (auth)
36
- CREATE TABLE IF NOT EXISTS users (
37
- id TEXT PRIMARY KEY,
38
- username TEXT UNIQUE NOT NULL,
39
- password_hash TEXT NOT NULL,
40
- role TEXT NOT NULL DEFAULT 'user',
41
- is_locked INTEGER NOT NULL DEFAULT 0,
42
- failed_attempts INTEGER NOT NULL DEFAULT 0,
43
- last_failed_attempt INTEGER,
44
- locked_until INTEGER,
45
- created_at INTEGER NOT NULL,
46
- last_login INTEGER
47
- );
48
-
49
- -- API Keys table
50
- CREATE TABLE IF NOT EXISTS api_keys (
51
- provider TEXT PRIMARY KEY,
52
- api_key TEXT NOT NULL,
53
- created_at INTEGER NOT NULL,
54
- updated_at INTEGER NOT NULL
55
- );
56
-
57
- -- Session summaries table
58
- CREATE TABLE IF NOT EXISTS session_summaries (
59
- conversation_id TEXT PRIMARY KEY,
60
- summary_short TEXT,
61
- key_decisions TEXT,
62
- files_modified TEXT,
63
- updated_at INTEGER,
64
- FOREIGN KEY (conversation_id) REFERENCES conversations(id)
65
- );
66
-
67
- -- Indexes
68
- CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(conversation_id, created_at);
69
- CREATE INDEX IF NOT EXISTS idx_conversations_status ON conversations(status, updated_at);
70
- CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
@@ -1,134 +0,0 @@
1
- const crypto = require('crypto');
2
- const jwt = require('jsonwebtoken');
3
- const User = require('../models/User');
4
- const { getConfig } = require('../../config/manager');
5
-
6
- let warnedEphemeralSecret = false;
7
-
8
- function getJwtSecret() {
9
- if (process.env.JWT_SECRET) {
10
- return process.env.JWT_SECRET;
11
- }
12
-
13
- const config = getConfig();
14
- if (config.auth?.jwt_secret) {
15
- return config.auth.jwt_secret;
16
- }
17
-
18
- if (!global.__NEXUSCREW_EPHEMERAL_JWT_SECRET__) {
19
- global.__NEXUSCREW_EPHEMERAL_JWT_SECRET__ = crypto.randomBytes(48).toString('hex');
20
- }
21
-
22
- if (!warnedEphemeralSecret) {
23
- warnedEphemeralSecret = true;
24
- console.warn('[Auth] JWT secret missing from env and config; using ephemeral in-memory secret for this process');
25
- }
26
-
27
- return global.__NEXUSCREW_EPHEMERAL_JWT_SECRET__;
28
- }
29
-
30
- /**
31
- * Get config user (admin from init) by id
32
- */
33
- function findConfigUserById(id) {
34
- if (id !== 'config-admin') return null;
35
-
36
- const config = getConfig();
37
- if (config.auth && config.auth.user) {
38
- return {
39
- id: 'config-admin',
40
- username: config.auth.user,
41
- role: 'admin',
42
- is_locked: false,
43
- created_at: Date.now()
44
- };
45
- }
46
- return null;
47
- }
48
-
49
- const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
50
-
51
- function generateToken(user) {
52
- return jwt.sign(
53
- {
54
- id: user.id,
55
- username: user.username,
56
- role: user.role
57
- },
58
- getJwtSecret(),
59
- { expiresIn: JWT_EXPIRES_IN }
60
- );
61
- }
62
-
63
- function verifyToken(token) {
64
- try {
65
- return jwt.verify(token, getJwtSecret());
66
- } catch (err) {
67
- return null;
68
- }
69
- }
70
-
71
- function authMiddleware(req, res, next) {
72
- // BYPASS for localhost
73
- const clientIp = req.ip || req.connection.remoteAddress || req.socket.remoteAddress;
74
- if (clientIp === '127.0.0.1' || clientIp === '::1' || clientIp === '::ffff:127.0.0.1') {
75
- req.user = { id: 'localhost', username: 'local', role: 'admin' };
76
- return next();
77
- }
78
-
79
- const authHeader = req.headers.authorization;
80
-
81
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
82
- return res.status(401).json({ error: 'No token provided' });
83
- }
84
-
85
- const token = authHeader.substring(7);
86
- const decoded = verifyToken(token);
87
-
88
- if (!decoded) {
89
- return res.status(401).json({ error: 'Invalid or expired token' });
90
- }
91
-
92
- // Verify user still exists - check config user first, then database
93
- let user = findConfigUserById(decoded.id);
94
- let isConfigUser = !!user;
95
-
96
- if (!user) {
97
- user = User.findById(decoded.id);
98
- }
99
-
100
- if (!user) {
101
- return res.status(401).json({ error: 'User not found' });
102
- }
103
-
104
- // Check if account is locked (only for DB users)
105
- if (!isConfigUser && User.isAccountLocked(user)) {
106
- return res.status(403).json({
107
- error: 'Account locked',
108
- locked_until: user.locked_until
109
- });
110
- }
111
-
112
- req.user = {
113
- id: user.id,
114
- username: user.username,
115
- role: user.role
116
- };
117
-
118
- next();
119
- }
120
-
121
- function adminOnly(req, res, next) {
122
- if (req.user.role !== 'admin') {
123
- return res.status(403).json({ error: 'Admin access required' });
124
- }
125
- next();
126
- }
127
-
128
- module.exports = {
129
- generateToken,
130
- verifyToken,
131
- authMiddleware,
132
- adminOnly,
133
- getJwtSecret
134
- };
@@ -1,63 +0,0 @@
1
- /**
2
- * Rate Limiting Middleware for Chat Endpoints
3
- *
4
- * Protects AI chat endpoints from abuse by limiting requests per user.
5
- * Uses express-rate-limit with user-based keying via JWT.
6
- */
7
-
8
- const rateLimit = require('express-rate-limit');
9
-
10
- /**
11
- * Chat endpoints rate limiter
12
- * - 10 requests per minute per user
13
- * - Applies to: /api/v1/chat, /api/v1/codex, /api/v1/gemini, /api/v1/qwen
14
- */
15
- const chatRateLimiter = rateLimit({
16
- windowMs: 60 * 1000, // 1 minute window
17
- max: 10, // 10 requests per window
18
- standardHeaders: true, // Return rate limit info in headers
19
- legacyHeaders: false,
20
-
21
- // Key by user ID from JWT (set by authMiddleware)
22
- keyGenerator: (req) => {
23
- return req.user?.id || req.ip;
24
- },
25
-
26
- // Custom error response
27
- handler: (req, res) => {
28
- res.status(429).json({
29
- error: 'Too many requests',
30
- message: 'Rate limit exceeded. Please wait before sending more messages.',
31
- retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
32
- });
33
- },
34
-
35
- // Skip rate limiting for interrupt endpoints
36
- skip: (req) => {
37
- return req.path.endsWith('/interrupt');
38
- }
39
- });
40
-
41
- /**
42
- * General API rate limiter (for non-chat endpoints)
43
- * - 60 requests per minute per IP
44
- */
45
- const apiRateLimiter = rateLimit({
46
- windowMs: 60 * 1000,
47
- max: 60,
48
- standardHeaders: true,
49
- legacyHeaders: false,
50
- keyGenerator: (req) => req.user?.id || req.ip,
51
- handler: (req, res) => {
52
- res.status(429).json({
53
- error: 'Too many requests',
54
- message: 'API rate limit exceeded.',
55
- retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
56
- });
57
- }
58
- });
59
-
60
- module.exports = {
61
- chatRateLimiter,
62
- apiRateLimiter
63
- };
@@ -1,128 +0,0 @@
1
- const { prepare, saveDb } = require('../db/adapter');
2
- const bcrypt = require('bcryptjs');
3
- const { v4: uuidv4 } = require('uuid');
4
-
5
- class User {
6
- static create(username, password, role = 'user') {
7
- const id = uuidv4();
8
- const passwordHash = bcrypt.hashSync(password, 10);
9
- const now = Date.now();
10
-
11
- const stmt = prepare(`
12
- INSERT INTO users (id, username, password_hash, role, created_at)
13
- VALUES (?, ?, ?, ?, ?)
14
- `);
15
-
16
- stmt.run(id, username, passwordHash, role, now);
17
- saveDb();
18
-
19
- return { id, username, role, created_at: now };
20
- }
21
-
22
- static findByUsername(username) {
23
- const stmt = prepare('SELECT * FROM users WHERE username = ?');
24
- return stmt.get(username);
25
- }
26
-
27
- static findById(id) {
28
- const stmt = prepare('SELECT * FROM users WHERE id = ?');
29
- return stmt.get(id);
30
- }
31
-
32
- static count() {
33
- const stmt = prepare('SELECT COUNT(*) as count FROM users');
34
- const result = stmt.get();
35
- return result ? result.count : 0;
36
- }
37
-
38
- static verifyPassword(user, password) {
39
- return bcrypt.compareSync(password, user.password_hash);
40
- }
41
-
42
- static updateLastLogin(userId) {
43
- const stmt = prepare('UPDATE users SET last_login = ? WHERE id = ?');
44
- stmt.run(Date.now(), userId);
45
- saveDb();
46
- }
47
-
48
- static incrementFailedAttempts(userId) {
49
- const now = Date.now();
50
- const stmt = prepare(`
51
- UPDATE users
52
- SET failed_attempts = failed_attempts + 1,
53
- last_failed_attempt = ?
54
- WHERE id = ?
55
- `);
56
- stmt.run(now, userId);
57
- saveDb();
58
-
59
- // Lock account after 5 failed attempts for 15 minutes
60
- const user = this.findById(userId);
61
- if (user.failed_attempts >= 5) {
62
- const lockUntil = now + (15 * 60 * 1000); // 15 minutes
63
- const lockStmt = prepare(`
64
- UPDATE users
65
- SET is_locked = 1, locked_until = ?
66
- WHERE id = ?
67
- `);
68
- lockStmt.run(lockUntil, userId);
69
- saveDb();
70
- }
71
- }
72
-
73
- static resetFailedAttempts(userId) {
74
- const stmt = prepare(`
75
- UPDATE users
76
- SET failed_attempts = 0,
77
- is_locked = 0,
78
- locked_until = NULL
79
- WHERE id = ?
80
- `);
81
- stmt.run(userId);
82
- saveDb();
83
- }
84
-
85
- static isAccountLocked(user) {
86
- if (!user.is_locked) return false;
87
- if (!user.locked_until) return true;
88
-
89
- const now = Date.now();
90
- if (now < user.locked_until) {
91
- return true;
92
- }
93
-
94
- // Unlock account if lock period expired
95
- this.resetFailedAttempts(user.id);
96
- return false;
97
- }
98
-
99
- static logLoginAttempt(ipAddress, username, success) {
100
- const stmt = prepare(`
101
- INSERT INTO login_attempts (ip_address, username, success, timestamp)
102
- VALUES (?, ?, ?, ?)
103
- `);
104
- stmt.run(ipAddress, username, success ? 1 : 0, Date.now());
105
- saveDb();
106
- }
107
-
108
- static getRecentLoginAttempts(ipAddress, windowMs = 15 * 60 * 1000) {
109
- const since = Date.now() - windowMs;
110
- const stmt = prepare(`
111
- SELECT COUNT(*) as count
112
- FROM login_attempts
113
- WHERE ip_address = ? AND timestamp > ?
114
- `);
115
- const result = stmt.get(ipAddress, since);
116
- return result ? result.count : 0;
117
- }
118
-
119
- static cleanupOldLoginAttempts(daysToKeep = 7) {
120
- const cutoff = Date.now() - (daysToKeep * 24 * 60 * 60 * 1000);
121
- const stmt = prepare('DELETE FROM login_attempts WHERE timestamp < ?');
122
- stmt.run(cutoff);
123
- saveDb();
124
- return 0; // sql.js doesn't return changes count easily
125
- }
126
- }
127
-
128
- module.exports = User;