@pixelbyte-software/pixcode 1.51.4 → 1.51.6

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 (42) hide show
  1. package/dist/assets/index-Bbmw_Gy1.css +32 -0
  2. package/dist/assets/{index-HfGHXhD6.js → index-DodOzOl5.js} +182 -184
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/cli.js +1 -1
  5. package/dist-server/server/cli.js.map +1 -1
  6. package/dist-server/server/index.js +530 -34
  7. package/dist-server/server/index.js.map +1 -1
  8. package/dist-server/server/middleware/auth.js +20 -3
  9. package/dist-server/server/middleware/auth.js.map +1 -1
  10. package/dist-server/server/modules/providers/provider.routes.js +33 -17
  11. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  12. package/dist-server/server/routes/commands.js +20 -9
  13. package/dist-server/server/routes/commands.js.map +1 -1
  14. package/dist-server/server/routes/live-view.js +30 -7
  15. package/dist-server/server/routes/live-view.js.map +1 -1
  16. package/dist-server/server/routes/messages.js +30 -0
  17. package/dist-server/server/routes/messages.js.map +1 -1
  18. package/dist-server/server/routes/network.js +3 -2
  19. package/dist-server/server/routes/network.js.map +1 -1
  20. package/dist-server/server/routes/platformization.js +17 -1
  21. package/dist-server/server/routes/platformization.js.map +1 -1
  22. package/dist-server/server/routes/projects.js +4 -3
  23. package/dist-server/server/routes/projects.js.map +1 -1
  24. package/dist-server/server/routes/remote.js +2 -1
  25. package/dist-server/server/routes/remote.js.map +1 -1
  26. package/dist-server/server/services/platformization.js +168 -1
  27. package/dist-server/server/services/platformization.js.map +1 -1
  28. package/package.json +1 -1
  29. package/scripts/update-git-install.mjs +15 -10
  30. package/server/cli.js +4 -4
  31. package/server/index.js +552 -34
  32. package/server/middleware/auth.js +26 -2
  33. package/server/modules/providers/provider.routes.ts +91 -53
  34. package/server/routes/commands.js +43 -32
  35. package/server/routes/live-view.js +47 -24
  36. package/server/routes/messages.js +47 -12
  37. package/server/routes/network.js +9 -8
  38. package/server/routes/platformization.js +27 -9
  39. package/server/routes/projects.js +7 -6
  40. package/server/routes/remote.js +6 -5
  41. package/server/services/platformization.js +196 -24
  42. package/dist/assets/index-B9N-gfOQ.css +0 -32
@@ -12,10 +12,12 @@ import {
12
12
  createTeamMember,
13
13
  detectTailscaleStatus,
14
14
  exportAuditLog,
15
- getAuditLog,
16
- getPlatformizationState,
17
- getRemoteAccessState,
18
- listSecrets,
15
+ getAuditLog,
16
+ getPlatformizationState,
17
+ getRemoteAccessState,
18
+ installTailscale,
19
+ loginTailscale,
20
+ listSecrets,
19
21
  materializeScopedEnv,
20
22
  recordUsageEvent,
21
23
  saveRemoteAccessConfig,
@@ -187,11 +189,27 @@ router.post('/remote-access/configs', (req, res) => {
187
189
  }
188
190
  });
189
191
 
