@adobe/spacecat-shared-tokowaka-client 1.5.8 → 1.6.1

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,17 @@
1
+ # [@adobe/spacecat-shared-tokowaka-client-v1.6.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.6.0...@adobe/spacecat-shared-tokowaka-client-v1.6.1) (2026-01-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * api key now optional for edge-preview ([#1299](https://github.com/adobe/spacecat-shared/issues/1299)) ([76eb046](https://github.com/adobe/spacecat-shared/commit/76eb04640dab752a83909a51827abde1e41fc57e))
7
+
8
+ # [@adobe/spacecat-shared-tokowaka-client-v1.6.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.8...@adobe/spacecat-shared-tokowaka-client-v1.6.0) (2026-01-29)
9
+
10
+
11
+ ### Features
12
+
13
+ * new flag opted added in edgeoptimizeconfig ([#1294](https://github.com/adobe/spacecat-shared/issues/1294)) ([8386e38](https://github.com/adobe/spacecat-shared/commit/8386e3805e9e67c18e4569428bef7ac5723eb991))
14
+
1
15
  # [@adobe/spacecat-shared-tokowaka-client-v1.5.8](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.7...@adobe/spacecat-shared-tokowaka-client-v1.5.8) (2026-01-27)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tokowaka-client",
3
- "version": "1.5.8",
3
+ "version": "1.6.1",
4
4
  "description": "Tokowaka Client for SpaceCat - Edge optimization config management",
5
5
  "type": "module",
6
6
  "engines": {
package/src/index.js CHANGED
@@ -290,7 +290,7 @@ class TokowakaClient {
290
290
  * @param {boolean} options.enhancements - Whether to enable enhancements (default: true)
291
291
  * @returns {Promise<Object>} - Object with s3Path and metaconfig
292
292
  */
293
- async createMetaconfig(url, siteId, options = {}) {
293
+ async createMetaconfig(url, siteId, options = {}, metadata = {}) {
294
294
  if (!hasText(url)) {
295
295
  throw this.#createError('URL is required', HTTP_BAD_REQUEST);
296
296
  }
@@ -316,8 +316,7 @@ class TokowakaClient {
316
316
  patches: {},
317
317
  };
318
318
 
319
- const s3Path = await this.uploadMetaconfig(url, metaconfig);
320
-
319
+ const s3Path = await this.uploadMetaconfig(url, metaconfig, metadata);
321
320
  this.log.info(`Created new Tokowaka metaconfig for ${normalizedHostName} at ${s3Path}`);
322
321
 
323
322
  return metaconfig;
@@ -331,7 +330,7 @@ class TokowakaClient {
331
330
  * @param {Object} options - Optional configuration
332
331
  * @returns {Promise<Object>} - Object with s3Path and metaconfig
333
332
  */
334
- async updateMetaconfig(url, siteId, options = {}) {
333
+ async updateMetaconfig(url, siteId, options = {}, metadata = {}) {
335
334
  if (!hasText(url)) {
336
335
  throw this.#createError('URL is required', HTTP_BAD_REQUEST);
337
336
  }
@@ -371,8 +370,7 @@ class TokowakaClient {
371
370
  ...(hasPrerender && { prerender }),
372
371
  };
373
372
 
374
- const s3Path = await this.uploadMetaconfig(url, metaconfig);
375
-
373
+ const s3Path = await this.uploadMetaconfig(url, metaconfig, metadata);
376
374
  this.log.info(`Updated Tokowaka metaconfig for ${normalizedHostName} at ${s3Path}`);
377
375
 
378
376
  return metaconfig;
@@ -382,9 +380,10 @@ class TokowakaClient {
382
380
  * Uploads domain-level metaconfig to S3
383
381
  * @param {string} url - Full URL (used to extract domain)
384
382
  * @param {Object} metaconfig - Metaconfig object (siteId, apiKeys, prerender)
383
+ * @param {Object} metadata - Optional S3 user-defined metadata (key-value pairs)
385
384
  * @returns {Promise<string>} - S3 key of uploaded metaconfig
386
- */
387
- async uploadMetaconfig(url, metaconfig) {
385
+ */
386
+ async uploadMetaconfig(url, metaconfig, metadata = {}) {
388
387
  if (!hasText(url)) {
389
388
  throw this.#createError('URL is required', HTTP_BAD_REQUEST);
390
389
  }
@@ -397,12 +396,19 @@ class TokowakaClient {
397
396
  const bucketName = this.deployBucketName;
398
397
 
399
398
  try {
400
- const command = new PutObjectCommand({
399
+ const putObjectParams = {
401
400
  Bucket: bucketName,
402
401
  Key: s3Path,
403
402
  Body: JSON.stringify(metaconfig, null, 2),
404
403
  ContentType: 'application/json',
405
- });
404
+ };
405
+
406
+ // Add user-defined metadata if provided
407
+ if (isNonEmptyObject(metadata)) {
408
+ putObjectParams.Metadata = metadata;
409
+ }
410
+
411
+ const command = new PutObjectCommand(putObjectParams);
406
412
 
407
413
  await this.s3Client.send(command);
408
414
  this.log.info(`Successfully uploaded metaconfig to s3://${bucketName}/${s3Path}`);
@@ -907,26 +913,13 @@ class TokowakaClient {
907
913
  }
908
914
 
909
915
  // Fetch metaconfig to get API key
910
- const metaconfig = await this.fetchMetaconfig(previewUrl);
916
+ const metaconfig = await this.fetchMetaconfig(previewUrl) || {};
911
917
 
912
- if (!metaconfig) {
913
- throw this.#createError(
914
- 'No domain-level metaconfig found. '
915
- + 'A domain-level metaconfig needs to be created first before previewing suggestions.',
916
- HTTP_INTERNAL_SERVER_ERROR,
917
- );
918
- }
919
-
920
- const { apiKeys } = metaconfig;
921
- if (!Array.isArray(apiKeys) || apiKeys.length === 0 || !hasText(apiKeys[0])) {
922
- throw this.#createError(
923
- 'Metaconfig does not have valid API keys configured. '
924
- + 'Please ensure the metaconfig has at least one API key.',
925
- HTTP_INTERNAL_SERVER_ERROR,
926
- );
918
+ let apiKey;
919
+ if (Array.isArray(metaconfig.apiKeys) && metaconfig.apiKeys.length > 0) {
920
+ [apiKey] = metaconfig.apiKeys;
927
921
  }
928
922
 
929
- const apiKey = apiKeys[0];
930
923
  const forwardedHost = calculateForwardedHost(previewUrl, this.log);
931
924
 
932
925
  // Fetch existing deployed configuration for this URL from production S3
@@ -104,7 +104,7 @@ async function fetchWithRetry(url, options, maxRetries, retryDelayMs, log, fetch
104
104
  * Fetches HTML content from edge with warmup call and retry logic
105
105
  * Makes an initial warmup call, waits, then makes the actual call with retries
106
106
  * @param {string} url - Full URL to fetch
107
- * @param {string} apiKey - Edge Optimize API key
107
+ * @param {string} apiKey - Edge Optimize API key (optional)
108
108
  * @param {string} forwardedHost - Host to forward in x-forwarded-host header
109
109
  * @param {string} edgeUrl - Edge URL
110
110
  * @param {boolean} isOptimized - Whether to fetch optimized HTML (with preview param)
@@ -130,10 +130,6 @@ export async function fetchHtmlWithWarmup(
130
130
  throw new Error('URL is required for fetching HTML');
131
131
  }
132
132
 
133
- if (!hasText(apiKey)) {
134
- throw new Error('Edge Optimize API key is required for fetching HTML');
135
- }
136
-
137
133
  if (!hasText(forwardedHost)) {
138
134
  throw new Error('Forwarded host is required for fetching HTML');
139
135
  }
@@ -158,7 +154,7 @@ export async function fetchHtmlWithWarmup(
158
154
 
159
155
  const headers = {
160
156
  'x-forwarded-host': forwardedHost,
161
- 'x-edgeoptimize-api-key': apiKey,
157
+ ...(apiKey && { 'x-edgeoptimize-api-key': apiKey }),
162
158
  'x-edgeoptimize-url': urlPath,
163
159
  'Accept-Encoding': 'identity', // Disable compression to avoid content-length: 0 issue
164
160
  };
@@ -454,6 +454,43 @@ describe('TokowakaClient', () => {
454
454
  expect(error.status).to.equal(500);
455
455
  }
456
456
  });
457
+
458
+ it('should upload metaconfig with user-defined metadata when provided', async () => {
459
+ const metaconfig = {
460
+ siteId: 'site-123',
461
+ prerender: true,
462
+ };
463
+ const metadata = {
464
+ 'last-modified-by': 'john@example.com',
465
+ 'created-by': 'admin',
466
+ };
467
+
468
+ const s3Path = await client.uploadMetaconfig('https://example.com/page1', metaconfig, metadata);
469
+
470
+ expect(s3Path).to.equal('opportunities/example.com/config');
471
+
472
+ const command = s3Client.send.firstCall.args[0];
473
+ expect(command.input.Bucket).to.equal('test-bucket');
474
+ expect(command.input.Key).to.equal('opportunities/example.com/config');
475
+ expect(command.input.ContentType).to.equal('application/json');
476
+ expect(JSON.parse(command.input.Body)).to.deep.equal(metaconfig);
477
+ expect(command.input.Metadata).to.deep.equal(metadata);
478
+ });
479
+
480
+ it('should upload metaconfig without Metadata field when metadata is empty object', async () => {
481
+ const metaconfig = {
482
+ siteId: 'site-123',
483
+ prerender: true,
484
+ };
485
+
486
+ const s3Path = await client.uploadMetaconfig('https://example.com/page1', metaconfig, {});
487
+
488
+ expect(s3Path).to.equal('opportunities/example.com/config');
489
+
490
+ const command = s3Client.send.firstCall.args[0];
491
+ expect(command.input.Bucket).to.equal('test-bucket');
492
+ expect(command.input.Metadata).to.be.undefined;
493
+ });
457
494
  });
458
495
 
459
496
  describe('createMetaconfig', () => {
@@ -567,6 +604,50 @@ describe('TokowakaClient', () => {
567
604
  const command = s3Client.send.firstCall.args[0];
568
605
  expect(command.input.Key).to.equal('opportunities/example.com/config');
569
606
  });
607
+
608
+ it('should include user-defined metadata when metadata is provided', async () => {
609
+ const siteId = 'site-123';
610
+ const url = 'https://www.example.com/page1';
611
+ const noSuchKeyError = new Error('NoSuchKey');
612
+ noSuchKeyError.name = 'NoSuchKey';
613
+ s3Client.send.onFirstCall().rejects(noSuchKeyError);
614
+
615
+ await client.createMetaconfig(url, siteId, {}, { 'last-modified-by': 'john@example.com' });
616
+
617
+ // Second call is the uploadMetaconfig
618
+ const uploadCommand = s3Client.send.secondCall.args[0];
619
+ expect(uploadCommand.input.Metadata).to.deep.equal({
620
+ 'last-modified-by': 'john@example.com',
621
+ });
622
+ });
623
+
624
+ it('should not include metadata when metadata is empty object', async () => {
625
+ const siteId = 'site-123';
626
+ const url = 'https://www.example.com/page1';
627
+ const noSuchKeyError = new Error('NoSuchKey');
628
+ noSuchKeyError.name = 'NoSuchKey';
629
+ s3Client.send.onFirstCall().rejects(noSuchKeyError);
630
+
631
+ await client.createMetaconfig(url, siteId, {}, {});
632
+
633
+ // Second call is the uploadMetaconfig
634
+ const uploadCommand = s3Client.send.secondCall.args[0];
635
+ expect(uploadCommand.input.Metadata).to.be.undefined;
636
+ });
637
+
638
+ it('should not include metadata when metadata is not provided', async () => {
639
+ const siteId = 'site-123';
640
+ const url = 'https://www.example.com/page1';
641
+ const noSuchKeyError = new Error('NoSuchKey');
642
+ noSuchKeyError.name = 'NoSuchKey';
643
+ s3Client.send.onFirstCall().rejects(noSuchKeyError);
644
+
645
+ await client.createMetaconfig(url, siteId);
646
+
647
+ // Second call is the uploadMetaconfig
648
+ const uploadCommand = s3Client.send.secondCall.args[0];
649
+ expect(uploadCommand.input.Metadata).to.be.undefined;
650
+ });
570
651
  });
571
652
 
572
653
  describe('updateMetaconfig', () => {
@@ -1308,6 +1389,47 @@ describe('TokowakaClient', () => {
1308
1389
  expect(result).to.have.property('prerender');
1309
1390
  expect(result.prerender).to.deep.equal(prerenderConfig);
1310
1391
  });
1392
+
1393
+ it('should include user-defined metadata when metadata is provided', async () => {
1394
+ const siteId = 'site-456';
1395
+ const url = 'https://www.example.com';
1396
+
1397
+ await client.updateMetaconfig(url, siteId, {
1398
+ tokowakaEnabled: true,
1399
+ }, { 'last-modified-by': 'jane@example.com' });
1400
+
1401
+ // Second call is the uploadMetaconfig
1402
+ const uploadCommand = s3Client.send.secondCall.args[0];
1403
+ expect(uploadCommand.input.Metadata).to.deep.equal({
1404
+ 'last-modified-by': 'jane@example.com',
1405
+ });
1406
+ });
1407
+
1408
+ it('should not include metadata when metadata is empty object', async () => {
1409
+ const siteId = 'site-456';
1410
+ const url = 'https://www.example.com';
1411
+
1412
+ await client.updateMetaconfig(url, siteId, {
1413
+ tokowakaEnabled: true,
1414
+ }, {});
1415
+
1416
+ // Second call is the uploadMetaconfig
1417
+ const uploadCommand = s3Client.send.secondCall.args[0];
1418
+ expect(uploadCommand.input.Metadata).to.be.undefined;
1419
+ });
1420
+
1421
+ it('should not include metadata when metadata is not provided', async () => {
1422
+ const siteId = 'site-456';
1423
+ const url = 'https://www.example.com';
1424
+
1425
+ await client.updateMetaconfig(url, siteId, {
1426
+ tokowakaEnabled: true,
1427
+ });
1428
+
1429
+ // Second call is the uploadMetaconfig
1430
+ const uploadCommand = s3Client.send.secondCall.args[0];
1431
+ expect(uploadCommand.input.Metadata).to.be.undefined;
1432
+ });
1311
1433
  });
1312
1434
 
1313
1435
  describe('uploadConfig', () => {
@@ -2819,61 +2941,70 @@ describe('TokowakaClient', () => {
2819
2941
  }
2820
2942
  });
2821
2943
 
2822
- it('should throw error if metaconfig does not exist', async () => {
2944
+ it('should preview suggestions successfully without metaconfig (optional)', async () => {
2945
+ // Override the stub from beforeEach to return null
2823
2946
  client.fetchMetaconfig.resolves(null);
2824
2947
 
2825
- try {
2826
- await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
2827
- expect.fail('Should have thrown error');
2828
- } catch (error) {
2829
- expect(error.message).to.include('No domain-level metaconfig found');
2830
- expect(error.status).to.equal(500);
2831
- }
2832
- });
2948
+ const result = await client.previewSuggestions(
2949
+ mockSite,
2950
+ mockOpportunity,
2951
+ mockSuggestions,
2952
+ { warmupDelayMs: 0 },
2953
+ );
2833
2954
 
2834
- it('should throw error if metaconfig does not have apiKeys', async () => {
2835
- client.fetchMetaconfig.resolves({
2836
- siteId: 'site-123',
2837
- // apiKeys missing
2838
- });
2955
+ expect(result.succeededSuggestions).to.have.length(2);
2956
+ expect(result.failedSuggestions).to.have.length(0);
2957
+ expect(result.config).to.exist;
2958
+ expect(result.html).to.exist;
2959
+ expect(result.html.originalHtml).to.equal('<html><body>Test HTML</body></html>');
2960
+ expect(result.html.optimizedHtml).to.equal('<html><body>Test HTML</body></html>');
2839
2961
 
2840
- try {
2841
- await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
2842
- expect.fail('Should have thrown error');
2843
- } catch (error) {
2844
- expect(error.message).to.include('Metaconfig does not have valid API keys configured');
2845
- expect(error.status).to.equal(500);
2846
- }
2962
+ // Verify fetch was called without API key (undefined)
2963
+ expect(fetchStub.callCount).to.equal(4); // 2 warmup + 2 actual (original + optimized)
2847
2964
  });
2848
2965
 
2849
- it('should throw error if metaconfig has empty apiKeys array', async () => {
2966
+ it('should preview suggestions successfully without apiKeys in metaconfig (optional)', async () => {
2967
+ // Override the stub from beforeEach
2850
2968
  client.fetchMetaconfig.resolves({
2851
2969
  siteId: 'site-123',
2852
- apiKeys: [],
2970
+ // apiKeys missing - should work without it
2853
2971
  });
2854
2972
 
2855
- try {
2856
- await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
2857
- expect.fail('Should have thrown error');
2858
- } catch (error) {
2859
- expect(error.message).to.include('Metaconfig does not have valid API keys configured');
2860
- expect(error.status).to.equal(500);
2861
- }
2973
+ const result = await client.previewSuggestions(
2974
+ mockSite,
2975
+ mockOpportunity,
2976
+ mockSuggestions,
2977
+ { warmupDelayMs: 0 },
2978
+ );
2979
+
2980
+ expect(result.succeededSuggestions).to.have.length(2);
2981
+ expect(result.failedSuggestions).to.have.length(0);
2982
+ expect(result.config).to.exist;
2983
+ expect(result.html).to.exist;
2984
+ expect(result.html.originalHtml).to.equal('<html><body>Test HTML</body></html>');
2985
+ expect(result.html.optimizedHtml).to.equal('<html><body>Test HTML</body></html>');
2986
+ expect(fetchStub.callCount).to.equal(4);
2862
2987
  });
2863
2988
 
2864
- it('should throw error if metaconfig apiKeys first value is empty', async () => {
2989
+ it('should preview suggestions successfully with empty apiKeys array (optional)', async () => {
2990
+ // Override the stub from beforeEach
2865
2991
  client.fetchMetaconfig.resolves({
2866
2992
  siteId: 'site-123',
2867
- apiKeys: ['', 'test-api-key-2'],
2993
+ apiKeys: [],
2868
2994
  });
2869
2995
 
2870
- try {
2871
- await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
2872
- expect.fail('Should have thrown error');
2873
- } catch (error) {
2874
- expect(error.message).to.include('Metaconfig does not have valid API keys configured');
2875
- expect(error.status).to.equal(500);
2876
- }
2996
+ const result = await client.previewSuggestions(
2997
+ mockSite,
2998
+ mockOpportunity,
2999
+ mockSuggestions,
3000
+ { warmupDelayMs: 0 },
3001
+ );
3002
+
3003
+ expect(result.succeededSuggestions).to.have.length(2);
3004
+ expect(result.failedSuggestions).to.have.length(0);
3005
+ expect(result.config).to.exist;
3006
+ expect(result.html).to.exist;
3007
+ expect(fetchStub.callCount).to.equal(4);
2877
3008
  });
2878
3009
 
2879
3010
  it('should throw error for unsupported opportunity type', async () => {
@@ -82,20 +82,33 @@ describe('HTML Utils', () => {
82
82
  }
83
83
  });
84
84
 
85
- it('should throw error when apiKey is missing', async () => {
86
- try {
87
- await fetchHtmlWithWarmup(
88
- 'https://example.com/page',
89
- '',
90
- 'host',
91
- 'edge-url',
92
- log,
93
- false,
94
- );
95
- expect.fail('Should have thrown error');
96
- } catch (error) {
97
- expect(error.message).to.equal('Edge Optimize API key is required for fetching HTML');
98
- }
85
+ it('should successfully fetch HTML without apiKey (optional parameter)', async () => {
86
+ fetchStub.resolves({
87
+ ok: true,
88
+ status: 200,
89
+ statusText: 'OK',
90
+ headers: {
91
+ get: (name) => (name === 'x-edgeoptimize-cache' ? 'HIT' : null),
92
+ },
93
+ text: async () => '<html>Test HTML without API key</html>',
94
+ });
95
+
96
+ const html = await fetchHtmlWithWarmup(
97
+ 'https://example.com/page',
98
+ null, // apiKey is optional
99
+ 'host',
100
+ 'https://edge.example.com',
101
+ log,
102
+ false,
103
+ { warmupDelayMs: 0 },
104
+ );
105
+
106
+ expect(html).to.equal('<html>Test HTML without API key</html>');
107
+ expect(fetchStub.callCount).to.equal(2); // warmup + actual
108
+
109
+ // Verify that x-edgeoptimize-api-key header is not present
110
+ const fetchOptions = fetchStub.firstCall.args[1];
111
+ expect(fetchOptions.headers).to.not.have.property('x-edgeoptimize-api-key');
99
112
  });
100
113
 
101
114
  it('should successfully fetch HTML with all required parameters', async () => {