confluence-cli 1.30.2 → 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"
@@ -271,6 +295,8 @@ For **read-only** usage, select at minimum: `read:confluence-content.all`, `read
271
295
 
272
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.
273
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
+
274
300
  ## Usage
275
301
 
276
302
  ### Read a Page
package/bin/confluence.js CHANGED
@@ -46,9 +46,10 @@ program
46
46
  .option('-d, --domain <domain>', 'Confluence domain')
47
47
  .option('--protocol <protocol>', 'Protocol (http or https)')
48
48
  .option('-p, --api-path <path>', 'REST API path')
49
- .option('-a, --auth-type <type>', 'Authentication type (basic, bearer, or mtls)')
49
+ .option('-a, --auth-type <type>', 'Authentication type (basic, bearer, mtls, or cookie)')
50
50
  .option('-e, --email <email>', 'Email or username for basic auth')
51
51
  .option('-t, --token <token>', 'API token')
52
+ .option('-c, --cookie <cookie>', 'Cookie for Enterprise SSO authentication (e.g., "JSESSIONID=...")')
52
53
  .option('--tls-ca-cert <path>', 'CA certificate for mTLS connections')
53
54
  .option('--tls-client-cert <path>', 'Client certificate for mTLS connections')
54
55
  .option('--tls-client-key <path>', 'Client private key for mTLS connections')
@@ -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})`;
@@ -1334,9 +1336,24 @@ function isExportDirectory(fs, path, dir) {
1334
1336
  return fs.existsSync(path.join(dir, EXPORT_MARKER));
1335
1337
  }