190
- router.get('/remote-access/tailscale', async (_req, res) => {
191
- res.json({ success: true, tailscale: await detectTailscaleStatus() });
192
- });
193
-
194
- router.post('/remote-access/health', async (req, res) => {
192
+ router.get('/remote-access/tailscale', async (_req, res) => {
193
+ res.json({ success: true, tailscale: await detectTailscaleStatus() });
194
+ });
195
+
196
+ router.post('/remote-access/tailscale/install', async (req, res) => {
197
+ try {
198
+ res.json({ success: true, result: await installTailscale(userId(req)) });
199
+ } catch (error) {
200
+ handleError(res, error);
201
+ }
202
+ });
203
+
204
+ router.post('/remote-access/tailscale/login', async (req, res) => {
205
+ try {
206
+ res.json({ success: true, result: await loginTailscale(userId(req)) });
207
+ } catch (error) {
208
+ handleError(res, error);
209
+ }
210
+ });
211
+
212
+ router.post('/remote-access/health', async (req, res) => {
195
213
  try {
196
214
  res.json({ success: true, health: await checkRemoteAccessHealth(req.body || {}, userId(req)) });
197
215
  } catch (error) {
@@ -3,9 +3,10 @@ import path from 'path';
3
3
  import { spawn } from 'child_process';
4
4
  import os from 'os';
5
5
 
6
- import express from 'express';
7
-
8
- import { addProjectManually, extractProjectDirectory } from '../projects.js';
6
+ import express from 'express';
7
+
8
+ import { addProjectManually, extractProjectDirectory } from '../projects.js';
9
+ import { requireAdmin } from '../middleware/auth.js';
9
10
 
10
11
  const router = express.Router();
11
12
 
@@ -302,7 +303,7 @@ async function projectHasAnySessions(workspacePath) {
302
303
  * composer and surface a "directory deleted" warning instead of letting
303
304
  * the user fire prompts into a void.
304
305
  */
305
- router.get('/:projectName/dir-status', async (req, res) => {
306
+ router.get('/:projectName/dir-status', requireAdmin, async (req, res) => {
306
307
  const { projectName } = req.params;
307
308
  try {
308
309
  const actualPath = await extractProjectDirectory(projectName);
@@ -340,7 +341,7 @@ router.get('/:projectName/dir-status', async (req, res) => {
340
341
  * matches ChatGPT's "New chat" which reuses the empty canvas until the
341
342
  * user actually commits a message.
342
343
  */
343
- router.post('/quick-start', async (req, res) => {
344
+ router.post('/quick-start', requireAdmin, async (req, res) => {
344
345
  try {
345
346
  await fs.mkdir(WORKSPACES_BASE, { recursive: true });
346
347
 
@@ -424,7 +425,7 @@ router.post('/quick-start', async (req, res) => {
424
425
  * - githubTokenId?: number (optional, ID of stored token)
425
426
  * - newGithubToken?: string (optional, one-time token)
426
427
  */
427
- router.post('/create-workspace', async (req, res) => {
428
+ router.post('/create-workspace', requireAdmin, async (req, res) => {
428
429
  try {
429
430
  const { workspaceType, path: workspacePath, githubUrl, githubTokenId, newGithubToken, subfolderName } = req.body;
430
431
 
@@ -5,10 +5,11 @@ import {
5
5
  getPublicRemoteConnectionConfig,
6
6
  saveRemoteConnectionConfig,
7
7
  } from '../services/remote-connection.js';
8
- import {
9
- buildControlRoomSnapshot,
10
- buildMobileConsoleLayout,
11
- } from '../services/control-room.js';
8
+ import {
9
+ buildControlRoomSnapshot,
10
+ buildMobileConsoleLayout,
11
+ } from '../services/control-room.js';
12
+ import { requireAdmin } from '../middleware/auth.js';
12
13
 
13
14
  const router = express.Router();
14
15
 
@@ -16,7 +17,7 @@ router.get('/config', (req, res) => {
16
17
  res.json({ success: true, connection: getPublicRemoteConnectionConfig() });
17
18
  });
18
19
 
19
- router.put('/config', (req, res) => {
20
+ router.put('/config', requireAdmin, (req, res) => {
20
21
  try {
21
22
  const connection = saveRemoteConnectionConfig(req.body || {});
22
23
  res.json({ success: true, connection });
@@ -1,6 +1,7 @@
1
- import crypto from 'node:crypto';
2
- import os from 'node:os';
3
- import { execFile } from 'node:child_process';
1
+ import crypto from 'node:crypto';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { execFile, spawn } from 'node:child_process';
4
5
  import { promisify } from 'node:util';
5
6
 
6
7
  import bcrypt from 'bcryptjs';
@@ -8,7 +9,7 @@ import bcrypt from 'bcryptjs';
8
9
  import { appConfigDb, userDb } from '../database/db.js';
9
10
 
10
11
  const CONFIG_KEY = 'platformization';
11
- const execFileAsync = promisify(execFile);
12
+ const execFileAsync = promisify(execFile);
12
13
 
13
14
  export const TEAM_ROLES = {
14
15
  owner: [
@@ -174,6 +175,25 @@ function projectMatches(collaborator, project = {}) {
174
175
  );
175
176
  }
176
177
 
178
+ function isPathInside(basePath, targetPath) {
179
+ const relative = path.relative(path.resolve(basePath), path.resolve(targetPath));
180
+ return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
181
+ }
182
+
183
+ function normalizeAllowedRoots(input) {
184
+ const roots = Array.isArray(input) ? input : [];
185
+ const normalized = roots
186
+ .filter((entry) => typeof entry === 'string')
187
+ .map((entry) => entry.trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/g, ''))
188
+ .map((entry) => entry || '.')
189
+ .filter((entry) => !entry.includes('..'));
190
+ return Array.from(new Set(normalized.length > 0 ? normalized : ['.']));
191
+ }
192
+
193
+ function collaboratorAllowedRoots(collaborator) {
194
+ return normalizeAllowedRoots(collaborator.allowedRoots || collaborator.allowedFolders || ['.']);
195
+ }
196
+
177
197
  export function userHasProjectAccess(user, project, capability = 'viewFiles') {
178
198
  if (isAdminUser(user)) return true;
179
199
  if (!user?.id && !user?.userId) return false;
@@ -198,6 +218,55 @@ export function userHasProjectAccess(user, project, capability = 'viewFiles') {
198
218
  });
199
219
  }
200
220
 
221
+ export function getProjectAccessForUser(user, project, capability = 'viewFiles') {
222
+ if (isAdminUser(user)) {
223
+ return { unrestricted: true, allowedRoots: ['.'] };
224
+ }
225
+ if (!user?.id && !user?.userId) return { unrestricted: false, allowedRoots: [] };
226
+
227
+ const userId = Number(user.id ?? user.userId);
228
+ const username = String(user.username || '').toLowerCase();
229
+ const store = readStore();
230
+ const allowedRoots = [];
231
+
232
+ for (const collaborator of store.projectCollaborators) {
233
+ if (collaborator.status === 'disabled') continue;
234
+ if (!projectMatches(collaborator, project)) continue;
235
+ const sameUser = Number(collaborator.userId) === userId ||
236
+ String(collaborator.userRef || '').toLowerCase() === username;
237
+ if (!sameUser) continue;
238
+ const capabilityAllowed = capability === 'viewFiles'
239
+ ? collaborator.capabilities?.viewFiles !== false
240
+ : collaborator.capabilities?.[capability] === true;
241
+ if (!capabilityAllowed) continue;
242
+ allowedRoots.push(...collaboratorAllowedRoots(collaborator));
243
+ }
244
+
245
+ return { unrestricted: false, allowedRoots: Array.from(new Set(allowedRoots)) };
246
+ }
247
+
248
+ export function userHasProjectPathAccess(user, project, targetPath, capability = 'viewFiles') {
249
+ if (isAdminUser(user)) return true;
250
+ const projectPath = project?.fullPath || project?.path || project?.projectPath;
251
+ if (!projectPath || !targetPath) return false;
252
+ const access = getProjectAccessForUser(user, project, capability);
253
+ return access.allowedRoots.some((root) => {
254
+ const allowedPath = root === '.' ? projectPath : path.resolve(projectPath, root);
255
+ return isPathInside(allowedPath, targetPath);
256
+ });
257
+ }
258
+
259
+ export function filterFileTreeForUser(files = [], user, project, capability = 'viewFiles') {
260
+ if (isAdminUser(user)) return files;
261
+ const projectPath = project?.fullPath || project?.path || project?.projectPath;
262
+ if (!projectPath) return [];
263
+ return files.filter((entry) => {
264
+ const entryPath = entry?.path || entry?.fullPath || entry?.relativePath || '';
265
+ const absoluteEntryPath = path.isAbsolute(entryPath) ? entryPath : path.resolve(projectPath, entryPath);
266
+ return userHasProjectPathAccess(user, { ...project, fullPath: projectPath }, absoluteEntryPath, capability);
267
+ });
268
+ }
269
+
201
270
  export function filterProjectsForUser(projects = [], user) {
202
271
  if (isAdminUser(user)) return projects;
203
272
  return projects.filter((project) => userHasProjectAccess(user, project, 'viewFiles'));
@@ -427,11 +496,12 @@ export function createProjectCollaborator(input = {}, actorId = null) {
427
496
  userId: targetUser.id,
428
497
  userRef,
429
498
  role,
430
- capabilities: {
431
- ...capabilities,
432
- ...(input.capabilities && typeof input.capabilities === 'object' ? input.capabilities : {}),
433
- },
434
- status: input.status || 'active',
499
+ capabilities: {
500
+ ...capabilities,
501
+ ...(input.capabilities && typeof input.capabilities === 'object' ? input.capabilities : {}),
502
+ },
503
+ allowedRoots: normalizeAllowedRoots(input.allowedRoots || input.allowedFolders || ['.']),
504
+ status: input.status || 'active',
435
505
  createdAt: nowIso(),
436
506
  updatedAt: nowIso(),
437
507
  };
@@ -451,11 +521,14 @@ export function updateProjectCollaborator(collaboratorId, patch = {}, actorId =
451
521
  ...collaborator,
452
522
  ...patch,
453
523
  id: collaborator.id,
454
- capabilities: {
455
- ...collaborator.capabilities,
456
- ...(patch.capabilities && typeof patch.capabilities === 'object' ? patch.capabilities : {}),
457
- },
458
- updatedAt: nowIso(),
524
+ capabilities: {
525
+ ...collaborator.capabilities,
526
+ ...(patch.capabilities && typeof patch.capabilities === 'object' ? patch.capabilities : {}),
527
+ },
528
+ allowedRoots: patch.allowedRoots || patch.allowedFolders
529
+ ? normalizeAllowedRoots(patch.allowedRoots || patch.allowedFolders)
530
+ : collaboratorAllowedRoots(collaborator),
531
+ updatedAt: nowIso(),
459
532
  };
460
533
  return updated;
461
534
  });
@@ -730,9 +803,66 @@ export function exportAuditLog(format = 'json', filters = {}) {
730
803
  return JSON.stringify(entries, null, 2);
731
804
  }
732
805
 
733
- function normalizeAccessMode(mode) {
734
- return ['lan', 'tailscale', 'cloudflare_tunnel', 'custom_domain'].includes(mode) ? mode : 'lan';
735
- }
806
+ function normalizeAccessMode(mode) {
807
+ return ['lan', 'tailscale', 'cloudflare_tunnel', 'custom_domain'].includes(mode) ? mode : 'lan';
808
+ }
809
+
810
+ function resolveTailscaleInstallPlan() {
811
+ const platform = os.platform();
812
+ if (platform === 'darwin') {
813
+ return {
814
+ platform,
815
+ command: 'brew',
816
+ args: ['install', 'tailscale'],
817
+ displayCommand: 'brew install tailscale',
818
+ docsUrl: 'https://tailscale.com/download/mac',
819
+ note: 'If Homebrew is not installed, open the download page and install the macOS app.',
820
+ };
821
+ }
822
+ if (platform === 'win32') {
823
+ return {
824
+ platform,
825
+ command: 'winget',
826
+ args: ['install', '--id', 'Tailscale.Tailscale', '-e', '--silent'],
827
+ displayCommand: 'winget install --id Tailscale.Tailscale -e --silent',
828
+ docsUrl: 'https://tailscale.com/download/windows',
829
+ note: 'If winget is unavailable, install from the Tailscale download page.',
830
+ };
831
+ }
832
+ return {
833
+ platform,
834
+ command: 'sh',
835
+ args: ['-c', 'curl -fsSL https://tailscale.com/install.sh | sh'],
836
+ displayCommand: 'curl -fsSL https://tailscale.com/install.sh | sh',
837
+ docsUrl: 'https://tailscale.com/download/linux',
838
+ note: 'Linux install may require root privileges. If this fails, run the command with sudo in a terminal.',
839
+ };
840
+ }
841
+
842
+ function extractFirstUrl(text = '') {
843
+ return String(text).match(/https?:\/\/[^\s]+/i)?.[0] || null;
844
+ }
845
+
846
+ function runTailscaleCommand(command, args, options = {}) {
847
+ return new Promise((resolve) => {
848
+ const child = spawn(command, args, {
849
+ shell: false,
850
+ windowsHide: true,
851
+ env: process.env,
852
+ ...options,
853
+ });
854
+ let stdout = '';
855
+ let stderr = '';
856
+ child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
857
+ child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); });
858
+ child.on('error', (error) => {
859
+ resolve({ ok: false, code: null, stdout, stderr, error: error.message });
860
+ });
861
+ child.on('close', (code) => {
862
+ resolve({ ok: code === 0, code, stdout, stderr, error: code === 0 ? null : `${command} exited with code ${code}` });
863
+ });
864
+ });
865
+ }
736
866
 
737
867
  function normalizePublicUrl(value) {
738
868
  const raw = typeof value === 'string' ? value.trim() : '';
@@ -799,8 +929,9 @@ export function getRemoteAccessState() {
799
929
  };
800
930
  }
801
931
 
802
- export async function detectTailscaleStatus() {
803
- try {
932
+ export async function detectTailscaleStatus() {
933
+ const installPlan = resolveTailscaleInstallPlan();
934
+ try {
804
935
  const { stdout } = await execFileAsync('tailscale', ['status', '--json'], { timeout: 5000 });
805
936
  const status = JSON.parse(stdout || '{}');
806
937
  const self = status.Self || {};
@@ -813,7 +944,8 @@ export async function detectTailscaleStatus() {
813
944
  magicDnsName: self.DNSName || null,
814
945
  tailscaleIp: tailscaleIps[0] || null,
815
946
  pixcodeUrl: tailscaleIps[0] ? `http://${tailscaleIps[0]}:${process.env.SERVER_PORT || 3001}` : null,
816
- installUrl: 'https://tailscale.com/download',
947
+ installUrl: 'https://tailscale.com/download',
948
+ installPlan,
817
949
  checkedAt: nowIso(),
818
950
  message: tailscaleIps[0] ? 'Tailscale is ready for private Pixcode access.' : 'Tailscale CLI is installed but no device IP was detected.',
819
951
  };
@@ -827,14 +959,54 @@ export async function detectTailscaleStatus() {
827
959
  magicDnsName: null,
828
960
  tailscaleIp: null,
829
961
  pixcodeUrl: null,
830
- installUrl: 'https://tailscale.com/download',
962
+ installUrl: 'https://tailscale.com/download',
963
+ installPlan,
831
964
  checkedAt: nowIso(),
832
965
  message: isMissing
833
966
  ? 'Tailscale is optional. Use the LAN links now, or install Tailscale from Settings > Access for private team access without a public domain.'
834
967
  : (error?.message || 'Tailscale status could not be read.'),
835
- };
836
- }
837
- }
968
+ };
969
+ }
970
+ }
971
+
972
+ export async function installTailscale(actorId = null) {
973
+ const plan = resolveTailscaleInstallPlan();
974
+ const result = await runTailscaleCommand(plan.command, plan.args);
975
+ const store = readStore();
976
+ addAudit(store, 'remote.access.tailscale.install', actorId, {
977
+ platform: plan.platform,
978
+ ok: result.ok,
979
+ command: plan.displayCommand,
980
+ });
981
+ writeStore(store);
982
+ return {
983
+ ...result,
984
+ plan,
985
+ message: result.ok
986
+ ? 'Tailscale install command completed. Run login/connect next.'
987
+ : `Install command failed. ${plan.note}`,
988
+ };
989
+ }
990
+
991
+ export async function loginTailscale(actorId = null) {
992
+ const result = await runTailscaleCommand('tailscale', ['up']);
993
+ const combinedOutput = `${result.stdout || ''}\n${result.stderr || ''}`;
994
+ const authUrl = extractFirstUrl(combinedOutput);
995
+ const store = readStore();
996
+ addAudit(store, 'remote.access.tailscale.login', actorId, {
997
+ ok: result.ok,
998
+ authUrl: Boolean(authUrl),
999
+ });
1000
+ writeStore(store);
1001
+ return {
1002
+ ...result,
1003
+ authUrl,
1004
+ message: result.ok
1005
+ ? 'Tailscale is connected.'
1006
+ : (authUrl ? 'Open the login URL to finish connecting this device.' : 'Tailscale login command failed.'),
1007
+ tailscale: await detectTailscaleStatus(),
1008
+ };
1009
+ }
838
1010
 
839
1011
  export async function checkRemoteAccessHealth(input = {}, actorId = null) {
840
1012
  const url = normalizePublicUrl(input.url || input.remoteUrl || '');