@sitecore-content-sdk/core 0.2.0-beta.2 → 0.2.0-beta.21
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/content.d.ts +1 -0
- package/content.js +1 -0
- package/dist/cjs/client/graphql-edge-proxy.js +3 -3
- package/dist/cjs/client/sitecore-client.js +34 -17
- package/dist/cjs/config/define-config.js +6 -5
- package/dist/cjs/constants.js +12 -1
- package/dist/cjs/content/content-client.js +148 -0
- package/dist/cjs/content/index.js +13 -0
- package/dist/cjs/content/locales.js +32 -0
- package/dist/cjs/content/taxonomies.js +78 -0
- package/dist/cjs/content/utils.js +16 -0
- package/dist/cjs/debug.js +1 -0
- package/dist/cjs/editing/design-library.js +2 -1
- package/dist/cjs/editing/rest-component-layout-service.js +26 -45
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/layout/content-styles.js +2 -1
- package/dist/cjs/layout/themes.js +2 -1
- package/dist/cjs/site/graphql-robots-service.js +3 -2
- package/dist/cjs/tools/auth/encryption.js +141 -0
- package/dist/cjs/tools/auth/fetcher.js +34 -0
- package/dist/cjs/tools/auth/flow.js +123 -0
- package/dist/cjs/tools/auth/index.js +27 -0
- package/dist/cjs/tools/auth/models.js +2 -0
- package/dist/cjs/tools/auth/renewal.js +130 -0
- package/dist/cjs/tools/auth/tenant-state.js +110 -0
- package/dist/cjs/tools/auth/tenant-store.js +250 -0
- package/dist/cjs/tools/index.js +26 -1
- package/dist/cjs/utils/normalize-url.js +5 -0
- package/dist/cjs/utils/utils.js +5 -3
- package/dist/esm/client/graphql-edge-proxy.js +1 -1
- package/dist/esm/client/sitecore-client.js +34 -17
- package/dist/esm/config/define-config.js +6 -5
- package/dist/esm/constants.js +11 -0
- package/dist/esm/content/content-client.js +141 -0
- package/dist/esm/content/index.js +4 -0
- package/dist/esm/content/locales.js +29 -0
- package/dist/esm/content/taxonomies.js +75 -0
- package/dist/esm/content/utils.js +13 -0
- package/dist/esm/debug.js +1 -0
- package/dist/esm/editing/design-library.js +2 -1
- package/dist/esm/editing/rest-component-layout-service.js +23 -45
- package/dist/esm/index.js +2 -0
- package/dist/esm/layout/content-styles.js +2 -1
- package/dist/esm/layout/themes.js +2 -1
- package/dist/esm/site/graphql-robots-service.js +3 -2
- package/dist/esm/tools/auth/encryption.js +101 -0
- package/dist/esm/tools/auth/fetcher.js +31 -0
- package/dist/esm/tools/auth/flow.js +118 -0
- package/dist/esm/tools/auth/index.js +5 -0
- package/dist/esm/tools/auth/models.js +1 -0
- package/dist/esm/tools/auth/renewal.js +124 -0
- package/dist/esm/tools/auth/tenant-state.js +73 -0
- package/dist/esm/tools/auth/tenant-store.js +213 -0
- package/dist/esm/tools/index.js +3 -0
- package/dist/esm/utils/normalize-url.js +1 -0
- package/dist/esm/utils/utils.js +5 -3
- package/package.json +19 -18
- package/types/client/index.d.ts +1 -1
- package/types/client/models.d.ts +17 -1
- package/types/client/sitecore-client.d.ts +50 -22
- package/types/config/index.d.ts +1 -1
- package/types/config/models.d.ts +12 -2
- package/types/constants.d.ts +10 -0
- package/types/content/content-client.d.ts +92 -0
- package/types/content/index.d.ts +4 -0
- package/types/content/locales.d.ts +38 -0
- package/types/content/taxonomies.d.ts +125 -0
- package/types/content/utils.d.ts +15 -0
- package/types/debug.d.ts +1 -0
- package/types/editing/rest-component-layout-service.d.ts +23 -58
- package/types/index.d.ts +2 -0
- package/types/native-fetcher.d.ts +0 -7
- package/types/site/graphql-robots-service.d.ts +3 -2
- package/types/tools/auth/encryption.d.ts +34 -0
- package/types/tools/auth/fetcher.d.ts +13 -0
- package/types/tools/auth/flow.d.ts +40 -0
- package/types/tools/auth/index.d.ts +5 -0
- package/types/tools/auth/models.d.ts +233 -0
- package/types/tools/auth/renewal.d.ts +36 -0
- package/types/tools/auth/tenant-state.d.ts +21 -0
- package/types/tools/auth/tenant-store.d.ts +63 -0
- package/types/tools/index.d.ts +3 -0
- package/types/utils/normalize-url.d.ts +1 -0
- package/dist/cjs/data-fetcher.js +0 -22
- package/dist/esm/data-fetcher.js +0 -17
- package/form.d.ts +0 -1
- package/form.js +0 -1
- package/types/data-fetcher.d.ts +0 -34
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL query to retrieve a specific locale by its ID.
|
|
3
|
+
*
|
|
4
|
+
* Variables:
|
|
5
|
+
* - id: The ID of the locale to retrieve.
|
|
6
|
+
*/
|
|
7
|
+
export const GET_LOCALE_QUERY = `
|
|
8
|
+
query GetLocaleById($id: ID!) {
|
|
9
|
+
locale(id: $id) {
|
|
10
|
+
system {
|
|
11
|
+
id
|
|
12
|
+
label
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
/**
|
|
18
|
+
* GraphQL query to retrieve all available locales.
|
|
19
|
+
*/
|
|
20
|
+
export const GET_LOCALES_QUERY = `
|
|
21
|
+
query GetAllLocales {
|
|
22
|
+
manyLocale {
|
|
23
|
+
system {
|
|
24
|
+
id
|
|
25
|
+
label
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// --- GraphQL queries ---
|
|
2
|
+
/**
|
|
3
|
+
* GraphQL query to retrieve all taxonomies with optional pagination for taxonomies only.
|
|
4
|
+
*
|
|
5
|
+
* Variables:
|
|
6
|
+
* - pageSize: The number of taxonomies to retrieve per page.
|
|
7
|
+
* - after: The cursor for fetching the next page of taxonomies.
|
|
8
|
+
*/
|
|
9
|
+
export const GET_TAXONOMIES_QUERY = `
|
|
10
|
+
query GetAllTaxonomies(
|
|
11
|
+
$pageSize: Int
|
|
12
|
+
$after: String
|
|
13
|
+
) {
|
|
14
|
+
manyTaxonomy(minimumPageSize: $pageSize, after: $after) {
|
|
15
|
+
cursor
|
|
16
|
+
hasMore
|
|
17
|
+
results {
|
|
18
|
+
terms {
|
|
19
|
+
cursor
|
|
20
|
+
hasMore
|
|
21
|
+
results {
|
|
22
|
+
id
|
|
23
|
+
name
|
|
24
|
+
label
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
system {
|
|
28
|
+
id
|
|
29
|
+
name
|
|
30
|
+
version
|
|
31
|
+
label
|
|
32
|
+
createdAt
|
|
33
|
+
createdBy
|
|
34
|
+
updatedAt
|
|
35
|
+
updatedBy
|
|
36
|
+
publishStatus
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
/**
|
|
43
|
+
* GraphQL query to retrieve a specific taxonomy by its ID, with optional pagination for its terms.
|
|
44
|
+
*
|
|
45
|
+
* Variables:
|
|
46
|
+
* - id: The unique ID of the taxonomy to retrieve.
|
|
47
|
+
* - termsPageSize: The number of terms to retrieve per page.
|
|
48
|
+
* - termsAfter: The cursor for fetching the next page of terms.
|
|
49
|
+
*/
|
|
50
|
+
export const GET_TAXONOMY_QUERY = `
|
|
51
|
+
query GetTaxonomyById($id: ID!, $termsPageSize: Int, $termsAfter: String) {
|
|
52
|
+
taxonomy(id: $id) {
|
|
53
|
+
terms(minimumPageSize: $termsPageSize, after: $termsAfter) {
|
|
54
|
+
cursor
|
|
55
|
+
hasMore
|
|
56
|
+
results {
|
|
57
|
+
id
|
|
58
|
+
name
|
|
59
|
+
label
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
system {
|
|
63
|
+
id
|
|
64
|
+
name
|
|
65
|
+
version
|
|
66
|
+
label
|
|
67
|
+
createdAt
|
|
68
|
+
createdBy
|
|
69
|
+
updatedAt
|
|
70
|
+
updatedBy
|
|
71
|
+
publishStatus
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { normalizeUrl } from '../utils/normalize-url';
|
|
2
|
+
/**
|
|
3
|
+
* Get the Content graphql endpoint url
|
|
4
|
+
* @param {object} params Parameters
|
|
5
|
+
* @param {string} [params.url] Content base graphql endpoint url
|
|
6
|
+
* @param {string} params.tenant Tenant name
|
|
7
|
+
* @param {string} params.environment Environment name
|
|
8
|
+
* @param {boolean} params.preview Indicates if preview mode is enabled
|
|
9
|
+
* @returns {string} Content graphql endpoint url
|
|
10
|
+
*/
|
|
11
|
+
export function getContentUrl({ url = 'https://edge-platform.sitecorecloud.io', tenant, environment, preview, }) {
|
|
12
|
+
return `${normalizeUrl(url)}/cs/api/v2/graphql/${tenant}/${environment}?preview=${preview}`;
|
|
13
|
+
}
|
package/dist/esm/debug.js
CHANGED
|
@@ -22,6 +22,7 @@ export const enableDebug = (namespaces) => debug.enable(namespaces);
|
|
|
22
22
|
*/
|
|
23
23
|
export default {
|
|
24
24
|
common: debug(`${rootNamespace}:common`),
|
|
25
|
+
content: debug(`${rootNamespace}:content`),
|
|
25
26
|
form: debug(`${rootNamespace}:form`),
|
|
26
27
|
http: debug(`${rootNamespace}:http`),
|
|
27
28
|
layout: debug(`${rootNamespace}:layout`),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SITECORE_EDGE_URL_DEFAULT } from '../constants';
|
|
2
|
+
import { normalizeUrl } from '../utils/normalize-url';
|
|
2
3
|
/**
|
|
3
4
|
* Event to be sent when report status to design library
|
|
4
5
|
*/
|
|
@@ -103,5 +104,5 @@ export function getDesignLibraryStatusEvent(status, uid) {
|
|
|
103
104
|
* @returns The full URL to the design library script.
|
|
104
105
|
*/
|
|
105
106
|
export function getDesignLibraryScriptLink(sitecoreEdgeUrl = SITECORE_EDGE_URL_DEFAULT) {
|
|
106
|
-
return `${sitecoreEdgeUrl}/v1/files/designlibrary/lib/rh-lib-script.js`;
|
|
107
|
+
return `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/designlibrary/lib/rh-lib-script.js`;
|
|
107
108
|
}
|
|
@@ -1,44 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { NativeDataFetcher } from '../native-fetcher';
|
|
2
|
+
import debug from '../debug';
|
|
3
|
+
import { SITECORE_EDGE_URL_DEFAULT } from '../constants';
|
|
4
|
+
import { resolveUrl } from '../utils';
|
|
3
5
|
/**
|
|
4
|
-
* REST service that enables
|
|
5
|
-
* Makes a request to /sitecore/api/layout/component in 'library' mode in Pages.
|
|
6
|
+
* REST service that enables design Library functionality
|
|
6
7
|
* Returns layoutData for one single rendered component
|
|
7
8
|
*/
|
|
8
9
|
export class RestComponentLayoutService {
|
|
9
10
|
constructor(config) {
|
|
10
11
|
this.config = config;
|
|
11
|
-
this.getFetcher = (req, res) => {
|
|
12
|
-
return this.config.dataFetcherResolver
|
|
13
|
-
? this.config.dataFetcherResolver(req, res)
|
|
14
|
-
: this.getDefaultFetcher(req);
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Provides default @see NativeDataFetcher data fetcher
|
|
18
|
-
* @param {IncomingMessage} [req] Request instance
|
|
19
|
-
* @returns default fetcher
|
|
20
|
-
*/
|
|
21
|
-
this.getDefaultFetcher = (req) => {
|
|
22
|
-
var _a;
|
|
23
|
-
const config = {
|
|
24
|
-
debugger: debug.editing,
|
|
25
|
-
};
|
|
26
|
-
const nativeFetcher = new NativeDataFetcher(config);
|
|
27
|
-
const headers = req && Object.assign(Object.assign({}, req.headers), (((_a = req.socket) === null || _a === void 0 ? void 0 : _a.remoteAddress) ? { 'X-Forwarded-For': req.socket.remoteAddress } : {}));
|
|
28
|
-
const fetcher = (url, data) => {
|
|
29
|
-
data = Object.assign(Object.assign({}, data), { headers: headers });
|
|
30
|
-
return nativeFetcher.fetch(url, data);
|
|
31
|
-
};
|
|
32
|
-
return fetcher;
|
|
33
|
-
};
|
|
34
12
|
}
|
|
35
|
-
fetchComponentData(params
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
debug.layout('fetching component with uid %s for %s %s %s', params.componentUid, params.itemId, params.language, params.siteName);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
13
|
+
fetchComponentData(params) {
|
|
14
|
+
const config = { debugger: debug.layout };
|
|
15
|
+
const fetcher = new NativeDataFetcher(config);
|
|
16
|
+
debug.layout('fetching component with uid %s for %s %s %s %s', params.componentUid, params.itemId, params.language, params.siteName, params.dataSourceId);
|
|
17
|
+
const fetchUrl = this.getFetchUrl(params);
|
|
18
|
+
return fetcher
|
|
19
|
+
.get(fetchUrl)
|
|
20
|
+
.then((response) => response.data)
|
|
21
|
+
.catch((error) => {
|
|
42
22
|
var _a;
|
|
43
23
|
if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 404) {
|
|
44
24
|
return error.response.data;
|
|
@@ -46,19 +26,10 @@ export class RestComponentLayoutService {
|
|
|
46
26
|
throw error;
|
|
47
27
|
});
|
|
48
28
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Resolves layout service url
|
|
51
|
-
* @param {string} apiType which layout service API to call ('render' or 'placeholder')
|
|
52
|
-
* @returns the layout service url
|
|
53
|
-
*/
|
|
54
|
-
resolveLayoutServiceUrl(apiType) {
|
|
55
|
-
const { apiHost = '', configurationName = 'jss' } = this.config;
|
|
56
|
-
return `${apiHost}/sitecore/api/layout/${apiType}/${configurationName}`;
|
|
57
|
-
}
|
|
58
29
|
getComponentFetchParams(params) {
|
|
59
30
|
// exclude undefined params with this one simple trick
|
|
60
31
|
return JSON.parse(JSON.stringify({
|
|
61
|
-
|
|
32
|
+
sitecoreContextId: this.config.contextId,
|
|
62
33
|
item: params.itemId,
|
|
63
34
|
uid: params.componentUid,
|
|
64
35
|
dataSourceId: params.dataSourceId,
|
|
@@ -66,7 +37,14 @@ export class RestComponentLayoutService {
|
|
|
66
37
|
version: params.version,
|
|
67
38
|
sc_site: params.siteName,
|
|
68
39
|
sc_lang: params.language || 'en',
|
|
69
|
-
sc_mode: params.editMode,
|
|
70
40
|
}));
|
|
71
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the fetch URL for the partial layout data endpoint
|
|
44
|
+
* @param {ComponentLayoutRequestParams} params - The parameters for the request
|
|
45
|
+
* @returns {string} The fetch URL for the component data
|
|
46
|
+
*/
|
|
47
|
+
getFetchUrl(params) {
|
|
48
|
+
return resolveUrl(`${this.config.edgeUrl || SITECORE_EDGE_URL_DEFAULT}/layout/component`, this.getComponentFetchParams(params));
|
|
49
|
+
}
|
|
72
50
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// NOTE: all imports are now named as to not make breaking changes
|
|
2
2
|
// and to keep react-native working with cjs modules.
|
|
3
3
|
import * as constants from './constants';
|
|
4
|
+
import * as form from './form';
|
|
4
5
|
export { default as debug, enableDebug } from './debug';
|
|
5
6
|
export { GraphQLRequestClient, } from './graphql-request-client';
|
|
6
7
|
export { DefaultRetryStrategy } from './retries';
|
|
@@ -8,4 +9,5 @@ export { MemoryCacheClient } from './cache-client';
|
|
|
8
9
|
export { ClientError } from 'graphql-request';
|
|
9
10
|
export { NativeDataFetcher, } from './native-fetcher';
|
|
10
11
|
export { constants };
|
|
12
|
+
export { form };
|
|
11
13
|
export { defineConfig } from './config';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SITECORE_EDGE_URL_DEFAULT } from '../constants';
|
|
2
|
+
import { normalizeUrl } from '../utils/normalize-url';
|
|
2
3
|
/**
|
|
3
4
|
* Regular expression to check if the content styles are used in the field value
|
|
4
5
|
*/
|
|
@@ -22,7 +23,7 @@ export const getContentStylesheetLink = (layoutData, sitecoreEdgeContextId, site
|
|
|
22
23
|
rel: 'stylesheet',
|
|
23
24
|
};
|
|
24
25
|
};
|
|
25
|
-
export const getContentStylesheetUrl = (sitecoreEdgeContextId, sitecoreEdgeUrl = SITECORE_EDGE_URL_DEFAULT) => `${sitecoreEdgeUrl}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`;
|
|
26
|
+
export const getContentStylesheetUrl = (sitecoreEdgeContextId, sitecoreEdgeUrl = SITECORE_EDGE_URL_DEFAULT) => `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/pages/styles/content-styles.css?sitecoreContextId=${sitecoreEdgeContextId}`;
|
|
26
27
|
export const traversePlaceholder = (components, config) => {
|
|
27
28
|
if (config.loadStyles)
|
|
28
29
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getFieldValue } from '.';
|
|
2
2
|
import { SITECORE_EDGE_URL_DEFAULT } from '../constants';
|
|
3
|
+
import { normalizeUrl } from '../utils/normalize-url';
|
|
3
4
|
/**
|
|
4
5
|
* Pattern for library ids
|
|
5
6
|
* @example -library--foo
|
|
@@ -23,7 +24,7 @@ export function getDesignLibraryStylesheetLinks(layoutData, sitecoreEdgeContextI
|
|
|
23
24
|
}));
|
|
24
25
|
}
|
|
25
26
|
export const getStylesheetUrl = (id, sitecoreEdgeContextId, sitecoreEdgeUrl = SITECORE_EDGE_URL_DEFAULT) => {
|
|
26
|
-
return `${sitecoreEdgeUrl}/v1/files/components/styles/${id}.css?sitecoreContextId=${sitecoreEdgeContextId}`;
|
|
27
|
+
return `${normalizeUrl(sitecoreEdgeUrl)}/v1/files/components/styles/${id}.css?sitecoreContextId=${sitecoreEdgeContextId}`;
|
|
27
28
|
};
|
|
28
29
|
/**
|
|
29
30
|
* Traverse placeholder and components to add library ids
|
|
@@ -27,17 +27,18 @@ export class GraphQLRobotsService {
|
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Fetch a data of robots.txt from API
|
|
30
|
+
* @param {FetchOptions} fetchOptions - The fetch options to be used for the request.
|
|
30
31
|
* @returns text of robots.txt
|
|
31
32
|
* @throws {Error} if the siteName is empty.
|
|
32
33
|
*/
|
|
33
|
-
async fetchRobots() {
|
|
34
|
+
async fetchRobots(fetchOptions) {
|
|
34
35
|
const siteName = this.options.siteName;
|
|
35
36
|
if (!siteName) {
|
|
36
37
|
throw new Error(siteNameError);
|
|
37
38
|
}
|
|
38
39
|
const robotsResult = this.graphQLClient.request(this.query, {
|
|
39
40
|
siteName,
|
|
40
|
-
});
|
|
41
|
+
}, fetchOptions);
|
|
41
42
|
try {
|
|
42
43
|
return robotsResult.then((result) => {
|
|
43
44
|
var _a, _b;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/require-jsdoc */
|
|
2
|
+
import keytar from 'keytar';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import { deleteTenantAuthInfo } from './tenant-store';
|
|
5
|
+
import { clearActiveTenant } from './tenant-state';
|
|
6
|
+
const algorithm = 'aes-256-gcm';
|
|
7
|
+
const SERVICE_NAME = 'sitecore-tools-cli';
|
|
8
|
+
/**
|
|
9
|
+
* Encrypts plaintext using AES-256-GCM for a given tenant.
|
|
10
|
+
* @param {string} plaintext
|
|
11
|
+
* @param {string} tenantId
|
|
12
|
+
*/
|
|
13
|
+
export let encryptData = _encryptData;
|
|
14
|
+
/**
|
|
15
|
+
* Decrypts encrypted payload using AES-256-GCM for a specific tenant.
|
|
16
|
+
* If key is corrupted or invalid, optionally clears both key and tenant data.
|
|
17
|
+
* @param {EncryptedPayload} payload
|
|
18
|
+
* @param {string} tenantId
|
|
19
|
+
* @param {string} cleanupOnFailure
|
|
20
|
+
*/
|
|
21
|
+
export let decryptData = _decryptData;
|
|
22
|
+
/**
|
|
23
|
+
* Deletes the encryption key for a tenant (useful for cleanup).
|
|
24
|
+
* @param {string} tenantId
|
|
25
|
+
*/
|
|
26
|
+
export let deleteKey = _deleteKey;
|
|
27
|
+
// mock setup for unit tests to make sinon happy and mock-able with esbuild/tsx
|
|
28
|
+
// https://sinonjs.org/how-to/typescript-swc/
|
|
29
|
+
// This, plus the `_` names make the exports writable for sinon
|
|
30
|
+
export const unitMocks = {
|
|
31
|
+
set encryptData(mockImplementation) {
|
|
32
|
+
encryptData = mockImplementation;
|
|
33
|
+
},
|
|
34
|
+
get encryptData() {
|
|
35
|
+
return _encryptData;
|
|
36
|
+
},
|
|
37
|
+
set decryptData(mockImplementation) {
|
|
38
|
+
decryptData = mockImplementation;
|
|
39
|
+
},
|
|
40
|
+
get decryptData() {
|
|
41
|
+
return _decryptData;
|
|
42
|
+
},
|
|
43
|
+
set deleteKey(mockImplementation) {
|
|
44
|
+
deleteKey = mockImplementation;
|
|
45
|
+
},
|
|
46
|
+
get deleteKey() {
|
|
47
|
+
return _deleteKey;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Generates or retrieves a 32-byte AES key for a specific tenant.
|
|
52
|
+
* @param {string} tenantId
|
|
53
|
+
*/
|
|
54
|
+
export async function getKey(tenantId) {
|
|
55
|
+
const account = `encryptionKey-${tenantId}`;
|
|
56
|
+
const key = await keytar.getPassword(SERVICE_NAME, account);
|
|
57
|
+
if (!key) {
|
|
58
|
+
const keyBuffer = crypto.randomBytes(32);
|
|
59
|
+
await keytar.setPassword(SERVICE_NAME, account, keyBuffer.toString('base64'));
|
|
60
|
+
return keyBuffer;
|
|
61
|
+
}
|
|
62
|
+
return Buffer.from(key, 'base64');
|
|
63
|
+
}
|
|
64
|
+
async function _encryptData(plaintext, tenantId) {
|
|
65
|
+
const key = await getKey(tenantId);
|
|
66
|
+
const iv = crypto.randomBytes(12);
|
|
67
|
+
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
|
68
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
|
|
69
|
+
encrypted += cipher.final('base64');
|
|
70
|
+
const authTag = cipher.getAuthTag().toString('base64');
|
|
71
|
+
return {
|
|
72
|
+
iv: iv.toString('base64'),
|
|
73
|
+
authTag,
|
|
74
|
+
encryptedData: encrypted,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function _decryptData(payload, tenantId, cleanupOnFailure = true) {
|
|
78
|
+
try {
|
|
79
|
+
const key = await getKey(tenantId);
|
|
80
|
+
const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(payload.iv, 'base64'));
|
|
81
|
+
decipher.setAuthTag(Buffer.from(payload.authTag, 'base64'));
|
|
82
|
+
let decrypted = decipher.update(payload.encryptedData, 'base64', 'utf8');
|
|
83
|
+
decrypted += decipher.final('utf8');
|
|
84
|
+
return decrypted;
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error(`\nFailed to decrypt data for tenant '${tenantId}':`, err);
|
|
88
|
+
if (cleanupOnFailure) {
|
|
89
|
+
console.warn(`\nCleaning up key and auth data for corrupted tenant '${tenantId}'...`);
|
|
90
|
+
await deleteTenantAuthInfo(tenantId);
|
|
91
|
+
await deleteKey(`encryptionKey-${tenantId}`);
|
|
92
|
+
clearActiveTenant();
|
|
93
|
+
console.warn(`\nCleanup completed for tenant '${tenantId}'.`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function _deleteKey(tenantId) {
|
|
100
|
+
await keytar.deletePassword(SERVICE_NAME, `encryptionKey-${tenantId}`);
|
|
101
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/require-jsdoc */
|
|
2
|
+
export const unitMocks = {
|
|
3
|
+
set sendPostRequest(mockImplementation) {
|
|
4
|
+
sendPostRequest = mockImplementation;
|
|
5
|
+
},
|
|
6
|
+
get sendPostRequest() {
|
|
7
|
+
return _sendPostRequest;
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Performs a POST request with application/x-www-form-urlencoded headers.
|
|
12
|
+
* @param {string} url - The endpoint to post to.
|
|
13
|
+
* @param { URLSearchParams} params - A URLSearchParams instance representing the body.
|
|
14
|
+
* @returns Parsed JSON response.
|
|
15
|
+
* @throws Error if response is not OK.
|
|
16
|
+
*/
|
|
17
|
+
export let sendPostRequest = _sendPostRequest;
|
|
18
|
+
async function _sendPostRequest(url, params, throwOnError = true) {
|
|
19
|
+
const response = await fetch(url, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
23
|
+
},
|
|
24
|
+
body: params.toString(),
|
|
25
|
+
});
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
if (throwOnError && !response.ok) {
|
|
28
|
+
throw new Error(data.error_description || data.error || 'Unknown error occurred');
|
|
29
|
+
}
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { decodeJwtPayload } from './tenant-store';
|
|
2
|
+
import { sendPostRequest } from './fetcher';
|
|
3
|
+
import { DEFAULT_SITECORE_AUTH_DOMAIN, DEFAULT_SITECORE_AUTH_AUDIENCE, DEFAULT_SITECORE_AUTH_BASE_URL, DEVICE_GRANT_TYPE, CLIENT_GRANT_TYPE, SCOPE, TIMEOUT, DEFAULT_INTERVAL, } from '../../constants';
|
|
4
|
+
/**
|
|
5
|
+
* Performs the OAuth 2.0 client credentials flow to obtain a JWT access token
|
|
6
|
+
* from the Sitecore Identity Provider using the provided client credentials.
|
|
7
|
+
* @param {TenantArgs} params - Parameters including clientId, clientSecret, organizationId, tenantId, audience, authority, and baseUrl.
|
|
8
|
+
* @returns A Promise that resolves to the access token response (including access token, token type, expiry, etc.)
|
|
9
|
+
* @throws Will log and exit the process if the request fails or returns a non-OK status
|
|
10
|
+
*/
|
|
11
|
+
export let clientCredentialsFlow = _clientCredentialsFlow;
|
|
12
|
+
/**
|
|
13
|
+
* Initiates the OAuth 2.0 Device Authorization flow by requesting a device and user code.
|
|
14
|
+
* This flow is typically used by devices or CLI apps that cannot input credentials directly.
|
|
15
|
+
* @param {DeviceAuthRequest} params - Parameters including clientId, audience, authority, and baseUrl.
|
|
16
|
+
* @returns {Promise<DeviceAuthResponse>} A promise resolving to device authorization metadata needed for polling.
|
|
17
|
+
* @throws {Error} If the device authorization request fails or returns an error response.
|
|
18
|
+
*/
|
|
19
|
+
export let startDeviceAuthFlow = _startDeviceAuthFlow;
|
|
20
|
+
/**
|
|
21
|
+
* Polls the OAuth 2.0 device token endpoint to retrieve the access token once the user has authorized the device.
|
|
22
|
+
* This is typically used to continue the device authorization process after a user enters a code on a browser.
|
|
23
|
+
* @param {DeviceTokenPollRequest} params - Parameters for polling including clientId, deviceCode, interval, and authority.
|
|
24
|
+
* @returns {Promise<any>} A promise resolving to the device token response including access token and refresh token.
|
|
25
|
+
* @throws {Error} If polling fails or exceeds the timeout period.
|
|
26
|
+
*/
|
|
27
|
+
export let pollForDeviceToken = _pollForDeviceToken;
|
|
28
|
+
// mock setup for unit tests to make sinon happy and mock-able with esbuild/tsx
|
|
29
|
+
// https://sinonjs.org/how-to/typescript-swc/
|
|
30
|
+
// This, plus the `_` names make the exports writable for sinon
|
|
31
|
+
export const unitMocks = {
|
|
32
|
+
set clientCredentialsFlow(mockImplementation) {
|
|
33
|
+
clientCredentialsFlow = mockImplementation;
|
|
34
|
+
},
|
|
35
|
+
get clientCredentialsFlow() {
|
|
36
|
+
return _clientCredentialsFlow;
|
|
37
|
+
},
|
|
38
|
+
set startDeviceAuthFlow(mockImplementation) {
|
|
39
|
+
startDeviceAuthFlow = mockImplementation;
|
|
40
|
+
},
|
|
41
|
+
get startDeviceAuthFlow() {
|
|
42
|
+
return _startDeviceAuthFlow;
|
|
43
|
+
},
|
|
44
|
+
set pollForDeviceToken(mockImplementation) {
|
|
45
|
+
pollForDeviceToken = mockImplementation;
|
|
46
|
+
},
|
|
47
|
+
get pollForDeviceToken() {
|
|
48
|
+
return _pollForDeviceToken;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
async function _clientCredentialsFlow({ clientId, clientSecret, organizationId, tenantId, audience = DEFAULT_SITECORE_AUTH_AUDIENCE, authority = DEFAULT_SITECORE_AUTH_DOMAIN, baseUrl = DEFAULT_SITECORE_AUTH_BASE_URL, }) {
|
|
52
|
+
const params = new URLSearchParams({
|
|
53
|
+
client_id: clientId,
|
|
54
|
+
client_secret: clientSecret !== null && clientSecret !== void 0 ? clientSecret : '',
|
|
55
|
+
organization_id: organizationId !== null && organizationId !== void 0 ? organizationId : '',
|
|
56
|
+
tenant_id: tenantId !== null && tenantId !== void 0 ? tenantId : '',
|
|
57
|
+
audience,
|
|
58
|
+
grant_type: CLIENT_GRANT_TYPE,
|
|
59
|
+
baseUrl: baseUrl !== null && baseUrl !== void 0 ? baseUrl : '',
|
|
60
|
+
});
|
|
61
|
+
const url = `${authority}/oauth/token`;
|
|
62
|
+
const data = await sendPostRequest(url, params);
|
|
63
|
+
const decodedPayload = decodeJwtPayload(data.access_token) || {};
|
|
64
|
+
if (!(decodedPayload === null || decodedPayload === void 0 ? void 0 : decodedPayload.tokenTenantId) || !decodedPayload.tokenOrgId) {
|
|
65
|
+
throw new Error('\n Token is missing required claims tenant_id or org_id.');
|
|
66
|
+
}
|
|
67
|
+
const { tokenTenantId, tokenOrgId, tokenTenantName } = decodedPayload;
|
|
68
|
+
if (tenantId && tenantId !== tokenTenantId) {
|
|
69
|
+
throw new Error('\n Mismatch: Provided tenant ID does not match claims tenant ID.');
|
|
70
|
+
}
|
|
71
|
+
if (organizationId && organizationId !== tokenOrgId) {
|
|
72
|
+
throw new Error('\n Mismatch: Provided organization ID does not match claims organization ID.');
|
|
73
|
+
}
|
|
74
|
+
return { data, tokenOrgId, tokenTenantId, tokenTenantName, accessToken: data.access_token };
|
|
75
|
+
}
|
|
76
|
+
export async function _startDeviceAuthFlow({ clientId, audience, authority, baseUrl, }) {
|
|
77
|
+
const params = new URLSearchParams({
|
|
78
|
+
client_id: clientId,
|
|
79
|
+
scope: SCOPE,
|
|
80
|
+
audience,
|
|
81
|
+
baseUrl,
|
|
82
|
+
});
|
|
83
|
+
const url = `${authority}/oauth/device/code`;
|
|
84
|
+
const responseBody = (await sendPostRequest(url, params));
|
|
85
|
+
return responseBody;
|
|
86
|
+
}
|
|
87
|
+
export async function _pollForDeviceToken({ clientId, device_code, interval = DEFAULT_INTERVAL, authority = DEFAULT_SITECORE_AUTH_DOMAIN, }) {
|
|
88
|
+
const startTime = Date.now();
|
|
89
|
+
while (Date.now() - startTime < TIMEOUT * 1000) {
|
|
90
|
+
const params = new URLSearchParams({
|
|
91
|
+
grant_type: DEVICE_GRANT_TYPE,
|
|
92
|
+
device_code,
|
|
93
|
+
client_id: clientId,
|
|
94
|
+
});
|
|
95
|
+
const url = `${authority}/oauth/token`;
|
|
96
|
+
const responseBody = await sendPostRequest(url, params, false);
|
|
97
|
+
if ('error' in responseBody) {
|
|
98
|
+
switch (responseBody.error) {
|
|
99
|
+
case 'authorization_pending':
|
|
100
|
+
console.log('\n ⌛ Waiting for user authorization...');
|
|
101
|
+
break;
|
|
102
|
+
case 'slow_down':
|
|
103
|
+
console.log('🐢 Slowing down polling interval...');
|
|
104
|
+
interval += 5;
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(responseBody.error_description ||
|
|
108
|
+
responseBody.error ||
|
|
109
|
+
'Unknown error during device token polling.');
|
|
110
|
+
}
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
return responseBody;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
throw new Error('⏳ Timeout: User did not complete authorization in time.');
|
|
118
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { clientCredentialsFlow, startDeviceAuthFlow, pollForDeviceToken } from './flow';
|
|
2
|
+
export { renewClientToken, validateAndRenewAuthIfExpired, validateAuthInfo, getRefreshAccessToken, } from './renewal';
|
|
3
|
+
export { getActiveTenant, setActiveTenant, clearActiveTenant } from './tenant-state';
|
|
4
|
+
export { writeTenantAuthInfo, readTenantAuthInfo, deleteTenantAuthInfo, readTenantInfo, getAllTenantsInfo, writeTenantInfo, } from './tenant-store';
|
|
5
|
+
export { encryptData, decryptData, deleteKey } from './encryption';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|