confluence-cli 1.30.0 → 1.30.2
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 +50 -3
- package/bin/confluence.js +58 -76
- package/lib/config.js +239 -25
- package/lib/confluence-client.js +92 -12
- package/npm-shrinkwrap.json +5 -5
- package/package.json +1 -1
- package/plugins/confluence/skills/confluence/SKILL.md +2 -0
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,28 @@ 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.
|
|
206
|
+
|
|
207
|
+
**Custom domains on Confluence Cloud:**
|
|
208
|
+
|
|
209
|
+
If your Confluence Cloud instance uses a custom domain (e.g., `wiki.example.org` instead of `*.atlassian.net`), the CLI may misidentify it as a Server/Data Center instance and produce broken link formats. Set `CONFLUENCE_FORCE_CLOUD=true` to override the automatic detection:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
export CONFLUENCE_FORCE_CLOUD=true
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Or add `"forceCloud": true` to your profile in `~/.confluence-cli/config.json`:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"profiles": {
|
|
220
|
+
"default": {
|
|
221
|
+
"domain": "wiki.example.org",
|
|
222
|
+
"forceCloud": true
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
182
227
|
|
|
183
228
|
**Read-only mode** (recommended for AI agents):
|
|
184
229
|
```bash
|
|
@@ -224,6 +269,8 @@ For **read-only** usage, select at minimum: `read:confluence-content.all`, `read
|
|
|
224
269
|
|
|
225
270
|
**On-premise / Data Center:** Use your Confluence username and password for basic authentication.
|
|
226
271
|
|
|
272
|
+
**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.
|
|
273
|
+
|
|
227
274
|
## Usage
|
|
228
275
|
|
|
229
276
|
### Read a Page
|
package/bin/confluence.js
CHANGED
|
@@ -16,6 +16,18 @@ function assertWritable(config) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function assertNonEmpty(value, label) {
|
|
20
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
21
|
+
throw new Error(`${label} is required and cannot be empty.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function handleCommandError(analytics, commandName, error) {
|
|
26
|
+
analytics.track(commandName, false);
|
|
27
|
+
console.error(chalk.red('Error:'), error.message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
program
|
|
20
32
|
.name('confluence')
|
|
21
33
|
.description('CLI tool for Atlassian Confluence')
|
|
@@ -34,9 +46,12 @@ program
|
|
|
34
46
|
.option('-d, --domain <domain>', 'Confluence domain')
|
|
35
47
|
.option('--protocol <protocol>', 'Protocol (http or https)')
|
|
36
48
|
.option('-p, --api-path <path>', 'REST API path')
|
|
37
|
-
.option('-a, --auth-type <type>', 'Authentication type (basic or
|
|
49
|
+
.option('-a, --auth-type <type>', 'Authentication type (basic, bearer, or mtls)')
|
|
38
50
|
.option('-e, --email <email>', 'Email or username for basic auth')
|
|
39
51
|
.option('-t, --token <token>', 'API token')
|
|
52
|
+
.option('--tls-ca-cert <path>', 'CA certificate for mTLS connections')
|
|
53
|
+
.option('--tls-client-cert <path>', 'Client certificate for mTLS connections')
|
|
54
|
+
.option('--tls-client-key <path>', 'Client private key for mTLS connections')
|
|
40
55
|
.option('--read-only', 'Set profile to read-only mode (blocks write operations)')
|
|
41
56
|
.action(async (options) => {
|
|
42
57
|
const profile = getProfileName();
|
|
@@ -56,9 +71,7 @@ program
|
|
|
56
71
|
console.log(content);
|
|
57
72
|
analytics.track('read', true);
|
|
58
73
|
} catch (error) {
|
|
59
|
-
analytics
|
|
60
|
-
console.error(chalk.red('Error:'), error.message);
|
|
61
|
-
process.exit(1);
|
|
74
|
+
handleCommandError(analytics, 'read', error);
|
|
62
75
|
}
|
|
63
76
|
});
|
|
64
77
|
|
|
@@ -81,9 +94,7 @@ program
|
|
|
81
94
|
}
|
|
82
95
|
analytics.track('info', true);
|
|
83
96
|
} catch (error) {
|
|
84
|
-
analytics
|
|
85
|
-
console.error(chalk.red('Error:'), error.message);
|
|
86
|
-
process.exit(1);
|
|
97
|
+
handleCommandError(analytics, 'info', error);
|
|
87
98
|
}
|
|
88
99
|
});
|
|
89
100
|
|
|
@@ -114,9 +125,7 @@ program
|
|
|
114
125
|
});
|
|
115
126
|
analytics.track('search', true);
|
|
116
127
|
} catch (error) {
|
|
117
|
-
analytics
|
|
118
|
-
console.error(chalk.red('Error:'), error.message);
|
|
119
|
-
process.exit(1);
|
|
128
|
+
handleCommandError(analytics, 'search', error);
|
|
120
129
|
}
|
|
121
130
|
});
|
|
122
131
|
|
|
@@ -137,9 +146,7 @@ program
|
|
|
137
146
|
});
|
|
138
147
|
analytics.track('spaces', true);
|
|
139
148
|
} catch (error) {
|
|
140
|
-
analytics
|
|
141
|
-
console.error(chalk.red('Error:'), error.message);
|
|
142
|
-
process.exit(1);
|
|
149
|
+
handleCommandError(analytics, 'spaces', error);
|
|
143
150
|
}
|
|
144
151
|
});
|
|
145
152
|
|
|
@@ -211,12 +218,15 @@ program
|
|
|
211
218
|
.action(async (title, spaceKey, options) => {
|
|
212
219
|
const analytics = new Analytics();
|
|
213
220
|
try {
|
|
221
|
+
assertNonEmpty(title, 'title');
|
|
222
|
+
assertNonEmpty(spaceKey, 'spaceKey');
|
|
223
|
+
|
|
214
224
|
const config = getConfig(getProfileName());
|
|
215
225
|
assertWritable(config);
|
|
216
226
|
const client = new ConfluenceClient(config);
|
|
217
227
|
|
|
218
228
|
let content = '';
|
|
219
|
-
|
|
229
|
+
|
|
220
230
|
if (options.file) {
|
|
221
231
|
const fs = require('fs');
|
|
222
232
|
if (!fs.existsSync(options.file)) {
|
|
@@ -228,7 +238,7 @@ program
|
|
|
228
238
|
} else {
|
|
229
239
|
throw new Error('Either --file or --content option is required');
|
|
230
240
|
}
|
|
231
|
-
|
|
241
|
+
|
|
232
242
|
const result = await client.createPage(title, spaceKey, content, options.format);
|
|
233
243
|
|
|
234
244
|
console.log(chalk.green('✅ Page created successfully!'));
|
|
@@ -239,9 +249,7 @@ program
|
|
|
239
249
|
|
|
240
250
|
analytics.track('create', true);
|
|
241
251
|
} catch (error) {
|
|
242
|
-
analytics
|
|
243
|
-
console.error(chalk.red('Error:'), error.message);
|
|
244
|
-
process.exit(1);
|
|
252
|
+
handleCommandError(analytics, 'create', error);
|
|
245
253
|
}
|
|
246
254
|
});
|
|
247
255
|
|
|
@@ -255,6 +263,9 @@ program
|
|
|
255
263
|
.action(async (title, parentId, options) => {
|
|
256
264
|
const analytics = new Analytics();
|
|
257
265
|
try {
|
|
266
|
+
assertNonEmpty(title, 'title');
|
|
267
|
+
assertNonEmpty(parentId, 'parentId');
|
|
268
|
+
|
|
258
269
|
const config = getConfig(getProfileName());
|
|
259
270
|
assertWritable(config);
|
|
260
271
|
const client = new ConfluenceClient(config);
|
|
@@ -288,9 +299,7 @@ program
|
|
|
288
299
|
|
|
289
300
|
analytics.track('create_child', true);
|
|
290
301
|
} catch (error) {
|
|
291
|
-
analytics
|
|
292
|
-
console.error(chalk.red('Error:'), error.message);
|
|
293
|
-
process.exit(1);
|
|
302
|
+
handleCommandError(analytics, 'create_child', error);
|
|
294
303
|
}
|
|
295
304
|
});
|
|
296
305
|
|
|
@@ -310,6 +319,10 @@ program
|
|
|
310
319
|
throw new Error('At least one of --title, --file, or --content must be provided.');
|
|
311
320
|
}
|
|
312
321
|
|
|
322
|
+
if (options.title !== undefined) {
|
|
323
|
+
assertNonEmpty(options.title, '--title');
|
|
324
|
+
}
|
|
325
|
+
|
|
313
326
|
const config = getConfig(getProfileName());
|
|
314
327
|
assertWritable(config);
|
|
315
328
|
const client = new ConfluenceClient(config);
|
|
@@ -336,9 +349,7 @@ program
|
|
|
336
349
|
|
|
337
350
|
analytics.track('update', true);
|
|
338
351
|
} catch (error) {
|
|
339
|
-
analytics
|
|
340
|
-
console.error(chalk.red('Error:'), error.message);
|
|
341
|
-
process.exit(1);
|
|
352
|
+
handleCommandError(analytics, 'update', error);
|
|
342
353
|
}
|
|
343
354
|
});
|
|
344
355
|
|
|
@@ -364,9 +375,7 @@ program
|
|
|
364
375
|
|
|
365
376
|
analytics.track('move', true);
|
|
366
377
|
} catch (error) {
|
|
367
|
-
analytics
|
|
368
|
-
console.error(chalk.red('Error:'), error.message);
|
|
369
|
-
process.exit(1);
|
|
378
|
+
handleCommandError(analytics, 'move', error);
|
|
370
379
|
}
|
|
371
380
|
});
|
|
372
381
|
|
|
@@ -408,9 +417,7 @@ program
|
|
|
408
417
|
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
409
418
|
analytics.track('delete', true);
|
|
410
419
|
} catch (error) {
|
|
411
|
-
analytics
|
|
412
|
-
console.error(chalk.red('Error:'), error.message);
|
|
413
|
-
process.exit(1);
|
|
420
|
+
handleCommandError(analytics, 'delete', error);
|
|
414
421
|
}
|
|
415
422
|
});
|
|
416
423
|
|
|
@@ -446,9 +453,7 @@ program
|
|
|
446
453
|
|
|
447
454
|
analytics.track('edit', true);
|
|
448
455
|
} catch (error) {
|
|
449
|
-
analytics
|
|
450
|
-
console.error(chalk.red('Error:'), error.message);
|
|
451
|
-
process.exit(1);
|
|
456
|
+
handleCommandError(analytics, 'edit', error);
|
|
452
457
|
}
|
|
453
458
|
});
|
|
454
459
|
|
|
@@ -472,9 +477,7 @@ program
|
|
|
472
477
|
|
|
473
478
|
analytics.track('find', true);
|
|
474
479
|
} catch (error) {
|
|
475
|
-
analytics
|
|
476
|
-
console.error(chalk.red('Error:'), error.message);
|
|
477
|
-
process.exit(1);
|
|
480
|
+
handleCommandError(analytics, 'find', error);
|
|
478
481
|
}
|
|
479
482
|
});
|
|
480
483
|
|
|
@@ -594,9 +597,7 @@ program
|
|
|
594
597
|
|
|
595
598
|
analytics.track('attachments', true);
|
|
596
599
|
} catch (error) {
|
|
597
|
-
analytics
|
|
598
|
-
console.error(chalk.red('Error:'), error.message);
|
|
599
|
-
process.exit(1);
|
|
600
|
+
handleCommandError(analytics, 'attachments', error);
|
|
600
601
|
}
|
|
601
602
|
});
|
|
602
603
|
|
|
@@ -656,9 +657,7 @@ program
|
|
|
656
657
|
console.log(chalk.green(`Uploaded ${uploaded} attachment${uploaded === 1 ? '' : 's'} to page ${pageId}`));
|
|
657
658
|
analytics.track('attachment_upload', true);
|
|
658
659
|
} catch (error) {
|
|
659
|
-
analytics
|
|
660
|
-
console.error(chalk.red('Error:'), error.message);
|
|
661
|
-
process.exit(1);
|
|
660
|
+
handleCommandError(analytics, 'attachment_upload', error);
|
|
662
661
|
}
|
|
663
662
|
});
|
|
664
663
|
|
|
@@ -698,9 +697,7 @@ program
|
|
|
698
697
|
console.log(`Page ID: ${chalk.blue(result.pageId)}`);
|
|
699
698
|
analytics.track('attachment_delete', true);
|
|
700
699
|
} catch (error) {
|
|
701
|
-
analytics
|
|
702
|
-
console.error(chalk.red('Error:'), error.message);
|
|
703
|
-
process.exit(1);
|
|
700
|
+
handleCommandError(analytics, 'attachment_delete', error);
|
|
704
701
|
}
|
|
705
702
|
});
|
|
706
703
|
|
|
@@ -771,9 +768,7 @@ program
|
|
|
771
768
|
}
|
|
772
769
|
analytics.track('property_list', true);
|
|
773
770
|
} catch (error) {
|
|
774
|
-
analytics
|
|
775
|
-
console.error(chalk.red('Error:'), error.message);
|
|
776
|
-
process.exit(1);
|
|
771
|
+
handleCommandError(analytics, 'property_list', error);
|
|
777
772
|
}
|
|
778
773
|
});
|
|
779
774
|
|
|
@@ -805,9 +800,7 @@ program
|
|
|
805
800
|
}
|
|
806
801
|
analytics.track('property_get', true);
|
|
807
802
|
} catch (error) {
|
|
808
|
-
analytics
|
|
809
|
-
console.error(chalk.red('Error:'), error.message);
|
|
810
|
-
process.exit(1);
|
|
803
|
+
handleCommandError(analytics, 'property_get', error);
|
|
811
804
|
}
|
|
812
805
|
});
|
|
813
806
|
|
|
@@ -864,9 +857,7 @@ program
|
|
|
864
857
|
}
|
|
865
858
|
analytics.track('property_set', true);
|
|
866
859
|
} catch (error) {
|
|
867
|
-
analytics
|
|
868
|
-
console.error(chalk.red('Error:'), error.message);
|
|
869
|
-
process.exit(1);
|
|
860
|
+
handleCommandError(analytics, 'property_set', error);
|
|
870
861
|
}
|
|
871
862
|
});
|
|
872
863
|
|
|
@@ -906,9 +897,7 @@ program
|
|
|
906
897
|
console.log(`${chalk.green('Page ID:')} ${chalk.blue(result.pageId)}`);
|
|
907
898
|
analytics.track('property_delete', true);
|
|
908
899
|
} catch (error) {
|
|
909
|
-
analytics
|
|
910
|
-
console.error(chalk.red('Error:'), error.message);
|
|
911
|
-
process.exit(1);
|
|
900
|
+
handleCommandError(analytics, 'property_delete', error);
|
|
912
901
|
}
|
|
913
902
|
});
|
|
914
903
|
|
|
@@ -1057,9 +1046,7 @@ program
|
|
|
1057
1046
|
|
|
1058
1047
|
analytics.track('comments', true);
|
|
1059
1048
|
} catch (error) {
|
|
1060
|
-
analytics
|
|
1061
|
-
console.error(chalk.red('Error:'), error.message);
|
|
1062
|
-
process.exit(1);
|
|
1049
|
+
handleCommandError(analytics, 'comments', error);
|
|
1063
1050
|
}
|
|
1064
1051
|
});
|
|
1065
1052
|
|
|
@@ -1222,9 +1209,7 @@ program
|
|
|
1222
1209
|
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
1223
1210
|
analytics.track('comment_delete', true);
|
|
1224
1211
|
} catch (error) {
|
|
1225
|
-
analytics
|
|
1226
|
-
console.error(chalk.red('Error:'), error.message);
|
|
1227
|
-
process.exit(1);
|
|
1212
|
+
handleCommandError(analytics, 'comment_delete', error);
|
|
1228
1213
|
}
|
|
1229
1214
|
});
|
|
1230
1215
|
|
|
@@ -1329,9 +1314,7 @@ program
|
|
|
1329
1314
|
|
|
1330
1315
|
analytics.track('export', true);
|
|
1331
1316
|
} catch (error) {
|
|
1332
|
-
analytics
|
|
1333
|
-
console.error(chalk.red('Error:'), error.message);
|
|
1334
|
-
process.exit(1);
|
|
1317
|
+
handleCommandError(analytics, 'export', error);
|
|
1335
1318
|
}
|
|
1336
1319
|
});
|
|
1337
1320
|
|
|
@@ -1717,9 +1700,7 @@ program
|
|
|
1717
1700
|
|
|
1718
1701
|
analytics.track('copy_tree', true);
|
|
1719
1702
|
} catch (error) {
|
|
1720
|
-
analytics
|
|
1721
|
-
console.error(chalk.red('Error:'), error.message);
|
|
1722
|
-
process.exit(1);
|
|
1703
|
+
handleCommandError(analytics, 'copy_tree', error);
|
|
1723
1704
|
}
|
|
1724
1705
|
});
|
|
1725
1706
|
|
|
@@ -1816,9 +1797,7 @@ program
|
|
|
1816
1797
|
|
|
1817
1798
|
analytics.track('children', true);
|
|
1818
1799
|
} catch (error) {
|
|
1819
|
-
analytics
|
|
1820
|
-
console.error(chalk.red('Error:'), error.message);
|
|
1821
|
-
process.exit(1);
|
|
1800
|
+
handleCommandError(analytics, 'children', error);
|
|
1822
1801
|
}
|
|
1823
1802
|
});
|
|
1824
1803
|
|
|
@@ -1917,9 +1896,12 @@ profileCmd
|
|
|
1917
1896
|
.option('-d, --domain <domain>', 'Confluence domain')
|
|
1918
1897
|
.option('--protocol <protocol>', 'Protocol (http or https)')
|
|
1919
1898
|
.option('-p, --api-path <path>', 'REST API path')
|
|
1920
|
-
.option('-a, --auth-type <type>', 'Authentication type (basic or
|
|
1899
|
+
.option('-a, --auth-type <type>', 'Authentication type (basic, bearer, or mtls)')
|
|
1921
1900
|
.option('-e, --email <email>', 'Email or username for basic auth')
|
|
1922
1901
|
.option('-t, --token <token>', 'API token')
|
|
1902
|
+
.option('--tls-ca-cert <path>', 'CA certificate for mTLS connections')
|
|
1903
|
+
.option('--tls-client-cert <path>', 'Client certificate for mTLS connections')
|
|
1904
|
+
.option('--tls-client-key <path>', 'Client private key for mTLS connections')
|
|
1923
1905
|
.option('--read-only', 'Set profile to read-only mode (blocks write operations)')
|
|
1924
1906
|
.action(async (name, options) => {
|
|
1925
1907
|
if (!isValidProfileName(name)) {
|
|
@@ -2033,9 +2015,7 @@ program
|
|
|
2033
2015
|
}
|
|
2034
2016
|
analytics.track('convert', true);
|
|
2035
2017
|
} catch (error) {
|
|
2036
|
-
analytics
|
|
2037
|
-
console.error(chalk.red('Error:'), error.message);
|
|
2038
|
-
process.exit(1);
|
|
2018
|
+
handleCommandError(analytics, 'convert', error);
|
|
2039
2019
|
}
|
|
2040
2020
|
});
|
|
2041
2021
|
|
|
@@ -2050,6 +2030,8 @@ module.exports = {
|
|
|
2050
2030
|
exportRecursive,
|
|
2051
2031
|
sanitizeTitle,
|
|
2052
2032
|
assertWritable,
|
|
2033
|
+
assertNonEmpty,
|
|
2034
|
+
handleCommandError,
|
|
2053
2035
|
},
|
|
2054
2036
|
};
|
|
2055
2037
|
|
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,116 @@ 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
|
+
// Validate a resolved auth configuration (post-normalization).
|
|
104
|
+
// Returns error messages only; callers format source-specific hints.
|
|
105
|
+
const validateAuthConfig = (auth, mtlsSourceLabel) => {
|
|
106
|
+
const errors = [];
|
|
107
|
+
|
|
108
|
+
if (auth.authType === 'basic' && !auth.email) {
|
|
109
|
+
errors.push('Basic authentication requires an email address or username.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (auth.authType !== 'mtls' && !auth.token) {
|
|
113
|
+
errors.push('Bearer or basic authentication requires a token.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (auth.authType === 'mtls') {
|
|
117
|
+
errors.push(...validateMtlsConfig(auth.mtls, mtlsSourceLabel));
|
|
118
|
+
const protocolError = validateMtlsProtocol(auth.protocol);
|
|
119
|
+
if (protocolError) {
|
|
120
|
+
errors.push(protocolError);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return errors;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build an inquirer question for an mTLS file path prompt.
|
|
129
|
+
* @param {string} name - answer key (e.g. 'tlsClientCert')
|
|
130
|
+
* @param {string} message - prompt text
|
|
131
|
+
* @param {boolean} required - whether the field is mandatory
|
|
132
|
+
* @param {Function} [whenFn] - optional custom `when` predicate; defaults to authType === 'mtls'
|
|
133
|
+
*/
|
|
134
|
+
const mtlsCertQuestion = (name, message, required, whenFn) => ({
|
|
135
|
+
type: 'input',
|
|
136
|
+
name,
|
|
137
|
+
message,
|
|
138
|
+
when: whenFn || ((responses) => responses.authType === 'mtls'),
|
|
139
|
+
validate: (input) => {
|
|
140
|
+
const value = (input || '').trim();
|
|
141
|
+
if (!value) {
|
|
142
|
+
return required ? `${message.replace(/:$/, '')} is required for mTLS.` : true;
|
|
143
|
+
}
|
|
144
|
+
if (!fs.existsSync(value)) {
|
|
145
|
+
return `File not found: ${value}`;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
46
151
|
const inferApiPath = (domain) => {
|
|
47
152
|
if (!domain) {
|
|
48
153
|
return '/rest/api';
|
|
@@ -89,6 +194,10 @@ function readConfigFile() {
|
|
|
89
194
|
token: raw.token,
|
|
90
195
|
authType: raw.authType
|
|
91
196
|
};
|
|
197
|
+
const mtls = normalizeMtlsConfig(raw.mtls);
|
|
198
|
+
if (mtls) {
|
|
199
|
+
profile.mtls = mtls;
|
|
200
|
+
}
|
|
92
201
|
if (raw.email) {
|
|
93
202
|
profile.email = raw.email;
|
|
94
203
|
}
|
|
@@ -123,7 +232,7 @@ const validateCliOptions = (options) => {
|
|
|
123
232
|
errors.push('--domain cannot be empty');
|
|
124
233
|
}
|
|
125
234
|
|
|
126
|
-
if (options.token && !options.token.trim()) {
|
|
235
|
+
if (options.token !== undefined && !options.token.trim()) {
|
|
127
236
|
errors.push('--token cannot be empty');
|
|
128
237
|
}
|
|
129
238
|
|
|
@@ -148,8 +257,8 @@ const validateCliOptions = (options) => {
|
|
|
148
257
|
errors.push('--protocol must be "http" or "https"');
|
|
149
258
|
}
|
|
150
259
|
|
|
151
|
-
if (options.authType && !['basic', 'bearer'].includes(options.authType.toLowerCase())) {
|
|
152
|
-
errors.push('--auth-type must be "basic" or "
|
|
260
|
+
if (options.authType && !['basic', 'bearer', 'mtls'].includes(options.authType.toLowerCase())) {
|
|
261
|
+
errors.push('--auth-type must be "basic", "bearer", or "mtls"');
|
|
153
262
|
}
|
|
154
263
|
|
|
155
264
|
// Check if basic auth is provided with email
|
|
@@ -158,6 +267,16 @@ const validateCliOptions = (options) => {
|
|
|
158
267
|
errors.push('--email is required when using basic authentication (use your username for on-premise)');
|
|
159
268
|
}
|
|
160
269
|
|
|
270
|
+
if (normAuthType === 'mtls') {
|
|
271
|
+
validateMtlsConfig(options.mtls, '--auth-type mtls').forEach((error) => {
|
|
272
|
+
errors.push(error);
|
|
273
|
+
});
|
|
274
|
+
const protocolError = validateMtlsProtocol(options.protocol);
|
|
275
|
+
if (protocolError) {
|
|
276
|
+
errors.push(protocolError);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
161
280
|
return errors;
|
|
162
281
|
};
|
|
163
282
|
|
|
@@ -167,14 +286,22 @@ const saveConfig = (configData, profileName) => {
|
|
|
167
286
|
domain: configData.domain.trim(),
|
|
168
287
|
protocol: normalizeProtocol(configData.protocol),
|
|
169
288
|
apiPath: normalizeApiPath(configData.apiPath, configData.domain),
|
|
170
|
-
token: configData.token.trim(),
|
|
171
289
|
authType: configData.authType
|
|
172
290
|
};
|
|
173
291
|
|
|
292
|
+
if (configData.token) {
|
|
293
|
+
config.token = configData.token.trim();
|
|
294
|
+
}
|
|
295
|
+
|
|
174
296
|
if (configData.authType === 'basic' && configData.email) {
|
|
175
297
|
config.email = configData.email.trim();
|
|
176
298
|
}
|
|
177
299
|
|
|
300
|
+
const mtls = normalizeMtlsConfig(configData.mtls);
|
|
301
|
+
if (mtls) {
|
|
302
|
+
config.mtls = mtls;
|
|
303
|
+
}
|
|
304
|
+
|
|
178
305
|
if (configData.readOnly) {
|
|
179
306
|
config.readOnly = true;
|
|
180
307
|
}
|
|
@@ -283,10 +410,30 @@ const promptForMissingValues = async (providedValues) => {
|
|
|
283
410
|
type: 'password',
|
|
284
411
|
name: 'token',
|
|
285
412
|
message: 'API token / password:',
|
|
413
|
+
when: (responses) => {
|
|
414
|
+
const authType = providedValues.authType || responses.authType;
|
|
415
|
+
return authType !== 'mtls';
|
|
416
|
+
},
|
|
286
417
|
validate: requiredInput('API token / password')
|
|
287
418
|
});
|
|
288
419
|
}
|
|
289
420
|
|
|
421
|
+
// mTLS certificate path questions
|
|
422
|
+
const mtls = normalizeMtlsConfig(providedValues.mtls);
|
|
423
|
+
const mtlsWhen = (responses) => {
|
|
424
|
+
const authType = providedValues.authType || responses.authType;
|
|
425
|
+
return authType === 'mtls';
|
|
426
|
+
};
|
|
427
|
+
if (!mtls || !mtls.clientCert) {
|
|
428
|
+
questions.push(mtlsCertQuestion('tlsClientCert', 'Path to client certificate file (PEM):', true, mtlsWhen));
|
|
429
|
+
}
|
|
430
|
+
if (!mtls || !mtls.clientKey) {
|
|
431
|
+
questions.push(mtlsCertQuestion('tlsClientKey', 'Path to client key file (PEM):', true, mtlsWhen));
|
|
432
|
+
}
|
|
433
|
+
if (!mtls || !mtls.caCert) {
|
|
434
|
+
questions.push(mtlsCertQuestion('tlsCaCert', 'Path to CA certificate file (PEM, optional):', false, mtlsWhen));
|
|
435
|
+
}
|
|
436
|
+
|
|
290
437
|
if (questions.length === 0) {
|
|
291
438
|
return providedValues;
|
|
292
439
|
}
|
|
@@ -313,7 +460,12 @@ async function initConfig(cliOptions = {}) {
|
|
|
313
460
|
apiPath: cliOptions.apiPath,
|
|
314
461
|
authType: cliOptions.authType,
|
|
315
462
|
email: cliOptions.email,
|
|
316
|
-
token: cliOptions.token
|
|
463
|
+
token: cliOptions.token,
|
|
464
|
+
mtls: cliOptions.mtls || {
|
|
465
|
+
caCert: cliOptions.tlsCaCert,
|
|
466
|
+
clientCert: cliOptions.tlsClientCert,
|
|
467
|
+
clientKey: cliOptions.tlsClientKey,
|
|
468
|
+
}
|
|
317
469
|
};
|
|
318
470
|
|
|
319
471
|
// Check if any CLI options were provided
|
|
@@ -380,11 +532,24 @@ async function initConfig(cliOptions = {}) {
|
|
|
380
532
|
type: 'password',
|
|
381
533
|
name: 'token',
|
|
382
534
|
message: 'API token / password:',
|
|
535
|
+
when: (responses) => responses.authType !== 'mtls',
|
|
383
536
|
validate: requiredInput('API token / password')
|
|
384
|
-
}
|
|
537
|
+
},
|
|
538
|
+
mtlsCertQuestion('tlsClientCert', 'Path to client certificate file (PEM):', true),
|
|
539
|
+
mtlsCertQuestion('tlsClientKey', 'Path to client key file (PEM):', true),
|
|
540
|
+
mtlsCertQuestion('tlsCaCert', 'Path to CA certificate file (PEM, optional):', false)
|
|
385
541
|
]);
|
|
386
542
|
|
|
387
|
-
|
|
543
|
+
const configData = { ...answers, readOnly };
|
|
544
|
+
if (answers.authType === 'mtls') {
|
|
545
|
+
configData.mtls = {
|
|
546
|
+
clientCert: answers.tlsClientCert,
|
|
547
|
+
clientKey: answers.tlsClientKey,
|
|
548
|
+
caCert: answers.tlsCaCert || undefined,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
saveConfig(configData, profileName);
|
|
388
553
|
return;
|
|
389
554
|
}
|
|
390
555
|
|
|
@@ -403,8 +568,13 @@ async function initConfig(cliOptions = {}) {
|
|
|
403
568
|
// Non-interactive requires: domain, token, and either authType or email (for inference)
|
|
404
569
|
const hasRequiredValues = Boolean(
|
|
405
570
|
providedValues.domain &&
|
|
406
|
-
|
|
407
|
-
|
|
571
|
+
(
|
|
572
|
+
providedValues.authType === 'mtls'
|
|
573
|
+
|| (
|
|
574
|
+
providedValues.token &&
|
|
575
|
+
(providedValues.authType || providedValues.email)
|
|
576
|
+
)
|
|
577
|
+
)
|
|
408
578
|
);
|
|
409
579
|
|
|
410
580
|
if (hasRequiredValues) {
|
|
@@ -425,6 +595,11 @@ async function initConfig(cliOptions = {}) {
|
|
|
425
595
|
process.exit(1);
|
|
426
596
|
}
|
|
427
597
|
|
|
598
|
+
if (normalizedAuthType !== 'mtls' && !providedValues.token) {
|
|
599
|
+
console.error(chalk.red('❌ Token is required for basic or bearer authentication'));
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
|
|
428
603
|
// Verify API path format if provided
|
|
429
604
|
if (providedValues.apiPath) {
|
|
430
605
|
normalizeApiPath(providedValues.apiPath, normalizedDomain);
|
|
@@ -437,6 +612,7 @@ async function initConfig(cliOptions = {}) {
|
|
|
437
612
|
token: providedValues.token,
|
|
438
613
|
authType: normalizedAuthType,
|
|
439
614
|
email: providedValues.email,
|
|
615
|
+
mtls: providedValues.mtls,
|
|
440
616
|
readOnly
|
|
441
617
|
};
|
|
442
618
|
|
|
@@ -461,6 +637,15 @@ async function initConfig(cliOptions = {}) {
|
|
|
461
637
|
// Normalize auth type
|
|
462
638
|
mergedValues.authType = normalizeAuthType(mergedValues.authType, Boolean(mergedValues.email));
|
|
463
639
|
|
|
640
|
+
// Build mTLS config from prompted values if needed
|
|
641
|
+
if (mergedValues.authType === 'mtls') {
|
|
642
|
+
mergedValues.mtls = normalizeMtlsConfig({
|
|
643
|
+
clientCert: mergedValues.tlsClientCert || (mergedValues.mtls && mergedValues.mtls.clientCert),
|
|
644
|
+
clientKey: mergedValues.tlsClientKey || (mergedValues.mtls && mergedValues.mtls.clientKey),
|
|
645
|
+
caCert: mergedValues.tlsCaCert || (mergedValues.mtls && mergedValues.mtls.caCert),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
464
649
|
saveConfig({ ...mergedValues, readOnly }, profileName);
|
|
465
650
|
} catch (error) {
|
|
466
651
|
console.error(chalk.red(`❌ ${error.message}`));
|
|
@@ -476,9 +661,16 @@ function getConfig(profileName) {
|
|
|
476
661
|
const envApiPath = process.env.CONFLUENCE_API_PATH;
|
|
477
662
|
const envProtocol = process.env.CONFLUENCE_PROTOCOL;
|
|
478
663
|
const envReadOnly = process.env.CONFLUENCE_READ_ONLY;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
664
|
+
const envForceCloud = process.env.CONFLUENCE_FORCE_CLOUD;
|
|
665
|
+
const envMtls = normalizeMtlsConfig({
|
|
666
|
+
caCert: process.env.CONFLUENCE_TLS_CA_CERT,
|
|
667
|
+
clientCert: process.env.CONFLUENCE_TLS_CLIENT_CERT,
|
|
668
|
+
clientKey: process.env.CONFLUENCE_TLS_CLIENT_KEY,
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
if (envDomain && (envToken || envAuthType === 'mtls' || envMtls)) {
|
|
672
|
+
const inferredAuthType = envAuthType || (envMtls && !envToken ? 'mtls' : undefined);
|
|
673
|
+
const authType = normalizeAuthType(inferredAuthType, Boolean(envEmail));
|
|
482
674
|
let apiPath;
|
|
483
675
|
|
|
484
676
|
try {
|
|
@@ -488,9 +680,18 @@ function getConfig(profileName) {
|
|
|
488
680
|
process.exit(1);
|
|
489
681
|
}
|
|
490
682
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
683
|
+
const authErrors = validateAuthConfig(
|
|
684
|
+
{ authType, token: envToken, email: envEmail, mtls: envMtls, protocol: envProtocol },
|
|
685
|
+
'CONFLUENCE_AUTH_TYPE=mtls'
|
|
686
|
+
);
|
|
687
|
+
if (authErrors.length > 0) {
|
|
688
|
+
console.error(chalk.red(`❌ ${authErrors.join(' ')}`));
|
|
689
|
+
if (authType === 'basic' && !envEmail) {
|
|
690
|
+
console.log(chalk.yellow('Set CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME for on-premise) or switch to bearer auth by setting CONFLUENCE_AUTH_TYPE=bearer.'));
|
|
691
|
+
}
|
|
692
|
+
if (authType === 'mtls' && !envMtls) {
|
|
693
|
+
console.log(chalk.yellow('Set CONFLUENCE_TLS_CLIENT_CERT and CONFLUENCE_TLS_CLIENT_KEY. Optionally set CONFLUENCE_TLS_CA_CERT.'));
|
|
694
|
+
}
|
|
494
695
|
process.exit(1);
|
|
495
696
|
}
|
|
496
697
|
|
|
@@ -498,10 +699,12 @@ function getConfig(profileName) {
|
|
|
498
699
|
domain: envDomain.trim(),
|
|
499
700
|
protocol: normalizeProtocol(envProtocol),
|
|
500
701
|
apiPath,
|
|
501
|
-
token: envToken.trim(),
|
|
702
|
+
token: envToken ? envToken.trim() : undefined,
|
|
502
703
|
email: envEmail ? envEmail.trim() : undefined,
|
|
503
704
|
authType,
|
|
504
|
-
|
|
705
|
+
mtls: envMtls,
|
|
706
|
+
readOnly: envReadOnly === 'true',
|
|
707
|
+
forceCloud: envForceCloud === 'true'
|
|
505
708
|
};
|
|
506
709
|
}
|
|
507
710
|
|
|
@@ -534,20 +737,25 @@ function getConfig(profileName) {
|
|
|
534
737
|
|
|
535
738
|
try {
|
|
536
739
|
const trimmedDomain = (storedConfig.domain || '').trim();
|
|
537
|
-
const trimmedToken = (storedConfig.token
|
|
740
|
+
const trimmedToken = trimOptional(storedConfig.token);
|
|
538
741
|
const trimmedEmail = storedConfig.email ? storedConfig.email.trim() : undefined;
|
|
539
742
|
const authType = normalizeAuthType(storedConfig.authType, Boolean(trimmedEmail));
|
|
743
|
+
const mtls = normalizeMtlsConfig(storedConfig.mtls);
|
|
540
744
|
let apiPath;
|
|
541
745
|
|
|
542
|
-
if (!trimmedDomain
|
|
746
|
+
if (!trimmedDomain) {
|
|
543
747
|
console.error(chalk.red('❌ Configuration file is missing required values.'));
|
|
544
748
|
console.log(chalk.yellow('Run "confluence init" to refresh your settings.'));
|
|
545
749
|
process.exit(1);
|
|
546
750
|
}
|
|
547
751
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
752
|
+
const authErrors = validateAuthConfig(
|
|
753
|
+
{ authType, token: trimmedToken, email: trimmedEmail, mtls, protocol: storedConfig.protocol },
|
|
754
|
+
'mTLS authentication'
|
|
755
|
+
);
|
|
756
|
+
if (authErrors.length > 0) {
|
|
757
|
+
console.error(chalk.red(`❌ ${authErrors.join(' ')}`));
|
|
758
|
+
console.log(chalk.yellow('Please rerun "confluence init" to refresh your settings.'));
|
|
551
759
|
process.exit(1);
|
|
552
760
|
}
|
|
553
761
|
|
|
@@ -563,6 +771,10 @@ function getConfig(profileName) {
|
|
|
563
771
|
? envReadOnly === 'true'
|
|
564
772
|
: Boolean(storedConfig.readOnly);
|
|
565
773
|
|
|
774
|
+
const forceCloud = envForceCloud !== undefined
|
|
775
|
+
? envForceCloud === 'true'
|
|
776
|
+
: Boolean(storedConfig.forceCloud);
|
|
777
|
+
|
|
566
778
|
return {
|
|
567
779
|
domain: trimmedDomain,
|
|
568
780
|
protocol: normalizeProtocol(storedConfig.protocol),
|
|
@@ -570,7 +782,9 @@ function getConfig(profileName) {
|
|
|
570
782
|
token: trimmedToken,
|
|
571
783
|
email: trimmedEmail,
|
|
572
784
|
authType,
|
|
573
|
-
|
|
785
|
+
mtls,
|
|
786
|
+
readOnly,
|
|
787
|
+
forceCloud
|
|
574
788
|
};
|
|
575
789
|
} catch (error) {
|
|
576
790
|
console.error(chalk.red('❌ Error reading configuration file:'), error.message);
|
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,8 @@ 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.forceCloud = !!config.forceCloud;
|
|
39
|
+
this.mtls = config.mtls;
|
|
37
40
|
this.apiPath = this.sanitizeApiPath(config.apiPath);
|
|
38
41
|
this.webUrlPrefix = this.apiPath.startsWith('/wiki/') ? '/wiki' : '';
|
|
39
42
|
this.baseURL = `${this.protocol}://${this.domain}${this.apiPath}`;
|
|
@@ -41,14 +44,23 @@ class ConfluenceClient {
|
|
|
41
44
|
this.setupConfluenceMarkdownExtensions();
|
|
42
45
|
|
|
43
46
|
const headers = {
|
|
44
|
-
'Content-Type': 'application/json'
|
|
45
|
-
'Authorization': this.authType === 'basic' ? this.buildBasicAuthHeader() : `Bearer ${this.token}`
|
|
47
|
+
'Content-Type': 'application/json'
|
|
46
48
|
};
|
|
49
|
+
const authHeader = this.buildAuthHeader();
|
|
50
|
+
if (authHeader) {
|
|
51
|
+
headers.Authorization = authHeader;
|
|
52
|
+
}
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
const clientOptions = {
|
|
49
55
|
baseURL: this.baseURL,
|
|
50
56
|
headers
|
|
51
|
-
}
|
|
57
|
+
};
|
|
58
|
+
const httpsAgent = this.buildHttpsAgent();
|
|
59
|
+
if (httpsAgent) {
|
|
60
|
+
clientOptions.httpsAgent = httpsAgent;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.client = axios.create(clientOptions);
|
|
52
64
|
|
|
53
65
|
this.client.interceptors.response.use(
|
|
54
66
|
response => response,
|
|
@@ -72,6 +84,10 @@ class ConfluenceClient {
|
|
|
72
84
|
hints.push(
|
|
73
85
|
'Please verify your username and password are correct.'
|
|
74
86
|
);
|
|
87
|
+
} else if (this.authType === 'mtls') {
|
|
88
|
+
hints.push(
|
|
89
|
+
'Please verify your client certificate, client key, and CA certificate are correct and trusted by the server.'
|
|
90
|
+
);
|
|
75
91
|
} else {
|
|
76
92
|
hints.push(
|
|
77
93
|
'Please verify your personal access token is valid and not expired.'
|
|
@@ -85,7 +101,7 @@ class ConfluenceClient {
|
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
isCloud() {
|
|
88
|
-
return this.isScopedToken() || (this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net'));
|
|
104
|
+
return this.isScopedToken() || (this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net')) || this.forceCloud;
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
isScopedToken() {
|
|
@@ -115,6 +131,67 @@ class ConfluenceClient {
|
|
|
115
131
|
return `Basic ${encodedCredentials}`;
|
|
116
132
|
}
|
|
117
133
|
|
|
134
|
+
buildAuthHeader() {
|
|
135
|
+
if (this.authType === 'mtls') {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!this.token) {
|
|
140
|
+
throw new Error(`Authentication type "${this.authType}" requires a token or password.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return this.authType === 'basic' ? this.buildBasicAuthHeader() : `Bearer ${this.token}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
buildHttpsAgent() {
|
|
147
|
+
if (this.protocol !== 'https' || !this.mtls) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const options = {};
|
|
152
|
+
|
|
153
|
+
if (this.mtls.caCert) {
|
|
154
|
+
if (!fs.existsSync(this.mtls.caCert)) {
|
|
155
|
+
throw new Error(`CA certificate file not found: ${this.mtls.caCert}`);
|
|
156
|
+
}
|
|
157
|
+
options.ca = fs.readFileSync(this.mtls.caCert);
|
|
158
|
+
}
|
|
159
|
+
if (this.mtls.clientCert) {
|
|
160
|
+
if (!fs.existsSync(this.mtls.clientCert)) {
|
|
161
|
+
throw new Error(`Client certificate file not found: ${this.mtls.clientCert}`);
|
|
162
|
+
}
|
|
163
|
+
options.cert = fs.readFileSync(this.mtls.clientCert);
|
|
164
|
+
}
|
|
165
|
+
if (this.mtls.clientKey) {
|
|
166
|
+
if (!fs.existsSync(this.mtls.clientKey)) {
|
|
167
|
+
throw new Error(`Client key file not found: ${this.mtls.clientKey}`);
|
|
168
|
+
}
|
|
169
|
+
// Warn if private key file is readable by others (Unix only)
|
|
170
|
+
if (process.platform !== 'win32') {
|
|
171
|
+
try {
|
|
172
|
+
const keyStats = fs.statSync(this.mtls.clientKey);
|
|
173
|
+
const keyMode = keyStats.mode & 0o777;
|
|
174
|
+
if (keyMode & 0o077) {
|
|
175
|
+
console.error(
|
|
176
|
+
`Warning: Client key file "${this.mtls.clientKey}" has mode ${keyMode.toString(8)}. ` +
|
|
177
|
+
'Private keys should not be readable by other users (recommended: 0600). ' +
|
|
178
|
+
`Fix with: chmod 600 "${this.mtls.clientKey}"`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Ignore stat errors — the read below will surface them
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
options.key = fs.readFileSync(this.mtls.clientKey);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (Object.keys(options).length === 0) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return new https.Agent(options);
|
|
193
|
+
}
|
|
194
|
+
|
|
118
195
|
/**
|
|
119
196
|
* Extract page ID from URL or return the ID if it's already a number
|
|
120
197
|
*/
|
|
@@ -401,12 +478,12 @@ class ConfluenceClient {
|
|
|
401
478
|
// Replace userkey references with display names in HTML
|
|
402
479
|
let resolvedHtml = html;
|
|
403
480
|
userMap.forEach((displayName, userKey) => {
|
|
404
|
-
|
|
481
|
+
const escapedKey = userKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
405
482
|
const userLinkRegex = new RegExp(
|
|
406
|
-
`<ac:link>\\s*<ri:user\\s+ri:userkey="${
|
|
483
|
+
`<ac:link>\\s*<ri:user\\s+ri:userkey="${escapedKey}"\\s*/>\\s*</ac:link>`,
|
|
407
484
|
'g'
|
|
408
485
|
);
|
|
409
|
-
resolvedHtml = resolvedHtml.replace(userLinkRegex, `@${displayName}`);
|
|
486
|
+
resolvedHtml = resolvedHtml.replace(userLinkRegex, () => `@${displayName}`);
|
|
410
487
|
});
|
|
411
488
|
|
|
412
489
|
return { html: resolvedHtml, userMap };
|
|
@@ -1423,9 +1500,12 @@ class ConfluenceClient {
|
|
|
1423
1500
|
// Format: <ac:link><ri:page ri:space-key="xxx" ri:content-title="Page Title" /></ac:link>
|
|
1424
1501
|
markdown = markdown.replace(/<ac:link>\s*<ri:page[^>]*ri:content-title="([^"]*)"[^>]*\/>\s*<\/ac:link>/g, '[$1]');
|
|
1425
1502
|
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, '');
|
|
1503
|
+
|
|
1504
|
+
// Convert internal page links with custom display text (ac:link-body)
|
|
1505
|
+
markdown = markdown.replace(/<ac:link[^>]*>[\s\S]*?<ac:link-body>([\s\S]*?)<\/ac:link-body>[\s\S]*?<\/ac:link>/g, '$1');
|
|
1506
|
+
|
|
1507
|
+
// Remove any remaining ac:link tags that weren't matched (including those with attributes)
|
|
1508
|
+
markdown = markdown.replace(/<ac:link[^>]*>[\s\S]*?<\/ac:link>/g, '');
|
|
1429
1509
|
|
|
1430
1510
|
// Convert remaining HTML to markdown
|
|
1431
1511
|
markdown = this.htmlToMarkdown(markdown);
|
|
@@ -2132,4 +2212,4 @@ ConfluenceClient.createLocalConverter = function () {
|
|
|
2132
2212
|
};
|
|
2133
2213
|
|
|
2134
2214
|
module.exports = ConfluenceClient;
|
|
2135
|
-
module.exports.NAMED_ENTITIES = NAMED_ENTITIES;
|
|
2215
|
+
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.2",
|
|
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.2",
|
|
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",
|
package/package.json
CHANGED
|
@@ -29,6 +29,7 @@ confluence --version # verify install
|
|
|
29
29
|
| `CONFLUENCE_API_TOKEN` | API token or personal access token | `ATATT3x...` |
|
|
30
30
|
| `CONFLUENCE_PROFILE` | Named profile to use (optional) | `staging` |
|
|
31
31
|
| `CONFLUENCE_READ_ONLY` | Block all write operations when `true` | `true` |
|
|
32
|
+
| `CONFLUENCE_FORCE_CLOUD` | Force Cloud link format for custom domains | `true` |
|
|
32
33
|
|
|
33
34
|
**Global `--profile` flag (use a named profile for any command):**
|
|
34
35
|
|
|
@@ -53,6 +54,7 @@ confluence init \
|
|
|
53
54
|
|
|
54
55
|
**Cloud vs Server/DC:**
|
|
55
56
|
- Atlassian Cloud (`*.atlassian.net`): use `--api-path "/wiki/rest/api"`, auth type `basic` with email + API token
|
|
57
|
+
- Atlassian Cloud (custom domain): if your Cloud instance uses a custom domain (e.g., `wiki.example.org`), set `CONFLUENCE_FORCE_CLOUD=true` or add `"forceCloud": true` to your profile in `~/.confluence-cli/config.json`. Without this, links will render incorrectly.
|
|
56
58
|
- Atlassian Cloud (scoped token): use `--domain "api.atlassian.com"`, `--api-path "/ex/confluence/<your-cloud-id>/wiki/rest/api"`, auth type `basic` with email + scoped token. Get your Cloud ID from `https://<your-site>.atlassian.net/_edge/tenant_info`. Recommended for agents (least privilege).
|
|
57
59
|
- Self-hosted / Data Center: use `--api-path "/rest/api"`, auth type `bearer` with a personal access token (no email needed)
|
|
58
60
|
|