@adobe/spacecat-shared-tokowaka-client 1.4.0 → 1.4.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,10 @@
1
+ # [@adobe/spacecat-shared-tokowaka-client-v1.4.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.4.0...@adobe/spacecat-shared-tokowaka-client-v1.4.1) (2025-12-12)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * fastly CDN cache invalidation ([#1240](https://github.com/adobe/spacecat-shared/issues/1240)) ([fbb43e4](https://github.com/adobe/spacecat-shared/commit/fbb43e411053c6dc71255fc3883255785c301659))
7
+
1
8
  # [@adobe/spacecat-shared-tokowaka-client-v1.4.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.3.2...@adobe/spacecat-shared-tokowaka-client-v1.4.0) (2025-12-11)
2
9
 
3
10
 
package/README.md CHANGED
@@ -28,8 +28,8 @@ Creates a client instance from a context object.
28
28
  - `context.log` (Object, optional): Logger instance
29
29
  - `context.env.TOKOWAKA_SITE_CONFIG_BUCKET` (string): S3 bucket name for deployed configurations
30
30
  - `context.env.TOKOWAKA_PREVIEW_BUCKET` (string): S3 bucket name for preview configurations
31
- - `context.env.TOKOWAKA_CDN_PROVIDER` (string): CDN provider for cache invalidation
32
- - `context.env.TOKOWAKA_CDN_CONFIG` (string): JSON configuration for CDN client
31
+ - `context.env.TOKOWAKA_CDN_PROVIDER` (string | string[]): CDN provider(s) for cache invalidation
32
+ - `context.env.TOKOWAKA_CDN_CONFIG` (string): JSON configuration for CDN clients
33
33
  - `context.env.TOKOWAKA_EDGE_URL` (string): Tokowaka edge URL for preview HTML fetching
34
34
 
35
35
  ## Environment Variables
@@ -39,8 +39,23 @@ Creates a client instance from a context object.
39
39
  - `TOKOWAKA_PREVIEW_BUCKET` - S3 bucket name for storing preview configurations
40
40
 
41
41
  **Optional (for CDN invalidation):**
42
- - `TOKOWAKA_CDN_PROVIDER` - CDN provider name (e.g., "cloudfront")
43
- - `TOKOWAKA_CDN_CONFIG` - JSON string with CDN-specific configuration. (e.g., { "cloudfront": { "distributionId": <distribution-id>, "region": "us-east-1" }})
42
+ - `TOKOWAKA_CDN_PROVIDER` - CDN provider name(s). Can be a single provider (e.g., "cloudfront") or multiple providers as comma-separated string (e.g., "cloudfront,fastly") or array
43
+ - `TOKOWAKA_CDN_CONFIG` - JSON string with CDN-specific configuration for all providers:
44
+ ```json
45
+ {
46
+ "cloudfront": {
47
+ "distributionId": "<distribution-id>",
48
+ "region": "us-east-1"
49
+ },
50
+ "fastly": {
51
+ "serviceId": "<service-id>",
52
+ "apiToken": "<api-token>",
53
+ "distributionUrl": "https://<cloudfront-distribution>.cloudfront.net"
54
+ }
55
+ }
56
+ ```
57
+
58
+ **Note for Fastly**: The `distributionUrl` is required and should be the full CloudFront distribution URL (e.g., `https://deftbrsarcsf4.cloudfront.net`). Fastly uses this to construct full URLs as surrogate keys for cache purging.
44
59
 
45
60
  **Optional (for preview functionality):**
46
61
  - `TOKOWAKA_EDGE_URL` - Tokowaka edge URL for fetching HTML content during preview
@@ -55,7 +70,7 @@ Generates configuration and uploads to S3 **per URL**. **Automatically fetches e
55
70
 
56
71
  **Returns:** `Promise<DeploymentResult>` with:
57
72
  - `s3Paths` - Array of S3 keys where configs were uploaded (one per URL)
58
- - `cdnInvalidations` - Array of CDN invalidation results (one per URL)
73
+ - `cdnInvalidations` - Array of CDN invalidation results (one per URL per provider)
59
74
  - `succeededSuggestions` - Array of deployed suggestions
60
75
  - `failedSuggestions` - Array of `{suggestion, reason}` objects for ineligible suggestions
61
76
 
@@ -72,7 +87,7 @@ Rolls back previously deployed suggestions by removing their patches from the co
72
87
 
73
88
  **Returns:** `Promise<RollbackResult>` with:
