outrank-cli 0.5.0 → 0.6.0

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.
Files changed (58) hide show
  1. package/README.md +317 -20
  2. package/SKILL.md +20 -2
  3. package/bin/outrank-mcp.js +3 -0
  4. package/package.json +21 -5
  5. package/src/adapters/cli/jsonInput.js +42 -0
  6. package/src/adapters/cli/parse.js +124 -0
  7. package/src/adapters/cli/run.js +139 -0
  8. package/src/adapters/mcp/constants.js +49 -0
  9. package/src/adapters/mcp/descriptions.js +87 -0
  10. package/src/adapters/mcp/help.js +34 -0
  11. package/src/adapters/mcp/input.js +195 -0
  12. package/src/adapters/mcp/instructions.js +139 -0
  13. package/src/adapters/mcp/logger.js +45 -0
  14. package/src/adapters/mcp/main.js +36 -0
  15. package/src/adapters/mcp/options.js +118 -0
  16. package/src/adapters/mcp/result.js +33 -0
  17. package/src/adapters/mcp/server.js +174 -0
  18. package/src/adapters/mcp/session.js +274 -0
  19. package/src/adapters/mcp/toolNotes.js +426 -0
  20. package/src/adapters/mcp/toolsets.js +106 -0
  21. package/src/commands/articles.js +285 -301
  22. package/src/commands/auth.js +204 -86
  23. package/src/commands/billing.js +102 -104
  24. package/src/commands/improvements.js +319 -211
  25. package/src/commands/index.js +22 -0
  26. package/src/commands/keywords.js +266 -249
  27. package/src/commands/marketplace/buyer.js +386 -0
  28. package/src/commands/marketplace/index.js +83 -0
  29. package/src/commands/marketplace/notifications.js +60 -0
  30. package/src/commands/marketplace/seller.js +595 -0
  31. package/src/commands/marketplace/shared.js +27 -0
  32. package/src/commands/products/backlinks.js +181 -0
  33. package/src/commands/products/core.js +149 -0
  34. package/src/commands/products/gsc.js +250 -0
  35. package/src/commands/products/index.js +174 -0
  36. package/src/commands/products/integrations.js +343 -0
  37. package/src/commands/products/linking.js +192 -0
  38. package/src/commands/products/shared.js +8 -0
  39. package/src/commands/registry.js +272 -0
  40. package/src/commands/subscription.js +20 -30
  41. package/src/commands/usage.js +17 -30
  42. package/src/core/config.js +243 -62
  43. package/src/core/constants.js +101 -0
  44. package/src/core/context.js +311 -0
  45. package/src/core/errors.js +39 -0
  46. package/src/core/execute.js +71 -0
  47. package/src/core/files.js +186 -0
  48. package/src/core/http.js +393 -0
  49. package/src/core/profileSelection.js +7 -0
  50. package/src/core/query.js +11 -0
  51. package/src/core/redact.js +27 -0
  52. package/src/core/schema.js +222 -0
  53. package/src/core/version.js +12 -0
  54. package/src/index.js +30 -58
  55. package/src/commands/marketplace.js +0 -740
  56. package/src/commands/products.js +0 -1185
  57. package/src/core/api.js +0 -82
  58. package/src/core/args.js +0 -144
@@ -1,80 +1,261 @@
1
+ import { randomBytes } from 'node:crypto';
1
2
  import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import fs from 'node:fs/promises';
4
5
 
5
- import { ensureDirectory, fileExists } from './io.js';
6
+ import {
7
+ API_KEY_SUFFIX_LENGTH,
8
+ CONFIG_DIR_MODE,
9
+ CONFIG_FILE_MODE,
10
+ CONFIG_TEMP_SUFFIX_BYTES,
11
+ DEFAULT_PROFILE_NAME,
12
+ MAX_CONFIG_SYMLINK_HOPS,
13
+ MAX_PROFILE_NAME_LENGTH,
14
+ MIN_API_KEY_LENGTH_FOR_SUFFIX,
15
+ PROFILE_NAME_PATTERN,
16
+ } from './constants.js';
17
+ import { CliErrorCode, codedError } from './errors.js';
6
18
 
