Skip to content

Commit be000e0

Browse files
authored
feat: make Confluence API path configurable (#14)
* feat(config): allow configuring Confluence API path * fix(config): allow blank api path to use default Fixes #13
1 parent d01020b commit be000e0

5 files changed

Lines changed: 115 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## [Unreleased]
2+
3+
### Added
4+
- Make the Confluence REST base path configurable to support both `/rest/api` and `/wiki/rest/api`.
5+
16
# [1.7.0](https://github.com/pchuri/confluence-cli/compare/v1.6.0...v1.7.0) (2025-09-28)
27

38

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,19 @@ npx confluence-cli
5858
confluence init
5959
```
6060

61-
The wizard now asks for your authentication method. Choose **Basic** to supply your Atlassian email and API token, or switch to **Bearer** when working with self-hosted/Data Center environments.
61+
The wizard now helps you choose the right API endpoint and authentication method. It recommends `/wiki/rest/api` for Atlassian Cloud domains (e.g., `*.atlassian.net`) and `/rest/api` for self-hosted/Data Center instances, then prompts for Basic (email + token) or Bearer authentication.
6262

6363
### Option 2: Environment Variables
6464
```bash
6565
export CONFLUENCE_DOMAIN="your-domain.atlassian.net"
6666
export CONFLUENCE_API_TOKEN="your-api-token"
6767
export CONFLUENCE_EMAIL="your.email@example.com" # required when using Atlassian Cloud
68+
export CONFLUENCE_API_PATH="/wiki/rest/api" # Cloud default; use /rest/api for Server/DC
6869
# Optional: set to 'bearer' for self-hosted/Data Center instances
6970
export CONFLUENCE_AUTH_TYPE="basic"
7071
```
7172

72-
`CONFLUENCE_AUTH_TYPE` defaults to `basic` when an email is present and falls back to `bearer` otherwise. Provide an email for Atlassian Cloud (Basic auth) or set `CONFLUENCE_AUTH_TYPE=bearer` to keep bearer-token flows for on-premises installations.
73+
`CONFLUENCE_API_PATH` defaults to `/wiki/rest/api` for Atlassian Cloud domains and `/rest/api` otherwise. Override it when your site lives under a custom reverse proxy or on-premises path. `CONFLUENCE_AUTH_TYPE` defaults to `basic` when an email is present and falls back to `bearer` otherwise.
7374

7475
### Getting Your API Token
7576

lib/config.js

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,34 @@ const normalizeAuthType = (rawValue, hasEmail) => {
2727
return hasEmail ? 'basic' : 'bearer';
2828
};
2929

30+
const inferApiPath = (domain) => {
31+
if (!domain) {
32+
return '/rest/api';
33+
}
34+
35+
const normalizedDomain = domain.trim().toLowerCase();
36+
if (normalizedDomain.endsWith('.atlassian.net')) {
37+
return '/wiki/rest/api';
38+
}
39+
40+
return '/rest/api';
41+
};
42+
43+
const normalizeApiPath = (rawValue, domain) => {
44+
const trimmed = (rawValue || '').trim();
45+
46+
if (!trimmed) {
47+
return inferApiPath(domain);
48+
}
49+
50+
if (!trimmed.startsWith('/')) {
51+
throw new Error('Confluence API path must start with "/".');
52+
}
53+
54+
const withoutTrailing = trimmed.replace(/\/+$/, '');
55+
return withoutTrailing || inferApiPath(domain);
56+
};
57+
3058
async function initConfig() {
3159
console.log(chalk.blue('🚀 Confluence CLI Configuration'));
3260
console.log('Please provide your Confluence connection details:\n');
@@ -38,6 +66,27 @@ async function initConfig() {
3866
message: 'Confluence domain (e.g., yourcompany.atlassian.net):',
3967
validate: requiredInput('Domain')
4068
},
69+
{
70+
type: 'input',
71+
name: 'apiPath',
72+
message: 'REST API path (Cloud: /wiki/rest/api, Server: /rest/api):',
73+
default: (responses) => inferApiPath(responses.domain),
74+
validate: (input, responses) => {
75+
const value = (input || '').trim();
76+
if (!value) {
77+
return true;
78+
}
79+
if (!value.startsWith('/')) {
80+
return 'API path must start with "/"';
81+
}
82+
try {
83+
normalizeApiPath(value, responses.domain);
84+
return true;
85+
} catch (error) {
86+
return error.message;
87+
}
88+
}
89+
},
4190
{
4291
type: 'list',
4392
name: 'authType',
@@ -66,6 +115,7 @@ async function initConfig() {
66115

67116
const config = {
68117
domain: answers.domain.trim(),
118+
apiPath: normalizeApiPath(answers.apiPath, answers.domain),
69119
token: answers.token.trim(),
70120
authType: answers.authType,
71121
email: answers.authType === 'basic' ? answers.email.trim() : undefined
@@ -83,9 +133,18 @@ function getConfig() {
83133
const envToken = process.env.CONFLUENCE_API_TOKEN;
84134
const envEmail = process.env.CONFLUENCE_EMAIL;
85135
const envAuthType = process.env.CONFLUENCE_AUTH_TYPE;
136+
const envApiPath = process.env.CONFLUENCE_API_PATH;
86137

87138
if (envDomain && envToken) {
88139
const authType = normalizeAuthType(envAuthType, Boolean(envEmail));
140+
let apiPath;
141+
142+
try {
143+
apiPath = normalizeApiPath(envApiPath, envDomain);
144+
} catch (error) {
145+
console.error(chalk.red(`❌ ${error.message}`));
146+
process.exit(1);
147+
}
89148

90149
if (authType === 'basic' && !envEmail) {
91150
console.error(chalk.red('❌ Basic authentication requires CONFLUENCE_EMAIL.'));
@@ -95,6 +154,7 @@ function getConfig() {
95154

96155
return {
97156
domain: envDomain.trim(),
157+
apiPath,
98158
token: envToken.trim(),
99159
email: envEmail ? envEmail.trim() : undefined,
100160
authType
@@ -104,7 +164,7 @@ function getConfig() {
104164
if (!fs.existsSync(CONFIG_FILE)) {
105165
console.error(chalk.red('❌ No configuration found!'));
106166
console.log(chalk.yellow('Please run "confluence init" to set up your configuration.'));
107-
console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN, CONFLUENCE_API_TOKEN, and CONFLUENCE_EMAIL.'));
167+
console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN, CONFLUENCE_API_TOKEN, CONFLUENCE_EMAIL, and optionally CONFLUENCE_API_PATH.'));
108168
process.exit(1);
109169
}
110170

@@ -114,6 +174,7 @@ function getConfig() {
114174
const trimmedToken = (storedConfig.token || '').trim();
115175
const trimmedEmail = storedConfig.email ? storedConfig.email.trim() : undefined;
116176
const authType = normalizeAuthType(storedConfig.authType, Boolean(trimmedEmail));
177+
let apiPath;
117178

118179
if (!trimmedDomain || !trimmedToken) {
119180
console.error(chalk.red('❌ Configuration file is missing required values.'));
@@ -127,8 +188,17 @@ function getConfig() {
127188
process.exit(1);
128189
}
129190

191+
try {
192+
apiPath = normalizeApiPath(storedConfig.apiPath, trimmedDomain);
193+
} catch (error) {
194+
console.error(chalk.red(`❌ ${error.message}`));
195+
console.log(chalk.yellow('Please rerun "confluence init" to update your API path.'));
196+
process.exit(1);
197+
}
198+
130199
return {
131200
domain: trimmedDomain,
201+
apiPath,
132202
token: trimmedToken,
133203
email: trimmedEmail,
134204
authType

lib/confluence-client.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ class ConfluenceClient {
88
this.token = config.token;
99
this.email = config.email;
1010
this.authType = (config.authType || (this.email ? 'basic' : 'bearer')).toLowerCase();
11-
this.baseURL = `https://${this.domain}/rest/api`;
11+
this.apiPath = this.sanitizeApiPath(config.apiPath);
12+
this.baseURL = `https://${this.domain}${this.apiPath}`;
1213
this.markdown = new MarkdownIt();
1314
this.setupConfluenceMarkdownExtensions();
1415

@@ -23,6 +24,19 @@ class ConfluenceClient {
2324
});
2425
}
2526

