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 +48 -1
- package/bin/confluence.js +80 -81
- package/lib/config.js +134 -54
- package/lib/confluence-client.js +56 -19
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/plugins/confluence/skills/confluence/SKILL.md +2 -0
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"
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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})`;
|
|
@@ -597,9 +599,7 @@ program
|
|
|
597
599
|
|
|
598
600
|
analytics.track('attachments', true);
|
|
599
601
|
} catch (error) {
|
|
600
|
-
analytics
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1359
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 && !
|
|
237
|
-
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"');
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
647
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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 (
|
|
677
|
-
|
|
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
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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);
|
package/lib/confluence-client.js
CHANGED
|
@@ -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 ~ "${
|
|
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
|
-
|
|
511
|
+
const escapedKey = userKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
481
512
|
const userLinkRegex = new RegExp(
|
|
482
|
-
`<ac:link>\\s*<ri:user\\s+ri:userkey="${
|
|
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
|
|
999
|
+
const downloadRequestConfig = {
|
|
969
1000
|
responseType: options.responseType || 'stream',
|
|
970
|
-
headers:
|
|
971
|
-
|
|
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
|
-
|
|
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) {
|
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",
|
package/package.json
CHANGED
|
@@ -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
|
|