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 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 Bearer authentication.
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 `bearer`
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 bearer)')
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.track('read', false);
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.track('info', false);
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.track('search', false);
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.track('spaces', false);
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.track('create', false);
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.track('create_child', false);
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.track('update', false);
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.track('move', false);
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.track('delete', false);
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.track('edit', false);
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.track('find', false);
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.track('attachments', false);
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.track('attachment_upload', false);
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.track('attachment_delete', false);
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.track('property_list', false);
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.track('property_get', false);
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.track('property_set', false);
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.track('property_delete', false);
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.track('comments', false);
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.track('comment_delete', false);
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.track('export', false);
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.track('copy_tree', false);
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.track('children', false);
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 bearer)')
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.track('convert', false);
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 "bearer"');
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
- saveConfig({ ...answers, readOnly }, profileName);
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
- providedValues.token &&
407
- (providedValues.authType || providedValues.email)
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
- if (envDomain && envToken) {
481
- const authType = normalizeAuthType(envAuthType, Boolean(envEmail));
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
- if (authType === 'basic' && !envEmail) {
492
- console.error(chalk.red('❌ Basic authentication requires CONFLUENCE_EMAIL or CONFLUENCE_USERNAME.'));
493
- console.log(chalk.yellow('Set CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME for on-premise) or switch to bearer auth by setting CONFLUENCE_AUTH_TYPE=bearer.'));
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
- readOnly: envReadOnly === 'true'
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 || '').trim();
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 || !trimmedToken) {
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
- if (authType === 'basic' && !trimmedEmail) {
549
- console.error(chalk.red('❌ Basic authentication requires an email address or username.'));
550
- console.log(chalk.yellow('Please rerun "confluence init" to add your Confluence email or username.'));
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
- readOnly
785
+ mtls,
786
+ readOnly,
787
+ forceCloud
574
788
  };
575
789
  } catch (error) {
576
790
  console.error(chalk.red('❌ Error reading configuration file:'), error.message);
@@ -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
- this.client = axios.create({
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
- // Replace <ac:link><ri:user ri:userkey="xxx" /></ac:link> with @displayName
481
+ const escapedKey = userKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
405
482
  const userLinkRegex = new RegExp(
406
- `<ac:link>\\s*<ri:user\\s+ri:userkey="${userKey}"\\s*/>\\s*</ac:link>`,
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
- // Remove any remaining ac:link tags that weren't matched
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;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.30.0",
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.0",
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.15.11",
2688
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
2689
- "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.30.0",
3
+ "version": "1.30.2",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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