27+
sanitizeApiPath(rawPath) {
28+
const fallback = '/rest/api';
29+
const value = (rawPath || '').trim();
30+
31+
if (!value) {
32+
return fallback;
33+
}
34+
35+
const withoutLeading = value.replace(/^\/+/, '');
36+
const normalized = `/${withoutLeading}`.replace(/\/+$/, '');
37+
return normalized || fallback;
38+
}
39+
2640
buildBasicAuthHeader() {
2741
if (!this.email) {
2842
throw new Error('Basic authentication requires an email address.');

tests/confluence-client.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,27 @@ describe('ConfluenceClient', () => {
1010
});
1111
});
1212

13+
describe('api path handling', () => {
14+
test('defaults to /rest/api when path is not provided', () => {
15+
const defaultClient = new ConfluenceClient({
16+
domain: 'example.com',
17+
token: 'no-path-token'
18+
});
19+
20+
expect(defaultClient.baseURL).toBe('https://example.com/rest/api');
21+
});
22+
23+
test('normalizes custom api paths', () => {
24+
const customClient = new ConfluenceClient({
25+
domain: 'cloud.example',
26+
token: 'custom-path',
27+
apiPath: 'wiki/rest/api/'
28+
});
29+
30+
expect(customClient.baseURL).toBe('https://cloud.example/wiki/rest/api');
31+
});
32+
});
33+
1334
describe('authentication setup', () => {
1435
test('uses bearer token headers by default', () => {
1536
const bearerClient = new ConfluenceClient({

0 commit comments

Comments
 (0)