@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.
- package/dist/assets/index-Bbmw_Gy1.css +32 -0
- package/dist/assets/{index-HfGHXhD6.js → index-DodOzOl5.js} +182 -184
- package/dist/index.html +2 -2
- package/dist-server/server/cli.js +1 -1
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/index.js +530 -34
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/auth.js +20 -3
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +33 -17
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/routes/commands.js +20 -9
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/live-view.js +30 -7
- package/dist-server/server/routes/live-view.js.map +1 -1
- package/dist-server/server/routes/messages.js +30 -0
- package/dist-server/server/routes/messages.js.map +1 -1
- package/dist-server/server/routes/network.js +3 -2
- package/dist-server/server/routes/network.js.map +1 -1
- package/dist-server/server/routes/platformization.js +17 -1
- package/dist-server/server/routes/platformization.js.map +1 -1
- package/dist-server/server/routes/projects.js +4 -3
- package/dist-server/server/routes/projects.js.map +1 -1
- package/dist-server/server/routes/remote.js +2 -1
- package/dist-server/server/routes/remote.js.map +1 -1
- package/dist-server/server/services/platformization.js +168 -1
- package/dist-server/server/services/platformization.js.map +1 -1
- package/package.json +1 -1
- package/scripts/update-git-install.mjs +15 -10
- package/server/cli.js +4 -4
- package/server/index.js +552 -34
- package/server/middleware/auth.js +26 -2
- package/server/modules/providers/provider.routes.ts +91 -53
- package/server/routes/commands.js +43 -32
- package/server/routes/live-view.js +47 -24
- package/server/routes/messages.js +47 -12
- package/server/routes/network.js +9 -8
- package/server/routes/platformization.js +27 -9
- package/server/routes/projects.js +7 -6
- package/server/routes/remote.js +6 -5
- package/server/services/platformization.js +196 -24
- 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
|
-
|
|
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/
|
|
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
|
|
package/server/routes/remote.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 || '');
|