7
- const DEFAULT_BASE_URL = 'https://www.outrank.so';
19
+ const CONFIG_DIR_ENV = 'OUTRANK_CONFIG_DIR';
20
+ const CONFIG_DIR_NAME = '.outrank';
21
+ const CONFIG_FILE_NAME = 'config.json';
22
+ const TEMP_FILE_EXTENSION = '.tmp';
23
+ const BYTE_ORDER_MARK = '';
24
+ const PRINTABLE_ASCII_PATTERN = /^[\x20-\x7e]$/;
25
+ const TRUNCATION_MARK = '...';
26
+ export const API_KEY_FIELD = 'apiKey';
27
+ export const BASE_URL_FIELD = 'baseUrl';
28
+ const DEFAULT_PROFILE_FIELD = 'defaultProfile';
29
+ const PROFILES_FIELD = 'profiles';
30
+ const PROFILE_FIELDS = Object.freeze([API_KEY_FIELD, BASE_URL_FIELD]);
31
+ const MANAGED_TOP_LEVEL_KEYS = new Set([API_KEY_FIELD, BASE_URL_FIELD, DEFAULT_PROFILE_FIELD, PROFILES_FIELD]);
32
+ const MISSING_FILE_CODE = 'ENOENT';
33
+ const SYMLINK_LOOP_CODE = 'ELOOP';
34
+ const UNKNOWN_FS_ERROR_REASON = 'unknown filesystem error';
35
+ const INVALID_JSON_REASON = 'it is not valid JSON';
36
+ const NOT_AN_OBJECT_REASON = 'it is not a JSON object';
37
+ const CONFIG_UNREADABLE_GUIDANCE =
38
+ 'Fix or delete the file at details.config_path, or point OUTRANK_CONFIG_DIR at another directory, then retry. ' +
39
+ '`auth login` and `auth logout` never overwrite a config file they cannot fully parse.';
40
+ const CONFIG_UNWRITABLE_GUIDANCE =
41
+ 'Nothing was saved. Check that details.config_path is writable and not locked by another program, or point ' +
42
+ 'OUTRANK_CONFIG_DIR at a writable directory, then run the command again.';
8
43
 
