Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
API_URL_OPT,
CONFIG_OPT,
DEFAULT_NAMESPACE,
EXTRA_HEADERS,
EXTRACTOR,
FILE_PATTERNS,
FORMAT_OPT,
Expand Down Expand Up @@ -150,6 +151,7 @@ const preHandler = (config: Schema) =>
: config.projectId !== undefined
? Number(config.projectId)
: undefined,
extraHeaders: opts.extraHeaders,
});

cmd.setOptionValue('client', client);
Expand Down Expand Up @@ -182,6 +184,7 @@ async function run() {
program.addOption(CONFIG_OPT);
program.addOption(API_URL_OPT.default(config.apiUrl ?? DEFAULT_API_URL));
program.addOption(API_KEY_OPT.default(config.apiKey));
program.addOption(EXTRA_HEADERS);
program.addOption(PROJECT_ID_OPT.default(config.projectId ?? -1));
program.addOption(PROJECT_BRANCH.default(config.branch));
program.addOption(FORMAT_OPT.default(config.format ?? 'JSON_TOLGEE'));
Expand Down
5 changes: 4 additions & 1 deletion src/client/ApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,22 @@ export type ApiClientProps = {
apiKey?: string;
projectId?: number | undefined;
autoThrow?: boolean;
extraHeaders?: Record<string, string>;
};

export function createApiClient({
baseUrl,
apiKey,
projectId,
autoThrow = false,
extraHeaders,
}: ApiClientProps) {
const computedProjectId =
projectId ?? (apiKey ? projectIdFromKey(apiKey) : undefined);
const apiClient = createClient<paths>({
baseUrl,
headers: {
...extraHeaders,
'user-agent': USER_AGENT,
'x-api-key': apiKey,
},
Comment thread
Nabal22 marked this conversation as resolved.
Expand Down Expand Up @@ -110,7 +113,7 @@ export function createApiClient({
return getApiKeyInformation(apiClient, apiKey!);
},
getSettings(): ApiClientProps {
return { baseUrl, apiKey, projectId, autoThrow };
return { baseUrl, apiKey, projectId, autoThrow, extraHeaders };
},
};
}
Expand Down
9 changes: 9 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { resolve } from 'path';
import { Option, InvalidArgumentError } from 'commander';
import { createTolgeeClient } from './client/TolgeeClient.js';
import { VerboseOption } from './extractor/index.js';
import { parseExtraHeadersArg } from './utils/extraHeaders.js';

function parseProjectId(v: string) {
const val = Number(v);
Expand Down Expand Up @@ -39,6 +40,7 @@ export type BaseOptions = {
patterns: string[] | undefined;
strictNamespace: boolean | undefined;
verbose: VerboseOption[] | boolean | undefined;
extraHeaders?: Record<string, string>;
};

export const API_KEY_OPT = new Option(
Expand Down Expand Up @@ -141,3 +143,10 @@ export const VERBOSE = new Option(
'-v, --verbose [rules...]',
'Enable verbose logging. If you want more info to be logged pass an option.'
).choices(['extractor']);

export const EXTRA_HEADERS = new Option(
'--extra-headers <headers>',
"Additional HTTP headers to send with every Tolgee API request. Accepts comma-separated Name=Value pairs (e.g. 'X-Foo=bar,X-Baz=qux'). Useful for traversing Cloudflare Access, WAFs, or other gateways."
)
.env('TOLGEE_EXTRA_HEADERS')
.argParser(parseExtraHeadersArg);
20 changes: 20 additions & 0 deletions src/utils/extraHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { InvalidArgumentError } from 'commander';

export function parseExtraHeadersArg(value: string): Record<string, string> {
const trimmed = value.trim();
if (!trimmed) return {};

const out: Record<string, string> = {};
for (const pair of trimmed.split(',')) {
const segment = pair.trim();
if (!segment) continue;
const idx = segment.indexOf('=');
if (idx <= 0) {
throw new InvalidArgumentError(
`Invalid extra header "${segment}". Use Name=Value.`
);
}
out[segment.slice(0, idx).trim()] = segment.slice(idx + 1).trim();
}
return out;
}
39 changes: 39 additions & 0 deletions test/unit/extraHeaders.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { parseExtraHeadersArg } from '#cli/utils/extraHeaders.js';

describe('parseExtraHeadersArg', () => {
it('parses comma-separated Name=Value pairs', () => {
expect(parseExtraHeadersArg('X-Foo=bar, X-Baz=qux')).toEqual({
'X-Foo': 'bar',
'X-Baz': 'qux',
});
});

it('trims whitespace around names and values', () => {
expect(parseExtraHeadersArg(' X-Foo = bar ')).toEqual({
'X-Foo': 'bar',
});
});

it('keeps "=" characters inside the value', () => {
expect(parseExtraHeadersArg('X-Token=abc=def=ghi')).toEqual({
'X-Token': 'abc=def=ghi',
});
});

it('skips empty segments', () => {
expect(parseExtraHeadersArg('X-Foo=bar,,X-Baz=qux,')).toEqual({
'X-Foo': 'bar',
'X-Baz': 'qux',
});
});

it('returns empty for empty input', () => {
expect(parseExtraHeadersArg('')).toEqual({});
expect(parseExtraHeadersArg(' ')).toEqual({});
});

it('rejects malformed pairs', () => {
expect(() => parseExtraHeadersArg('not-a-header')).toThrow();
expect(() => parseExtraHeadersArg('=missing-name')).toThrow();
});
});