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.
- package/README.md +317 -20
- package/SKILL.md +20 -2
- package/bin/outrank-mcp.js +3 -0
- package/package.json +21 -5
- package/src/adapters/cli/jsonInput.js +42 -0
- package/src/adapters/cli/parse.js +124 -0
- package/src/adapters/cli/run.js +139 -0
- package/src/adapters/mcp/constants.js +49 -0
- package/src/adapters/mcp/descriptions.js +87 -0
- package/src/adapters/mcp/help.js +34 -0
- package/src/adapters/mcp/input.js +195 -0
- package/src/adapters/mcp/instructions.js +139 -0
- package/src/adapters/mcp/logger.js +45 -0
- package/src/adapters/mcp/main.js +36 -0
- package/src/adapters/mcp/options.js +118 -0
- package/src/adapters/mcp/result.js +33 -0
- package/src/adapters/mcp/server.js +174 -0
- package/src/adapters/mcp/session.js +274 -0
- package/src/adapters/mcp/toolNotes.js +426 -0
- package/src/adapters/mcp/toolsets.js +106 -0
- package/src/commands/articles.js +285 -301
- package/src/commands/auth.js +204 -86
- package/src/commands/billing.js +102 -104
- package/src/commands/improvements.js +319 -211
- package/src/commands/index.js +22 -0
- package/src/commands/keywords.js +266 -249
- package/src/commands/marketplace/buyer.js +386 -0
- package/src/commands/marketplace/index.js +83 -0
- package/src/commands/marketplace/notifications.js +60 -0
- package/src/commands/marketplace/seller.js +595 -0
- package/src/commands/marketplace/shared.js +27 -0
- package/src/commands/products/backlinks.js +181 -0
- package/src/commands/products/core.js +149 -0
- package/src/commands/products/gsc.js +250 -0
- package/src/commands/products/index.js +174 -0
- package/src/commands/products/integrations.js +343 -0
- package/src/commands/products/linking.js +192 -0
- package/src/commands/products/shared.js +8 -0
- package/src/commands/registry.js +272 -0
- package/src/commands/subscription.js +20 -30
- package/src/commands/usage.js +17 -30
- package/src/core/config.js +243 -62
- package/src/core/constants.js +101 -0
- package/src/core/context.js +311 -0
- package/src/core/errors.js +39 -0
- package/src/core/execute.js +71 -0
- package/src/core/files.js +186 -0
- package/src/core/http.js +393 -0
- package/src/core/profileSelection.js +7 -0
- package/src/core/query.js +11 -0
- package/src/core/redact.js +27 -0
- package/src/core/schema.js +222 -0
- package/src/core/version.js +12 -0
- package/src/index.js +30 -58
- package/src/commands/marketplace.js +0 -740
- package/src/commands/products.js +0 -1185
- package/src/core/api.js +0 -82
- package/src/core/args.js +0 -144
package/src/core/config.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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 = () =>
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
207
|
+
const removeQuietly = async (filePath) => {
|
|
19
208
|
try {
|
|
20
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
await
|
|
30
|
-
|
|
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
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
|
|
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
|
+
]);
|