@redocly/cli
Version:
[@Redocly](https://redocly.com) CLI is your all-in-one API documentation utility. It builds, manages, improves, and quality-checks your API descriptions, all of which comes in handy for various phases of the API Lifecycle. Create your own rulesets to make
245 lines • 9.12 kB
JavaScript
import * as os from 'node:os';
import * as fs from 'node:fs';
import { execSync } from 'node:child_process';
import { isAbsoluteUrl, isPlainObject } from '@redocly/openapi-core';
import { version } from './package.js';
import { getReuniteUrl } from '../reunite/api/index.js';
import { respondWithinMs } from './network-check.js';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { existsSync, writeFileSync, readFileSync } from 'node:fs';
import { ANONYMOUS_ID_CACHE_FILE } from './constants.js';
import { ulid } from 'ulid';
const SECRET_REPLACEMENT = '***';
export async function sendTelemetry({ config, argv, exit_code, execution_time, spec_version, spec_keyword, spec_full_version, respect_x_security_auth_types, }) {
try {
if (!argv) {
return;
}
const hasInternet = await respondWithinMs(1000);
if (!hasInternet) {
return;
}
const { _: [command], $0: _, ...args } = argv;
const { RedoclyOAuthClient } = await import('../auth/oauth-client.js');
const oauthClient = new RedoclyOAuthClient();
const reuniteUrl = getReuniteUrl(config, args.residency);
const logged_in = await oauthClient.isAuthorized(reuniteUrl);
let anonymous_id = getCachedAnonymousId();
if (!anonymous_id) {
anonymous_id = `ann_${ulid()}`;
cacheAnonymousId(anonymous_id);
}
const eventData = {
id: 'cli-command-run',
object: 'command',
logged_in: logged_in ? 'yes' : 'no',
command: `${command}`,
...cleanArgs(args, process.argv.slice(2)),
node_version: process.version,
npm_version: execSync('npm -v').toString().replace('\n', ''),
version,
exit_code,
execution_time,
metadata: process.env.REDOCLY_CLI_TELEMETRY_METADATA,
environment_ci: process.env.CI,
has_config: typeof config?.document?.parsed === 'undefined' ? 'no' : 'yes',
spec_version,
spec_keyword,
spec_full_version,
respect_x_security_auth_types: spec_version === 'arazzo1' && respect_x_security_auth_types?.length
? JSON.stringify(respect_x_security_auth_types)
: undefined,
};
const cloudEvent = {
id: `evt_${ulid()}`,
time: new Date().toISOString(),
type: 'command.ran',
object: 'event',
specversion: '1.0',
datacontenttype: 'application/json',
source: 'com.redocly.cli',
origin: 'cli',
productType: 'redocly-cli',
os_platform: os.platform(),
subjects: [
{
id: ulid(),
object: 'command.ran',
uri: '',
},
],
environment: process.env.REDOCLY_ENVIRONMENT,
sourceDetails: {
id: anonymous_id,
object: 'user',
uri: '',
},
data: eventData,
};
const { otelTelemetry } = await import('./otel.js');
otelTelemetry.send(cloudEvent);
}
catch (err) {
// Do nothing.
}
}
export function collectXSecurityAuthTypes(document, respectXSecurityAuthTypesAndSchemeName) {
for (const workflow of document.workflows ?? []) {
// Collect auth types from workflow-level x-security
for (const security of workflow['x-security'] ?? []) {
const scheme = security.scheme;
if (scheme?.type) {
const authType = scheme.type === 'http' ? scheme.scheme : scheme.type;
if (authType && !respectXSecurityAuthTypesAndSchemeName.includes(authType)) {
respectXSecurityAuthTypesAndSchemeName.push(authType);
}
}
}
// Collect auth types from step-level x-security
for (const step of workflow.steps ?? []) {
for (const security of step['x-security'] ?? []) {
// Handle scheme case
const scheme = security.scheme;
if (scheme?.type) {
const authType = scheme.type === 'http' ? scheme.scheme : scheme.type;
if (authType && !respectXSecurityAuthTypesAndSchemeName.includes(authType)) {
respectXSecurityAuthTypesAndSchemeName.push(authType);
}
}
// Handle schemeName case
const schemeName = security.schemeName;
if (schemeName && !respectXSecurityAuthTypesAndSchemeName.includes(schemeName)) {
respectXSecurityAuthTypesAndSchemeName.push(schemeName);
}
}
}
}
}
function isFile(value) {
return fs.existsSync(value) && fs.statSync(value).isFile();
}
function isDirectory(value) {
return fs.existsSync(value) && fs.statSync(value).isDirectory();
}
function cleanString(value) {
if (!value) {
return value;
}
if (isAbsoluteUrl(value)) {
return value.split('://')[0] + '://url';
}
if (isFile(value)) {
return value.replace(/.+\.([^.]+)$/, (_, ext) => 'file-' + ext);
}
if (isDirectory(value)) {
return 'folder';
}
return value;
}
function replaceArgs(commandInput, targets, replacement) {
const targetValues = Array.isArray(targets) ? targets : [targets];
for (const target of targetValues) {
commandInput = commandInput.replaceAll(target, replacement);
}
return commandInput;
}
function cleanObject(obj, keysToClean) {
const cleaned = {};
for (const [key, value] of Object.entries(obj)) {
if (keysToClean.includes(key)) {
cleaned[key] = SECRET_REPLACEMENT;
}
else if (isPlainObject(value)) {
cleaned[key] = cleanObject(value, keysToClean);
}
else {
cleaned[key] = value;
}
}
return cleaned;
}
function collectSensitiveValues(obj, keysToClean, values = []) {
if (typeof obj !== 'object' || obj === null) {
return values;
}
if (Array.isArray(obj)) {
obj.forEach((item) => collectSensitiveValues(item, keysToClean, values));
return values;
}
for (const [key, value] of Object.entries(obj)) {
if (keysToClean.includes(key) && typeof value === 'string') {
values.push(value);
}
else if (typeof value === 'object' && value !== null) {
collectSensitiveValues(value, keysToClean, values);
}
}
return values;
}
export function cleanArgs(parsedArgs, rawArgv) {
const KEYS_TO_CLEAN = ['organization', 'o', 'input', 'i', 'clientCert', 'clientKey', 'caCert'];
let commandInput = rawArgv.join(' ');
const commandArguments = {};
for (const [key, value] of Object.entries(parsedArgs)) {
if (KEYS_TO_CLEAN.includes(key)) {
commandArguments[key] = SECRET_REPLACEMENT;
commandInput = replaceArgs(commandInput, value, SECRET_REPLACEMENT);
}
else if (typeof value === 'string') {
const cleanedValue = cleanString(value);
commandArguments[key] = cleanedValue;
commandInput = replaceArgs(commandInput, value, cleanedValue);
}
else if (Array.isArray(value)) {
commandArguments[key] = value.map(cleanString);
for (const replacedValue of value) {
const newValue = cleanString(replacedValue);
if (commandInput.includes(replacedValue)) {
commandInput = commandInput.replaceAll(replacedValue, newValue);
}
}
}
else if (isPlainObject(value)) {
const sensitiveValues = collectSensitiveValues(value, KEYS_TO_CLEAN);
for (const sensitiveValue of sensitiveValues) {
commandInput = replaceArgs(commandInput, sensitiveValue, SECRET_REPLACEMENT);
}
commandArguments[key] = cleanObject(value, KEYS_TO_CLEAN);
}
else {
commandArguments[key] = value;
}
}
return { arguments: JSON.stringify(commandArguments), raw_input: commandInput };
}
export const cacheAnonymousId = (anonymousId) => {
const isCI = !!process.env.CI;
if (isCI || !anonymousId) {
return;
}
try {
const anonymousIdFile = join(tmpdir(), ANONYMOUS_ID_CACHE_FILE);
writeFileSync(anonymousIdFile, anonymousId);
}
catch (e) {
// Do nothing
}
};
export const getCachedAnonymousId = () => {
const isCI = !!process.env.CI;
if (isCI) {
return;
}
try {
const anonymousIdFile = join(tmpdir(), ANONYMOUS_ID_CACHE_FILE);
if (!existsSync(anonymousIdFile)) {
return;
}
return readFileSync(anonymousIdFile).toString().trim();
}
catch (e) {
return;
}
};
//# sourceMappingURL=telemetry.js.map