Skip to content

Commit a0c87be

Browse files
authored
Merge pull request #64 from Telefonica/feat/56/custom-auth-methods
feat: Support different authentication methods
2 parents d98e17f + ab4d8e4 commit a0c87be

36 files changed

+753
-223
lines changed

.vscode/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mcp.json

components/confluence-sync/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
#### Deprecated
1212
#### Removed
1313

14+
## [2.1.0] - 2025-10-17
15+
16+
### Added
17+
18+
* feat: Add authentication options (OAuth2, Basic, JWT). Deprecate personalAccessToken.
19+
20+
### Changed
21+
22+
* feat: Use confluence.js library to retrieve also data about page children, not only pages. The new version 2.1.0 of confluence.js supports passing pagination options to get all children pages.
23+
* chore: Update confluence.js to 2.1.0
24+
* refactor: Adapt error handling to the new confluence.js error structure.
25+
1426
## [2.0.2] - 2025-07-11
1527

1628
### Fixed

components/confluence-sync/README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ This library requires:
4444

4545
* A Confluence instance.
4646
* The id of the Confluence space where the pages will be created.
47-
* A personal access token to authenticate. You can create a personal access token following the instructions in the [Atlassian documentation](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/).
47+
* Valid authentication credentials to access the Confluence instance. It uses the `confluence.js` library internally, so it supports the [same authentication methods](https://github.com/MrRefactoring/confluence.js?tab=readme-ov-file#authentication) as it.
4848

4949
### Compatibility
5050

@@ -68,7 +68,11 @@ import { ConfluenceSyncPages } from '@telefonica/confluence-sync';
6868

6969
const confluenceSyncPages = new ConfluenceSyncPages({
7070
url: "https://your.confluence.com",
71-
personalAccessToken: "*******",
71+
authentication: {
72+
oauth2: {
73+
accessToken: "your-oauth2-access-token"
74+
}
75+
},
7276
spaceId: "your-space-id",
7377
rootPageId: "12345678"
7478
logLevel: "debug",
@@ -191,7 +195,11 @@ import { ConfluenceSyncPages, SyncModes } from '@telefonica/confluence-sync';
191195

192196
const confluenceSyncPages = new ConfluenceSyncPages({
193197
url: "https://my.confluence.es",
194-
personalAccessToken: "*******",
198+
authentication: {
199+
oauth2: {
200+
accessToken: "my-oauth2-access-token"
201+
}
202+
},
195203
spaceId: "MY-SPACE",
196204
logLevel: "debug",
197205
dryRun: false,
@@ -214,7 +222,17 @@ await confluenceSyncPages.sync([
214222
The main class of the library. It receives a configuration object with the following properties:
215223

216224
* `url`: URL of the Confluence instance.
217-
* `personalAccessToken`: Personal access token to authenticate in Confluence.
225+
* `personalAccessToken`: Personal access token to authenticate in Confluence. To be DEPRECATED in future versions. Use the `authentication` property instead.
226+
* `authentication`: Authentication options to access Confluence. It supports the following methods:
227+
* `oauth2`: OAuth2 authentication. It requires:
228+
* `accessToken`: Access token to authenticate.
229+
* `basic`: Basic authentication.
230+
* `email`: Email of the user.
231+
* `apiToken`: API token to authenticate.
232+
* `jwt`: JWT authentication.
233+
* `issuer`: Issuer of the JWT.
234+
* `secret`: Secret to sign the JWT.
235+
* `expiryTimeSeconds`: Optional expiry time of the JWT in seconds.
218236
* `spaceId`: Key of the space where the pages will be created.
219237
* `rootPageId`: ID of the root page under the pages will be created. It only can be missing if the sync mode is `flat` and all the pages provided have an id.
220238
* `logLevel`: One of `silly`, `debug`, `info`, `warn`, `error` or `silent`. Default is `silent`.

components/confluence-sync/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@telefonica/confluence-sync",
33
"description": "Creates/updates/deletes Confluence pages based on a list of objects containing the page contents. Supports nested pages and attachments upload",
4-
"version": "2.0.2",
4+
"version": "2.1.0",
55
"license": "Apache-2.0",
66
"author": "Telefónica Innovación Digital",
77
"repository": {
@@ -58,9 +58,9 @@
5858
]
5959
},
6060
"dependencies": {
61-
"@mocks-server/logger": "2.0.0-beta.2",
6261
"axios": "1.6.7",
63-
"confluence.js": "1.7.4",
62+
"@mocks-server/logger": "2.0.0-beta.2",
63+
"confluence.js": "2.1.0",
6464
"fastq": "1.17.1"
6565
},
6666
"devDependencies": {

components/confluence-sync/src/ConfluenceSyncPages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const ConfluenceSyncPages: ConfluenceSyncPagesConstructor = class Conflue
5555
url,
5656
spaceId,
5757
personalAccessToken,
58+
authentication,
5859
dryRun,
5960
syncMode,
6061
rootPageId,
@@ -66,6 +67,7 @@ export const ConfluenceSyncPages: ConfluenceSyncPagesConstructor = class Conflue
6667
url,
6768
spaceId,
6869
personalAccessToken,
70+
authentication,
6971
logger: this._logger.namespace("confluence"),
7072
dryRun,
7173
});

components/confluence-sync/src/ConfluenceSyncPages.types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
ConfluencePage,
88
ConfluencePageBasicInfo,
99
ConfluenceId,
10+
ConfluenceClientAuthenticationConfig,
1011
} from "./confluence/CustomConfluenceClient.types";
1112

1213
export enum SyncModes {
@@ -56,8 +57,13 @@ export interface ConfluenceSyncPagesConfig {
5657
spaceId: string;
5758
/** Confluence page under which all pages will be synced */
5859
rootPageId?: ConfluenceId;
59-
/** Confluence personal access token */
60-
personalAccessToken: string;
60+
/**
61+
* Confluence personal access token. Use authentication.oauth2.accessToken instead
62+
* @deprecated Use authentication.oauth2.accessToken instead
63+
*/
64+
personalAccessToken?: string;
65+
/** Authentication configuration */
66+
authentication?: ConfluenceClientAuthenticationConfig;
6167
/** Log level */
6268
logLevel?: LogLevel;
6369
/** Dry run option */

components/confluence-sync/src/confluence/CustomConfluenceClient.ts

Lines changed: 106 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
import type { LoggerInterface } from "@mocks-server/logger";
55
import type { Models } from "confluence.js";
66
import { ConfluenceClient } from "confluence.js";
7-
import axios from "axios";
8-
97
import type {
8+
ConfluenceClientAuthenticationConfig,
109
Attachments,
1110
ConfluenceClientConfig,
1211
ConfluenceClientConstructor,
@@ -16,16 +15,77 @@ import type {
1615
ConfluenceId,
1716
CreatePageParams,
1817
} from "./CustomConfluenceClient.types";
18+
1919
import { AttachmentsNotFoundError } from "./errors/AttachmentsNotFoundError";
2020
import { toConfluenceClientError } from "./errors/AxiosErrors";
2121
import { CreateAttachmentsError } from "./errors/CreateAttachmentsError";
2222
import { CreatePageError } from "./errors/CreatePageError";
2323
import { DeletePageError } from "./errors/DeletePageError";
2424
import { PageNotFoundError } from "./errors/PageNotFoundError";
2525
import { UpdatePageError } from "./errors/UpdatePageError";
26+
import { CustomError } from "./errors/CustomError";
2627

2728
const GET_CHILDREN_LIMIT = 100;
2829

30+
/**
31+
* Type guard to check if the authentication is basic
32+
* @param auth - Object to check
33+
* @returns True if the authentication is basic, false otherwise
34+
*/
35+
function isBasicAuthentication(
36+
auth: ConfluenceClientAuthenticationConfig,
37+
): auth is { basic: { email: string; apiToken: string } } {
38+
return (
39+
(auth as { basic: { email: string; apiToken: string } }).basic !== undefined
40+
);
41+
}
42+
43+
/**
44+
* Type guard to check if the authentication is OAuth2
45+
* @param auth - Object to check
46+
* @returns True if the authentication is OAuth2, false otherwise
47+
*/
48+
function isOAuth2Authentication(
49+
auth: ConfluenceClientAuthenticationConfig,
50+
): auth is {
51+
oauth2: { accessToken: string };
52+
} {
53+
return (auth as { oauth2: { accessToken: string } }).oauth2 !== undefined;
54+
}
55+
56+
/**
57+
* Type guard to check if the authentication is JWT
58+
* @param auth - Object to check
59+
* @returns True if the authentication is JWT, false otherwise
60+
*/
61+
function isJWTAuthentication(
62+
auth: ConfluenceClientAuthenticationConfig,
63+
): auth is {
64+
jwt: { issuer: string; secret: string; expiryTimeSeconds?: number };
65+
} {
66+
return (
67+
(auth as { jwt: { issuer: string; secret: string } }).jwt !== undefined
68+
);
69+
}
70+
71+
/**
72+
* Type guard to check if the authentication is valid
73+
* @param auth The authentication object to check
74+
* @returns True if the authentication is valid, false otherwise
75+
*/
76+
function isAuthentication(
77+
auth: unknown,
78+
): auth is ConfluenceClientAuthenticationConfig {
79+
if (typeof auth !== "object" || auth === null) {
80+
return false;
81+
}
82+
return (
83+
isBasicAuthentication(auth as ConfluenceClientAuthenticationConfig) ||
84+
isOAuth2Authentication(auth as ConfluenceClientAuthenticationConfig) ||
85+
isJWTAuthentication(auth as ConfluenceClientAuthenticationConfig)
86+
);
87+
}
88+
2989
export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomConfluenceClient
3090
implements ConfluenceClientInterface
3191
{
@@ -35,11 +95,28 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
3595

3696
constructor(config: ConfluenceClientConfig) {
3797
this._config = config;
98+
99+
if (
100+
!isAuthentication(config.authentication) &&
101+
!config.personalAccessToken
102+
) {
103+
throw new Error(
104+
"Either authentication or personalAccessToken must be provided",
105+
);
106+
}
107+
108+
// Backward compatibility with personalAccessToken
109+
const authentication = isAuthentication(config.authentication)
110+
? config.authentication
111+
: {
112+
oauth2: {
113+
accessToken: config.personalAccessToken as string,
114+
},
115+
};
116+
38117
this._client = new ConfluenceClient({
39118
host: config.url,
40-
authentication: {
41-
personalAccessToken: config.personalAccessToken,
42-
},
119+
authentication,
43120
apiPrefix: "/rest/",
44121
});
45122
this._logger = config.logger;
@@ -57,26 +134,19 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
57134
): Promise<Models.Content[]> {
58135
try {
59136
this._logger.silly(`Getting child pages of parent with id ${parentId}`);
60-
const response = await axios.get<Models.ContentChildren>(
61-
`${this._config.url}/rest/api/content/${parentId}/child`,
62-
{
63-
params: {
64-
start,
65-
limit: GET_CHILDREN_LIMIT,
66-
expand: "page",
67-
},
68-
headers: {
69-
accept: "application/json",
70-
Authorization: `Bearer ${this._config.personalAccessToken}`,
71-
},
72-
},
73-
);
137+
const response: Models.ContentChildren =
138+
await this._client.contentChildrenAndDescendants.getContentChildren({
139+
id: parentId,
140+
start,
141+
limit: GET_CHILDREN_LIMIT,
142+
expand: ["page"],
143+
});
74144
this._logger.silly(
75-
`Get child pages response of page ${parentId}, starting at ${start}: ${JSON.stringify(response.data, null, 2)}`,
145+
`Get child pages response of page ${parentId}, starting at ${start}: ${JSON.stringify(response.page, null, 2)}`,
76146
);
77147

78-
const childrenResults = response.data.page?.results || [];
79-
const size = response.data.page?.size || 0;
148+
const childrenResults = response.page?.results || [];
149+
const size = response.page?.size || 0;
80150

81151
const allChildren: Models.Content[] = [
82152
...otherChildren,
@@ -92,7 +162,8 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
92162
}
93163

94164
return allChildren;
95-
} catch (error) {
165+
} catch (e) {
166+
const error = toConfluenceClientError(e);
96167
throw new PageNotFoundError(parentId, { cause: error });
97168
}
98169
}
@@ -132,8 +203,12 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
132203
this._convertToConfluencePageBasicInfo(child),
133204
),
134205
};
135-
} catch (error) {
136-
throw new PageNotFoundError(id, { cause: error });
206+
} catch (e) {
207+
if (!(e instanceof CustomError)) {
208+
const error = toConfluenceClientError(e);
209+
throw new PageNotFoundError(id, { cause: error });
210+
}
211+
throw e;
137212
}
138213
}
139214

@@ -247,7 +322,8 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
247322
try {
248323
this._logger.silly(`Deleting content with id ${id}`);
249324
await this._client.content.deleteContent({ id });
250-
} catch (error) {
325+
} catch (e) {
326+
const error = toConfluenceClientError(e);
251327
throw new DeletePageError(id, { cause: error });
252328
}
253329
} else {
@@ -272,7 +348,8 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
272348
title: attachment.title,
273349
})) || []
274350
);
275-
} catch (error) {
351+
} catch (e) {
352+
const error = toConfluenceClientError(e);
276353
throw new AttachmentsNotFoundError(id, { cause: error });
277354
}
278355
}
@@ -300,7 +377,8 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
300377
this._logger.silly(
301378
`Create attachments response: ${JSON.stringify(response, null, 2)}`,
302379
);
303-
} catch (error) {
380+
} catch (e) {
381+
const error = toConfluenceClientError(e);
304382
throw new CreateAttachmentsError(id, { cause: error });
305383
}
306384
} else {

components/confluence-sync/src/confluence/CustomConfluenceClient.types.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,47 @@ export interface ConfluencePage {
3030
children?: ConfluencePageBasicInfo[];
3131
}
3232

33+
export type ConfluenceClientBasicAuthenticationConfig = {
34+
basic: {
35+
/** Basic auth email */
36+
email: string;
37+
/** Basic auth API token */
38+
apiToken: string;
39+
};
40+
};
41+
42+
export type ConfluenceClientOAuth2AuthenticationConfig = {
43+
oauth2: {
44+
/** OAuth2 access token */
45+
accessToken: string;
46+
};
47+
};
48+
49+
export type ConfluenceClientJWTAuthenticationConfig = {
50+
jwt: {
51+
/** JWT issuer */
52+
issuer: string;
53+
/** JWT secret */
54+
secret: string;
55+
/** JWT expiry time in seconds */
56+
expiryTimeSeconds?: number;
57+
};
58+
};
59+
60+
export type ConfluenceClientAuthenticationConfig =
61+
| ConfluenceClientBasicAuthenticationConfig
62+
| ConfluenceClientOAuth2AuthenticationConfig
63+
| ConfluenceClientJWTAuthenticationConfig;
64+
3365
/** Config for creating a Confluence client */
3466
export interface ConfluenceClientConfig {
35-
/** Confluence personal access token */
36-
personalAccessToken: string;
67+
/**
68+
* Confluence personal access token
69+
* @deprecated Use authentication.oauth2.accessToken instead
70+
**/
71+
personalAccessToken?: string;
72+
/** Confluence authentication configuration */
73+
authentication?: ConfluenceClientAuthenticationConfig;
3774
/** Confluence url */
3875
url: string;
3976
/** Confluence space id */

0 commit comments

Comments
 (0)