confluence-cli 1.30.1 → 1.31.0

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
@@ -149,6 +149,21 @@ confluence --profile corp init \
149
149
  --tls-ca-cert "~/.certs/ca-chain.pem"
150
150
  ```
151
151
 
152
+ **Cookie authentication profile** (Enterprise SSO):
153
+ ```bash
154
+ confluence --profile sso init \
155
+ --domain "confluence.company.com" \
156
+ --api-path "/rest/api" \
157
+ --auth-type "cookie" \
158
+ --cookie "JSESSIONID=abc123xyz..."
159
+
160
+ # Multiple cookies are also supported:
161
+ confluence --profile sso init \
162
+ --domain "confluence.company.com" \
163
+ --auth-type "cookie" \
164
+ --cookie "JSESSIONID=abc123; XSRF-TOKEN=xyz789"
165
+ ```
166
+
152
167
  **Hybrid mode** (some fields provided, rest via prompts):
153
168
  ```bash
154
169
  # Domain and token provided, will prompt for auth method and email
@@ -161,9 +176,10 @@ confluence init --email "user@example.com" --token "your-api-token"
161
176
  **Available flags:**
162
177
  - `-d, --domain <domain>` - Confluence domain (e.g., `company.atlassian.net`)
163
178
  - `-p, --api-path <path>` - REST API path (e.g., `/wiki/rest/api`)
164
- - `-a, --auth-type <type>` - Authentication type: `basic`, `bearer`, or `mtls`
179
+ - `-a, --auth-type <type>` - Authentication type: `basic`, `bearer`, `mtls`, or `cookie`
165
180
  - `-e, --email <email>` - Email or username for basic authentication
166
181
  - `-t, --token <token>` - API token or password
182
+ - `-c, --cookie <cookie>` - Cookie for Enterprise SSO authentication (e.g., `"JSESSIONID=..."`)
167
183
  - `--tls-client-cert <path>` - Client certificate for mTLS authentication
168
184
  - `--tls-client-key <path>` - Client private key for mTLS authentication
169
185
  - `--tls-ca-cert <path>` - Optional CA certificate chain for mTLS authentication
@@ -193,6 +209,14 @@ export CONFLUENCE_TLS_CLIENT_KEY="~/.certs/client.key"
193
209
  export CONFLUENCE_TLS_CA_CERT="~/.certs/ca-chain.pem" # optional
194
210
  ```
195
211
 
212
+ **Cookie environment variables** (Enterprise SSO):
213
+ ```bash
214
+ export CONFLUENCE_DOMAIN="confluence.company.com"
215
+ export CONFLUENCE_API_PATH="/rest/api"
216
+ export CONFLUENCE_AUTH_TYPE="cookie"
217
+ export CONFLUENCE_COOKIE="JSESSIONID=abc123xyz..."
218
+ ```
219
+
196
220
  **Scoped API token** (recommended for agents):
197
221
  ```bash
198
222
  export CONFLUENCE_DOMAIN="api.atlassian.com"
@@ -204,6 +228,27 @@ export CONFLUENCE_API_TOKEN="your-scoped-token"
204
228
 
205
229
  `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
230
 
231
+ **Custom domains on Confluence Cloud:**
232
+
233
+ 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:
234
+
235
+ ```bash
236
+ export CONFLUENCE_FORCE_CLOUD=true
237
+ ```
238
+
239
+ Or add `"forceCloud": true` to your profile in `~/.confluence-cli/config.json`:
240
+
241
+ ```json
242
+ {
243
+ "profiles": {
244
+ "default": {
245
+ "domain": "wiki.example.org",
246
+ "forceCloud": true
247
+ }
248
+ }
249
+ }
250
+ ```
251
+
207
252
  **Read-only mode** (recommended for AI agents):
208
253
  ```bash
209
254
  export CONFLUENCE_READ_ONLY=true
