confluence-cli 1.30.0 → 1.30.1
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 +29 -3
- package/bin/confluence.js +8 -2
- package/lib/config.js +221 -17
- package/lib/confluence-client.js +87 -8
- package/npm-shrinkwrap.json +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ This creates `.claude/skills/confluence/SKILL.md` in your current directory. Cla
|
|
|
102
102
|
confluence init
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
The wizard 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/username + token/password) or
|
|
105
|
+
The wizard 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/username + token/password), Bearer, or client-certificate (mTLS) authentication.
|
|
106
106
|
|
|
107
107
|
### Option 2: Non-interactive Setup (CLI Flags)
|
|
108
108
|
|
|
@@ -138,6 +138,17 @@ confluence --profile staging init \
|
|
|
138
138
|
--token "your-personal-access-token"
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
**mTLS profile** (self-hosted or reverse-proxied Confluence APIs):
|
|
142
|
+
```bash
|
|
143
|
+
confluence --profile corp init \
|
|
144
|
+
--domain "docs.example.com" \
|
|
145
|
+
--api-path "/confluence/rest/api" \
|
|
146
|
+
--auth-type "mtls" \
|
|
147
|
+
--tls-client-cert "~/.certs/client.pem" \
|
|
148
|
+
--tls-client-key "~/.certs/client.key" \
|
|
149
|
+
--tls-ca-cert "~/.certs/ca-chain.pem"
|
|
150
|
+
```
|
|
151
|
+
|
|
141
152
|
**Hybrid mode** (some fields provided, rest via prompts):
|
|
142
153
|
```bash
|
|
143
154
|
# Domain and token provided, will prompt for auth method and email
|
|
@@ -150,9 +161,12 @@ confluence init --email "user@example.com" --token "your-api-token"
|
|
|
150
161
|
**Available flags:**
|
|
151
162
|
- `-d, --domain <domain>` - Confluence domain (e.g., `company.atlassian.net`)
|
|
152
163
|
- `-p, --api-path <path>` - REST API path (e.g., `/wiki/rest/api`)
|
|
153
|
-
- `-a, --auth-type <type>` - Authentication type: `basic` or `
|
|
164
|
+
- `-a, --auth-type <type>` - Authentication type: `basic`, `bearer`, or `mtls`
|
|
154
165
|
- `-e, --email <email>` - Email or username for basic authentication
|
|
155
166
|
- `-t, --token <token>` - API token or password
|
|
167
|
+
- `--tls-client-cert <path>` - Client certificate for mTLS authentication
|
|
168
|
+
- `--tls-client-key <path>` - Client private key for mTLS authentication
|
|
169
|
+
- `--tls-ca-cert <path>` - Optional CA certificate chain for mTLS authentication
|
|
156
170
|
- `--read-only` - Enable read-only mode (blocks all write operations)
|
|
157
171
|
|
|
158
172
|
⚠️ **Security note:** While flags work, storing tokens in shell history is risky. Prefer environment variables (Option 3) for production environments.
|
|
@@ -169,6 +183,16 @@ export CONFLUENCE_AUTH_TYPE="basic"
|
|
|
169
183
|
export CONFLUENCE_PROFILE="default"
|
|
170
184
|
```
|
|
171
185
|
|
|
186
|
+
**mTLS environment variables**:
|
|
187
|
+
```bash
|
|
188
|
+
export CONFLUENCE_DOMAIN="docs.example.com"
|
|
189
|
+
export CONFLUENCE_API_PATH="/confluence/rest/api"
|
|
190
|
+
export CONFLUENCE_AUTH_TYPE="mtls"
|
|
191
|
+
export CONFLUENCE_TLS_CLIENT_CERT="~/.certs/client.pem"
|
|
192
|
+
export CONFLUENCE_TLS_CLIENT_KEY="~/.certs/client.key"
|
|
193
|
+
export CONFLUENCE_TLS_CA_CERT="~/.certs/ca-chain.pem" # optional
|
|
194
|
+
```
|
|
195
|
+
|
|
172
196
|
**Scoped API token** (recommended for agents):
|
|
173
197
|
```bash
|
|
174
198
|
export CONFLUENCE_DOMAIN="api.atlassian.com"
|
|
@@ -178,7 +202,7 @@ export CONFLUENCE_EMAIL="user@example.com"
|
|
|
178
202
|
export CONFLUENCE_API_TOKEN="your-scoped-token"
|
|
179
203
|
```
|
|
180
204
|
|
|
181
|
-
`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.
|
|
205
|
+
`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. For `mtls`, set `CONFLUENCE_TLS_CLIENT_CERT` and `CONFLUENCE_TLS_CLIENT_KEY`; `CONFLUENCE_TLS_CA_CERT` is optional.
|
|
182
206
|
|
|
183
207
|
**Read-only mode** (recommended for AI agents):
|
|
184
208
|
```bash
|
|
@@ -224,6 +248,8 @@ For **read-only** usage, select at minimum: `read:confluence-content.all`, `read
|
|
|
224
248
|
|
|
225
249
|
**On-premise / Data Center:** Use your Confluence username and password for basic authentication.
|
|
226
250
|
|
|
251
|
+
**mTLS-protected Confluence APIs:** Some self-hosted or reverse-proxied deployments authenticate at the TLS layer with a client certificate instead of sending an application-level token. In these environments, configure `authType=mtls` and provide certificate paths via CLI flags or environment variables. No `Authorization` header will be sent in mTLS mode.
|
|
252
|
+
|
|
227
253
|
## Usage
|
|
228
254
|
|
|
229
255
|
### Read a Page
|
package/bin/confluence.js
CHANGED
|
@@ -34,9 +34,12 @@ program
|
|
|
34
34
|
.option('-d, --domain <domain>', 'Confluence domain')
|
|
35
35
|
.option('--protocol <protocol>', 'Protocol (http or https)')
|
|
36
36
|
.option('-p, --api-path <path>', 'REST API path')
|
|
37
|
-
.option('-a, --auth-type <type>', 'Authentication type (basic or
|
|
37
|
+
.option('-a, --auth-type <type>', 'Authentication type (basic, bearer, or mtls)')
|
|
38
38
|
.option('-e, --email <email>', 'Email or username for basic auth')
|
|
39
39
|
.option('-t, --token <token>', 'API token')
|
|
40
|
+
.option('--tls-ca-cert <path>', 'CA certificate for mTLS connections')
|
|
41
|
+
.option('--tls-client-cert <path>', 'Client certificate for mTLS connections')
|
|
42
|
+
.option('--tls-client-key <path>', 'Client private key for mTLS connections')
|
|
40
43
|
.option('--read-only', 'Set profile to read-only mode (blocks write operations)')
|
|
41
44
|
.action(async (options) => {
|
|
42
45
|
const profile = getProfileName();
|
|
@@ -1917,9 +1920,12 @@ profileCmd
|
|
|
1917
1920
|
.option('-d, --domain <domain>', 'Confluence domain')
|
|
1918
1921
|
.option('--protocol <protocol>', 'Protocol (http or https)')
|
|
1919
1922
|
.option('-p, --api-path <path>', 'REST API path')
|
|
1920
|
-
.option('-a, --auth-type <type>', 'Authentication type (basic or
|
|
1923
|
+
.option('-a, --auth-type <type>', 'Authentication type (basic, bearer, or mtls)')
|
|
1921
1924
|
.option('-e, --email <email>', 'Email or username for basic auth')
|
|
1922
1925
|
.option('-t, --token <token>', 'API token')
|
|
1926
|
+
.option('--tls-ca-cert <path>', 'CA certificate for mTLS connections')
|
|
1927
|
+
.option('--tls-client-cert <path>', 'Client certificate for mTLS connections')
|
|
1928
|
+
.option('--tls-client-key <path>', 'Client private key for mTLS connections')
|
|
1923
1929
|
.option('--read-only', 'Set profile to read-only mode (blocks write operations)')
|
|
1924
1930
|
.action(async (name, options) => {
|
|
1925
1931
|
if (!isValidProfileName(name)) {
|
package/lib/config.js
CHANGED
|
@@ -10,7 +10,8 @@ const DEFAULT_PROFILE = 'default';
|
|
|
10
10
|
|
|
11
11
|
const AUTH_CHOICES = [
|
|
12
12
|
{ name: 'Basic (credentials)', value: 'basic' },
|
|
13
|
-
{ name: 'Bearer token', value: 'bearer' }
|
|
13
|
+
{ name: 'Bearer token', value: 'bearer' },
|
|
14
|
+
{ name: 'Client certificate (mTLS)', value: 'mtls' }
|
|
14
15
|
];
|
|
15
16
|
|
|
16
17
|
const isValidProfileName = (name) => /^[a-zA-Z0-9_-]+$/.test(name);
|
|
@@ -37,12 +38,92 @@ const normalizeProtocol = (rawValue) => {
|
|
|
37
38
|
|
|
38
39
|
const normalizeAuthType = (rawValue, hasEmail) => {
|
|
39
40
|
const normalized = (rawValue || '').trim().toLowerCase();
|
|
40
|
-
if (normalized === 'basic' || normalized === 'bearer') {
|
|
41
|
+
if (normalized === 'basic' || normalized === 'bearer' || normalized === 'mtls') {
|
|
41
42
|
return normalized;
|
|
42
43
|
}
|
|
43
44
|
return hasEmail ? 'basic' : 'bearer';
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
const trimOptional = (value) => {
|
|
48
|
+
if (typeof value !== 'string') {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
return trimmed || undefined;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const normalizeMtlsConfig = (mtls) => {
|
|
56
|
+
if (!mtls) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const normalized = {
|
|
61
|
+
caCert: trimOptional(mtls.caCert),
|
|
62
|
+
clientCert: trimOptional(mtls.clientCert),
|
|
63
|
+
clientKey: trimOptional(mtls.clientKey),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (!normalized.caCert && !normalized.clientCert && !normalized.clientKey) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return normalized;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const validateMtlsConfig = (mtls, labelPrefix = 'mTLS') => {
|
|
74
|
+
const normalized = normalizeMtlsConfig(mtls);
|
|
75
|
+
if (!normalized) {
|
|
76
|
+
return [`${labelPrefix} requires a client certificate and client key.`];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const errors = [];
|
|
80
|
+
if (!normalized.clientCert) {
|
|
81
|
+
errors.push(`${labelPrefix} requires a client certificate.`);
|
|
82
|
+
} else if (!fs.existsSync(normalized.clientCert)) {
|
|
83
|
+
errors.push(`${labelPrefix} client certificate file not found: ${normalized.clientCert}`);
|
|
84
|
+
}
|
|
85
|
+
if (!normalized.clientKey) {
|
|
86
|
+
errors.push(`${labelPrefix} requires a client key.`);
|
|
87
|
+
} else if (!fs.existsSync(normalized.clientKey)) {
|
|
88
|
+
errors.push(`${labelPrefix} client key file not found: ${normalized.clientKey}`);
|
|
89
|
+
}
|
|
90
|
+
if (normalized.caCert && !fs.existsSync(normalized.caCert)) {
|
|
91
|
+
errors.push(`${labelPrefix} CA certificate file not found: ${normalized.caCert}`);
|
|
92
|
+
}
|
|
93
|
+
return errors;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const validateMtlsProtocol = (protocol) => {
|
|
97
|
+
if (normalizeProtocol(protocol) === 'http') {
|
|
98
|
+
return 'mTLS authentication requires HTTPS and is not compatible with HTTP.';
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build an inquirer question for an mTLS file path prompt.
|
|
105
|
+
* @param {string} name - answer key (e.g. 'tlsClientCert')
|
|
106
|
+
* @param {string} message - prompt text
|
|
107
|
+
* @param {boolean} required - whether the field is mandatory
|
|
108
|
+
* @param {Function} [whenFn] - optional custom `when` predicate; defaults to authType === 'mtls'
|
|
109
|
+
*/
|
|
110
|
+
const mtlsCertQuestion = (name, message, required, whenFn) => ({
|
|
111
|
+
type: 'input',
|
|
112
|
+
name,
|
|
113
|
+
message,
|
|
114
|
+
when: whenFn || ((responses) => responses.authType === 'mtls'),
|
|
115
|
+
validate: (input) => {
|
|
116
|
+
const value = (input || '').trim();
|
|
117
|
+
if (!value) {
|
|
118
|
+
return required ? `${message.replace(/:$/, '')} is required for mTLS.` : true;
|
|
119
|
+
}
|
|
120
|
+
if (!fs.existsSync(value)) {
|
|
121
|
+
return `File not found: ${value}`;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
46
127
|
const inferApiPath = (domain) => {
|
|
47
128
|
if (!domain) {
|
|
48
129
|
return '/rest/api';
|
|
@@ -89,6 +170,10 @@ function readConfigFile() {
|
|
|
89
170
|
token: raw.token,
|
|
90
171
|
authType: raw.authType
|
|
91
172
|
};
|
|
173
|
+
const mtls = normalizeMtlsConfig(raw.mtls);
|
|
174
|
+
if (mtls) {
|
|
175
|
+
profile.mtls = mtls;
|
|
176
|
+
}
|
|
92
177
|
if (raw.email) {
|
|
93
178
|
profile.email = raw.email;
|
|
94
179
|
}
|
|
@@ -123,7 +208,7 @@ const validateCliOptions = (options) => {
|
|
|
123
208
|
errors.push('--domain cannot be empty');
|
|
124
209
|
}
|
|
125
210
|
|
|
126
|
-
if (options.token && !options.token.trim()) {
|
|
211
|
+
if (options.token !== undefined && !options.token.trim()) {
|
|
127
212
|
errors.push('--token cannot be empty');
|
|
128
213
|
}
|
|
129
214
|
|
|
@@ -148,8 +233,8 @@ const validateCliOptions = (options) => {
|
|
|
148
233
|
errors.push('--protocol must be "http" or "https"');
|
|
149
234
|
}
|
|
150
235
|
|
|
151
|
-
if (options.authType && !['basic', 'bearer'].includes(options.authType.toLowerCase())) {
|
|
152
|
-
errors.push('--auth-type must be "basic" or "
|
|
236
|
+
if (options.authType && !['basic', 'bearer', 'mtls'].includes(options.authType.toLowerCase())) {
|
|
237
|
+
errors.push('--auth-type must be "basic", "bearer", or "mtls"');
|
|
153
238
|
}
|
|
154
239
|
|
|
155
240
|
// Check if basic auth is provided with email
|
|
@@ -158,6 +243,16 @@ const validateCliOptions = (options) => {
|
|
|
158
243
|
errors.push('--email is required when using basic authentication (use your username for on-premise)');
|
|
159
244
|
}
|
|
160
245
|
|
|
246
|
+
if (normAuthType === 'mtls') {
|
|
247
|
+
validateMtlsConfig(options.mtls, '--auth-type mtls').forEach((error) => {
|
|
248
|
+
errors.push(error);
|
|
249
|
+
});
|
|
250
|
+
const protocolError = validateMtlsProtocol(options.protocol);
|
|
251
|
+
if (protocolError) {
|
|
252
|
+
errors.push(protocolError);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
161
256
|
return errors;
|
|
162
257
|
};
|
|
163
258
|
|
|
@@ -167,14 +262,22 @@ const saveConfig = (configData, profileName) => {
|
|
|
167
262
|
domain: configData.domain.trim(),
|
|
168
263
|
protocol: normalizeProtocol(configData.protocol),
|
|
169
264
|
apiPath: normalizeApiPath(configData.apiPath, configData.domain),
|
|
170
|
-
token: configData.token.trim(),
|
|
171
265
|
authType: configData.authType
|
|
172
266
|
};
|
|
173
267
|
|
|
268
|
+
if (configData.token) {
|
|
269
|
+
config.token = configData.token.trim();
|
|
270
|
+
}
|
|
271
|
+
|
|
174
272
|
if (configData.authType === 'basic' && configData.email) {
|
|
175
273
|
config.email = configData.email.trim();
|
|
176
274
|
}
|
|
177
275
|
|
|
276
|
+
const mtls = normalizeMtlsConfig(configData.mtls);
|
|
277
|
+
if (mtls) {
|
|
278
|
+
config.mtls = mtls;
|
|
279
|
+
}
|
|
280
|
+
|
|
178
281
|
if (configData.readOnly) {
|
|
179
282
|
config.readOnly = true;
|
|
180
283
|
}
|
|
@@ -283,10 +386,30 @@ const promptForMissingValues = async (providedValues) => {
|
|
|
283
386
|
type: 'password',
|
|
284
387
|
name: 'token',
|
|
285
388
|
message: 'API token / password:',
|
|
389
|
+
when: (responses) => {
|
|
390
|
+
const authType = providedValues.authType || responses.authType;
|
|
391
|
+
return authType !== 'mtls';
|
|
392
|
+
},
|
|
286
393
|
validate: requiredInput('API token / password')
|
|
287
394
|
});
|
|
288
395
|
}
|
|
289
396
|
|
|
397
|
+
// mTLS certificate path questions
|
|
398
|
+
const mtls = normalizeMtlsConfig(providedValues.mtls);
|
|
399
|
+
const mtlsWhen = (responses) => {
|
|
400
|
+
const authType = providedValues.authType || responses.authType;
|
|
401
|
+
return authType === 'mtls';
|
|
402
|
+
};
|
|
403
|
+
if (!mtls || !mtls.clientCert) {
|
|
404
|
+
questions.push(mtlsCertQuestion('tlsClientCert', 'Path to client certificate file (PEM):', true, mtlsWhen));
|
|
405
|
+
}
|
|
406
|
+
if (!mtls || !mtls.clientKey) {
|
|
407
|
+
questions.push(mtlsCertQuestion('tlsClientKey', 'Path to client key file (PEM):', true, mtlsWhen));
|
|
408
|
+
}
|
|
409
|
+
if (!mtls || !mtls.caCert) {
|
|
410
|
+
questions.push(mtlsCertQuestion('tlsCaCert', 'Path to CA certificate file (PEM, optional):', false, mtlsWhen));
|
|
411
|
+
}
|
|
412
|
+
|
|
290
413
|
if (questions.length === 0) {
|
|
291
414
|
return providedValues;
|
|
292
415
|
}
|
|
@@ -313,7 +436,12 @@ async function initConfig(cliOptions = {}) {
|
|
|
313
436
|
apiPath: cliOptions.apiPath,
|
|
314
437
|
authType: cliOptions.authType,
|
|
315
438
|
email: cliOptions.email,
|
|
316
|
-
token: cliOptions.token
|
|
439
|
+
token: cliOptions.token,
|
|
440
|
+
mtls: cliOptions.mtls || {
|
|
441
|
+
caCert: cliOptions.tlsCaCert,
|
|
442
|
+
clientCert: cliOptions.tlsClientCert,
|
|
443
|
+
clientKey: cliOptions.tlsClientKey,
|
|
444
|
+
}
|
|
317
445
|
};
|
|
318
446
|
|
|
319
447
|
// Check if any CLI options were provided
|
|
@@ -380,11 +508,24 @@ async function initConfig(cliOptions = {}) {
|
|
|
380
508
|
type: 'password',
|
|
381
509
|
name: 'token',
|
|
382
510
|
message: 'API token / password:',
|
|
511
|
+
when: (responses) => responses.authType !== 'mtls',
|
|
383
512
|
validate: requiredInput('API token / password')
|
|
384
|
-
}
|
|
513
|
+
},
|
|
514
|
+
mtlsCertQuestion('tlsClientCert', 'Path to client certificate file (PEM):', true),
|
|
515
|
+
mtlsCertQuestion('tlsClientKey', 'Path to client key file (PEM):', true),
|
|
516
|
+
mtlsCertQuestion('tlsCaCert', 'Path to CA certificate file (PEM, optional):', false)
|
|
385
517
|
]);
|
|
386
518
|
|
|
387
|
-
|
|
519
|
+
const configData = { ...answers, readOnly };
|
|
520
|
+
if (answers.authType === 'mtls') {
|
|
521
|
+
configData.mtls = {
|
|
522
|
+
clientCert: answers.tlsClientCert,
|
|
523
|
+
clientKey: answers.tlsClientKey,
|
|
524
|
+
caCert: answers.tlsCaCert || undefined,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
saveConfig(configData, profileName);
|
|
388
529
|
return;
|
|
389
530
|
}
|
|
390
531
|
|
|
@@ -403,8 +544,13 @@ async function initConfig(cliOptions = {}) {
|
|
|
403
544
|
// Non-interactive requires: domain, token, and either authType or email (for inference)
|
|
404
545
|
const hasRequiredValues = Boolean(
|
|
405
546
|
providedValues.domain &&
|
|
406
|
-
|
|
407
|
-
|
|
547
|
+
(
|
|
548
|
+
providedValues.authType === 'mtls'
|
|
549
|
+
|| (
|
|
550
|
+
providedValues.token &&
|
|
551
|
+
(providedValues.authType || providedValues.email)
|
|
552
|
+
)
|
|
553
|
+
)
|
|
408
554
|
);
|
|
409
555
|
|
|
410
556
|
if (hasRequiredValues) {
|
|
@@ -425,6 +571,11 @@ async function initConfig(cliOptions = {}) {
|
|
|
425
571
|
process.exit(1);
|
|
426
572
|
}
|
|
427
573
|
|
|
574
|
+
if (normalizedAuthType !== 'mtls' && !providedValues.token) {
|
|
575
|
+
console.error(chalk.red('❌ Token is required for basic or bearer authentication'));
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
|
|
428
579
|
// Verify API path format if provided
|
|
429
580
|
if (providedValues.apiPath) {
|
|
430
581
|
normalizeApiPath(providedValues.apiPath, normalizedDomain);
|
|
@@ -437,6 +588,7 @@ async function initConfig(cliOptions = {}) {
|
|
|
437
588
|
token: providedValues.token,
|
|
438
589
|
authType: normalizedAuthType,
|
|
439
590
|
email: providedValues.email,
|
|
591
|
+
mtls: providedValues.mtls,
|
|
440
592
|
readOnly
|
|
441
593
|
};
|
|
442
594
|
|
|
@@ -461,6 +613,15 @@ async function initConfig(cliOptions = {}) {
|
|
|
461
613
|
// Normalize auth type
|
|
462
614
|
mergedValues.authType = normalizeAuthType(mergedValues.authType, Boolean(mergedValues.email));
|
|
463
615
|
|
|
616
|
+
// Build mTLS config from prompted values if needed
|
|
617
|
+
if (mergedValues.authType === 'mtls') {
|
|
618
|
+
mergedValues.mtls = normalizeMtlsConfig({
|
|
619
|
+
clientCert: mergedValues.tlsClientCert || (mergedValues.mtls && mergedValues.mtls.clientCert),
|
|
620
|
+
clientKey: mergedValues.tlsClientKey || (mergedValues.mtls && mergedValues.mtls.clientKey),
|
|
621
|
+
caCert: mergedValues.tlsCaCert || (mergedValues.mtls && mergedValues.mtls.caCert),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
464
625
|
saveConfig({ ...mergedValues, readOnly }, profileName);
|
|
465
626
|
} catch (error) {
|
|
466
627
|
console.error(chalk.red(`❌ ${error.message}`));
|
|
@@ -476,9 +637,15 @@ function getConfig(profileName) {
|
|
|
476
637
|
const envApiPath = process.env.CONFLUENCE_API_PATH;
|
|
477
638
|
const envProtocol = process.env.CONFLUENCE_PROTOCOL;
|
|
478
639
|
const envReadOnly = process.env.CONFLUENCE_READ_ONLY;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
640
|
+
const envMtls = normalizeMtlsConfig({
|
|
641
|
+
caCert: process.env.CONFLUENCE_TLS_CA_CERT,
|
|
642
|
+
clientCert: process.env.CONFLUENCE_TLS_CLIENT_CERT,
|
|
643
|
+
clientKey: process.env.CONFLUENCE_TLS_CLIENT_KEY,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
if (envDomain && (envToken || envAuthType === 'mtls' || envMtls)) {
|
|
647
|
+
const inferredAuthType = envAuthType || (envMtls && !envToken ? 'mtls' : undefined);
|
|
648
|
+
const authType = normalizeAuthType(inferredAuthType, Boolean(envEmail));
|
|
482
649
|
let apiPath;
|
|
483
650
|
|
|
484
651
|
try {
|
|
@@ -494,13 +661,33 @@ function getConfig(profileName) {
|
|
|
494
661
|
process.exit(1);
|
|
495
662
|
}
|
|
496
663
|
|
|
664
|
+
if (authType !== 'mtls' && !envToken) {
|
|
665
|
+
console.error(chalk.red('❌ Bearer/basic authentication requires CONFLUENCE_API_TOKEN or CONFLUENCE_PASSWORD.'));
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (authType === 'mtls') {
|
|
670
|
+
const mtlsErrors = validateMtlsConfig(envMtls, 'CONFLUENCE_AUTH_TYPE=mtls');
|
|
671
|
+
if (mtlsErrors.length > 0) {
|
|
672
|
+
console.error(chalk.red(`❌ ${mtlsErrors.join(' ')}`));
|
|
673
|
+
console.log(chalk.yellow('Set CONFLUENCE_TLS_CLIENT_CERT and CONFLUENCE_TLS_CLIENT_KEY. Optionally set CONFLUENCE_TLS_CA_CERT.'));
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
if (normalizeProtocol(envProtocol) === 'http') {
|
|
677
|
+
const protocolError = validateMtlsProtocol(envProtocol);
|
|
678
|
+
console.error(chalk.red(`❌ ${protocolError}`));
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
497
683
|
return {
|
|
498
684
|
domain: envDomain.trim(),
|
|
499
685
|
protocol: normalizeProtocol(envProtocol),
|
|
500
686
|
apiPath,
|
|
501
|
-
token: envToken.trim(),
|
|
687
|
+
token: envToken ? envToken.trim() : undefined,
|
|
502
688
|
email: envEmail ? envEmail.trim() : undefined,
|
|
503
689
|
authType,
|
|
690
|
+
mtls: envMtls,
|
|
504
691
|
readOnly: envReadOnly === 'true'
|
|
505
692
|
};
|
|
506
693
|
}
|
|
@@ -534,12 +721,13 @@ function getConfig(profileName) {
|
|
|
534
721
|
|
|
535
722
|
try {
|
|
536
723
|
const trimmedDomain = (storedConfig.domain || '').trim();
|
|
537
|
-
const trimmedToken = (storedConfig.token
|
|
724
|
+
const trimmedToken = trimOptional(storedConfig.token);
|
|
538
725
|
const trimmedEmail = storedConfig.email ? storedConfig.email.trim() : undefined;
|
|
539
726
|
const authType = normalizeAuthType(storedConfig.authType, Boolean(trimmedEmail));
|
|
727
|
+
const mtls = normalizeMtlsConfig(storedConfig.mtls);
|
|
540
728
|
let apiPath;
|
|
541
729
|
|
|
542
|
-
if (!trimmedDomain || !trimmedToken) {
|
|
730
|
+
if (!trimmedDomain || (authType !== 'mtls' && !trimmedToken)) {
|
|
543
731
|
console.error(chalk.red('❌ Configuration file is missing required values.'));
|
|
544
732
|
console.log(chalk.yellow('Run "confluence init" to refresh your settings.'));
|
|
545
733
|
process.exit(1);
|
|
@@ -551,6 +739,21 @@ function getConfig(profileName) {
|
|
|
551
739
|
process.exit(1);
|
|
552
740
|
}
|
|
553
741
|
|
|
742
|
+
if (authType === 'mtls') {
|
|
743
|
+
const mtlsErrors = validateMtlsConfig(mtls, 'mTLS authentication');
|
|
744
|
+
if (mtlsErrors.length > 0) {
|
|
745
|
+
console.error(chalk.red(`❌ ${mtlsErrors.join(' ')}`));
|
|
746
|
+
console.log(chalk.yellow('Please rerun "confluence init" to add your mTLS certificate paths.'));
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
if (normalizeProtocol(storedConfig.protocol) === 'http') {
|
|
750
|
+
const protocolError = validateMtlsProtocol(storedConfig.protocol);
|
|
751
|
+
console.error(chalk.red(`❌ ${protocolError}`));
|
|
752
|
+
console.log(chalk.yellow('Please rerun "confluence init" to update your protocol to HTTPS.'));
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
554
757
|
try {
|
|
555
758
|
apiPath = normalizeApiPath(storedConfig.apiPath, trimmedDomain);
|
|
556
759
|
} catch (error) {
|
|
@@ -570,6 +773,7 @@ function getConfig(profileName) {
|
|
|
570
773
|
token: trimmedToken,
|
|
571
774
|
email: trimmedEmail,
|
|
572
775
|
authType,
|
|
776
|
+
mtls,
|
|
573
777
|
readOnly
|
|
574
778
|
};
|
|
575
779
|
} catch (error) {
|
package/lib/confluence-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
2
|
const fs = require('fs');
|
|
3
|
+
const https = require('https');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const FormData = require('form-data');
|
|
5
6
|
const { convert } = require('html-to-text');
|
|
@@ -34,6 +35,7 @@ class ConfluenceClient {
|
|
|
34
35
|
this.token = config.token;
|
|
35
36
|
this.email = config.email;
|
|
36
37
|
this.authType = (config.authType || (this.email ? 'basic' : 'bearer')).toLowerCase();
|
|
38
|
+
this.mtls = config.mtls;
|
|
37
39
|
this.apiPath = this.sanitizeApiPath(config.apiPath);
|
|
38
40
|
this.webUrlPrefix = this.apiPath.startsWith('/wiki/') ? '/wiki' : '';
|
|
39
41
|
this.baseURL = `${this.protocol}://${this.domain}${this.apiPath}`;
|
|
@@ -41,14 +43,23 @@ class ConfluenceClient {
|
|
|
41
43
|
this.setupConfluenceMarkdownExtensions();
|
|
42
44
|
|
|
43
45
|
const headers = {
|
|
44
|
-
'Content-Type': 'application/json'
|
|
45
|
-
'Authorization': this.authType === 'basic' ? this.buildBasicAuthHeader() : `Bearer ${this.token}`
|
|
46
|
+
'Content-Type': 'application/json'
|
|
46
47
|
};
|
|
48
|
+
const authHeader = this.buildAuthHeader();
|
|
49
|
+
if (authHeader) {
|
|
50
|
+
headers.Authorization = authHeader;
|
|
51
|
+
}
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
const clientOptions = {
|
|
49
54
|
baseURL: this.baseURL,
|
|
50
55
|
headers
|
|
51
|
-
}
|
|
56
|
+
};
|
|
57
|
+
const httpsAgent = this.buildHttpsAgent();
|
|
58
|
+
if (httpsAgent) {
|
|
59
|
+
clientOptions.httpsAgent = httpsAgent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.client = axios.create(clientOptions);
|
|
52
63
|
|
|
53
64
|
this.client.interceptors.response.use(
|
|
54
65
|
response => response,
|
|
@@ -72,6 +83,10 @@ class ConfluenceClient {
|
|
|
72
83
|
hints.push(
|
|
73
84
|
'Please verify your username and password are correct.'
|
|
74
85
|
);
|
|
86
|
+
} else if (this.authType === 'mtls') {
|
|
87
|
+
hints.push(
|
|
88
|
+
'Please verify your client certificate, client key, and CA certificate are correct and trusted by the server.'
|
|
89
|
+
);
|
|
75
90
|
} else {
|
|
76
91
|
hints.push(
|
|
77
92
|
'Please verify your personal access token is valid and not expired.'
|
|
@@ -115,6 +130,67 @@ class ConfluenceClient {
|
|
|
115
130
|
return `Basic ${encodedCredentials}`;
|
|
116
131
|
}
|
|
117
132
|
|
|
133
|
+
buildAuthHeader() {
|
|
134
|
+
if (this.authType === 'mtls') {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!this.token) {
|
|
139
|
+
throw new Error(`Authentication type "${this.authType}" requires a token or password.`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return this.authType === 'basic' ? this.buildBasicAuthHeader() : `Bearer ${this.token}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
buildHttpsAgent() {
|
|
146
|
+
if (this.protocol !== 'https' || !this.mtls) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const options = {};
|
|
151
|
+
|
|
152
|
+
if (this.mtls.caCert) {
|
|
153
|
+
if (!fs.existsSync(this.mtls.caCert)) {
|
|
154
|
+
throw new Error(`CA certificate file not found: ${this.mtls.caCert}`);
|
|
155
|
+
}
|
|
156
|
+
options.ca = fs.readFileSync(this.mtls.caCert);
|
|
157
|
+
}
|
|
158
|
+
if (this.mtls.clientCert) {
|
|
159
|
+
if (!fs.existsSync(this.mtls.clientCert)) {
|
|
160
|
+
throw new Error(`Client certificate file not found: ${this.mtls.clientCert}`);
|
|
161
|
+
}
|
|
162
|
+
options.cert = fs.readFileSync(this.mtls.clientCert);
|
|
163
|
+
}
|
|
164
|
+
if (this.mtls.clientKey) {
|
|
165
|
+
if (!fs.existsSync(this.mtls.clientKey)) {
|
|
166
|
+
throw new Error(`Client key file not found: ${this.mtls.clientKey}`);
|
|
167
|
+
}
|
|
168
|
+
// Warn if private key file is readable by others (Unix only)
|
|
169
|
+
if (process.platform !== 'win32') {
|
|
170
|
+
try {
|
|
171
|
+
const keyStats = fs.statSync(this.mtls.clientKey);
|
|
172
|
+
const keyMode = keyStats.mode & 0o777;
|
|
173
|
+
if (keyMode & 0o077) {
|
|
174
|
+
console.error(
|
|
175
|
+
`Warning: Client key file "${this.mtls.clientKey}" has mode ${keyMode.toString(8)}. ` +
|
|
176
|
+
'Private keys should not be readable by other users (recommended: 0600). ' +
|
|
177
|
+
`Fix with: chmod 600 "${this.mtls.clientKey}"`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// Ignore stat errors — the read below will surface them
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
options.key = fs.readFileSync(this.mtls.clientKey);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (Object.keys(options).length === 0) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return new https.Agent(options);
|
|
192
|
+
}
|
|
193
|
+
|
|
118
194
|
/**
|
|
119
195
|
* Extract page ID from URL or return the ID if it's already a number
|
|
120
196
|
*/
|
|
@@ -1423,9 +1499,12 @@ class ConfluenceClient {
|
|
|
1423
1499
|
// Format: <ac:link><ri:page ri:space-key="xxx" ri:content-title="Page Title" /></ac:link>
|
|
1424
1500
|
markdown = markdown.replace(/<ac:link>\s*<ri:page[^>]*ri:content-title="([^"]*)"[^>]*\/>\s*<\/ac:link>/g, '[$1]');
|
|
1425
1501
|
markdown = markdown.replace(/<ac:link>\s*<ri:page[^>]*ri:content-title="([^"]*)"[^>]*>\s*<\/ri:page>\s*<\/ac:link>/g, '[$1]');
|
|
1426
|
-
|
|
1427
|
-
//
|
|
1428
|
-
markdown = markdown.replace(/<ac:link>[\s\S]*?<\/ac:link>/g, '');
|
|
1502
|
+
|
|
1503
|
+
// Convert internal page links with custom display text (ac:link-body)
|
|
1504
|
+
markdown = markdown.replace(/<ac:link[^>]*>[\s\S]*?<ac:link-body>([\s\S]*?)<\/ac:link-body>[\s\S]*?<\/ac:link>/g, '$1');
|
|
1505
|
+
|
|
1506
|
+
// Remove any remaining ac:link tags that weren't matched (including those with attributes)
|
|
1507
|
+
markdown = markdown.replace(/<ac:link[^>]*>[\s\S]*?<\/ac:link>/g, '');
|
|
1429
1508
|
|
|
1430
1509
|
// Convert remaining HTML to markdown
|
|
1431
1510
|
markdown = this.htmlToMarkdown(markdown);
|
|
@@ -2132,4 +2211,4 @@ ConfluenceClient.createLocalConverter = function () {
|
|
|
2132
2211
|
};
|
|
2133
2212
|
|
|
2134
2213
|
module.exports = ConfluenceClient;
|
|
2135
|
-
module.exports.NAMED_ENTITIES = NAMED_ENTITIES;
|
|
2214
|
+
module.exports.NAMED_ENTITIES = NAMED_ENTITIES;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "1.30.
|
|
3
|
+
"version": "1.30.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "confluence-cli",
|
|
9
|
-
"version": "1.30.
|
|
9
|
+
"version": "1.30.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"axios": "^1.15.0",
|
|
@@ -2684,9 +2684,9 @@
|
|
|
2684
2684
|
"license": "ISC"
|
|
2685
2685
|
},
|
|
2686
2686
|
"node_modules/follow-redirects": {
|
|
2687
|
-
"version": "1.
|
|
2688
|
-
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.
|
|
2689
|
-
"integrity": "sha512-
|
|
2687
|
+
"version": "1.16.0",
|
|
2688
|
+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
|
2689
|
+
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
|
2690
2690
|
"funding": [
|
|
2691
2691
|
{
|
|
2692
2692
|
"type": "individual",
|