@adobe/spacecat-shared-tokowaka-client 1.0.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.
@@ -0,0 +1,87 @@
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 CloudFrontCdnClient from './cloudfront-cdn-client.js';
14
+
15
+ /**
16
+ * Registry for CDN clients
17
+ * Manages different CDN provider implementations
18
+ */
19
+ export default class CdnClientRegistry {
20
+ constructor(env, log) {
21
+ this.env = env;
22
+ this.log = log;
23
+ this.clients = new Map();
24
+ this.#registerDefaultClients();
25
+ }
26
+
27
+ /**
28
+ * Registers default CDN clients
29
+ * @private
30
+ */
31
+ #registerDefaultClients() {
32
+ this.registerClient('cloudfront', CloudFrontCdnClient);
33
+ }
34
+
35
+ /**
36
+ * Registers a CDN client class
37
+ * @param {string} provider - CDN provider name
38
+ * @param {Class} ClientClass - CDN client class
39
+ */
40
+ registerClient(provider, ClientClass) {
41
+ this.clients.set(provider.toLowerCase(), ClientClass);
42
+ }
43
+
44
+ /**
45
+ * Gets a CDN client instance for the specified provider
46
+ * @param {string} provider - CDN provider name
47
+ * @param {Object} config - CDN configuration
48
+ * @returns {BaseCdnClient|null} CDN client instance or null if not found
49
+ */
50
+ getClient(provider) {
51
+ if (!provider) {
52
+ this.log.warn('No CDN provider specified');
53
+ return null;
54
+ }
55
+
56
+ const ClientClass = this.clients.get(provider.toLowerCase());
57
+
58
+ if (!ClientClass) {
59
+ this.log.warn(`No CDN client found for provider: ${provider}`);
60
+ return null;
61
+ }
62
+
63
+ try {
64
+ return new ClientClass(this.env, this.log);
65
+ } catch (error) {
66
+ this.log.error(`Failed to create CDN client for ${provider}: ${error.message}`, error);
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Gets list of supported CDN providers
73
+ * @returns {Array<string>} List of provider names
74
+ */
75
+ getSupportedProviders() {
76
+ return Array.from(this.clients.keys());
77
+ }
78
+
79
+ /**
80
+ * Checks if a provider is supported
81
+ * @param {string} provider - CDN provider name
82
+ * @returns {boolean} True if provider is supported
83
+ */
84
+ isProviderSupported(provider) {
85
+ return this.clients.has(provider?.toLowerCase());
86
+ }
87
+ }
@@ -0,0 +1,128 @@
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 {
14
+ CloudFrontClient,
15
+ CreateInvalidationCommand,
16
+ } from '@aws-sdk/client-cloudfront';
17
+ import BaseCdnClient from './base-cdn-client.js';
18
+
19
+ /**
20
+ * CloudFront CDN client implementation
21
+ * Handles cache invalidation for AWS CloudFront
22
+ */
23
+ export default class CloudFrontCdnClient extends BaseCdnClient {
24
+ constructor(env, log) {
25
+ super(env, log);
26
+ let parsedConfig = {};
27
+ try {
28
+ parsedConfig = JSON.parse(env.TOKOWAKA_CDN_CONFIG);
29
+ } catch (e) {
30
+ throw new Error('Invalid TOKOWAKA_CDN_CONFIG: must be valid JSON');
31
+ }
32
+
33
+ if (!parsedConfig.cloudfront) {
34
+ throw new Error("Missing 'cloudfront' config in TOKOWAKA_CDN_CONFIG");
35
+ }
36
+
37
+ this.cdnConfig = parsedConfig.cloudfront;
38
+ this.client = null;
39
+ this.providerName = 'cloudfront';
40
+ }
41
+
42
+ getProviderName() {
43
+ return this.providerName;
44
+ }
45
+
46
+ validateConfig() {
47
+ // Only distributionId is required - credentials are optional when running on Lambda
48
+ if (!this.cdnConfig.distributionId || !this.cdnConfig.region) {
49
+ this.log.error('CloudFront CDN config missing required fields: distributionId and region');
50
+ return false;
51
+ }
52
+
53
+ return true;
54
+ }
55
+
56
+ /**
57
+ * Initializes the CloudFront client
58
+ * @private
59
+ */
60
+ #initializeClient() {
61
+ if (!this.client) {
62
+ this.client = new CloudFrontClient({
63
+ region: this.cdnConfig.region,
64
+ });
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Invalidates CloudFront CDN cache for given paths
70
+ * @param {Array<string>} paths - Array of URL paths to invalidate
71
+ * @returns {Promise<Object>} Result of the invalidation request
72
+ */
73
+ async invalidateCache(paths) {
74
+ if (!this.validateConfig()) {
75
+ throw new Error('Invalid CloudFront CDN configuration');
76
+ }
77
+
78
+ if (!Array.isArray(paths) || paths.length === 0) {
79
+ this.log.warn('No paths provided for cache invalidation');
80
+ return { status: 'skipped', message: 'No paths to invalidate' };
81
+ }
82
+
83
+ this.#initializeClient();
84
+
85
+ // CloudFront requires paths to start with '/'
86
+ const formattedPaths = paths.map((path) => {
87
+ if (!path.startsWith('/')) {
88
+ return `/${path}`;
89
+ }
90
+ return path;
91
+ });
92
+
93
+ const callerReference = `tokowaka-${Date.now()}`;
94
+
95
+ const command = new CreateInvalidationCommand({
96
+ DistributionId: this.cdnConfig.distributionId,
97
+ InvalidationBatch: {
98
+ CallerReference: callerReference,
99
+ Paths: {
100
+ Quantity: formattedPaths.length,
101
+ Items: formattedPaths,
102
+ },
103
+ },
104
+ });
105
+
106
+ this.log.debug(`Initiating CloudFront cache invalidation for ${JSON.stringify(formattedPaths)} paths`);
107
+ const startTime = Date.now();
108
+
109
+ try {
110
+ const response = await this.client.send(command);
111
+ const invalidation = response.Invalidation;
112
+
113
+ this.log.info(`CloudFront cache invalidation initiated: ${invalidation.Id} (took ${Date.now() - startTime}ms)`);
114
+
115
+ return {
116
+ status: 'success',
117
+ provider: 'cloudfront',
118
+ invalidationId: invalidation.Id,
119
+ invalidationStatus: invalidation.Status,
120
+ createTime: invalidation.CreateTime,
121
+ paths: formattedPaths.length,
122
+ };
123
+ } catch (error) {
124
+ this.log.error(`Failed to invalidate CloudFront cache after ${Date.now() - startTime}ms: ${error.message}`, error);
125
+ throw error;
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,17 @@
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
+ export const TARGET_USER_AGENTS_CATEGORIES = {
14
+ AI_BOTS: 'ai-bots',
15
+ BOTS: 'bots',
16
+ ALL: 'all',
17
+ };
package/src/index.d.ts ADDED
@@ -0,0 +1,289 @@
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 { S3Client } from '@aws-sdk/client-s3';
14
+
15
+ export interface TokawakaPatch {
16
+ op: 'replace' | 'insertAfter' | 'insertBefore' | 'appendChild';
17
+ selector: string;
18
+ value: string | object;
19
+ valueFormat: 'text' | 'hast';
20
+ tag?: string;
21
+ currValue?: string;
22
+ target: 'ai-bots' | 'bots' | 'all';
23
+ opportunityId: string;
24
+ suggestionId: string;
25
+ prerenderRequired: boolean;
26
+ lastUpdated: number;
27
+ }
28
+
29
+ export const TARGET_USER_AGENTS_CATEGORIES: {
30
+ AI_BOTS: 'ai-bots';
31
+ BOTS: 'bots';
32
+ ALL: 'all';
33
+ };
34
+
35
+ export interface TokowakaUrlOptimization {
36
+ prerender: boolean;
37
+ patches: TokawakaPatch[];
38
+ }
39
+
40
+ export interface TokowakaConfig {
41
+ siteId: string;
42
+ baseURL: string;
43
+ version: string;
44
+ tokowakaForceFail: boolean;
45
+ tokowakaOptimizations: Record<string, TokowakaUrlOptimization>;
46
+ }
47
+
48
+ export interface CdnInvalidationResult {
49
+ status: string;
50
+ provider?: string;
51
+ purgeId?: string;
52
+ estimatedSeconds?: number;
53
+ paths?: number;
54
+ message?: string;
55
+ }
56
+
57
+ export interface DeploymentResult {
58
+ s3Path: string;
59
+ cdnInvalidation: CdnInvalidationResult | null;
60
+ succeededSuggestions: Array<any>;
61
+ failedSuggestions: Array<{ suggestion: any; reason: string }>;
62
+ }
63
+
64
+ export interface SiteConfig {
65
+ getTokowakaConfig(): {
66
+ apiKey: string;
67
+ cdnProvider?: string;
68
+ };
69
+ getFetchConfig?(): {
70
+ overrideBaseURL?: string;
71
+ };
72
+ }
73
+
74
+ export interface Site {
75
+ getId(): string;
76
+ getBaseURL(): string;
77
+ getConfig(): SiteConfig;
78
+ }
79
+
80
+ export interface Opportunity {
81
+ getId(): string;
82
+ getType(): string;
83
+ }
84
+
85
+ export interface Suggestion {
86
+ getId(): string;
87
+ getData(): Record<string, any>;
88
+ getUpdatedAt(): string;
89
+ }
90
+
91
+ /**
92
+ * Base class for opportunity mappers
93
+ * Extend this class to create custom mappers for new opportunity types
94
+ */
95
+ export abstract class BaseOpportunityMapper {
96
+ protected log: any;
97
+
98
+ constructor(log: any);
99
+
100
+ /**
101
+ * Returns the opportunity type this mapper handles
102
+ */
103
+ abstract getOpportunityType(): string;
104
+
105
+ /**
106
+ * Determines if prerendering is required for this opportunity type
107
+ */
108
+ abstract requiresPrerender(): boolean;
109
+
110
+ /**
111
+ * Converts a suggestion to a Tokowaka patch
112
+ */
113
+ abstract suggestionToPatch(
114
+ suggestion: Suggestion,
115
+ opportunityId: string
116
+ ): TokawakaPatch | null;
117
+
118
+ /**
119
+ * Checks if a suggestion can be deployed for this opportunity type
120
+ * This method should validate all eligibility and data requirements
121
+ */
122
+ abstract canDeploy(suggestion: Suggestion): {
123
+ eligible: boolean;
124
+ reason?: string;
125
+ };
126
+
127
+ /**
128
+ * Helper method to create base patch structure
129
+ */
130
+ protected createBasePatch(
131
+ suggestion: Suggestion,
132
+ opportunityId: string
133
+ ): Partial<TokawakaPatch>;
134
+ }
135
+
136
+ /**
137
+ * Headings opportunity mapper
138
+ * Handles conversion of heading suggestions (heading-empty, heading-missing-h1, heading-h1-length) to Tokowaka patches
139
+ */
140
+ export class HeadingsMapper extends BaseOpportunityMapper {
141
+ constructor(log: any);
142
+
143
+ getOpportunityType(): string;
144
+ requiresPrerender(): boolean;
145
+ suggestionToPatch(suggestion: Suggestion, opportunityId: string): TokawakaPatch | null;
146
+ canDeploy(suggestion: Suggestion): { eligible: boolean; reason?: string };
147
+ }
148
+
149
+ /**
150
+ * Content summarization opportunity mapper
151
+ * Handles conversion of content summarization suggestions to Tokowaka patches with HAST format
152
+ */
153
+ export class ContentSummarizationMapper extends BaseOpportunityMapper {
154
+ constructor(log: any);
155
+
156
+ getOpportunityType(): string;
157
+ requiresPrerender(): boolean;
158
+ suggestionToPatch(suggestion: Suggestion, opportunityId: string): TokawakaPatch | null;
159
+ canDeploy(suggestion: Suggestion): { eligible: boolean; reason?: string };
160
+
161
+ /**
162
+ * Converts markdown text to HAST (Hypertext Abstract Syntax Tree) format
163
+ */
164
+ markdownToHast(markdown: string): object;
165
+ }
166
+
167
+ /**
168
+ * Registry for opportunity mappers
169
+ */
170
+ export class MapperRegistry {
171
+ constructor(log: any);
172
+
173
+ registerMapper(MapperClass: typeof BaseOpportunityMapper): void;
174
+ getMapper(opportunityType: string): BaseOpportunityMapper | null;
175
+ getSupportedOpportunityTypes(): string[];
176
+ }
177
+
178
+ /**
179
+ * Base class for CDN clients
180
+ * Extend this class to create custom CDN clients for different providers
181
+ */
182
+ export abstract class BaseCdnClient {
183
+ protected env: any;
184
+ protected log: any;
185
+
186
+ constructor(env: any, log: any);
187
+
188
+ /**
189
+ * Returns the CDN provider name
190
+ */
191
+ abstract getProviderName(): string;
192
+
193
+ /**
194
+ * Validates the CDN configuration
195
+ */
196
+ abstract validateConfig(): boolean;
197
+
198
+ /**
199
+ * Invalidates the CDN cache for the given paths
200
+ */
201
+ abstract invalidateCache(paths: string[]): Promise<CdnInvalidationResult>;
202
+ }
203
+
204
+ /**
205
+ * CloudFront CDN client implementation
206
+ */
207
+ export class CloudFrontCdnClient extends BaseCdnClient {
208
+ constructor(env: {
209
+ TOKOWAKA_CDN_CONFIG: string; // JSON string with cloudfront config
210
+ }, log: any);
211
+
212
+ getProviderName(): string;
213
+ validateConfig(): boolean;
214
+ invalidateCache(paths: string[]): Promise<CdnInvalidationResult>;
215
+ }
216
+
217
+ /**
218
+ * Registry for CDN clients
219
+ */
220
+ export class CdnClientRegistry {
221
+ constructor(log: any);
222
+
223
+ registerClient(provider: string, ClientClass: typeof BaseCdnClient): void;
224
+ getClient(provider: string, config: Record<string, any>): BaseCdnClient | null;
225
+ getSupportedProviders(): string[];
226
+ isProviderSupported(provider: string): boolean;
227
+ }
228
+
229
+ /**
230
+ * Main Tokowaka Client for managing edge optimization configurations
231
+ */
232
+ export default class TokowakaClient {
233
+ constructor(config: {
234
+ bucketName: string;
235
+ s3Client: S3Client;
236
+ env?: Record<string, any>;
237
+ }, log: any);
238
+
239
+ static createFrom(context: {
240
+ env: {
241
+ TOKOWAKA_SITE_CONFIG_BUCKET: string;
242
+ TOKOWAKA_CDN_PROVIDER?: string;
243
+ TOKOWAKA_CDN_CONFIG?: string;
244
+ };
245
+ log?: any;
246
+ s3: { s3Client: S3Client };
247
+ tokowakaClient?: TokowakaClient;
248
+ }): TokowakaClient;
249
+
250
+ generateConfig(
251
+ site: Site,
252
+ opportunity: Opportunity,
253
+ suggestions: Suggestion[]
254
+ ): TokowakaConfig;
255
+
256
+ uploadConfig(apiKey: string, config: TokowakaConfig): Promise<string>;
257
+
258
+ /**
259
+ * Fetches existing Tokowaka configuration from S3
260
+ */
261
+ fetchConfig(apiKey: string): Promise<TokowakaConfig | null>;
262
+
263
+ /**
264
+ * Merges existing configuration with new configuration
265
+ */
266
+ mergeConfigs(existingConfig: TokowakaConfig, newConfig: TokowakaConfig): TokowakaConfig;
267
+
268
+ /**
269
+ * Invalidates CDN cache
270
+ */
271
+ invalidateCdnCache(apiKey: string, cdnProvider?: string): Promise<CdnInvalidationResult | null>;
272
+
273
+ deploySuggestions(
274
+ site: Site,
275
+ opportunity: Opportunity,
276
+ suggestions: Suggestion[]
277
+ ): Promise<DeploymentResult>;
278
+
279
+ /**
280
+ * Registers a custom mapper for an opportunity type
281
+ */
282
+ registerMapper(mapper: BaseOpportunityMapper): void;
283
+
284
+ /**
285
+ * Gets list of supported opportunity types
286
+ */
287
+ getSupportedOpportunityTypes(): string[];
288
+ }
289
+