fix(node-cli): keep install secrets out of process args
This commit is contained in:
1 parent
08340725ec
commit
3d2dccd8d4
6 files changed
+353
-18
No files matched your search
@@ -55,6 +55,8 @@ This repository publishes the open-source server, console, integration catalog,
|
|||||||
- Honor valid `Retry-After` delay-seconds and HTTP-date values, and release retryable responses before sleeping.
|
- Honor valid `Retry-After` delay-seconds and HTTP-date values, and release retryable responses before sleeping.
|
||||||
- Avoid compatibility shims without a named public contract.
|
- Avoid compatibility shims without a named public contract.
|
||||||
- Preserve the minimum runtime versions declared by each package.
|
- Preserve the minimum runtime versions declared by each package.
|
||||||
|
- Keep private keys and API tokens out of process arguments and generated setup commands. Accept them through hidden terminal input or owner-only credential files.
|
||||||
|
- Persist credential-bearing files through owner-only same-directory temporary files and atomic rename.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -48,10 +48,11 @@ npx elydora install \
|
|||||||
--agent claudecode \
|
--agent claudecode \
|
||||||
--org_id org-123 \
|
--org_id org-123 \
|
||||||
--agent_id agent-456 \
|
--agent_id agent-456 \
|
||||||
--private_key <key> \
|
|
||||||
--kid agent-456-key-v1
|
--kid agent-456-key-v1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The CLI reads the private key and optional API token through hidden terminal prompts. For non-interactive installation, store each secret in an owner-only file and pass `--private_key_file <path>` and `--token_file <path>`. Secret values are rejected as command-line arguments because process listings and shell history can expose them.
|
||||||
|
|
||||||
Codex performs a one-time trust review for user hooks. Run `/hooks` in Codex after installation and trust the Elydora `PreToolUse` and `PostToolUse` definitions.
|
Codex performs a one-time trust review for user hooks. Run `/hooks` in Codex after installation and trust the Elydora `PreToolUse` and `PostToolUse` definitions.
|
||||||
|
|
||||||
Cline installation writes `PreToolUse.mjs` and `PostToolUse.mjs` to `$CLINE_DIR/hooks` (default `~/.cline/hooks`). Elydora leaves the Documents and workspace hook roots unchanged. The guard translates a frozen agent into Cline's JSON stdout cancellation control.
|
Cline installation writes `PreToolUse.mjs` and `PostToolUse.mjs` to `$CLINE_DIR/hooks` (default `~/.cline/hooks`). Elydora leaves the Documents and workspace hook roots unchanged. The guard translates a frozen agent into Cline's JSON stdout cancellation control.
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import fsp from 'node:fs/promises';
|
||||||
|
import readline from 'node:readline';
|
||||||
|
import { Writable } from 'node:stream';
|
||||||
|
|
||||||
|
const MAX_SECRET_FILE_BYTES = 64 * 1024;
|
||||||
|
|
||||||
|
interface SecretSources {
|
||||||
|
readonly privateKeyFile?: string;
|
||||||
|
readonly tokenFile?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SecretTerminal {
|
||||||
|
readonly interactive: boolean;
|
||||||
|
readHidden(prompt: string): Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstallSecrets {
|
||||||
|
readonly privateKey: string;
|
||||||
|
readonly token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSingleLineSecret(raw: string, label: string, allowEmpty: boolean): string {
|
||||||
|
const value = raw.endsWith('\r\n')
|
||||||
|
? raw.slice(0, -2)
|
||||||
|
: raw.endsWith('\n')
|
||||||
|
? raw.slice(0, -1)
|
||||||
|
: raw;
|
||||||
|
|
||||||
|
if (!allowEmpty && value.length === 0) {
|
||||||
|
throw new Error(`${label} is empty`);
|
||||||
|
}
|
||||||
|
if (value.includes('\r') || value.includes('\n') || value.includes('\0')) {
|
||||||
|
throw new Error(`${label} must contain exactly one line`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSecretFile(filePath: string, label: string): Promise<string> {
|
||||||
|
const stats = await fsp.lstat(filePath);
|
||||||
|
if (!stats.isFile()) {
|
||||||
|
throw new Error(`${label} file is not a regular file: ${filePath}`);
|
||||||
|
}
|
||||||
|
if (stats.size > MAX_SECRET_FILE_BYTES) {
|
||||||
|
throw new Error(`${label} file exceeds ${MAX_SECRET_FILE_BYTES} bytes: ${filePath}`);
|
||||||
|
}
|
||||||
|
if (process.platform !== 'win32' && (stats.mode & 0o077) !== 0) {
|
||||||
|
throw new Error(`${label} file must be accessible only by its owner: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = await fsp.readFile(filePath, 'utf-8');
|
||||||
|
return parseSingleLineSecret(raw, label, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readHidden(prompt: string): Promise<string> {
|
||||||
|
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
||||||
|
throw new Error('hidden input requires an interactive terminal');
|
||||||
|
}
|
||||||
|
|
||||||
|
let muted = false;
|
||||||
|
const output = new Writable({
|
||||||
|
write(chunk, encoding, callback) {
|
||||||
|
if (!muted) process.stderr.write(chunk, encoding);
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const terminal = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output,
|
||||||
|
terminal: true,
|
||||||
|
historySize: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stderr.write(prompt);
|
||||||
|
muted = true;
|
||||||
|
try {
|
||||||
|
return await new Promise<string>((resolve, reject) => {
|
||||||
|
terminal.once('SIGINT', () => reject(new Error('secret input cancelled')));
|
||||||
|
terminal.question('', resolve);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
muted = false;
|
||||||
|
terminal.close();
|
||||||
|
process.stderr.write('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTerminal: SecretTerminal = {
|
||||||
|
interactive: Boolean(process.stdin.isTTY && process.stderr.isTTY),
|
||||||
|
readHidden,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function resolveInstallSecrets(
|
||||||
|
sources: SecretSources,
|
||||||
|
terminal: SecretTerminal = defaultTerminal,
|
||||||
|
): Promise<InstallSecrets> {
|
||||||
|
let privateKey: string;
|
||||||
|
if (sources.privateKeyFile) {
|
||||||
|
privateKey = await readSecretFile(sources.privateKeyFile, 'private key');
|
||||||
|
} else if (terminal.interactive) {
|
||||||
|
privateKey = parseSingleLineSecret(
|
||||||
|
await terminal.readHidden('Private key: '),
|
||||||
|
'private key',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'private key input requires an interactive terminal or --private_key_file <path>',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token: string | undefined;
|
||||||
|
if (sources.tokenFile) {
|
||||||
|
token = await readSecretFile(sources.tokenFile, 'API token');
|
||||||
|
} else if (terminal.interactive) {
|
||||||
|
token = parseSingleLineSecret(
|
||||||
|
await terminal.readHidden('API token (optional): '),
|
||||||
|
'API token',
|
||||||
|
true,
|
||||||
|
) || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { privateKey, token };
|
||||||
|
}
|
||||||
+14
-17
@@ -2,7 +2,9 @@ import { parseArgs } from 'node:util';
|
|||||||
import fsp from 'node:fs/promises';
|
import fsp from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
|
import { resolveInstallSecrets } from './cli-secrets.js';
|
||||||
import { derivePublicKey } from './crypto.js';
|
import { derivePublicKey } from './crypto.js';
|
||||||
|
import { ensurePrivateDirectory, writePrivateFile } from './secure-files.js';
|
||||||
import { SUPPORTED_AGENTS } from './plugins/registry.js';
|
import { SUPPORTED_AGENTS } from './plugins/registry.js';
|
||||||
import type { AgentPlugin, InstallConfig } from './plugins/base.js';
|
import type { AgentPlugin, InstallConfig } from './plugins/base.js';
|
||||||
import { generateHookScript, generateGuardScript } from './plugins/hook-template.js';
|
import { generateHookScript, generateGuardScript } from './plugins/hook-template.js';
|
||||||
@@ -55,7 +57,7 @@ function printUsage(): void {
|
|||||||
console.log(`Elydora CLI — Tamper-evident audit for AI coding agents
|
console.log(`Elydora CLI — Tamper-evident audit for AI coding agents
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
elydora install --agent <name> --org_id <id> --agent_id <id> --private_key <key> --kid <kid> [--token <token>] [--base_url <url>]
|
elydora install --agent <name> --org_id <id> --agent_id <id> --kid <kid> [--private_key_file <path>] [--token_file <path>] [--base_url <url>]
|
||||||
elydora uninstall --agent <name> [--agent_id <id>]
|
elydora uninstall --agent <name> [--agent_id <id>]
|
||||||
elydora status
|
elydora status
|
||||||
elydora agents
|
elydora agents
|
||||||
@@ -81,9 +83,9 @@ async function cmdInstall(args: string[]): Promise<void> {
|
|||||||
agent: { type: 'string' },
|
agent: { type: 'string' },
|
||||||
org_id: { type: 'string' },
|
org_id: { type: 'string' },
|
||||||
agent_id: { type: 'string' },
|
agent_id: { type: 'string' },
|
||||||
private_key: { type: 'string' },
|
private_key_file: { type: 'string' },
|
||||||
kid: { type: 'string' },
|
kid: { type: 'string' },
|
||||||
token: { type: 'string' },
|
token_file: { type: 'string' },
|
||||||
base_url: { type: 'string' },
|
base_url: { type: 'string' },
|
||||||
},
|
},
|
||||||
strict: true,
|
strict: true,
|
||||||
@@ -101,13 +103,13 @@ async function cmdInstall(args: string[]): Promise<void> {
|
|||||||
const agentId = values.agent_id;
|
const agentId = values.agent_id;
|
||||||
if (!agentId) die('--agent_id is required');
|
if (!agentId) die('--agent_id is required');
|
||||||
|
|
||||||
const privateKey = values.private_key;
|
|
||||||
if (!privateKey) die('--private_key is required');
|
|
||||||
|
|
||||||
const kid = values.kid;
|
const kid = values.kid;
|
||||||
if (!kid) die('--kid is required');
|
if (!kid) die('--kid is required');
|
||||||
|
|
||||||
const token = values.token;
|
const { privateKey, token } = await resolveInstallSecrets({
|
||||||
|
privateKeyFile: values.private_key_file,
|
||||||
|
tokenFile: values.token_file,
|
||||||
|
});
|
||||||
const baseUrl = values.base_url ?? 'https://api.elydora.com';
|
const baseUrl = values.base_url ?? 'https://api.elydora.com';
|
||||||
|
|
||||||
// Validate private key by deriving public key
|
// Validate private key by deriving public key
|
||||||
@@ -122,7 +124,8 @@ async function cmdInstall(args: string[]): Promise<void> {
|
|||||||
|
|
||||||
// Create ~/.elydora/{agentId}/ directory
|
// Create ~/.elydora/{agentId}/ directory
|
||||||
const agentDir = path.join(ELYDORA_DIR, agentId);
|
const agentDir = path.join(ELYDORA_DIR, agentId);
|
||||||
await fsp.mkdir(agentDir, { recursive: true });
|
await ensurePrivateDirectory(ELYDORA_DIR);
|
||||||
|
await ensurePrivateDirectory(agentDir);
|
||||||
|
|
||||||
// Write agent config
|
// Write agent config
|
||||||
const agentConfigPath = path.join(agentDir, 'config.json');
|
const agentConfigPath = path.join(agentDir, 'config.json');
|
||||||
@@ -134,18 +137,12 @@ async function cmdInstall(args: string[]): Promise<void> {
|
|||||||
...(token ? { token } : {}),
|
...(token ? { token } : {}),
|
||||||
agent_name: agentName,
|
agent_name: agentName,
|
||||||
};
|
};
|
||||||
await fsp.writeFile(agentConfigPath, JSON.stringify(agentConfig, null, 2) + '\n', 'utf-8');
|
await writePrivateFile(agentConfigPath, JSON.stringify(agentConfig, null, 2) + '\n');
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
await fsp.chmod(agentConfigPath, 0o600);
|
|
||||||
}
|
|
||||||
console.log(` Agent config: ${agentConfigPath}`);
|
console.log(` Agent config: ${agentConfigPath}`);
|
||||||
|
|
||||||
// Write private key (chmod 600)
|
// Write private key with owner-restricted access
|
||||||
const keyPath = path.join(agentDir, 'private.key');
|
const keyPath = path.join(agentDir, 'private.key');
|
||||||
await fsp.writeFile(keyPath, privateKey, { encoding: 'utf-8', mode: 0o600 });
|
await writePrivateFile(keyPath, privateKey);
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
await fsp.chmod(keyPath, 0o600);
|
|
||||||
}
|
|
||||||
console.log(` Private key: ${keyPath}`);
|
console.log(` Private key: ${keyPath}`);
|
||||||
|
|
||||||
// Generate and write hook script (PostToolUse — audit logging)
|
// Generate and write hook script (PostToolUse — audit logging)
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import crypto from 'node:crypto';
|
||||||
|
import fsp from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
function isMissingFile(error: unknown): boolean {
|
||||||
|
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensurePrivateDirectory(directory: string): Promise<void> {
|
||||||
|
await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
await fsp.chmod(directory, 0o700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writePrivateFile(filePath: string, contents: string): Promise<void> {
|
||||||
|
const directory = path.dirname(filePath);
|
||||||
|
await ensurePrivateDirectory(directory);
|
||||||
|
|
||||||
|
const temporaryPath = path.join(
|
||||||
|
directory,
|
||||||
|
`.${path.basename(filePath)}.${crypto.randomUUID()}.tmp`,
|
||||||
|
);
|
||||||
|
let committed = false;
|
||||||
|
try {
|
||||||
|
const handle = await fsp.open(temporaryPath, 'wx', 0o600);
|
||||||
|
try {
|
||||||
|
await handle.writeFile(contents, 'utf-8');
|
||||||
|
await handle.sync();
|
||||||
|
} finally {
|
||||||
|
await handle.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsp.rename(temporaryPath, filePath);
|
||||||
|
committed = true;
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
await fsp.chmod(filePath, 0o600);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!committed) {
|
||||||
|
try {
|
||||||
|
await fsp.unlink(temporaryPath);
|
||||||
|
} catch (error) {
|
||||||
|
if (!isMissingFile(error)) throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { chmod, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { resolveInstallSecrets } from '../dist/cli-secrets.js';
|
||||||
|
|
||||||
|
const PRIVATE_KEY = Buffer.alloc(32, 7).toString('base64url');
|
||||||
|
const API_TOKEN = 'ely_test_token';
|
||||||
|
|
||||||
|
async function runCli(args, homeDir) {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(process.execPath, [path.resolve('dist/cli.js'), ...args], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
HOME: homeDir,
|
||||||
|
USERPROFILE: homeDir,
|
||||||
|
},
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
const stdout = [];
|
||||||
|
const stderr = [];
|
||||||
|
child.stdout.on('data', (chunk) => stdout.push(chunk));
|
||||||
|
child.stderr.on('data', (chunk) => stderr.push(chunk));
|
||||||
|
child.once('error', reject);
|
||||||
|
child.once('close', (code) => resolve({
|
||||||
|
code,
|
||||||
|
stdout: Buffer.concat(stdout).toString('utf-8'),
|
||||||
|
stderr: Buffer.concat(stderr).toString('utf-8'),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('install secrets resolve from hidden prompts without process arguments', async () => {
|
||||||
|
const answers = [PRIVATE_KEY, API_TOKEN];
|
||||||
|
const prompts = [];
|
||||||
|
const secrets = await resolveInstallSecrets({}, {
|
||||||
|
interactive: true,
|
||||||
|
async readHidden(prompt) {
|
||||||
|
prompts.push(prompt);
|
||||||
|
return answers.shift();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(secrets, { privateKey: PRIVATE_KEY, token: API_TOKEN });
|
||||||
|
assert.deepEqual(prompts, ['Private key: ', 'API token (optional): ']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-interactive install requires a credential file for the private key', async () => {
|
||||||
|
await assert.rejects(
|
||||||
|
resolveInstallSecrets({}, {
|
||||||
|
interactive: false,
|
||||||
|
async readHidden() {
|
||||||
|
throw new Error('unexpected prompt');
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/--private_key_file <path>/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('credential files contain exactly one secret line', async () => {
|
||||||
|
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-cli-'));
|
||||||
|
const privateKeyFile = path.join(homeDir, 'private-key');
|
||||||
|
try {
|
||||||
|
await writeFile(privateKeyFile, `${PRIVATE_KEY}\nsecond-line\n`, { mode: 0o600 });
|
||||||
|
if (process.platform !== 'win32') await chmod(privateKeyFile, 0o600);
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
resolveInstallSecrets({ privateKeyFile }, {
|
||||||
|
interactive: false,
|
||||||
|
async readHidden() {
|
||||||
|
throw new Error('unexpected prompt');
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/private key must contain exactly one line/,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await rm(homeDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CLI rejects legacy secret arguments before writing files', async () => {
|
||||||
|
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-cli-'));
|
||||||
|
try {
|
||||||
|
const result = await runCli([
|
||||||
|
'install',
|
||||||
|
'--agent', 'opencode',
|
||||||
|
'--org_id', 'org-1',
|
||||||
|
'--agent_id', 'agent-1',
|
||||||
|
'--kid', 'key-1',
|
||||||
|
'--private_key', PRIVATE_KEY,
|
||||||
|
'--token', API_TOKEN,
|
||||||
|
], homeDir);
|
||||||
|
|
||||||
|
assert.equal(result.code, 1);
|
||||||
|
assert.match(result.stderr, /Unknown option '--private_key'/);
|
||||||
|
await assert.rejects(stat(path.join(homeDir, '.elydora')), { code: 'ENOENT' });
|
||||||
|
} finally {
|
||||||
|
await rm(homeDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CLI installs from owner-only files and protects persisted credentials', async () => {
|
||||||
|
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-cli-'));
|
||||||
|
const privateKeyFile = path.join(homeDir, 'private-key');
|
||||||
|
const tokenFile = path.join(homeDir, 'api-token');
|
||||||
|
try {
|
||||||
|
await writeFile(privateKeyFile, `${PRIVATE_KEY}\n`, { mode: 0o600 });
|
||||||
|
await writeFile(tokenFile, `${API_TOKEN}\n`, { mode: 0o600 });
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
await chmod(privateKeyFile, 0o600);
|
||||||
|
await chmod(tokenFile, 0o600);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await runCli([
|
||||||
|
'install',
|
||||||
|
'--agent', 'opencode',
|
||||||
|
'--org_id', 'org-1',
|
||||||
|
'--agent_id', 'agent-1',
|
||||||
|
'--kid', 'key-1',
|
||||||
|
'--private_key_file', privateKeyFile,
|
||||||
|
'--token_file', tokenFile,
|
||||||
|
'--base_url', 'https://api.example.test',
|
||||||
|
], homeDir);
|
||||||
|
|
||||||
|
assert.equal(result.code, 0, result.stderr);
|
||||||
|
assert.doesNotMatch(result.stdout + result.stderr, new RegExp(PRIVATE_KEY));
|
||||||
|
assert.doesNotMatch(result.stdout + result.stderr, new RegExp(API_TOKEN));
|
||||||
|
|
||||||
|
const agentDir = path.join(homeDir, '.elydora', 'agent-1');
|
||||||
|
assert.equal(await readFile(path.join(agentDir, 'private.key'), 'utf-8'), PRIVATE_KEY);
|
||||||
|
const config = JSON.parse(await readFile(path.join(agentDir, 'config.json'), 'utf-8'));
|
||||||
|
assert.equal(config.token, API_TOKEN);
|
||||||
|
|
||||||
|
const rotatedToken = `${API_TOKEN}_rotated`;
|
||||||
|
await writeFile(tokenFile, `${rotatedToken}\n`);
|
||||||
|
const reinstall = await runCli([
|
||||||
|
'install',
|
||||||
|
'--agent', 'opencode',
|
||||||
|
'--org_id', 'org-1',
|
||||||
|
'--agent_id', 'agent-1',
|
||||||
|
'--kid', 'key-1',
|
||||||
|
'--private_key_file', privateKeyFile,
|
||||||
|
'--token_file', tokenFile,
|
||||||
|
'--base_url', 'https://api.example.test',
|
||||||
|
], homeDir);
|
||||||
|
assert.equal(reinstall.code, 0, reinstall.stderr);
|
||||||
|
assert.doesNotMatch(reinstall.stdout + reinstall.stderr, new RegExp(rotatedToken));
|
||||||
|
const rotatedConfig = JSON.parse(
|
||||||
|
await readFile(path.join(agentDir, 'config.json'), 'utf-8'),
|
||||||
|
);
|
||||||
|
assert.equal(rotatedConfig.token, rotatedToken);
|
||||||
|
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
assert.equal((await stat(agentDir)).mode & 0o777, 0o700);
|
||||||
|
assert.equal((await stat(path.join(agentDir, 'private.key'))).mode & 0o777, 0o600);
|
||||||
|
assert.equal((await stat(path.join(agentDir, 'config.json'))).mode & 0o777, 0o600);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await rm(homeDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user