74
89
  - `s3Paths` - Array of S3 keys where configs were uploaded (one per URL)
75
- - `cdnInvalidations` - Array of CDN invalidation results (one per URL)
90
+ - `cdnInvalidations` - Array of CDN invalidation results (one per URL per provider)
76
91
  - `succeededSuggestions` - Array of rolled back suggestions
77
92
  - `failedSuggestions` - Array of `{suggestion, reason}` objects for ineligible suggestions
78
93
  - `removedPatchesCount` - Total number of patches removed across all URLs
@@ -84,7 +99,7 @@ Previews suggestions by uploading to preview S3 path and fetching HTML compariso
84
99
  **Returns:** `Promise<PreviewResult>` with:
85
100
  - `s3Path` - S3 key where preview config was uploaded
86
101
  - `config` - Preview configuration object
87
- - `cdnInvalidation` - CDN invalidation result
102
+ - `cdnInvalidations` - Array of CDN invalidation results (one per provider)
88
103
  - `succeededSuggestions` - Array of previewed suggestions
89
104
  - `failedSuggestions` - Array of `{suggestion, reason}` objects for ineligible suggestions
90
105
  - `html` - Object with `url`, `originalHtml`, and `optimizedHtml`
@@ -235,6 +250,91 @@ Per-URL configuration (flat structure):
235
250
  - The `tokowakaOptimizations` nested structure has been removed
236
251
  - The `tokowakaForceFail` field has been renamed to `forceFail`
237
252
 
253
+ ## CDN Cache Invalidation
254
+
255
+ The client supports multiple CDN providers for cache invalidation with **intelligent batching** for optimal performance.
256
+
257
+ ### Batch Optimization
258
+
259
+ When deploying or rolling back multiple URLs:
260
+ - **Before**: Each URL triggered separate CDN invalidation calls (N URLs = N×2 API calls for 2 providers)
261
+ - **After**: All URLs are collected and invalidated in a single batch call per provider (N URLs = 2 API calls for 2 providers)
262
+
263
+ **Example Performance Improvement:**
264
+ - 10 URLs with CloudFront + Fastly
265
+ - Old: 20 API calls (10 per provider)
266
+ - New: **2 API calls** (1 per provider)
267
+ - **90% reduction in API calls!**
268
+
269
+ ### Supported CDN Providers
270
+
271
+ #### CloudFront
272
+ AWS CloudFront CDN invalidation using the AWS SDK.
273
+
274
+ **Configuration:**
275
+ ```json
276
+ {
277
+ "cloudfront": {
278
+ "distributionId": "E1234567890ABC",
279
+ "region": "us-east-1"
280
+ }
281
+ }
282
+ ```
283
+
284
+ #### Fastly
285
+ Fastly CDN purging using surrogate key purging.
286
+
287
+ **Configuration:**
288
+ ```json
289
+ {
290
+ "fastly": {
291
+ "serviceId": "abc123xyz",
292
+ "apiToken": "your-fastly-api-token"
293
+ }
294
+ }
295
+ ```
296
+
297
+ ### Multiple CDN Providers
298
+
299
+ To invalidate cache on multiple CDNs simultaneously:
300
+
301
+ **Environment Variable:**
302
+ ```bash
303
+ TOKOWAKA_CDN_PROVIDER="cloudfront,fastly"
304
+ # or as array in code: ["cloudfront", "fastly"]
305
+
306
+ TOKOWAKA_CDN_CONFIG='{"cloudfront":{"distributionId":"...","region":"us-east-1"},"fastly":{"serviceId":"...","apiToken":"...","distributionUrl":"https://xxx.cloudfront.net"}}'
307
+ ```
308
+
309
+ **Behavior:**
310
+ - All URLs are batched into a single invalidation request per provider
311
+ - All CDN providers are invalidated in parallel using `Promise.all()`
312
+ - Failures in one CDN don't block others
313
+ - Each CDN returns its own result in the `cdnInvalidations` array
314
+ - Results include status, provider name, and provider-specific details
315
+
316
+ ### Fastly Batch Purging
317
+
318
+ The Fastly client uses surrogate key purging with full CloudFront URLs:
319
+ - **Surrogate Keys**: Constructed as full CloudFront URLs (e.g., `https://xxx.cloudfront.net/opportunities/adobe.com/config`)
320
+ - **Batch Purging**: All paths are sent in a single API call using the `Surrogate-Key` header (space-separated URLs)
321
+ - **Configuration**: Requires `distributionUrl` in the Fastly config to construct full URLs
322
+ - Significantly reduces API calls and improves performance for bulk operations
323
+
324
+ **Example Fastly Configuration:**
325
+ ```json
326
+ {
327
+ "serviceId": "abc123xyz",
328
+ "apiToken": "your-fastly-api-token",
329
+ "distributionUrl": "https://deftbrsarcsf4.cloudfront.net"
330
+ }
331
+ ```
332
+
333
+ This configuration allows Fastly to purge cache entries using URLs like:
334
+ ```
335
+ fastly purge --key https://deftbrsarcsf4.cloudfront.net/opportunities/adobe.com/config
336
+ ```
337
+
238
338
  ## Reference Material
