@lamalibre/portlama-agent 1.0.5 → 1.0.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/package.json +1 -1
- package/src/commands/cp.js +2 -6
- package/src/commands/deploy.js +5 -25
- package/src/commands/setup.js +279 -3
- package/src/commands/shell-log.js +2 -4
- package/src/commands/shell-server.js +8 -1
- package/src/commands/shell.js +7 -0
- package/src/commands/sites.js +4 -4
- package/src/commands/status.js +1 -1
- package/src/commands/update.js +2 -6
- package/src/lib/config.js +18 -1
- package/src/lib/keychain.js +208 -0
- package/src/lib/panel-api.js +337 -104
package/package.json
CHANGED
package/src/commands/cp.js
CHANGED
|
@@ -96,9 +96,7 @@ async function runDownload(config, source, dest) {
|
|
|
96
96
|
|
|
97
97
|
try {
|
|
98
98
|
await downloadRemoteFile(
|
|
99
|
-
config
|
|
100
|
-
config.p12Path,
|
|
101
|
-
config.p12Password,
|
|
99
|
+
config,
|
|
102
100
|
agentLabel,
|
|
103
101
|
remotePath,
|
|
104
102
|
localPath,
|
|
@@ -152,9 +150,7 @@ async function runUpload(config, source, dest) {
|
|
|
152
150
|
|
|
153
151
|
try {
|
|
154
152
|
await uploadRemoteFile(
|
|
155
|
-
config
|
|
156
|
-
config.p12Path,
|
|
157
|
-
config.p12Password,
|
|
153
|
+
config,
|
|
158
154
|
agentLabel,
|
|
159
155
|
remotePath,
|
|
160
156
|
localPath,
|
package/src/commands/deploy.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function runDeploy(args) {
|
|
|
71
71
|
|
|
72
72
|
let data;
|
|
73
73
|
try {
|
|
74
|
-
data = await fetchSites(config
|
|
74
|
+
data = await fetchSites(config);
|
|
75
75
|
} catch (err) {
|
|
76
76
|
console.error(`\n ${chalk.red(`Failed to connect to panel: ${err.message}`)}\n`);
|
|
77
77
|
process.exit(1);
|
|
@@ -161,22 +161,10 @@ export async function runDeploy(args) {
|
|
|
161
161
|
{
|
|
162
162
|
title: 'Clearing remote files',
|
|
163
163
|
task: async (_ctx, task) => {
|
|
164
|
-
const { files: remoteFiles } = await fetchSiteFiles(
|
|
165
|
-
config.panelUrl,
|
|
166
|
-
config.p12Path,
|
|
167
|
-
config.p12Password,
|
|
168
|
-
siteId,
|
|
169
|
-
'.',
|
|
170
|
-
);
|
|
164
|
+
const { files: remoteFiles } = await fetchSiteFiles(config, siteId, '.');
|
|
171
165
|
for (const f of remoteFiles) {
|
|
172
166
|
task.output = `Removing ${f.name}`;
|
|
173
|
-
await deleteSiteFile(
|
|
174
|
-
config.panelUrl,
|
|
175
|
-
config.p12Path,
|
|
176
|
-
config.p12Password,
|
|
177
|
-
siteId,
|
|
178
|
-
f.name,
|
|
179
|
-
);
|
|
167
|
+
await deleteSiteFile(config, siteId, f.name);
|
|
180
168
|
}
|
|
181
169
|
},
|
|
182
170
|
rendererOptions: { persistentOutput: false },
|
|
@@ -200,9 +188,7 @@ export async function runDeploy(args) {
|
|
|
200
188
|
const batch = groupFiles.slice(i, i + 10);
|
|
201
189
|
const uploadDir = dir === '.' ? '.' : dir;
|
|
202
190
|
await uploadSiteFiles(
|
|
203
|
-
config
|
|
204
|
-
config.p12Path,
|
|
205
|
-
config.p12Password,
|
|
191
|
+
config,
|
|
206
192
|
siteId,
|
|
207
193
|
uploadDir,
|
|
208
194
|
batch.map((f) => f.absolutePath),
|
|
@@ -220,13 +206,7 @@ export async function runDeploy(args) {
|
|
|
220
206
|
// Count remote files recursively to match local recursive scan
|
|
221
207
|
let remoteCount = 0;
|
|
222
208
|
const countRemote = async (dirPath) => {
|
|
223
|
-
const { files: entries } = await fetchSiteFiles(
|
|
224
|
-
config.panelUrl,
|
|
225
|
-
config.p12Path,
|
|
226
|
-
config.p12Password,
|
|
227
|
-
siteId,
|
|
228
|
-
dirPath,
|
|
229
|
-
);
|
|
209
|
+
const { files: entries } = await fetchSiteFiles(config, siteId, dirPath);
|
|
230
210
|
for (const entry of entries) {
|
|
231
211
|
if (entry.type === 'directory') {
|
|
232
212
|
const subPath = dirPath === '.' ? entry.name : `${dirPath}/${entry.name}`;
|
package/src/commands/setup.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
+
import path from 'node:path';
|
|
5
6
|
import { Listr } from 'listr2';
|
|
6
7
|
import chalk from 'chalk';
|
|
7
|
-
import { assertMacOS, CHISEL_BIN_DIR, LOGS_DIR } from '../lib/platform.js';
|
|
8
|
+
import { assertMacOS, CHISEL_BIN_DIR, LOGS_DIR, AGENT_DIR } from '../lib/platform.js';
|
|
8
9
|
import { loadAgentConfig, saveAgentConfig } from '../lib/config.js';
|
|
9
|
-
import { fetchHealth, fetchPlist, fetchTunnels } from '../lib/panel-api.js';
|
|
10
|
+
import { fetchHealth, fetchPlist, fetchTunnels, curlPostUnauthenticated } from '../lib/panel-api.js';
|
|
10
11
|
import { extractPemFromP12, cleanupPemFiles } from '../lib/ws-helpers.js';
|
|
11
12
|
import { installChisel } from '../lib/chisel.js';
|
|
12
13
|
import { rewritePlist, writePlistFile } from '../lib/plist.js';
|
|
13
14
|
import { isAgentLoaded, unloadAgent, loadAgent, getAgentPid } from '../lib/launchctl.js';
|
|
15
|
+
import { generateKeypairAndCSR, importIdentityToKeychain, secureDelete } from '../lib/keychain.js';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Prompt for user input via readline.
|
|
@@ -34,10 +36,283 @@ function prompt(question, defaultValue) {
|
|
|
34
36
|
});
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Parse --token and --panel-url flags from argv.
|
|
41
|
+
* @returns {{ token?: string, panelUrl?: string }}
|
|
42
|
+
*/
|
|
43
|
+
function parseSetupFlags() {
|
|
44
|
+
const args = process.argv.slice(2);
|
|
45
|
+
const flags = {};
|
|
46
|
+
for (let i = 0; i < args.length; i++) {
|
|
47
|
+
if (args[i] === '--token' && args[i + 1]) {
|
|
48
|
+
flags.token = args[++i];
|
|
49
|
+
} else if (args[i] === '--panel-url' && args[i + 1]) {
|
|
50
|
+
flags.panelUrl = args[++i];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return flags;
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
/**
|
|
38
57
|
* Run the interactive setup flow.
|
|
58
|
+
* If --token is provided, uses the hardware-bound enrollment flow.
|
|
39
59
|
*/
|
|
40
60
|
export async function runSetup() {
|
|
61
|
+
const flags = parseSetupFlags();
|
|
62
|
+
|
|
63
|
+
if (flags.token) {
|
|
64
|
+
return runTokenSetup(flags);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return runP12Setup();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Hardware-bound enrollment flow using a one-time token.
|
|
72
|
+
* Generates a keypair locally, sends CSR to the panel, imports the signed
|
|
73
|
+
* certificate into macOS Keychain as a non-extractable identity.
|
|
74
|
+
*
|
|
75
|
+
* @param {{ token: string, panelUrl?: string }} flags
|
|
76
|
+
*/
|
|
77
|
+
async function runTokenSetup(flags) {
|
|
78
|
+
// Step 1: Verify macOS
|
|
79
|
+
assertMacOS();
|
|
80
|
+
|
|
81
|
+
// Check for existing config
|
|
82
|
+
const existingConfig = await loadAgentConfig();
|
|
83
|
+
if (existingConfig) {
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(chalk.yellow(' An existing agent configuration was found.'));
|
|
86
|
+
console.log(chalk.yellow(' Running setup again will overwrite it.'));
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(chalk.bold(' Portlama Agent Setup (Hardware-Bound Certificate)'));
|
|
92
|
+
console.log(chalk.dim(' Connect this Mac to your Portlama server using a Keychain-bound certificate.'));
|
|
93
|
+
console.log('');
|
|
94
|
+
|
|
95
|
+
let panelUrl = flags.panelUrl;
|
|
96
|
+
if (!panelUrl) {
|
|
97
|
+
panelUrl = await prompt('Panel URL (e.g. https://1.2.3.4:9292)', existingConfig?.panelUrl);
|
|
98
|
+
}
|
|
99
|
+
if (!panelUrl) {
|
|
100
|
+
throw new Error('Panel URL is required. Pass --panel-url <url> or enter interactively.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const normalizedUrl = panelUrl.replace(/\/+$/, '');
|
|
104
|
+
|
|
105
|
+
console.log('');
|
|
106
|
+
|
|
107
|
+
// Context shared across tasks
|
|
108
|
+
const ctx = {
|
|
109
|
+
panelUrl: normalizedUrl,
|
|
110
|
+
token: flags.token,
|
|
111
|
+
agentLabel: null,
|
|
112
|
+
keychainIdentity: null,
|
|
113
|
+
chiselVersion: null,
|
|
114
|
+
plistXml: null,
|
|
115
|
+
domain: null,
|
|
116
|
+
tunnels: [],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const tasks = new Listr(
|
|
120
|
+
[
|
|
121
|
+
{
|
|
122
|
+
title: 'Creating directories',
|
|
123
|
+
task: async () => {
|
|
124
|
+
await mkdir(AGENT_DIR, { recursive: true, mode: 0o700 });
|
|
125
|
+
await mkdir(CHISEL_BIN_DIR, { recursive: true });
|
|
126
|
+
await mkdir(LOGS_DIR, { recursive: true });
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
title: 'Generating keypair and CSR',
|
|
131
|
+
task: async (_ctx, task) => {
|
|
132
|
+
// We use a temporary label placeholder — the actual label comes from the token
|
|
133
|
+
// We'll pass 'pending' and fix after enrollment
|
|
134
|
+
ctx._keyData = await generateKeypairAndCSR('pending');
|
|
135
|
+
task.output = 'Keypair generated (4096-bit RSA)';
|
|
136
|
+
},
|
|
137
|
+
rendererOptions: { persistentOutput: true },
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
title: 'Enrolling with panel',
|
|
141
|
+
task: async (_ctx, task) => {
|
|
142
|
+
const enrollUrl = `${ctx.panelUrl}/api/enroll`;
|
|
143
|
+
const result = await curlPostUnauthenticated(enrollUrl, {
|
|
144
|
+
token: ctx.token,
|
|
145
|
+
csr: ctx._keyData.csrPem,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!result.ok) {
|
|
149
|
+
throw new Error(result.error || 'Enrollment failed');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
ctx.agentLabel = result.label;
|
|
153
|
+
ctx._certPem = result.cert;
|
|
154
|
+
ctx._caCertPem = result.caCert;
|
|
155
|
+
task.output = `Enrolled as "${result.label}" (serial: ${result.serial})`;
|
|
156
|
+
},
|
|
157
|
+
rendererOptions: { persistentOutput: true },
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
title: 'Importing certificate into Keychain',
|
|
161
|
+
task: async (_ctx, task) => {
|
|
162
|
+
// The server overrides the CSR subject with the correct CN=agent:<label>
|
|
163
|
+
// during signing, so the CSR placeholder subject doesn't matter.
|
|
164
|
+
const { identity } = await importIdentityToKeychain(
|
|
165
|
+
ctx._keyData.keyPath,
|
|
166
|
+
ctx._certPem,
|
|
167
|
+
ctx._caCertPem,
|
|
168
|
+
ctx.agentLabel,
|
|
169
|
+
console,
|
|
170
|
+
);
|
|
171
|
+
ctx.keychainIdentity = identity;
|
|
172
|
+
task.output = `Identity "${identity}" imported (non-extractable)`;
|
|
173
|
+
},
|
|
174
|
+
rendererOptions: { persistentOutput: true },
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
title: 'Saving CA certificate',
|
|
178
|
+
task: async () => {
|
|
179
|
+
const caPath = path.join(AGENT_DIR, 'ca.crt');
|
|
180
|
+
await writeFile(caPath, ctx._caCertPem, { mode: 0o644 });
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
title: 'Verifying panel connectivity',
|
|
185
|
+
task: async (_ctx, task) => {
|
|
186
|
+
const config = {
|
|
187
|
+
panelUrl: ctx.panelUrl,
|
|
188
|
+
authMethod: 'keychain',
|
|
189
|
+
keychainIdentity: ctx.keychainIdentity,
|
|
190
|
+
};
|
|
191
|
+
const health = await fetchHealth(config);
|
|
192
|
+
task.output = `Panel is reachable (status: ${health.status || 'ok'})`;
|
|
193
|
+
},
|
|
194
|
+
rendererOptions: { persistentOutput: true },
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
title: 'Installing Chisel',
|
|
198
|
+
task: async (_ctx, task) => {
|
|
199
|
+
const result = await installChisel();
|
|
200
|
+
ctx.chiselVersion = result.version;
|
|
201
|
+
if (result.skipped) {
|
|
202
|
+
task.output = `Already installed (${result.version})`;
|
|
203
|
+
} else {
|
|
204
|
+
task.output = `Installed ${result.version}`;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
rendererOptions: { persistentOutput: true },
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
title: 'Fetching tunnel configuration',
|
|
211
|
+
task: async (_ctx, task) => {
|
|
212
|
+
const config = {
|
|
213
|
+
panelUrl: ctx.panelUrl,
|
|
214
|
+
authMethod: 'keychain',
|
|
215
|
+
keychainIdentity: ctx.keychainIdentity,
|
|
216
|
+
};
|
|
217
|
+
const data = await fetchPlist(config);
|
|
218
|
+
ctx.plistXml = data.plist;
|
|
219
|
+
|
|
220
|
+
const tunnelData = await fetchTunnels(config);
|
|
221
|
+
ctx.tunnels = tunnelData.tunnels || [];
|
|
222
|
+
task.output = `${ctx.tunnels.length} tunnel(s) configured`;
|
|
223
|
+
},
|
|
224
|
+
rendererOptions: { persistentOutput: true },
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
title: 'Rewriting plist paths',
|
|
228
|
+
task: async () => {
|
|
229
|
+
ctx.plistXml = rewritePlist(ctx.plistXml);
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
title: 'Writing plist file',
|
|
234
|
+
task: async () => {
|
|
235
|
+
await writePlistFile(ctx.plistXml);
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
title: 'Unloading previous agent',
|
|
240
|
+
skip: async () => {
|
|
241
|
+
const loaded = await isAgentLoaded();
|
|
242
|
+
return !loaded && 'No previous agent loaded';
|
|
243
|
+
},
|
|
244
|
+
task: async () => {
|
|
245
|
+
await unloadAgent();
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
title: 'Loading agent',
|
|
250
|
+
task: async () => {
|
|
251
|
+
await loadAgent();
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
title: 'Verifying agent is running',
|
|
256
|
+
task: async (_ctx, task) => {
|
|
257
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
258
|
+
const pid = await getAgentPid();
|
|
259
|
+
if (pid) {
|
|
260
|
+
task.output = `Agent running (PID ${pid})`;
|
|
261
|
+
} else {
|
|
262
|
+
const loaded = await isAgentLoaded();
|
|
263
|
+
if (loaded) {
|
|
264
|
+
task.output = 'Agent loaded (process starting...)';
|
|
265
|
+
} else {
|
|
266
|
+
throw new Error('Agent failed to load. Check logs with: portlama-agent logs');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
rendererOptions: { persistentOutput: true },
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
title: 'Saving configuration',
|
|
274
|
+
task: async () => {
|
|
275
|
+
const domainMatch = ctx.plistXml.match(/wss:\/\/tunnel\.([^:]+):/);
|
|
276
|
+
ctx.domain = domainMatch ? domainMatch[1] : null;
|
|
277
|
+
|
|
278
|
+
await saveAgentConfig({
|
|
279
|
+
panelUrl: ctx.panelUrl,
|
|
280
|
+
authMethod: 'keychain',
|
|
281
|
+
keychainIdentity: ctx.keychainIdentity,
|
|
282
|
+
agentLabel: ctx.agentLabel,
|
|
283
|
+
domain: ctx.domain,
|
|
284
|
+
chiselVersion: ctx.chiselVersion,
|
|
285
|
+
setupAt: new Date().toISOString(),
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
{
|
|
291
|
+
renderer: 'default',
|
|
292
|
+
rendererOptions: { collapseSubtasks: false },
|
|
293
|
+
exitOnError: true,
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await tasks.run();
|
|
299
|
+
} catch (err) {
|
|
300
|
+
// Securely delete the temporary private key if it was generated but not
|
|
301
|
+
// yet imported into Keychain (importIdentityToKeychain handles its own cleanup).
|
|
302
|
+
if (ctx._keyData?.keyPath) {
|
|
303
|
+
await secureDelete(ctx._keyData.keyPath).catch(() => {});
|
|
304
|
+
}
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Print summary
|
|
309
|
+
printSetupSummary(ctx);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Traditional P12-based setup flow.
|
|
314
|
+
*/
|
|
315
|
+
async function runP12Setup() {
|
|
41
316
|
// Step 1: Verify macOS
|
|
42
317
|
assertMacOS();
|
|
43
318
|
|
|
@@ -206,6 +481,7 @@ export async function runSetup() {
|
|
|
206
481
|
|
|
207
482
|
await saveAgentConfig({
|
|
208
483
|
panelUrl: ctx.panelUrl,
|
|
484
|
+
authMethod: 'p12',
|
|
209
485
|
p12Path: ctx.p12Path,
|
|
210
486
|
p12Password: ctx.p12Password,
|
|
211
487
|
domain: ctx.domain,
|
|
@@ -76,7 +76,7 @@ async function runList(config, agentLabel) {
|
|
|
76
76
|
|
|
77
77
|
let sessions;
|
|
78
78
|
try {
|
|
79
|
-
const data = await fetchShellSessions(config
|
|
79
|
+
const data = await fetchShellSessions(config);
|
|
80
80
|
sessions = data.sessions || [];
|
|
81
81
|
} catch (err) {
|
|
82
82
|
console.log(` ${y(`Could not fetch sessions: ${err.message}`)}`);
|
|
@@ -142,9 +142,7 @@ async function runDownload(config, agentLabel, sessionId) {
|
|
|
142
142
|
|
|
143
143
|
try {
|
|
144
144
|
await downloadShellRecording(
|
|
145
|
-
config
|
|
146
|
-
config.p12Path,
|
|
147
|
-
config.p12Password,
|
|
145
|
+
config,
|
|
148
146
|
agentLabel,
|
|
149
147
|
sessionId,
|
|
150
148
|
outputPath,
|
|
@@ -399,6 +399,13 @@ export async function runShellServer() {
|
|
|
399
399
|
process.exit(1);
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
// WebSocket requires PEM cert/key — not available with Keychain-bound keys
|
|
403
|
+
if (config.authMethod === 'keychain') {
|
|
404
|
+
console.error(chalk.red(' Shell server is not yet supported with hardware-bound (Keychain) certificates.'));
|
|
405
|
+
console.error(chalk.dim(' Use a P12-enrolled agent for shell access.'));
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
|
|
402
409
|
// Extract PEM certificates from p12
|
|
403
410
|
let pem;
|
|
404
411
|
try {
|
|
@@ -438,7 +445,7 @@ export async function runShellServer() {
|
|
|
438
445
|
while (running) {
|
|
439
446
|
let agentStatus;
|
|
440
447
|
try {
|
|
441
|
-
agentStatus = await fetchAgentStatus(config
|
|
448
|
+
agentStatus = await fetchAgentStatus(config);
|
|
442
449
|
} catch (err) {
|
|
443
450
|
console.error(chalk.yellow(` Could not reach panel: ${err.message}`));
|
|
444
451
|
await sleep(POLL_INTERVAL_MS);
|
package/src/commands/shell.js
CHANGED
|
@@ -20,6 +20,13 @@ export async function runShell(args) {
|
|
|
20
20
|
console.log('');
|
|
21
21
|
console.log(chalk.dim(` Connecting to agent ${chalk.bold(agentLabel)}...`));
|
|
22
22
|
|
|
23
|
+
// WebSocket requires PEM cert/key — not available with Keychain-bound keys
|
|
24
|
+
if (config.authMethod === 'keychain') {
|
|
25
|
+
console.error(chalk.red('\n Shell access is not yet supported with hardware-bound (Keychain) certificates.'));
|
|
26
|
+
console.error(chalk.dim(' Use a P12-enrolled agent for shell access.\n'));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
// Extract PEM certificates from p12
|
|
24
31
|
let pem;
|
|
25
32
|
try {
|
package/src/commands/sites.js
CHANGED
|
@@ -87,7 +87,7 @@ async function runList(config) {
|
|
|
87
87
|
|
|
88
88
|
let sites;
|
|
89
89
|
try {
|
|
90
|
-
const data = await fetchSites(config
|
|
90
|
+
const data = await fetchSites(config);
|
|
91
91
|
sites = data.sites || [];
|
|
92
92
|
} catch {
|
|
93
93
|
console.log(` ${y('Could not reach panel to fetch site list.')}`);
|
|
@@ -163,7 +163,7 @@ async function runCreate(config, args) {
|
|
|
163
163
|
|
|
164
164
|
let result;
|
|
165
165
|
try {
|
|
166
|
-
result = await createSite(config
|
|
166
|
+
result = await createSite(config, body);
|
|
167
167
|
} catch (err) {
|
|
168
168
|
const detail = err.message || 'Unknown error';
|
|
169
169
|
console.error(`\n ${chalk.red(`Failed to create site: ${detail}`)}\n`);
|
|
@@ -207,7 +207,7 @@ async function runDelete(config, args) {
|
|
|
207
207
|
} else {
|
|
208
208
|
let data;
|
|
209
209
|
try {
|
|
210
|
-
data = await fetchSites(config
|
|
210
|
+
data = await fetchSites(config);
|
|
211
211
|
} catch (err) {
|
|
212
212
|
console.error(`\n ${chalk.red(`Failed to connect to panel: ${err.message}`)}\n`);
|
|
213
213
|
process.exit(1);
|
|
@@ -229,7 +229,7 @@ async function runDelete(config, args) {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
try {
|
|
232
|
-
await deleteSite(config
|
|
232
|
+
await deleteSite(config, siteId);
|
|
233
233
|
} catch (err) {
|
|
234
234
|
const detail = err.message || 'Unknown error';
|
|
235
235
|
console.error(`\n ${chalk.red(`Failed to delete site: ${detail}`)}\n`);
|
package/src/commands/status.js
CHANGED
|
@@ -69,7 +69,7 @@ export async function runStatus() {
|
|
|
69
69
|
console.log(d(' ─'.repeat(28)));
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
|
-
const data = await fetchTunnels(config
|
|
72
|
+
const data = await fetchTunnels(config);
|
|
73
73
|
const tunnels = data.tunnels || [];
|
|
74
74
|
|
|
75
75
|
if (tunnels.length === 0) {
|
package/src/commands/update.js
CHANGED
|
@@ -25,14 +25,10 @@ export async function runUpdate() {
|
|
|
25
25
|
{
|
|
26
26
|
title: 'Fetching updated tunnel configuration',
|
|
27
27
|
task: async (_ctx, task) => {
|
|
28
|
-
const data = await fetchPlist(config
|
|
28
|
+
const data = await fetchPlist(config);
|
|
29
29
|
ctx.plistXml = data.plist;
|
|
30
30
|
|
|
31
|
-
const tunnelData = await fetchTunnels(
|
|
32
|
-
config.panelUrl,
|
|
33
|
-
config.p12Path,
|
|
34
|
-
config.p12Password,
|
|
35
|
-
);
|
|
31
|
+
const tunnelData = await fetchTunnels(config);
|
|
36
32
|
ctx.tunnels = tunnelData.tunnels || [];
|
|
37
33
|
task.output = `${ctx.tunnels.length} tunnel(s) configured`;
|
|
38
34
|
},
|
package/src/lib/config.js
CHANGED
|
@@ -4,12 +4,29 @@ import { CONFIG_PATH, AGENT_DIR } from './platform.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Load the agent config from ~/.portlama/agent.json.
|
|
6
6
|
* Returns null if the file does not exist.
|
|
7
|
+
*
|
|
8
|
+
* Config fields:
|
|
9
|
+
* - panelUrl: string — Panel URL
|
|
10
|
+
* - authMethod: 'p12' | 'keychain' — Authentication method (defaults to 'p12' if missing)
|
|
11
|
+
* - p12Path: string — Path to P12 file (when authMethod is 'p12')
|
|
12
|
+
* - p12Password: string — P12 password (when authMethod is 'p12')
|
|
13
|
+
* - keychainIdentity: string — Keychain identity name (when authMethod is 'keychain')
|
|
14
|
+
* - agentLabel: string — Agent label (when authMethod is 'keychain')
|
|
15
|
+
* - domain?: string
|
|
16
|
+
* - chiselVersion?: string
|
|
17
|
+
* - setupAt?: string
|
|
18
|
+
*
|
|
7
19
|
* @returns {Promise<object | null>}
|
|
8
20
|
*/
|
|
9
21
|
export async function loadAgentConfig() {
|
|
10
22
|
try {
|
|
11
23
|
const raw = await readFile(CONFIG_PATH, 'utf8');
|
|
12
|
-
|
|
24
|
+
const config = JSON.parse(raw);
|
|
25
|
+
// Default authMethod to 'p12' for backwards compatibility
|
|
26
|
+
if (config && !config.authMethod) {
|
|
27
|
+
config.authMethod = 'p12';
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
13
30
|
} catch {
|
|
14
31
|
return null;
|
|
15
32
|
}
|