confluence-cli 1.27.0 → 1.27.2

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.
@@ -64,6 +64,11 @@ export CONFLUENCE_EMAIL="user@company.com"
64
64
  export CONFLUENCE_API_TOKEN="your-scoped-token"
65
65
  ```
66
66
 
67
+ Required classic scopes for scoped tokens:
68
+ - Read-only: `read:confluence-content.all`, `read:confluence-space.summary`, `search:confluence`
69
+ - Write: add `write:confluence-content`, `write:confluence-file`, `write:confluence-space`
70
+ - Attachments: `readonly:content.attachment:confluence` (download), `write:confluence-file` (upload)
71
+
67
72
  **Read-only mode (recommended for AI agents):**
68
73
 
69
74
  Prevents all write operations (create, update, delete, move, etc.) at the profile level. Useful when giving an AI agent access to Confluence for reading only.
package/README.md CHANGED
@@ -193,6 +193,22 @@ Scoped tokens restrict access to specific Atlassian products and permissions, fo
193
193
  - **API path:** `/ex/confluence/<your-cloud-id>/wiki/rest/api`
194
194
  - **Auth type:** `basic` (email + scoped token)
195
195
 
196
+ **Required scopes for scoped API tokens:**
197
+
198
+ When creating a scoped token, select the following [classic scopes](https://developer.atlassian.com/cloud/confluence/scopes-for-oauth-2-3LO-and-forge-apps/) based on your needs:
199
+
200
+ | Scope | Required for |
201
+ |-------|-------------|
202
+ | `read:confluence-content.all` | Reading pages and blog posts (`read`, `info`) |
203
+ | `read:confluence-space.summary` | Listing spaces (`spaces`) |
204
+ | `search:confluence` | Searching content (`search`) |
205
+ | `readonly:content.attachment:confluence` | Downloading attachments (`attachments --download`) |
206
+ | `write:confluence-content` | Creating and updating pages (`create`, `update`) |
207
+ | `write:confluence-file` | Uploading attachments (`attachments --upload`) |
208
+ | `write:confluence-space` | Managing spaces |
209
+
210
+ For **read-only** usage, select at minimum: `read:confluence-content.all`, `read:confluence-space.summary`, and `search:confluence`.
211
+
196
212
  **On-premise / Data Center:** Use your Confluence username and password for basic authentication.
197
213
 
198
214
  ## Usage
package/lib/config.js CHANGED
@@ -107,9 +107,12 @@ function readConfigFile() {
107
107
  // Write the full multi-profile config structure
108
108
  function saveConfigFile(data) {
109
109
  if (!fs.existsSync(CONFIG_DIR)) {
110
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
110
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
111
+ } else {
112
+ fs.chmodSync(CONFIG_DIR, 0o700);
111
113
  }
112
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
114
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
115
+ fs.chmodSync(CONFIG_FILE, 0o600);
113
116
  }
114
117
 
115
118
  // Helper function to validate CLI-provided options
@@ -48,6 +48,48 @@ class ConfluenceClient {
48
48
  baseURL: this.baseURL,
49
49
  headers
50
50
  });
51
+
52
+ this.client.interceptors.response.use(
53
+ response => response,
54
+ error => {
55
+ if (error.response?.status === 401) {
56
+ const hints = ['Authentication failed (401 Unauthorized).'];
57
+ if (this.isScopedToken()) {
58
+ hints.push(
59
+ 'You are using a scoped API token (api.atlassian.com). Please verify:',
60
+ ' - Your token has the required scopes (e.g., read:confluence-content.all, read:confluence-space.summary)',
61
+ ' - Your Cloud ID in the API path is correct',
62
+ ' - Your email matches the account that created the token',
63
+ 'See: https://developer.atlassian.com/cloud/confluence/scopes-for-oauth-2-3LO-and-forge-apps/'
64
+ );
65
+ } else if (this.authType === 'basic' && this.isCloud()) {
66
+ hints.push(
67
+ 'Please verify your email and API token are correct.',
68
+ 'Generate a token at: https://id.atlassian.com/manage-profile/security/api-tokens'
69
+ );
70
+ } else if (this.authType === 'basic') {
71
+ hints.push(
72
+ 'Please verify your username and password are correct.'
73
+ );
74
+ } else {
75
+ hints.push(
76
+ 'Please verify your personal access token is valid and not expired.'
77
+ );
78
+ }
79
+ error.message = hints.join('\n');
80
+ }
81
+ return Promise.reject(error);
82
+ }
83
+ );
84
+ }
85
+
86
+ isCloud() {
87
+ return this.isScopedToken() || (this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net'));
88
+ }
89
+
90
+ isScopedToken() {
91
+ const d = (this.domain || '').trim().toLowerCase();
92
+ return d === 'api.atlassian.com' || this.apiPath?.includes('/ex/confluence/');
51
93
  }
52
94
 
53
95
  sanitizeApiPath(rawPath) {
@@ -1026,10 +1068,15 @@ class ConfluenceClient {
1026
1068
  // Convert code blocks to Confluence code macro
1027
1069
  storage = storage.replace(/<pre><code(?:\s+class="language-(\w+)")?>(.*?)<\/code><\/pre>/gs, (_, lang, code) => {
1028
1070
  const language = lang || 'text';
1029
- return `<ac:structured-macro ac:name="code">
1030
- <ac:parameter ac:name="language">${language}</ac:parameter>
1031
- <ac:plain-text-body><![CDATA[${code}]]></ac:plain-text-body>
1032
- </ac:structured-macro>`;
1071
+ // Trim trailing newline added by markdown-it during HTML rendering,
1072
+ // and decode HTML entities that markdown-it encodes inside <code> blocks
1073
+ // so they appear as literal characters in the CDATA output
1074
+ const decodedCode = code.replace(/\n$/, '')
1075
+ .replace(/&quot;/g, '"')
1076
+ .replace(/&amp;/g, '&')
1077
+ .replace(/&lt;/g, '<')
1078
+ .replace(/&gt;/g, '>');
1079
+ return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${language}</ac:parameter><ac:plain-text-body><![CDATA[${decodedCode}]]></ac:plain-text-body></ac:structured-macro>`;
1033
1080
  });
