@adobe/spacecat-shared-tokowaka-client 1.4.4 → 1.4.5
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 +7 -0
- package/README.md +5 -8
- package/package.json +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.js +37 -25
- package/src/utils/custom-html-utils.js +26 -0
- package/test/index.test.js +45 -10
- package/test/utils/html-utils.test.js +101 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [@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
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* fetch api key from s3 and fix invalidation sequence ([#1251](https://github.com/adobe/spacecat-shared/issues/1251)) ([fe66bd6](https://github.com/adobe/spacecat-shared/commit/fe66bd6732dc200ab4df27babc1e0831f6187271))
|
|
7
|
+
|
|
1
8
|
# [@adobe/spacecat-shared-tokowaka-client-v1.4.4](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.4.3...@adobe/spacecat-shared-tokowaka-client-v1.4.4) (2026-01-05)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -146,21 +146,17 @@ The client invalidates CDN cache after uploading configurations. Failures are lo
|
|
|
146
146
|
|
|
147
147
|
## Site Configuration
|
|
148
148
|
|
|
149
|
-
Sites must have the
|
|
149
|
+
Sites must have the `tokowakaConfig` field in their site-config to confirm that tokowaka is enabled for that particular site.
|
|
150
150
|
|
|
151
151
|
```javascript
|
|
152
152
|
{
|
|
153
|
+
...
|
|
153
154
|
"tokowakaConfig": {
|
|
154
|
-
"
|
|
155
|
-
"forwardedHost": "www.example.com" // Required for preview functionality
|
|
155
|
+
"enabled": true
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
-
**Note:**
|
|
161
|
-
- `apiKey` is optional and **not used** for S3 paths or HTTP headers (kept in schema for potential future use)
|
|
162
|
-
- `forwardedHost` is **required** for preview functionality to fetch HTML from Tokowaka edge
|
|
163
|
-
|
|
164
160
|
## Supported Opportunity Types
|
|
165
161
|
|
|
166
162
|
### Headings
|
|
@@ -215,6 +211,7 @@ Domain-level metaconfig (created once per domain, shared by all URLs):
|
|
|
215
211
|
```json
|
|
216
212
|
{
|
|
217
213
|
"siteId": "abc-123",
|
|
214
|
+
"apiKeys": ["tokowaka-api-key-1"],
|
|
218
215
|
"prerender": true
|
|
219
216
|
}
|
|
220
217
|
```
|
|
@@ -308,7 +305,7 @@ TOKOWAKA_CDN_CONFIG='{"cloudfront":{"distributionId":"...","region":"us-east-1"}
|
|
|
308
305
|
|
|
309
306
|
**Behavior:**
|
|
310
307
|
- All URLs are batched into a single invalidation request per provider
|
|
311
|
-
- All CDN providers are invalidated
|
|
308
|
+
- All CDN providers are invalidated sequentially
|
|
312
309
|
- Failures in one CDN don't block others
|
|
313
310
|
- Each CDN returns its own result in the `cdnInvalidations` array
|
|
314
311
|
- Results include status, provider name, and provider-specific details
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { mergePatches } from './utils/patch-utils.js';
|
|
|
18
18
|
import { getTokowakaConfigS3Path, getTokowakaMetaconfigS3Path } from './utils/s3-utils.js';
|
|
19
19
|
import { groupSuggestionsByUrlPath, filterEligibleSuggestions } from './utils/suggestion-utils.js';
|
|
20
20
|
import { getEffectiveBaseURL } from './utils/site-utils.js';
|
|
21
|
-
import { fetchHtmlWithWarmup } from './utils/custom-html-utils.js';
|
|
21
|
+
import { fetchHtmlWithWarmup, calculateForwardedHost } from './utils/custom-html-utils.js';
|
|
22
22
|
|
|
23
23
|
const HTTP_BAD_REQUEST = 400;
|
|
24
24
|
const HTTP_INTERNAL_SERVER_ERROR = 500;
|
|
@@ -222,7 +222,7 @@ class TokowakaClient {
|
|
|
222
222
|
/**
|
|
223
223
|
* Uploads domain-level metaconfig to S3
|
|
224
224
|
* @param {string} url - Full URL (used to extract domain)
|
|
225
|
-
* @param {Object} metaconfig - Metaconfig object (siteId, prerender)
|
|
225
|
+
* @param {Object} metaconfig - Metaconfig object (siteId, apiKeys, prerender)
|
|
226
226
|
* @returns {Promise<string>} - S3 key of uploaded metaconfig
|
|
227
227
|
*/
|
|
228
228
|
async uploadMetaconfig(url, metaconfig) {
|
|
@@ -373,7 +373,7 @@ class TokowakaClient {
|
|
|
373
373
|
|
|
374
374
|
/**
|
|
375
375
|
* CDN cache invalidation method that supports invalidating URL configs
|
|
376
|
-
* or custom S3 paths across provided or default CDN providers
|
|
376
|
+
* or custom S3 paths across provided or default CDN providers.
|
|
377
377
|
* @param {Object} options - Invalidation options
|
|
378
378
|
* @param {Array<string>} options.urls - Array of full URLs to invalidate (for URL configs)
|
|
379
379
|
* @param {Array<string>} options.paths - Custom S3 paths to invalidate directly
|
|
@@ -416,37 +416,37 @@ class TokowakaClient {
|
|
|
416
416
|
+ `via providers: ${providerList.join(', ')}`,
|
|
417
417
|
);
|
|
418
418
|
|
|
419
|
-
|
|
420
|
-
const
|
|
419
|
+
const results = [];
|
|
420
|
+
for (const provider of providerList) {
|
|
421
421
|
try {
|
|
422
422
|
const cdnClient = this.cdnClientRegistry.getClient(provider);
|
|
423
423
|
if (!cdnClient) {
|
|
424
424
|
this.log.warn(`No CDN client available for provider: ${provider}`);
|
|
425
|
-
|
|
425
|
+
results.push({
|
|
426
426
|
status: 'error',
|
|
427
427
|
provider,
|
|
428
428
|
message: `No CDN client available for provider: ${provider}`,
|
|
429
|
-
};
|
|
429
|
+
});
|
|
430
|
+
// eslint-disable-next-line no-continue
|
|
431
|
+
continue;
|
|
430
432
|
}
|
|
431
433
|
|
|
434
|
+
// eslint-disable-next-line no-await-in-loop
|
|
432
435
|
const result = await cdnClient.invalidateCache(pathsToInvalidate);
|
|
433
436
|
this.log.info(
|
|
434
437
|
`CDN cache invalidation completed for ${provider}: `
|
|
435
438
|
+ `${pathsToInvalidate.length} path(s)`,
|
|
436
439
|
);
|
|
437
|
-
|
|
440
|
+
results.push(result);
|
|
438
441
|
} catch (error) {
|
|
439
442
|
this.log.warn(`Failed to invalidate ${provider} CDN cache: ${error.message}`, error);
|
|
440
|
-
|
|
443
|
+
results.push({
|
|
441
444
|
status: 'error',
|
|
442
445
|
provider,
|
|
443
446
|
message: error.message,
|
|
444
|
-
};
|
|
447
|
+
});
|
|
445
448
|
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Wait for all provider invalidations to complete
|
|
449
|
-
const results = await Promise.all(invalidationPromises);
|
|
449
|
+
}
|
|
450
450
|
return results;
|
|
451
451
|
}
|
|
452
452
|
|
|
@@ -699,17 +699,6 @@ class TokowakaClient {
|
|
|
699
699
|
* @returns {Promise<Object>} - Preview result with config and succeeded/failed suggestions
|
|
700
700
|
*/
|
|
701
701
|
async previewSuggestions(site, opportunity, suggestions, options = {}) {
|
|
702
|
-
// Get site's forwarded host for preview
|
|
703
|
-
const { forwardedHost, apiKey } = site.getConfig()?.getTokowakaConfig() || {};
|
|
704
|
-
|
|
705
|
-
if (!hasText(forwardedHost) || !hasText(apiKey)) {
|
|
706
|
-
throw this.#createError(
|
|
707
|
-
'Site does not have a Tokowaka API key or forwarded host configured. '
|
|
708
|
-
+ 'Please onboard the site to Tokowaka first.',
|
|
709
|
-
HTTP_BAD_REQUEST,
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
702
|
const opportunityType = opportunity.getType();
|
|
714
703
|
const mapper = this.mapperRegistry.getMapper(opportunityType);
|
|
715
704
|
if (!mapper) {
|
|
@@ -755,6 +744,29 @@ class TokowakaClient {
|
|
|
755
744
|
throw this.#createError('Preview URL not found in suggestion data', HTTP_BAD_REQUEST);
|
|
756
745
|
}
|
|
757
746
|
|
|
747
|
+
// Fetch metaconfig to get API key
|
|
748
|
+
const metaconfig = await this.fetchMetaconfig(previewUrl);
|
|
749
|
+
|
|
750
|
+
if (!metaconfig) {
|
|
751
|
+
throw this.#createError(
|
|
752
|
+
'No domain-level metaconfig found. '
|
|
753
|
+
+ 'A domain-level metaconfig needs to be created first before previewing suggestions.',
|
|
754
|
+
HTTP_INTERNAL_SERVER_ERROR,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const { apiKeys } = metaconfig;
|
|
759
|
+
if (!Array.isArray(apiKeys) || apiKeys.length === 0 || !hasText(apiKeys[0])) {
|
|
760
|
+
throw this.#createError(
|
|
761
|
+
'Metaconfig does not have valid API keys configured. '
|
|
762
|
+
+ 'Please ensure the metaconfig has at least one API key.',
|
|
763
|
+
HTTP_INTERNAL_SERVER_ERROR,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const apiKey = apiKeys[0];
|
|
768
|
+
const forwardedHost = calculateForwardedHost(previewUrl, this.log);
|
|
769
|
+
|
|
758
770
|
// Fetch existing deployed configuration for this URL from production S3
|
|
759
771
|
this.log.debug(`Fetching existing deployed Tokowaka config for URL: ${previewUrl}`);
|
|
760
772
|
const existingConfig = await this.fetchConfig(previewUrl, false);
|
|
@@ -193,3 +193,29 @@ export async function fetchHtmlWithWarmup(
|
|
|
193
193
|
throw new Error(errorMsg);
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
export function calculateForwardedHost(url, logger = console) {
|
|
198
|
+
try {
|
|
199
|
+
const urlObj = new URL(url);
|
|
200
|
+
const { hostname } = urlObj;
|
|
201
|
+
|
|
202
|
+
// If hostname already starts with www., keep it as is
|
|
203
|
+
if (hostname.startsWith('www.')) {
|
|
204
|
+
logger.debug(`Forwarded host: ${hostname}`);
|
|
205
|
+
return hostname;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Count dots to determine if it's a bare domain or has a subdomain
|
|
209
|
+
const dotCount = (hostname.match(/\./g) || []).length;
|
|
210
|
+
|
|
211
|
+
// If only 1 dot (bare domain like example.com), prepend www.
|
|
212
|
+
// If 2+ dots (subdomain like subdomain.example.com), keep as is
|
|
213
|
+
const forwardedHost = dotCount === 1 ? `www.${hostname}` : hostname;
|
|
214
|
+
|
|
215
|
+
logger.debug(`Forwarded host: ${forwardedHost}`);
|
|
216
|
+
return forwardedHost;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
logger.error(`Error calculating forwarded host from URL ${url}: ${error.message}`);
|
|
219
|
+
throw new Error(`Error calculating forwarded host from URL ${url}: ${error.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
package/test/index.test.js
CHANGED
|
@@ -1698,6 +1698,12 @@ describe('TokowakaClient', () => {
|
|
|
1698
1698
|
// Stub fetchConfig to return null by default (no existing config)
|
|
1699
1699
|
sinon.stub(client, 'fetchConfig').resolves(null);
|
|
1700
1700
|
|
|
1701
|
+
// Stub fetchMetaconfig to return metaconfig with apiKeys array
|
|
1702
|
+
sinon.stub(client, 'fetchMetaconfig').resolves({
|
|
1703
|
+
siteId: 'site-123',
|
|
1704
|
+
apiKeys: ['test-api-key-1', 'test-api-key-2'],
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1701
1707
|
// Add TOKOWAKA_EDGE_URL to env
|
|
1702
1708
|
client.env.TOKOWAKA_EDGE_URL = 'https://edge-dev.tokowaka.now';
|
|
1703
1709
|
});
|
|
@@ -1804,31 +1810,60 @@ describe('TokowakaClient', () => {
|
|
|
1804
1810
|
}
|
|
1805
1811
|
});
|
|
1806
1812
|
|
|
1807
|
-
it('should throw error if
|
|
1808
|
-
|
|
1809
|
-
|
|
1813
|
+
it('should throw error if metaconfig does not exist', async () => {
|
|
1814
|
+
client.fetchMetaconfig.resolves(null);
|
|
1815
|
+
|
|
1816
|
+
try {
|
|
1817
|
+
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1818
|
+
expect.fail('Should have thrown error');
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
expect(error.message).to.include('No domain-level metaconfig found');
|
|
1821
|
+
expect(error.status).to.equal(500);
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
it('should throw error if metaconfig does not have apiKeys', async () => {
|
|
1826
|
+
client.fetchMetaconfig.resolves({
|
|
1827
|
+
siteId: 'site-123',
|
|
1828
|
+
// apiKeys missing
|
|
1810
1829
|
});
|
|
1811
1830
|
|
|
1812
1831
|
try {
|
|
1813
1832
|
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1814
1833
|
expect.fail('Should have thrown error');
|
|
1815
1834
|
} catch (error) {
|
|
1816
|
-
expect(error.message).to.include('
|
|
1817
|
-
expect(error.status).to.equal(
|
|
1835
|
+
expect(error.message).to.include('Metaconfig does not have valid API keys configured');
|
|
1836
|
+
expect(error.status).to.equal(500);
|
|
1818
1837
|
}
|
|
1819
1838
|
});
|
|
1820
1839
|
|
|
1821
|
-
it('should throw error if
|
|
1822
|
-
|
|
1823
|
-
|
|
1840
|
+
it('should throw error if metaconfig has empty apiKeys array', async () => {
|
|
1841
|
+
client.fetchMetaconfig.resolves({
|
|
1842
|
+
siteId: 'site-123',
|
|
1843
|
+
apiKeys: [],
|
|
1824
1844
|
});
|
|
1825
1845
|
|
|
1826
1846
|
try {
|
|
1827
1847
|
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1828
1848
|
expect.fail('Should have thrown error');
|
|
1829
1849
|
} catch (error) {
|
|
1830
|
-
expect(error.message).to.include('
|
|
1831
|
-
expect(error.status).to.equal(
|
|
1850
|
+
expect(error.message).to.include('Metaconfig does not have valid API keys configured');
|
|
1851
|
+
expect(error.status).to.equal(500);
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
it('should throw error if metaconfig apiKeys first value is empty', async () => {
|
|
1856
|
+
client.fetchMetaconfig.resolves({
|
|
1857
|
+
siteId: 'site-123',
|
|
1858
|
+
apiKeys: ['', 'test-api-key-2'],
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
try {
|
|
1862
|
+
await client.previewSuggestions(mockSite, mockOpportunity, mockSuggestions);
|
|
1863
|
+
expect.fail('Should have thrown error');
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
expect(error.message).to.include('Metaconfig does not have valid API keys configured');
|
|
1866
|
+
expect(error.status).to.equal(500);
|
|
1832
1867
|
}
|
|
1833
1868
|
});
|
|
1834
1869
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { expect } from 'chai';
|
|
16
16
|
import sinon from 'sinon';
|
|
17
|
-
import { fetchHtmlWithWarmup } from '../../src/utils/custom-html-utils.js';
|
|
17
|
+
import { fetchHtmlWithWarmup, calculateForwardedHost } from '../../src/utils/custom-html-utils.js';
|
|
18
18
|
|
|
19
19
|
describe('HTML Utils', () => {
|
|
20
20
|
describe('fetchHtmlWithWarmup', () => {
|
|
@@ -429,4 +429,104 @@ describe('HTML Utils', () => {
|
|
|
429
429
|
expect(fetchStub.callCount).to.equal(2); // warmup + 1 actual
|
|
430
430
|
});
|
|
431
431
|
});
|
|
432
|
+
|
|
433
|
+
describe('calculateForwardedHost', () => {
|
|
434
|
+
let log;
|
|
435
|
+
|
|
436
|
+
beforeEach(() => {
|
|
437
|
+
log = {
|
|
438
|
+
debug: sinon.stub(),
|
|
439
|
+
warn: sinon.stub(),
|
|
440
|
+
error: sinon.stub(),
|
|
441
|
+
info: sinon.stub(),
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
afterEach(() => {
|
|
446
|
+
sinon.restore();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should add www prefix to bare domain', async () => {
|
|
450
|
+
const result = calculateForwardedHost('https://example.com/page', log);
|
|
451
|
+
expect(result).to.equal('www.example.com');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should keep www prefix when already present', async () => {
|
|
455
|
+
const result = calculateForwardedHost('https://www.example.com/page', log);
|
|
456
|
+
expect(result).to.equal('www.example.com');
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should keep subdomain without adding www', async () => {
|
|
460
|
+
const result = calculateForwardedHost('https://subdomain.example.com/page', log);
|
|
461
|
+
expect(result).to.equal('subdomain.example.com');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should keep www.subdomain as is', async () => {
|
|
465
|
+
const result = calculateForwardedHost('https://www.subdomain.example.com/page', log);
|
|
466
|
+
expect(result).to.equal('www.subdomain.example.com');
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should add www to bare domain with port', async () => {
|
|
470
|
+
const result = calculateForwardedHost('https://example.com:8080/page', log);
|
|
471
|
+
expect(result).to.equal('www.example.com');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should keep www with port', async () => {
|
|
475
|
+
const result = calculateForwardedHost('https://www.example.com:8080/page', log);
|
|
476
|
+
expect(result).to.equal('www.example.com');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should handle URL with query parameters', async () => {
|
|
480
|
+
const result = calculateForwardedHost('https://example.com/page?param=value', log);
|
|
481
|
+
expect(result).to.equal('www.example.com');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should handle URL with hash fragment', async () => {
|
|
485
|
+
const result = calculateForwardedHost('https://example.com/page#section', log);
|
|
486
|
+
expect(result).to.equal('www.example.com');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should handle http protocol', async () => {
|
|
490
|
+
const result = calculateForwardedHost('http://example.com/page', log);
|
|
491
|
+
expect(result).to.equal('www.example.com');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should handle deep subdomains', async () => {
|
|
495
|
+
const result = calculateForwardedHost('https://level1.level2.example.com/page', log);
|
|
496
|
+
expect(result).to.equal('level1.level2.example.com');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should throw error for invalid URL', async () => {
|
|
500
|
+
try {
|
|
501
|
+
calculateForwardedHost('not-a-valid-url', log);
|
|
502
|
+
expect.fail('Should have thrown error');
|
|
503
|
+
} catch (error) {
|
|
504
|
+
expect(error.message).to.include('Error calculating forwarded host from URL');
|
|
505
|
+
expect(error.message).to.include('not-a-valid-url');
|
|
506
|
+
expect(log.error).to.have.been.calledOnce;
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should throw error for empty URL', async () => {
|
|
511
|
+
try {
|
|
512
|
+
calculateForwardedHost('', log);
|
|
513
|
+
expect.fail('Should have thrown error');
|
|
514
|
+
} catch (error) {
|
|
515
|
+
expect(error.message).to.include('Error calculating forwarded host from URL');
|
|
516
|
+
expect(log.error).to.have.been.calledOnce;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('should use console as default logger when not provided', async () => {
|
|
521
|
+
const result = calculateForwardedHost('https://example.com/page');
|
|
522
|
+
expect(result).to.equal('www.example.com');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should handle hostname with no dots (edge case)', async () => {
|
|
526
|
+
// This is an edge case for localhost or single-word hostnames
|
|
527
|
+
const result = calculateForwardedHost('http://localhost/page', log);
|
|
528
|
+
// No dots means dotCount = 0, so it won't match dotCount === 1, keeps as is
|
|
529
|
+
expect(result).to.equal('localhost');
|
|
530
|
+
});
|
|
531
|
+
});
|
|
432
532
|
});
|