@adobe/spacecat-shared-utils 1.112.5 → 1.114.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,15 @@
1
+ ## [@adobe/spacecat-shared-utils-v1.114.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.113.0...@adobe/spacecat-shared-utils-v1.114.0) (2026-05-04)
2
+
3
+ ### Features
4
+
5
+ * **llmo-config:** fail-closed writeConfig validation (SITES-43238) ([#1574](https://github.com/adobe/spacecat-shared/issues/1574)) ([a177743](https://github.com/adobe/spacecat-shared/commit/a177743a7def37dd3fabb026241d0e0ea9e9bac3))
6
+
7
+ ## [@adobe/spacecat-shared-utils-v1.113.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.112.5...@adobe/spacecat-shared-utils-v1.113.0) (2026-04-29)
8
+
9
+ ### Features
10
+
11
+ * **utils/sqs:** add MessageDeduplicationId support for FIFO sends ([#1566](https://github.com/adobe/spacecat-shared/issues/1566)) ([b3b9511](https://github.com/adobe/spacecat-shared/commit/b3b95112d14ac9f07dcfae8baa1599c1429ea1f7))
12
+
1
13
  ## [@adobe/spacecat-shared-utils-v1.112.5](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.112.4...@adobe/spacecat-shared-utils-v1.112.5) (2026-04-10)
2
14
 
3
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-utils",
3
- "version": "1.112.5",
3
+ "version": "1.114.0",
4
4
  "description": "Shared modules of the Spacecat Services - utils",
5
5
  "type": "module",
6
6
  "exports": {
package/src/index.js CHANGED
@@ -109,6 +109,7 @@ export { detectAEMVersion, DELIVERY_TYPES, AUTHORING_TYPES } from './aem.js';
109
109
  export { determineAEMCSPageId, getPageEditUrl } from './aem-content-api-utils.js';
110
110
 
111
111
  export * as llmoConfig from './llmo-config.js';
112
+ export { LlmoConfigValidationError } from './llmo-config.js';
112
113
  export * as llmoStrategy from './llmo-strategy.js';
113
114
  export * as schemas from './schemas.js';
114
115
 
@@ -18,6 +18,31 @@ import { llmoConfig } from './schemas.js';
18
18
  * @import { LLMOConfig } from "./schemas.js"
19
19
  */
20
20
 
21
+ /**
22
+ * Thrown by `writeConfig` when the supplied LLMO configuration does not match
23
+ * the published Zod schema. Exposes the offending site and the Zod issue list
24
+ * so callers and log readers can identify the failing fields without
25
+ * re-parsing the error.
26
+ */
27
+ export class LlmoConfigValidationError extends Error {
28
+ constructor(siteId, zodError) {
29
+ // Use issue `code` rather than `message` in the summary: Zod's default
30
+ // messages can echo received values, which may include user-supplied
31
+ // content (brand names, competitor URLs) on the api-service write path.
32
+ // The full message and value remain on `this.issues` for trusted callers.
33
+ const summary = zodError.issues
34
+ .map((i) => `${i.path.join('.')}: ${i.code}`)
35
+ .join('; ');
36
+ super(
37
+ `LLMO config for site ${siteId} failed schema validation: ${summary}`,
38
+ { cause: zodError },
39
+ );
40
+ this.name = 'LlmoConfigValidationError';
41
+ this.siteId = siteId;
42
+ this.issues = zodError.issues;
43
+ }
44
+ }
45
+
21
46
  /**
22
47
  * @param {string} siteId The ID of the site to get the config directory for.
23
48
  * @returns {string} The configuration directory path for the given site ID.
@@ -62,6 +87,10 @@ export function defaultConfig() {
62
87
  * Reads the LLMO configuration for a given site.
63
88
  * Returns an empty configuration if the configuration does not exist.
64
89
  *
90
+ * If the persisted config exists but fails schema validation, throws
91
+ * `LlmoConfigValidationError` so callers have a uniform error contract
92
+ * across read and write paths.
93
+ *
65
94
  * @param {string} sideId The ID of the site.
66
95
  * @param {S3Client} s3Client The S3 client to use for reading the configuration.
67
96
  * @param {object} [options]
@@ -70,6 +99,7 @@ export function defaultConfig() {
70
99
  * @param {string} [options.s3Bucket] Optional S3 bucket name.
71
100
  * @returns {Promise<{config: LLMOConfig, exists: boolean, version?: string}>} The configuration,
72
101
  * a flag indicating if it existed, and the version ID if it exists.
102
+ * @throws {LlmoConfigValidationError} If the persisted config fails schema validation.
73
103
  * @throws {Error} If reading the configuration fails for reasons other than it not existing.
74
104
  */
75
105
  export async function readConfig(sideId, s3Client, options) {
@@ -97,20 +127,35 @@ export async function readConfig(sideId, s3Client, options) {
97
127
  throw new Error('LLMO config body is empty');
98
128
  }
99
129
  const text = await body.transformToString();
100
- const config = llmoConfig.parse(JSON.parse(text));
101
- return { config, exists: true, version: res.VersionId || undefined };
130
+ const result = llmoConfig.safeParse(JSON.parse(text));
131
+ if (!result.success) {
132
+ throw new LlmoConfigValidationError(sideId, result.error);
133
+ }
134
+ return { config: result.data, exists: true, version: res.VersionId || undefined };
102
135
  }
103
136
 
104
137
  /**
105
138
  * Writes the LLMO configuration for a given site.
139
+ *
140
+ * Validates `config` against the published Zod schema before issuing the S3
141
+ * `PutObject`. If validation fails, throws `LlmoConfigValidationError` and
142
+ * does not call S3, so invalid configs cannot reach the bucket through this
143
+ * function.
144
+ *
106
145
  * @param {string} siteId The ID of the site.
107
146
  * @param {LLMOConfig} config The configuration object to write.
108
147
  * @param {S3Client} s3Client The S3 client to use for reading the configuration.
109
148
  * @param {object} [options]
110
149
  * @param {string} [options.s3Bucket] Optional S3 bucket name.
111
150
  * @returns {Promise<{ version: string }>} The version of the configuration written.
151
+ * @throws {LlmoConfigValidationError} If `config` does not match the LLMO schema.
112
152
  */
113
153
  export async function writeConfig(siteId, config, s3Client, options) {
154
+ const result = llmoConfig.safeParse(config);
155
+ if (!result.success) {
156
+ throw new LlmoConfigValidationError(siteId, result.error);
157
+ }
158
+
114
159
  const s3Bucket = options?.s3Bucket || process.env.S3_BUCKET_NAME;
115
160
 
116
161
  const putObjectCommand = new PutObjectCommand({
package/src/sqs.js CHANGED
@@ -57,9 +57,16 @@ class SQS {
57
57
  * @param {object} message - The message body to send.
58
58
  * Can include traceId for propagation or set to null to opt-out.
59
59
  * @param {string} messageGroupId - (Optional) The message group ID for FIFO queues.
60
+ * @param {string} messageDeduplicationId - (Optional) The deduplication ID for FIFO
61
+ * queues that have content-based deduplication disabled. Ignored on standard queues.
60
62
  * @return {Promise<void>}
61
63
  */
62
- async sendMessage(queueUrl, message, messageGroupId = undefined) {
64
+ async sendMessage(
65
+ queueUrl,
66
+ message,
67
+ messageGroupId = undefined,
68
+ messageDeduplicationId = undefined,
69
+ ) {
63
70
  const body = {
64
71
  ...message,
65
72
  timestamp: new Date().toISOString(),
@@ -87,9 +94,14 @@ class SQS {
87
94
  QueueUrl: queueUrl,
88
95
  };
89
96
 
90
- // Only include MessageGroupId if the queue is a FIFO queue
91
- if (SQS.#isFifoQueue(queueUrl) && hasText(messageGroupId)) {
92
- params.MessageGroupId = messageGroupId;
97
+ // Only include FIFO-specific attributes if the queue is a FIFO queue
98
+ if (SQS.#isFifoQueue(queueUrl)) {
99
+ if (hasText(messageGroupId)) {
100
+ params.MessageGroupId = messageGroupId;
101
+ }
102
+ if (hasText(messageDeduplicationId)) {
103
+ params.MessageDeduplicationId = messageDeduplicationId;
104
+ }
93
105
  }
94
106
 
95
107
  const msgCommand = new SendMessageCommand(params);