@adobe/spacecat-shared-tokowaka-client 1.4.5 → 1.4.6

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [@adobe/spacecat-shared-tokowaka-client-v1.4.6](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.4.5...@adobe/spacecat-shared-tokowaka-client-v1.4.6) (2026-01-13)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add api to create/update s3 edge config ([#1260](https://github.com/adobe/spacecat-shared/issues/1260)) ([b5cfff2](https://github.com/adobe/spacecat-shared/commit/b5cfff29cdb6fa29e1b440f86eb86cf8dd471e43))
7
+
1
8
  # [@adobe/spacecat-shared-tokowaka-client-v1.4.5](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.4.4...@adobe/spacecat-shared-tokowaka-client-v1.4.5) (2026-01-07)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tokowaka-client",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "description": "Tokowaka Client for SpaceCat - Edge optimization config management",
5
5
  "type": "module",
6
6
  "engines": {
@@ -38,7 +38,8 @@
38
38
  "@aws-sdk/client-cloudfront": "3.940.0",
39
39
  "@aws-sdk/client-s3": "3.940.0",
40
40
  "mdast-util-from-markdown": "2.0.2",
41
- "mdast-util-to-hast": "12.3.0"
41
+ "mdast-util-to-hast": "12.3.0",
42
+ "uuid": "11.0.5"
42
43
  },
43
44
  "devDependencies": {
44
45
  "aws-sdk-client-mock": "4.1.0",
package/src/index.js CHANGED
@@ -10,12 +10,14 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import crypto from 'crypto';
13
14
  import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
14
15
  import { hasText, isNonEmptyObject } from '@adobe/spacecat-shared-utils';
16
+ import { v4 as uuidv4 } from 'uuid';
15
17
  import MapperRegistry from './mappers/mapper-registry.js';
16
18
  import CdnClientRegistry from './cdn/cdn-client-registry.js';
17
19
  import { mergePatches } from './utils/patch-utils.js';
18
- import { getTokowakaConfigS3Path, getTokowakaMetaconfigS3Path } from './utils/s3-utils.js';
20
+ import { getTokowakaConfigS3Path, getTokowakaMetaconfigS3Path, getHostName } from './utils/s3-utils.js';
19
21
  import { groupSuggestionsByUrlPath, filterEligibleSuggestions } from './utils/suggestion-utils.js';
20
22
  import { getEffectiveBaseURL } from './utils/site-utils.js';
21
23
  import { fetchHtmlWithWarmup, calculateForwardedHost } from './utils/custom-html-utils.js';
@@ -219,6 +221,56 @@ class TokowakaClient {
219
221
  }
220
222
  }
221
223
 
224
+ /**
225
+ * Generates an API key for Tokowaka based on domain
226
+ * @param {string} domain - Domain name (e.g., 'example.com')
227
+ * @returns {string} - Base64 URL-encoded API key
228
+ * @private
229
+ */
230
+ /* eslint-disable class-methods-use-this */
231
+ #generateApiKey(normalizedHostName) {
232
+ const uuid = uuidv4();
233
+ return crypto
234
+ .createHash('sha256')
235
+ .update(`${uuid}${normalizedHostName}`)
236
+ .digest('base64url');
237
+ }
238
+
239
+ /**
240
+ * Creates and uploads domain-level metaconfig to S3
241
+ * Generates a new API key and creates the metaconfig structure
242
+ * @param {string} url - Full URL (used to extract domain)
243
+ * @param {string} siteId - Site ID
244
+ * @param {Object} options - Optional configuration
245
+ * @param {boolean} options.tokowakaEnabled - Whether to enable Tokowaka (default: true)
246
+ * @returns {Promise<Object>} - Object with s3Path and metaconfig
247
+ */
248
+ async createMetaconfig(url, siteId, options = {}) {
249
+ if (!hasText(url)) {
250
+ throw this.#createError('URL is required', HTTP_BAD_REQUEST);
251
+ }
252
+
253
+ if (!hasText(siteId)) {
254
+ throw this.#createError('Site ID is required', HTTP_BAD_REQUEST);
255
+ }
256
+
257
+ const normalizedHostName = getHostName(url, this.log);
258
+ const apiKey = this.#generateApiKey(normalizedHostName);
259
+
260
+ const metaconfig = {
261
+ siteId,
262
+ apiKeys: [apiKey],
263
+ tokowakaEnabled: options.tokowakaEnabled ?? true,
264
+ enhancements: false,
265
+ };
266
+
267
+ const s3Path = await this.uploadMetaconfig(url, metaconfig);
268
+
269
+ this.log.info(`Created new Tokowaka metaconfig for ${normalizedHostName} at ${s3Path}`);
270
+
271
+ return metaconfig;
272
+ }
273
+
222
274
  /**
223
275
  * Uploads domain-level metaconfig to S3
224
276
  * @param {string} url - Full URL (used to extract domain)
@@ -35,10 +35,17 @@ export function normalizePath(pathname) {
35
35
  * @returns {string} - Normalized hostname
36
36
  * @throws {Error} - If hostname extraction fails
37
37
  */
38
- export function getHostName(url, logger) {
38
+ export function getHostName(url, logger = console) {
39
39
  try {
40
- const finalHostname = url.hostname.replace(/^www\./, '');
41
- return finalHostname;
40
+ let urlObj;
41
+ if (url instanceof URL) {
42
+ urlObj = url;
43
+ } else if (typeof url === 'string') {
44
+ urlObj = new URL(url);
45
+ } else {
46
+ throw new TypeError('Input must be a URL or a string');
47
+ }
48
+ return urlObj.hostname.replace(/^www\./, '');
42
49
  } catch (error) {
43
50
  logger.error(`Error extracting host name: ${error.message}`);
44
51
  throw new Error(`Error extracting host name: ${url.toString()}`);
@@ -456,6 +456,92 @@ describe('TokowakaClient', () => {
456
456
  });
457
457
  });
458
458
 
459
+ describe('createMetaconfig', () => {
460
+ it('should create metaconfig with generated API key and default options', async () => {
461
+ const siteId = 'site-123';
462
+ const url = 'https://www.example.com/page1';
463
+
464
+ const result = await client.createMetaconfig(url, siteId);
465
+
466
+ expect(result).to.have.property('siteId', siteId);
467
+ expect(result).to.have.property('apiKeys');
468
+ expect(result.apiKeys).to.be.an('array').with.lengthOf(1);
469
+ expect(result.apiKeys[0]).to.be.a('string');
470
+ expect(result).to.have.property('tokowakaEnabled', true);
471
+ expect(result).to.have.property('enhancements', false);
472
+
473
+ // Verify uploadMetaconfig was called with correct metaconfig
474
+ expect(s3Client.send).to.have.been.calledOnce;
475
+ const command = s3Client.send.firstCall.args[0];
476
+ expect(command.input.Bucket).to.equal('test-bucket');
477
+ expect(command.input.Key).to.equal('opportunities/example.com/config');
478
+ });
479
+
480
+ it('should create metaconfig with tokowakaEnabled set to false', async () => {
481
+ const siteId = 'site-123';
482
+ const url = 'https://example.com';
483
+
484
+ const result = await client.createMetaconfig(url, siteId, { tokowakaEnabled: false });
485
+
486
+ expect(result).to.have.property('tokowakaEnabled', false);
487
+ expect(result).to.have.property('enhancements', false);
488
+ expect(result.apiKeys).to.have.lengthOf(1);
489
+ });
490
+
491
+ it('should create metaconfig with tokowakaEnabled set to true explicitly', async () => {
492
+ const siteId = 'site-123';
493
+ const url = 'https://example.com';
494
+
495
+ const result = await client.createMetaconfig(url, siteId, { tokowakaEnabled: true });
496
+
497
+ expect(result).to.have.property('tokowakaEnabled', true);
498
+ expect(result).to.have.property('enhancements', false);
499
+ });
500
+
501
+ it('should throw error if URL is missing', async () => {
502
+ try {
503
+ await client.createMetaconfig('', 'site-123');
504
+ expect.fail('Should have thrown error');
505
+ } catch (error) {
506
+ expect(error.message).to.include('URL is required');
507
+ expect(error.status).to.equal(400);
508
+ }
509
+ });
510
+
511
+ it('should throw error if siteId is missing', async () => {
512
+ try {
513
+ await client.createMetaconfig('https://example.com', '');
514
+ expect.fail('Should have thrown error');
515
+ } catch (error) {
516
+ expect(error.message).to.include('Site ID is required');
517
+ expect(error.status).to.equal(400);
518
+ }
519
+ });
520
+
521
+ it('should handle S3 upload failure', async () => {
522
+ const s3Error = new Error('S3 network error');
523
+ s3Client.send.rejects(s3Error);
524
+
525
+ try {
526
+ await client.createMetaconfig('https://example.com', 'site-123');
527
+ expect.fail('Should have thrown error');
528
+ } catch (error) {
529
+ expect(error.message).to.include('S3 upload failed');
530
+ expect(error.status).to.equal(500);
531
+ }
532
+ });
533
+
534
+ it('should strip www. from domain in metaconfig path', async () => {
535
+ const siteId = 'site-123';
536
+ const url = 'https://www.example.com/some/path';
537
+
538
+ await client.createMetaconfig(url, siteId);
539
+
540
+ const command = s3Client.send.firstCall.args[0];
541
+ expect(command.input.Key).to.equal('opportunities/example.com/config');
542
+ });
543
+ });
544
+
459
545
  describe('uploadConfig', () => {
460
546
  it('should upload config to S3', async () => {
461
547
  const config = {