9
- const getConfigDir = () => process.env.OUTRANK_CONFIG_DIR || path.join(os.homedir(), '.outrank');
10
- export const getConfigPath = () => path.join(getConfigDir(), 'config.json');
44
+ const getConfigDir = () => {
45
+ const override = process.env[CONFIG_DIR_ENV];
46
+ return override ? path.resolve(override) : path.join(os.homedir(), CONFIG_DIR_NAME);
47
+ };
48
+ export const getConfigPath = () => path.join(getConfigDir(), CONFIG_FILE_NAME);
49
+
50
+ export const isSet = (value) => typeof value === 'string' && value !== '';
51
+ const isPlainObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
52
+
53
+ export const isValidProfileName = (name) =>
54
+ typeof name === 'string' && name.length <= MAX_PROFILE_NAME_LENGTH && PROFILE_NAME_PATTERN.test(name);
55
+
56
+ export const redactApiKey = (apiKey) =>
57
+ typeof apiKey === 'string' && apiKey.length >= MIN_API_KEY_LENGTH_FOR_SUFFIX
58
+ ? apiKey.slice(-API_KEY_SUFFIX_LENGTH)
59
+ : null;
60
+
61
+ const ownEntry = (source, name) => (isPlainObject(source) && Object.hasOwn(source, name) ? source[name] : undefined);
62
+
63
+ const pickProfileFields = (source) =>
64
+ Object.fromEntries(PROFILE_FIELDS.filter((field) => isSet(source?.[field])).map((field) => [field, source[field]]));
65
+
66
+ const preservedProfileFields = (source) =>
67
+ Object.fromEntries(Object.entries(source ?? {}).filter(([key]) => !PROFILE_FIELDS.includes(key)));
68
+
69
+ const omitField = (source, field) => Object.fromEntries(Object.entries(source).filter(([key]) => key !== field));
70
+
71
+ const configUnreadableError = (reason) =>
72
+ codedError(
73
+ CliErrorCode.CONFIG_UNREADABLE,
74
+ `The stored config file could not be read: ${reason}.`,
75
+ CONFIG_UNREADABLE_GUIDANCE,
76
+ { config_path: getConfigPath() },
77
+ );
78
+
79
+ const configUnwritableError = (error) =>
80
+ codedError(
81
+ CliErrorCode.CONFIG_UNWRITABLE,
82
+ `The config file could not be written: ${error.code ?? UNKNOWN_FS_ERROR_REASON}.`,
83
+ CONFIG_UNWRITABLE_GUIDANCE,
84
+ { config_path: getConfigPath(), fs_code: error.code ?? null },
85
+ );
86
+
87
+ const isPrintableAscii = (character) => PRINTABLE_ASCII_PATTERN.test(character);
88
+
89
+ export const describeUntrustedValue = (value) => {
90
+ if (typeof value !== 'string') return `a non-string value (${value === null ? 'null' : typeof value})`;
91
+ const printable = [...value].filter(isPrintableAscii).join('');
92
+ const excerpt = printable.slice(0, MAX_PROFILE_NAME_LENGTH);
93
+ return JSON.stringify(excerpt.length < printable.length ? `${excerpt}${TRUNCATION_MARK}` : excerpt);
94
+ };
95
+
96
+ const fieldTypeProblem = (owner, entry) => {
97
+ for (const field of PROFILE_FIELDS) {
98
+ if (entry[field] !== undefined && typeof entry[field] !== 'string') return `${owner} has a non-string ${field}`;
99
+ }
100
+ return null;
101
+ };
102
+
103
+ const profilesShapeProblem = (profiles) => {
104
+ if (profiles === undefined) return null;
105
+ if (!isPlainObject(profiles)) return `${PROFILES_FIELD} is not a JSON object`;
106
+ for (const [name, entry] of Object.entries(profiles)) {
107
+ if (!isValidProfileName(name)) return `profile name ${describeUntrustedValue(name)} is invalid`;
108
+ if (!isPlainObject(entry)) return `profile ${describeUntrustedValue(name)} is not a JSON object`;
109
+ const problem = fieldTypeProblem(`profile ${describeUntrustedValue(name)}`, entry);
110
+ if (problem) return problem;
111
+ }
112
+ return null;
113
+ };
114
+
115
+ const configShapeProblem = (raw) => {
116
+ if (!isPlainObject(raw)) return null;
117
+ const topLevelProblem = fieldTypeProblem('the config file', raw);
118
+ if (topLevelProblem) return topLevelProblem;
119
+ const defaultProfile = ownEntry(raw, DEFAULT_PROFILE_FIELD);
120
+ if (defaultProfile !== undefined && !isValidProfileName(defaultProfile)) {
121
+ return `${DEFAULT_PROFILE_FIELD} ${describeUntrustedValue(defaultProfile)} is not a valid profile name`;
122
+ }
123
+ return profilesShapeProblem(ownEntry(raw, PROFILES_FIELD));
124
+ };
125
+
126
+ export const readProfiles = (raw) => {
127
+ const problem = configShapeProblem(raw);
128
+ if (problem) throw configUnreadableError(problem);
129
+ const mirror = pickProfileFields(raw);
130
+ const defaultProfile = ownEntry(raw, DEFAULT_PROFILE_FIELD) ?? DEFAULT_PROFILE_NAME;
131
+ const rawProfiles = ownEntry(raw, PROFILES_FIELD);
132
+ if (rawProfiles === undefined) {
133
+ const profiles = Object.keys(mirror).length > 0 ? { [defaultProfile]: mirror } : {};
134
+ return { defaultProfile, profiles };
135
+ }
136
+ const profiles = Object.fromEntries(
137
+ Object.entries(rawProfiles).map(([name, entry]) => [name, pickProfileFields(entry)]),
138
+ );
139
+ if (Object.keys(mirror).length > 0) profiles[defaultProfile] = mirror;
140
+ return { defaultProfile, profiles };
141
+ };
142
+
143
+ export const serializeProfiles = (raw, { defaultProfile, profiles }) => {
144
+ const rawProfiles = ownEntry(raw, PROFILES_FIELD) ?? {};
145
+ const preserved = Object.fromEntries(
146
+ Object.entries(isPlainObject(raw) ? raw : {}).filter(([key]) => !MANAGED_TOP_LEVEL_KEYS.has(key)),
147
+ );
148
+ const merged = Object.fromEntries(
149
+ Object.entries(profiles).map(([name, entry]) => [
150
+ name,
151
+ { ...preservedProfileFields(ownEntry(rawProfiles, name)), ...entry },
152
+ ]),
153
+ );
154
+ return { ...pickProfileFields(profiles[defaultProfile]), defaultProfile, profiles: merged, ...preserved };
155
+ };
11
156
 
