@sitecore-content-sdk/core 0.2.0-beta.14 → 0.2.0-beta.16
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/dist/cjs/constants.js +2 -1
- package/dist/cjs/tools/auth/encryption.js +141 -0
- package/dist/cjs/tools/auth/flow.js +19 -25
- package/dist/cjs/tools/auth/index.js +6 -2
- package/dist/cjs/tools/auth/renewal.js +4 -2
- package/dist/cjs/tools/auth/tenant-store.js +14 -7
- package/dist/esm/constants.js +1 -0
- package/dist/esm/tools/auth/encryption.js +101 -0
- package/dist/esm/tools/auth/flow.js +19 -25
- package/dist/esm/tools/auth/index.js +2 -1
- package/dist/esm/tools/auth/renewal.js +3 -1
- package/dist/esm/tools/auth/tenant-store.js +11 -4
- package/package.json +3 -2
- package/types/constants.d.ts +1 -0
- package/types/tools/auth/encryption.d.ts +34 -0
- package/types/tools/auth/index.d.ts +2 -1
- package/types/tools/auth/models.d.ts +11 -0
- package/types/tools/auth/renewal.d.ts +1 -1
package/dist/cjs/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_SITECORE_AUTH_BASE_URL = exports.DEFAULT_SITECORE_AUTH_AUDIENCE = exports.DEFAULT_SITECORE_AUTH_DOMAIN = exports.HIDDEN_RENDERING_NAME = exports.SITECORE_EDGE_URL_DEFAULT = exports.siteNameError = exports.SitecoreTemplateId = void 0;
|
|
3
|
+
exports.DEFAULT_SITECORE_AUTH_BASE_URL = exports.DEFAULT_SITECORE_AUTH_AUDIENCE = exports.DEFAULT_SITECORE_AUTH_DOMAIN = exports.CLAIMS = exports.HIDDEN_RENDERING_NAME = exports.SITECORE_EDGE_URL_DEFAULT = exports.siteNameError = exports.SitecoreTemplateId = void 0;
|
|
4
4
|
var SitecoreTemplateId;
|
|
5
5
|
(function (SitecoreTemplateId) {
|
|
6
6
|
// /sitecore/templates/Foundation/JavaScript Services/App
|
|
@@ -11,6 +11,7 @@ var SitecoreTemplateId;
|
|
|
11
11
|
exports.siteNameError = 'The siteName cannot be empty';
|
|
12
12
|
exports.SITECORE_EDGE_URL_DEFAULT = 'https://edge-platform.sitecorecloud.io';
|
|
13
13
|
exports.HIDDEN_RENDERING_NAME = 'Hidden Rendering';
|
|
14
|
+
exports.CLAIMS = 'https://auth.sitecorecloud.io/claims';
|
|
14
15
|
exports.DEFAULT_SITECORE_AUTH_DOMAIN = 'https://auth.sitecorecloud.io';
|
|
15
16
|
exports.DEFAULT_SITECORE_AUTH_AUDIENCE = 'https://api.sitecorecloud.io';
|
|
16
17
|
exports.DEFAULT_SITECORE_AUTH_BASE_URL = 'https://edge-platform.sitecorecloud.io/cs/api';
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.unitMocks = exports.deleteKey = exports.decryptData = exports.encryptData = void 0;
|
|
40
|
+
exports.getKey = getKey;
|
|
41
|
+
/* eslint-disable jsdoc/require-jsdoc */
|
|
42
|
+
const keytar_1 = __importDefault(require("keytar"));
|
|
43
|
+
const crypto = __importStar(require("crypto"));
|
|
44
|
+
const tenant_store_1 = require("./tenant-store");
|
|
45
|
+
const tenant_state_1 = require("./tenant-state");
|
|
46
|
+
const algorithm = 'aes-256-gcm';
|
|
47
|
+
const SERVICE_NAME = 'sitecore-tools-cli';
|
|
48
|
+
/**
|
|
49
|
+
* Encrypts plaintext using AES-256-GCM for a given tenant.
|
|
50
|
+
* @param {string} plaintext
|
|
51
|
+
* @param {string} tenantId
|
|
52
|
+
*/
|
|
53
|
+
exports.encryptData = _encryptData;
|
|
54
|
+
/**
|
|
55
|
+
* Decrypts encrypted payload using AES-256-GCM for a specific tenant.
|
|
56
|
+
* If key is corrupted or invalid, optionally clears both key and tenant data.
|
|
57
|
+
* @param {EncryptedPayload} payload
|
|
58
|
+
* @param {string} tenantId
|
|
59
|
+
* @param {string} cleanupOnFailure
|
|
60
|
+
*/
|
|
61
|
+
exports.decryptData = _decryptData;
|
|
62
|
+
/**
|
|
63
|
+
* Deletes the encryption key for a tenant (useful for cleanup).
|
|
64
|
+
* @param {string} tenantId
|
|
65
|
+
*/
|
|
66
|
+
exports.deleteKey = _deleteKey;
|
|
67
|
+
// mock setup for unit tests to make sinon happy and mock-able with esbuild/tsx
|
|
68
|
+
// https://sinonjs.org/how-to/typescript-swc/
|
|
69
|
+
// This, plus the `_` names make the exports writable for sinon
|
|
70
|
+
exports.unitMocks = {
|
|
71
|
+
set encryptData(mockImplementation) {
|
|
72
|
+
exports.encryptData = mockImplementation;
|
|
73
|
+
},
|
|
74
|
+
get encryptData() {
|
|
75
|
+
return _encryptData;
|
|
76
|
+
},
|
|
77
|
+
set decryptData(mockImplementation) {
|
|
78
|
+
exports.decryptData = mockImplementation;
|
|
79
|
+
},
|
|
80
|
+
get decryptData() {
|
|
81
|
+
return _decryptData;
|
|
82
|
+
},
|
|
83
|
+
set deleteKey(mockImplementation) {
|
|
84
|
+
exports.deleteKey = mockImplementation;
|
|
85
|
+
},
|
|
86
|
+
get deleteKey() {
|
|
87
|
+
return _deleteKey;
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Generates or retrieves a 32-byte AES key for a specific tenant.
|
|
92
|
+
* @param {string} tenantId
|
|
93
|
+
*/
|
|
94
|
+
async function getKey(tenantId) {
|
|
95
|
+
const account = `encryptionKey-${tenantId}`;
|
|
96
|
+
const key = await keytar_1.default.getPassword(SERVICE_NAME, account);
|
|
97
|
+
if (!key) {
|
|
98
|
+
const keyBuffer = crypto.randomBytes(32);
|
|
99
|
+
await keytar_1.default.setPassword(SERVICE_NAME, account, keyBuffer.toString('base64'));
|
|
100
|
+
return keyBuffer;
|
|
101
|
+
}
|
|
102
|
+
return Buffer.from(key, 'base64');
|
|
103
|
+
}
|
|
104
|
+
async function _encryptData(plaintext, tenantId) {
|
|
105
|
+
const key = await getKey(tenantId);
|
|
106
|
+
const iv = crypto.randomBytes(12);
|
|
107
|
+
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
|
108
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
|
|
109
|
+
encrypted += cipher.final('base64');
|
|
110
|
+
const authTag = cipher.getAuthTag().toString('base64');
|
|
111
|
+
return {
|
|
112
|
+
iv: iv.toString('base64'),
|
|
113
|
+
authTag,
|
|
114
|
+
encryptedData: encrypted,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async function _decryptData(payload, tenantId, cleanupOnFailure = true) {
|
|
118
|
+
try {
|
|
119
|
+
const key = await getKey(tenantId);
|
|
120
|
+
const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(payload.iv, 'base64'));
|
|
121
|
+
decipher.setAuthTag(Buffer.from(payload.authTag, 'base64'));
|
|
122
|
+
let decrypted = decipher.update(payload.encryptedData, 'base64', 'utf8');
|
|
123
|
+
decrypted += decipher.final('utf8');
|
|
124
|
+
return decrypted;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error(`\nFailed to decrypt data for tenant '${tenantId}':`, err);
|
|
128
|
+
if (cleanupOnFailure) {
|
|
129
|
+
console.warn(`\nCleaning up key and auth data for corrupted tenant '${tenantId}'...`);
|
|
130
|
+
await (0, tenant_store_1.deleteTenantAuthInfo)(tenantId);
|
|
131
|
+
await (0, exports.deleteKey)(`encryptionKey-${tenantId}`);
|
|
132
|
+
(0, tenant_state_1.clearActiveTenant)();
|
|
133
|
+
console.warn(`\nCleanup completed for tenant '${tenantId}'.`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function _deleteKey(tenantId) {
|
|
140
|
+
await keytar_1.default.deletePassword(SERVICE_NAME, `encryptionKey-${tenantId}`);
|
|
141
|
+
}
|
|
@@ -40,31 +40,25 @@ async function _clientCredentialsFlow({ clientId, clientSecret, organizationId,
|
|
|
40
40
|
grant_type: GRANT_TYPE,
|
|
41
41
|
baseUrl: baseUrl !== null && baseUrl !== void 0 ? baseUrl : '',
|
|
42
42
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
throw new Error('\n Mismatch: Provided tenant ID does not match claims tenant ID.');
|
|
60
|
-
}
|
|
61
|
-
if (organizationId && organizationId !== tokenOrgId) {
|
|
62
|
-
throw new Error('\n Mismatch: Provided organization ID does not match claims organization ID.');
|
|
63
|
-
}
|
|
64
|
-
return { data, tokenOrgId, tokenTenantId, tokenTenantName, accessToken: data.access_token };
|
|
43
|
+
const response = await fetch(`${authority}/oauth/token`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
46
|
+
body: params.toString(),
|
|
47
|
+
});
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(data.error_description || data.error || 'Error during client credentials flow');
|
|
51
|
+
}
|
|
52
|
+
const decodedPayload = (0, tenant_store_1.decodeJwtPayload)(data.access_token) || {};
|
|
53
|
+
if (!(decodedPayload === null || decodedPayload === void 0 ? void 0 : decodedPayload.tokenTenantId) || !decodedPayload.tokenOrgId) {
|
|
54
|
+
throw new Error('\n Token is missing required claims tenant_id or org_id.');
|
|
55
|
+
}
|
|
56
|
+
const { tokenTenantId, tokenOrgId, tokenTenantName } = decodedPayload;
|
|
57
|
+
if (tenantId && tenantId !== tokenTenantId) {
|
|
58
|
+
throw new Error('\n Mismatch: Provided tenant ID does not match claims tenant ID.');
|
|
65
59
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
throw error;
|
|
60
|
+
if (organizationId && organizationId !== tokenOrgId) {
|
|
61
|
+
throw new Error('\n Mismatch: Provided organization ID does not match claims organization ID.');
|
|
69
62
|
}
|
|
63
|
+
return { data, tokenOrgId, tokenTenantId, tokenTenantName, accessToken: data.access_token };
|
|
70
64
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.writeTenantInfo = exports.getAllTenantsInfo = exports.readTenantInfo = exports.deleteTenantAuthInfo = exports.readTenantAuthInfo = exports.writeTenantAuthInfo = exports.clearActiveTenant = exports.setActiveTenant = exports.getActiveTenant = exports.validateAuthInfo = exports.
|
|
3
|
+
exports.deleteKey = exports.decryptData = exports.encryptData = exports.writeTenantInfo = exports.getAllTenantsInfo = exports.readTenantInfo = exports.deleteTenantAuthInfo = exports.readTenantAuthInfo = exports.writeTenantAuthInfo = exports.clearActiveTenant = exports.setActiveTenant = exports.getActiveTenant = exports.validateAuthInfo = exports.validateAndRenewAuthIfExpired = exports.renewClientToken = exports.clientCredentialsFlow = void 0;
|
|
4
4
|
var flow_1 = require("./flow");
|
|
5
5
|
Object.defineProperty(exports, "clientCredentialsFlow", { enumerable: true, get: function () { return flow_1.clientCredentialsFlow; } });
|
|
6
6
|
var renewal_1 = require("./renewal");
|
|
7
7
|
Object.defineProperty(exports, "renewClientToken", { enumerable: true, get: function () { return renewal_1.renewClientToken; } });
|
|
8
|
-
Object.defineProperty(exports, "
|
|
8
|
+
Object.defineProperty(exports, "validateAndRenewAuthIfExpired", { enumerable: true, get: function () { return renewal_1.validateAndRenewAuthIfExpired; } });
|
|
9
9
|
Object.defineProperty(exports, "validateAuthInfo", { enumerable: true, get: function () { return renewal_1.validateAuthInfo; } });
|
|
10
10
|
var tenant_state_1 = require("./tenant-state");
|
|
11
11
|
Object.defineProperty(exports, "getActiveTenant", { enumerable: true, get: function () { return tenant_state_1.getActiveTenant; } });
|
|
@@ -18,3 +18,7 @@ Object.defineProperty(exports, "deleteTenantAuthInfo", { enumerable: true, get:
|
|
|
18
18
|
Object.defineProperty(exports, "readTenantInfo", { enumerable: true, get: function () { return tenant_store_1.readTenantInfo; } });
|
|
19
19
|
Object.defineProperty(exports, "getAllTenantsInfo", { enumerable: true, get: function () { return tenant_store_1.getAllTenantsInfo; } });
|
|
20
20
|
Object.defineProperty(exports, "writeTenantInfo", { enumerable: true, get: function () { return tenant_store_1.writeTenantInfo; } });
|
|
21
|
+
var encryption_1 = require("./encryption");
|
|
22
|
+
Object.defineProperty(exports, "encryptData", { enumerable: true, get: function () { return encryption_1.encryptData; } });
|
|
23
|
+
Object.defineProperty(exports, "decryptData", { enumerable: true, get: function () { return encryption_1.decryptData; } });
|
|
24
|
+
Object.defineProperty(exports, "deleteKey", { enumerable: true, get: function () { return encryption_1.deleteKey; } });
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateAuthInfo = validateAuthInfo;
|
|
4
4
|
exports.renewClientToken = renewClientToken;
|
|
5
|
-
exports.
|
|
5
|
+
exports.validateAndRenewAuthIfExpired = validateAndRenewAuthIfExpired;
|
|
6
6
|
const tenant_state_1 = require("./tenant-state");
|
|
7
7
|
const flow_1 = require("./flow");
|
|
8
8
|
const tenant_store_1 = require("./tenant-store");
|
|
9
|
+
const encryption_1 = require("./encryption");
|
|
9
10
|
/**
|
|
10
11
|
* Validates whether a given auth config is still valid (i.e., not expired).
|
|
11
12
|
* @param {TenantAuth} authInfo - The tenant auth configuration.
|
|
@@ -46,7 +47,7 @@ async function renewClientToken(authInfo, tenantInfo) {
|
|
|
46
47
|
* Ensures a valid token exists, renews it if expired.
|
|
47
48
|
* Returns tenant context if successful, otherwise null.
|
|
48
49
|
*/
|
|
49
|
-
async function
|
|
50
|
+
async function validateAndRenewAuthIfExpired() {
|
|
50
51
|
const tenantId = (0, tenant_state_1.getActiveTenant)();
|
|
51
52
|
if (!tenantId)
|
|
52
53
|
return null;
|
|
@@ -75,6 +76,7 @@ async function renewAuthIfExpired() {
|
|
|
75
76
|
console.error(`\n Failed to renew token for tenant '${tenantId}'`);
|
|
76
77
|
console.warn(`\n Cleaning up stale authentication data for tenant '${tenantId}'...`);
|
|
77
78
|
await (0, tenant_store_1.deleteTenantAuthInfo)(tenantId);
|
|
79
|
+
await (0, encryption_1.deleteKey)(tenantId);
|
|
78
80
|
(0, tenant_state_1.clearActiveTenant)();
|
|
79
81
|
console.info('\n You will need to login again to re-authenticate.');
|
|
80
82
|
process.exit(1);
|
|
@@ -39,7 +39,8 @@ exports.getTenantPath = getTenantPath;
|
|
|
39
39
|
const fs = __importStar(require("fs"));
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const os = __importStar(require("os"));
|
|
42
|
-
const
|
|
42
|
+
const encryption_1 = require("./encryption");
|
|
43
|
+
const constants_1 = require("./../../constants");
|
|
43
44
|
const rootDir = path.join(os.homedir(), '.sitecore', 'sitecore-tools');
|
|
44
45
|
/**
|
|
45
46
|
* Decodes a JWT without verifying its signature.
|
|
@@ -139,7 +140,8 @@ async function _writeTenantAuthInfo(tenantId, authInfo) {
|
|
|
139
140
|
try {
|
|
140
141
|
const dir = getTenantPath(tenantId);
|
|
141
142
|
fs.mkdirSync(dir, { recursive: true });
|
|
142
|
-
|
|
143
|
+
const encrypted = await (0, encryption_1.encryptData)(JSON.stringify(authInfo), tenantId);
|
|
144
|
+
fs.writeFileSync(path.join(dir, 'auth.json'), JSON.stringify(encrypted));
|
|
143
145
|
}
|
|
144
146
|
catch (error) {
|
|
145
147
|
console.error(`\n Failed to write auth.json for tenant '${tenantId}': ${error.message}`);
|
|
@@ -150,8 +152,13 @@ async function _readTenantAuthInfo(tenantId) {
|
|
|
150
152
|
if (!fs.existsSync(filePath))
|
|
151
153
|
return null;
|
|
152
154
|
try {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
+
const encryptedPayloadRaw = fs.readFileSync(filePath, 'utf8');
|
|
156
|
+
const encryptedPayload = JSON.parse(encryptedPayloadRaw);
|
|
157
|
+
const decryptedData = await (0, encryption_1.decryptData)(encryptedPayload, tenantId, true);
|
|
158
|
+
if (decryptedData === null) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return JSON.parse(decryptedData);
|
|
155
162
|
}
|
|
156
163
|
catch (error) {
|
|
157
164
|
console.error(`\n Failed to read auth.json for tenant '${tenantId}': ${error.message}`);
|
|
@@ -231,9 +238,9 @@ function _decodeJwtPayload(token) {
|
|
|
231
238
|
const payload = Buffer.from(base64Payload, 'base64').toString('utf-8');
|
|
232
239
|
const decoded = JSON.parse(payload);
|
|
233
240
|
return {
|
|
234
|
-
tokenTenantId: decoded === null || decoded === void 0 ? void 0 : decoded[`${CLAIMS}/tenant_id`],
|
|
235
|
-
tokenOrgId: decoded === null || decoded === void 0 ? void 0 : decoded[`${CLAIMS}/org_id`],
|
|
236
|
-
tokenTenantName: decoded === null || decoded === void 0 ? void 0 : decoded[`${CLAIMS}/tenant_name`],
|
|
241
|
+
tokenTenantId: decoded === null || decoded === void 0 ? void 0 : decoded[`${constants_1.CLAIMS}/tenant_id`],
|
|
242
|
+
tokenOrgId: decoded === null || decoded === void 0 ? void 0 : decoded[`${constants_1.CLAIMS}/org_id`],
|
|
243
|
+
tokenTenantName: decoded === null || decoded === void 0 ? void 0 : decoded[`${constants_1.CLAIMS}/tenant_name`],
|
|
237
244
|
};
|
|
238
245
|
}
|
|
239
246
|
catch (error) {
|
package/dist/esm/constants.js
CHANGED
|
@@ -8,6 +8,7 @@ export var SitecoreTemplateId;
|
|
|
8
8
|
export const siteNameError = 'The siteName cannot be empty';
|
|
9
9
|
export const SITECORE_EDGE_URL_DEFAULT = 'https://edge-platform.sitecorecloud.io';
|
|
10
10
|
export const HIDDEN_RENDERING_NAME = 'Hidden Rendering';
|
|
11
|
+
export const CLAIMS = 'https://auth.sitecorecloud.io/claims';
|
|
11
12
|
export const DEFAULT_SITECORE_AUTH_DOMAIN = 'https://auth.sitecorecloud.io';
|
|
12
13
|
export const DEFAULT_SITECORE_AUTH_AUDIENCE = 'https://api.sitecorecloud.io';
|
|
13
14
|
export const DEFAULT_SITECORE_AUTH_BASE_URL = 'https://edge-platform.sitecorecloud.io/cs/api';
|
|
@@ -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
|
+
}
|
|
@@ -37,31 +37,25 @@ async function _clientCredentialsFlow({ clientId, clientSecret, organizationId,
|
|
|
37
37
|
grant_type: GRANT_TYPE,
|
|
38
38
|
baseUrl: baseUrl !== null && baseUrl !== void 0 ? baseUrl : '',
|
|
39
39
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw new Error('\n Mismatch: Provided tenant ID does not match claims tenant ID.');
|
|
57
|
-
}
|
|
58
|
-
if (organizationId && organizationId !== tokenOrgId) {
|
|
59
|
-
throw new Error('\n Mismatch: Provided organization ID does not match claims organization ID.');
|
|
60
|
-
}
|
|
61
|
-
return { data, tokenOrgId, tokenTenantId, tokenTenantName, accessToken: data.access_token };
|
|
40
|
+
const response = await fetch(`${authority}/oauth/token`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
43
|
+
body: params.toString(),
|
|
44
|
+
});
|
|
45
|
+
const data = await response.json();
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(data.error_description || data.error || 'Error during client credentials flow');
|
|
48
|
+
}
|
|
49
|
+
const decodedPayload = decodeJwtPayload(data.access_token) || {};
|
|
50
|
+
if (!(decodedPayload === null || decodedPayload === void 0 ? void 0 : decodedPayload.tokenTenantId) || !decodedPayload.tokenOrgId) {
|
|
51
|
+
throw new Error('\n Token is missing required claims tenant_id or org_id.');
|
|
52
|
+
}
|
|
53
|
+
const { tokenTenantId, tokenOrgId, tokenTenantName } = decodedPayload;
|
|
54
|
+
if (tenantId && tenantId !== tokenTenantId) {
|
|
55
|
+
throw new Error('\n Mismatch: Provided tenant ID does not match claims tenant ID.');
|
|
62
56
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
throw error;
|
|
57
|
+
if (organizationId && organizationId !== tokenOrgId) {
|
|
58
|
+
throw new Error('\n Mismatch: Provided organization ID does not match claims organization ID.');
|
|
66
59
|
}
|
|
60
|
+
return { data, tokenOrgId, tokenTenantId, tokenTenantName, accessToken: data.access_token };
|
|
67
61
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { clientCredentialsFlow } from './flow';
|
|
2
|
-
export { renewClientToken,
|
|
2
|
+
export { renewClientToken, validateAndRenewAuthIfExpired, validateAuthInfo } from './renewal';
|
|
3
3
|
export { getActiveTenant, setActiveTenant, clearActiveTenant } from './tenant-state';
|
|
4
4
|
export { writeTenantAuthInfo, readTenantAuthInfo, deleteTenantAuthInfo, readTenantInfo, getAllTenantsInfo, writeTenantInfo, } from './tenant-store';
|
|
5
|
+
export { encryptData, decryptData, deleteKey } from './encryption';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getActiveTenant, clearActiveTenant } from './tenant-state';
|
|
2
2
|
import { clientCredentialsFlow } from './flow';
|
|
3
3
|
import { writeTenantAuthInfo, readTenantAuthInfo, deleteTenantAuthInfo, readTenantInfo, } from './tenant-store';
|
|
4
|
+
import { deleteKey } from './encryption';
|
|
4
5
|
/**
|
|
5
6
|
* Validates whether a given auth config is still valid (i.e., not expired).
|
|
6
7
|
* @param {TenantAuth} authInfo - The tenant auth configuration.
|
|
@@ -41,7 +42,7 @@ export async function renewClientToken(authInfo, tenantInfo) {
|
|
|
41
42
|
* Ensures a valid token exists, renews it if expired.
|
|
42
43
|
* Returns tenant context if successful, otherwise null.
|
|
43
44
|
*/
|
|
44
|
-
export async function
|
|
45
|
+
export async function validateAndRenewAuthIfExpired() {
|
|
45
46
|
const tenantId = getActiveTenant();
|
|
46
47
|
if (!tenantId)
|
|
47
48
|
return null;
|
|
@@ -70,6 +71,7 @@ export async function renewAuthIfExpired() {
|
|
|
70
71
|
console.error(`\n Failed to renew token for tenant '${tenantId}'`);
|
|
71
72
|
console.warn(`\n Cleaning up stale authentication data for tenant '${tenantId}'...`);
|
|
72
73
|
await deleteTenantAuthInfo(tenantId);
|
|
74
|
+
await deleteKey(tenantId);
|
|
73
75
|
clearActiveTenant();
|
|
74
76
|
console.info('\n You will need to login again to re-authenticate.');
|
|
75
77
|
process.exit(1);
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as os from 'os';
|
|
5
|
-
|
|
5
|
+
import { encryptData, decryptData } from './encryption';
|
|
6
|
+
import { CLAIMS } from './../../constants';
|
|
6
7
|
const rootDir = path.join(os.homedir(), '.sitecore', 'sitecore-tools');
|
|
7
8
|
/**
|
|
8
9
|
* Decodes a JWT without verifying its signature.
|
|
@@ -102,7 +103,8 @@ async function _writeTenantAuthInfo(tenantId, authInfo) {
|
|
|
102
103
|
try {
|
|
103
104
|
const dir = getTenantPath(tenantId);
|
|
104
105
|
fs.mkdirSync(dir, { recursive: true });
|
|
105
|
-
|
|
106
|
+
const encrypted = await encryptData(JSON.stringify(authInfo), tenantId);
|
|
107
|
+
fs.writeFileSync(path.join(dir, 'auth.json'), JSON.stringify(encrypted));
|
|
106
108
|
}
|
|
107
109
|
catch (error) {
|
|
108
110
|
console.error(`\n Failed to write auth.json for tenant '${tenantId}': ${error.message}`);
|
|
@@ -113,8 +115,13 @@ async function _readTenantAuthInfo(tenantId) {
|
|
|
113
115
|
if (!fs.existsSync(filePath))
|
|
114
116
|
return null;
|
|
115
117
|
try {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
+
const encryptedPayloadRaw = fs.readFileSync(filePath, 'utf8');
|
|
119
|
+
const encryptedPayload = JSON.parse(encryptedPayloadRaw);
|
|
120
|
+
const decryptedData = await decryptData(encryptedPayload, tenantId, true);
|
|
121
|
+
if (decryptedData === null) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return JSON.parse(decryptedData);
|
|
118
125
|
}
|
|
119
126
|
catch (error) {
|
|
120
127
|
console.error(`\n Failed to read auth.json for tenant '${tenantId}': ${error.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sitecore-content-sdk/core",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.16",
|
|
4
4
|
"main": "dist/cjs/index.js",
|
|
5
5
|
"module": "dist/esm/index.js",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -71,13 +71,14 @@
|
|
|
71
71
|
"debug": "^4.4.0",
|
|
72
72
|
"graphql": "^16.11.0",
|
|
73
73
|
"graphql-request": "^6.1.0",
|
|
74
|
+
"keytar": "^7.9.0",
|
|
74
75
|
"memory-cache": "^0.2.0",
|
|
75
76
|
"sinon-chai": "^4.0.0",
|
|
76
77
|
"url-parse": "^1.5.10"
|
|
77
78
|
},
|
|
78
79
|
"description": "",
|
|
79
80
|
"types": "types/index.d.ts",
|
|
80
|
-
"gitHead": "
|
|
81
|
+
"gitHead": "c3fdb664842dd1bf000cb6bca7c55cf6d33ff446",
|
|
81
82
|
"files": [
|
|
82
83
|
"dist",
|
|
83
84
|
"types",
|
package/types/constants.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare enum SitecoreTemplateId {
|
|
|
5
5
|
export declare const siteNameError = "The siteName cannot be empty";
|
|
6
6
|
export declare const SITECORE_EDGE_URL_DEFAULT = "https://edge-platform.sitecorecloud.io";
|
|
7
7
|
export declare const HIDDEN_RENDERING_NAME = "Hidden Rendering";
|
|
8
|
+
export declare const CLAIMS = "https://auth.sitecorecloud.io/claims";
|
|
8
9
|
export declare const DEFAULT_SITECORE_AUTH_DOMAIN = "https://auth.sitecorecloud.io";
|
|
9
10
|
export declare const DEFAULT_SITECORE_AUTH_AUDIENCE = "https://api.sitecorecloud.io";
|
|
10
11
|
export declare const DEFAULT_SITECORE_AUTH_BASE_URL = "https://edge-platform.sitecorecloud.io/cs/api";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EncryptedPayload } from './models';
|
|
2
|
+
/**
|
|
3
|
+
* Encrypts plaintext using AES-256-GCM for a given tenant.
|
|
4
|
+
* @param {string} plaintext
|
|
5
|
+
* @param {string} tenantId
|
|
6
|
+
*/
|
|
7
|
+
export declare let encryptData: typeof _encryptData;
|
|
8
|
+
/**
|
|
9
|
+
* Decrypts encrypted payload using AES-256-GCM for a specific tenant.
|
|
10
|
+
* If key is corrupted or invalid, optionally clears both key and tenant data.
|
|
11
|
+
* @param {EncryptedPayload} payload
|
|
12
|
+
* @param {string} tenantId
|
|
13
|
+
* @param {string} cleanupOnFailure
|
|
14
|
+
*/
|
|
15
|
+
export declare let decryptData: typeof _decryptData;
|
|
16
|
+
/**
|
|
17
|
+
* Deletes the encryption key for a tenant (useful for cleanup).
|
|
18
|
+
* @param {string} tenantId
|
|
19
|
+
*/
|
|
20
|
+
export declare let deleteKey: typeof _deleteKey;
|
|
21
|
+
export declare const unitMocks: {
|
|
22
|
+
encryptData: typeof _encryptData;
|
|
23
|
+
decryptData: typeof _decryptData;
|
|
24
|
+
deleteKey: typeof _deleteKey;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Generates or retrieves a 32-byte AES key for a specific tenant.
|
|
28
|
+
* @param {string} tenantId
|
|
29
|
+
*/
|
|
30
|
+
export declare function getKey(tenantId: string): Promise<Buffer>;
|
|
31
|
+
declare function _encryptData(plaintext: string, tenantId: string): Promise<EncryptedPayload>;
|
|
32
|
+
declare function _decryptData(payload: EncryptedPayload, tenantId: string, cleanupOnFailure?: boolean): Promise<string | null>;
|
|
33
|
+
declare function _deleteKey(tenantId: string): Promise<void>;
|
|
34
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { clientCredentialsFlow } from './flow';
|
|
2
|
-
export { renewClientToken,
|
|
2
|
+
export { renewClientToken, validateAndRenewAuthIfExpired, validateAuthInfo } from './renewal';
|
|
3
3
|
export { getActiveTenant, setActiveTenant, clearActiveTenant } from './tenant-state';
|
|
4
4
|
export { writeTenantAuthInfo, readTenantAuthInfo, deleteTenantAuthInfo, readTenantInfo, getAllTenantsInfo, writeTenantInfo, } from './tenant-store';
|
|
5
|
+
export { encryptData, decryptData, deleteKey } from './encryption';
|
|
@@ -92,3 +92,14 @@ export interface TenantInfo {
|
|
|
92
92
|
*/
|
|
93
93
|
baseUrl: string;
|
|
94
94
|
}
|
|
95
|
+
export type EncryptedPayload = {
|
|
96
|
+
iv: string;
|
|
97
|
+
/**
|
|
98
|
+
* Authentication tag for integrity verification
|
|
99
|
+
*/
|
|
100
|
+
authTag: string;
|
|
101
|
+
/**
|
|
102
|
+
* Base64-encoded encrypted data
|
|
103
|
+
*/
|
|
104
|
+
encryptedData: string;
|
|
105
|
+
};
|
|
@@ -17,6 +17,6 @@ export declare function renewClientToken(authInfo: TenantAuth, tenantInfo: Tenan
|
|
|
17
17
|
* Ensures a valid token exists, renews it if expired.
|
|
18
18
|
* Returns tenant context if successful, otherwise null.
|
|
19
19
|
*/
|
|
20
|
-
export declare function
|
|
20
|
+
export declare function validateAndRenewAuthIfExpired(): Promise<{
|
|
21
21
|
tenantId: string;
|
|
22
22
|
} | null>;
|