239
339
 
240
340
  https://wiki.corp.adobe.com/display/AEMSites/Tokowaka+-+Spacecat+Integration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tokowaka-client",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Tokowaka Client for SpaceCat - Edge optimization config management",
5
5
  "type": "module",
6
6
  "engines": {
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import CloudFrontCdnClient from './cloudfront-cdn-client.js';
14
+ import FastlyCdnClient from './fastly-cdn-client.js';
14
15
 
15
16
  /**
16
17
  * Registry for CDN clients
@@ -30,6 +31,7 @@ export default class CdnClientRegistry {
30
31
  */
31
32
  #registerDefaultClients() {
32
33
  this.registerClient('cloudfront', CloudFrontCdnClient);
34
+ this.registerClient('fastly', FastlyCdnClient);
33
35
  }
34
36
 
35
37
  /**
@@ -0,0 +1,156 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import BaseCdnClient from './base-cdn-client.js';
14
+
15
+ /**
16
+ * Fastly CDN client implementation
17
+ * Handles cache invalidation for Fastly CDN using surrogate key purging
18
+ */
19
+ export default class FastlyCdnClient extends BaseCdnClient {
20
+ constructor(env, log) {
21
+ super(env, log);
22
+ let parsedConfig = {};
23
+ try {
24
+ parsedConfig = JSON.parse(env.TOKOWAKA_CDN_CONFIG);
25
+ } catch (e) {
26
+ throw new Error('Invalid TOKOWAKA_CDN_CONFIG: must be valid JSON');
27
+ }
28
+
29
+ if (!parsedConfig.fastly) {
30
+ throw new Error("Missing 'fastly' config in TOKOWAKA_CDN_CONFIG");
31
+ }
32
+
33
+ this.cdnConfig = parsedConfig.fastly;
34
+ this.providerName = 'fastly';
35
+ }
36
+
37
+ getProviderName() {
38
+ return this.providerName;
39
+ }
40
+
41
+ validateConfig() {
42
+ // serviceId, apiToken, and distributionUrl are required for Fastly API
43
+ if (!this.cdnConfig.serviceId || !this.cdnConfig.apiToken) {
44
+ this.log.error('Fastly CDN config missing required fields: serviceId and apiToken');
45
+ return false;
46
+ }
47
+
48
+ if (!this.cdnConfig.distributionUrl) {
49
+ this.log.error('Fastly CDN config missing distributionUrl (CloudFront distribution URL)');
50
+ return false;
51
+ }
52
+
53
+ return true;
54
+ }
55
+
56
+ /**
57
+ * Invalidates Fastly CDN cache for given paths using surrogate key purging
58
+ * Constructs full CloudFront URLs as surrogate keys for Fastly purge
59
+ * Works efficiently for both single and multiple paths
60
+ * @param {Array<string>} paths - Array of URL paths to invalidate
61
+ * (e.g., '/opportunities/adobe.com/config')
62
+ * @returns {Promise<Object>} Result of the invalidation request
63
+ */
64
+ async invalidateCache(paths) {
65
+ if (!this.validateConfig()) {
66
+ throw new Error('Invalid Fastly CDN configuration');
67
+ }
68
+
69
+ if (!Array.isArray(paths) || paths.length === 0) {
70
+ this.log.warn('No paths provided for cache invalidation');
71
+ return { status: 'skipped', message: 'No paths to invalidate' };
72
+ }
73
+
74
+ this.log.info(`Starting Fastly cache invalidation for ${paths.length} path(s)`);
75
+
76
+ // Construct full CloudFront URLs as surrogate keys
77
+ // Example: "https://deftbrsarcsf4.cloudfront.net/opportunities/adobe.com/config"
78
+ const surrogateKeys = paths.map((path) => {
79
+ // Ensure path starts with /
80
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
81
+ // Combine CloudFront distribution URL with path
82
+ const fullUrl = `${this.cdnConfig.distributionUrl}${normalizedPath}`;
83
+ return fullUrl;
84
+ });
85
+
86
+ this.log.debug(`Generated ${surrogateKeys.length} surrogate key(s) for purge`);
87
+ this.log.debug(`Surrogate keys: ${surrogateKeys.join(', ')}`);
88
+
89
+ const startTime = Date.now();
90
+ const apiEndpoint = `https://api.fastly.com/service/${this.cdnConfig.serviceId}/purge`;
91
+ this.log.debug(`Calling Fastly API: POST ${apiEndpoint}`);
92
+ try {
93
+ // Fastly batch purge using Surrogate-Key header (space-separated)
94
+ // Works for both single and multiple keys
95
+ const response = await fetch(
96
+ `https://api.fastly.com/service/${this.cdnConfig.serviceId}/purge`,
97
+ {
98
+ method: 'POST',
99
+ headers: {
100
+ 'Fastly-Key': this.cdnConfig.apiToken,
101
+ 'Surrogate-Key': surrogateKeys.join(' '),
102
+ Accept: 'application/json',
103
+ },
104
+ },
105
+ );
106
+
107
+ if (!response.ok) {
108
+ const errorText = await response.text();
109
+ this.log.error(`Failed to purge Fastly cache: ${response.status} - ${errorText}`);
110
+ return {
111
+ status: 'failed',
112
+ provider: 'fastly',
113
+ serviceId: this.cdnConfig.serviceId,
114
+ totalPaths: paths.length,
115
+ totalKeys: surrogateKeys.length,
116
+ successCount: 0,
117
+ failedCount: surrogateKeys.length,
118
+ error: errorText,
119
+ duration: Date.now() - startTime,
120
+ };
121
+ }
122
+
123
+ const result = await response.json();
124
+ const duration = Date.now() - startTime;
125
+ this.log.info(
126
+ `Successfully purged ${surrogateKeys.length} Fastly cache key(s) `
127
+ + `for service ${this.cdnConfig.serviceId} (took ${duration}ms)`,
128
+ );
129
+
130
+ return {
131
+ status: 'success',
132
+ provider: 'fastly',
133
+ serviceId: this.cdnConfig.serviceId,
134
+ totalPaths: paths.length,
135
+ totalKeys: surrogateKeys.length,
136
+ successCount: surrogateKeys.length,
137
+ failedCount: 0,
138
+ purgeId: result.id,
139
+ duration,
140
+ };
141
+ } catch (error) {
142
+ this.log.error(`Error purging Fastly cache: ${error.message}`, error);
143
+ return {
144
+ status: 'error',
145
+ provider: 'fastly',
146
+ serviceId: this.cdnConfig.serviceId,
147
+ totalPaths: paths.length,
148
+ totalKeys: surrogateKeys.length,
149
+ successCount: 0,
150
+ failedCount: surrogateKeys.length,
151
+ error: error.message,
152
+ duration: Date.now() - startTime,
153
+ };
154
+ }
155
+ }
156
+ }
package/src/index.d.ts CHANGED
@@ -49,23 +49,39 @@ export interface TokowakaConfig {
49
49
 
50
50
  export interface CdnInvalidationResult {
51
51
  status: string;
52
- provider?: string;
52
+ provider: string;
53
53
  purgeId?: string;
54
+ invalidationId?: string;
55
+ invalidationStatus?: string;
56
+ createTime?: string;
54
57
  estimatedSeconds?: number;
55
58
  paths?: number;
59
+ totalPaths?: number;
60
+ totalKeys?: number;
61
+ successCount?: number;
62
+ failedCount?: number;
63
+ serviceId?: string;
64
+ duration?: number;
65
+ results?: Array<{
66
+ key: string;
67
+ status: string;
68
+ statusCode?: number;
69
+ error?: string;
70
+ }>;
56
71
  message?: string;
72
+ error?: string;
57
73
  }
58
74
 
59
75
  export interface DeploymentResult {
60
76
  s3Paths: string[];
61
- cdnInvalidations: (CdnInvalidationResult | null)[];
77
+ cdnInvalidations: CdnInvalidationResult[];
62
78
  succeededSuggestions: Array<any>;
63
79
  failedSuggestions: Array<{ suggestion: any; reason: string }>;
64
80
  }
65
81
 
66
82
  export interface RollbackResult {
67
83
  s3Paths: string[];
68
- cdnInvalidations: (CdnInvalidationResult | null)[];
84
+ cdnInvalidations: CdnInvalidationResult[];
69
85
  succeededSuggestions: Array<any>;
70
86
  failedSuggestions: Array<{ suggestion: any; reason: string }>;
71
87
  removedPatchesCount: number;
@@ -74,7 +90,7 @@ export interface RollbackResult {
74
90
  export interface PreviewResult {
75
91
  s3Path: string;
76
92
  config: TokowakaConfig;
77
- cdnInvalidation: CdnInvalidationResult | null;
93
+ cdnInvalidations: CdnInvalidationResult[];
78
94
  succeededSuggestions: Array<any>;
79
95
  failedSuggestions: Array<{ suggestion: any; reason: string }>;
80
96
  html: {
@@ -300,14 +316,27 @@ export class CloudFrontCdnClient extends BaseCdnClient {
300
316
  invalidateCache(paths: string[]): Promise<CdnInvalidationResult>;
301
317
  }
302
318
 
319
+ /**
320
+ * Fastly CDN client implementation
321
+ */
322
+ export class FastlyCdnClient extends BaseCdnClient {
323
+ constructor(env: {
324
+ TOKOWAKA_CDN_CONFIG: string; // JSON string with fastly config
325
+ }, log: any);
326
+
327
+ getProviderName(): string;
328
+ validateConfig(): boolean;
329
+ invalidateCache(paths: string[]): Promise<CdnInvalidationResult>;
330
+ }
331
+
303
332
  /**
304
333
  * Registry for CDN clients
305
334
  */
306
335
  export class CdnClientRegistry {
307
- constructor(log: any);
336
+ constructor(env: Record<string, any>, log: any);
308
337
 
309
338
  registerClient(provider: string, ClientClass: typeof BaseCdnClient): void;
310
- getClient(provider: string, config: Record<string, any>): BaseCdnClient | null;
339
+ getClient(provider: string): BaseCdnClient | null;
311
340
  getSupportedProviders(): string[];
312
341
  isProviderSupported(provider: string): boolean;
313
342
  }
@@ -327,8 +356,8 @@ export default class TokowakaClient {
327
356
  env: {
328
357
  TOKOWAKA_SITE_CONFIG_BUCKET: string;
329
358
  TOKOWAKA_PREVIEW_BUCKET?: string;
330
- TOKOWAKA_CDN_PROVIDER?: string;
331
- TOKOWAKA_CDN_CONFIG?: string;
359
+ TOKOWAKA_CDN_PROVIDER?: string | string[]; // Single provider or comma-separated list
360
+ TOKOWAKA_CDN_CONFIG?: string; // JSON with cloudfront and/or fastly config
332
361
  TOKOWAKA_EDGE_URL?: string;
333
362
  };
334
363
  log?: any;
@@ -358,12 +387,12 @@ export default class TokowakaClient {
358
387
  /**
359
388
  * Fetches domain-level metaconfig from S3
360
389
  */
361
- fetchMetaconfig(url: string, isPreview?: boolean): Promise<TokowakaMetaconfig | null>;
390
+ fetchMetaconfig(url: string): Promise<TokowakaMetaconfig | null>;
362
391
 
363
392
  /**
364
393
  * Uploads domain-level metaconfig to S3
365
394
  */
366
- uploadMetaconfig(url: string, metaconfig: TokowakaMetaconfig, isPreview?: boolean): Promise<string>;
395
+ uploadMetaconfig(url: string, metaconfig: TokowakaMetaconfig): Promise<string>;
367
396
 
368
397
  /**
369
398
  * Merges existing configuration with new configuration
@@ -372,8 +401,15 @@ export default class TokowakaClient {
372
401
 
373
402
  /**
374
403
  * Invalidates CDN cache for a specific URL
404
+ * Supports multiple CDN providers in parallel
405
+ */
406
+ invalidateCdnCache(url: string, providers?: string | string[], isPreview?: boolean): Promise<CdnInvalidationResult[]>;
407
+
408
+ /**
409
+ * Batch invalidates CDN cache for multiple URLs at once
410
+ * More efficient than individual invalidations when processing multiple URLs
375
411
  */
376
- invalidateCdnCache(url: string, provider?: string, isPreview?: boolean): Promise<CdnInvalidationResult | null>;
412
+ batchInvalidateCdnCache(urls: string[], providers?: string | string[], isPreview?: boolean): Promise<CdnInvalidationResult[]>;
377
413
 
378
414
  /**
379
415
  * Deploys suggestions to Tokowaka edge