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 +27 -1
- package/bin/confluence.js +30 -7
- package/lib/config.js +86 -16
- package/lib/confluence-client.js +51 -15
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
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 `
|
|
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
|
|
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
|
|
555
|
-
|
|
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
|
|
1339
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 && !
|
|
261
|
-
errors.push('--auth-type must be "basic", "bearer", or "
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
672
|
-
|
|
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,
|
package/lib/confluence-client.js
CHANGED
|
@@ -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 ~ "${
|
|
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
|
|
999
|
+
const downloadRequestConfig = {
|
|
970
1000
|
responseType: options.responseType || 'stream',
|
|
971
|
-
headers:
|
|
972
|
-
|
|
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
|
-
|
|
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) {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "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.
|
|
9
|
+
"version": "1.31.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"axios": "^1.15.0",
|