confluence-cli 1.30.2 → 1.31.1
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 +92 -26
- 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
|
@@ -27,6 +27,28 @@ const NAMED_ENTITIES = {
|
|
|
27
27
|
Eth: 'Ð', Thorn: 'Þ'
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
function createSemaphore(limit) {
|
|
31
|
+
let active = 0;
|
|
32
|
+
const waiters = [];
|
|
33
|
+
return {
|
|
34
|
+
async acquire() {
|
|
35
|
+
if (active < limit) {
|
|
36
|
+
active++;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await new Promise(resolve => waiters.push(resolve));
|
|
40
|
+
},
|
|
41
|
+
release() {
|
|
42
|
+
if (waiters.length > 0) {
|
|
43
|
+
const next = waiters.shift();
|
|
44
|
+
next();
|
|
45
|
+
} else {
|
|
46
|
+
active--;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
class ConfluenceClient {
|
|
31
53
|
constructor(config) {
|
|
32
54
|
this.domain = config.domain;
|
|
@@ -34,6 +56,7 @@ class ConfluenceClient {
|
|
|
34
56
|
this.protocol = (rawProtocol === 'http' || rawProtocol === 'https') ? rawProtocol : 'https';
|
|
35
57
|
this.token = config.token;
|
|
36
58
|
this.email = config.email;
|
|
59
|
+
this.cookie = config.cookie;
|
|
37
60
|
this.authType = (config.authType || (this.email ? 'basic' : 'bearer')).toLowerCase();
|
|
38
61
|
this.forceCloud = !!config.forceCloud;
|
|
39
62
|
this.mtls = config.mtls;
|
|
@@ -44,12 +67,9 @@ class ConfluenceClient {
|
|
|
44
67
|
this.setupConfluenceMarkdownExtensions();
|
|
45
68
|
|
|
46
69
|
const headers = {
|
|
47
|
-
'Content-Type': 'application/json'
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
...this.buildAuthHeaders()
|
|
48
72
|
};
|
|
49
|
-
const authHeader = this.buildAuthHeader();
|
|
50
|
-
if (authHeader) {
|
|
51
|
-
headers.Authorization = authHeader;
|
|
52
|
-
}
|
|
53
73
|
|
|
54
74
|
const clientOptions = {
|
|
55
75
|
baseURL: this.baseURL,
|
|
@@ -88,6 +108,11 @@ class ConfluenceClient {
|
|
|
88
108
|
hints.push(
|
|
89
109
|
'Please verify your client certificate, client key, and CA certificate are correct and trusted by the server.'
|
|
90
110
|
);
|
|
111
|
+
} else if (this.authType === 'cookie') {
|
|
112
|
+
hints.push(
|
|
113
|
+
'Please verify your cookie is valid and not expired.',
|
|
114
|
+
'You may need to re-authenticate through your Enterprise SSO to get a fresh cookie.'
|
|
115
|
+
);
|
|
91
116
|
} else {
|
|
92
117
|
hints.push(
|
|
93
118
|
'Please verify your personal access token is valid and not expired.'
|
|
@@ -132,7 +157,7 @@ class ConfluenceClient {
|
|
|
132
157
|
}
|
|
133
158
|
|
|
134
159
|
buildAuthHeader() {
|
|
135
|
-
if (this.authType === 'mtls') {
|
|
160
|
+
if (this.authType === 'mtls' || this.authType === 'cookie') {
|
|
136
161
|
return null;
|
|
137
162
|
}
|
|
138
163
|
|
|
@@ -143,6 +168,18 @@ class ConfluenceClient {
|
|
|
143
168
|
return this.authType === 'basic' ? this.buildBasicAuthHeader() : `Bearer ${this.token}`;
|
|
144
169
|
}
|
|
145
170
|
|
|
171
|
+
buildAuthHeaders() {
|
|
172
|
+
const headers = {};
|
|
173
|
+
const authHeader = this.buildAuthHeader();
|
|
174
|
+
if (authHeader) {
|
|
175
|
+
headers.Authorization = authHeader;
|
|
176
|
+
}
|
|
177
|
+
if (this.authType === 'cookie' && this.cookie) {
|
|
178
|
+
headers.Cookie = this.cookie;
|
|
179
|
+
}
|
|
180
|
+
return headers;
|
|
181
|
+
}
|
|
182
|
+
|
|
146
183
|
buildHttpsAgent() {
|
|
147
184
|
if (this.protocol !== 'https' || !this.mtls) {
|
|
148
185
|
return null;
|
|
@@ -381,11 +418,26 @@ class ConfluenceClient {
|
|
|
381
418
|
};
|
|
382
419
|
}
|
|
383
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Escape a string for safe use inside a CQL double-quoted literal.
|
|
423
|
+
* Only escapes characters that can break out of the literal: backslash and
|
|
424
|
+
* double quote. Wildcards (*, ?) and fuzzy (~) are left as-is so existing
|
|
425
|
+
* search semantics are preserved.
|
|
426
|
+
*/
|
|
427
|
+
escapeCql(str) {
|
|
428
|
+
if (typeof str !== 'string') {
|
|
429
|
+
return '';
|
|
430
|
+
}
|
|
431
|
+
return str
|
|
432
|
+
.replace(/\\/g, '\\\\')
|
|
433
|
+
.replace(/"/g, '\\"');
|
|
434
|
+
}
|
|
435
|
+
|
|
384
436
|
/**
|
|
385
437
|
* Search for pages
|
|
386
438
|
*/
|
|
387
439
|
async search(query, limit = 10, rawCql = false) {
|
|
388
|
-
const cql = rawCql ? query : `text ~ "${
|
|
440
|
+
const cql = rawCql ? query : `text ~ "${this.escapeCql(query)}"`;
|
|
389
441
|
const response = await this.client.get('/search', {
|
|
390
442
|
params: {
|
|
391
443
|
cql,
|
|
@@ -966,12 +1018,15 @@ class ConfluenceClient {
|
|
|
966
1018
|
}
|
|
967
1019
|
|
|
968
1020
|
// Download directly using axios with the same auth headers
|
|
969
|
-
const
|
|
1021
|
+
const downloadRequestConfig = {
|
|
970
1022
|
responseType: options.responseType || 'stream',
|
|
971
|
-
headers:
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1023
|
+
headers: this.buildAuthHeaders()
|
|
1024
|
+
};
|
|
1025
|
+
const httpsAgent = this.buildHttpsAgent();
|
|
1026
|
+
if (httpsAgent) {
|
|
1027
|
+
downloadRequestConfig.httpsAgent = httpsAgent;
|
|
1028
|
+
}
|
|
1029
|
+
const downloadResponse = await axios.get(downloadUrl, downloadRequestConfig);
|
|
975
1030
|
return downloadResponse.data;
|
|
976
1031
|
}
|
|
977
1032
|
|
|
@@ -1872,9 +1927,9 @@ class ConfluenceClient {
|
|
|
1872
1927
|
* Search for a page by title and space
|
|
1873
1928
|
*/
|
|
1874
1929
|
async findPageByTitle(title, spaceKey = null) {
|
|
1875
|
-
let cql = `title = "${title}"`;
|
|
1930
|
+
let cql = `title = "${this.escapeCql(title)}"`;
|
|
1876
1931
|
if (spaceKey) {
|
|
1877
|
-
cql += ` AND space = "${spaceKey}"`;
|
|
1932
|
+
cql += ` AND space = "${this.escapeCql(spaceKey)}"`;
|
|
1878
1933
|
}
|
|
1879
1934
|
|
|
1880
1935
|
const response = await this.client.get('/search', {
|
|
@@ -1926,25 +1981,33 @@ class ConfluenceClient {
|
|
|
1926
1981
|
* Get all descendant pages recursively
|
|
1927
1982
|
*/
|
|
1928
1983
|
async getAllDescendantPages(pageId, maxDepth = 10, currentDepth = 0) {
|
|
1984
|
+
const semaphore = createSemaphore(10);
|
|
1985
|
+
return this._collectDescendants(pageId, maxDepth, currentDepth, semaphore);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
async _collectDescendants(pageId, maxDepth, currentDepth, semaphore) {
|
|
1929
1989
|
if (currentDepth >= maxDepth) {
|
|
1930
1990
|
return [];
|
|
1931
1991
|
}
|
|
1932
1992
|
|
|
1933
|
-
|
|
1993
|
+
await semaphore.acquire();
|
|
1994
|
+
let children;
|
|
1995
|
+
try {
|
|
1996
|
+
children = await this.getChildPages(pageId);
|
|
1997
|
+
} finally {
|
|
1998
|
+
semaphore.release();
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1934
2001
|
// Attach parentId so we can later reconstruct hierarchy if needed
|
|
1935
2002
|
const childrenWithParent = children.map(child => ({ ...child, parentId: pageId }));
|
|
1936
|
-
let allDescendants = [...childrenWithParent];
|
|
1937
2003
|
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
child.id,
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
);
|
|
1944
|
-
allDescendants = allDescendants.concat(grandChildren);
|
|
1945
|
-
}
|
|
2004
|
+
const grandChildrenLists = await Promise.all(
|
|
2005
|
+
children.map(child =>
|
|
2006
|
+
this._collectDescendants(child.id, maxDepth, currentDepth + 1, semaphore)
|
|
2007
|
+
)
|
|
2008
|
+
);
|
|
1946
2009
|
|
|
1947
|
-
return
|
|
2010
|
+
return childrenWithParent.concat(...grandChildrenLists);
|
|
1948
2011
|
}
|
|
1949
2012
|
|
|
1950
2013
|
/**
|
|
@@ -2178,7 +2241,10 @@ class ConfluenceClient {
|
|
|
2178
2241
|
return pathOrUrl;
|
|
2179
2242
|
}
|
|
2180
2243
|
|
|
2181
|
-
|
|
2244
|
+
const pathWithPrefix = this.webUrlPrefix && !pathOrUrl.startsWith(this.webUrlPrefix)
|
|
2245
|
+
? `${this.webUrlPrefix}${pathOrUrl}`
|
|
2246
|
+
: pathOrUrl;
|
|
2247
|
+
return this.buildUrl(pathWithPrefix);
|
|
2182
2248
|
}
|
|
2183
2249
|
|
|
2184
2250
|
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.1",
|
|
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.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"axios": "^1.15.0",
|