Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mcp.json
12 changes: 12 additions & 0 deletions components/confluence-sync/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
#### Deprecated
#### Removed

## [2.1.0] - 2025-10-17

### Added

* feat: Add authentication options (OAuth2, Basic, JWT). Deprecate personalAccessToken.

### Changed

* 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.
* chore: Update confluence.js to 2.1.0
* refactor: Adapt error handling to the new confluence.js error structure.

## [2.0.2] - 2025-07-11

### Fixed
Expand Down
26 changes: 22 additions & 4 deletions components/confluence-sync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ This library requires:

* A Confluence instance.
* The id of the Confluence space where the pages will be created.
* 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/).
* 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.

### Compatibility

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

const confluenceSyncPages = new ConfluenceSyncPages({
url: "https://your.confluence.com",
personalAccessToken: "*******",
authentication: {
oauth2: {
accessToken: "your-oauth2-access-token"
}
},
spaceId: "your-space-id",
rootPageId: "12345678"
logLevel: "debug",
Expand Down Expand Up @@ -191,7 +195,11 @@ import { ConfluenceSyncPages, SyncModes } from '@telefonica/confluence-sync';

const confluenceSyncPages = new ConfluenceSyncPages({
url: "https://my.confluence.es",
personalAccessToken: "*******",
authentication: {
oauth2: {
accessToken: "my-oauth2-access-token"
}
},
spaceId: "MY-SPACE",
logLevel: "debug",
dryRun: false,
Expand All @@ -214,7 +222,17 @@ await confluenceSyncPages.sync([
The main class of the library. It receives a configuration object with the following properties:

* `url`: URL of the Confluence instance.
* `personalAccessToken`: Personal access token to authenticate in Confluence.
* `personalAccessToken`: Personal access token to authenticate in Confluence. To be DEPRECATED in future versions. Use the `authentication` property instead.
* `authentication`: Authentication options to access Confluence. It supports the following methods:
* `oauth2`: OAuth2 authentication. It requires:
* `accessToken`: Access token to authenticate.
* `basic`: Basic authentication.
* `email`: Email of the user.
* `apiToken`: API token to authenticate.
* `jwt`: JWT authentication.
* `issuer`: Issuer of the JWT.
* `secret`: Secret to sign the JWT.
* `expiryTimeSeconds`: Optional expiry time of the JWT in seconds.
* `spaceId`: Key of the space where the pages will be created.
* `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.
* `logLevel`: One of `silly`, `debug`, `info`, `warn`, `error` or `silent`. Default is `silent`.
Expand Down
6 changes: 3 additions & 3 deletions components/confluence-sync/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@telefonica/confluence-sync",
"description": "Creates/updates/deletes Confluence pages based on a list of objects containing the page contents. Supports nested pages and attachments upload",
"version": "2.0.2",
"version": "2.1.0",
"license": "Apache-2.0",
"author": "Telefónica Innovación Digital",
"repository": {
Expand Down Expand Up @@ -58,9 +58,9 @@
]
},
"dependencies": {
"@mocks-server/logger": "2.0.0-beta.2",
"axios": "1.6.7",
"confluence.js": "1.7.4",
"@mocks-server/logger": "2.0.0-beta.2",
"confluence.js": "2.1.0",
"fastq": "1.17.1"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions components/confluence-sync/src/ConfluenceSyncPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const ConfluenceSyncPages: ConfluenceSyncPagesConstructor = class Conflue
url,
spaceId,
personalAccessToken,
authentication,
dryRun,
syncMode,
rootPageId,
Expand All @@ -66,6 +67,7 @@ export const ConfluenceSyncPages: ConfluenceSyncPagesConstructor = class Conflue
url,
spaceId,
personalAccessToken,
authentication,
logger: this._logger.namespace("confluence"),
dryRun,
});
Expand Down
10 changes: 8 additions & 2 deletions components/confluence-sync/src/ConfluenceSyncPages.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ConfluencePage,
ConfluencePageBasicInfo,
ConfluenceId,
ConfluenceClientAuthenticationConfig,
} from "./confluence/CustomConfluenceClient.types";

export enum SyncModes {
Expand Down Expand Up @@ -56,8 +57,13 @@ export interface ConfluenceSyncPagesConfig {
spaceId: string;
/** Confluence page under which all pages will be synced */
rootPageId?: ConfluenceId;
/** Confluence personal access token */
personalAccessToken: string;
/**
* Confluence personal access token. Use authentication.oauth2.accessToken instead
* @deprecated Use authentication.oauth2.accessToken instead
*/
personalAccessToken?: string;
/** Authentication configuration */
authentication?: ConfluenceClientAuthenticationConfig;
/** Log level */
logLevel?: LogLevel;
/** Dry run option */
Expand Down
134 changes: 106 additions & 28 deletions components/confluence-sync/src/confluence/CustomConfluenceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
import type { LoggerInterface } from "@mocks-server/logger";
import type { Models } from "confluence.js";
import { ConfluenceClient } from "confluence.js";
import axios from "axios";

import type {
ConfluenceClientAuthenticationConfig,
Attachments,
ConfluenceClientConfig,
ConfluenceClientConstructor,
Expand All @@ -16,16 +15,77 @@ import type {
ConfluenceId,
CreatePageParams,
} from "./CustomConfluenceClient.types";

import { AttachmentsNotFoundError } from "./errors/AttachmentsNotFoundError";
import { toConfluenceClientError } from "./errors/AxiosErrors";
import { CreateAttachmentsError } from "./errors/CreateAttachmentsError";
import { CreatePageError } from "./errors/CreatePageError";
import { DeletePageError } from "./errors/DeletePageError";
import { PageNotFoundError } from "./errors/PageNotFoundError";
import { UpdatePageError } from "./errors/UpdatePageError";
import { CustomError } from "./errors/CustomError";

const GET_CHILDREN_LIMIT = 100;

/**
* Type guard to check if the authentication is basic
* @param auth - Object to check
* @returns True if the authentication is basic, false otherwise
*/
function isBasicAuthentication(
auth: ConfluenceClientAuthenticationConfig,
): auth is { basic: { email: string; apiToken: string } } {
return (
(auth as { basic: { email: string; apiToken: string } }).basic !== undefined
);
}

/**
* Type guard to check if the authentication is OAuth2
* @param auth - Object to check
* @returns True if the authentication is OAuth2, false otherwise
*/
function isOAuth2Authentication(
auth: ConfluenceClientAuthenticationConfig,
): auth is {
oauth2: { accessToken: string };
} {
return (auth as { oauth2: { accessToken: string } }).oauth2 !== undefined;
}

/**
* Type guard to check if the authentication is JWT
* @param auth - Object to check
* @returns True if the authentication is JWT, false otherwise
*/
function isJWTAuthentication(
auth: ConfluenceClientAuthenticationConfig,
): auth is {
jwt: { issuer: string; secret: string; expiryTimeSeconds?: number };
} {
return (
(auth as { jwt: { issuer: string; secret: string } }).jwt !== undefined
);
}

/**
* Type guard to check if the authentication is valid
* @param auth The authentication object to check
* @returns True if the authentication is valid, false otherwise
*/
function isAuthentication(
auth: unknown,
): auth is ConfluenceClientAuthenticationConfig {
if (typeof auth !== "object" || auth === null) {
return false;
}
return (
isBasicAuthentication(auth as ConfluenceClientAuthenticationConfig) ||
isOAuth2Authentication(auth as ConfluenceClientAuthenticationConfig) ||
isJWTAuthentication(auth as ConfluenceClientAuthenticationConfig)
);
}

export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomConfluenceClient
implements ConfluenceClientInterface
{
Expand All @@ -35,11 +95,28 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC

constructor(config: ConfluenceClientConfig) {
this._config = config;

if (
!isAuthentication(config.authentication) &&
!config.personalAccessToken
) {
throw new Error(
"Either authentication or personalAccessToken must be provided",
);
}

// Backward compatibility with personalAccessToken
const authentication = isAuthentication(config.authentication)
? config.authentication
: {
oauth2: {
accessToken: config.personalAccessToken as string,
},
};

this._client = new ConfluenceClient({
host: config.url,
authentication: {
personalAccessToken: config.personalAccessToken,
},
authentication,
apiPrefix: "/rest/",
});
this._logger = config.logger;
Expand All @@ -57,26 +134,19 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
): Promise<Models.Content[]> {
try {
this._logger.silly(`Getting child pages of parent with id ${parentId}`);
const response = await axios.get<Models.ContentChildren>(
`${this._config.url}/rest/api/content/${parentId}/child`,
{
params: {
start,
limit: GET_CHILDREN_LIMIT,
expand: "page",
},
headers: {
accept: "application/json",
Authorization: `Bearer ${this._config.personalAccessToken}`,
},
},
);
const response: Models.ContentChildren =
await this._client.contentChildrenAndDescendants.getContentChildren({
id: parentId,
start,
limit: GET_CHILDREN_LIMIT,
expand: ["page"],
});
this._logger.silly(
`Get child pages response of page ${parentId}, starting at ${start}: ${JSON.stringify(response.data, null, 2)}`,
`Get child pages response of page ${parentId}, starting at ${start}: ${JSON.stringify(response.page, null, 2)}`,
);

const childrenResults = response.data.page?.results || [];
const size = response.data.page?.size || 0;
const childrenResults = response.page?.results || [];
const size = response.page?.size || 0;

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

return allChildren;
} catch (error) {
} catch (e) {
const error = toConfluenceClientError(e);
throw new PageNotFoundError(parentId, { cause: error });
}
}
Expand Down Expand Up @@ -132,8 +203,12 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
this._convertToConfluencePageBasicInfo(child),
),
};
} catch (error) {
throw new PageNotFoundError(id, { cause: error });
} catch (e) {
if (!(e instanceof CustomError)) {
const error = toConfluenceClientError(e);
throw new PageNotFoundError(id, { cause: error });
}
throw e;
}
}

Expand Down Expand Up @@ -247,7 +322,8 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
try {
this._logger.silly(`Deleting content with id ${id}`);
await this._client.content.deleteContent({ id });
} catch (error) {
} catch (e) {
const error = toConfluenceClientError(e);
throw new DeletePageError(id, { cause: error });
}
} else {
Expand All @@ -272,7 +348,8 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
title: attachment.title,
})) || []
);
} catch (error) {
} catch (e) {
const error = toConfluenceClientError(e);
throw new AttachmentsNotFoundError(id, { cause: error });
}
}
Expand Down Expand Up @@ -300,7 +377,8 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
this._logger.silly(
`Create attachments response: ${JSON.stringify(response, null, 2)}`,
);
} catch (error) {
} catch (e) {
const error = toConfluenceClientError(e);
throw new CreateAttachmentsError(id, { cause: error });
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,47 @@ export interface ConfluencePage {
children?: ConfluencePageBasicInfo[];
}

export type ConfluenceClientBasicAuthenticationConfig = {
basic: {
/** Basic auth email */
email: string;
/** Basic auth API token */
apiToken: string;
};
};

export type ConfluenceClientOAuth2AuthenticationConfig = {
oauth2: {
/** OAuth2 access token */
accessToken: string;
};
};

export type ConfluenceClientJWTAuthenticationConfig = {
jwt: {
/** JWT issuer */
issuer: string;
/** JWT secret */
secret: string;
/** JWT expiry time in seconds */
expiryTimeSeconds?: number;
};
};

export type ConfluenceClientAuthenticationConfig =
| ConfluenceClientBasicAuthenticationConfig
| ConfluenceClientOAuth2AuthenticationConfig
| ConfluenceClientJWTAuthenticationConfig;

/** Config for creating a Confluence client */
export interface ConfluenceClientConfig {
/** Confluence personal access token */
personalAccessToken: string;
/**
* Confluence personal access token
* @deprecated Use authentication.oauth2.accessToken instead
**/
personalAccessToken?: string;
/** Confluence authentication configuration */
authentication?: ConfluenceClientAuthenticationConfig;
/** Confluence url */
url: string;
/** Confluence space id */
Expand Down
Loading
Loading