1336
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
+
1337
1353
  function uniquePathFor(fs, path, dir, filename) {
1338
- const parsed = path.parse(filename);
1339
- let attempt = path.join(dir, filename);
1354
+ const safeFilename = sanitizeFilename(filename);
1355
+ const parsed = path.parse(safeFilename);
1356
+ let attempt = path.join(dir, safeFilename);
1340
1357
  let counter = 1;
1341
1358
  while (fs.existsSync(attempt)) {
1342
1359
  const suffix = ` (${counter})`;
@@ -1536,7 +1553,11 @@ function sanitizeTitle(value) {
1536
1553
  if (!value || typeof value !== 'string') {
1537
1554
  return fallback;
1538
1555
  }
1539
- 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();
1540
1561
  return cleaned || fallback;
1541
1562
  }
1542
1563
 
@@ -1896,9 +1917,10 @@ profileCmd
1896
1917
  .option('-d, --domain <domain>', 'Confluence domain')
1897
1918
  .option('--protocol <protocol>', 'Protocol (http or https)')
1898
1919
  .option('-p, --api-path <path>', 'REST API path')
1899
- .option('-a, --auth-type <type>', 'Authentication type (basic, bearer, or mtls)')
1920
+ .option('-a, --auth-type <type>', 'Authentication type (basic, bearer, mtls, or cookie)')
1900
1921
  .option('-e, --email <email>', 'Email or username for basic auth')
1901
1922
  .option('-t, --token <token>', 'API token')
1923
+ .option('-c, --cookie <cookie>', 'Cookie for Enterprise SSO authentication (e.g., "JSESSIONID=...")')
1902
1924
  .option('--tls-ca-cert <path>', 'CA certificate for mTLS connections')
1903
1925
  .option('--tls-client-cert <path>', 'Client certificate for mTLS connections')
1904
1926
  .option('--tls-client-key <path>', 'Client private key for mTLS connections')
@@ -2029,6 +2051,7 @@ module.exports = {
2029
2051
  uniquePathFor,
2030
2052
  exportRecursive,
2031
2053
  sanitizeTitle,
2054
+ sanitizeFilename,
2032
2055
  assertWritable,
2033
2056
  assertNonEmpty,
2034
2057
  handleCommandError,
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';
@@ -109,7 +112,11 @@ const validateAuthConfig = (auth, mtlsSourceLabel) => {
109
112
  errors.push('Basic authentication requires an email address or username.');
110
113
  }
111
114
 
112
- if (auth.authType !== 'mtls' && !auth.token) {
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) {
113
120
  errors.push('Bearer or basic authentication requires a token.');
114
121
  }
115
122
 
@@ -201,6 +208,9 @@ function readConfigFile() {
201
208
  if (raw.email) {
202
209
  profile.email = raw.email;
203
210
  }
211
+ if (raw.cookie) {
212
+ profile.cookie = raw.cookie;
213
+ }
204
214
  return {
205
215
  activeProfile: DEFAULT_PROFILE,
206
216
  profiles: { [DEFAULT_PROFILE]: profile }
@@ -257,8 +267,8 @@ const validateCliOptions = (options) => {
257
267
  errors.push('--protocol must be "http" or "https"');
258
268
  }
259
269
 
260
- if (options.authType && !['basic', 'bearer', 'mtls'].includes(options.authType.toLowerCase())) {
261
- 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"');
262
272
  }
263
273
 
264
274
  // Check if basic auth is provided with email
@@ -277,6 +287,10 @@ const validateCliOptions = (options) => {
277
287
  }
278
288
  }
279
289
 
290
+ if (normAuthType === 'cookie' && options.cookie !== undefined && !options.cookie.trim()) {
291
+ errors.push('--cookie cannot be empty when using cookie authentication');
292
+ }
293
+
280
294
  return errors;
281
295
  };
282
296
 
@@ -297,6 +311,10 @@ const saveConfig = (configData, profileName) => {
297
311
  config.email = configData.email.trim();
298
312
  }
299
313
 
314
+ if (configData.authType === 'cookie' && configData.cookie) {
315
+ config.cookie = configData.cookie.trim();
316
+ }
317
+
300
318
  const mtls = normalizeMtlsConfig(configData.mtls);
301
319
  if (mtls) {
302
320
  config.mtls = mtls;
@@ -412,12 +430,26 @@ const promptForMissingValues = async (providedValues) => {
412
430
  message: 'API token / password:',
413
431
  when: (responses) => {
414
432
  const authType = providedValues.authType || responses.authType;
415
- return authType !== 'mtls';
433
+ return authType !== 'mtls' && authType !== 'cookie';
416
434
  },
417
435
  validate: requiredInput('API token / password')
418
436
  });
419
437
  }
420
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
+
421
453
  // mTLS certificate path questions
422
454
  const mtls = normalizeMtlsConfig(providedValues.mtls);
423
455
  const mtlsWhen = (responses) => {
@@ -453,14 +485,18 @@ async function initConfig(cliOptions = {}) {
453
485
 
454
486
  const readOnly = cliOptions.readOnly || false;
455
487
 
456
- // 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`.
457
492
  const providedValues = {
458
493
  protocol: cliOptions.protocol,
459
494
  domain: cliOptions.domain,
460
495
  apiPath: cliOptions.apiPath,
461
- authType: cliOptions.authType,
496
+ authType: cliOptions.authType ? cliOptions.authType.trim().toLowerCase() : undefined,
462
497
  email: cliOptions.email,
463
498
  token: cliOptions.token,
499
+ cookie: cliOptions.cookie,
464
500
  mtls: cliOptions.mtls || {
465
501
  caCert: cliOptions.tlsCaCert,
466
502
  clientCert: cliOptions.tlsClientCert,
@@ -532,9 +568,16 @@ async function initConfig(cliOptions = {}) {
532
568
  type: 'password',
533
569
  name: 'token',
534
570
  message: 'API token / password:',
535
- when: (responses) => responses.authType !== 'mtls',
571
+ when: (responses) => responses.authType !== 'mtls' && responses.authType !== 'cookie',
536
572
  validate: requiredInput('API token / password')
537
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
+ },
538
581
  mtlsCertQuestion('tlsClientCert', 'Path to client certificate file (PEM):', true),
539
582
  mtlsCertQuestion('tlsClientKey', 'Path to client key file (PEM):', true),
540
583
  mtlsCertQuestion('tlsCaCert', 'Path to CA certificate file (PEM, optional):', false)
@@ -565,11 +608,15 @@ async function initConfig(cliOptions = {}) {
565
608
  }
566
609
 
567
610
  // Check if all required values are provided for non-interactive mode
568
- // 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
569
615
  const hasRequiredValues = Boolean(
570
616
  providedValues.domain &&
571
617
  (
572
618
  providedValues.authType === 'mtls'
619
+ || (providedValues.authType === 'cookie' && providedValues.cookie)
573
620
  || (
574
621
  providedValues.token &&
575
622
  (providedValues.authType || providedValues.email)
@@ -595,11 +642,16 @@ async function initConfig(cliOptions = {}) {
595
642
  process.exit(1);
596
643
  }
597
644
 
598
- if (normalizedAuthType !== 'mtls' && !providedValues.token) {
645
+ if (normalizedAuthType !== 'mtls' && normalizedAuthType !== 'cookie' && !providedValues.token) {
599
646
  console.error(chalk.red('❌ Token is required for basic or bearer authentication'));
600
647
  process.exit(1);
601
648
  }
602
649
 
650
+ if (normalizedAuthType === 'cookie' && !providedValues.cookie) {
651
+ console.error(chalk.red('❌ Cookie is required for cookie authentication'));
652
+ process.exit(1);
653
+ }
654
+
603
655
  // Verify API path format if provided
604
656
  if (providedValues.apiPath) {
605
657
  normalizeApiPath(providedValues.apiPath, normalizedDomain);
@@ -612,6 +664,7 @@ async function initConfig(cliOptions = {}) {
612
664
  token: providedValues.token,
613
665
  authType: normalizedAuthType,
614
666
  email: providedValues.email,
667
+ cookie: providedValues.cookie,
615
668
  mtls: providedValues.mtls,
616
669
  readOnly
617
670
  };
@@ -657,19 +710,30 @@ function getConfig(profileName) {
657
710
  const envDomain = process.env.CONFLUENCE_DOMAIN || process.env.CONFLUENCE_HOST;
658
711
  const envToken = process.env.CONFLUENCE_API_TOKEN || process.env.CONFLUENCE_PASSWORD;
659
712
  const envEmail = process.env.CONFLUENCE_EMAIL || process.env.CONFLUENCE_USERNAME;
660
- 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;
661
718
  const envApiPath = process.env.CONFLUENCE_API_PATH;
662
719
  const envProtocol = process.env.CONFLUENCE_PROTOCOL;
663
720
  const envReadOnly = process.env.CONFLUENCE_READ_ONLY;
664
721
  const envForceCloud = process.env.CONFLUENCE_FORCE_CLOUD;
722
+ const envCookie = process.env.CONFLUENCE_COOKIE;
665
723
  const envMtls = normalizeMtlsConfig({
666
724
  caCert: process.env.CONFLUENCE_TLS_CA_CERT,
667
725
  clientCert: process.env.CONFLUENCE_TLS_CLIENT_CERT,
668
726
  clientKey: process.env.CONFLUENCE_TLS_CLIENT_KEY,
669
727
  });
670
728
 
671
- if (envDomain && (envToken || envAuthType === 'mtls' || envMtls)) {
672
- 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);
673
737
  const authType = normalizeAuthType(inferredAuthType, Boolean(envEmail));
674
738
  let apiPath;
675
739
 
@@ -681,7 +745,7 @@ function getConfig(profileName) {
681
745
  }
682
746
 
683
747
  const authErrors = validateAuthConfig(
684
- { authType, token: envToken, email: envEmail, mtls: envMtls, protocol: envProtocol },
748
+ { authType, token: envToken, email: envEmail, cookie: envCookie, mtls: envMtls, protocol: envProtocol },
685
749
  'CONFLUENCE_AUTH_TYPE=mtls'
686
750
  );
687
751
  if (authErrors.length > 0) {
@@ -692,6 +756,9 @@ function getConfig(profileName) {
692
756
  if (authType === 'mtls' && !envMtls) {
693
757
  console.log(chalk.yellow('Set CONFLUENCE_TLS_CLIENT_CERT and CONFLUENCE_TLS_CLIENT_KEY. Optionally set CONFLUENCE_TLS_CA_CERT.'));
694
758
  }
759
+ if (authType === 'cookie' && !envCookie) {
760
+ console.log(chalk.yellow('Set CONFLUENCE_COOKIE with your session cookie (e.g., "JSESSIONID=...").'));
761
+ }
695
762
  process.exit(1);
696
763
  }
697
764
 
@@ -701,6 +768,7 @@ function getConfig(profileName) {
701
768
  apiPath,
702
769
  token: envToken ? envToken.trim() : undefined,
703
770
  email: envEmail ? envEmail.trim() : undefined,
771
+ cookie: envCookie ? envCookie.trim() : undefined,
704
772
  authType,
705
773
  mtls: envMtls,
706
774
  readOnly: envReadOnly === 'true',
@@ -739,6 +807,7 @@ function getConfig(profileName) {
739
807
  const trimmedDomain = (storedConfig.domain || '').trim();
740
808
  const trimmedToken = trimOptional(storedConfig.token);
741
809
  const trimmedEmail = storedConfig.email ? storedConfig.email.trim() : undefined;
810
+ const trimmedCookie = trimOptional(storedConfig.cookie);
742
811
  const authType = normalizeAuthType(storedConfig.authType, Boolean(trimmedEmail));
743
812
  const mtls = normalizeMtlsConfig(storedConfig.mtls);
744
813
  let apiPath;
@@ -750,7 +819,7 @@ function getConfig(profileName) {
750
819
  }
751
820
 
752
821
  const authErrors = validateAuthConfig(
753
- { authType, token: trimmedToken, email: trimmedEmail, mtls, protocol: storedConfig.protocol },
822
+ { authType, token: trimmedToken, email: trimmedEmail, cookie: trimmedCookie, mtls, protocol: storedConfig.protocol },
754
823
  'mTLS authentication'
755
824
  );
756
825
  if (authErrors.length > 0) {
@@ -781,6 +850,7 @@ function getConfig(profileName) {
781
850
  apiPath,
782
851
  token: trimmedToken,
783
852
  email: trimmedEmail,
853
+ cookie: trimmedCookie,
784
854
  authType,
785
855
  mtls,
786
856
  readOnly,
@@ -34,6 +34,7 @@ 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();
38
39
  this.forceCloud = !!config.forceCloud;
39
40
  this.mtls = config.mtls;
@@ -44,12 +45,9 @@ class ConfluenceClient {
44
45
  this.setupConfluenceMarkdownExtensions();
45
46
 
46
47
  const headers = {
47
- 'Content-Type': 'application/json'
48
+ 'Content-Type': 'application/json',
49
+ ...this.buildAuthHeaders()
48
50
  };
49
- const authHeader = this.buildAuthHeader();
50
- if (authHeader) {
51
- headers.Authorization = authHeader;
52
- }
53
51
 
54
52
  const clientOptions = {
55
53
  baseURL: this.baseURL,
@@ -88,6 +86,11 @@ class ConfluenceClient {
88
86
  hints.push(
89
87
  'Please verify your client certificate, client key, and CA certificate are correct and trusted by the server.'
90
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
+ );
91
94
  } else {
92
95
  hints.push(
93
96
  'Please verify your personal access token is valid and not expired.'
@@ -132,7 +135,7 @@ class ConfluenceClient {
132
135
  }
133
136
 
134
137
  buildAuthHeader() {
135
- if (this.authType === 'mtls') {
138
+ if (this.authType === 'mtls' || this.authType === 'cookie') {
136
139
  return null;
137
140
  }
138
141
 
@@ -143,6 +146,18 @@ class ConfluenceClient {
143
146
  return this.authType === 'basic' ? this.buildBasicAuthHeader() : `Bearer ${this.token}`;
144
147
  }
145
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
+
146
161
  buildHttpsAgent() {
147
162
  if (this.protocol !== 'https' || !this.mtls) {
148
163
  return null;
@@ -381,11 +396,26 @@ class ConfluenceClient {
381
396
  };
382
397
  }
383
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
+
384
414
  /**
385
415
  * Search for pages
386
416
  */
387
417
  async search(query, limit = 10, rawCql = false) {
388
- const cql = rawCql ? query : `text ~ "${String(query).replace(/"/g, '\\"')}"`;
418
+ const cql = rawCql ? query : `text ~ "${this.escapeCql(query)}"`;
389
419
  const response = await this.client.get('/search', {
390
420
  params: {
391
421
  cql,
@@ -966,12 +996,15 @@ class ConfluenceClient {
966
996
  }
967
997
 
968
998
  // Download directly using axios with the same auth headers
969
- const downloadResponse = await axios.get(downloadUrl, {
999
+ const downloadRequestConfig = {
970
1000
  responseType: options.responseType || 'stream',
971
- headers: {
972
- 'Authorization': this.authType === 'basic' ? this.buildBasicAuthHeader() : `Bearer ${this.token}`
973
- }
974
- });
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);
975
1008
  return downloadResponse.data;
976
1009
  }
977
1010
 
@@ -1872,9 +1905,9 @@ class ConfluenceClient {
1872
1905
  * Search for a page by title and space
1873
1906
  */
1874
1907
  async findPageByTitle(title, spaceKey = null) {
1875
- let cql = `title = "${title}"`;
1908
+ let cql = `title = "${this.escapeCql(title)}"`;
1876
1909
  if (spaceKey) {
1877
- cql += ` AND space = "${spaceKey}"`;
1910
+ cql += ` AND space = "${this.escapeCql(spaceKey)}"`;
1878
1911
  }
1879
1912
 
1880
1913
  const response = await this.client.get('/search', {
@@ -2178,7 +2211,10 @@ class ConfluenceClient {
2178
2211
  return pathOrUrl;
2179
2212
  }
2180
2213
 
2181
- return this.buildUrl(pathOrUrl);
2214
+ const pathWithPrefix = this.webUrlPrefix && !pathOrUrl.startsWith(this.webUrlPrefix)
2215
+ ? `${this.webUrlPrefix}${pathOrUrl}`
2216
+ : pathOrUrl;
2217
+ return this.buildUrl(pathWithPrefix);
2182
2218
  }
2183
2219
 
2184
2220
  parseNextStart(nextLink) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.30.2",
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.2",
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.2",
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": {