@pixelbyte-software/pixcode 1.51.3 → 1.51.4

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.
@@ -4,8 +4,9 @@ import { userDb, appConfigDb, apiKeysDb } from '../database/db.js';
4
4
  import { IS_PLATFORM } from '../constants/config.js';
5
5
 
6
6
  // Use env var if set, otherwise auto-generate a unique secret per installation
7
- const JWT_SECRET = process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
8
- const isPixcodeApiKey = (token) => typeof token === 'string' && (token.startsWith('px_') || token.startsWith('ck_'));
7
+ const JWT_SECRET = process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
8
+ const isPixcodeApiKey = (token) => typeof token === 'string' && (token.startsWith('px_') || token.startsWith('ck_'));
9
+ const ADMIN_ROLES = new Set(['owner', 'admin']);
9
10
 
10
11
  // Optional API key middleware
11
12
  const validateApiKey = (req, res, next) => {
@@ -22,7 +23,7 @@ const validateApiKey = (req, res, next) => {
22
23
  };
23
24
 
24
25
  // JWT authentication middleware
25
- const authenticateToken = async (req, res, next) => {
26
+ const authenticateToken = async (req, res, next) => {
26
27
  // Platform mode: use single database user
27
28
  if (IS_PLATFORM) {
28
29
  try {
@@ -101,15 +102,28 @@ const authenticateToken = async (req, res, next) => {
101
102
  console.error('Token verification error:', error);
102
103
  return res.status(403).json({ error: 'Invalid token' });
103
104
  }
104
- };
105
+ };
106
+
107
+ const requireAdmin = (req, res, next) => {
108
+ if (!req.user) {
109
+ return res.status(401).json({ error: 'Access denied. No authenticated user.' });
110
+ }
111
+
112
+ if (!ADMIN_ROLES.has(req.user.role)) {
113
+ return res.status(403).json({ error: 'Admin access required.' });
114
+ }
115
+
116
+ next();
117
+ };
105
118
 
106
119
  // Generate JWT token
107
120
  const generateToken = (user) => {
108
121
  return jwt.sign(
109
122
  {
110
- userId: user.id,
111
- username: user.username
112
- },
123
+ userId: user.id,
124
+ username: user.username,
125
+ role: user.role || null,
126
+ },
113
127
  JWT_SECRET,
114
128
  { expiresIn: '7d' }
115
129
  );
@@ -120,10 +134,10 @@ const authenticateWebSocket = (token) => {
120
134
  // Platform mode: bypass token validation, return first user
121
135
  if (IS_PLATFORM) {
122
136
  try {
123
- const user = userDb.getFirstUser();
124
- if (user) {
125
- return { id: user.id, userId: user.id, username: user.username };
126
- }
137
+ const user = userDb.getFirstUser();
138
+ if (user) {
139
+ return { id: user.id, userId: user.id, username: user.username, role: user.role || null };
140
+ }
127
141
  return null;
128
142
  } catch (error) {
129
143
  console.error('Platform mode WebSocket error:', error);
@@ -143,9 +157,9 @@ const authenticateWebSocket = (token) => {
143
157
 
144
158
  if (isPixcodeApiKey(token)) {
145
159
  try {
146
- const user = apiKeysDb.validateApiKey(token);
147
- if (!user) return null;
148
- return { userId: user.id, username: user.username };
160
+ const user = apiKeysDb.validateApiKey(token);
161
+ if (!user) return null;
162
+ return { id: user.id, userId: user.id, username: user.username, role: user.role || null };
149
163
  } catch (error) {
150
164
  console.error('WebSocket API key validation error:', error);
151
165
  return null;
@@ -159,7 +173,7 @@ const authenticateWebSocket = (token) => {
159
173
  if (!user) {
160
174
  return null;
161
175
  }
162
- return { userId: user.id, username: user.username };
176
+ return { id: user.id, userId: user.id, username: user.username, role: user.role || null };
163
177
  } catch (error) {
164
178
  console.error('WebSocket token verification error:', error);
165
179
  return null;
@@ -167,9 +181,10 @@ const authenticateWebSocket = (token) => {
167
181
  };
168
182
 
169
183
  export {
170
- validateApiKey,
171
- authenticateToken,
172
- generateToken,
184
+ validateApiKey,
185
+ authenticateToken,
186
+ requireAdmin,
187
+ generateToken,
173
188
  authenticateWebSocket,
174
189
  JWT_SECRET
175
190
  };
@@ -12,7 +12,15 @@ import {
12
12
  saveRemoteConnectionConfig,
13
13
  } from '../services/remote-connection.js';
14
14
 
15
- const router = express.Router();
15
+ const router = express.Router();
16
+
17
+ function publicUser(user) {
18
+ return {
19
+ id: user.id,
20
+ username: user.username,
21
+ role: user.role || 'member',
22
+ };
23
+ }
16
24
 
17
25
  // Check auth status and setup requirements
18
26
  router.get('/status', async (req, res) => {
@@ -60,19 +68,19 @@ router.post('/register', async (req, res) => {
60
68
  // Use a transaction to prevent race conditions
61
69
  db.prepare('BEGIN').run();
62
70
  try {
63
- // Check if users already exist (only allow one user)
64
- const hasUsers = userDb.hasUsers();
65
- if (hasUsers) {
66
- db.prepare('ROLLBACK').run();
67
- return res.status(403).json({ error: 'User already exists. This is a single-user system.' });
68
- }
71
+ // Check if users already exist. Additional accounts are created by admins.
72
+ const hasUsers = userDb.hasUsers();
73
+ if (hasUsers) {
74
+ db.prepare('ROLLBACK').run();
75
+ return res.status(403).json({ error: 'Initial admin already exists. Ask an admin to create another account.' });
76
+ }
69
77
 
70
78
  // Hash password
71
79
  const saltRounds = 12;
72
80
  const passwordHash = await bcrypt.hash(password, saltRounds);
73
81
 
74
82
  // Create user
75
- const user = userDb.createUser(username, passwordHash);
83
+ const user = userDb.createUser(username, passwordHash, { role: 'admin' });
76
84
 
77
85
  // Generate token
78
86
  const token = generateToken(user);
@@ -83,10 +91,10 @@ router.post('/register', async (req, res) => {
83
91
  userDb.updateLastLogin(user.id);
84
92
 
85
93
  res.json({
86
- success: true,
87
- user: { id: user.id, username: user.username },
88
- token
89
- });
94
+ success: true,
95
+ user: publicUser(user),
96
+ token
97
+ });
90
98
  } catch (error) {
91
99
  db.prepare('ROLLBACK').run();
92
100
  throw error;
@@ -130,11 +138,11 @@ router.post('/login', async (req, res) => {
130
138
  // Update last login
131
139
  userDb.updateLastLogin(user.id);
132
140
 
133
- res.json({
134
- success: true,
135
- user: { id: user.id, username: user.username },
136
- token
137
- });
141
+ res.json({
142
+ success: true,
143
+ user: publicUser(user),
144
+ token
145
+ });
138
146
 
139
147
  } catch (error) {
140
148
  console.error('Login error:', error);
@@ -4,16 +4,17 @@ import { promises as fs } from 'fs';
4
4
 
5
5
  import express from 'express';
6
6
 
7
- import { extractProjectDirectory } from '../projects.js';
8
- import { queryClaudeSDK } from '../claude-sdk.js';
9
- import { spawnCursor } from '../cursor-cli.js';
7
+ import { extractProjectDirectory } from '../projects.js';
8
+ import { queryClaudeSDK } from '../claude-sdk.js';
9
+ import { spawnCursor } from '../cursor-cli.js';
10
+ import { userHasProjectAccess } from '../services/platformization.js';
10
11
 
11
- const router = express.Router();
12
+ const router = express.Router();
12
13
  const COMMIT_DIFF_CHARACTER_LIMIT = 500_000;
13
14
  const FILESYSTEM_SCAN_MAX_FILES = 5_000;
14
15
  const FILESYSTEM_SCAN_MAX_DEPTH = 10;
15
- const filesystemChangeSnapshots = new Map();
16
- const FILESYSTEM_SCAN_EXCLUDED_DIRS = new Set([
16
+ const filesystemChangeSnapshots = new Map();
17
+ const FILESYSTEM_SCAN_EXCLUDED_DIRS = new Set([
17
18
  '.git',
18
19
  '.hg',
19
20
  '.svn',
@@ -28,9 +29,23 @@ const FILESYSTEM_SCAN_EXCLUDED_DIRS = new Set([
28
29
  '.turbo',
29
30
  '.cache',
30
31
  '.pixcode-dev',
31
- ]);
32
-
33
- function isNotGitRepositoryMessage(message = '') {
32
+ ]);
33
+
34
+ router.use((req, res, next) => {
35
+ const project = req.query.project || req.body?.project;
36
+ if (!project) {
37
+ return next();
38
+ }
39
+
40
+ const capability = req.method === 'GET' ? 'viewFiles' : 'editFiles';
41
+ if (!userHasProjectAccess(req.user, { name: String(project), projectName: String(project) }, capability)) {
42
+ return res.status(403).json({ error: 'Project access denied.' });
43
+ }
44
+
45
+ next();
46
+ });
47
+
48
+ function isNotGitRepositoryMessage(message = '') {
34
49
  return message.includes('Not a git repository')
35
50
  || message.includes('not a git repository')
36
51
  || message.includes('Project directory is not a git repository');
@@ -1,6 +1,7 @@
1
- import express from 'express';
2
-
3
- import {
1
+ import express from 'express';
2
+
3
+ import { requireAdmin } from '../middleware/auth.js';
4
+ import {
4
5
  checkRemoteAccessHealth,
5
6
  createAdminUser,
6
7
  createEvaluationRun,
@@ -66,20 +67,20 @@ router.patch('/team/members/:id', (req, res) => {
66
67
  res.json({ success: true, member });
67
68
  });
68
69
 
69
- router.get('/admin/users', (_req, res) => {
70
- res.json({ success: true, users: getPlatformizationState().adminUsers });
71
- });
72
-
73
- router.post('/admin/users', async (req, res) => {
74
- try {
75
- res.status(201).json({ success: true, user: await createAdminUser(req.body || {}, userId(req)) });
76
- } catch (error) {
70
+ router.get('/admin/users', requireAdmin, (_req, res) => {
71
+ res.json({ success: true, users: getPlatformizationState().adminUsers });
72
+ });
73
+
74
+ router.post('/admin/users', requireAdmin, async (req, res) => {
75
+ try {
76
+ res.status(201).json({ success: true, user: await createAdminUser(req.body || {}, userId(req)) });
77
+ } catch (error) {
77
78
  handleError(res, error);
78
79
  }
79
80
  });
80
81
 
81
- router.patch('/admin/users/:id', (req, res) => {
82
- const user = updateAdminUser(req.params.id, req.body || {}, userId(req));
82
+ router.patch('/admin/users/:id', requireAdmin, (req, res) => {
83
+ const user = updateAdminUser(req.params.id, req.body || {}, userId(req));
83
84
  if (!user) {
84
85
  res.status(404).json({ success: false, error: 'Admin user not found.' });
85
86
  return;
@@ -87,20 +88,20 @@ router.patch('/admin/users/:id', (req, res) => {
87
88
  res.json({ success: true, user });
88
89
  });
89
90
 
90
- router.get('/project-collaborators', (_req, res) => {
91
- res.json({ success: true, collaborators: getPlatformizationState().projectCollaborators });
92
- });
93
-
94
- router.post('/project-collaborators', (req, res) => {
95
- try {
91
+ router.get('/project-collaborators', requireAdmin, (_req, res) => {
92
+ res.json({ success: true, collaborators: getPlatformizationState().projectCollaborators });
93
+ });
94
+
95
+ router.post('/project-collaborators', requireAdmin, (req, res) => {
96
+ try {
96
97
  res.status(201).json({ success: true, collaborator: createProjectCollaborator(req.body || {}, userId(req)) });
97
98
  } catch (error) {
98
99
  handleError(res, error);
99
100
  }
100
101
  });
101
102
 
102
- router.patch('/project-collaborators/:id', (req, res) => {
103
- const collaborator = updateProjectCollaborator(req.params.id, req.body || {}, userId(req));
103
+ router.patch('/project-collaborators/:id', requireAdmin, (req, res) => {
104
+ const collaborator = updateProjectCollaborator(req.params.id, req.body || {}, userId(req));
104
105
  if (!collaborator) {
105
106
  res.status(404).json({ success: false, error: 'Project collaborator not found.' });
106
107
  return;
@@ -116,10 +116,14 @@ function writeStore(store) {
116
116
  appConfigDb.set(CONFIG_KEY, JSON.stringify(store));
117
117
  }
118
118
 
119
- function compact(text, max = 120) {
120
- const value = String(text || '').replace(/\s+/g, ' ').trim();
121
- return value.length > max ? value.slice(0, max).replace(/[-_\s]+$/g, '') : value;
122
- }
119
+ function compact(text, max = 120) {
120
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
121
+ return value.length > max ? value.slice(0, max).replace(/[-_\s]+$/g, '') : value;
122
+ }
123
+
124
+ function compactProjectIdentifier(value) {
125
+ return String(value || '').replace(/\s+/g, ' ').trim();
126
+ }
123
127
 
124
128
  function slugify(value) {
125
129
  const slug = compact(value, 72)
@@ -140,9 +144,64 @@ function addAudit(store, action, actorId, details = {}) {
140
144
  store.auditLog = store.auditLog.slice(0, 250);
141
145
  }
142
146
 
143
- function normalizeRole(role) {
144
- return TEAM_ROLES[role] ? role : 'viewer';
145
- }
147
+ function normalizeRole(role) {
148
+ return TEAM_ROLES[role] ? role : 'viewer';
149
+ }
150
+
151
+ export function isAdminUser(user = {}) {
152
+ return user?.role === 'admin' || user?.role === 'owner';
153
+ }
154
+
155
+ function resolveUser(input = {}) {
156
+ const users = userDb.listUsers();
157
+ const userId = Number(input.userId);
158
+ if (Number.isFinite(userId)) {
159
+ return users.find((user) => user.id === userId && user.is_active) || null;
160
+ }
161
+
162
+ const userRef = compact(input.userRef || input.email || input.username || '').toLowerCase();
163
+ if (!userRef) return null;
164
+ return users.find((user) => user.is_active && String(user.username).toLowerCase() === userRef) || null;
165
+ }
166
+
167
+ function projectMatches(collaborator, project = {}) {
168
+ const projectName = compactProjectIdentifier(project.name || project.projectName || project);
169
+ const projectPath = compactProjectIdentifier(project.fullPath || project.path || project.projectPath || '');
170
+
171
+ return Boolean(
172
+ (projectName && collaborator.projectName === projectName) ||
173
+ (projectPath && collaborator.projectPath === projectPath)
174
+ );
175
+ }
176
+
177
+ export function userHasProjectAccess(user, project, capability = 'viewFiles') {
178
+ if (isAdminUser(user)) return true;
179
+ if (!user?.id && !user?.userId) return false;
180
+
181
+ const userId = Number(user.id ?? user.userId);
182
+ const username = String(user.username || '').toLowerCase();
183
+ const store = readStore();
184
+
185
+ return store.projectCollaborators.some((collaborator) => {
186
+ if (collaborator.status === 'disabled') return false;
187
+ if (!projectMatches(collaborator, project)) return false;
188
+
189
+ const sameUser = Number(collaborator.userId) === userId ||
190
+ String(collaborator.userRef || '').toLowerCase() === username;
191
+ if (!sameUser) return false;
192
+
193
+ if (capability === 'viewFiles') {
194
+ return collaborator.capabilities?.viewFiles !== false;
195
+ }
196
+
197
+ return collaborator.capabilities?.[capability] === true;
198
+ });
199
+ }
200
+
201
+ export function filterProjectsForUser(projects = [], user) {
202
+ if (isAdminUser(user)) return projects;
203
+ return projects.filter((project) => userHasProjectAccess(user, project, 'viewFiles'));
204
+ }
146
205
 
147
206
  function normalizeScope(scope) {
148
207
  return SECRET_SCOPES.includes(scope) ? scope : 'project';
@@ -338,13 +397,18 @@ export function updateAdminUser(userId, patch = {}, actorId = null) {
338
397
  };
339
398
  }
340
399
 
341
- export function createProjectCollaborator(input = {}, actorId = null) {
342
- const projectName = compact(input.projectName || input.project || '');
343
- const projectPath = input.projectPath || null;
344
- const userRef = compact(input.userRef || input.email || input.username || '');
345
- if (!projectName || !userRef) {
346
- throw new Error('Project collaborator requires a project name and user reference.');
347
- }
400
+ export function createProjectCollaborator(input = {}, actorId = null) {
401
+ const projectName = compactProjectIdentifier(input.projectName || input.project || '');
402
+ const projectPath = input.projectPath || null;
403
+ const targetUser = resolveUser(input);
404
+ const userRef = compact(input.userRef || input.email || input.username || targetUser?.username || '');
405
+ if (!projectName || !userRef) {
406
+ throw new Error('Project collaborator requires a project name and user reference.');
407
+ }
408
+
409
+ if (!targetUser) {
410
+ throw new Error('Create the user account before assigning project access.');
411
+ }
348
412
 
349
413
  const role = ['partner', 'worker', 'reviewer', 'viewer'].includes(input.role) ? input.role : 'worker';
350
414
  const capabilities = {
@@ -357,10 +421,11 @@ export function createProjectCollaborator(input = {}, actorId = null) {
357
421
  manageProjectSettings: role === 'partner',
358
422
  };
359
423
  const collaborator = {
360
- id: crypto.randomUUID(),
361
- projectName,
362
- projectPath,
363
- userRef,
424
+ id: crypto.randomUUID(),
425
+ projectName,
426
+ projectPath,
427
+ userId: targetUser.id,
428
+ userRef,
364
429
  role,
365
430
  capabilities: {
366
431
  ...capabilities,