UNPKG

@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
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