12
157
  export const loadConfig = async () => {
13
- const configPath = getConfigPath();
14
- if (!(await fileExists(configPath))) {
15
- return {};
158
+ let text;
159
+ try {
160
+ text = await fs.readFile(getConfigPath(), 'utf8');
161
+ } catch (error) {
162
+ if (error.code === MISSING_FILE_CODE) return {};
163
+ throw configUnreadableError(error.code ?? UNKNOWN_FS_ERROR_REASON);
164
+ }
165
+ let config;
166
+ try {
167
+ config = JSON.parse(text.startsWith(BYTE_ORDER_MARK) ? text.slice(BYTE_ORDER_MARK.length) : text);
168
+ } catch {
169
+ throw configUnreadableError(INVALID_JSON_REASON);
170
+ }
171
+ if (!isPlainObject(config)) throw configUnreadableError(NOT_AN_OBJECT_REASON);
172
+ const problem = configShapeProblem(config);
173
+ if (problem) throw configUnreadableError(problem);
174
+ return config;
175
+ };
176
+
177
+ export const loadProfiles = async () => readProfiles(await loadConfig());
178
+
179
+ const isSymbolicLink = async (filePath) => {
180
+ try {
181
+ return (await fs.lstat(filePath)).isSymbolicLink();
182
+ } catch (error) {
183
+ if (error.code === MISSING_FILE_CODE) return false;
184
+ throw error;
185
+ }
186
+ };
187
+
188
+ const resolveDanglingLink = async (filePath) => {
189
+ let current = filePath;
190
+ for (let hop = 0; hop < MAX_CONFIG_SYMLINK_HOPS; hop += 1) {
191
+ if (!(await isSymbolicLink(current))) return current;
192
+ const directory = await fs.realpath(path.dirname(current));
193
+ current = path.resolve(directory, await fs.readlink(current));
194
+ }
195
+ throw Object.assign(new Error(`Too many symbolic links at ${filePath}`), { code: SYMLINK_LOOP_CODE });
196
+ };
197
+
198
+ const resolveWriteTarget = async (filePath) => {
199
+ try {
200
+ return await fs.realpath(filePath);
201
+ } catch (error) {
202
+ if (error.code === MISSING_FILE_CODE) return resolveDanglingLink(filePath);
203
+ throw error;
16
204
  }
205
+ };
17
206
 
18
- const raw = await fs.readFile(configPath, 'utf8');
207
+ const removeQuietly = async (filePath) => {
19
208
  try {
20
- const config = JSON.parse(raw);
21
- return config && typeof config === 'object' ? config : {};
209
+ await fs.rm(filePath, { force: true });
22
210
  } catch {
23
- return {};
211
+ return;
24
212
  }
25
213
  };
26
214
 
27
- export const saveConfig = async (config) => {
28
- const configPath = getConfigPath();
29
- await ensureDirectory(path.dirname(configPath));
30
- await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
215
+ const writeFileAtomically = async (filePath, text) => {
216
+ await fs.mkdir(path.dirname(filePath), { recursive: true, mode: CONFIG_DIR_MODE });
217
+ const target = await resolveWriteTarget(filePath);
218
+ const suffix = randomBytes(CONFIG_TEMP_SUFFIX_BYTES).toString('hex');
219
+ const tempPath = `${target}.${process.pid}.${suffix}${TEMP_FILE_EXTENSION}`;
220
+ try {
221
+ await fs.writeFile(tempPath, text, { encoding: 'utf8', mode: CONFIG_FILE_MODE, flag: 'wx' });
222
+ await fs.chmod(tempPath, CONFIG_FILE_MODE);
223
+ await fs.rename(tempPath, target);
224
+ } catch (error) {
225
+ await removeQuietly(tempPath);
226
+ throw error;
227
+ }
31
228
  };
32
229
 
33
- export const clearStoredAuth = async () => {
34
- const configPath = getConfigPath();
35
- const current = await loadConfig();
36
- if (!(await fileExists(configPath))) {
37
- return current;
230
+ export const saveProfiles = async (raw, state) => {
231
+ const serialized = serializeProfiles(raw, state);
232
+ try {
233
+ await writeFileAtomically(getConfigPath(), `${JSON.stringify(serialized, null, 2)}\n`);
234
+ } catch (error) {
235
+ throw configUnwritableError(error);
38
236
  }
39
- const next = { ...current };
40
- delete next.apiKey;
41
- await saveConfig(next);
42
- return next;
43
- };
44
-
45
- const normalizeBaseUrl = (value) => {
46
- const url = new URL(value || DEFAULT_BASE_URL);
47
- return url.origin.replace(/\/$/, '');
48
- };
49
-
50
- export const resolveRuntimeConfig = async (parsed) => {
51
- const config = await loadConfig();
52
- const apiKey =
53
- process.env.OUTRANK_API_KEY ||
54
- (parsed ? parsed.options.get('api-key')?.slice(-1)[0] : undefined) ||
55
- config.apiKey;
56
- const baseUrl =
57
- process.env.OUTRANK_BASE_URL ||
58
- (parsed ? parsed.options.get('base-url')?.slice(-1)[0] : undefined) ||
59
- config.baseUrl ||
60
- DEFAULT_BASE_URL;
61
-
62
- return {
63
- apiKey,
64
- baseUrl: normalizeBaseUrl(baseUrl),
65
- config,
66
- };
67
- };
68
-
69
- export const storeAuthConfig = async ({ apiKey, baseUrl }) => {
70
- const current = await loadConfig();
71
- const next = {
72
- ...current,
73
- ...(apiKey ? { apiKey } : {}),
74
- ...(baseUrl ? { baseUrl: normalizeBaseUrl(baseUrl) } : {}),
75
- };
76
- await saveConfig(next);
77
- return next;
78
- };
79
-
80
- export const getDefaultBaseUrl = () => DEFAULT_BASE_URL;
237
+ return serialized;
238
+ };
239
+
240
+ const hasStoredApiKey = (profiles) => Object.values(profiles).some((profile) => isSet(profile[API_KEY_FIELD]));
241
+
242
+ export const storeProfileAuth = async ({ profile, apiKey, baseUrl, makeDefault = false }) => {
243
+ const raw = await loadConfig();
244
+ const state = readProfiles(raw);
245
+ const stored = { ...(ownEntry(state.profiles, profile) ?? {}), [API_KEY_FIELD]: apiKey };
246
+ if (baseUrl) stored[BASE_URL_FIELD] = baseUrl;
247
+ const profiles = { ...state.profiles, [profile]: stored };
248
+ const defaultProfile = makeDefault || !hasStoredApiKey(state.profiles) ? profile : state.defaultProfile;
249
+ await saveProfiles(raw, { defaultProfile, profiles });
250
+ return { profile, isDefault: defaultProfile === profile, stored, defaultProfile, profiles };
251
+ };
252
+
253
+ export const clearProfileAuth = async ({ profile }) => {
254
+ const raw = await loadConfig();
255
+ const state = readProfiles(raw);
256
+ const entry = ownEntry(state.profiles, profile);
257
+ if (!entry || !isSet(entry[API_KEY_FIELD])) return { ...state, removed: false };
258
+ const profiles = { ...state.profiles, [profile]: omitField(entry, API_KEY_FIELD) };
259
+ await saveProfiles(raw, { defaultProfile: state.defaultProfile, profiles });
260
+ return { defaultProfile: state.defaultProfile, profiles, removed: true };
261
+ };
@@ -0,0 +1,101 @@
1
+ const BYTES_PER_MB = 1024 * 1024;
2
+
3
+ export const API_BASE_PATH = '/api/agent/v1';
4
+ export const DEFAULT_BASE_URL = 'https://www.outrank.so';
5
+ export const ALLOWED_BASE_URL_HOSTS = Object.freeze(['www.outrank.so', 'outrank.so']);
6
+ export const LOOPBACK_HOSTNAMES = Object.freeze(['localhost', '127.0.0.1', '[::1]']);
7
+ export const ALLOW_CUSTOM_BASE_URL_ENV = 'OUTRANK_ALLOW_CUSTOM_BASE_URL';
8
+ export const API_KEY_ENV = 'OUTRANK_API_KEY';
9
+ export const BASE_URL_ENV = 'OUTRANK_BASE_URL';
10
+ export const PROFILE_ENV = 'OUTRANK_PROFILE';
11
+ export const ProfileSource = Object.freeze({
12
+ FLAG: 'flag',
13
+ ENV: 'env',
14
+ DEFAULT: 'default',
15
+ });
16
+ export const Surface = Object.freeze({ CLI: 'cli', MCP: 'mcp' });
17
+ export const DEFAULT_PROFILE_NAME = 'default';
18
+ export const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
19
+ export const MAX_PROFILE_NAME_LENGTH = 64;
20
+ export const API_KEY_SUFFIX_LENGTH = 4;
21
+ export const MIN_API_KEY_LENGTH_FOR_SUFFIX = 8;
22
+ export const HEADER_SAFE_API_KEY_PATTERN = /^[\x21-\x7e]+$/;
23
+
24
+ export const HTTP_REQUEST_TIMEOUT_MS = 60_000;
25
+ export const HTTP_LONG_RUNNING_TIMEOUT_MS = 330_000;
26
+ export const MCP_STARTUP_RESOLVE_TIMEOUT_MS = 3_000;
27
+
28
+ export const MAX_IMAGE_UPLOAD_MEGABYTES = 5;
29
+ export const MAX_IMAGE_UPLOAD_BYTES = MAX_IMAGE_UPLOAD_MEGABYTES * BYTES_PER_MB;
30
+ export const MAX_UPSTREAM_ERROR_BODY_CHARS = 500;
31
+ export const MAX_RATE_LIMIT_HEADER_LENGTH = 64;
32
+ export const MAX_TOOL_NAME_LENGTH = 46;
33
+
34
+ export const LIST_MIN_LIMIT = 1;
35
+ export const LIST_MAX_LIMIT = 100;
36
+ export const MIN_OFFSET = 0;
37
+ export const MIN_SEARCH_VOLUME = 0;
38
+ export const MIN_DIFFICULTY = 0;
39
+ export const MAX_DIFFICULTY = 100;
40
+ export const KEYWORDS_SUGGEST_MAX_LIMIT = 20;
41
+ export const IMPROVEMENTS_CANDIDATES_MAX_LIMIT = 50;
42
+ export const GSC_MIN_MONTHS = 1;
43
+ export const GSC_MAX_MONTHS = 16;
44
+ export const GSC_SEARCH_ANALYTICS_MAX_LIMIT = 1000;
45
+ export const KEYWORDS_GENERATE_MIN_COUNT = 5;
46
+ export const KEYWORDS_GENERATE_MAX_COUNT = 30;
47
+
48
+ export const BILLING_MIN_ADDITIONAL_PRODUCTS = 1;
49
+ export const BILLING_MAX_ADDITIONAL_PRODUCTS = 30;
50
+ export const BILLING_MIN_CONFIRM_CHARGE_CENTS = 0;
51
+
52
+ export const HttpStatus = Object.freeze({
53
+ NO_CONTENT: 204,
54
+ RESET_CONTENT: 205,
55
+ NOT_FOUND: 404,
56
+ CONFLICT: 409,
57
+ TOO_MANY_REQUESTS: 429,
58
+ BAD_GATEWAY: 502,
59
+ GATEWAY_TIMEOUT: 504,
60
+ });
61
+ export const HTTP_STATUS_CONFLICT = HttpStatus.CONFLICT;
62
+ export const NO_BODY_STATUSES = Object.freeze([HttpStatus.NO_CONTENT, HttpStatus.RESET_CONTENT]);
63
+ export const LONG_RUNNING_GATEWAY_STATUSES = Object.freeze([HttpStatus.BAD_GATEWAY, HttpStatus.GATEWAY_TIMEOUT]);
64
+
65
+ export const JSON_CONTENT_TYPE = 'application/json';
66
+ export const CONFIG_FILE_MODE = 0o600;
67
+ export const CONFIG_DIR_MODE = 0o700;
68
+ export const CONFIG_TEMP_SUFFIX_BYTES = 6;
69
+ export const MAX_CONFIG_SYMLINK_HOPS = 8;
70
+
71
+ export const RESERVED_OPTION_NAMES = Object.freeze([
72
+ 'api-key',
73
+ 'base-url',
74
+ 'allow-custom-base-url',
75
+ 'profile',
76
+ 'json',
77
+ 'json-file',
78
+ 'json-stdin',
79
+ 'help',
80
+ ]);
81
+
82
+ export const RATE_LIMIT_HEADERS = Object.freeze({
83
+ retryAfter: 'retry-after',
84
+ limitMinute: 'x-ratelimit-limit-minute',
85
+ remainingMinute: 'x-ratelimit-remaining-minute',
86
+ limitDay: 'x-ratelimit-limit-day',
87
+ remainingDay: 'x-ratelimit-remaining-day',
88
+ });
89
+
90
+ export const SENSITIVE_BODY_FIELDS = Object.freeze([
91
+ 'api_key',
92
+ 'apiKey',
93
+ 'api_token',
94
+ 'access_token',
95
+ 'refresh_token',
96
+ 'server_api_key',
97
+ 'secret',
98
+ 'password',
99
+ 'token',
100
+ 'authorization',
101
+ ]);