1034
1081
 
1035
1082
  // Convert inline code
@@ -1070,13 +1117,21 @@ class ConfluenceClient {
1070
1117
  storage = storage.replace(/<td>(.*?)<\/td>/g, '<td><p>$1</p></td>');
1071
1118
 
1072
1119
  // Convert links
1073
- storage = storage.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<ac:link><ri:url ri:value="$1" /><ac:plain-text-link-body><![CDATA[$2]]></ac:plain-text-link-body></ac:link>');
1120
+ // Confluence Cloud does not render ac:link + ri:url; use smart links instead.
1121
+ // Server/Data Center instances continue to use the ac:link storage format.
1122
+ if (this.isCloud()) {
1123
+ storage = storage.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<a href="$1" data-card-appearance="inline">$2</a>');
1124
+ } else {
1125
+ storage = storage.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<ac:link><ri:url ri:value="$1" /><ac:plain-text-link-body><![CDATA[$2]]></ac:plain-text-link-body></ac:link>');
1126
+ }
1074
1127
 
1075
1128
  // Convert horizontal rules
1076
1129
  storage = storage.replace(/<hr\s*\/?>/g, '<hr />');
1077
1130
 
1078
- // Clean up any remaining HTML entities and normalize whitespace
1079
- storage = storage.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
1131
+ // Note: Do NOT globally decode &lt; &gt; &amp; here. These represent literal
1132
+ // characters in user content (e.g. <placeholder> in inline text) and
1133
+ // Confluence storage format renders them correctly as-is. Code block
1134
+ // entities are decoded separately above before CDATA insertion.
1080
1135
 
1081
1136
  return storage;
1082
1137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.27.0",
3
+ "version": "1.27.2",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {