@adobe/spacecat-shared-tokowaka-client 1.19.0 → 1.20.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 +12 -0
- package/package.json +4 -1
- package/src/cdn/cloudfront/edge-code.js +152 -0
- package/src/cdn/cloudfront/index.js +1863 -0
- package/src/index.d.ts +262 -1
- package/src/index.js +108 -6
- package/test/cdn/cloudfront/index.test.js +4185 -0
- package/test/index.test.js +212 -0
package/src/index.d.ts
CHANGED
|
@@ -155,6 +155,268 @@ export class FastlyKVClient {
|
|
|
155
155
|
*/
|
|
156
156
|
export function calculateForwardedHost(url: string, logger?: { debug?: (msg: string) => void; error?: (msg: string) => void }): string;
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Temporary AWS credentials returned by {@link assumeConnectorRole}.
|
|
160
|
+
*/
|
|
161
|
+
export interface AWSCredentials {
|
|
162
|
+
accessKeyId: string;
|
|
163
|
+
secretAccessKey: string;
|
|
164
|
+
sessionToken: string;
|
|
165
|
+
expiration?: Date;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* One row of the Edge Optimize deploy/plan step contract.
|
|
170
|
+
*/
|
|
171
|
+
export interface DeployStep {
|
|
172
|
+
key: string;
|
|
173
|
+
label: string;
|
|
174
|
+
status?: string;
|
|
175
|
+
action?: string;
|
|
176
|
+
detail?: string;
|
|
177
|
+
probe?: Record<string, any>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── CloudFront "Optimize at Edge" control-plane (free functions) ──────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Assume the customer's cross-account connector role and return short-lived credentials.
|
|
184
|
+
*/
|
|
185
|
+
export function assumeConnectorRole(params: {
|
|
186
|
+
accountId: string;
|
|
187
|
+
externalId: string;
|
|
188
|
+
roleName?: string;
|
|
189
|
+
region?: string;
|
|
190
|
+
}): Promise<{ roleArn: string; accountId: string; credentials: AWSCredentials }>;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* List the CloudFront distributions in the customer account using assumed-role credentials.
|
|
194
|
+
*/
|
|
195
|
+
export function listDistributions(
|
|
196
|
+
credentials: AWSCredentials,
|
|
197
|
+
region?: string,
|
|
198
|
+
): Promise<Array<{
|
|
199
|
+
id: string;
|
|
200
|
+
domainName: string;
|
|
201
|
+
aliases: string[];
|
|
202
|
+
status: string;
|
|
203
|
+
enabled: boolean;
|
|
204
|
+
comment: string;
|
|
205
|
+
}>>;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Fetch a single CloudFront distribution's configuration using assumed-role credentials.
|
|
209
|
+
*/
|
|
210
|
+
export function getDistributionConfig(
|
|
211
|
+
credentials: AWSCredentials,
|
|
212
|
+
distributionId: string,
|
|
213
|
+
region?: string,
|
|
214
|
+
): Promise<{
|
|
215
|
+
origins: Array<{ id: string; domainName: string; originPath: string }>;
|
|
216
|
+
defaultCacheBehavior: { pathPattern: string; targetOriginId: string } | null;
|
|
217
|
+
cacheBehaviors: Array<{ pathPattern: string; targetOriginId: string }>;
|
|
218
|
+
}>;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Add the Edge Optimize origin to a CloudFront distribution (idempotent + self-healing).
|
|
222
|
+
*/
|
|
223
|
+
export function createOrigin(
|
|
224
|
+
credentials: AWSCredentials,
|
|
225
|
+
distributionId: string,
|
|
226
|
+
originDomain?: string,
|
|
227
|
+
headers?: { apiKey?: string; forwardedHost?: string; fetcherKey?: string },
|
|
228
|
+
region?: string,
|
|
229
|
+
): Promise<{ created: boolean; alreadyExisted: boolean; updated: boolean; originId: string }>;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create or update the routing CloudFront Function and publish it to LIVE (idempotent).
|
|
233
|
+
*/
|
|
234
|
+
export function createCloudFrontFunction(
|
|
235
|
+
credentials: AWSCredentials,
|
|
236
|
+
defaultOriginId: string,
|
|
237
|
+
distributionId: string,
|
|
238
|
+
targetedPaths?: string[] | null,
|
|
239
|
+
region?: string,
|
|
240
|
+
): Promise<{ name: string; created: boolean; stage: string }>;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Add the Edge Optimize routing headers to the cache key for the target behavior.
|
|
244
|
+
*/
|
|
245
|
+
export function updateCacheSettings(
|
|
246
|
+
credentials: AWSCredentials,
|
|
247
|
+
distributionId: string,
|
|
248
|
+
pathPattern: string,
|
|
249
|
+
opts?: { setMinTTLZero?: boolean; region?: string },
|
|
250
|
+
): Promise<{
|
|
251
|
+
scenario: string;
|
|
252
|
+
policyId: string | null;
|
|
253
|
+
updated: boolean;
|
|
254
|
+
alreadyForwarded: boolean;
|
|
255
|
+
reused?: boolean;
|
|
256
|
+
}>;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Create (or update) the Edge Optimize Lambda@Edge function and publish a version (idempotent).
|
|
260
|
+
*/
|
|
261
|
+
export function createLambdaAtEdge(
|
|
262
|
+
credentials: AWSCredentials,
|
|
263
|
+
accountId: string,
|
|
264
|
+
opts?: {
|
|
265
|
+
region?: string;
|
|
266
|
+
distributionId?: string;
|
|
267
|
+
originDomain?: string;
|
|
268
|
+
retryDelayMs?: number;
|
|
269
|
+
},
|
|
270
|
+
): Promise<{
|
|
271
|
+
status: string;
|
|
272
|
+
functionArn?: string;
|
|
273
|
+
versionArn: string | null;
|
|
274
|
+
version?: string;
|
|
275
|
+
roleArn: string;
|
|
276
|
+
created: boolean;
|
|
277
|
+
alreadyExisted?: boolean;
|
|
278
|
+
}>;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Read-only status of the Edge Optimize Lambda@Edge function and its execution role.
|
|
282
|
+
*/
|
|
283
|
+
export function getLambdaAtEdgeStatus(
|
|
284
|
+
credentials: AWSCredentials,
|
|
285
|
+
distributionId: string,
|
|
286
|
+
region?: string,
|
|
287
|
+
): Promise<{
|
|
288
|
+
exists: boolean;
|
|
289
|
+
roleExists: boolean;
|
|
290
|
+
roleOk: boolean;
|
|
291
|
+
state?: string;
|
|
292
|
+
lastUpdateStatus?: string;
|
|
293
|
+
functionArn?: string;
|
|
294
|
+
versionArn: string | null;
|
|
295
|
+
version?: string;
|
|
296
|
+
ready: boolean;
|
|
297
|
+
}>;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Wire the routing CloudFront Function and the Lambda@Edge function onto a cache behavior.
|
|
301
|
+
*/
|
|
302
|
+
export function applyAssociations(
|
|
303
|
+
credentials: AWSCredentials,
|
|
304
|
+
distributionId: string,
|
|
305
|
+
pathPattern: string,
|
|
306
|
+
lambdaVersionArn: string,
|
|
307
|
+
region?: string,
|
|
308
|
+
): Promise<{ cloudFrontFunctionArn: string; lambdaArn: string }>;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Verify Edge Optimize routing end-to-end by probing as a bot and as a human.
|
|
312
|
+
*/
|
|
313
|
+
export function verifyRouting(url: string): Promise<{
|
|
314
|
+
passed: boolean;
|
|
315
|
+
requestId: string | null;
|
|
316
|
+
details: { bot: Record<string, any>; human: Record<string, any> };
|
|
317
|
+
}>;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Run one poll of the idempotent Edge Optimize "Deploy routing" orchestrator.
|
|
321
|
+
*/
|
|
322
|
+
export function runDeployStep(
|
|
323
|
+
credentials: AWSCredentials,
|
|
324
|
+
params: {
|
|
325
|
+
distributionId: string;
|
|
326
|
+
originId: string;
|
|
327
|
+
behavior: string;
|
|
328
|
+
originDomain?: string;
|
|
329
|
+
originHeaders?: { apiKey?: string; forwardedHost?: string; fetcherKey?: string };
|
|
330
|
+
accountId: string;
|
|
331
|
+
},
|
|
332
|
+
region?: string,
|
|
333
|
+
): Promise<{ routingDeployed: boolean; verified: boolean; steps: DeployStep[] }>;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Read-only "preview" of what {@link runDeployStep} would do, without mutating.
|
|
337
|
+
*/
|
|
338
|
+
export function planDeploy(
|
|
339
|
+
credentials: AWSCredentials,
|
|
340
|
+
params: {
|
|
341
|
+
distributionId: string;
|
|
342
|
+
originId?: string;
|
|
343
|
+
behavior: string;
|
|
344
|
+
originDomain?: string;
|
|
345
|
+
originHeaders?: { apiKey?: string; forwardedHost?: string; fetcherKey?: string };
|
|
346
|
+
accountId?: string;
|
|
347
|
+
},
|
|
348
|
+
region?: string,
|
|
349
|
+
): Promise<{ canProceed: boolean; blocker: string | null; steps: DeployStep[] }>;
|
|
350
|
+
|
|
351
|
+
export interface CloudFrontEdgeClientOptions {
|
|
352
|
+
credentials: AWSCredentials;
|
|
353
|
+
region?: string;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* CloudFront control-plane client initialized with short-lived AWS credentials.
|
|
358
|
+
*/
|
|
359
|
+
export class CloudFrontEdgeClient {
|
|
360
|
+
constructor(options: CloudFrontEdgeClientOptions);
|
|
361
|
+
|
|
362
|
+
listDistributions(): ReturnType<typeof listDistributions>;
|
|
363
|
+
|
|
364
|
+
getDistributionConfig(distributionId: string): ReturnType<typeof getDistributionConfig>;
|
|
365
|
+
|
|
366
|
+
createOrigin(
|
|
367
|
+
distributionId: string,
|
|
368
|
+
originDomain?: string,
|
|
369
|
+
headers?: { apiKey?: string; forwardedHost?: string; fetcherKey?: string },
|
|
370
|
+
): ReturnType<typeof createOrigin>;
|
|
371
|
+
|
|
372
|
+
createCloudFrontFunction(
|
|
373
|
+
defaultOriginId: string,
|
|
374
|
+
distributionId: string,
|
|
375
|
+
targetedPaths?: string[] | null,
|
|
376
|
+
): ReturnType<typeof createCloudFrontFunction>;
|
|
377
|
+
|
|
378
|
+
updateCacheSettings(
|
|
379
|
+
distributionId: string,
|
|
380
|
+
pathPattern: string,
|
|
381
|
+
opts?: { setMinTTLZero?: boolean; region?: string },
|
|
382
|
+
): ReturnType<typeof updateCacheSettings>;
|
|
383
|
+
|
|
384
|
+
createLambdaAtEdge(
|
|
385
|
+
accountId: string,
|
|
386
|
+
opts?: {
|
|
387
|
+
region?: string;
|
|
388
|
+
distributionId?: string;
|
|
389
|
+
originDomain?: string;
|
|
390
|
+
retryDelayMs?: number;
|
|
391
|
+
},
|
|
392
|
+
): ReturnType<typeof createLambdaAtEdge>;
|
|
393
|
+
|
|
394
|
+
getLambdaAtEdgeStatus(distributionId: string): ReturnType<typeof getLambdaAtEdgeStatus>;
|
|
395
|
+
|
|
396
|
+
applyAssociations(
|
|
397
|
+
distributionId: string,
|
|
398
|
+
pathPattern: string,
|
|
399
|
+
lambdaVersionArn: string,
|
|
400
|
+
): ReturnType<typeof applyAssociations>;
|
|
401
|
+
|
|
402
|
+
runDeployStep(params: Parameters<typeof runDeployStep>[1]): ReturnType<typeof runDeployStep>;
|
|
403
|
+
|
|
404
|
+
planDeploy(params: Parameters<typeof planDeploy>[1]): ReturnType<typeof planDeploy>;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Build the CloudFront Function (viewer-request) routing code.
|
|
409
|
+
*/
|
|
410
|
+
export function buildCloudfrontFunctionCode(
|
|
411
|
+
defaultOriginId: string,
|
|
412
|
+
targetedPaths?: string[] | null,
|
|
413
|
+
): string;
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Build an in-memory zip containing a single file (used to package the Lambda@Edge code).
|
|
417
|
+
*/
|
|
418
|
+
export function buildLambdaZip(filename: string, content: string | Buffer): Buffer;
|
|
419
|
+
|
|
158
420
|
/**
|
|
159
421
|
* Base class for opportunity mappers
|
|
160
422
|
* Extend this class to create custom mappers for new opportunity types
|
|
@@ -481,4 +743,3 @@ export default class TokowakaClient {
|
|
|
481
743
|
*/
|
|
482
744
|
getSupportedOpportunityTypes(): string[];
|
|
483
745
|
}
|
|
484
|
-
|
package/src/index.js
CHANGED
|
@@ -51,6 +51,25 @@ import {
|
|
|
51
51
|
export { FastlyKVClient } from './fastly-kv-client.js';
|
|
52
52
|
export { calculateForwardedHost } from './utils/custom-html-utils.js';
|
|
53
53
|
|
|
54
|
+
// CloudFront control-plane (free functions + constants).
|
|
55
|
+
export {
|
|
56
|
+
assumeConnectorRole,
|
|
57
|
+
listDistributions,
|
|
58
|
+
getDistributionConfig,
|
|
59
|
+
createOrigin,
|
|
60
|
+
createCloudFrontFunction,
|
|
61
|
+
updateCacheSettings,
|
|
62
|
+
createLambdaAtEdge,
|
|
63
|
+
getLambdaAtEdgeStatus,
|
|
64
|
+
applyAssociations,
|
|
65
|
+
verifyRouting,
|
|
66
|
+
runDeployStep,
|
|
67
|
+
planDeploy,
|
|
68
|
+
CloudFrontEdgeClient,
|
|
69
|
+
buildCloudfrontFunctionCode,
|
|
70
|
+
buildLambdaZip,
|
|
71
|
+
} from './cdn/cloudfront/index.js';
|
|
72
|
+
|
|
54
73
|
const HTTP_BAD_REQUEST = 400;
|
|
55
74
|
const HTTP_INTERNAL_SERVER_ERROR = 500;
|
|
56
75
|
const HTTP_NOT_IMPLEMENTED = 501;
|
|
@@ -712,7 +731,8 @@ class TokowakaClient {
|
|
|
712
731
|
* @param {Array} suggestions - Array of suggestion entities to deploy
|
|
713
732
|
* @returns {Promise<Object>} - Deployment result with succeeded/failed suggestions
|
|
714
733
|
*/
|
|
715
|
-
async deploySuggestions(site, opportunity, suggestions) {
|
|
734
|
+
async deploySuggestions(site, opportunity, suggestions, metadata = {}) {
|
|
735
|
+
const { applyStale = false } = metadata;
|
|
716
736
|
const opportunityType = opportunity.getType();
|
|
717
737
|
const baseURL = getEffectiveBaseURL(site);
|
|
718
738
|
const mapper = this.mapperRegistry.getMapper(opportunityType);
|
|
@@ -788,6 +808,10 @@ class TokowakaClient {
|
|
|
788
808
|
continue;
|
|
789
809
|
}
|
|
790
810
|
|
|
811
|
+
if (applyStale && newConfig.patches?.length > 0) {
|
|
812
|
+
newConfig.patches = newConfig.patches.map((patch) => ({ ...patch, applyStale: true }));
|
|
813
|
+
}
|
|
814
|
+
|
|
791
815
|
// Merge with existing config for this URL
|
|
792
816
|
const config = this.mergeConfigs(existingConfig, newConfig);
|
|
793
817
|
|
|
@@ -828,8 +852,15 @@ class TokowakaClient {
|
|
|
828
852
|
* @returns {Promise<number>} Number of patches removed
|
|
829
853
|
* @private
|
|
830
854
|
*/
|
|
831
|
-
|
|
832
|
-
|
|
855
|
+
async #rollbackPerUrlConfig(
|
|
856
|
+
fullUrl,
|
|
857
|
+
urlSuggestions,
|
|
858
|
+
opportunity,
|
|
859
|
+
mapper,
|
|
860
|
+
opportunityType,
|
|
861
|
+
s3Paths,
|
|
862
|
+
rolledBackUrls,
|
|
863
|
+
) {
|
|
833
864
|
const existingConfig = await this.fetchConfig(fullUrl);
|
|
834
865
|
if (!existingConfig) {
|
|
835
866
|
this.log.warn(`No existing configuration found for URL: ${fullUrl}`);
|
|
@@ -1427,9 +1458,11 @@ class TokowakaClient {
|
|
|
1427
1458
|
* @returns {Promise<{ succeededSuggestions: Array, failedSuggestions: Array }>}
|
|
1428
1459
|
* @private
|
|
1429
1460
|
*/
|
|
1430
|
-
|
|
1461
|
+
// eslint-disable-next-line max-len
|
|
1462
|
+
async #deployPerUrlSuggestions(site, opportunity, validSuggestions, updatedBy, metadata = {}) {
|
|
1431
1463
|
try {
|
|
1432
|
-
|
|
1464
|
+
// eslint-disable-next-line max-len
|
|
1465
|
+
const result = await this.deploySuggestions(site, opportunity, validSuggestions, metadata);
|
|
1433
1466
|
const deploymentTimestamp = Date.now();
|
|
1434
1467
|
|
|
1435
1468
|
const succeeded = result.succeededSuggestions.map((s) => {
|
|
@@ -1590,6 +1623,7 @@ class TokowakaClient {
|
|
|
1590
1623
|
targetSuggestions,
|
|
1591
1624
|
allSuggestions,
|
|
1592
1625
|
updatedBy = 'edge-deploy',
|
|
1626
|
+
metadata = {},
|
|
1593
1627
|
}) {
|
|
1594
1628
|
// Step 1: classify suggestions into pattern vs per-URL buckets.
|
|
1595
1629
|
// eslint-disable-next-line max-len
|
|
@@ -1609,7 +1643,7 @@ class TokowakaClient {
|
|
|
1609
1643
|
// Step 3: deploy per-URL suggestions via S3.
|
|
1610
1644
|
if (validSuggestions.length > 0) {
|
|
1611
1645
|
// eslint-disable-next-line max-len
|
|
1612
|
-
const result = await this.#deployPerUrlSuggestions(site, opportunity, validSuggestions, updatedBy);
|
|
1646
|
+
const result = await this.#deployPerUrlSuggestions(site, opportunity, validSuggestions, updatedBy, metadata);
|
|
1613
1647
|
const { succeededSuggestions: perUrlSucceeded, failedSuggestions: perUrlFailed } = result;
|
|
1614
1648
|
succeededSuggestions = perUrlSucceeded;
|
|
1615
1649
|
failedSuggestions.push(...perUrlFailed);
|
|
@@ -1698,6 +1732,74 @@ class TokowakaClient {
|
|
|
1698
1732
|
|
|
1699
1733
|
return { succeededSuggestions, failedSuggestions, coveredSuggestions };
|
|
1700
1734
|
}
|
|
1735
|
+
|
|
1736
|
+
/**
|
|
1737
|
+
* Strips the `applyStale` flag from patches in S3 for the given per-URL suggestions.
|
|
1738
|
+
* Called when an experiment completes so edge reverts to normal (non-stale) behaviour.
|
|
1739
|
+
* Pattern / domain-wide suggestions are skipped — they have no per-URL S3 config.
|
|
1740
|
+
* CDN cache is invalidated for every URL whose config was actually modified.
|
|
1741
|
+
*
|
|
1742
|
+
* @param {Object} site - Site entity
|
|
1743
|
+
* @param {Object} opportunity - Opportunity entity
|
|
1744
|
+
* @param {Array} suggestions - Suggestion entities to clear (may include pattern suggestions,
|
|
1745
|
+
* which are silently skipped)
|
|
1746
|
+
* @returns {Promise<void>}
|
|
1747
|
+
*/
|
|
1748
|
+
async clearApplyStaleFromPatches(site, opportunity, suggestions) {
|
|
1749
|
+
const baseURL = getEffectiveBaseURL(site);
|
|
1750
|
+
|
|
1751
|
+
const perUrlSuggestions = suggestions.filter((s) => {
|
|
1752
|
+
const data = s.getData();
|
|
1753
|
+
return data?.url && !Array.isArray(data?.allowedRegexPatterns);
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
if (perUrlSuggestions.length === 0) {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
const suggestionsByUrl = groupSuggestionsByUrlPath(perUrlSuggestions, baseURL, this.log);
|
|
1761
|
+
const clearedUrls = [];
|
|
1762
|
+
|
|
1763
|
+
for (const [urlPath, urlSuggestions] of Object.entries(suggestionsByUrl)) {
|
|
1764
|
+
const fullUrl = new URL(urlPath, baseURL).toString();
|
|
1765
|
+
try {
|
|
1766
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1767
|
+
const existingConfig = await this.fetchConfig(fullUrl);
|
|
1768
|
+
if (!existingConfig?.patches?.length) {
|
|
1769
|
+
// eslint-disable-next-line no-continue
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
const suggestionIds = new Set(urlSuggestions.map((s) => s.getId()));
|
|
1774
|
+
let modified = false;
|
|
1775
|
+
const updatedPatches = existingConfig.patches.map((patch) => {
|
|
1776
|
+
if (suggestionIds.has(patch.suggestionId) && patch.applyStale) {
|
|
1777
|
+
modified = true;
|
|
1778
|
+
const { applyStale: _, ...rest } = patch;
|
|
1779
|
+
return rest;
|
|
1780
|
+
}
|
|
1781
|
+
return patch;
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
if (modified) {
|
|
1785
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1786
|
+
await this.uploadConfig(fullUrl, { ...existingConfig, patches: updatedPatches });
|
|
1787
|
+
clearedUrls.push(fullUrl);
|
|
1788
|
+
}
|
|
1789
|
+
} catch (err) {
|
|
1790
|
+
this.log.error(`[clear-apply-stale-failed] Failed to process URL ${fullUrl}: ${err.message}`, err);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
if (clearedUrls.length > 0) {
|
|
1795
|
+
try {
|
|
1796
|
+
await this.invalidateCdnCache({ urls: clearedUrls });
|
|
1797
|
+
this.log.info(`[clear-apply-stale] Cleared applyStale from ${clearedUrls.length} URL(s) and invalidated CDN`);
|
|
1798
|
+
} catch (err) {
|
|
1799
|
+
this.log.warn(`[clear-apply-stale-failed] CDN invalidation failed for ${clearedUrls.length} URL(s), S3 configs already updated: ${err.message}`, err);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1701
1803
|
}
|
|
1702
1804
|
|
|
1703
1805
|
// Export the client as default and base classes for custom implementations
|