@@ -250,6 +295,8 @@ For **read-only** usage, select at minimum: `read:confluence-content.all`, `read
250
295
 
251
296
  **mTLS-protected Confluence APIs:** Some self-hosted or reverse-proxied deployments authenticate at the TLS layer with a client certificate instead of sending an application-level token. In these environments, configure `authType=mtls` and provide certificate paths via CLI flags or environment variables. No `Authorization` header will be sent in mTLS mode.
252
297
 
298
+ **Enterprise SSO with Cookie Authentication:** For Confluence instances behind Enterprise SSO (SAML, OAuth, Okta, etc.) where API tokens or Basic/Bearer auth are not available, you can authenticate using session cookies. After logging in through your browser, extract the session cookie (typically `JSESSIONID` or similar) from your browser's dev tools and configure it via the `--cookie` flag or `CONFLUENCE_COOKIE` environment variable. The cookie is sent in the `Cookie` header instead of an `Authorization` header. Note that session cookies typically expire, so you'll need to refresh them periodically. For security, prefer `CONFLUENCE_COOKIE` env var or interactive prompt over `--cookie` flag since command-line arguments may be visible in shell history and process listings.
299
+
253
300
  ## Usage
254
301
 
255
302
  ### 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,10 @@ 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, bearer, or mtls)')
49
+ .option('-a, --auth-type <type>', 'Authentication type (basic, bearer, mtls, or cookie)')
38
50
  .option('-e, --email <email>', 'Email or username for basic auth')
39
51
  .option('-t, --token <token>', 'API token')
52
+ .option('-c, --cookie <cookie>', 'Cookie for Enterprise SSO authentication (e.g., "JSESSIONID=...")')
40
53
  .option('--tls-ca-cert <path>', 'CA certificate for mTLS connections')
41
54
  .option('--tls-client-cert <path>', 'Client certificate for mTLS connections')
42
55
  .option('--tls-client-key <path>', 'Client private key for mTLS connections')
@@ -59,9 +72,7 @@ program
59
72
  console.log(content);
60
73
  analytics.track('read', true);
61
74
  } catch (error) {
62
- analytics.track('read', false);
63
- console.error(chalk.red('Error:'), error.message);
64
- process.exit(1);
75
+ handleCommandError(analytics, 'read', error);
65
76
  }
66
77
  });
67
78
 
@@ -84,9 +95,7 @@ program
84
95
  }
85
96
  analytics.track('info', true);
86
97
  } catch (error) {
87
- analytics.track('info', false);
88
- console.error(chalk.red('Error:'), error.message);
89
- process.exit(1);
98
+ handleCommandError(analytics, 'info', error);
90
99
  }
91
100
  });
92
101
 
@@ -117,9 +126,7 @@ program
117
126
  });
118
127
  analytics.track('search', true);
119
128
  } catch (error) {
120
- analytics.track('search', false);
121
- console.error(chalk.red('Error:'), error.message);
122
- process.exit(1);
129
+ handleCommandError(analytics, 'search', error);
123
130
  }
124
131
  });
125
132
 
@@ -140,9 +147,7 @@ program
140
147
  });
141
148
  analytics.track('spaces', true);
142
149
  } catch (error) {
143
- analytics.track('spaces', false);
144
- console.error(chalk.red('Error:'), error.message);
145
- process.exit(1);
150
+ handleCommandError(analytics, 'spaces', error);
146
151
  }
147
152
  });
148
153
 
@@ -214,12 +219,15 @@ program
214
219
  .action(async (title, spaceKey, options) => {
215
220
  const analytics = new Analytics();
216
221
  try {
222
+ assertNonEmpty(title, 'title');
223
+ assertNonEmpty(spaceKey, 'spaceKey');
224
+
217
225
  const config = getConfig(getProfileName());
218
226
  assertWritable(config);
219
227
  const client = new ConfluenceClient(config);
220
228
 
221
229
  let content = '';
222
-
230
+
223
231
  if (options.file) {
224
232
  const fs = require('fs');
225
233
  if (!fs.existsSync(options.file)) {
@@ -231,7 +239,7 @@ program
231
239
  } else {
232
240
  throw new Error('Either --file or --content option is required');
233
241
  }
234
-
242
+
235
243
  const result = await client.createPage(title, spaceKey, content, options.format);
236
244
 
237
245
  console.log(chalk.green('✅ Page created successfully!'));
@@ -242,9 +250,7 @@ program
242
250
 
243
251
  analytics.track('create', true);
244
252
  } catch (error) {
245
- analytics.track('create', false);
246
- console.error(chalk.red('Error:'), error.message);
247
- process.exit(1);
253
+ handleCommandError(analytics, 'create', error);
248
254
  }
249
255
  });
250
256
 
@@ -258,6 +264,9 @@ program
258
264
  .action(async (title, parentId, options) => {
259
265
  const analytics = new Analytics();
260
266
  try {
267
+ assertNonEmpty(title, 'title');
268
+ assertNonEmpty(parentId, 'parentId');
269
+
261
270
  const config = getConfig(getProfileName());
262
271
  assertWritable(config);
263
272
  const client = new ConfluenceClient(config);
@@ -291,9 +300,7 @@ program
291
300
 
292
301
  analytics.track('create_child', true);
293
302
  } catch (error) {
294
- analytics.track('create_child', false);
295
- console.error(chalk.red('Error:'), error.message);
296
- process.exit(1);
303
+ handleCommandError(analytics, 'create_child', error);
297
304
  }
298
305
  });
299
306
 
@@ -313,6 +320,10 @@ program
313
320
  throw new Error('At least one of --title, --file, or --content must be provided.');
314
321
  }
315
322
 
323
+ if (options.title !== undefined) {
324
+ assertNonEmpty(options.title, '--title');
325
+ }
326
+
316
327
  const config = getConfig(getProfileName());
317
328
  assertWritable(config);
318
329
  const client = new ConfluenceClient(config);
@@ -339,9 +350,7 @@ program
339
350
 
340
351
  analytics.track('update', true);
341
352
  } catch (error) {
342
- analytics.track('update', false);
343
- console.error(chalk.red('Error:'), error.message);
344
- process.exit(1);
353
+ handleCommandError(analytics, 'update', error);
345
354
  }
346
355
  });
347
356
 
@@ -367,9 +376,7 @@ program
367
376
 
368
377
  analytics.track('move', true);
369
378
  } catch (error) {
370
- analytics.track('move', false);
371
- console.error(chalk.red('Error:'), error.message);
372
- process.exit(1);
379
+ handleCommandError(analytics, 'move', error);
373
380
  }
374
381
  });
375
382
 
@@ -411,9 +418,7 @@ program
411
418
  console.log(`ID: ${chalk.blue(result.id)}`);
412
419
  analytics.track('delete', true);
413
420
  } catch (error) {
414
- analytics.track('delete', false);
415
- console.error(chalk.red('Error:'), error.message);
416
- process.exit(1);
421
+ handleCommandError(analytics, 'delete', error);
417
422
  }
418
423
  });
419
424
 
@@ -449,9 +454,7 @@ program
449
454
 
450
455
  analytics.track('edit', true);
451
456
  } catch (error) {
452
- analytics.track('edit', false);
453
- console.error(chalk.red('Error:'), error.message);
454
- process.exit(1);
457
+ handleCommandError(analytics, 'edit', error);
455
458
  }
456
459
  });
457
460
 
@@ -475,9 +478,7 @@ program
475
478
 
476
479
  analytics.track('find', true);
477
480
  } catch (error) {
478
- analytics.track('find', false);
479
- console.error(chalk.red('Error:'), error.message);
480
- process.exit(1);
481
+ handleCommandError(analytics, 'find', error);
481
482
  }
482
483
  });
483
484
 
@@ -551,8 +552,9 @@ program
551
552
  fs.mkdirSync(destDir, { recursive: true });
552
553
 
553
554
  const uniquePathFor = (dir, filename) => {
554
- const parsed = path.parse(filename);
555
- let attempt = path.join(dir, filename);
555
+ const safeFilename = sanitizeFilename(filename);
556
+ const parsed = path.parse(safeFilename);
557
+ let attempt = path.join(dir, safeFilename);
556
558
  let counter = 1;
557
559
  while (fs.existsSync(attempt)) {
558
560
  const suffix = ` (${counter})`;
@@ -597,9 +599,7 @@ program
597
599
 
598
600
  analytics.track('attachments', true);
599
601
  } catch (error) {
600
- analytics.track('attachments', false);
601
- console.error(chalk.red('Error:'), error.message);
602
- process.exit(1);
602
+ handleCommandError(analytics, 'attachments', error);
603
603
  }
604
604
  });
605
605
 
@@ -659,9 +659,7 @@ program
659
659
  console.log(chalk.green(`Uploaded ${uploaded} attachment${uploaded === 1 ? '' : 's'} to page ${pageId}`));
660
660
  analytics.track('attachment_upload', true);
661
661
  } catch (error) {
662
- analytics.track('attachment_upload', false);
663
- console.error(chalk.red('Error:'), error.message);
664
- process.exit(1);
662
+ handleCommandError(analytics, 'attachment_upload', error);
665
663
  }
666
664
  });
667
665
 
@@ -701,9 +699,7 @@ program
701
699
  console.log(`Page ID: ${chalk.blue(result.pageId)}`);
702
700
  analytics.track('attachment_delete', true);
703
701
  } catch (error) {
704
- analytics.track('attachment_delete', false);
705
- console.error(chalk.red('Error:'), error.message);
706
- process.exit(1);
702
+ handleCommandError(analytics, 'attachment_delete', error);
707
703
  }
708
704
  });
709
705
 
@@ -774,9 +770,7 @@ program
774
770
  }
775
771
  analytics.track('property_list', true);
776
772
  } catch (error) {
777
- analytics.track('property_list', false);
778
- console.error(chalk.red('Error:'), error.message);
779
- process.exit(1);
773
+ handleCommandError(analytics, 'property_list', error);
780
774
  }
781
775
  });
782
776
 
@@ -808,9 +802,7 @@ program
808
802
  }
809
803
  analytics.track('property_get', true);
810
804
  } catch (error) {
811
- analytics.track('property_get', false);
812
- console.error(chalk.red('Error:'), error.message);
813
- process.exit(1);
805
+ handleCommandError(analytics, 'property_get', error);
814
806
  }
815
807
  });
816
808
 
@@ -867,9 +859,7 @@ program
867
859
  }
868
860
  analytics.track('property_set', true);
869
861
  } catch (error) {
870
- analytics.track('property_set', false);
871
- console.error(chalk.red('Error:'), error.message);
872
- process.exit(1);
862
+ handleCommandError(analytics, 'property_set', error);
873
863
  }
874
864
  });
875
865
 
@@ -909,9 +899,7 @@ program
909
899
  console.log(`${chalk.green('Page ID:')} ${chalk.blue(result.pageId)}`);
910
900
  analytics.track('property_delete', true);
911
901
  } catch (error) {
912
- analytics.track('property_delete', false);
913
- console.error(chalk.red('Error:'), error.message);
914
- process.exit(1);
902
+ handleCommandError(analytics, 'property_delete', error);
915
903
  }
916
904
  });
917
905
 
@@ -1060,9 +1048,7 @@ program
1060
1048
 
1061
1049
  analytics.track('comments', true);
1062
1050
  } catch (error) {
1063
- analytics.track('comments', false);
1064
- console.error(chalk.red('Error:'), error.message);
1065
- process.exit(1);
1051
+ handleCommandError(analytics, 'comments', error);
1066
1052
  }
1067
1053
  });
1068
1054
 
@@ -1225,9 +1211,7 @@ program
1225
1211
  console.log(`ID: ${chalk.blue(result.id)}`);
1226
1212
  analytics.track('comment_delete', true);
1227
1213
  } catch (error) {
1228
- analytics.track('comment_delete', false);
1229
- console.error(chalk.red('Error:'), error.message);
1230
- process.exit(1);
1214
+ handleCommandError(analytics, 'comment_delete', error);
1231
1215
  }
1232
1216
  });
1233
1217
 
@@ -1332,9 +1316,7 @@ program
1332
1316
 
1333
1317
  analytics.track('export', true);
1334
1318
  } catch (error) {
1335
- analytics.track('export', false);
1336
- console.error(chalk.red('Error:'), error.message);
1337
- process.exit(1);
1319
+ handleCommandError(analytics, 'export', error);
1338
1320
  }
1339
1321
  });
1340
1322
 
@@ -1354,9 +1336,24 @@ function isExportDirectory(fs, path, dir) {
1354
1336
  return fs.existsSync(path.join(dir, EXPORT_MARKER));
1355
1337
  }
1356
1338
 
1339
+ function sanitizeFilename(filename) {
1340
+ if (!filename || typeof filename !== 'string') {
1341
+ return 'unnamed';
1342
+ }
1343
+ const path = require('path');
1344
+ const stripped = path.basename(filename.replace(/\\/g, '/'));
1345
+ const cleaned = stripped
1346
+ // eslint-disable-next-line no-control-regex
1347
+ .replace(/[\\/:*?"<>|\x00-\x1f]/g, '_')
1348
+ .replace(/^\.+/, '')
1349
+ .trim();
1350
+ return cleaned || 'unnamed';
1351
+ }
1352
+
1357
1353
  function uniquePathFor(fs, path, dir, filename) {
1358
- const parsed = path.parse(filename);
1359
- let attempt = path.join(dir, filename);
1354
+ const safeFilename = sanitizeFilename(filename);
1355
+ const parsed = path.parse(safeFilename);
1356
+ let attempt = path.join(dir, safeFilename);
1360
1357
  let counter = 1;
1361
1358
  while (fs.existsSync(attempt)) {
1362
1359
  const suffix = ` (${counter})`;
@@ -1556,7 +1553,11 @@ function sanitizeTitle(value) {
1556
1553
  if (!value || typeof value !== 'string') {
1557
1554
  return fallback;
1558
1555
  }
1559
- const cleaned = value.replace(/[\\/:*?"<>|]/g, ' ').trim();
1556
+ const cleaned = value
1557
+ // eslint-disable-next-line no-control-regex
1558
+ .replace(/[\\/:*?"<>|\x00-\x1f]/g, ' ')
1559
+ .replace(/^\.+/, '')
1560
+ .trim();
1560
1561
  return cleaned || fallback;
1561
1562
  }
1562
1563
 
@@ -1720,9 +1721,7 @@ program
1720
1721
 
1721
1722
  analytics.track('copy_tree', true);
1722
1723
  } catch (error) {
1723
- analytics.track('copy_tree', false);
1724
- console.error(chalk.red('Error:'), error.message);
1725
- process.exit(1);
1724
+ handleCommandError(analytics, 'copy_tree', error);
1726
1725
  }
1727
1726
  });
1728
1727
 
@@ -1819,9 +1818,7 @@ program
1819
1818
 
1820
1819
  analytics.track('children', true);
1821
1820
  } catch (error) {
1822
- analytics.track('children', false);
1823
- console.error(chalk.red('Error:'), error.message);
1824
- process.exit(1);
1821
+ handleCommandError(analytics, 'children', error);
1825
1822
  }
1826
1823
  });
1827
1824
 
@@ -1920,9 +1917,10 @@ profileCmd
1920
1917
  .option('-d, --domain <domain>', 'Confluence domain')
1921
1918
  .option('--protocol <protocol>', 'Protocol (http or https)')
1922
1919
  .option('-p, --api-path <path>', 'REST API path')
1923
- .option('-a, --auth-type <type>', 'Authentication type (basic, bearer, or mtls)')
1920
+ .option('-a, --auth-type <type>', 'Authentication type (basic, bearer, mtls, or cookie)')
1924
1921
  .option('-e, --email <email>', 'Email or username for basic auth')
1925
1922
  .option('-t, --token <token>', 'API token')
1923
+ .option('-c, --cookie <cookie>', 'Cookie for Enterprise SSO authentication (e.g., "JSESSIONID=...")')
1926
1924
  .option('--tls-ca-cert <path>', 'CA certificate for mTLS connections')
1927
1925
  .option('--tls-client-cert <path>', 'Client certificate for mTLS connections')
1928
1926
  .option('--tls-client-key <path>', 'Client private key for mTLS connections')
@@ -2039,9 +2037,7 @@ program
2039
2037
  }
2040
2038
  analytics.track('convert', true);
2041
2039
  } catch (error) {
2042
- analytics.track('convert', false);
2043
- console.error(chalk.red('Error:'), error.message);
2044
- process.exit(1);
2040
+ handleCommandError(analytics, 'convert', error);
2045
2041
  }
2046
2042
  });
2047
2043
 
@@ -2055,7 +2051,10 @@ module.exports = {
2055
2051
  uniquePathFor,
2056
2052
  exportRecursive,
2057
2053
  sanitizeTitle,
2054
+ sanitizeFilename,
2058
2055
  assertWritable,
2056
+ assertNonEmpty,
2057
+ handleCommandError,
2059
2058
  },
2060
2059
  };
2061
2060
 
package/lib/config.js CHANGED
@@ -11,9 +11,12 @@ const DEFAULT_PROFILE = 'default';
11
11
  const AUTH_CHOICES = [
12
12
  { name: 'Basic (credentials)', value: 'basic' },
13
13
  { name: 'Bearer token', value: 'bearer' },
14
- { name: 'Client certificate (mTLS)', value: 'mtls' }
14
+ { name: 'Client certificate (mTLS)', value: 'mtls' },
15
+ { name: 'Cookie (Enterprise SSO)', value: 'cookie' }
15
16
  ];
16
17
 
18
+ const AUTH_TYPES = ['basic', 'bearer', 'mtls', 'cookie'];
19
+
17
20
  const isValidProfileName = (name) => /^[a-zA-Z0-9_-]+$/.test(name);
18
21
 
19
22
  const requiredInput = (label) => (input) => {
@@ -38,7 +41,7 @@ const normalizeProtocol = (rawValue) => {
38
41
 
39
42
  const normalizeAuthType = (rawValue, hasEmail) => {
40
43
  const normalized = (rawValue || '').trim().toLowerCase();
41
- if (normalized === 'basic' || normalized === 'bearer' || normalized === 'mtls') {
44
+ if (AUTH_TYPES.includes(normalized)) {
42
45
  return normalized;
43
46
  }
44
47
  return hasEmail ? 'basic' : 'bearer';
@@ -100,6 +103,34 @@ const validateMtlsProtocol = (protocol) => {
100
103
  return null;
101
104
  };
102
105
 
106
+ // Validate a resolved auth configuration (post-normalization).
107
+ // Returns error messages only; callers format source-specific hints.
108
+ const validateAuthConfig = (auth, mtlsSourceLabel) => {
109
+ const errors = [];
110
+
111
+ if (auth.authType === 'basic' && !auth.email) {
112
+ errors.push('Basic authentication requires an email address or username.');
113
+ }
114
+
115
+ if (auth.authType === 'cookie' && !auth.cookie) {
116
+ errors.push('Cookie authentication requires a cookie value.');
117
+ }
118
+
119
+ if (auth.authType !== 'mtls' && auth.authType !== 'cookie' && !auth.token) {
120
+ errors.push('Bearer or basic authentication requires a token.');
121
+ }
122
+
123
+ if (auth.authType === 'mtls') {
124
+ errors.push(...validateMtlsConfig(auth.mtls, mtlsSourceLabel));
125
+ const protocolError = validateMtlsProtocol(auth.protocol);
126
+ if (protocolError) {
127
+ errors.push(protocolError);
128
+ }
129
+ }
130
+
131
+ return errors;
132
+ };
133
+
103
134
  /**
104
135
  * Build an inquirer question for an mTLS file path prompt.
105
136
  * @param {string} name - answer key (e.g. 'tlsClientCert')
@@ -177,6 +208,9 @@ function readConfigFile() {
177
208
  if (raw.email) {
178
209
  profile.email = raw.email;
179
210
  }
211
+ if (raw.cookie) {
212
+ profile.cookie = raw.cookie;
213
+ }
180
214
  return {
181
215
  activeProfile: DEFAULT_PROFILE,
182
216
  profiles: { [DEFAULT_PROFILE]: profile }
@@ -233,8 +267,8 @@ const validateCliOptions = (options) => {
233
267
  errors.push('--protocol must be "http" or "https"');
234
268
  }
235
269
 
236
- if (options.authType && !['basic', 'bearer', 'mtls'].includes(options.authType.toLowerCase())) {
237
- errors.push('--auth-type must be "basic", "bearer", or "mtls"');
270
+ if (options.authType && !AUTH_TYPES.includes(options.authType.toLowerCase())) {
271
+ errors.push('--auth-type must be "basic", "bearer", "mtls", or "cookie"');
238
272
  }
239
273
 
240
274
  // Check if basic auth is provided with email
@@ -253,6 +287,10 @@ const validateCliOptions = (options) => {
253
287
  }
254
288
  }
255
289
 
290
+ if (normAuthType === 'cookie' && options.cookie !== undefined && !options.cookie.trim()) {
291
+ errors.push('--cookie cannot be empty when using cookie authentication');
292
+ }
293
+
256
294
  return errors;
257
295
  };
258
296
 
@@ -273,6 +311,10 @@ const saveConfig = (configData, profileName) => {
273
311
  config.email = configData.email.trim();
274
312
  }
275
313
 
314
+ if (configData.authType === 'cookie' && configData.cookie) {
315
+ config.cookie = configData.cookie.trim();
316
+ }
317
+
276
318
  const mtls = normalizeMtlsConfig(configData.mtls);
277
319
  if (mtls) {
278
320
  config.mtls = mtls;
@@ -388,12 +430,26 @@ const promptForMissingValues = async (providedValues) => {
388
430
  message: 'API token / password:',
389
431
  when: (responses) => {
390
432
  const authType = providedValues.authType || responses.authType;
391
- return authType !== 'mtls';
433
+ return authType !== 'mtls' && authType !== 'cookie';
392
434
  },
393
435
  validate: requiredInput('API token / password')
394
436
  });
395
437
  }
396
438
 
439
+ // Cookie question (Enterprise SSO)
440
+ if (!providedValues.cookie) {
441
+ questions.push({
442
+ type: 'password',
443
+ name: 'cookie',
444
+ message: 'Cookie (format: "name=value" or "name=value; name2=value2"):',
445
+ when: (responses) => {
446
+ const authType = providedValues.authType || responses.authType;
447
+ return authType === 'cookie';
448
+ },
449
+ validate: requiredInput('Cookie')
450
+ });
451
+ }
452
+
397
453
  // mTLS certificate path questions
398
454
  const mtls = normalizeMtlsConfig(providedValues.mtls);
399
455
  const mtlsWhen = (responses) => {
@@ -429,14 +485,18 @@ async function initConfig(cliOptions = {}) {
429
485
 
430
486
  const readOnly = cliOptions.readOnly || false;
431
487
 
432
- // Extract provided values from CLI options
488
+ // Extract provided values from CLI options.
489
+ // Normalize authType up front so downstream case-insensitive checks
490
+ // (hasRequiredValues, prompt `when` predicates) work for values like
491
+ // `--auth-type COOKIE` or `--auth-type MTLS`.
433
492
  const providedValues = {
434
493
  protocol: cliOptions.protocol,
435
494
  domain: cliOptions.domain,
436
495
  apiPath: cliOptions.apiPath,
437
- authType: cliOptions.authType,
496
+ authType: cliOptions.authType ? cliOptions.authType.trim().toLowerCase() : undefined,
438
497
  email: cliOptions.email,
439
498
  token: cliOptions.token,
499
+ cookie: cliOptions.cookie,
440
500
  mtls: cliOptions.mtls || {
441
501
  caCert: cliOptions.tlsCaCert,
442
502
  clientCert: cliOptions.tlsClientCert,
@@ -508,9 +568,16 @@ async function initConfig(cliOptions = {}) {
508
568
  type: 'password',
509
569
  name: 'token',
510
570
  message: 'API token / password:',
511
- when: (responses) => responses.authType !== 'mtls',
571
+ when: (responses) => responses.authType !== 'mtls' && responses.authType !== 'cookie',
512
572
  validate: requiredInput('API token / password')
513
573
  },
574
+ {
575
+ type: 'password',
576
+ name: 'cookie',
577
+ message: 'Cookie (format: "name=value" or "name=value; name2=value2"):',
578
+ when: (responses) => responses.authType === 'cookie',
579
+ validate: requiredInput('Cookie')
580
+ },
514
581
  mtlsCertQuestion('tlsClientCert', 'Path to client certificate file (PEM):', true),
515
582
  mtlsCertQuestion('tlsClientKey', 'Path to client key file (PEM):', true),
516
583
  mtlsCertQuestion('tlsCaCert', 'Path to CA certificate file (PEM, optional):', false)
@@ -541,11 +608,15 @@ async function initConfig(cliOptions = {}) {
541
608
  }
542
609
 
543
610
  // Check if all required values are provided for non-interactive mode
544
- // Non-interactive requires: domain, token, and either authType or email (for inference)
611
+ // Non-interactive requires: domain, and one of:
612
+ // - authType === 'mtls' (certs via flags/env)
613
+ // - authType === 'cookie' + cookie
614
+ // - token + (authType or email) for basic/bearer
545
615
  const hasRequiredValues = Boolean(
546
616
  providedValues.domain &&
547
617
  (
548
618
  providedValues.authType === 'mtls'
619
+ || (providedValues.authType === 'cookie' && providedValues.cookie)
549
620
  || (
550
621
  providedValues.token &&
551
622
  (providedValues.authType || providedValues.email)
@@ -571,11 +642,16 @@ async function initConfig(cliOptions = {}) {
571
642
  process.exit(1);
572
643
  }
573
644
 
574
- if (normalizedAuthType !== 'mtls' && !providedValues.token) {
645
+ if (normalizedAuthType !== 'mtls' && normalizedAuthType !== 'cookie' && !providedValues.token) {
575
646
  console.error(chalk.red('❌ Token is required for basic or bearer authentication'));
576
647
  process.exit(1);
577
648
  }
578
649
 
650
+ if (normalizedAuthType === 'cookie' && !providedValues.cookie) {
651
+ console.error(chalk.red('❌ Cookie is required for cookie authentication'));
652
+ process.exit(1);
653
+ }
654
+
579
655
  // Verify API path format if provided
580
656
  if (providedValues.apiPath) {
581
657
  normalizeApiPath(providedValues.apiPath, normalizedDomain);
@@ -588,6 +664,7 @@ async function initConfig(cliOptions = {}) {
588
664
  token: providedValues.token,
589
665
  authType: normalizedAuthType,
590
666
  email: providedValues.email,
667
+ cookie: providedValues.cookie,
591
668
  mtls: providedValues.mtls,
592
669
  readOnly
593
670
  };
@@ -633,18 +710,30 @@ function getConfig(profileName) {
633
710
  const envDomain = process.env.CONFLUENCE_DOMAIN || process.env.CONFLUENCE_HOST;
634
711
  const envToken = process.env.CONFLUENCE_API_TOKEN || process.env.CONFLUENCE_PASSWORD;
635
712
  const envEmail = process.env.CONFLUENCE_EMAIL || process.env.CONFLUENCE_USERNAME;
636
- const envAuthType = process.env.CONFLUENCE_AUTH_TYPE;
713
+ // Normalize up front so env gating and inferredAuthType are case-insensitive
714
+ // for values like CONFLUENCE_AUTH_TYPE=COOKIE / MTLS.
715
+ const envAuthType = process.env.CONFLUENCE_AUTH_TYPE
716
+ ? process.env.CONFLUENCE_AUTH_TYPE.trim().toLowerCase()
717
+ : undefined;
637
718
  const envApiPath = process.env.CONFLUENCE_API_PATH;
638
719
  const envProtocol = process.env.CONFLUENCE_PROTOCOL;
639
720
  const envReadOnly = process.env.CONFLUENCE_READ_ONLY;
721
+ const envForceCloud = process.env.CONFLUENCE_FORCE_CLOUD;
722
+ const envCookie = process.env.CONFLUENCE_COOKIE;
640
723
  const envMtls = normalizeMtlsConfig({
641
724
  caCert: process.env.CONFLUENCE_TLS_CA_CERT,
642
725
  clientCert: process.env.CONFLUENCE_TLS_CLIENT_CERT,
643
726
  clientKey: process.env.CONFLUENCE_TLS_CLIENT_KEY,
644
727
  });
645
728
 
646
- if (envDomain && (envToken || envAuthType === 'mtls' || envMtls)) {
647
- const inferredAuthType = envAuthType || (envMtls && !envToken ? 'mtls' : undefined);
729
+ const hasEnvAuth = envToken
730
+ || envAuthType === 'mtls' || envMtls
731
+ || envAuthType === 'cookie' || envCookie;
732
+
733
+ if (envDomain && hasEnvAuth) {
734
+ const inferredAuthType = envAuthType
735
+ || (envMtls && !envToken ? 'mtls' : undefined)
736
+ || (envCookie && !envToken ? 'cookie' : undefined);
648
737
  const authType = normalizeAuthType(inferredAuthType, Boolean(envEmail));
649
738
  let apiPath;
650
739
 
@@ -655,29 +744,22 @@ function getConfig(profileName) {
655
744
  process.exit(1);
656
745
  }
657
746
 
658
- if (authType === 'basic' && !envEmail) {
659
- console.error(chalk.red('❌ Basic authentication requires CONFLUENCE_EMAIL or CONFLUENCE_USERNAME.'));
660
- console.log(chalk.yellow('Set CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME for on-premise) or switch to bearer auth by setting CONFLUENCE_AUTH_TYPE=bearer.'));
661
- process.exit(1);
662
- }
663
-
664
- if (authType !== 'mtls' && !envToken) {
665
- console.error(chalk.red(' Bearer/basic authentication requires CONFLUENCE_API_TOKEN or CONFLUENCE_PASSWORD.'));
666
- process.exit(1);
667
- }
668
-
669
- if (authType === 'mtls') {
670
- const mtlsErrors = validateMtlsConfig(envMtls, 'CONFLUENCE_AUTH_TYPE=mtls');
671
- if (mtlsErrors.length > 0) {
672
- console.error(chalk.red(`❌ ${mtlsErrors.join(' ')}`));
747
+ const authErrors = validateAuthConfig(
748
+ { authType, token: envToken, email: envEmail, cookie: envCookie, mtls: envMtls, protocol: envProtocol },
749
+ 'CONFLUENCE_AUTH_TYPE=mtls'
750
+ );
751
+ if (authErrors.length > 0) {
752
+ console.error(chalk.red(`❌ ${authErrors.join(' ')}`));
753
+ if (authType === 'basic' && !envEmail) {
754
+ console.log(chalk.yellow('Set CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME for on-premise) or switch to bearer auth by setting CONFLUENCE_AUTH_TYPE=bearer.'));
755
+ }
756
+ if (authType === 'mtls' && !envMtls) {
673
757
  console.log(chalk.yellow('Set CONFLUENCE_TLS_CLIENT_CERT and CONFLUENCE_TLS_CLIENT_KEY. Optionally set CONFLUENCE_TLS_CA_CERT.'));
674
- process.exit(1);
675
758
  }
676
- if (normalizeProtocol(envProtocol) === 'http') {
677
- const protocolError = validateMtlsProtocol(envProtocol);
678
- console.error(chalk.red(`❌ ${protocolError}`));
679
- process.exit(1);
759
+ if (authType === 'cookie' && !envCookie) {
760
+ console.log(chalk.yellow('Set CONFLUENCE_COOKIE with your session cookie (e.g., "JSESSIONID=...").'));
680
761
  }
762
+ process.exit(1);
681
763
  }
682
764
 
683
765
  return {
@@ -686,9 +768,11 @@ function getConfig(profileName) {
686
768
  apiPath,
687
769
  token: envToken ? envToken.trim() : undefined,
688
770
  email: envEmail ? envEmail.trim() : undefined,
771
+ cookie: envCookie ? envCookie.trim() : undefined,
689
772
  authType,
690
773
  mtls: envMtls,
691
- readOnly: envReadOnly === 'true'
774
+ readOnly: envReadOnly === 'true',
775
+ forceCloud: envForceCloud === 'true'
692
776
  };
693
777
  }
694
778
 
@@ -723,37 +807,27 @@ function getConfig(profileName) {
723
807
  const trimmedDomain = (storedConfig.domain || '').trim();
724
808
  const trimmedToken = trimOptional(storedConfig.token);
725
809
  const trimmedEmail = storedConfig.email ? storedConfig.email.trim() : undefined;
810
+ const trimmedCookie = trimOptional(storedConfig.cookie);
726
811
  const authType = normalizeAuthType(storedConfig.authType, Boolean(trimmedEmail));
727
812
  const mtls = normalizeMtlsConfig(storedConfig.mtls);
728
813
  let apiPath;
729
814
 
730
- if (!trimmedDomain || (authType !== 'mtls' && !trimmedToken)) {
815
+ if (!trimmedDomain) {
731
816
  console.error(chalk.red('❌ Configuration file is missing required values.'));
732
817
  console.log(chalk.yellow('Run "confluence init" to refresh your settings.'));
733
818
  process.exit(1);
734
819
  }
735
820
 
736
- if (authType === 'basic' && !trimmedEmail) {
737
- console.error(chalk.red('❌ Basic authentication requires an email address or username.'));
738
- console.log(chalk.yellow('Please rerun "confluence init" to add your Confluence email or username.'));
821
+ const authErrors = validateAuthConfig(
822
+ { authType, token: trimmedToken, email: trimmedEmail, cookie: trimmedCookie, mtls, protocol: storedConfig.protocol },
823
+ 'mTLS authentication'
824
+ );
825
+ if (authErrors.length > 0) {
826
+ console.error(chalk.red(`❌ ${authErrors.join(' ')}`));
827
+ console.log(chalk.yellow('Please rerun "confluence init" to refresh your settings.'));
739
828
  process.exit(1);
740
829
  }
741
830
 
742
- if (authType === 'mtls') {
743
- const mtlsErrors = validateMtlsConfig(mtls, 'mTLS authentication');
744
- if (mtlsErrors.length > 0) {
745
- console.error(chalk.red(`❌ ${mtlsErrors.join(' ')}`));
746
- console.log(chalk.yellow('Please rerun "confluence init" to add your mTLS certificate paths.'));
747
- process.exit(1);
748
- }
749
- if (normalizeProtocol(storedConfig.protocol) === 'http') {
750
- const protocolError = validateMtlsProtocol(storedConfig.protocol);
751
- console.error(chalk.red(`❌ ${protocolError}`));
752
- console.log(chalk.yellow('Please rerun "confluence init" to update your protocol to HTTPS.'));
753
- process.exit(1);
754
- }
755
- }
756
-
757
831
  try {
758
832
  apiPath = normalizeApiPath(storedConfig.apiPath, trimmedDomain);
759
833
  } catch (error) {
@@ -766,15 +840,21 @@ function getConfig(profileName) {
766
840
  ? envReadOnly === 'true'
767
841
  : Boolean(storedConfig.readOnly);
768
842
 
843
+ const forceCloud = envForceCloud !== undefined
844
+ ? envForceCloud === 'true'
845
+ : Boolean(storedConfig.forceCloud);
846
+
769
847
  return {
770
848
  domain: trimmedDomain,
771
849
  protocol: normalizeProtocol(storedConfig.protocol),
772
850
  apiPath,
773
851
  token: trimmedToken,
774
852
  email: trimmedEmail,
853
+ cookie: trimmedCookie,
775
854
  authType,
776
855
  mtls,
777
- readOnly
856
+ readOnly,
857
+ forceCloud
778
858
  };
779
859
  } catch (error) {
780
860
  console.error(chalk.red('❌ Error reading configuration file:'), error.message);
@@ -34,7 +34,9 @@ class ConfluenceClient {
34
34
  this.protocol = (rawProtocol === 'http' || rawProtocol === 'https') ? rawProtocol : 'https';
35
35
  this.token = config.token;
36
36
  this.email = config.email;
37
+ this.cookie = config.cookie;
37
38
  this.authType = (config.authType || (this.email ? 'basic' : 'bearer')).toLowerCase();
39
+ this.forceCloud = !!config.forceCloud;
38
40
  this.mtls = config.mtls;
39
41
  this.apiPath = this.sanitizeApiPath(config.apiPath);
40
42
  this.webUrlPrefix = this.apiPath.startsWith('/wiki/') ? '/wiki' : '';
@@ -43,12 +45,9 @@ class ConfluenceClient {
43
45
  this.setupConfluenceMarkdownExtensions();
44
46
 
45
47
  const headers = {
46
- 'Content-Type': 'application/json'
48
+ 'Content-Type': 'application/json',
49
+ ...this.buildAuthHeaders()
47
50
  };
48
- const authHeader = this.buildAuthHeader();
49
- if (authHeader) {
50
- headers.Authorization = authHeader;
51
- }
52
51
 
53
52
  const clientOptions = {
54
53
  baseURL: this.baseURL,
@@ -87,6 +86,11 @@ class ConfluenceClient {
87
86
  hints.push(
88
87
  'Please verify your client certificate, client key, and CA certificate are correct and trusted by the server.'
89
88
  );
89
+ } else if (this.authType === 'cookie') {
90
+ hints.push(
91
+ 'Please verify your cookie is valid and not expired.',
92
+ 'You may need to re-authenticate through your Enterprise SSO to get a fresh cookie.'
93
+ );
90
94
  } else {
91
95
  hints.push(
92
96
  'Please verify your personal access token is valid and not expired.'
@@ -100,7 +104,7 @@ class ConfluenceClient {
100
104
  }
101
105
 
102
106
  isCloud() {
103
- return this.isScopedToken() || (this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net'));
107
+ return this.isScopedToken() || (this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net')) || this.forceCloud;
104
108
  }
105
109
 
106
110
  isScopedToken() {
@@ -131,7 +135,7 @@ class ConfluenceClient {
131
135
  }
132
136
 
133
137
  buildAuthHeader() {
134
- if (this.authType === 'mtls') {
138
+ if (this.authType === 'mtls' || this.authType === 'cookie') {
135
139
  return null;
136
140
  }
137
141
 
@@ -142,6 +146,18 @@ class ConfluenceClient {
142
146
  return this.authType === 'basic' ? this.buildBasicAuthHeader() : `Bearer ${this.token}`;
143
147
  }
144
148
 
149
+ buildAuthHeaders() {
150
+ const headers = {};
151
+ const authHeader = this.buildAuthHeader();
152
+ if (authHeader) {
153
+ headers.Authorization = authHeader;
154
+ }
155
+ if (this.authType === 'cookie' && this.cookie) {
156
+ headers.Cookie = this.cookie;
157
+ }
158
+ return headers;
159
+ }
160
+
145
161
  buildHttpsAgent() {
146
162
  if (this.protocol !== 'https' || !this.mtls) {
147
163
  return null;
@@ -380,11 +396,26 @@ class ConfluenceClient {
380
396
  };
381
397
  }
382
398
 
399
+ /**
400
+ * Escape a string for safe use inside a CQL double-quoted literal.
401
+ * Only escapes characters that can break out of the literal: backslash and
402
+ * double quote. Wildcards (*, ?) and fuzzy (~) are left as-is so existing
403
+ * search semantics are preserved.
404
+ */
405
+ escapeCql(str) {
406
+ if (typeof str !== 'string') {
407
+ return '';
408
+ }
409
+ return str
410
+ .replace(/\\/g, '\\\\')
411
+ .replace(/"/g, '\\"');
412
+ }
413
+
383
414
  /**
384
415
  * Search for pages
385
416
  */
386
417
  async search(query, limit = 10, rawCql = false) {
387
- const cql = rawCql ? query : `text ~ "${String(query).replace(/"/g, '\\"')}"`;
418
+ const cql = rawCql ? query : `text ~ "${this.escapeCql(query)}"`;
388
419
  const response = await this.client.get('/search', {
389
420
  params: {
390
421
  cql,
@@ -477,12 +508,12 @@ class ConfluenceClient {
477
508
  // Replace userkey references with display names in HTML
478
509
  let resolvedHtml = html;
479
510
  userMap.forEach((displayName, userKey) => {
480
- // Replace <ac:link><ri:user ri:userkey="xxx" /></ac:link> with @displayName
511
+ const escapedKey = userKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
481
512
  const userLinkRegex = new RegExp(
482
- `<ac:link>\\s*<ri:user\\s+ri:userkey="${userKey}"\\s*/>\\s*</ac:link>`,
513
+ `<ac:link>\\s*<ri:user\\s+ri:userkey="${escapedKey}"\\s*/>\\s*</ac:link>`,
483
514
  'g'
484
515
  );
485
- resolvedHtml = resolvedHtml.replace(userLinkRegex, `@${displayName}`);
516
+ resolvedHtml = resolvedHtml.replace(userLinkRegex, () => `@${displayName}`);
486
517
  });
487
518
 
488
519
  return { html: resolvedHtml, userMap };
@@ -965,12 +996,15 @@ class ConfluenceClient {
965
996
  }
966
997
 
967
998
  // Download directly using axios with the same auth headers
968
- const downloadResponse = await axios.get(downloadUrl, {
999
+ const downloadRequestConfig = {
969
1000
  responseType: options.responseType || 'stream',
970
- headers: {
971
- 'Authorization': this.authType === 'basic' ? this.buildBasicAuthHeader() : `Bearer ${this.token}`
972
- }
973
- });
1001
+ headers: this.buildAuthHeaders()
1002
+ };
1003
+ const httpsAgent = this.buildHttpsAgent();
1004
+ if (httpsAgent) {
1005
+ downloadRequestConfig.httpsAgent = httpsAgent;
1006
+ }
1007
+ const downloadResponse = await axios.get(downloadUrl, downloadRequestConfig);
974
1008
  return downloadResponse.data;
975
1009
  }
976
1010
 
@@ -1871,9 +1905,9 @@ class ConfluenceClient {
1871
1905
  * Search for a page by title and space
1872
1906
  */
1873
1907
  async findPageByTitle(title, spaceKey = null) {
1874
- let cql = `title = "${title}"`;
1908
+ let cql = `title = "${this.escapeCql(title)}"`;
1875
1909
  if (spaceKey) {
1876
- cql += ` AND space = "${spaceKey}"`;
1910
+ cql += ` AND space = "${this.escapeCql(spaceKey)}"`;
1877
1911
  }
1878
1912
 
1879
1913
  const response = await this.client.get('/search', {
@@ -2177,7 +2211,10 @@ class ConfluenceClient {
2177
2211
  return pathOrUrl;
2178
2212
  }
2179
2213
 
2180
- return this.buildUrl(pathOrUrl);
2214
+ const pathWithPrefix = this.webUrlPrefix && !pathOrUrl.startsWith(this.webUrlPrefix)
2215
+ ? `${this.webUrlPrefix}${pathOrUrl}`
2216
+ : pathOrUrl;
2217
+ return this.buildUrl(pathWithPrefix);
2181
2218
  }
2182
2219
 
2183
2220
  parseNextStart(nextLink) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.30.1",
3
+ "version": "1.31.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "confluence-cli",
9
- "version": "1.30.1",
9
+ "version": "1.31.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "axios": "^1.15.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.30.1",
3
+ "version": "1.31.0",
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