@adobe/spacecat-shared-data-access 3.70.2 → 3.71.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-data-access-v3.71.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.70.2...@adobe/spacecat-shared-data-access-v3.71.0) (2026-05-25)
2
+
3
+ ### Features
4
+
5
+ * add scraperConfig to SiteConfig ([#1618](https://github.com/adobe/spacecat-shared/issues/1618)) ([8e3707b](https://github.com/adobe/spacecat-shared/commit/8e3707b505a2834c9351c50a965d838b3bad6123))
6
+
1
7
  ## [@adobe/spacecat-shared-data-access-v3.70.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.70.1...@adobe/spacecat-shared-data-access-v3.70.2) (2026-05-23)
2
8
 
3
9
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "3.70.2",
3
+ "version": "3.71.0",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -173,14 +173,28 @@ class Audit extends BaseModel {
173
173
  getQueueUrl: (context) => context.env?.CONTENT_SCRAPER_QUEUE_URL,
174
174
  /**
175
175
  * Formats the payload for the content scraper queue.
176
+ *
177
+ * Per-site scraper headers (configured via the site's `scraperConfig.headers`)
178
+ * are auto-injected into the SQS payload's `customHeaders`, reusing the
179
+ * `site` already loaded by the audit framework on the context — no extra
180
+ * Site.findById is performed. Steps that need a different value can set
181
+ * `stepResult.customHeaders` explicitly to forward it verbatim instead.
182
+ *
176
183
  * @param {object} stepResult - The result of the audit step.
177
184
  * @param {object[]} stepResult.urls - The list of URLs to scrape.
178
185
  * @param {string} stepResult.urls[].url - The URL to scrape.
179
186
  * @param {string} stepResult.siteId - The site ID. Will be used as the job ID.
180
187
  * @param {string} stepResult.options - The options for the scraper.
181
188
  * @param {string} stepResult.processingType - The scraping processing type to trigger.
189
+ * @param {Object<string,string>} [stepResult.customHeaders] - Explicit
190
+ * override for the HTTP headers forwarded to the scraper. When unset,
191
+ * the dispatcher auto-loads from `context.site.getConfig().getScraperConfig()?.headers`.
182
192
  * @param {object} auditContext - The audit context.
183
- * @param {object} context - The context object.
193
+ * @param {object} context - The context object. `context.site` is the
194
+ * loaded site model when available; the auto-load uses optional
195
+ * chaining so older code paths that have not yet attached a site to
196
+ * context (or sites whose Config predates the scraperConfig getter)
197
+ * degrade to "no headers" rather than throwing.
184
198
  * @param {object} auditContext.next - The next audit step to run.
185
199
  * @param {string} auditContext.auditId - The audit ID.
186
200
  * @param {string} auditContext.auditType - The audit type.
@@ -188,16 +202,29 @@ class Audit extends BaseModel {
188
202
  *
189
203
  * @returns {object} - The formatted payload.
190
204
  */
191
- formatPayload: (stepResult, auditContext, context) => ({
192
- urls: stepResult.urls,
193
- jobId: stepResult.siteId,
194
- processingType: stepResult.processingType || 'default',
195
- skipMessage: false,
196
- allowCache: isBoolean(stepResult.allowCache) ? stepResult.allowCache : true,
197
- options: stepResult.options || {},
198
- completionQueueUrl: stepResult.completionQueueUrl || context.env?.AUDIT_JOBS_QUEUE_URL,
199
- auditContext,
200
- }),
205
+ formatPayload: (stepResult, auditContext, context) => {
206
+ const payload = {
207
+ urls: stepResult.urls,
208
+ jobId: stepResult.siteId,
209
+ processingType: stepResult.processingType || 'default',
210
+ skipMessage: false,
211
+ allowCache: isBoolean(stepResult.allowCache) ? stepResult.allowCache : true,
212
+ options: stepResult.options || {},
213
+ completionQueueUrl: stepResult.completionQueueUrl || context.env?.AUDIT_JOBS_QUEUE_URL,
214
+ auditContext,
215
+ };
216
+
217
+ // Prefer step-supplied customHeaders; otherwise auto-load from site config.
218
+ const customHeaders = stepResult.customHeaders
219
+ ?? context?.site?.getConfig?.()?.getScraperConfig?.()?.headers;
220
+
221
+ // Reject empty object so the scraper does not receive `customHeaders: {}`.
222
+ if (customHeaders && Object.keys(customHeaders).length > 0) {
223
+ payload.customHeaders = customHeaders;
224
+ }
225
+
226
+ return payload;
227
+ },
201
228
  },
202
229
  [Audit.AUDIT_STEP_DESTINATIONS.SCRAPE_CLIENT]: {
203
230
  /**
@@ -46,6 +46,28 @@ export const IMPORT_SOURCES = {
46
46
  RUM: 'rum',
47
47
  };
48
48
 
49
+ // HTTP header names that must not be set via `scraperConfig.headers`
50
+ // (case-insensitive). These are owned by the scraper, its HTTP client, or its
51
+ // auth flow; persisting them as per-site overrides would either be stripped at
52
+ // scrape time or open a security surface outside the per-site config contract.
53
+ const RESERVED_SCRAPER_HEADER_NAMES = new Set([
54
+ // Credential / authentication headers.
55
+ 'authorization',
56
+ 'cookie',
57
+ 'proxy-authorization',
58
+ // Routing / fingerprint headers the scraper or its environment owns.
59
+ 'host',
60
+ 'user-agent',
61
+ // Hop-by-hop / connection-management headers (RFC 7230 section 6.1).
62
+ 'content-length',
63
+ 'transfer-encoding',
64
+ 'connection',
65
+ 'keep-alive',
66
+ 'upgrade',
67
+ 'te',
68
+ 'trailer',
69
+ ]);
70
+
49
71
  const LLMO_TAG_PATTERN = /^(market|product|topic):\s?.+/;
50
72
  const AWS_REGION_PATTERN = /^[a-z]{2}(?:-[a-z]+)+-\d+$/i;
51
73
  const LLMO_TAG = Joi.alternatives()
@@ -379,6 +401,42 @@ export const configSchema = Joi.object({
379
401
  ).optional(),
380
402
  outputLocation: Joi.string().required(),
381
403
  }).optional(),
404
+ /**
405
+ * Per-site configuration intended to be forwarded by the content scraper as
406
+ * HTTP headers on outbound requests. Values are bounded and restricted to
407
+ * printable ASCII to avoid header-injection (CR/LF) and oversized-payload
408
+ * hazards. Header names are restricted to RFC 7230 token characters.
409
+ * Reserved names (see `RESERVED_SCRAPER_HEADER_NAMES`) are rejected.
410
+ *
411
+ * Enforcement lives at the schema layer (rather than at each consumer or API
412
+ * boundary) so any writer using `updateScraperConfig` -- API endpoint,
413
+ * internal tool, Slack command -- inherits the same checks.
414
+ */
415
+ scraperConfig: Joi.object({
416
+ headers: Joi.object()
417
+ .pattern(
418
+ // RFC 7230 token characters for header names, 64 chars max.
419
+ Joi.string().pattern(/^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$/).max(64),
420
+ // Visible ASCII + tab, no CR/LF/NUL, 1 to 1024 chars (empty rejected).
421
+ Joi.string().pattern(/^[\t\x20-\x7E]+$/).max(1024),
422
+ )
423
+ .max(32)
424
+ // Case-insensitive denylist for credential / routing / hop-by-hop names.
425
+ // Done as an object-level custom validator (rather than on the key
426
+ // pattern) so the error message names the offending header instead of
427
+ // surfacing as Joi's generic "is not allowed" pattern-object error.
428
+ .custom((value, helpers) => {
429
+ for (const key of Object.keys(value)) {
430
+ if (RESERVED_SCRAPER_HEADER_NAMES.has(key.toLowerCase())) {
431
+ return helpers.message(
432
+ `"${key}" is a reserved scraper header name and cannot be set via scraperConfig.headers`,
433
+ );
434
+ }
435
+ }
436
+ return value;
437
+ })
438
+ .optional(),
439
+ }).optional(),
382
440
  tokowakaConfig: Joi.object({
383
441
  apiKey: Joi.string().optional(),
384
442
  forwardedHost: Joi.string().optional(),
@@ -529,6 +587,7 @@ export const Config = (data = {}) => {
529
587
  self.getBrandConfig = () => state?.brandConfig;
530
588
  self.getBrandProfile = () => state?.brandProfile;
531
589
  self.getCdnLogsConfig = () => state?.cdnLogsConfig;
590
+ self.getScraperConfig = () => state?.scraperConfig;
532
591
  self.getLlmoConfig = () => state?.llmo;
533
592
  self.getLlmoDataFolder = () => state?.llmo?.dataFolder;
534
593
  self.getLlmoBrand = () => state?.llmo?.brand;
@@ -949,6 +1008,21 @@ export const Config = (data = {}) => {
949
1008
  state.cdnLogsConfig = cdnLogsConfig;
950
1009
  };
951
1010
 
1011
+ self.updateScraperConfig = (scraperConfig) => {
1012
+ // Validate eagerly (unlike neighboring setters) so bad input is rejected
1013
+ // at the setter rather than silently round-tripping through Dynamo. This
1014
+ // eagerness is the contract every writer relies on for scraperConfig;
1015
+ // do not "harmonize" with cdnLogs/tokowaka setters.
1016
+ //
1017
+ // Use the sanitized value Joi returns so any future `default()` or
1018
+ // `lowercase()` on the schema applies here too — assigning raw input
1019
+ // would silently drift from what validation guarantees.
1020
+ const { scraperConfig: validated } = validateConfiguration(
1021
+ { ...state, scraperConfig },
1022
+ );
1023
+ state.scraperConfig = validated;
1024
+ };
1025
+
952
1026
  self.updateTokowakaConfig = (tokowakaConfig) => {
953
1027
  state.tokowakaConfig = tokowakaConfig;
954
1028
  };
@@ -1003,6 +1077,7 @@ Config.toDynamoItem = (config) => ({
1003
1077
  brandConfig: config.getBrandConfig(),
1004
1078
  brandProfile: config.getBrandProfile(),
1005
1079
  cdnLogsConfig: config.getCdnLogsConfig(),
1080
+ scraperConfig: config.getScraperConfig(),
1006
1081
  llmo: config.getLlmoConfig(),
1007
1082
  tokowakaConfig: config.getTokowakaConfig(),
1008
1083
  edgeOptimizeConfig: config.getEdgeOptimizeConfig(),