@adobe/spacecat-shared-tokowaka-client 1.9.0 → 1.10.0

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,9 @@
1
+ ## [@adobe/spacecat-shared-tokowaka-client-v1.10.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.9.0...@adobe/spacecat-shared-tokowaka-client-v1.10.0) (2026-02-26)
2
+
3
+ ### Features
4
+
5
+ * LLMO-2805 Stage config addition api changes ([#1370](https://github.com/adobe/spacecat-shared/issues/1370)) ([0789f4a](https://github.com/adobe/spacecat-shared/commit/0789f4af95433b733c86638a79c184850804dba9))
6
+
1
7
  ## [@adobe/spacecat-shared-tokowaka-client-v1.9.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.8.0...@adobe/spacecat-shared-tokowaka-client-v1.9.0) (2026-02-24)
2
8
 
3
9
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tokowaka-client",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Tokowaka Client for SpaceCat - Edge optimization config management",
5
5
  "type": "module",
6
6
  "engines": {
package/src/index.js CHANGED
@@ -230,11 +230,13 @@ class TokowakaClient {
230
230
  }
231
231
 
232
232
  /**
233
- * Fetches domain-level metaconfig from S3
233
+ * Internal method to fetch domain-level metaconfig from S3 with metadata
234
234
  * @param {string} url - Full URL (used to extract domain)
235
- * @returns {Promise<Object|null>} - Metaconfig object or null if not found
235
+ * @returns {Promise<Object|null>} - Object with metaconfig and s3Metadata,
236
+ * or null if not found
237
+ * @private
236
238
  */
237
- async fetchMetaconfig(url) {
239
+ async #fetchMetaconfigWithMetadata(url) {
238
240
  if (!hasText(url)) {
239
241
  throw this.#createError('URL is required', HTTP_BAD_REQUEST);
240
242
  }
@@ -254,7 +256,11 @@ class TokowakaClient {
254
256
  const metaconfig = JSON.parse(bodyContents);
255
257
 
256
258
  this.log.debug(`Successfully fetched metaconfig from s3://${bucketName}/${s3Path} in ${Date.now() - fetchStartTime}ms`);
257
- return metaconfig;
259
+
260
+ return {
261
+ metaconfig,
262
+ s3Metadata: response.Metadata || {},
263
+ };
258
264
  } catch (error) {
259
265
  // If metaconfig doesn't exist (NoSuchKey), return null
260
266
  if (error.name === 'NoSuchKey' || error.Code === 'NoSuchKey') {
@@ -268,6 +274,16 @@ class TokowakaClient {
268
274
  }
269
275
  }
270
276
 
277
+ /**
278
+ * Fetches domain-level metaconfig from S3
279
+ * @param {string} url - Full URL (used to extract domain)
280
+ * @returns {Promise<Object|null>} - Metaconfig object or null if not found
281
+ */
282
+ async fetchMetaconfig(url) {
283
+ const result = await this.#fetchMetaconfigWithMetadata(url);
284
+ return result?.metaconfig ?? null;
285
+ }
286
+
271
287
  /**
272
288
  * Generates an API key for Tokowaka based on domain
273
289
  * @param {string} domain - Domain name (e.g., 'example.com')
@@ -289,7 +305,13 @@ class TokowakaClient {
289
305
  * @param {string} url - Full URL (used to extract domain)
290
306
  * @param {string} siteId - Site ID
291
307
  * @param {Object} options - Optional configuration
292
- * @param {boolean} options.enhancements - Whether to enable enhancements (default: true)
308
+ * @param {boolean} options.enhancements - Whether to enable enhancements
309
+ * (default: true)
310
+ * @param {Object} metadata - Optional S3 user-defined metadata for
311
+ * audit trail and behavior flags
312
+ * @param {string} metadata.lastModifiedBy - User who modified the config
313
+ * @param {boolean} metadata.isStageDomain - Whether this is a staging
314
+ * domain (enables wildcard prerender)
293
315
  * @returns {Promise<Object>} - Object with s3Path and metaconfig
294
316
  */
295
317
  async createMetaconfig(url, siteId, options = {}, metadata = {}) {
@@ -318,7 +340,19 @@ class TokowakaClient {
318
340
  patches: {},
319
341
  };
320
342
 
321
- const s3Path = await this.uploadMetaconfig(url, metaconfig, metadata);
343
+ // Handle staging domain with automatic prerender configuration
344
+ const isStageDomain = metadata.isStageDomain === true;
345
+ if (isStageDomain) {
346
+ metaconfig.prerender = { allowList: ['/*'] };
347
+ }
348
+
349
+ // Persist isStageDomain in S3 metadata for future updates
350
+ const s3Metadata = {
351
+ ...metadata,
352
+ ...(isStageDomain && { isStageDomain: 'true' }),
353
+ };
354
+
355
+ const s3Path = await this.uploadMetaconfig(url, metaconfig, s3Metadata);
322
356
  this.log.info(`Created new Tokowaka metaconfig for ${normalizedHostName} at ${s3Path}`);
323
357
 
324
358
  return metaconfig;
@@ -330,6 +364,11 @@ class TokowakaClient {
330
364
  * @param {string} url - Full URL (used to extract domain)
331
365
  * @param {string} siteId - Site ID
332
366
  * @param {Object} options - Optional configuration
367
+ * @param {Object} metadata - Optional S3 user-defined metadata for
368
+ * audit trail and behavior flags
369
+ * @param {string} metadata.lastModifiedBy - User who modified the config
370
+ * @param {boolean} metadata.isStageDomain - Whether this is a staging
371
+ * domain (enables wildcard prerender)
333
372
  * @returns {Promise<Object>} - Object with s3Path and metaconfig
334
373
  */
335
374
  async updateMetaconfig(url, siteId, options = {}, metadata = {}) {
@@ -337,10 +376,11 @@ class TokowakaClient {
337
376
  throw this.#createError('URL is required', HTTP_BAD_REQUEST);
338
377
  }
339
378
 
340
- const existingMetaconfig = await this.fetchMetaconfig(url);
341
- if (!existingMetaconfig) {
379
+ const raw = await this.#fetchMetaconfigWithMetadata(url);
380
+ if (!raw?.metaconfig) {
342
381
  throw this.#createError('Metaconfig does not exist for this URL', HTTP_BAD_REQUEST);
343
382
  }
383
+ const { metaconfig: existingMetaconfig, s3Metadata: existingS3Metadata } = raw;
344
384
 
345
385
  if (!hasText(siteId)) {
346
386
  throw this.#createError('Site ID is required', HTTP_BAD_REQUEST);
@@ -356,10 +396,17 @@ class TokowakaClient {
356
396
  ?? existingMetaconfig.forceFail
357
397
  ?? false;
358
398
 
359
- const hasPrerender = isNonEmptyObject(options.prerender)
399
+ // Handle staging domain: check from metadata or from existing S3 metadata (S3 lowercases keys)
400
+ const isStageDomain = metadata.isStageDomain === true
401
+ || existingS3Metadata.isstagedomain === 'true';
402
+
403
+ const hasPrerender = isStageDomain
404
+ || isNonEmptyObject(options.prerender)
360
405
  || isNonEmptyObject(existingMetaconfig.prerender);
361
- const prerender = options.prerender
362
- ?? existingMetaconfig.prerender;
406
+
407
+ const prerender = isStageDomain
408
+ ? { allowList: ['/*'] }
409
+ : (options.prerender ?? existingMetaconfig.prerender);
363
410
 
364
411
  const metaconfig = {
365
412
  ...existingMetaconfig,
@@ -372,8 +419,14 @@ class TokowakaClient {
372
419
  ...(hasPrerender && { prerender }),
373
420
  };
374
421
 
375
- const s3Path = await this.uploadMetaconfig(url, metaconfig, metadata);
376
- this.log.info(`Updated Tokowaka metaconfig for ${normalizedHostName} at ${s3Path}`);
422
+ // Persist isStageDomain in S3 metadata for future updates
423
+ const s3Metadata = {
424
+ ...metadata,
425
+ ...(isStageDomain && { isStageDomain: 'true' }),
426
+ };
427
+
428
+ const uploadedPath = await this.uploadMetaconfig(url, metaconfig, s3Metadata);
429
+ this.log.info(`Updated Tokowaka metaconfig for ${normalizedHostName} at ${uploadedPath}`);
377
430
 
378
431
  return metaconfig;
379
432
  }
@@ -649,6 +649,56 @@ describe('TokowakaClient', () => {
649
649
  const uploadCommand = s3Client.send.secondCall.args[0];
650
650
  expect(uploadCommand.input.Metadata).to.be.undefined;
651
651
  });
652
+
653
+ it('should NOT include prerender when isStageDomain is not true in metadata', async () => {
654
+ const siteId = 'site-123';
655
+ const url = 'https://www.example.com/page1';
656
+ const noSuchKeyError = new Error('NoSuchKey');
657
+ noSuchKeyError.name = 'NoSuchKey';
658
+ s3Client.send.onFirstCall().rejects(noSuchKeyError);
659
+
660
+ const result = await client.createMetaconfig(url, siteId);
661
+
662
+ expect(result).to.not.have.property('prerender');
663
+
664
+ const uploadCommand = s3Client.send.secondCall.args[0];
665
+ const body = JSON.parse(uploadCommand.input.Body);
666
+ expect(body).to.not.have.property('prerender');
667
+ });
668
+
669
+ it('should set prerender with allowList when isStageDomain is true in metadata', async () => {
670
+ const siteId = 'site-123';
671
+ const url = 'https://staging.example.com/page1';
672
+ const noSuchKeyError = new Error('NoSuchKey');
673
+ noSuchKeyError.name = 'NoSuchKey';
674
+ s3Client.send.onFirstCall().rejects(noSuchKeyError);
675
+
676
+ const result = await client.createMetaconfig(url, siteId, {}, { isStageDomain: true });
677
+
678
+ expect(result).to.have.property('prerender');
679
+ expect(result.prerender).to.deep.equal({ allowList: ['/*'] });
680
+
681
+ const uploadCommand = s3Client.send.secondCall.args[0];
682
+ const body = JSON.parse(uploadCommand.input.Body);
683
+ expect(body.prerender).to.deep.equal({ allowList: ['/*'] });
684
+ expect(uploadCommand.input.Metadata).to.deep.equal({ isStageDomain: 'true' });
685
+ });
686
+
687
+ it('should NOT set prerender when isStageDomain is false in metadata', async () => {
688
+ const siteId = 'site-123';
689
+ const url = 'https://www.example.com/page1';
690
+ const noSuchKeyError = new Error('NoSuchKey');
691
+ noSuchKeyError.name = 'NoSuchKey';
692
+ s3Client.send.onFirstCall().rejects(noSuchKeyError);
693
+
694
+ const result = await client.createMetaconfig(url, siteId, {}, { isStageDomain: false });
695
+
696
+ expect(result).to.not.have.property('prerender');
697
+
698
+ const uploadCommand = s3Client.send.secondCall.args[0];
699
+ const body = JSON.parse(uploadCommand.input.Body);
700
+ expect(body).to.not.have.property('prerender');
701
+ });
652
702
  });
653
703
 
654
704
  describe('updateMetaconfig', () => {
@@ -661,11 +711,12 @@ describe('TokowakaClient', () => {
661
711
  };
662
712
 
663
713
  beforeEach(() => {
664
- // Mock fetchMetaconfig to return existing config
714
+ // Mock fetchMetaconfig to return existing config with metadata
665
715
  s3Client.send.onFirstCall().resolves({
666
716
  Body: {
667
717
  transformToString: sinon.stub().resolves(JSON.stringify(existingMetaconfig)),
668
718
  },
719
+ Metadata: {},
669
720
  });
670
721
  // Mock uploadMetaconfig S3 upload
671
722
  s3Client.send.onSecondCall().resolves();
@@ -1431,6 +1482,84 @@ describe('TokowakaClient', () => {
1431
1482
  const uploadCommand = s3Client.send.secondCall.args[0];
1432
1483
  expect(uploadCommand.input.Metadata).to.be.undefined;
1433
1484
  });
1485
+
1486
+ it('should set prerender with allowList when isStageDomain is true in metadata', async () => {
1487
+ const siteId = 'site-456';
1488
+ const url = 'https://staging.example.com';
1489
+
1490
+ const result = await client.updateMetaconfig(url, siteId, {}, { isStageDomain: true });
1491
+
1492
+ expect(result).to.have.property('prerender');
1493
+ expect(result.prerender).to.deep.equal({ allowList: ['/*'] });
1494
+
1495
+ const uploadCommand = s3Client.send.secondCall.args[0];
1496
+ const body = JSON.parse(uploadCommand.input.Body);
1497
+ expect(body.prerender).to.deep.equal({ allowList: ['/*'] });
1498
+ expect(uploadCommand.input.Metadata).to.deep.equal({ isStageDomain: 'true' });
1499
+ });
1500
+
1501
+ it('should override existing prerender when isStageDomain is true in metadata', async () => {
1502
+ const siteId = 'site-456';
1503
+ const url = 'https://staging.example.com';
1504
+ const existingMetaconfigWithPrerender = {
1505
+ ...existingMetaconfig,
1506
+ prerender: { allowList: ['/old-path/*'] },
1507
+ };
1508
+
1509
+ s3Client.send.onFirstCall().resolves({
1510
+ Body: {
1511
+ transformToString: sinon.stub().resolves(JSON.stringify(existingMetaconfigWithPrerender)),
1512
+ },
1513
+ Metadata: {},
1514
+ });
1515
+
1516
+ const result = await client.updateMetaconfig(url, siteId, {}, { isStageDomain: true });
1517
+
1518
+ expect(result).to.have.property('prerender');
1519
+ expect(result.prerender).to.deep.equal({ allowList: ['/*'] });
1520
+
1521
+ const uploadCommand = s3Client.send.secondCall.args[0];
1522
+ const body = JSON.parse(uploadCommand.input.Body);
1523
+ expect(body.prerender).to.deep.equal({ allowList: ['/*'] });
1524
+ });
1525
+
1526
+ it('should preserve existing prerender when isStageDomain is not in metadata', async () => {
1527
+ const siteId = 'site-456';
1528
+ const url = 'https://www.example.com';
1529
+ const existingMetaconfigWithPrerender = {
1530
+ ...existingMetaconfig,
1531
+ prerender: { allowList: ['/path/*'] },
1532
+ };
1533
+
1534
+ s3Client.send.onFirstCall().resolves({
1535
+ Body: {
1536
+ transformToString: sinon.stub().resolves(JSON.stringify(existingMetaconfigWithPrerender)),
1537
+ },
1538
+ Metadata: {},
1539
+ });
1540
+
1541
+ const result = await client.updateMetaconfig(url, siteId, {});
1542
+
1543
+ expect(result).to.have.property('prerender');
1544
+ expect(result.prerender).to.deep.equal({ allowList: ['/path/*'] });
1545
+
1546
+ const uploadCommand = s3Client.send.secondCall.args[0];
1547
+ const body = JSON.parse(uploadCommand.input.Body);
1548
+ expect(body.prerender).to.deep.equal({ allowList: ['/path/*'] });
1549
+ });
1550
+
1551
+ it('should NOT set prerender when isStageDomain is false in metadata', async () => {
1552
+ const siteId = 'site-456';
1553
+ const url = 'https://www.example.com';
1554
+
1555
+ const result = await client.updateMetaconfig(url, siteId, {}, { isStageDomain: false });
1556
+
1557
+ expect(result).to.not.have.property('prerender');
1558
+
1559
+ const uploadCommand = s3Client.send.secondCall.args[0];
1560
+ const body = JSON.parse(uploadCommand.input.Body);
1561
+ expect(body).to.not.have.property('prerender');
1562
+ });
1434
1563
  });
1435
1564
 
1436
1565
  describe('uploadConfig', () => {