From 996b74f577bc060e19a0d6b7351fa4541cf8c76f Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Thu, 29 Jan 2026 13:45:30 +0530 Subject: [PATCH 01/16] fix: add checks for dangling enrollments --- package-lock.json | 6 +++--- package.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16bd1ebf3..98023be76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@adobe/spacecat-shared-rum-api-client": "2.40.3", "@adobe/spacecat-shared-scrape-client": "2.3.6", "@adobe/spacecat-shared-slack-client": "1.5.32", - "@adobe/spacecat-shared-tier-client": "1.3.10", + "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", "@adobe/spacecat-shared-tokowaka-client": "1.5.7", "@adobe/spacecat-shared-utils": "1.88.0", "@aws-sdk/client-s3": "3.940.0", @@ -2499,8 +2499,8 @@ }, "node_modules/@adobe/spacecat-shared-tier-client": { "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-tier-client/-/spacecat-shared-tier-client-1.3.10.tgz", - "integrity": "sha512-eqBEuboVczrzmvMj/dFTi9p8jBWIOlVe1u0x9qaUKtJJwTrAutFtj5QR7JfxhWcSQdKnOPg1B0rytt4gea+T1Q==", + "resolved": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", + "integrity": "sha512-JHAL6cJCLPWZgZb8uql/henfsJO+YWz5+sG0Sgy85S+fszdpK6/G6aZDP+aBalXCPPD5drDsjLQnpMrSTm06VQ==", "license": "Apache-2.0", "dependencies": { "@adobe/spacecat-shared-data-access": "2.88.7", diff --git a/package.json b/package.json index 15a34d0e0..c16ce5681 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build": "hedy -v --test-bundle", "deploy": "hedy -v --deploy --aws-deploy-bucket=spacecat-prod-deploy --pkgVersion=latest", "deploy-stage": "hedy -v --deploy --aws-deploy-bucket=spacecat-stage-deploy --pkgVersion=latest", - "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l latest --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h", + "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l sandsinh --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h", "deploy-secrets": "hedy --aws-update-secrets --params-file=secrets/secrets.env", "docs": "npm run docs:lint && npm run docs:build", "docs:build": "npx @redocly/cli build-docs -o ./docs/index.html --config docs/openapi/redocly-config.yaml", @@ -84,7 +84,7 @@ "@adobe/spacecat-shared-rum-api-client": "2.40.3", "@adobe/spacecat-shared-scrape-client": "2.3.6", "@adobe/spacecat-shared-slack-client": "1.5.32", - "@adobe/spacecat-shared-tier-client": "1.3.10", + "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", "@adobe/spacecat-shared-tokowaka-client": "1.5.7", "@adobe/spacecat-shared-utils": "1.88.0", "@aws-sdk/client-s3": "3.940.0", From d6f08e1533f3f3e187e1e5e03b0f579cb4e8cee2 Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Thu, 29 Jan 2026 14:19:22 +0530 Subject: [PATCH 02/16] fix: testing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c16ce5681..894b8418b 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@adobe/spacecat-shared-rum-api-client": "2.40.3", "@adobe/spacecat-shared-scrape-client": "2.3.6", "@adobe/spacecat-shared-slack-client": "1.5.32", - "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", + "@adobe/spacecat-shared-tier-client": "1.3.10", "@adobe/spacecat-shared-tokowaka-client": "1.5.7", "@adobe/spacecat-shared-utils": "1.88.0", "@aws-sdk/client-s3": "3.940.0", From 85eedb8959a1213faa95b074f3bcc999a354a16a Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Thu, 29 Jan 2026 14:36:06 +0530 Subject: [PATCH 03/16] fix: testing --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 98023be76..9b50b1c01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@adobe/spacecat-shared-rum-api-client": "2.40.3", "@adobe/spacecat-shared-scrape-client": "2.3.6", "@adobe/spacecat-shared-slack-client": "1.5.32", - "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", + "@adobe/spacecat-shared-tier-client": "1.3.10", "@adobe/spacecat-shared-tokowaka-client": "1.5.7", "@adobe/spacecat-shared-utils": "1.88.0", "@aws-sdk/client-s3": "3.940.0", From 80d3a1d09fc2688a1835614419d326f7e8109293 Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Thu, 29 Jan 2026 14:39:04 +0530 Subject: [PATCH 04/16] fix: revert --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b50b1c01..16bd1ebf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2499,8 +2499,8 @@ }, "node_modules/@adobe/spacecat-shared-tier-client": { "version": "1.3.10", - "resolved": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", - "integrity": "sha512-JHAL6cJCLPWZgZb8uql/henfsJO+YWz5+sG0Sgy85S+fszdpK6/G6aZDP+aBalXCPPD5drDsjLQnpMrSTm06VQ==", + "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-tier-client/-/spacecat-shared-tier-client-1.3.10.tgz", + "integrity": "sha512-eqBEuboVczrzmvMj/dFTi9p8jBWIOlVe1u0x9qaUKtJJwTrAutFtj5QR7JfxhWcSQdKnOPg1B0rytt4gea+T1Q==", "license": "Apache-2.0", "dependencies": { "@adobe/spacecat-shared-data-access": "2.88.7", From c3b7ed9ba7ab5c9baaaa0877d2e017dfc76a4ed4 Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Fri, 6 Feb 2026 14:36:16 +0530 Subject: [PATCH 05/16] Revert "fix: revert" This reverts commit 80d3a1d09fc2688a1835614419d326f7e8109293. --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16bd1ebf3..9b50b1c01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2499,8 +2499,8 @@ }, "node_modules/@adobe/spacecat-shared-tier-client": { "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-tier-client/-/spacecat-shared-tier-client-1.3.10.tgz", - "integrity": "sha512-eqBEuboVczrzmvMj/dFTi9p8jBWIOlVe1u0x9qaUKtJJwTrAutFtj5QR7JfxhWcSQdKnOPg1B0rytt4gea+T1Q==", + "resolved": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", + "integrity": "sha512-JHAL6cJCLPWZgZb8uql/henfsJO+YWz5+sG0Sgy85S+fszdpK6/G6aZDP+aBalXCPPD5drDsjLQnpMrSTm06VQ==", "license": "Apache-2.0", "dependencies": { "@adobe/spacecat-shared-data-access": "2.88.7", From 83f5804da5bf54518457b306127a25183931ba2b Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Fri, 6 Feb 2026 14:38:42 +0530 Subject: [PATCH 06/16] fix: test --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b50b1c01..98023be76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@adobe/spacecat-shared-rum-api-client": "2.40.3", "@adobe/spacecat-shared-scrape-client": "2.3.6", "@adobe/spacecat-shared-slack-client": "1.5.32", - "@adobe/spacecat-shared-tier-client": "1.3.10", + "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", "@adobe/spacecat-shared-tokowaka-client": "1.5.7", "@adobe/spacecat-shared-utils": "1.88.0", "@aws-sdk/client-s3": "3.940.0", diff --git a/package.json b/package.json index 894b8418b..c16ce5681 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@adobe/spacecat-shared-rum-api-client": "2.40.3", "@adobe/spacecat-shared-scrape-client": "2.3.6", "@adobe/spacecat-shared-slack-client": "1.5.32", - "@adobe/spacecat-shared-tier-client": "1.3.10", + "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", "@adobe/spacecat-shared-tokowaka-client": "1.5.7", "@adobe/spacecat-shared-utils": "1.88.0", "@aws-sdk/client-s3": "3.940.0", From fee6874ec838c21f2383abee4b86cb78c0e85a16 Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Fri, 6 Feb 2026 14:51:41 +0530 Subject: [PATCH 07/16] fix: test --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98023be76..8e2017ef3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@adobe/spacecat-shared-rum-api-client": "2.40.3", "@adobe/spacecat-shared-scrape-client": "2.3.6", "@adobe/spacecat-shared-slack-client": "1.5.32", - "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", + "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/2d4e5215b9a5639277fd03acaad254af9ca463f7/adobe-spacecat-shared-tier-client-1.3.11.tgz", "@adobe/spacecat-shared-tokowaka-client": "1.5.7", "@adobe/spacecat-shared-utils": "1.88.0", "@aws-sdk/client-s3": "3.940.0", @@ -2498,9 +2498,9 @@ } }, "node_modules/@adobe/spacecat-shared-tier-client": { - "version": "1.3.10", - "resolved": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", - "integrity": "sha512-JHAL6cJCLPWZgZb8uql/henfsJO+YWz5+sG0Sgy85S+fszdpK6/G6aZDP+aBalXCPPD5drDsjLQnpMrSTm06VQ==", + "version": "1.3.11", + "resolved": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/2d4e5215b9a5639277fd03acaad254af9ca463f7/adobe-spacecat-shared-tier-client-1.3.11.tgz", + "integrity": "sha512-Qs2C2RIsEix9JURzqOMQHmE8Sy66MF4F6nsSIb5Fl437/dC6b4Gy+RSDW4wO8x8jhJNk59fkkrdJj3h31fr5yA==", "license": "Apache-2.0", "dependencies": { "@adobe/spacecat-shared-data-access": "2.88.7", diff --git a/package.json b/package.json index c16ce5681..5dfd0aa8b 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@adobe/spacecat-shared-rum-api-client": "2.40.3", "@adobe/spacecat-shared-scrape-client": "2.3.6", "@adobe/spacecat-shared-slack-client": "1.5.32", - "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/9ec8eb0046ff3f1e7a95ce15debc221b308b7f28/adobe-spacecat-shared-tier-client-1.3.10.tgz", + "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/2d4e5215b9a5639277fd03acaad254af9ca463f7/adobe-spacecat-shared-tier-client-1.3.11.tgz", "@adobe/spacecat-shared-tokowaka-client": "1.5.7", "@adobe/spacecat-shared-utils": "1.88.0", "@aws-sdk/client-s3": "3.940.0", From b43c0dd2a9aff3daf61ef8581c0ec3c36900f053 Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Tue, 17 Feb 2026 14:50:37 +0530 Subject: [PATCH 08/16] feat: SITES-40623 - token architecture in Spacecat --- package-lock.json | 18 ++--- package.json | 6 +- src/controllers/opportunities.js | 2 + src/controllers/suggestions.js | 39 ++++++++--- src/support/grant-complete-suggestions.js | 82 +++++++++++++++++++++++ 5 files changed, 125 insertions(+), 22 deletions(-) create mode 100644 src/support/grant-complete-suggestions.js diff --git a/package-lock.json b/package-lock.json index 8e2017ef3..a670f5a96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,16 +21,16 @@ "@adobe/spacecat-shared-ahrefs-client": "1.10.3", "@adobe/spacecat-shared-athena-client": "1.9.2", "@adobe/spacecat-shared-brand-client": "1.1.34", - "@adobe/spacecat-shared-data-access": "2.97.1", + "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/cbe43dc5a7bf4efef3ba543bb43f5e87183dd207/adobe-spacecat-shared-data-access-2.97.2.tgz", "@adobe/spacecat-shared-gpt-client": "1.6.15", "@adobe/spacecat-shared-http-utils": "1.19.4", "@adobe/spacecat-shared-ims-client": "1.11.7", "@adobe/spacecat-shared-rum-api-client": "2.40.3", "@adobe/spacecat-shared-scrape-client": "2.3.6", "@adobe/spacecat-shared-slack-client": "1.5.32", - "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/2d4e5215b9a5639277fd03acaad254af9ca463f7/adobe-spacecat-shared-tier-client-1.3.11.tgz", + "@adobe/spacecat-shared-tier-client": "1.3.11", "@adobe/spacecat-shared-tokowaka-client": "1.5.7", - "@adobe/spacecat-shared-utils": "1.88.0", + "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/cbe43dc5a7bf4efef3ba543bb43f5e87183dd207/adobe-spacecat-shared-utils-1.89.1.tgz", "@aws-sdk/client-s3": "3.940.0", "@aws-sdk/client-sfn": "3.940.0", "@aws-sdk/client-sqs": "3.940.0", @@ -1307,9 +1307,9 @@ } }, "node_modules/@adobe/spacecat-shared-data-access": { - "version": "2.97.1", - "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-data-access/-/spacecat-shared-data-access-2.97.1.tgz", - "integrity": "sha512-M5xsWnnGNYwzSe0D0fQqX+UP3hVIzWaoH9G67C2csyHOMlvrCTVBa0e202e/OGDNwGN5/BuPrfhXq9prS6uHgw==", + "version": "2.97.2", + "resolved": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/cbe43dc5a7bf4efef3ba543bb43f5e87183dd207/adobe-spacecat-shared-data-access-2.97.2.tgz", + "integrity": "sha512-wcMdI6RqAoI0SMgqdOZhmwq7nw30oU5iGsO9tWc4eVKAvLdVj1gDFyxBydMr1pvX/+vzyv52fyzhkYPmDxZhKA==", "license": "Apache-2.0", "dependencies": { "@adobe/spacecat-shared-utils": "1.81.1", @@ -2817,9 +2817,9 @@ } }, "node_modules/@adobe/spacecat-shared-utils": { - "version": "1.88.0", - "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.88.0.tgz", - "integrity": "sha512-KGvu8VEiF9TiuBXMkQzB/jqaBgSc4IRL84t9tGm7BgtetqESxxjiNfuQ4BIfsYGklB9jxfP/Suz+QQsR1BeG7g==", + "version": "1.89.1", + "resolved": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/cbe43dc5a7bf4efef3ba543bb43f5e87183dd207/adobe-spacecat-shared-utils-1.89.1.tgz", + "integrity": "sha512-fJCh4+hRzg62hvuT4FeAWkhDHGPrhgnaHtA+iBog/awvIdrSYUHXi/J/d26DY5s4XhkU9TfNink221Nxmw8Y/A==", "license": "Apache-2.0", "dependencies": { "@adobe/fetch": "4.2.3", diff --git a/package.json b/package.json index 5dfd0aa8b..2348217d1 100644 --- a/package.json +++ b/package.json @@ -77,16 +77,16 @@ "@adobe/spacecat-shared-ahrefs-client": "1.10.3", "@adobe/spacecat-shared-athena-client": "1.9.2", "@adobe/spacecat-shared-brand-client": "1.1.34", - "@adobe/spacecat-shared-data-access": "2.97.1", + "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/cbe43dc5a7bf4efef3ba543bb43f5e87183dd207/adobe-spacecat-shared-data-access-2.97.2.tgz", "@adobe/spacecat-shared-gpt-client": "1.6.15", "@adobe/spacecat-shared-http-utils": "1.19.4", "@adobe/spacecat-shared-ims-client": "1.11.7", "@adobe/spacecat-shared-rum-api-client": "2.40.3", "@adobe/spacecat-shared-scrape-client": "2.3.6", "@adobe/spacecat-shared-slack-client": "1.5.32", - "@adobe/spacecat-shared-tier-client": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/2d4e5215b9a5639277fd03acaad254af9ca463f7/adobe-spacecat-shared-tier-client-1.3.11.tgz", + "@adobe/spacecat-shared-tier-client": "1.3.11", "@adobe/spacecat-shared-tokowaka-client": "1.5.7", - "@adobe/spacecat-shared-utils": "1.88.0", + "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/cbe43dc5a7bf4efef3ba543bb43f5e87183dd207/adobe-spacecat-shared-utils-1.89.1.tgz", "@aws-sdk/client-s3": "3.940.0", "@aws-sdk/client-sfn": "3.940.0", "@aws-sdk/client-sqs": "3.940.0", diff --git a/src/controllers/opportunities.js b/src/controllers/opportunities.js index 928664870..a453db763 100644 --- a/src/controllers/opportunities.js +++ b/src/controllers/opportunities.js @@ -28,6 +28,7 @@ import { import { ValidationError } from '@adobe/spacecat-shared-data-access'; import { OpportunityDto } from '../dto/opportunity.js'; import AccessControlUtil from '../support/access-control-util.js'; +import { grantCompleteSuggestionsForOpportunity } from '../support/grant-complete-suggestions.js'; /** * Opportunities controller. @@ -155,6 +156,7 @@ function OpportunitiesController(ctx) { if (!oppty || oppty.getSiteId() !== siteId) { return notFound('Opportunity not found'); } + await grantCompleteSuggestionsForOpportunity(dataAccess, site, oppty); return ok(OpportunityDto.toJSON(oppty)); }; diff --git a/src/controllers/suggestions.js b/src/controllers/suggestions.js index add9291ec..6e107a823 100644 --- a/src/controllers/suggestions.js +++ b/src/controllers/suggestions.js @@ -20,6 +20,7 @@ import { import { hasText, isArray, isNonEmptyArray, + isGranted, isNonEmptyObject, isObject, isInteger, @@ -77,7 +78,7 @@ function SuggestionsController(ctx, sqs, env) { }; const { - Opportunity, Suggestion, Site, Configuration, + Opportunity, Suggestion, Site, Configuration, Entitlement, } = dataAccess; if (!isObject(Opportunity)) { @@ -90,6 +91,20 @@ function SuggestionsController(ctx, sqs, env) { const accessControlUtil = AccessControlUtil.fromContext(ctx); + /** + * When the organization is freemium, returns only suggestions that have been granted (unlocked). + * Otherwise returns the same list unchanged. + * @param {Object} site - Site model (must have getOrganizationId()). + * @param {Array} suggestionEntities - List of suggestion entities. + * @returns {Array} Filtered list when freemium, otherwise unchanged. + */ + const filterGrantedIfFreemium = (site, suggestionEntities) => { + if (!suggestionEntities || suggestionEntities.length === 0) return suggestionEntities; + const organizationId = typeof site?.getOrganizationId === 'function' ? site.getOrganizationId() : undefined; + if (!organizationId || !Entitlement?.isFreemium(organizationId)) return suggestionEntities; + return suggestionEntities.filter((s) => isGranted(s)); + }; + /** * Gets all suggestions for a given site and opportunity * @param {Object} context of the request @@ -117,14 +132,14 @@ function SuggestionsController(ctx, sqs, env) { } const suggestionEntities = await Suggestion.allByOpportunityId(opptyId); - // Check if the opportunity belongs to the site if (suggestionEntities.length > 0) { const oppty = await suggestionEntities[0].getOpportunity(); if (!oppty || oppty.getSiteId() !== siteId) { return notFound('Opportunity not found'); } } - const suggestions = suggestionEntities.map((sugg) => SuggestionDto.toJSON(sugg)); + const filtered = filterGrantedIfFreemium(site, suggestionEntities); + const suggestions = filtered.map((sugg) => SuggestionDto.toJSON(sugg)); return ok(suggestions); }; @@ -170,15 +185,14 @@ function SuggestionsController(ctx, sqs, env) { }); const { data: suggestionEntities = [], cursor: newCursor = null } = results; - // Check if the opportunity belongs to the site if (suggestionEntities.length > 0) { const oppty = await suggestionEntities[0].getOpportunity(); if (!oppty || oppty.getSiteId() !== siteId) { return notFound('Opportunity not found'); } } - - const suggestions = suggestionEntities.map((sugg) => SuggestionDto.toJSON(sugg)); + const filtered = filterGrantedIfFreemium(site, suggestionEntities); + const suggestions = filtered.map((sugg) => SuggestionDto.toJSON(sugg)); return ok({ suggestions, @@ -219,14 +233,14 @@ function SuggestionsController(ctx, sqs, env) { } const suggestionEntities = await Suggestion.allByOpportunityIdAndStatus(opptyId, status); - // Check if the opportunity belongs to the site if (suggestionEntities.length > 0) { const oppty = await suggestionEntities[0].getOpportunity(); if (!oppty || oppty.getSiteId() !== siteId) { return notFound('Opportunity not found'); } } - const suggestions = suggestionEntities.map((sugg) => SuggestionDto.toJSON(sugg)); + const filtered = filterGrantedIfFreemium(site, suggestionEntities); + const suggestions = filtered.map((sugg) => SuggestionDto.toJSON(sugg)); return ok(suggestions); }; @@ -271,14 +285,14 @@ function SuggestionsController(ctx, sqs, env) { returnCursor: true, }); const { data: suggestionEntities = [], cursor: newCursor = null } = results; - // Check if the opportunity belongs to the site if (suggestionEntities.length > 0) { const oppty = await suggestionEntities[0].getOpportunity(); if (!oppty || oppty.getSiteId() !== siteId) { return notFound('Opportunity not found'); } } - const suggestions = suggestionEntities.map((sugg) => SuggestionDto.toJSON(sugg)); + const filtered = filterGrantedIfFreemium(site, suggestionEntities); + const suggestions = filtered.map((sugg) => SuggestionDto.toJSON(sugg)); return ok({ suggestions, pagination: { @@ -328,6 +342,11 @@ function SuggestionsController(ctx, sqs, env) { if (!opportunity || opportunity.getSiteId() !== siteId) { return notFound(); } + // Freemium: only allow access to granted suggestions + const organizationId = typeof site?.getOrganizationId === 'function' ? site.getOrganizationId() : undefined; + if (organizationId && Entitlement?.isFreemium(organizationId) && !isGranted(suggestion)) { + return notFound('Suggestion not found'); + } return ok(SuggestionDto.toJSON(suggestion)); }; diff --git a/src/support/grant-complete-suggestions.js b/src/support/grant-complete-suggestions.js new file mode 100644 index 000000000..20ac1e5c4 --- /dev/null +++ b/src/support/grant-complete-suggestions.js @@ -0,0 +1,82 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + isGranted, + isSuggestionComplete, + OPPORTUNITY_TYPES, +} from '@adobe/spacecat-shared-utils'; + +/** Maps opportunity type (e.g. broken-backlinks) to token type for freemium grant. */ +const OPPORTUNITY_TYPE_TO_TOKEN_TYPE = { + [OPPORTUNITY_TYPES.BROKEN_BACKLINKS]: 'BROKEN_BACKLINK', + [OPPORTUNITY_TYPES.CWV]: 'CWV', + [OPPORTUNITY_TYPES.ALT_TEXT]: 'ALT_TEXT', +}; + +/** + * When the organization is freemium, grants up to remaining token count for suggestions that are + * complete (per suggestion-complete.js) and not yet granted. Loads suggestions for the opportunity, + * then runs grant logic. Only runs for opportunity types that have token types and complete + * handlers (broken-backlinks, cwv, alt-text). Mutates and persists grants on suggestion entities. + * + * @param {Object} dataAccess - Data access (Suggestion, Token, Entitlement). + * @param {Object} site - Site model (getOrganizationId, getId). + * @param {Object} opportunity - Opportunity model (getType, getId). + * @returns {Promise} + */ +export async function grantCompleteSuggestionsForOpportunity(dataAccess, site, opportunity) { + if (!dataAccess || !site || !opportunity) return; + + const { Suggestion, Token, Entitlement } = dataAccess; + if (!Token || !Suggestion || !Entitlement) return; + + const organizationId = typeof site?.getOrganizationId === 'function' ? site.getOrganizationId() : undefined; + if (!organizationId || !Entitlement.isFreemium(organizationId)) return; + + const opportunityType = typeof opportunity.getType === 'function' ? opportunity.getType() : opportunity.type; + const tokenType = OPPORTUNITY_TYPE_TO_TOKEN_TYPE[opportunityType]; + if (!tokenType) return; + + const siteId = typeof site?.getId === 'function' ? site.getId() : undefined; + if (!siteId) return; + + const opptyId = typeof opportunity?.getId === 'function' ? opportunity.getId() : opportunity.id; + if (!opptyId) return; + + const suggestionEntities = await Suggestion.allByOpportunityId(opptyId); + if (!suggestionEntities?.length) return; + + const cycle = new Date().toISOString().slice(0, 7); // YYYY-MM + const remaining = await Token.getRemainingToken(siteId, tokenType, cycle); + if (remaining < 1) return; + + const completeUngranted = suggestionEntities + .filter((s) => !isGranted(s) && isSuggestionComplete(s, opportunityType)) + .sort((a, b) => (a.getRank?.() ?? a.rank ?? 0) - (b.getRank?.() ?? b.rank ?? 0)); + + const toGrant = completeUngranted.slice(0, remaining); + const grantedAt = new Date().toISOString(); + + for (const suggestion of toGrant) { + // eslint-disable-next-line no-await-in-loop -- token use must be sequential per suggestion + const grant = await Token.useToken(siteId, tokenType, cycle); + if (!grant) break; + suggestion.setGrants({ + cycle: grant.cycle, + tokenId: grant.tokenId, + grantedAt, + }); + // eslint-disable-next-line no-await-in-loop -- save must complete before next iteration + await suggestion.save(); + } +} From d5a0fd99655386e12dc5db5c1397e98928d314aa Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Sun, 15 Mar 2026 12:06:53 +0530 Subject: [PATCH 09/16] fix: update handler --- package-lock.json | 24 +- package.json | 4 +- src/controllers/opportunities.js | 8 +- src/controllers/suggestions.js | 52 ++-- src/support/grant-complete-suggestions.js | 82 ----- src/support/grant-suggestions-handler.js | 126 ++++++++ test/controllers/opportunities.test.js | 101 +++++++ test/controllers/suggestions.test.js | 56 ++++ .../support/grant-suggestions-handler.test.js | 280 ++++++++++++++++++ 9 files changed, 610 insertions(+), 123 deletions(-) delete mode 100644 src/support/grant-complete-suggestions.js create mode 100644 src/support/grant-suggestions-handler.js create mode 100644 test/support/grant-suggestions-handler.test.js diff --git a/package-lock.json b/package-lock.json index 16b2b6ef1..751e1cfbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@adobe/spacecat-shared-ahrefs-client": "1.10.7", "@adobe/spacecat-shared-athena-client": "1.9.6", "@adobe/spacecat-shared-brand-client": "1.1.38", - "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/455812ffb2f22bebc900a31a5bf31edc281a7e69/adobe-spacecat-shared-data-access-3.19.0.tgz", + "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/669c7f824b915dc67dfb9fe2fc7e29917e38ace7/adobe-spacecat-shared-data-access-3.19.0.tgz", "@adobe/spacecat-shared-data-access-v2": "npm:@adobe/spacecat-shared-data-access@2.109.0", "@adobe/spacecat-shared-drs-client": "1.3.0", "@adobe/spacecat-shared-gpt-client": "1.6.19", @@ -31,7 +31,7 @@ "@adobe/spacecat-shared-slack-client": "1.6.2", "@adobe/spacecat-shared-tier-client": "1.3.15", "@adobe/spacecat-shared-tokowaka-client": "1.11.1", - "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/dd75eb8b13ba2555d7cde87df25c0ccfbd8085bc/adobe-spacecat-shared-utils-1.102.0.tgz", + "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/4dc817e97910e998b17ad1f535a7e289dca89d95/adobe-spacecat-shared-utils-1.102.0.tgz", "@adobe/spacecat-shared-vault-secrets": "1.2.0", "@aws-sdk/client-s3": "3.1004.0", "@aws-sdk/client-secrets-manager": "3.1004.0", @@ -2351,12 +2351,12 @@ }, "node_modules/@adobe/spacecat-shared-data-access": { "version": "3.19.0", - "resolved": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/455812ffb2f22bebc900a31a5bf31edc281a7e69/adobe-spacecat-shared-data-access-3.19.0.tgz", - "integrity": "sha512-kXExL0RpLNlCz6XiUqv2dkdNAxuQ5Uvzi0zTQ3GMzvOuku7cVh5X8+EQANbum4VzrLTvMlvB4+csvHU/jPtCIQ==", + "resolved": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/669c7f824b915dc67dfb9fe2fc7e29917e38ace7/adobe-spacecat-shared-data-access-3.19.0.tgz", + "integrity": "sha512-zG/eVCSkWrY9M2ymKLv7KHigKdQP+UNYGY+hMKv8QMmjca1FFCuil/dgiCeLBaom9wkoBkt4S6k/R4DILT5ipQ==", "license": "Apache-2.0", "dependencies": { "@adobe/fetch": "^4.2.3", - "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/dd75eb8b13ba2555d7cde87df25c0ccfbd8085bc/adobe-spacecat-shared-utils-1.102.0.tgz", + "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/4dc817e97910e998b17ad1f535a7e289dca89d95/adobe-spacecat-shared-utils-1.102.0.tgz", "@aws-sdk/client-s3": "^3.940.0", "@supabase/postgrest-js": "^1.21.4", "@types/joi": "17.2.3", @@ -6913,8 +6913,8 @@ }, "node_modules/@adobe/spacecat-shared-utils": { "version": "1.102.0", - "resolved": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/dd75eb8b13ba2555d7cde87df25c0ccfbd8085bc/adobe-spacecat-shared-utils-1.102.0.tgz", - "integrity": "sha512-9WeQ1WYwdXARKC/svOzzInpMyjbiX2tDP/Mtxz/XbZhBN7yWVW0s6M3/8knHFeP3FkEjxIeuhGUeoukbQZ3wag==", + "resolved": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/4dc817e97910e998b17ad1f535a7e289dca89d95/adobe-spacecat-shared-utils-1.102.0.tgz", + "integrity": "sha512-0EnNJR8aopCukdznZlcJPQd8/luGhzEDGUobWa51cCZgnwJoAsxB7RjDztQ3GbYACmM3bHbCILqkeOqVdp8mKw==", "license": "Apache-2.0", "dependencies": { "@adobe/fetch": "4.2.3", @@ -6938,7 +6938,7 @@ }, "node_modules/@adobe/spacecat-shared-utils/node_modules/cheerio": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/cheerio/-/cheerio-1.2.0.tgz", "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "license": "MIT", "dependencies": { @@ -6962,9 +6962,9 @@ } }, "node_modules/@adobe/spacecat-shared-utils/node_modules/undici": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.23.0.tgz", - "integrity": "sha512-HVMxHKZKi+eL2mrUZDzDkKW3XvCjynhbtpSq20xQp4ePDFeSFuAfnvM0GIwZIv8fiKHjXFQ5WjxhCt15KRNj+g==", + "version": "7.24.0", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/undici/-/undici-7.24.0.tgz", + "integrity": "sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -6972,7 +6972,7 @@ }, "node_modules/@adobe/spacecat-shared-utils/node_modules/zod": { "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { diff --git a/package.json b/package.json index 6fd99792a..f6d3b19b0 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@adobe/spacecat-shared-ahrefs-client": "1.10.7", "@adobe/spacecat-shared-athena-client": "1.9.6", "@adobe/spacecat-shared-brand-client": "1.1.38", - "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/455812ffb2f22bebc900a31a5bf31edc281a7e69/adobe-spacecat-shared-data-access-3.19.0.tgz", + "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/669c7f824b915dc67dfb9fe2fc7e29917e38ace7/adobe-spacecat-shared-data-access-3.19.0.tgz", "@adobe/spacecat-shared-data-access-v2": "npm:@adobe/spacecat-shared-data-access@2.109.0", "@adobe/spacecat-shared-drs-client": "1.3.0", "@adobe/spacecat-shared-gpt-client": "1.6.19", @@ -87,7 +87,7 @@ "@adobe/spacecat-shared-slack-client": "1.6.2", "@adobe/spacecat-shared-tier-client": "1.3.15", "@adobe/spacecat-shared-tokowaka-client": "1.11.1", - "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/40ebf34d5a83568c9f2571d9eca6cc54/raw/dd75eb8b13ba2555d7cde87df25c0ccfbd8085bc/adobe-spacecat-shared-utils-1.102.0.tgz", + "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/4dc817e97910e998b17ad1f535a7e289dca89d95/adobe-spacecat-shared-utils-1.102.0.tgz", "@adobe/spacecat-shared-vault-secrets": "1.2.0", "@aws-sdk/client-s3": "3.1004.0", "@aws-sdk/client-secrets-manager": "3.1004.0", diff --git a/src/controllers/opportunities.js b/src/controllers/opportunities.js index 296cc3aa4..774d98a60 100644 --- a/src/controllers/opportunities.js +++ b/src/controllers/opportunities.js @@ -27,7 +27,7 @@ import { } from '@adobe/spacecat-shared-utils'; import { OpportunityDto } from '../dto/opportunity.js'; import AccessControlUtil from '../support/access-control-util.js'; -import { grantCompleteSuggestionsForOpportunity } from '../support/grant-complete-suggestions.js'; +import { grantSuggestionsForOpportunity } from '../support/grant-suggestions-handler.js'; const VALIDATION_ERROR_NAME = 'ValidationError'; @@ -157,7 +157,11 @@ function OpportunitiesController(ctx) { if (!oppty || oppty.getSiteId() !== siteId) { return notFound('Opportunity not found'); } - await grantCompleteSuggestionsForOpportunity(dataAccess, site, oppty); + try { + await grantSuggestionsForOpportunity(dataAccess, site, oppty); + /* c8 ignore next */ } catch (err) { + ctx.log?.warn?.('Grant suggestions handler failed', err?.message ?? err); + } return ok(OpportunityDto.toJSON(oppty)); }; diff --git a/src/controllers/suggestions.js b/src/controllers/suggestions.js index b2ea76740..a8a1b1a86 100644 --- a/src/controllers/suggestions.js +++ b/src/controllers/suggestions.js @@ -21,14 +21,12 @@ import { import { hasText, isArray, isNonEmptyArray, - isGranted, isNonEmptyObject, isObject, isInteger, isValidUUID, isValidUrl, } from '@adobe/spacecat-shared-utils'; - import { Suggestion as SuggestionModel } from '@adobe/spacecat-shared-data-access'; import TokowakaClient from '@adobe/spacecat-shared-tokowaka-client'; import { SuggestionDto, SUGGESTION_VIEWS, SUGGESTION_SKIP_REASONS } from '../dto/suggestion.js'; @@ -187,20 +185,6 @@ function SuggestionsController(ctx, sqs, env) { const accessControlUtil = AccessControlUtil.fromContext(ctx); - /** - * When the organization is freemium, returns only suggestions that have been granted (unlocked). - * Otherwise returns the same list unchanged. - * @param {Object} site - Site model (must have getOrganizationId()). - * @param {Array} suggestionEntities - List of suggestion entities. - * @returns {Array} Filtered list when freemium, otherwise unchanged. - */ - const filterGrantedIfFreemium = (site, suggestionEntities) => { - if (!suggestionEntities || suggestionEntities.length === 0) return suggestionEntities; - const organizationId = typeof site?.getOrganizationId === 'function' ? site.getOrganizationId() : undefined; - if (!organizationId || !Entitlement?.isFreemium(organizationId)) return suggestionEntities; - return suggestionEntities.filter((s) => isGranted(s)); - }; - /** * Gets all suggestions for a given site and opportunity * @param {Object} context of the request @@ -254,8 +238,10 @@ function SuggestionsController(ctx, sqs, env) { (sugg) => statuses.includes(sugg.getStatus()), ); } - const filtered = filterGrantedIfFreemium(site, suggestionEntities); - const suggestions = filtered.map( + const suggestionIds = suggestionEntities.map((s) => s.getId()); + const { grantedIds } = await Suggestion.splitSuggestionsByGrantStatus(suggestionIds); + const grantedEntities = suggestionEntities.filter((s) => grantedIds.includes(s.getId())); + const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); return ok(suggestions); @@ -313,8 +299,10 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const filtered = filterGrantedIfFreemium(site, suggestionEntities); - const suggestions = filtered.map( + const suggestionIds = suggestionEntities.map((s) => s.getId()); + const { grantedIds } = await Suggestion.splitSuggestionsByGrantStatus(suggestionIds); + const grantedEntities = suggestionEntities.filter((s) => grantedIds.includes(s.getId())); + const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -369,8 +357,10 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const filtered = filterGrantedIfFreemium(site, suggestionEntities); - const suggestions = filtered.map( + const suggestionIds = suggestionEntities.map((s) => s.getId()); + const { grantedIds } = await Suggestion.splitSuggestionsByGrantStatus(suggestionIds); + const grantedEntities = suggestionEntities.filter((s) => grantedIds.includes(s.getId())); + const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); return ok(suggestions); @@ -428,8 +418,10 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const filtered = filterGrantedIfFreemium(site, suggestionEntities); - const suggestions = filtered.map( + const suggestionIds = suggestionEntities.map((s) => s.getId()); + const { grantedIds } = await Suggestion.splitSuggestionsByGrantStatus(suggestionIds); + const grantedEntities = suggestionEntities.filter((s) => grantedIds.includes(s.getId())); + const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); return ok({ @@ -487,7 +479,9 @@ function SuggestionsController(ctx, sqs, env) { } // Freemium: only allow access to granted suggestions const organizationId = typeof site?.getOrganizationId === 'function' ? site.getOrganizationId() : undefined; - if (organizationId && Entitlement?.isFreemium(organizationId) && !isGranted(suggestion)) { + const isFreemium = organizationId + && Entitlement?.isFreemium(organizationId); + if (isFreemium && !(await Suggestion.isSuggestionGranted(suggestion.getId()))) { return notFound('Suggestion not found'); } return ok(SuggestionDto.toJSON(suggestion, view, opportunity)); @@ -1011,6 +1005,14 @@ function SuggestionsController(ctx, sqs, env) { const suggestions = await Suggestion.allByOpportunityId( opportunityId, ); + const requestedSuggestions = suggestions.filter((s) => suggestionIds.includes(s.getId())); + if (requestedSuggestions.length > 0) { + const requestedIds = requestedSuggestions.map((s) => s.getId()); + const { notGrantedIds } = await Suggestion.splitSuggestionsByGrantStatus(requestedIds); + if (notGrantedIds.length > 0) { + return badRequest('All suggestion IDs must be granted before autofix can be executed'); + } + } const validSuggestions = []; const failedSuggestions = []; suggestions.forEach((suggestion) => { diff --git a/src/support/grant-complete-suggestions.js b/src/support/grant-complete-suggestions.js deleted file mode 100644 index 20ac1e5c4..000000000 --- a/src/support/grant-complete-suggestions.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2025 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import { - isGranted, - isSuggestionComplete, - OPPORTUNITY_TYPES, -} from '@adobe/spacecat-shared-utils'; - -/** Maps opportunity type (e.g. broken-backlinks) to token type for freemium grant. */ -const OPPORTUNITY_TYPE_TO_TOKEN_TYPE = { - [OPPORTUNITY_TYPES.BROKEN_BACKLINKS]: 'BROKEN_BACKLINK', - [OPPORTUNITY_TYPES.CWV]: 'CWV', - [OPPORTUNITY_TYPES.ALT_TEXT]: 'ALT_TEXT', -}; - -/** - * When the organization is freemium, grants up to remaining token count for suggestions that are - * complete (per suggestion-complete.js) and not yet granted. Loads suggestions for the opportunity, - * then runs grant logic. Only runs for opportunity types that have token types and complete - * handlers (broken-backlinks, cwv, alt-text). Mutates and persists grants on suggestion entities. - * - * @param {Object} dataAccess - Data access (Suggestion, Token, Entitlement). - * @param {Object} site - Site model (getOrganizationId, getId). - * @param {Object} opportunity - Opportunity model (getType, getId). - * @returns {Promise} - */ -export async function grantCompleteSuggestionsForOpportunity(dataAccess, site, opportunity) { - if (!dataAccess || !site || !opportunity) return; - - const { Suggestion, Token, Entitlement } = dataAccess; - if (!Token || !Suggestion || !Entitlement) return; - - const organizationId = typeof site?.getOrganizationId === 'function' ? site.getOrganizationId() : undefined; - if (!organizationId || !Entitlement.isFreemium(organizationId)) return; - - const opportunityType = typeof opportunity.getType === 'function' ? opportunity.getType() : opportunity.type; - const tokenType = OPPORTUNITY_TYPE_TO_TOKEN_TYPE[opportunityType]; - if (!tokenType) return; - - const siteId = typeof site?.getId === 'function' ? site.getId() : undefined; - if (!siteId) return; - - const opptyId = typeof opportunity?.getId === 'function' ? opportunity.getId() : opportunity.id; - if (!opptyId) return; - - const suggestionEntities = await Suggestion.allByOpportunityId(opptyId); - if (!suggestionEntities?.length) return; - - const cycle = new Date().toISOString().slice(0, 7); // YYYY-MM - const remaining = await Token.getRemainingToken(siteId, tokenType, cycle); - if (remaining < 1) return; - - const completeUngranted = suggestionEntities - .filter((s) => !isGranted(s) && isSuggestionComplete(s, opportunityType)) - .sort((a, b) => (a.getRank?.() ?? a.rank ?? 0) - (b.getRank?.() ?? b.rank ?? 0)); - - const toGrant = completeUngranted.slice(0, remaining); - const grantedAt = new Date().toISOString(); - - for (const suggestion of toGrant) { - // eslint-disable-next-line no-await-in-loop -- token use must be sequential per suggestion - const grant = await Token.useToken(siteId, tokenType, cycle); - if (!grant) break; - suggestion.setGrants({ - cycle: grant.cycle, - tokenId: grant.tokenId, - grantedAt, - }); - // eslint-disable-next-line no-await-in-loop -- save must complete before next iteration - await suggestion.save(); - } -} diff --git a/src/support/grant-suggestions-handler.js b/src/support/grant-suggestions-handler.js new file mode 100644 index 000000000..748fd9606 --- /dev/null +++ b/src/support/grant-suggestions-handler.js @@ -0,0 +1,126 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { Suggestion as SuggestionModel } from '@adobe/spacecat-shared-data-access'; +import { getTokenGrantConfigByOpportunity } from '@adobe/spacecat-shared-utils'; + +/** + * Default sort: by rank ascending, then id ascending. + */ +function defaultSortFn(groupA, groupB) { + const a = groupA[0]; + const b = groupB[0]; + const rankA = typeof a?.getRank === 'function' ? a.getRank() : (a?.rank ?? 0); + const rankB = typeof b?.getRank === 'function' ? b.getRank() : (b?.rank ?? 0); + if (rankA !== rankB) return rankA - rankB; + const idA = typeof a?.getId === 'function' ? a.getId() : (a?.id ?? ''); + const idB = typeof b?.getId === 'function' ? b.getId() : (b?.id ?? ''); + return idA.localeCompare(idB); +} + +/** + * Per-opportunity grouping and sorting strategies. + * Each entry can define `groupFn` and/or `sortFn`. + * Opportunities not listed here use the defaults + * (one group per suggestion, sorted by rank asc then id asc). + */ +const OPPORTUNITY_STRATEGIES = { + // Example: group broken-backlinks suggestions by source URL + // 'broken-backlinks': { + // groupFn: (suggestions) => { ... }, + // sortFn: (groupA, groupB) => { ... }, + // }, +}; + +/** + * Groups and sorts suggestions for grant selection. + * Each group consumes one token when granted. + * Uses per-opportunity strategies from OPPORTUNITY_STRATEGIES, + * falling back to default (one group per suggestion, rank asc). + * + * @param {Array} suggestions - Suggestion entities or plain objects. + * @param {string} [opportunityName] - Opportunity name for + * strategy lookup. + * @returns {Array} Sorted groups of suggestions. + */ +export function getTopSuggestions(suggestions, opportunityName) { + if (!Array.isArray(suggestions) || suggestions.length === 0) { + return []; + } + const strategy = OPPORTUNITY_STRATEGIES[opportunityName] || {}; + const { groupFn, sortFn } = strategy; + // c8 ignore: groupFn branch covered when strategies are added + const groups = groupFn + ? groupFn(suggestions) /* c8 ignore next */ + : suggestions.map((s) => [s]); + return [...groups].sort(sortFn ?? defaultSortFn); +} + +/** + * Grants top ungranted suggestions for an opportunity. Ensures a + * token exists for the current cycle; if none, creates one with + * total = (config max) minus already-granted count. Then grants + * top ungranted groups up to the remaining token count. + * + * @param {Object} dataAccess - Data access collections. + * @param {Object} site - Site model (getId()). + * @param {Object} opportunity - Opportunity model + * (getId(), getType()). + * @returns {Promise} + */ +export async function grantSuggestionsForOpportunity(dataAccess, site, opportunity) { + const Suggestion = dataAccess?.Suggestion; + const Token = dataAccess?.Token; + const siteId = site?.getId(); + const opptyId = opportunity?.getId(); + const oppType = opportunity?.getType(); + const config = oppType + ? getTokenGrantConfigByOpportunity(oppType) : null; + const tokenType = config?.tokenType; + + if (!Suggestion || !Token || !siteId || !opptyId || !config) return; + + const { STATUSES } = SuggestionModel; + const newSuggestions = await Suggestion + .allByOpportunityIdAndStatus(opptyId, STATUSES.NEW); + const newSuggestionIds = newSuggestions.map((s) => s.getId()); + if (!newSuggestionIds.length) return; + + let token = await Token.findBySiteIdAndTokenType(siteId, tokenType); + if (!token) { + const { grantIds } = await Suggestion + .splitSuggestionsByGrantStatus(newSuggestionIds); + const suppliedTotal = Math.max(1, config.tokensPerCycle - (grantIds?.length ?? 0)); + token = await Token.findBySiteIdAndTokenType(siteId, tokenType, { + createIfNotFound: true, + total: suppliedTotal, + }); + } + + const remaining = token.getRemaining(); + if (remaining <= 0) return; + + const { notGrantedIds } = await Suggestion + .splitSuggestionsByGrantStatus(newSuggestionIds); + const notGrantedEntities = newSuggestions + .filter((s) => notGrantedIds.includes(s.getId())); + const topGroups = getTopSuggestions(notGrantedEntities, oppType) + .slice(0, remaining); + await Promise.all( + topGroups.map((group) => { + const ids = group.map((s) => s.getId()).filter(Boolean); + return ids.length > 0 + ? Suggestion.grantSuggestions(ids, siteId, tokenType) + : Promise.resolve(); + }), + ); +} diff --git a/test/controllers/opportunities.test.js b/test/controllers/opportunities.test.js index 22b79ea07..840729535 100644 --- a/test/controllers/opportunities.test.js +++ b/test/controllers/opportunities.test.js @@ -362,6 +362,107 @@ describe('Opportunities Controller', () => { expect(error).to.have.property('message', 'Opportunity not found'); }); + it('gets opportunity by ID invokes grant suggestions handler when Token is in dataAccess', async () => { + const mockToken = { + findBySiteIdAndTokenType: sandbox.stub().resolves({ getRemaining: () => 1 }), + }; + const ctxWithToken = { + ...mockContext, + dataAccess: { ...mockOpportunityDataAccess, Token: mockToken }, + }; + const controllerWithToken = OpportunitiesController(ctxWithToken); + const previousType = opptys[0].type; + opptys[0].type = 'cwv'; + try { + const response = await controllerWithToken.getByID({ + params: { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID }, + }); + expect(response.status).to.equal(200); + if (mockToken.findBySiteIdAndTokenType.called) { + expect(mockToken.findBySiteIdAndTokenType).to.have.been.calledOnceWith( + SITE_ID, + 'monthly_suggestion_cwv', + ); + } + } finally { + opptys[0].type = previousType; + } + }); + + it('getByID catches grant suggestions handler errors gracefully', async () => { + const mockSuggestion = { + allByOpportunityIdAndStatus: sandbox.stub() + .rejects(new Error('db failure')), + }; + const mockToken = { + findBySiteIdAndTokenType: sandbox.stub(), + }; + const ctxWithToken = { + ...mockContext, + dataAccess: { + ...mockOpportunityDataAccess, + Suggestion: mockSuggestion, + Token: mockToken, + }, + }; + const controllerWithToken = OpportunitiesController(ctxWithToken); + const previousType = opptys[0].type; + opptys[0].type = 'cwv'; + try { + const response = await controllerWithToken.getByID({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + }, + }); + expect(response.status).to.equal(200); + expect(mockContext.log.warn).to.have.been.calledOnce; + } finally { + opptys[0].type = previousType; + } + }); + + it('getByID catches grant suggestions handler errors gracefully when error has no message', async () => { + const mockSuggestion = { + allByOpportunityIdAndStatus: sandbox.stub() + // eslint-disable-next-line prefer-promise-reject-errors + .callsFake(() => Promise.reject(null)), + }; + const mockToken = { + findBySiteIdAndTokenType: sandbox.stub(), + }; + const mockSiteEntity = { + getId: () => SITE_ID, + }; + const ctxWithToken = { + ...mockContext, + dataAccess: { + ...mockOpportunityDataAccess, + Site: { findById: sandbox.stub().resolves(mockSiteEntity) }, + Suggestion: mockSuggestion, + Token: mockToken, + }, + }; + const controllerWithToken = OpportunitiesController(ctxWithToken); + const previousType = opptys[0].type; + opptys[0].type = 'cwv'; + try { + const response = await controllerWithToken.getByID({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + }, + }); + expect(response.status).to.equal(200); + expect(mockContext.log.warn).to.have.been.calledOnceWith( + 'Grant suggestions handler failed', + null, + ); + } finally { + opptys[0].type = previousType; + } + }); + // TODO: Complete tests for OpportunitiesController it('creates an opportunity', async () => { const response = await opportunitiesController.createOpportunity({ diff --git a/test/controllers/suggestions.test.js b/test/controllers/suggestions.test.js index 2bf26d698..c7c8826c2 100644 --- a/test/controllers/suggestions.test.js +++ b/test/controllers/suggestions.test.js @@ -462,6 +462,10 @@ describe('Suggestions Controller', () => { return Promise.resolve(mockSuggestionEntity(suggData)); }), getFixEntitiesBySuggestionId: sandbox.stub(), + splitSuggestionsByGrantStatus: sandbox.stub().callsFake((suggestionIds) => { + const ids = suggestionIds || []; + return Promise.resolve({ grantedIds: ids, notGrantedIds: [], grantIds: ids.map((id) => `grant-${id}`) }); + }), }; mockSuggestionDataAccess = { @@ -1592,6 +1596,38 @@ describe('Suggestions Controller', () => { expect(error).to.have.property('message', 'not found'); }); + it('getByID returns not found for freemium org with ungranted suggestion', async () => { + const ORG_ID = 'org-uuid'; + const freemiumSite = { + getId: sandbox.stub().returns(SITE_ID), + getOrganizationId: sandbox.stub().returns(ORG_ID), + getDeliveryType: sandbox.stub().returns('aem_edge'), + }; + const mockEntitlement = { + isFreemium: sandbox.stub().returns(true), + }; + mockSite.findById.withArgs(SITE_ID).resolves(freemiumSite); + mockSuggestionDataAccess.Entitlement = mockEntitlement; + mockSuggestionDataAccess.Suggestion.isSuggestionGranted = sandbox.stub() + .resolves(false); + const ctrl = SuggestionsController({ + dataAccess: mockSuggestionDataAccess, + pathInfo: { headers: { 'x-product': 'llmo' } }, + ...authContext, + }, mockSqs, { AUTOFIX_JOBS_QUEUE: 'https://autofix-jobs-queue' }); + const response = await ctrl.getByID({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + suggestionId: SUGGESTION_IDS[0], + }, + ...context, + }); + expect(response.status).to.equal(404); + delete mockSuggestionDataAccess.Entitlement; + delete mockSuggestionDataAccess.Suggestion.isSuggestionGranted; + }); + describe('getSuggestionFixes', () => { const FIX_IDS = [ 'fix-id-1', @@ -3261,6 +3297,26 @@ describe('Suggestions Controller', () => { sandbox.restore(); }); + it('returns bad request when any suggestion ID is not granted for autofix', async () => { + opportunity.getType = sandbox.stub().returns('meta-tags'); + const requestedEntities = [mockSuggestionEntity(suggs[0]), mockSuggestionEntity(suggs[2])]; + mockSuggestion.allByOpportunityId.resolves(requestedEntities); + mockSuggestion.splitSuggestionsByGrantStatus.resolves({ + grantedIds: [requestedEntities[0].getId()], + notGrantedIds: [requestedEntities[1].getId()], + grantIds: ['grant-1'], + }); + const response = await suggestionsControllerWithMock.autofixSuggestions({ + params: { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID }, + data: { suggestionIds: [SUGGESTION_IDS[0], SUGGESTION_IDS[2]] }, + ...context, + }); + expect(response.status).to.equal(400); + const body = await response.json(); + expect(body).to.have.property('message', 'All suggestion IDs must be granted before autofix can be executed'); + expect(mockSuggestion.bulkUpdateStatus).to.not.have.been.called; + }); + it('triggers autofixSuggestion and sets suggestions to in-progress', async () => { opportunity.getType = sandbox.stub().returns('meta-tags'); mockSuggestion.allByOpportunityId.resolves( diff --git a/test/support/grant-suggestions-handler.test.js b/test/support/grant-suggestions-handler.test.js new file mode 100644 index 000000000..3b0a0f24f --- /dev/null +++ b/test/support/grant-suggestions-handler.test.js @@ -0,0 +1,280 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { getTopSuggestions, grantSuggestionsForOpportunity } from '../../src/support/grant-suggestions-handler.js'; + +use(chaiAsPromised); +use(sinonChai); + +describe('grant-suggestions-handler', () => { + const sandbox = sinon.createSandbox(); + + afterEach(() => { + sandbox.restore(); + }); + + describe('getTopSuggestions', () => { + it('returns empty array when suggestions is null or undefined', () => { + expect(getTopSuggestions(null)).to.deep.equal([]); + expect(getTopSuggestions(undefined)).to.deep.equal([]); + }); + + it('returns empty array when suggestions is empty', () => { + expect(getTopSuggestions([])).to.deep.equal([]); + }); + + it('returns one group per suggestion (default grouping)', () => { + const s1 = { getId: () => 'id-1', getRank: () => 10 }; + const s2 = { getId: () => 'id-2', getRank: () => 5 }; + const groups = getTopSuggestions([s1, s2]); + expect(groups).to.have.lengthOf(2); + expect(groups.every((g) => Array.isArray(g) && g.length === 1)).to.be.true; + const flat = groups.flat(); + expect(flat).to.include(s1); + expect(flat).to.include(s2); + }); + + it('sorts groups by rank ascending then by id (default sort)', () => { + const s1 = { getId: () => 'id-b', getRank: () => 10 }; + const s2 = { getId: () => 'id-a', getRank: () => 5 }; + const s3 = { getId: () => 'id-c', getRank: () => 10 }; + const groups = getTopSuggestions([s1, s2, s3]); + expect(groups).to.have.lengthOf(3); + expect(groups[0][0]).to.equal(s2); // rank 5 first + expect(groups[1][0]).to.equal(s1); // rank 10, id-b before id-c + expect(groups[2][0]).to.equal(s3); // rank 10, id-c + }); + + it('handles plain objects with id and rank', () => { + const s1 = { id: 'x', rank: 1 }; + const s2 = { id: 'y', rank: 0 }; + const groups = getTopSuggestions([s1, s2]); + expect(groups).to.have.lengthOf(2); + expect(groups[0][0]).to.equal(s2); + expect(groups[1][0]).to.equal(s1); + }); + + it('uses default strategy for unknown opportunity name', () => { + const s1 = { getId: () => 'id-1', getRank: () => 10 }; + const s2 = { getId: () => 'id-2', getRank: () => 5 }; + const groups = getTopSuggestions([s1, s2], 'unknown-type'); + expect(groups).to.have.lengthOf(2); + expect(groups[0][0]).to.equal(s2); + expect(groups[1][0]).to.equal(s1); + }); + + it('falls back to defaults for objects missing rank and id', () => { + const s1 = { foo: 'bar' }; + const s2 = { foo: 'baz' }; + const groups = getTopSuggestions([s1, s2]); + expect(groups).to.have.lengthOf(2); + }); + }); + + describe('grantSuggestionsForOpportunity', () => { + const siteId = 'site-uuid'; + const opptyId = 'oppty-uuid'; + const site = { getId: () => siteId }; + const opportunity = { getId: () => opptyId, getType: () => 'cwv' }; + + it('returns early when dataAccess is missing', async () => { + const Token = sandbox.stub(); + await grantSuggestionsForOpportunity(null, site, opportunity); + await grantSuggestionsForOpportunity(undefined, site, opportunity); + expect(Token).to.not.have.been.called; + }); + + it('returns early when site or opportunity is missing', async () => { + const dataAccess = { Suggestion: {}, Token: {} }; + await grantSuggestionsForOpportunity(dataAccess, null, opportunity); + await grantSuggestionsForOpportunity(dataAccess, site, null); + expect(true).to.be.true; + }); + + it('returns early when Token or Suggestion is missing from dataAccess', async () => { + const Suggestion = { allByOpportunityIdAndStatus: sandbox.stub() }; + await grantSuggestionsForOpportunity({ Suggestion, Token: null }, site, opportunity); + await grantSuggestionsForOpportunity({ Suggestion: null, Token: {} }, site, opportunity); + expect(Suggestion.allByOpportunityIdAndStatus).to.not.have.been.called; + }); + + it('returns early when opportunity type has no token type mapping', async () => { + const Token = { findBySiteIdAndTokenType: sandbox.stub() }; + const dataAccess = { Suggestion: {}, Token }; + const oppNoMapping = { getId: () => opptyId, getType: () => 'unknown-type' }; + await grantSuggestionsForOpportunity(dataAccess, site, oppNoMapping); + expect(Token.findBySiteIdAndTokenType).to.not.have.been.called; + }); + + it('returns early when no new suggestions exist', async () => { + const Suggestion = { + allByOpportunityIdAndStatus: sandbox.stub().resolves([]), + }; + const Token = { findBySiteIdAndTokenType: sandbox.stub() }; + const dataAccess = { Suggestion, Token }; + await grantSuggestionsForOpportunity(dataAccess, site, opportunity); + expect(Token.findBySiteIdAndTokenType).to.not.have.been.called; + }); + + it('returns early when token exists with no remaining', async () => { + const mockSugg = { getId: () => 'sugg-1', getRank: () => 1 }; + const existingToken = { getRemaining: () => 0 }; + const Suggestion = { + allByOpportunityIdAndStatus: sandbox.stub().resolves([mockSugg]), + splitSuggestionsByGrantStatus: sandbox.stub(), + grantSuggestions: sandbox.stub(), + }; + const Token = { + findBySiteIdAndTokenType: sandbox.stub().resolves(existingToken), + }; + const dataAccess = { Suggestion, Token }; + await grantSuggestionsForOpportunity(dataAccess, site, opportunity); + expect(Suggestion.splitSuggestionsByGrantStatus) + .to.not.have.been.called; + expect(Suggestion.grantSuggestions).to.not.have.been.called; + }); + + it('creates token when none exists and grants top suggestions', async () => { + const s1 = { getId: () => 'sugg-1', getRank: () => 1 }; + const s2 = { getId: () => 'sugg-2', getRank: () => 2 }; + const createdToken = { getRemaining: () => 2 }; + const Suggestion = { + allByOpportunityIdAndStatus: sandbox.stub().resolves([s1, s2]), + splitSuggestionsByGrantStatus: sandbox.stub(), + grantSuggestions: sandbox.stub().resolves({ success: true }), + }; + // First call: no token, second call: create + Suggestion.splitSuggestionsByGrantStatus + .onFirstCall().resolves({ grantIds: [] }) + .onSecondCall().resolves({ + notGrantedIds: ['sugg-1', 'sugg-2'], + }); + const Token = { + findBySiteIdAndTokenType: sandbox.stub(), + }; + Token.findBySiteIdAndTokenType + .onFirstCall().resolves(null) + .onSecondCall().resolves(createdToken); + const dataAccess = { Suggestion, Token }; + + await grantSuggestionsForOpportunity(dataAccess, site, opportunity); + + expect(Token.findBySiteIdAndTokenType).to.have.been.calledTwice; + expect(Token.findBySiteIdAndTokenType.secondCall.args[2]) + .to.deep.include({ createIfNotFound: true }); + expect(Suggestion.grantSuggestions).to.have.been.calledTwice; + }); + + it('grants only up to remaining token count', async () => { + const s1 = { getId: () => 'sugg-1', getRank: () => 1 }; + const s2 = { getId: () => 'sugg-2', getRank: () => 2 }; + const existingToken = { getRemaining: () => 1 }; + const Suggestion = { + allByOpportunityIdAndStatus: sandbox.stub().resolves([s1, s2]), + splitSuggestionsByGrantStatus: sandbox.stub().resolves({ + notGrantedIds: ['sugg-1', 'sugg-2'], + }), + grantSuggestions: sandbox.stub().resolves({ success: true }), + }; + const Token = { + findBySiteIdAndTokenType: sandbox.stub() + .resolves(existingToken), + }; + const dataAccess = { Suggestion, Token }; + + await grantSuggestionsForOpportunity(dataAccess, site, opportunity); + + // Only 1 remaining, so only 1 grant call + expect(Suggestion.grantSuggestions).to.have.been.calledOnce; + expect(Suggestion.grantSuggestions.firstCall.args[0]) + .to.deep.equal(['sugg-1']); + }); + + it('adjusts total by already-granted count when creating token', async () => { + const s1 = { getId: () => 'sugg-1', getRank: () => 1 }; + const createdToken = { getRemaining: () => 1 }; + const Suggestion = { + allByOpportunityIdAndStatus: sandbox.stub().resolves([s1]), + splitSuggestionsByGrantStatus: sandbox.stub(), + grantSuggestions: sandbox.stub().resolves({ success: true }), + }; + Suggestion.splitSuggestionsByGrantStatus + .onFirstCall().resolves({ grantIds: ['g1', 'g2'] }) + .onSecondCall().resolves({ notGrantedIds: ['sugg-1'] }); + const Token = { + findBySiteIdAndTokenType: sandbox.stub(), + }; + Token.findBySiteIdAndTokenType + .onFirstCall().resolves(null) + .onSecondCall().resolves(createdToken); + const dataAccess = { Suggestion, Token }; + + await grantSuggestionsForOpportunity(dataAccess, site, opportunity); + + // tokensPerCycle=3, 2 already granted => total=1 + const createArgs = Token.findBySiteIdAndTokenType.secondCall.args; + expect(createArgs[2]).to.deep.include({ total: 1 }); + }); + + it('handles undefined grantIds when creating token', async () => { + const s1 = { getId: () => 'sugg-1', getRank: () => 1 }; + const createdToken = { getRemaining: () => 1 }; + const Suggestion = { + allByOpportunityIdAndStatus: sandbox.stub().resolves([s1]), + splitSuggestionsByGrantStatus: sandbox.stub(), + grantSuggestions: sandbox.stub().resolves({ success: true }), + }; + Suggestion.splitSuggestionsByGrantStatus + .onFirstCall().resolves({}) + .onSecondCall().resolves({ notGrantedIds: ['sugg-1'] }); + const Token = { + findBySiteIdAndTokenType: sandbox.stub(), + }; + Token.findBySiteIdAndTokenType + .onFirstCall().resolves(null) + .onSecondCall().resolves(createdToken); + const dataAccess = { Suggestion, Token }; + + await grantSuggestionsForOpportunity(dataAccess, site, opportunity); + + const createArgs = Token.findBySiteIdAndTokenType.secondCall.args; + expect(createArgs[2]).to.deep.include({ total: 3 }); + }); + + it('skips grant for groups with no valid ids', async () => { + const s1 = { getId: () => '', getRank: () => 1 }; + const existingToken = { getRemaining: () => 1 }; + const Suggestion = { + allByOpportunityIdAndStatus: sandbox.stub().resolves([s1]), + splitSuggestionsByGrantStatus: sandbox.stub().resolves({ + notGrantedIds: [''], + }), + grantSuggestions: sandbox.stub(), + }; + const Token = { + findBySiteIdAndTokenType: sandbox.stub() + .resolves(existingToken), + }; + const dataAccess = { Suggestion, Token }; + + await grantSuggestionsForOpportunity(dataAccess, site, opportunity); + + expect(Suggestion.grantSuggestions).to.not.have.been.called; + }); + }); +}); From c610be1fa55d818a1d7c4fb8214452a53848b39c Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Mon, 16 Mar 2026 15:09:56 +0530 Subject: [PATCH 10/16] fix: filter by grant status --- src/controllers/opportunities.js | 11 ++++-- src/controllers/suggestions.js | 42 +++++++++++---------- test/controllers/opportunities.test.js | 21 ++++++++++- test/controllers/suggestions.test.js | 51 +++++++++++++++----------- 4 files changed, 79 insertions(+), 46 deletions(-) diff --git a/src/controllers/opportunities.js b/src/controllers/opportunities.js index 774d98a60..5c798bf53 100644 --- a/src/controllers/opportunities.js +++ b/src/controllers/opportunities.js @@ -28,6 +28,7 @@ import { import { OpportunityDto } from '../dto/opportunity.js'; import AccessControlUtil from '../support/access-control-util.js'; import { grantSuggestionsForOpportunity } from '../support/grant-suggestions-handler.js'; +import { getIsSummitPlgEnabled } from '../support/utils.js'; const VALIDATION_ERROR_NAME = 'ValidationError'; @@ -157,10 +158,12 @@ function OpportunitiesController(ctx) { if (!oppty || oppty.getSiteId() !== siteId) { return notFound('Opportunity not found'); } - try { - await grantSuggestionsForOpportunity(dataAccess, site, oppty); - /* c8 ignore next */ } catch (err) { - ctx.log?.warn?.('Grant suggestions handler failed', err?.message ?? err); + if (await getIsSummitPlgEnabled(site, ctx)) { + try { + await grantSuggestionsForOpportunity(dataAccess, site, oppty); + /* c8 ignore next */ } catch (err) { + ctx.log?.warn?.('Grant suggestions handler failed', err?.message ?? err); + } } return ok(OpportunityDto.toJSON(oppty)); }; diff --git a/src/controllers/suggestions.js b/src/controllers/suggestions.js index a8a1b1a86..f34b9fb1b 100644 --- a/src/controllers/suggestions.js +++ b/src/controllers/suggestions.js @@ -36,6 +36,7 @@ import { getIMSPromiseToken, ErrorWithStatusCode, getHostName, + getIsSummitPlgEnabled, } from '../support/utils.js'; import AccessControlUtil from '../support/access-control-util.js'; @@ -172,7 +173,7 @@ function SuggestionsController(ctx, sqs, env) { }; const { - Opportunity, Suggestion, Site, Configuration, Entitlement, + Opportunity, Suggestion, Site, Configuration, } = dataAccess; if (!isObject(Opportunity)) { @@ -185,6 +186,20 @@ function SuggestionsController(ctx, sqs, env) { const accessControlUtil = AccessControlUtil.fromContext(ctx); + /** + * Filters suggestions to only granted ones when summit-plg is enabled for the site. + * Returns all suggestions unchanged when summit-plg is not enabled. + * @param {Object} site - Site entity. + * @param {Array} suggestions - Suggestion entities to filter. + * @returns {Promise} Filtered suggestion entities. + */ + const filterByGrantStatus = async (site, suggestions) => { + if (!await getIsSummitPlgEnabled(site, ctx)) return suggestions; + const ids = suggestions.map((s) => s.getId()); + const { grantedIds } = await Suggestion.splitSuggestionsByGrantStatus(ids); + return suggestions.filter((s) => grantedIds.includes(s.getId())); + }; + /** * Gets all suggestions for a given site and opportunity * @param {Object} context of the request @@ -238,9 +253,7 @@ function SuggestionsController(ctx, sqs, env) { (sugg) => statuses.includes(sugg.getStatus()), ); } - const suggestionIds = suggestionEntities.map((s) => s.getId()); - const { grantedIds } = await Suggestion.splitSuggestionsByGrantStatus(suggestionIds); - const grantedEntities = suggestionEntities.filter((s) => grantedIds.includes(s.getId())); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -299,9 +312,7 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const suggestionIds = suggestionEntities.map((s) => s.getId()); - const { grantedIds } = await Suggestion.splitSuggestionsByGrantStatus(suggestionIds); - const grantedEntities = suggestionEntities.filter((s) => grantedIds.includes(s.getId())); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -357,9 +368,7 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const suggestionIds = suggestionEntities.map((s) => s.getId()); - const { grantedIds } = await Suggestion.splitSuggestionsByGrantStatus(suggestionIds); - const grantedEntities = suggestionEntities.filter((s) => grantedIds.includes(s.getId())); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -418,9 +427,7 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const suggestionIds = suggestionEntities.map((s) => s.getId()); - const { grantedIds } = await Suggestion.splitSuggestionsByGrantStatus(suggestionIds); - const grantedEntities = suggestionEntities.filter((s) => grantedIds.includes(s.getId())); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -477,11 +484,8 @@ function SuggestionsController(ctx, sqs, env) { if (!opportunity || opportunity.getSiteId() !== siteId) { return notFound(); } - // Freemium: only allow access to granted suggestions - const organizationId = typeof site?.getOrganizationId === 'function' ? site.getOrganizationId() : undefined; - const isFreemium = organizationId - && Entitlement?.isFreemium(organizationId); - if (isFreemium && !(await Suggestion.isSuggestionGranted(suggestion.getId()))) { + if (await getIsSummitPlgEnabled(site, ctx) + && !(await Suggestion.isSuggestionGranted(suggestion.getId()))) { return notFound('Suggestion not found'); } return ok(SuggestionDto.toJSON(suggestion, view, opportunity)); @@ -1006,7 +1010,7 @@ function SuggestionsController(ctx, sqs, env) { opportunityId, ); const requestedSuggestions = suggestions.filter((s) => suggestionIds.includes(s.getId())); - if (requestedSuggestions.length > 0) { + if (await getIsSummitPlgEnabled(site, ctx) && requestedSuggestions.length > 0) { const requestedIds = requestedSuggestions.map((s) => s.getId()); const { notGrantedIds } = await Suggestion.splitSuggestionsByGrantStatus(requestedIds); if (notGrantedIds.length > 0) { diff --git a/test/controllers/opportunities.test.js b/test/controllers/opportunities.test.js index 840729535..ac0fb6d82 100644 --- a/test/controllers/opportunities.test.js +++ b/test/controllers/opportunities.test.js @@ -366,9 +366,16 @@ describe('Opportunities Controller', () => { const mockToken = { findBySiteIdAndTokenType: sandbox.stub().resolves({ getRemaining: () => 1 }), }; + const mockConfig = { + findLatest: sandbox.stub().resolves({ + isHandlerEnabledForSite: sandbox.stub().returns(true), + }), + }; const ctxWithToken = { ...mockContext, - dataAccess: { ...mockOpportunityDataAccess, Token: mockToken }, + dataAccess: { + ...mockOpportunityDataAccess, Token: mockToken, Configuration: mockConfig, + }, }; const controllerWithToken = OpportunitiesController(ctxWithToken); const previousType = opptys[0].type; @@ -397,12 +404,18 @@ describe('Opportunities Controller', () => { const mockToken = { findBySiteIdAndTokenType: sandbox.stub(), }; + const mockConfig = { + findLatest: sandbox.stub().resolves({ + isHandlerEnabledForSite: sandbox.stub().returns(true), + }), + }; const ctxWithToken = { ...mockContext, dataAccess: { ...mockOpportunityDataAccess, Suggestion: mockSuggestion, Token: mockToken, + Configuration: mockConfig, }, }; const controllerWithToken = OpportunitiesController(ctxWithToken); @@ -434,6 +447,11 @@ describe('Opportunities Controller', () => { const mockSiteEntity = { getId: () => SITE_ID, }; + const mockConfig = { + findLatest: sandbox.stub().resolves({ + isHandlerEnabledForSite: sandbox.stub().returns(true), + }), + }; const ctxWithToken = { ...mockContext, dataAccess: { @@ -441,6 +459,7 @@ describe('Opportunities Controller', () => { Site: { findById: sandbox.stub().resolves(mockSiteEntity) }, Suggestion: mockSuggestion, Token: mockToken, + Configuration: mockConfig, }, }; const controllerWithToken = OpportunitiesController(ctxWithToken); diff --git a/test/controllers/suggestions.test.js b/test/controllers/suggestions.test.js index c7c8826c2..b551536bf 100644 --- a/test/controllers/suggestions.test.js +++ b/test/controllers/suggestions.test.js @@ -410,6 +410,7 @@ describe('Suggestions Controller', () => { isHandlerEnabledForSite.withArgs('meta-tags-auto-fix', site).returns(true); isHandlerEnabledForSite.withArgs('form-accessibility-auto-fix', site).returns(true); isHandlerEnabledForSite.withArgs('product-metatags-auto-fix', site).returns(true); + isHandlerEnabledForSite.withArgs('summit-plg', site).returns(true); isHandlerEnabledForSite.withArgs('broken-backlinks-auto-fix', siteNotEnabled).returns(false); mockOpportunity = { findById: sandbox.stub(), @@ -466,6 +467,7 @@ describe('Suggestions Controller', () => { const ids = suggestionIds || []; return Promise.resolve({ grantedIds: ids, notGrantedIds: [], grantIds: ids.map((id) => `grant-${id}`) }); }), + isSuggestionGranted: sandbox.stub().resolves(true), }; mockSuggestionDataAccess = { @@ -532,6 +534,25 @@ describe('Suggestions Controller', () => { expect(suggestions[0]).to.have.property('opportunityId', OPPORTUNITY_ID); }); + it('returns all suggestions without grant filtering when summit-plg is not enabled', async () => { + const nonPlgSite = { + getId: sandbox.stub().returns(SITE_ID), + getDeliveryType: sandbox.stub().returns('aem_edge'), + }; + mockSite.findById.withArgs(SITE_ID).resolves(nonPlgSite); + mockSuggestion.splitSuggestionsByGrantStatus.resetHistory(); + const response = await suggestionsController.getAllForOpportunity({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + }, + ...context, + }); + expect(response.status).to.equal(200); + expect(mockSuggestion.splitSuggestionsByGrantStatus).to.not.have.been.called; + mockSite.findById.withArgs(SITE_ID).resolves(site); + }); + it('gets all suggestions for an opportunity and a site for non belonging to the organization', async () => { sandbox.stub(AccessControlUtil.prototype, 'hasAccess').returns(false); sandbox.stub(context.attributes.authInfo, 'hasOrganization').returns(false); @@ -1596,26 +1617,9 @@ describe('Suggestions Controller', () => { expect(error).to.have.property('message', 'not found'); }); - it('getByID returns not found for freemium org with ungranted suggestion', async () => { - const ORG_ID = 'org-uuid'; - const freemiumSite = { - getId: sandbox.stub().returns(SITE_ID), - getOrganizationId: sandbox.stub().returns(ORG_ID), - getDeliveryType: sandbox.stub().returns('aem_edge'), - }; - const mockEntitlement = { - isFreemium: sandbox.stub().returns(true), - }; - mockSite.findById.withArgs(SITE_ID).resolves(freemiumSite); - mockSuggestionDataAccess.Entitlement = mockEntitlement; - mockSuggestionDataAccess.Suggestion.isSuggestionGranted = sandbox.stub() - .resolves(false); - const ctrl = SuggestionsController({ - dataAccess: mockSuggestionDataAccess, - pathInfo: { headers: { 'x-product': 'llmo' } }, - ...authContext, - }, mockSqs, { AUTOFIX_JOBS_QUEUE: 'https://autofix-jobs-queue' }); - const response = await ctrl.getByID({ + it('getByID returns not found for summit-plg enabled site with ungranted suggestion', async () => { + mockSuggestion.isSuggestionGranted.resolves(false); + const response = await suggestionsController.getByID({ params: { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID, @@ -1624,8 +1628,6 @@ describe('Suggestions Controller', () => { ...context, }); expect(response.status).to.equal(404); - delete mockSuggestionDataAccess.Entitlement; - delete mockSuggestionDataAccess.Suggestion.isSuggestionGranted; }); describe('getSuggestionFixes', () => { @@ -3284,6 +3286,7 @@ describe('Suggestions Controller', () => { const suggestionControllerWithMock = await esmock('../../src/controllers/suggestions.js', { '../../src/support/utils.js': { getIMSPromiseToken: async () => mockPromiseToken, + getIsSummitPlgEnabled: async () => true, }, }); suggestionsControllerWithMock = suggestionControllerWithMock({ @@ -3479,6 +3482,7 @@ describe('Suggestions Controller', () => { const ControllerWithSpy = await esmock('../../src/controllers/suggestions.js', { '../../src/support/utils.js': { getIMSPromiseToken: getIMSPromiseTokenStub, + getIsSummitPlgEnabled: async () => true, }, }); const controller = ControllerWithSpy({ @@ -4319,6 +4323,7 @@ describe('Suggestions Controller', () => { const SuggestionsControllerWithStub = await esmock('../../src/controllers/suggestions.js', { '../../src/support/utils.js': { getIMSPromiseToken: getIMSPromiseTokenStub, + getIsSummitPlgEnabled: async () => true, }, }); const controllerWithStub = SuggestionsControllerWithStub({ @@ -4588,6 +4593,7 @@ describe('Suggestions Controller', () => { const suggestionControllerWithMock = await esmock('../../src/controllers/suggestions.js', { '../../src/support/utils.js': { getIMSPromiseToken: async () => mockPromiseToken, + getIsSummitPlgEnabled: async () => true, }, }); suggestionsControllerWithMock = suggestionControllerWithMock({ @@ -7955,6 +7961,7 @@ describe('Suggestions Controller', () => { const suggestionControllerWithMock = await esmock('../../src/controllers/suggestions.js', { '../../src/support/utils.js': { getIMSPromiseToken: async () => mockPromiseToken, + getIsSummitPlgEnabled: async () => true, }, }); From 80848f85a412e0c75cb5d757ad935c1f100d381f Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Mon, 16 Mar 2026 16:51:05 +0530 Subject: [PATCH 11/16] fix: adds schema for SuggestionGrants --- src/controllers/suggestions.js | 17 ++++-- src/support/grant-suggestions-handler.js | 9 +-- test/controllers/opportunities.test.js | 8 ++- test/controllers/suggestions.test.js | 27 +++++++-- .../support/grant-suggestions-handler.test.js | 60 ++++++++++++------- 5 files changed, 86 insertions(+), 35 deletions(-) diff --git a/src/controllers/suggestions.js b/src/controllers/suggestions.js index f34b9fb1b..2afed8424 100644 --- a/src/controllers/suggestions.js +++ b/src/controllers/suggestions.js @@ -173,7 +173,7 @@ function SuggestionsController(ctx, sqs, env) { }; const { - Opportunity, Suggestion, Site, Configuration, + Opportunity, Suggestion, SuggestionGrant, Site, Configuration, } = dataAccess; if (!isObject(Opportunity)) { @@ -195,9 +195,14 @@ function SuggestionsController(ctx, sqs, env) { */ const filterByGrantStatus = async (site, suggestions) => { if (!await getIsSummitPlgEnabled(site, ctx)) return suggestions; - const ids = suggestions.map((s) => s.getId()); - const { grantedIds } = await Suggestion.splitSuggestionsByGrantStatus(ids); - return suggestions.filter((s) => grantedIds.includes(s.getId())); + try { + const ids = suggestions.map((s) => s.getId()); + const { grantedIds } = await SuggestionGrant.splitSuggestionsByGrantStatus(ids); + return suggestions.filter((s) => grantedIds.includes(s.getId())); + /* c8 ignore next */ } catch (err) { + ctx.log?.error?.('Failed to filter suggestions by grant status', err?.message ?? err); + return []; + } }; /** @@ -485,7 +490,7 @@ function SuggestionsController(ctx, sqs, env) { return notFound(); } if (await getIsSummitPlgEnabled(site, ctx) - && !(await Suggestion.isSuggestionGranted(suggestion.getId()))) { + && !(await SuggestionGrant.isSuggestionGranted(suggestion.getId()))) { return notFound('Suggestion not found'); } return ok(SuggestionDto.toJSON(suggestion, view, opportunity)); @@ -1012,7 +1017,7 @@ function SuggestionsController(ctx, sqs, env) { const requestedSuggestions = suggestions.filter((s) => suggestionIds.includes(s.getId())); if (await getIsSummitPlgEnabled(site, ctx) && requestedSuggestions.length > 0) { const requestedIds = requestedSuggestions.map((s) => s.getId()); - const { notGrantedIds } = await Suggestion.splitSuggestionsByGrantStatus(requestedIds); + const { notGrantedIds } = await SuggestionGrant.splitSuggestionsByGrantStatus(requestedIds); if (notGrantedIds.length > 0) { return badRequest('All suggestion IDs must be granted before autofix can be executed'); } diff --git a/src/support/grant-suggestions-handler.js b/src/support/grant-suggestions-handler.js index 748fd9606..734e48123 100644 --- a/src/support/grant-suggestions-handler.js +++ b/src/support/grant-suggestions-handler.js @@ -79,6 +79,7 @@ export function getTopSuggestions(suggestions, opportunityName) { */ export async function grantSuggestionsForOpportunity(dataAccess, site, opportunity) { const Suggestion = dataAccess?.Suggestion; + const SuggestionGrant = dataAccess?.SuggestionGrant; const Token = dataAccess?.Token; const siteId = site?.getId(); const opptyId = opportunity?.getId(); @@ -87,7 +88,7 @@ export async function grantSuggestionsForOpportunity(dataAccess, site, opportuni ? getTokenGrantConfigByOpportunity(oppType) : null; const tokenType = config?.tokenType; - if (!Suggestion || !Token || !siteId || !opptyId || !config) return; + if (!Suggestion || !SuggestionGrant || !Token || !siteId || !opptyId || !config) return; const { STATUSES } = SuggestionModel; const newSuggestions = await Suggestion @@ -97,7 +98,7 @@ export async function grantSuggestionsForOpportunity(dataAccess, site, opportuni let token = await Token.findBySiteIdAndTokenType(siteId, tokenType); if (!token) { - const { grantIds } = await Suggestion + const { grantIds } = await SuggestionGrant .splitSuggestionsByGrantStatus(newSuggestionIds); const suppliedTotal = Math.max(1, config.tokensPerCycle - (grantIds?.length ?? 0)); token = await Token.findBySiteIdAndTokenType(siteId, tokenType, { @@ -109,7 +110,7 @@ export async function grantSuggestionsForOpportunity(dataAccess, site, opportuni const remaining = token.getRemaining(); if (remaining <= 0) return; - const { notGrantedIds } = await Suggestion + const { notGrantedIds } = await SuggestionGrant .splitSuggestionsByGrantStatus(newSuggestionIds); const notGrantedEntities = newSuggestions .filter((s) => notGrantedIds.includes(s.getId())); @@ -119,7 +120,7 @@ export async function grantSuggestionsForOpportunity(dataAccess, site, opportuni topGroups.map((group) => { const ids = group.map((s) => s.getId()).filter(Boolean); return ids.length > 0 - ? Suggestion.grantSuggestions(ids, siteId, tokenType) + ? SuggestionGrant.grantSuggestions(ids, siteId, tokenType) : Promise.resolve(); }), ); diff --git a/test/controllers/opportunities.test.js b/test/controllers/opportunities.test.js index ac0fb6d82..0af76526c 100644 --- a/test/controllers/opportunities.test.js +++ b/test/controllers/opportunities.test.js @@ -374,7 +374,10 @@ describe('Opportunities Controller', () => { const ctxWithToken = { ...mockContext, dataAccess: { - ...mockOpportunityDataAccess, Token: mockToken, Configuration: mockConfig, + ...mockOpportunityDataAccess, + SuggestionGrant: {}, + Token: mockToken, + Configuration: mockConfig, }, }; const controllerWithToken = OpportunitiesController(ctxWithToken); @@ -401,6 +404,7 @@ describe('Opportunities Controller', () => { allByOpportunityIdAndStatus: sandbox.stub() .rejects(new Error('db failure')), }; + const mockSuggestionGrant = {}; const mockToken = { findBySiteIdAndTokenType: sandbox.stub(), }; @@ -414,6 +418,7 @@ describe('Opportunities Controller', () => { dataAccess: { ...mockOpportunityDataAccess, Suggestion: mockSuggestion, + SuggestionGrant: mockSuggestionGrant, Token: mockToken, Configuration: mockConfig, }, @@ -458,6 +463,7 @@ describe('Opportunities Controller', () => { ...mockOpportunityDataAccess, Site: { findById: sandbox.stub().resolves(mockSiteEntity) }, Suggestion: mockSuggestion, + SuggestionGrant: {}, Token: mockToken, Configuration: mockConfig, }, diff --git a/test/controllers/suggestions.test.js b/test/controllers/suggestions.test.js index b551536bf..e89f0dee3 100644 --- a/test/controllers/suggestions.test.js +++ b/test/controllers/suggestions.test.js @@ -175,6 +175,7 @@ describe('Suggestions Controller', () => { let mockSuggestionDataAccess; let mockSuggestion; + let mockSuggestionGrant; let mockOpportunity; let mockSite; let mockConfiguration; @@ -463,6 +464,9 @@ describe('Suggestions Controller', () => { return Promise.resolve(mockSuggestionEntity(suggData)); }), getFixEntitiesBySuggestionId: sandbox.stub(), + }; + + mockSuggestionGrant = { splitSuggestionsByGrantStatus: sandbox.stub().callsFake((suggestionIds) => { const ids = suggestionIds || []; return Promise.resolve({ grantedIds: ids, notGrantedIds: [], grantIds: ids.map((id) => `grant-${id}`) }); @@ -473,6 +477,7 @@ describe('Suggestions Controller', () => { mockSuggestionDataAccess = { Opportunity: mockOpportunity, Suggestion: mockSuggestion, + SuggestionGrant: mockSuggestionGrant, Site: mockSite, Configuration: mockConfiguration, }; @@ -534,13 +539,27 @@ describe('Suggestions Controller', () => { expect(suggestions[0]).to.have.property('opportunityId', OPPORTUNITY_ID); }); + it('returns empty array when grant filtering throws an error', async () => { + mockSuggestionGrant.splitSuggestionsByGrantStatus.rejects(new Error('db failure')); + const response = await suggestionsController.getAllForOpportunity({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + }, + ...context, + }); + expect(response.status).to.equal(200); + const suggestions = await response.json(); + expect(suggestions).to.be.an('array').with.lengthOf(0); + }); + it('returns all suggestions without grant filtering when summit-plg is not enabled', async () => { const nonPlgSite = { getId: sandbox.stub().returns(SITE_ID), getDeliveryType: sandbox.stub().returns('aem_edge'), }; mockSite.findById.withArgs(SITE_ID).resolves(nonPlgSite); - mockSuggestion.splitSuggestionsByGrantStatus.resetHistory(); + mockSuggestionGrant.splitSuggestionsByGrantStatus.resetHistory(); const response = await suggestionsController.getAllForOpportunity({ params: { siteId: SITE_ID, @@ -549,7 +568,7 @@ describe('Suggestions Controller', () => { ...context, }); expect(response.status).to.equal(200); - expect(mockSuggestion.splitSuggestionsByGrantStatus).to.not.have.been.called; + expect(mockSuggestionGrant.splitSuggestionsByGrantStatus).to.not.have.been.called; mockSite.findById.withArgs(SITE_ID).resolves(site); }); @@ -1618,7 +1637,7 @@ describe('Suggestions Controller', () => { }); it('getByID returns not found for summit-plg enabled site with ungranted suggestion', async () => { - mockSuggestion.isSuggestionGranted.resolves(false); + mockSuggestionGrant.isSuggestionGranted.resolves(false); const response = await suggestionsController.getByID({ params: { siteId: SITE_ID, @@ -3304,7 +3323,7 @@ describe('Suggestions Controller', () => { opportunity.getType = sandbox.stub().returns('meta-tags'); const requestedEntities = [mockSuggestionEntity(suggs[0]), mockSuggestionEntity(suggs[2])]; mockSuggestion.allByOpportunityId.resolves(requestedEntities); - mockSuggestion.splitSuggestionsByGrantStatus.resolves({ + mockSuggestionGrant.splitSuggestionsByGrantStatus.resolves({ grantedIds: [requestedEntities[0].getId()], notGrantedIds: [requestedEntities[1].getId()], grantIds: ['grant-1'], diff --git a/test/support/grant-suggestions-handler.test.js b/test/support/grant-suggestions-handler.test.js index 3b0a0f24f..c58c3a41b 100644 --- a/test/support/grant-suggestions-handler.test.js +++ b/test/support/grant-suggestions-handler.test.js @@ -106,16 +106,24 @@ describe('grant-suggestions-handler', () => { expect(true).to.be.true; }); - it('returns early when Token or Suggestion is missing from dataAccess', async () => { + it('returns early when Token, Suggestion, or SuggestionGrant is missing from dataAccess', async () => { const Suggestion = { allByOpportunityIdAndStatus: sandbox.stub() }; - await grantSuggestionsForOpportunity({ Suggestion, Token: null }, site, opportunity); - await grantSuggestionsForOpportunity({ Suggestion: null, Token: {} }, site, opportunity); + const SuggestionGrant = {}; + await grantSuggestionsForOpportunity({ + Suggestion, SuggestionGrant, Token: null, + }, site, opportunity); + await grantSuggestionsForOpportunity({ + Suggestion: null, SuggestionGrant, Token: {}, + }, site, opportunity); + await grantSuggestionsForOpportunity({ + Suggestion, SuggestionGrant: null, Token: {}, + }, site, opportunity); expect(Suggestion.allByOpportunityIdAndStatus).to.not.have.been.called; }); it('returns early when opportunity type has no token type mapping', async () => { const Token = { findBySiteIdAndTokenType: sandbox.stub() }; - const dataAccess = { Suggestion: {}, Token }; + const dataAccess = { Suggestion: {}, SuggestionGrant: {}, Token }; const oppNoMapping = { getId: () => opptyId, getType: () => 'unknown-type' }; await grantSuggestionsForOpportunity(dataAccess, site, oppNoMapping); expect(Token.findBySiteIdAndTokenType).to.not.have.been.called; @@ -126,7 +134,7 @@ describe('grant-suggestions-handler', () => { allByOpportunityIdAndStatus: sandbox.stub().resolves([]), }; const Token = { findBySiteIdAndTokenType: sandbox.stub() }; - const dataAccess = { Suggestion, Token }; + const dataAccess = { Suggestion, SuggestionGrant: {}, Token }; await grantSuggestionsForOpportunity(dataAccess, site, opportunity); expect(Token.findBySiteIdAndTokenType).to.not.have.been.called; }); @@ -136,17 +144,19 @@ describe('grant-suggestions-handler', () => { const existingToken = { getRemaining: () => 0 }; const Suggestion = { allByOpportunityIdAndStatus: sandbox.stub().resolves([mockSugg]), + }; + const SuggestionGrant = { splitSuggestionsByGrantStatus: sandbox.stub(), grantSuggestions: sandbox.stub(), }; const Token = { findBySiteIdAndTokenType: sandbox.stub().resolves(existingToken), }; - const dataAccess = { Suggestion, Token }; + const dataAccess = { Suggestion, SuggestionGrant, Token }; await grantSuggestionsForOpportunity(dataAccess, site, opportunity); - expect(Suggestion.splitSuggestionsByGrantStatus) + expect(SuggestionGrant.splitSuggestionsByGrantStatus) .to.not.have.been.called; - expect(Suggestion.grantSuggestions).to.not.have.been.called; + expect(SuggestionGrant.grantSuggestions).to.not.have.been.called; }); it('creates token when none exists and grants top suggestions', async () => { @@ -155,11 +165,13 @@ describe('grant-suggestions-handler', () => { const createdToken = { getRemaining: () => 2 }; const Suggestion = { allByOpportunityIdAndStatus: sandbox.stub().resolves([s1, s2]), + }; + const SuggestionGrant = { splitSuggestionsByGrantStatus: sandbox.stub(), grantSuggestions: sandbox.stub().resolves({ success: true }), }; // First call: no token, second call: create - Suggestion.splitSuggestionsByGrantStatus + SuggestionGrant.splitSuggestionsByGrantStatus .onFirstCall().resolves({ grantIds: [] }) .onSecondCall().resolves({ notGrantedIds: ['sugg-1', 'sugg-2'], @@ -170,14 +182,14 @@ describe('grant-suggestions-handler', () => { Token.findBySiteIdAndTokenType .onFirstCall().resolves(null) .onSecondCall().resolves(createdToken); - const dataAccess = { Suggestion, Token }; + const dataAccess = { Suggestion, SuggestionGrant, Token }; await grantSuggestionsForOpportunity(dataAccess, site, opportunity); expect(Token.findBySiteIdAndTokenType).to.have.been.calledTwice; expect(Token.findBySiteIdAndTokenType.secondCall.args[2]) .to.deep.include({ createIfNotFound: true }); - expect(Suggestion.grantSuggestions).to.have.been.calledTwice; + expect(SuggestionGrant.grantSuggestions).to.have.been.calledTwice; }); it('grants only up to remaining token count', async () => { @@ -186,6 +198,8 @@ describe('grant-suggestions-handler', () => { const existingToken = { getRemaining: () => 1 }; const Suggestion = { allByOpportunityIdAndStatus: sandbox.stub().resolves([s1, s2]), + }; + const SuggestionGrant = { splitSuggestionsByGrantStatus: sandbox.stub().resolves({ notGrantedIds: ['sugg-1', 'sugg-2'], }), @@ -195,13 +209,13 @@ describe('grant-suggestions-handler', () => { findBySiteIdAndTokenType: sandbox.stub() .resolves(existingToken), }; - const dataAccess = { Suggestion, Token }; + const dataAccess = { Suggestion, SuggestionGrant, Token }; await grantSuggestionsForOpportunity(dataAccess, site, opportunity); // Only 1 remaining, so only 1 grant call - expect(Suggestion.grantSuggestions).to.have.been.calledOnce; - expect(Suggestion.grantSuggestions.firstCall.args[0]) + expect(SuggestionGrant.grantSuggestions).to.have.been.calledOnce; + expect(SuggestionGrant.grantSuggestions.firstCall.args[0]) .to.deep.equal(['sugg-1']); }); @@ -210,10 +224,12 @@ describe('grant-suggestions-handler', () => { const createdToken = { getRemaining: () => 1 }; const Suggestion = { allByOpportunityIdAndStatus: sandbox.stub().resolves([s1]), + }; + const SuggestionGrant = { splitSuggestionsByGrantStatus: sandbox.stub(), grantSuggestions: sandbox.stub().resolves({ success: true }), }; - Suggestion.splitSuggestionsByGrantStatus + SuggestionGrant.splitSuggestionsByGrantStatus .onFirstCall().resolves({ grantIds: ['g1', 'g2'] }) .onSecondCall().resolves({ notGrantedIds: ['sugg-1'] }); const Token = { @@ -222,7 +238,7 @@ describe('grant-suggestions-handler', () => { Token.findBySiteIdAndTokenType .onFirstCall().resolves(null) .onSecondCall().resolves(createdToken); - const dataAccess = { Suggestion, Token }; + const dataAccess = { Suggestion, SuggestionGrant, Token }; await grantSuggestionsForOpportunity(dataAccess, site, opportunity); @@ -236,10 +252,12 @@ describe('grant-suggestions-handler', () => { const createdToken = { getRemaining: () => 1 }; const Suggestion = { allByOpportunityIdAndStatus: sandbox.stub().resolves([s1]), + }; + const SuggestionGrant = { splitSuggestionsByGrantStatus: sandbox.stub(), grantSuggestions: sandbox.stub().resolves({ success: true }), }; - Suggestion.splitSuggestionsByGrantStatus + SuggestionGrant.splitSuggestionsByGrantStatus .onFirstCall().resolves({}) .onSecondCall().resolves({ notGrantedIds: ['sugg-1'] }); const Token = { @@ -248,7 +266,7 @@ describe('grant-suggestions-handler', () => { Token.findBySiteIdAndTokenType .onFirstCall().resolves(null) .onSecondCall().resolves(createdToken); - const dataAccess = { Suggestion, Token }; + const dataAccess = { Suggestion, SuggestionGrant, Token }; await grantSuggestionsForOpportunity(dataAccess, site, opportunity); @@ -261,6 +279,8 @@ describe('grant-suggestions-handler', () => { const existingToken = { getRemaining: () => 1 }; const Suggestion = { allByOpportunityIdAndStatus: sandbox.stub().resolves([s1]), + }; + const SuggestionGrant = { splitSuggestionsByGrantStatus: sandbox.stub().resolves({ notGrantedIds: [''], }), @@ -270,11 +290,11 @@ describe('grant-suggestions-handler', () => { findBySiteIdAndTokenType: sandbox.stub() .resolves(existingToken), }; - const dataAccess = { Suggestion, Token }; + const dataAccess = { Suggestion, SuggestionGrant, Token }; await grantSuggestionsForOpportunity(dataAccess, site, opportunity); - expect(Suggestion.grantSuggestions).to.not.have.been.called; + expect(SuggestionGrant.grantSuggestions).to.not.have.been.called; }); }); }); From 477fba61ab7e24cd92a4e6aa8e6a667f6f854780 Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Mon, 16 Mar 2026 21:25:38 +0530 Subject: [PATCH 12/16] fix: update grouping and sorting functions --- src/support/grant-suggestions-handler.js | 88 +++++++++-- .../support/grant-suggestions-handler.test.js | 138 ++++++++++++++++-- 2 files changed, 199 insertions(+), 27 deletions(-) diff --git a/src/support/grant-suggestions-handler.js b/src/support/grant-suggestions-handler.js index 734e48123..66bc81803 100644 --- a/src/support/grant-suggestions-handler.js +++ b/src/support/grant-suggestions-handler.js @@ -14,14 +14,35 @@ import { Suggestion as SuggestionModel } from '@adobe/spacecat-shared-data-acces import { getTokenGrantConfigByOpportunity } from '@adobe/spacecat-shared-utils'; /** - * Default sort: by rank ascending, then id ascending. + * Extracts rank from a suggestion entity or plain object. + */ +function getSuggestionRank(s) { + return typeof s?.getRank === 'function' ? s.getRank() : (s?.rank ?? 0); +} + +/** + * Creates a suggestion group object. + * @param {Array} items - Suggestions in this group. + * @param {Function} [rankFn] - Custom rank function for the group. + * Defaults to the rank of the first item. + * @returns {{ items: Array, getRank: Function }} + */ +function createGroup(items, rankFn) { + return { + items, + getRank: rankFn ?? (() => getSuggestionRank(items[0])), + }; +} + +/** + * Default sort: by group rank ascending, then first item id ascending. */ function defaultSortFn(groupA, groupB) { - const a = groupA[0]; - const b = groupB[0]; - const rankA = typeof a?.getRank === 'function' ? a.getRank() : (a?.rank ?? 0); - const rankB = typeof b?.getRank === 'function' ? b.getRank() : (b?.rank ?? 0); + const rankA = groupA.getRank(); + const rankB = groupB.getRank(); if (rankA !== rankB) return rankA - rankB; + const a = groupA.items[0]; + const b = groupB.items[0]; const idA = typeof a?.getId === 'function' ? a.getId() : (a?.id ?? ''); const idB = typeof b?.getId === 'function' ? b.getId() : (b?.id ?? ''); return idA.localeCompare(idB); @@ -29,16 +50,51 @@ function defaultSortFn(groupA, groupB) { /** * Per-opportunity grouping and sorting strategies. - * Each entry can define `groupFn` and/or `sortFn`. - * Opportunities not listed here use the defaults - * (one group per suggestion, sorted by rank asc then id asc). + * + * Each entry is keyed by opportunity type and may define: + * + * groupFn(suggestions) => Array + * Groups suggestions into logical units. Each group is created via + * createGroup(items, rankFn?) and consumes one token when granted. + * Use this when multiple suggestions should be treated as a single + * grantable unit (e.g. all backlinks pointing to the same broken URL). + * The optional rankFn overrides how the group's rank is computed; + * if omitted, the group rank defaults to the first item's rank. + * Not needed when each suggestion should be granted independently + * (the default is one group per suggestion). + * + * sortFn(groupA, groupB) => number + * Custom comparator for ordering groups. Receives group objects + * with { items, getRank() }. Use this when the default sort + * (rank ascending, then first item's id ascending) does not match + * the desired grant priority for this opportunity type. + * Not needed when the default ascending-rank order is correct. + * + * Opportunities not listed here use the defaults: one group per + * suggestion, sorted by rank ascending then id ascending. */ const OPPORTUNITY_STRATEGIES = { - // Example: group broken-backlinks suggestions by source URL - // 'broken-backlinks': { - // groupFn: (suggestions) => { ... }, - // sortFn: (groupA, groupB) => { ... }, - // }, + 'broken-backlinks': { + groupFn: (suggestions) => { + const groups = new Map(); + for (const suggestion of suggestions) { + const data = typeof suggestion?.getData === 'function' + ? suggestion.getData() + : suggestion?.data; + const urlTo = data?.url_to ?? data?.urlTo ?? ''; + if (!groups.has(urlTo)) { + groups.set(urlTo, []); + } + groups.get(urlTo).push(suggestion); + } + return [...groups.values()].map( + (items) => createGroup( + items, + () => Math.max(...items.map(getSuggestionRank)), + ), + ); + }, + }, }; /** @@ -50,7 +106,7 @@ const OPPORTUNITY_STRATEGIES = { * @param {Array} suggestions - Suggestion entities or plain objects. * @param {string} [opportunityName] - Opportunity name for * strategy lookup. - * @returns {Array} Sorted groups of suggestions. + * @returns {Array<{items: Array, getRank: Function}>} Sorted groups. */ export function getTopSuggestions(suggestions, opportunityName) { if (!Array.isArray(suggestions) || suggestions.length === 0) { @@ -61,7 +117,7 @@ export function getTopSuggestions(suggestions, opportunityName) { // c8 ignore: groupFn branch covered when strategies are added const groups = groupFn ? groupFn(suggestions) /* c8 ignore next */ - : suggestions.map((s) => [s]); + : suggestions.map((s) => createGroup([s])); return [...groups].sort(sortFn ?? defaultSortFn); } @@ -118,7 +174,7 @@ export async function grantSuggestionsForOpportunity(dataAccess, site, opportuni .slice(0, remaining); await Promise.all( topGroups.map((group) => { - const ids = group.map((s) => s.getId()).filter(Boolean); + const ids = group.items.map((s) => s.getId()).filter(Boolean); return ids.length > 0 ? SuggestionGrant.grantSuggestions(ids, siteId, tokenType) : Promise.resolve(); diff --git a/test/support/grant-suggestions-handler.test.js b/test/support/grant-suggestions-handler.test.js index c58c3a41b..6c8d56cb6 100644 --- a/test/support/grant-suggestions-handler.test.js +++ b/test/support/grant-suggestions-handler.test.js @@ -43,10 +43,18 @@ describe('grant-suggestions-handler', () => { const s2 = { getId: () => 'id-2', getRank: () => 5 }; const groups = getTopSuggestions([s1, s2]); expect(groups).to.have.lengthOf(2); - expect(groups.every((g) => Array.isArray(g) && g.length === 1)).to.be.true; - const flat = groups.flat(); - expect(flat).to.include(s1); - expect(flat).to.include(s2); + expect(groups.every((g) => Array.isArray(g.items) && g.items.length === 1)).to.be.true; + const allItems = groups.flatMap((g) => g.items); + expect(allItems).to.include(s1); + expect(allItems).to.include(s2); + }); + + it('exposes getRank on default groups delegating to first item', () => { + const s1 = { getId: () => 'id-1', getRank: () => 10 }; + const s2 = { getId: () => 'id-2', getRank: () => 5 }; + const groups = getTopSuggestions([s1, s2]); + expect(groups[0].getRank()).to.equal(5); + expect(groups[1].getRank()).to.equal(10); }); it('sorts groups by rank ascending then by id (default sort)', () => { @@ -55,9 +63,9 @@ describe('grant-suggestions-handler', () => { const s3 = { getId: () => 'id-c', getRank: () => 10 }; const groups = getTopSuggestions([s1, s2, s3]); expect(groups).to.have.lengthOf(3); - expect(groups[0][0]).to.equal(s2); // rank 5 first - expect(groups[1][0]).to.equal(s1); // rank 10, id-b before id-c - expect(groups[2][0]).to.equal(s3); // rank 10, id-c + expect(groups[0].items[0]).to.equal(s2); // rank 5 first + expect(groups[1].items[0]).to.equal(s1); // rank 10, id-b before id-c + expect(groups[2].items[0]).to.equal(s3); // rank 10, id-c }); it('handles plain objects with id and rank', () => { @@ -65,8 +73,8 @@ describe('grant-suggestions-handler', () => { const s2 = { id: 'y', rank: 0 }; const groups = getTopSuggestions([s1, s2]); expect(groups).to.have.lengthOf(2); - expect(groups[0][0]).to.equal(s2); - expect(groups[1][0]).to.equal(s1); + expect(groups[0].items[0]).to.equal(s2); + expect(groups[1].items[0]).to.equal(s1); }); it('uses default strategy for unknown opportunity name', () => { @@ -74,8 +82,116 @@ describe('grant-suggestions-handler', () => { const s2 = { getId: () => 'id-2', getRank: () => 5 }; const groups = getTopSuggestions([s1, s2], 'unknown-type'); expect(groups).to.have.lengthOf(2); - expect(groups[0][0]).to.equal(s2); - expect(groups[1][0]).to.equal(s1); + expect(groups[0].items[0]).to.equal(s2); + expect(groups[1].items[0]).to.equal(s1); + }); + + it('groups broken-backlinks suggestions by data.url_to using getData()', () => { + const s1 = { getId: () => 'id-1', getRank: () => 10, getData: () => ({ url_to: 'https://example.com/a' }) }; + const s2 = { getId: () => 'id-2', getRank: () => 5, getData: () => ({ url_to: 'https://example.com/a' }) }; + const s3 = { getId: () => 'id-3', getRank: () => 8, getData: () => ({ url_to: 'https://example.com/b' }) }; + const groups = getTopSuggestions([s1, s2, s3], 'broken-backlinks'); + expect(groups).to.have.lengthOf(2); + const groupA = groups.find((g) => g.items[0].getData().url_to === 'https://example.com/a'); + const groupB = groups.find((g) => g.items[0].getData().url_to === 'https://example.com/b'); + expect(groupA.items).to.have.lengthOf(2); + expect(groupB.items).to.have.lengthOf(1); + }); + + it('broken-backlinks group getRank returns highest rank among items', () => { + const s1 = { getId: () => 'id-1', getRank: () => 10, getData: () => ({ url_to: 'https://example.com/a' }) }; + const s2 = { getId: () => 'id-2', getRank: () => 5, getData: () => ({ url_to: 'https://example.com/a' }) }; + const s3 = { getId: () => 'id-3', getRank: () => 8, getData: () => ({ url_to: 'https://example.com/b' }) }; + const groups = getTopSuggestions([s1, s2, s3], 'broken-backlinks'); + const groupA = groups.find((g) => g.items.includes(s1)); + const groupB = groups.find((g) => g.items.includes(s3)); + expect(groupA.getRank()).to.equal(10); + expect(groupB.getRank()).to.equal(8); + }); + + it('groups broken-backlinks suggestions by data.urlTo (camelCase fallback)', () => { + const s1 = { getId: () => 'id-1', getRank: () => 1, getData: () => ({ urlTo: 'https://example.com/x' }) }; + const s2 = { getId: () => 'id-2', getRank: () => 2, getData: () => ({ urlTo: 'https://example.com/x' }) }; + const groups = getTopSuggestions([s1, s2], 'broken-backlinks'); + expect(groups).to.have.lengthOf(1); + expect(groups[0].items).to.have.lengthOf(2); + }); + + it('groups broken-backlinks suggestions using plain object data', () => { + const s1 = { id: 'id-1', rank: 1, data: { url_to: 'https://example.com/a' } }; + const s2 = { id: 'id-2', rank: 2, data: { url_to: 'https://example.com/b' } }; + const groups = getTopSuggestions([s1, s2], 'broken-backlinks'); + expect(groups).to.have.lengthOf(2); + expect(groups[0].items).to.have.lengthOf(1); + expect(groups[1].items).to.have.lengthOf(1); + }); + + it('groups broken-backlinks suggestions with missing data under empty string key', () => { + const s1 = { getId: () => 'id-1', getRank: () => 1, getData: () => ({}) }; + const s2 = { getId: () => 'id-2', getRank: () => 2, getData: () => null }; + const groups = getTopSuggestions([s1, s2], 'broken-backlinks'); + expect(groups).to.have.lengthOf(1); + expect(groups[0].items).to.have.lengthOf(2); + }); + + it('groups and ranks 10 broken-backlinks suggestions correctly', () => { + const mk = (id, rank, urlTo) => ({ + getId: () => id, getRank: () => rank, getData: () => ({ url_to: urlTo }), + }); + // 4 distinct url_to values across 10 suggestions + const suggestions = [ + mk('s01', 100, 'https://example.com/page-a'), // group A + mk('s02', 500, 'https://example.com/page-b'), // group B + mk('s03', 200, 'https://example.com/page-a'), // group A + mk('s04', 50, 'https://example.com/page-c'), // group C + mk('s05', 800, 'https://example.com/page-b'), // group B (highest in B) + mk('s06', 300, 'https://example.com/page-d'), // group D + mk('s07', 150, 'https://example.com/page-c'), // group C + mk('s08', 900, 'https://example.com/page-a'), // group A (highest in A) + mk('s09', 700, 'https://example.com/page-d'), // group D (highest in D) + mk('s10', 400, 'https://example.com/page-c'), // group C (highest in C) + ]; + + const groups = getTopSuggestions(suggestions, 'broken-backlinks'); + + // 4 groups: A(s01,s03,s08), B(s02,s05), C(s04,s07,s10), D(s06,s09) + expect(groups).to.have.lengthOf(4); + + // group ranks: A=900, B=800, D=700, C=400 + // sorted ascending: C(400), D(700), B(800), A(900) + expect(groups[0].getRank()).to.equal(400); + expect(groups[1].getRank()).to.equal(700); + expect(groups[2].getRank()).to.equal(800); + expect(groups[3].getRank()).to.equal(900); + + // verify group C (rank 400) — page-c items + const groupC = groups[0]; + expect(groupC.items).to.have.lengthOf(3); + const groupCIds = groupC.items.map((s) => s.getId()); + expect(groupCIds).to.include.members(['s04', 's07', 's10']); + + // verify group D (rank 700) — page-d items + const groupD = groups[1]; + expect(groupD.items).to.have.lengthOf(2); + const groupDIds = groupD.items.map((s) => s.getId()); + expect(groupDIds).to.include.members(['s06', 's09']); + + // verify group B (rank 800) — page-b items + const groupB = groups[2]; + expect(groupB.items).to.have.lengthOf(2); + const groupBIds = groupB.items.map((s) => s.getId()); + expect(groupBIds).to.include.members(['s02', 's05']); + + // verify group A (rank 900) — page-a items + const groupA = groups[3]; + expect(groupA.items).to.have.lengthOf(3); + const groupAIds = groupA.items.map((s) => s.getId()); + expect(groupAIds).to.include.members(['s01', 's03', 's08']); + + // slicing top 2 groups gives the two lowest-ranked groups + const top2 = groups.slice(0, 2); + expect(top2[0].getRank()).to.equal(400); // group C + expect(top2[1].getRank()).to.equal(700); // group D }); it('falls back to defaults for objects missing rank and id', () => { From c8e945beac0bbcd8e048220a6ae008ddbf50374e Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Tue, 17 Mar 2026 17:33:55 +0530 Subject: [PATCH 13/16] fix: address review comments --- package-lock.json | 1405 ++++++++++++++-------- package.json | 4 +- src/controllers/opportunities.js | 3 +- src/controllers/suggestions.js | 7 +- src/support/grant-suggestions-handler.js | 3 +- test/controllers/suggestions.test.js | 9 +- 6 files changed, 914 insertions(+), 517 deletions(-) diff --git a/package-lock.json b/package-lock.json index 751e1cfbe..ba50aa6f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@adobe/spacecat-shared-ahrefs-client": "1.10.7", "@adobe/spacecat-shared-athena-client": "1.9.6", "@adobe/spacecat-shared-brand-client": "1.1.38", - "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/669c7f824b915dc67dfb9fe2fc7e29917e38ace7/adobe-spacecat-shared-data-access-3.19.0.tgz", + "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/f5aa0f15c554f5c31bdab9112e95011b6a723ec8/adobe-spacecat-shared-data-access-3.22.0.tgz", "@adobe/spacecat-shared-data-access-v2": "npm:@adobe/spacecat-shared-data-access@2.109.0", "@adobe/spacecat-shared-drs-client": "1.3.0", "@adobe/spacecat-shared-gpt-client": "1.6.19", @@ -31,7 +31,7 @@ "@adobe/spacecat-shared-slack-client": "1.6.2", "@adobe/spacecat-shared-tier-client": "1.3.15", "@adobe/spacecat-shared-tokowaka-client": "1.11.1", - "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/4dc817e97910e998b17ad1f535a7e289dca89d95/adobe-spacecat-shared-utils-1.102.0.tgz", + "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/5f65e64f31849dcae0699a7744f07a23f77bcc21/adobe-spacecat-shared-utils-1.102.1.tgz", "@adobe/spacecat-shared-vault-secrets": "1.2.0", "@aws-sdk/client-s3": "3.1004.0", "@aws-sdk/client-secrets-manager": "3.1004.0", @@ -792,6 +792,7 @@ "resolved": "https://registry.npmjs.org/@adobe/helix-universal/-/helix-universal-5.4.0.tgz", "integrity": "sha512-3ZfFdjYtpv7RCgul9yyOBsRVsxLNapwt0YjASBhyzJGNjnPxrWDlqDtbpBdwAgA1Nuh9nmjzFDFu8CJWv6BMKw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@adobe/fetch": "4.2.3", "aws4": "1.13.2" @@ -2350,15 +2351,15 @@ } }, "node_modules/@adobe/spacecat-shared-data-access": { - "version": "3.19.0", - "resolved": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/669c7f824b915dc67dfb9fe2fc7e29917e38ace7/adobe-spacecat-shared-data-access-3.19.0.tgz", - "integrity": "sha512-zG/eVCSkWrY9M2ymKLv7KHigKdQP+UNYGY+hMKv8QMmjca1FFCuil/dgiCeLBaom9wkoBkt4S6k/R4DILT5ipQ==", + "version": "3.22.0", + "resolved": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/f5aa0f15c554f5c31bdab9112e95011b6a723ec8/adobe-spacecat-shared-data-access-3.22.0.tgz", + "integrity": "sha512-hsN+dLHWpQ02r3Ntyqw7LgTvmdYYNitB7q+GWWzNOW5ledFRleRf+Z1zmLPMHpLbjGiWp75XXXHBqgqMwbY/fw==", "license": "Apache-2.0", "dependencies": { "@adobe/fetch": "^4.2.3", - "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/4dc817e97910e998b17ad1f535a7e289dca89d95/adobe-spacecat-shared-utils-1.102.0.tgz", + "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/5f65e64f31849dcae0699a7744f07a23f77bcc21/adobe-spacecat-shared-utils-1.102.1.tgz", "@aws-sdk/client-s3": "^3.940.0", - "@supabase/postgrest-js": "^1.21.4", + "@supabase/postgrest-js": "2.99.1", "@types/joi": "17.2.3", "aws-xray-sdk": "3.12.0", "joi": "18.0.2", @@ -6912,14 +6913,14 @@ } }, "node_modules/@adobe/spacecat-shared-utils": { - "version": "1.102.0", - "resolved": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/4dc817e97910e998b17ad1f535a7e289dca89d95/adobe-spacecat-shared-utils-1.102.0.tgz", - "integrity": "sha512-0EnNJR8aopCukdznZlcJPQd8/luGhzEDGUobWa51cCZgnwJoAsxB7RjDztQ3GbYACmM3bHbCILqkeOqVdp8mKw==", + "version": "1.102.1", + "resolved": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/5f65e64f31849dcae0699a7744f07a23f77bcc21/adobe-spacecat-shared-utils-1.102.1.tgz", + "integrity": "sha512-Fcv3+7+ceJsP5Z8Awe+qWHOCGoYaauc1H4P73ZmhW7CUUr0STFek26Yq+6cifjMHAxNe1fU4p8IWsEZyFUjTqA==", "license": "Apache-2.0", "dependencies": { "@adobe/fetch": "4.2.3", - "@aws-sdk/client-s3": "3.1004.0", - "@aws-sdk/client-sqs": "3.1004.0", + "@aws-sdk/client-s3": "3.1009.0", + "@aws-sdk/client-sqs": "3.1009.0", "@json2csv/plainjs": "7.0.6", "aws-xray-sdk": "3.12.0", "cheerio": "1.2.0", @@ -6936,6 +6937,383 @@ "npm": ">=10.9.0 <12.0.0" } }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/client-s3": { + "version": "3.1009.0", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/client-s3/-/client-s3-3.1009.0.tgz", + "integrity": "sha512-luy8CxallkoiGWTqU86ca/BbvkWJjs0oala7uIIRN1JtQxMb5i4Yl/PBZVcQFhbK9kQi0PK0GfD8gIpLkI91fw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", + "@aws-sdk/middleware-expect-continue": "^3.972.8", + "@aws-sdk/middleware-flexible-checksums": "^3.973.6", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-location-constraint": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-sdk-s3": "^3.972.20", + "@aws-sdk/middleware-ssec": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/signature-v4-multi-region": "^3.996.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/eventstream-serde-browser": "^4.2.12", + "@smithy/eventstream-serde-config-resolver": "^4.3.12", + "@smithy/eventstream-serde-node": "^4.2.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-blob-browser": "^4.2.13", + "@smithy/hash-node": "^4.2.12", + "@smithy/hash-stream-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/md5-js": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-stream": "^4.5.19", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/client-sqs": { + "version": "3.1009.0", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/client-sqs/-/client-sqs-3.1009.0.tgz", + "integrity": "sha512-emuPZV3PvPoTYVCk/YB4Ye2QtuT2sdz9UAqR8sAyG1RQ4KwBE42lgIuNuXW6d8XlxBp6R0o0qoGbFTkAC6XTFQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-sdk-sqs": "^3.972.15", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/md5-js": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.8.tgz", + "integrity": "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.8.tgz", + "integrity": "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.974.0", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.0.tgz", + "integrity": "sha512-BmdDjqvnuYaC4SY7ypHLXfCSsGYGUZkjCLSZyUAAYn1YT28vbNMJNDwhlfkvvE+hQHG5RJDlEmYuvBxcB9jX1g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/crc64-nvme": "^3.972.5", + "@aws-sdk/types": "^3.973.6", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.19", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.8.tgz", + "integrity": "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.20", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.20.tgz", + "integrity": "sha512-yhva/xL5H4tWQgsBjwV+RRD0ByCzg0TcByDCLp3GXdn/wlyRNfy8zsswDtCvr1WSKQkSQYlyEzPuWkJG0f5HvQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.19", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/middleware-sdk-sqs": { + "version": "3.972.15", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.15.tgz", + "integrity": "sha512-X7yt+gJzZEK247nppuUVWS1i83q8zhZdBk1H2b6/qeXNv1ILgw0bQLNbFNG4gJi3P7vZV+PhtPkax0nwXAvRtg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.8.tgz", + "integrity": "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.8.tgz", + "integrity": "sha512-n1qYFD+tbqZuyskVaxUE+t10AUz9g3qzDw3Tp6QZDKmqsjfDmZBd4GIk2EKJJNtcCBtE5YiUjDYA+3djFAFBBg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/types": { + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.5", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/spacecat-shared-utils/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, "node_modules/@adobe/spacecat-shared-utils/node_modules/cheerio": { "version": "1.2.0", "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/cheerio/-/cheerio-1.2.0.tgz", @@ -6962,9 +7340,9 @@ } }, "node_modules/@adobe/spacecat-shared-utils/node_modules/undici": { - "version": "7.24.0", - "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/undici/-/undici-7.24.0.tgz", - "integrity": "sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg==", + "version": "7.24.4", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -7676,6 +8054,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.940.0.tgz", "integrity": "sha512-u2sXsNJazJbuHeWICvsj6RvNyJh3isedEfPvB21jK/kxcriK+dE/izlKC2cyxUjERCmku0zTFNzY9FhrLbYHjQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -9380,22 +9759,22 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.19.tgz", - "integrity": "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/xml-builder": "^3.972.10", - "@smithy/core": "^3.23.9", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/signature-v4": "^5.3.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", + "version": "3.973.20", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/core/-/core-3.973.20.tgz", + "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.11", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.11", + "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -9404,12 +9783,12 @@ } }, "node_modules/@aws-sdk/core/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9417,12 +9796,12 @@ } }, "node_modules/@aws-sdk/core/node_modules/@aws-sdk/xml-builder": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz", - "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==", + "version": "3.972.11", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", + "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" }, @@ -9432,7 +9811,7 @@ }, "node_modules/@aws-sdk/core/node_modules/fast-xml-parser": { "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", "funding": [ { @@ -9450,12 +9829,12 @@ } }, "node_modules/@aws-sdk/crc64-nvme": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.4.tgz", - "integrity": "sha512-HKZIZLbRyvzo/bXZU7Zmk6XqU+1C9DjI56xd02vwuDIxedxBEqP17t9ExhbP9QFeNq/a3l9GOcyirFXxmbDhmw==", + "version": "3.972.5", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz", + "integrity": "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9463,15 +9842,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.16.tgz", - "integrity": "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA==", + "version": "3.972.18", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", + "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.18", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9479,12 +9858,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9492,20 +9871,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.18.tgz", - "integrity": "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A==", + "version": "3.972.20", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", + "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.18", - "@aws-sdk/types": "^3.973.5", - "@smithy/fetch-http-handler": "^5.3.13", - "@smithy/node-http-handler": "^4.4.14", - "@smithy/property-provider": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/smithy-client": "^4.12.2", - "@smithy/types": "^4.13.0", - "@smithy/util-stream": "^4.5.17", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.19", "tslib": "^2.6.2" }, "engines": { @@ -9513,12 +9892,12 @@ } }, "node_modules/@aws-sdk/credential-provider-http/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9526,24 +9905,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.17.tgz", - "integrity": "sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ==", + "version": "3.972.20", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", + "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.18", - "@aws-sdk/credential-provider-env": "^3.972.16", - "@aws-sdk/credential-provider-http": "^3.972.18", - "@aws-sdk/credential-provider-login": "^3.972.17", - "@aws-sdk/credential-provider-process": "^3.972.16", - "@aws-sdk/credential-provider-sso": "^3.972.17", - "@aws-sdk/credential-provider-web-identity": "^3.972.17", - "@aws-sdk/nested-clients": "^3.996.7", - "@aws-sdk/types": "^3.973.5", - "@smithy/credential-provider-imds": "^4.2.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-login": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9551,18 +9930,18 @@ } }, "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.17.tgz", - "integrity": "sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA==", + "version": "3.972.20", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", + "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.18", - "@aws-sdk/nested-clients": "^3.996.7", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9570,12 +9949,12 @@ } }, "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9717,22 +10096,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.18.tgz", - "integrity": "sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw==", + "version": "3.972.21", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", + "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.16", - "@aws-sdk/credential-provider-http": "^3.972.18", - "@aws-sdk/credential-provider-ini": "^3.972.17", - "@aws-sdk/credential-provider-process": "^3.972.16", - "@aws-sdk/credential-provider-sso": "^3.972.17", - "@aws-sdk/credential-provider-web-identity": "^3.972.17", - "@aws-sdk/types": "^3.973.5", - "@smithy/credential-provider-imds": "^4.2.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-ini": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9740,12 +10119,12 @@ } }, "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9753,16 +10132,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.16.tgz", - "integrity": "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g==", + "version": "3.972.18", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", + "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.18", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9770,12 +10149,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9783,18 +10162,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.17.tgz", - "integrity": "sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg==", + "version": "3.972.20", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", + "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.18", - "@aws-sdk/nested-clients": "^3.996.7", - "@aws-sdk/token-providers": "3.1004.0", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/token-providers": "3.1009.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9802,12 +10181,12 @@ } }, "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9815,17 +10194,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.17.tgz", - "integrity": "sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA==", + "version": "3.972.20", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", + "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.18", - "@aws-sdk/nested-clients": "^3.996.7", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -9833,12 +10212,12 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -10140,31 +10519,31 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.19.tgz", - "integrity": "sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A==", + "version": "3.972.21", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", + "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.18", - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/util-endpoints": "^3.996.4", - "@smithy/core": "^3.23.8", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", - "@smithy/util-retry": "^4.2.11", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.11", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } - }, - "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + }, + "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/types": { + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -10172,15 +10551,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz", - "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==", + "version": "3.996.5", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", - "@smithy/util-endpoints": "^3.3.2", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" }, "engines": { @@ -10188,47 +10567,47 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.7.tgz", - "integrity": "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg==", + "version": "3.996.10", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", + "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.18", - "@aws-sdk/middleware-host-header": "^3.972.7", - "@aws-sdk/middleware-logger": "^3.972.7", - "@aws-sdk/middleware-recursion-detection": "^3.972.7", - "@aws-sdk/middleware-user-agent": "^3.972.19", - "@aws-sdk/region-config-resolver": "^3.972.7", - "@aws-sdk/types": "^3.973.5", - "@aws-sdk/util-endpoints": "^3.996.4", - "@aws-sdk/util-user-agent-browser": "^3.972.7", - "@aws-sdk/util-user-agent-node": "^3.973.4", - "@smithy/config-resolver": "^4.4.10", - "@smithy/core": "^3.23.8", - "@smithy/fetch-http-handler": "^5.3.13", - "@smithy/hash-node": "^4.2.11", - "@smithy/invalid-dependency": "^4.2.11", - "@smithy/middleware-content-length": "^4.2.11", - "@smithy/middleware-endpoint": "^4.4.22", - "@smithy/middleware-retry": "^4.4.39", - "@smithy/middleware-serde": "^4.2.12", - "@smithy/middleware-stack": "^4.2.11", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/node-http-handler": "^4.4.14", - "@smithy/protocol-http": "^5.3.11", - "@smithy/smithy-client": "^4.12.2", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.38", - "@smithy/util-defaults-mode-node": "^4.2.41", - "@smithy/util-endpoints": "^3.3.2", - "@smithy/util-middleware": "^4.2.11", - "@smithy/util-retry": "^4.2.11", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -10237,14 +10616,14 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.7.tgz", - "integrity": "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ==", + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -10252,13 +10631,13 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.7.tgz", - "integrity": "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w==", + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -10266,15 +10645,15 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.7.tgz", - "integrity": "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ==", + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", + "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -10282,15 +10661,15 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.7.tgz", - "integrity": "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA==", + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/config-resolver": "^4.4.10", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -10298,12 +10677,12 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -10311,15 +10690,15 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz", - "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==", + "version": "3.996.5", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", - "@smithy/util-endpoints": "^3.3.2", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" }, "engines": { @@ -10327,13 +10706,13 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.7.tgz", - "integrity": "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw==", + "version": "3.972.8", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } @@ -10458,17 +10837,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1004.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1004.0.tgz", - "integrity": "sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA==", + "version": "3.1009.0", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", + "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.18", - "@aws-sdk/nested-clients": "^3.996.7", - "@aws-sdk/types": "^3.973.5", - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -10476,12 +10855,12 @@ } }, "node_modules/@aws-sdk/token-providers/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -10597,15 +10976,16 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.4.tgz", - "integrity": "sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q==", + "version": "3.973.7", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", + "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.19", - "@aws-sdk/types": "^3.973.5", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/types": "^4.13.0", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -10621,12 +11001,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node/node_modules/@aws-sdk/types": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", - "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "version": "3.973.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -11939,6 +12319,7 @@ "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.80.tgz", "integrity": "sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA==", "license": "MIT", + "peer": true, "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", @@ -12170,6 +12551,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -12376,6 +12758,7 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -12539,6 +12922,7 @@ "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", @@ -13691,12 +14075,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.11.tgz", - "integrity": "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", + "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -13729,16 +14113,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.10.tgz", - "integrity": "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg==", + "version": "4.4.11", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", + "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.11", - "@smithy/types": "^4.13.0", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.3.2", - "@smithy/util-middleware": "^4.2.11", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -13746,18 +14130,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.9", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.9.tgz", - "integrity": "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ==", + "version": "3.23.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.12", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.11", - "@smithy/util-stream": "^4.5.17", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -13767,15 +14151,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.11.tgz", - "integrity": "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", + "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -13783,13 +14167,13 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.11.tgz", - "integrity": "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz", + "integrity": "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" }, @@ -13798,13 +14182,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.11.tgz", - "integrity": "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz", + "integrity": "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -13812,12 +14196,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.11.tgz", - "integrity": "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA==", + "version": "4.3.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz", + "integrity": "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -13825,13 +14209,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.11.tgz", - "integrity": "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz", + "integrity": "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -13839,13 +14223,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.11.tgz", - "integrity": "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz", + "integrity": "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/eventstream-codec": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -13853,14 +14237,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.13", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.13.tgz", - "integrity": "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ==", + "version": "5.3.15", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.11", - "@smithy/querystring-builder": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, @@ -13869,14 +14253,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.12.tgz", - "integrity": "sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ==", + "version": "4.2.13", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.13.tgz", + "integrity": "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==", "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -13884,12 +14268,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.11.tgz", - "integrity": "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/hash-node/-/hash-node-4.2.12.tgz", + "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -13899,12 +14283,12 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.11.tgz", - "integrity": "sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/hash-stream-node/-/hash-stream-node-4.2.12.tgz", + "integrity": "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -13913,12 +14297,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.11.tgz", - "integrity": "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", + "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -13938,12 +14322,12 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.11.tgz", - "integrity": "sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/md5-js/-/md5-js-4.2.12.tgz", + "integrity": "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -13952,13 +14336,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.11.tgz", - "integrity": "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", + "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -13966,18 +14350,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.23", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.23.tgz", - "integrity": "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw==", + "version": "4.4.26", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", + "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.9", - "@smithy/middleware-serde": "^4.2.12", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.11", - "@smithy/util-middleware": "^4.2.11", + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -13985,18 +14369,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.40", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.40.tgz", - "integrity": "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/service-error-classification": "^4.2.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", - "@smithy/util-middleware": "^4.2.11", - "@smithy/util-retry": "^4.2.11", + "version": "4.4.43", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", + "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -14005,13 +14389,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.12.tgz", - "integrity": "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng==", + "version": "4.2.15", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14019,12 +14404,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.11.tgz", - "integrity": "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", + "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14032,14 +14417,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.11.tgz", - "integrity": "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg==", + "version": "4.3.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", + "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.11", - "@smithy/shared-ini-file-loader": "^4.4.6", - "@smithy/types": "^4.13.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14047,15 +14432,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.14", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.14.tgz", - "integrity": "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A==", + "version": "4.5.0", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/querystring-builder": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14063,12 +14448,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.11.tgz", - "integrity": "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/property-provider/-/property-provider-4.2.12.tgz", + "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14076,12 +14461,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.11.tgz", - "integrity": "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ==", + "version": "5.3.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", + "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14089,12 +14474,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.11.tgz", - "integrity": "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", + "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, @@ -14103,12 +14488,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.11.tgz", - "integrity": "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", + "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14116,24 +14501,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.11.tgz", - "integrity": "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", + "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0" + "@smithy/types": "^4.13.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.6.tgz", - "integrity": "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw==", + "version": "4.4.7", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", + "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14141,16 +14526,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.11.tgz", - "integrity": "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ==", + "version": "5.3.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", + "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.11", + "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -14160,17 +14545,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.3.tgz", - "integrity": "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw==", + "version": "4.12.6", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", + "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.9", - "@smithy/middleware-endpoint": "^4.4.23", - "@smithy/middleware-stack": "^4.2.11", - "@smithy/protocol-http": "^5.3.11", - "@smithy/types": "^4.13.0", - "@smithy/util-stream": "^4.5.17", + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" }, "engines": { @@ -14178,9 +14563,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", - "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", + "version": "4.13.1", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -14190,13 +14575,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.11.tgz", - "integrity": "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/url-parser/-/url-parser-4.2.12.tgz", + "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/querystring-parser": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14267,14 +14652,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.39", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.39.tgz", - "integrity": "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ==", + "version": "4.3.42", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", + "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14282,17 +14667,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.42", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.42.tgz", - "integrity": "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A==", + "version": "4.2.45", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", + "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.10", - "@smithy/credential-provider-imds": "^4.2.11", - "@smithy/node-config-provider": "^4.3.11", - "@smithy/property-provider": "^4.2.11", - "@smithy/smithy-client": "^4.12.3", - "@smithy/types": "^4.13.0", + "@smithy/config-resolver": "^4.4.11", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14300,13 +14685,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.2.tgz", - "integrity": "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA==", + "version": "3.3.3", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", + "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.11", - "@smithy/types": "^4.13.0", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14326,12 +14711,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.11.tgz", - "integrity": "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", + "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14339,13 +14724,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.11.tgz", - "integrity": "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw==", + "version": "4.2.12", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/util-retry/-/util-retry-4.2.12.tgz", + "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14353,14 +14738,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.17", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.17.tgz", - "integrity": "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ==", + "version": "4.5.20", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.13", - "@smithy/node-http-handler": "^4.4.14", - "@smithy/types": "^4.13.0", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", @@ -14397,13 +14782,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.11.tgz", - "integrity": "sha512-x7Rh2azQPs3XxbvCzcttRErKKvLnbZfqRf/gOjw2pb+ZscX88e5UkRPCB67bVnsFHxayvMvmePfKTqsRb+is1A==", + "version": "4.2.13", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@smithy/util-waiter/-/util-waiter-4.2.13.tgz", + "integrity": "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.11", - "@smithy/types": "^4.13.0", + "@smithy/abort-controller": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -14434,25 +14819,16 @@ "integrity": "sha512-VqAAkydywPpkw63WQhPVKCD3SdwXuihCUVZbbiY3SfSTGQyHmwRoq27y4dmJdZuJwd5JIlQoMPyGvMbUPY0RKQ==", "license": "MIT" }, - "node_modules/@supabase/node-fetch": { - "version": "2.6.15", - "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", - "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "node_modules/@supabase/postgrest-js": { + "version": "2.99.1", + "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npmjs-remote/@supabase/postgrest-js/-/postgrest-js-2.99.1.tgz", + "integrity": "sha512-gtw2ibJrADvfqrpUWXGNlrYUvxttF4WVWfPpTFKOb2IRj7B6YRWMDgcrYqIuD4ZEabK4m6YKQCCGy6clgf1lPA==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "tslib": "2.8.1" }, "engines": { - "node": "4.x || >=6.0.0" - } - }, - "node_modules/@supabase/postgrest-js": { - "version": "1.21.4", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.4.tgz", - "integrity": "sha512-TxZCIjxk6/dP9abAi89VQbWWMBbybpGWyvmIzTd79OeravM13OjR/YEYeyUOPcM1C3QyvXkvPZhUfItvmhY1IQ==", - "license": "MIT", - "dependencies": { - "@supabase/node-fetch": "^2.6.14" + "node": ">=20.0.0" } }, "node_modules/@tootallnate/once": { @@ -14521,6 +14897,7 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -14827,6 +15204,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -14873,6 +15251,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15348,6 +15727,7 @@ "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.12.0.tgz", "integrity": "sha512-lwalRdxXRy+Sn49/vN7W507qqmBRk5Fy2o0a9U6XTjL9IV+oR5PUiiptoBrOcaYCiVuGld8OEbNqhm6wvV3m6A==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "^3.4.1", "@smithy/service-error-classification": "^2.0.4", @@ -15998,6 +16378,7 @@ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -18187,6 +18568,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -22181,6 +22563,7 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -23236,6 +23619,7 @@ "integrity": "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -26408,6 +26792,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -27077,6 +27462,7 @@ "resolved": "https://registry.npmjs.org/openai/-/openai-5.12.2.tgz", "integrity": "sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==", "license": "Apache-2.0", + "peer": true, "bin": { "openai": "bin/cli" }, @@ -28169,6 +28555,7 @@ "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -28179,6 +28566,7 @@ "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -28888,6 +29276,7 @@ "integrity": "sha512-phCkJ6pjDi9ANdhuF5ElS10GGdAKY6R1Pvt9lT3SFhOwM4T7QZE7MLpBDbNruUx/Q3gFD92/UOFringGipRqZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", "@semantic-release/error": "^4.0.0", @@ -30349,6 +30738,7 @@ "integrity": "sha512-J72R4ltw0UBVUlEjTzI0gg2STOqlI9JBhQOL4Dxt7aJOnnSesy0qJDn4PYfMCafk9cWOaVg129Pesl5o+DIh0Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.4.0", "@emotion/unitless": "0.10.0", @@ -31097,6 +31487,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, "license": "MIT" }, "node_modules/traverse": { @@ -31431,6 +31822,7 @@ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", + "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -31869,6 +32261,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/whatwg-encoding": { @@ -31896,6 +32289,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -32155,6 +32549,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -32419,6 +32814,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -32428,6 +32824,7 @@ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", "license": "ISC", + "peer": true, "peerDependencies": { "zod": "^3.25 || ^4" } diff --git a/package.json b/package.json index f6d3b19b0..5e384b41e 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@adobe/spacecat-shared-ahrefs-client": "1.10.7", "@adobe/spacecat-shared-athena-client": "1.9.6", "@adobe/spacecat-shared-brand-client": "1.1.38", - "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/669c7f824b915dc67dfb9fe2fc7e29917e38ace7/adobe-spacecat-shared-data-access-3.19.0.tgz", + "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/f5aa0f15c554f5c31bdab9112e95011b6a723ec8/adobe-spacecat-shared-data-access-3.22.0.tgz", "@adobe/spacecat-shared-data-access-v2": "npm:@adobe/spacecat-shared-data-access@2.109.0", "@adobe/spacecat-shared-drs-client": "1.3.0", "@adobe/spacecat-shared-gpt-client": "1.6.19", @@ -87,7 +87,7 @@ "@adobe/spacecat-shared-slack-client": "1.6.2", "@adobe/spacecat-shared-tier-client": "1.3.15", "@adobe/spacecat-shared-tokowaka-client": "1.11.1", - "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/4dc817e97910e998b17ad1f535a7e289dca89d95/adobe-spacecat-shared-utils-1.102.0.tgz", + "@adobe/spacecat-shared-utils": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/5f65e64f31849dcae0699a7744f07a23f77bcc21/adobe-spacecat-shared-utils-1.102.1.tgz", "@adobe/spacecat-shared-vault-secrets": "1.2.0", "@aws-sdk/client-s3": "3.1004.0", "@aws-sdk/client-secrets-manager": "3.1004.0", diff --git a/src/controllers/opportunities.js b/src/controllers/opportunities.js index 5c798bf53..6342934a0 100644 --- a/src/controllers/opportunities.js +++ b/src/controllers/opportunities.js @@ -161,7 +161,8 @@ function OpportunitiesController(ctx) { if (await getIsSummitPlgEnabled(site, ctx)) { try { await grantSuggestionsForOpportunity(dataAccess, site, oppty); - /* c8 ignore next */ } catch (err) { + /* c8 ignore next 3 */ + } catch (err) { ctx.log?.warn?.('Grant suggestions handler failed', err?.message ?? err); } } diff --git a/src/controllers/suggestions.js b/src/controllers/suggestions.js index 2afed8424..98c06719e 100644 --- a/src/controllers/suggestions.js +++ b/src/controllers/suggestions.js @@ -199,9 +199,10 @@ function SuggestionsController(ctx, sqs, env) { const ids = suggestions.map((s) => s.getId()); const { grantedIds } = await SuggestionGrant.splitSuggestionsByGrantStatus(ids); return suggestions.filter((s) => grantedIds.includes(s.getId())); - /* c8 ignore next */ } catch (err) { - ctx.log?.error?.('Failed to filter suggestions by grant status', err?.message ?? err); - return []; + } catch (err) { + const message = 'Failed to filter suggestions by grant status'; + ctx.log?.error?.(message, err?.message ?? err); + throw new Error(message, { cause: err }); } }; diff --git a/src/support/grant-suggestions-handler.js b/src/support/grant-suggestions-handler.js index 66bc81803..162857b4a 100644 --- a/src/support/grant-suggestions-handler.js +++ b/src/support/grant-suggestions-handler.js @@ -144,7 +144,8 @@ export async function grantSuggestionsForOpportunity(dataAccess, site, opportuni ? getTokenGrantConfigByOpportunity(oppType) : null; const tokenType = config?.tokenType; - if (!Suggestion || !SuggestionGrant || !Token || !siteId || !opptyId || !config) return; + if (!Suggestion || !SuggestionGrant || !Token || !siteId || !opptyId || !config + || !tokenType) return; const { STATUSES } = SuggestionModel; const newSuggestions = await Suggestion diff --git a/test/controllers/suggestions.test.js b/test/controllers/suggestions.test.js index e89f0dee3..9b1751ff5 100644 --- a/test/controllers/suggestions.test.js +++ b/test/controllers/suggestions.test.js @@ -539,18 +539,15 @@ describe('Suggestions Controller', () => { expect(suggestions[0]).to.have.property('opportunityId', OPPORTUNITY_ID); }); - it('returns empty array when grant filtering throws an error', async () => { + it('propagates error when grant filtering throws an error', async () => { mockSuggestionGrant.splitSuggestionsByGrantStatus.rejects(new Error('db failure')); - const response = await suggestionsController.getAllForOpportunity({ + await expect(suggestionsController.getAllForOpportunity({ params: { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID, }, ...context, - }); - expect(response.status).to.equal(200); - const suggestions = await response.json(); - expect(suggestions).to.be.an('array').with.lengthOf(0); + })).to.be.rejectedWith('Failed to filter suggestions by grant status'); }); it('returns all suggestions without grant filtering when summit-plg is not enabled', async () => { From 21d115a0b562b79f77ce6878d9e39fd693bd260a Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Wed, 18 Mar 2026 09:44:36 +0530 Subject: [PATCH 14/16] fix: adds useFilter param --- src/controllers/suggestions.js | 33 +++--- test/controllers/opportunities.test.js | 20 ++++ test/controllers/suggestions.test.js | 150 +++++++++++++++++++++---- 3 files changed, 163 insertions(+), 40 deletions(-) diff --git a/src/controllers/suggestions.js b/src/controllers/suggestions.js index 6b97af74e..129f60878 100644 --- a/src/controllers/suggestions.js +++ b/src/controllers/suggestions.js @@ -188,14 +188,16 @@ function SuggestionsController(ctx, sqs, env) { const accessControlUtil = AccessControlUtil.fromContext(ctx); /** - * Filters suggestions to only granted ones when summit-plg is enabled for the site. - * Returns all suggestions unchanged when summit-plg is not enabled. + * Filters suggestions to only granted ones when summit-plg is enabled for the site + * and useFilters is enabled. + * Returns all suggestions unchanged when either condition is not met. * @param {Object} site - Site entity. * @param {Array} suggestions - Suggestion entities to filter. + * @param {boolean} useFilters - Whether grant filtering is enabled. * @returns {Promise} Filtered suggestion entities. */ - const filterByGrantStatus = async (site, suggestions) => { - if (!await getIsSummitPlgEnabled(site, ctx)) return suggestions; + const filterByGrantStatus = async (site, suggestions, useFilters = false) => { + if (!useFilters || !await getIsSummitPlgEnabled(site, ctx)) return suggestions; try { const ids = suggestions.map((s) => s.getId()); const { grantedIds } = await SuggestionGrant.splitSuggestionsByGrantStatus(ids); @@ -217,6 +219,7 @@ function SuggestionsController(ctx, sqs, env) { const opptyId = context.params?.opportunityId; const viewParam = context.data?.view; const statusParam = context.data?.status; + const useFilters = context.data?.useFilters === 'true'; if (!isValidUUID(siteId)) { return badRequest('Site ID required'); @@ -260,7 +263,7 @@ function SuggestionsController(ctx, sqs, env) { (sugg) => statuses.includes(sugg.getStatus()), ); } - const grantedEntities = await filterByGrantStatus(site, suggestionEntities); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities, useFilters); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -281,6 +284,7 @@ function SuggestionsController(ctx, sqs, env) { const limit = parseInt(context.params?.limit, 10) || DEFAULT_PAGE_SIZE; const cursor = context.params?.cursor || null; const viewParam = context.data?.view; + const useFilters = context.data?.useFilters === 'true'; if (!isValidUUID(siteId)) { return badRequest('Site ID required'); @@ -319,7 +323,7 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const grantedEntities = await filterByGrantStatus(site, suggestionEntities); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities, useFilters); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -344,6 +348,7 @@ function SuggestionsController(ctx, sqs, env) { const opptyId = context.params?.opportunityId; const status = context.params?.status || undefined; const viewParam = context.data?.view; + const useFilters = context.data?.useFilters === 'true'; if (!isValidUUID(siteId)) { return badRequest('Site ID required'); @@ -375,7 +380,7 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const grantedEntities = await filterByGrantStatus(site, suggestionEntities); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities, useFilters); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -394,6 +399,7 @@ function SuggestionsController(ctx, sqs, env) { const limit = parseInt(context.params?.limit, 10) || DEFAULT_PAGE_SIZE; const cursor = context.params?.cursor || null; const viewParam = context.data?.view; + const useFilters = context.data?.useFilters === 'true'; if (!isValidUUID(siteId)) { return badRequest('Site ID required'); @@ -434,7 +440,7 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const grantedEntities = await filterByGrantStatus(site, suggestionEntities); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities, useFilters); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -458,6 +464,7 @@ function SuggestionsController(ctx, sqs, env) { const opptyId = context.params?.opportunityId || undefined; const suggestionId = context.params?.suggestionId || undefined; const viewParam = context.data?.view; + const useFilters = context.data?.useFilters === 'true'; if (!isValidUUID(siteId)) { return badRequest('Site ID required'); @@ -491,7 +498,7 @@ function SuggestionsController(ctx, sqs, env) { if (!opportunity || opportunity.getSiteId() !== siteId) { return notFound(); } - if (await getIsSummitPlgEnabled(site, ctx) + if (useFilters && await getIsSummitPlgEnabled(site, ctx) && !(await SuggestionGrant.isSuggestionGranted(suggestion.getId()))) { return notFound('Suggestion not found'); } @@ -1018,14 +1025,6 @@ function SuggestionsController(ctx, sqs, env) { const suggestions = await Suggestion.allByOpportunityId( opportunityId, ); - const requestedSuggestions = suggestions.filter((s) => suggestionIds.includes(s.getId())); - if (await getIsSummitPlgEnabled(site, ctx) && requestedSuggestions.length > 0) { - const requestedIds = requestedSuggestions.map((s) => s.getId()); - const { notGrantedIds } = await SuggestionGrant.splitSuggestionsByGrantStatus(requestedIds); - if (notGrantedIds.length > 0) { - return badRequest('All suggestion IDs must be granted before autofix can be executed'); - } - } const validSuggestions = []; const failedSuggestions = []; suggestions.forEach((suggestion) => { diff --git a/test/controllers/opportunities.test.js b/test/controllers/opportunities.test.js index 0af76526c..aa7d3dd0e 100644 --- a/test/controllers/opportunities.test.js +++ b/test/controllers/opportunities.test.js @@ -413,14 +413,27 @@ describe('Opportunities Controller', () => { isHandlerEnabledForSite: sandbox.stub().returns(true), }), }; + const mockSiteWithOrg = { + findById: sandbox.stub().resolves({ + getId: () => SITE_ID, + getOrganizationId: () => 'org-123', + }), + }; + const mockEntitlement = { + findByOrganizationIdAndProductCode: sandbox.stub().resolves({ + getTier: () => 'FREE_TRIAL', + }), + }; const ctxWithToken = { ...mockContext, dataAccess: { ...mockOpportunityDataAccess, + Site: mockSiteWithOrg, Suggestion: mockSuggestion, SuggestionGrant: mockSuggestionGrant, Token: mockToken, Configuration: mockConfig, + Entitlement: mockEntitlement, }, }; const controllerWithToken = OpportunitiesController(ctxWithToken); @@ -451,12 +464,18 @@ describe('Opportunities Controller', () => { }; const mockSiteEntity = { getId: () => SITE_ID, + getOrganizationId: () => 'org-123', }; const mockConfig = { findLatest: sandbox.stub().resolves({ isHandlerEnabledForSite: sandbox.stub().returns(true), }), }; + const mockEntitlement = { + findByOrganizationIdAndProductCode: sandbox.stub().resolves({ + getTier: () => 'FREE_TRIAL', + }), + }; const ctxWithToken = { ...mockContext, dataAccess: { @@ -466,6 +485,7 @@ describe('Opportunities Controller', () => { SuggestionGrant: {}, Token: mockToken, Configuration: mockConfig, + Entitlement: mockEntitlement, }, }; const controllerWithToken = OpportunitiesController(ctxWithToken); diff --git a/test/controllers/suggestions.test.js b/test/controllers/suggestions.test.js index 33a400e1c..c54d74e56 100644 --- a/test/controllers/suggestions.test.js +++ b/test/controllers/suggestions.test.js @@ -541,13 +541,103 @@ describe('Suggestions Controller', () => { it('propagates error when grant filtering throws an error', async () => { mockSuggestionGrant.splitSuggestionsByGrantStatus.rejects(new Error('db failure')); - await expect(suggestionsController.getAllForOpportunity({ + const ControllerWithSummitPlg = await esmock('../../src/controllers/suggestions.js', { + '../../src/support/utils.js': { + getIsSummitPlgEnabled: async () => true, + }, + }); + const controllerWithSummitPlg = ControllerWithSummitPlg({ + dataAccess: mockSuggestionDataAccess, + pathInfo: { headers: { 'x-product': 'llmo' } }, + ...authContext, + }, mockSqs, { AUTOFIX_JOBS_QUEUE: 'https://autofix-jobs-queue' }); + await expect(controllerWithSummitPlg.getAllForOpportunity({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + }, + data: { useFilters: 'true' }, + ...context, + })).to.be.rejectedWith('Failed to filter suggestions by grant status'); + }); + + it('filters suggestions by grant status when summit-plg is enabled', async () => { + const grantedId = SUGGESTION_IDS[0]; + mockSuggestionGrant.splitSuggestionsByGrantStatus.resolves({ + grantedIds: [grantedId], + notGrantedIds: [], + grantIds: [`grant-${grantedId}`], + }); + const ControllerWithSummitPlg = await esmock('../../src/controllers/suggestions.js', { + '../../src/support/utils.js': { + getIsSummitPlgEnabled: async () => true, + }, + }); + const controllerWithSummitPlg = ControllerWithSummitPlg({ + dataAccess: mockSuggestionDataAccess, + pathInfo: { headers: { 'x-product': 'llmo' } }, + ...authContext, + }, mockSqs, { AUTOFIX_JOBS_QUEUE: 'https://autofix-jobs-queue' }); + const response = await controllerWithSummitPlg.getAllForOpportunity({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + }, + data: { useFilters: 'true' }, + ...context, + }); + expect(response.status).to.equal(200); + const result = await response.json(); + expect(result).to.be.an('array').with.lengthOf(1); + expect(mockSuggestionGrant.splitSuggestionsByGrantStatus).to.have.been.calledOnce; + }); + + it('skips grant filtering when useFilters is not set', async () => { + const ControllerWithSummitPlg = await esmock('../../src/controllers/suggestions.js', { + '../../src/support/utils.js': { + getIsSummitPlgEnabled: async () => true, + }, + }); + const controllerWithSummitPlg = ControllerWithSummitPlg({ + dataAccess: mockSuggestionDataAccess, + pathInfo: { headers: { 'x-product': 'llmo' } }, + ...authContext, + }, mockSqs, { AUTOFIX_JOBS_QUEUE: 'https://autofix-jobs-queue' }); + mockSuggestionGrant.splitSuggestionsByGrantStatus.resetHistory(); + const response = await controllerWithSummitPlg.getAllForOpportunity({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + }, + ...context, + }); + expect(response.status).to.equal(200); + expect(mockSuggestionGrant.splitSuggestionsByGrantStatus).to.not.have.been.called; + }); + + it('propagates error and logs when grant filtering throws a non-Error value', async () => { + mockSuggestionGrant.splitSuggestionsByGrantStatus.callsFake(() => Promise.reject({ code: 500 })); + const mockLog = { error: sandbox.stub() }; + const ControllerWithSummitPlg = await esmock('../../src/controllers/suggestions.js', { + '../../src/support/utils.js': { + getIsSummitPlgEnabled: async () => true, + }, + }); + const controllerWithSummitPlg = ControllerWithSummitPlg({ + dataAccess: mockSuggestionDataAccess, + pathInfo: { headers: { 'x-product': 'llmo' } }, + log: mockLog, + ...authContext, + }, mockSqs, { AUTOFIX_JOBS_QUEUE: 'https://autofix-jobs-queue' }); + await expect(controllerWithSummitPlg.getAllForOpportunity({ params: { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID, }, + data: { useFilters: 'true' }, ...context, })).to.be.rejectedWith('Failed to filter suggestions by grant status'); + expect(mockLog.error).to.have.been.calledOnce; }); it('returns all suggestions without grant filtering when summit-plg is not enabled', async () => { @@ -1633,19 +1723,53 @@ describe('Suggestions Controller', () => { expect(error).to.have.property('message', 'not found'); }); - it('getByID returns not found for summit-plg enabled site with ungranted suggestion', async () => { + it('getByID returns not found for summit-plg enabled site with ungranted suggestion when useFilters is true', async () => { mockSuggestionGrant.isSuggestionGranted.resolves(false); - const response = await suggestionsController.getByID({ + const ControllerWithSummitPlg = await esmock('../../src/controllers/suggestions.js', { + '../../src/support/utils.js': { + getIsSummitPlgEnabled: async () => true, + }, + }); + const controllerWithSummitPlg = ControllerWithSummitPlg({ + dataAccess: mockSuggestionDataAccess, + pathInfo: { headers: { 'x-product': 'llmo' } }, + ...authContext, + }, mockSqs, { AUTOFIX_JOBS_QUEUE: 'https://autofix-jobs-queue' }); + const response = await controllerWithSummitPlg.getByID({ params: { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID, suggestionId: SUGGESTION_IDS[0], }, + data: { useFilters: 'true' }, ...context, }); expect(response.status).to.equal(404); }); + it('getByID returns suggestion for summit-plg enabled site with ungranted suggestion when useFilters is not set', async () => { + mockSuggestionGrant.isSuggestionGranted.resolves(false); + const ControllerWithSummitPlg = await esmock('../../src/controllers/suggestions.js', { + '../../src/support/utils.js': { + getIsSummitPlgEnabled: async () => true, + }, + }); + const controllerWithSummitPlg = ControllerWithSummitPlg({ + dataAccess: mockSuggestionDataAccess, + pathInfo: { headers: { 'x-product': 'llmo' } }, + ...authContext, + }, mockSqs, { AUTOFIX_JOBS_QUEUE: 'https://autofix-jobs-queue' }); + const response = await controllerWithSummitPlg.getByID({ + params: { + siteId: SITE_ID, + opportunityId: OPPORTUNITY_ID, + suggestionId: SUGGESTION_IDS[0], + }, + ...context, + }); + expect(response.status).to.equal(200); + }); + describe('getSuggestionFixes', () => { const FIX_IDS = [ 'fix-id-1', @@ -3316,26 +3440,6 @@ describe('Suggestions Controller', () => { sandbox.restore(); }); - it('returns bad request when any suggestion ID is not granted for autofix', async () => { - opportunity.getType = sandbox.stub().returns('meta-tags'); - const requestedEntities = [mockSuggestionEntity(suggs[0]), mockSuggestionEntity(suggs[2])]; - mockSuggestion.allByOpportunityId.resolves(requestedEntities); - mockSuggestionGrant.splitSuggestionsByGrantStatus.resolves({ - grantedIds: [requestedEntities[0].getId()], - notGrantedIds: [requestedEntities[1].getId()], - grantIds: ['grant-1'], - }); - const response = await suggestionsControllerWithMock.autofixSuggestions({ - params: { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID }, - data: { suggestionIds: [SUGGESTION_IDS[0], SUGGESTION_IDS[2]] }, - ...context, - }); - expect(response.status).to.equal(400); - const body = await response.json(); - expect(body).to.have.property('message', 'All suggestion IDs must be granted before autofix can be executed'); - expect(mockSuggestion.bulkUpdateStatus).to.not.have.been.called; - }); - it('triggers autofixSuggestion and sets suggestions to in-progress', async () => { opportunity.getType = sandbox.stub().returns('meta-tags'); mockSuggestion.allByOpportunityId.resolves( From 19b50bcc1db3f71145f32c9b7b5e944e781a4541 Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Wed, 18 Mar 2026 10:09:17 +0530 Subject: [PATCH 15/16] fix: update package --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ddcf339c..2eccaa9dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@adobe/spacecat-shared-ahrefs-client": "1.10.7", "@adobe/spacecat-shared-athena-client": "1.9.7", "@adobe/spacecat-shared-brand-client": "1.1.38", - "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/f5aa0f15c554f5c31bdab9112e95011b6a723ec8/adobe-spacecat-shared-data-access-3.22.0.tgz", + "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/285c2ba0d9740c27994aa01b5df272138895c3b0/adobe-spacecat-shared-data-access-3.22.0.tgz", "@adobe/spacecat-shared-data-access-v2": "npm:@adobe/spacecat-shared-data-access@2.109.0", "@adobe/spacecat-shared-drs-client": "1.3.0", "@adobe/spacecat-shared-gpt-client": "1.6.19", @@ -2352,8 +2352,8 @@ }, "node_modules/@adobe/spacecat-shared-data-access": { "version": "3.22.0", - "resolved": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/f5aa0f15c554f5c31bdab9112e95011b6a723ec8/adobe-spacecat-shared-data-access-3.22.0.tgz", - "integrity": "sha512-hsN+dLHWpQ02r3Ntyqw7LgTvmdYYNitB7q+GWWzNOW5ledFRleRf+Z1zmLPMHpLbjGiWp75XXXHBqgqMwbY/fw==", + "resolved": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/285c2ba0d9740c27994aa01b5df272138895c3b0/adobe-spacecat-shared-data-access-3.22.0.tgz", + "integrity": "sha512-O/MxiDPl9XkXclZcPDssm1xY6BpMkGntjXu7PIw9yDV26QnC6vYGDWmUPuB7KBGrD5Kr+iPOWO1gusDCpnf7sw==", "license": "Apache-2.0", "dependencies": { "@adobe/fetch": "^4.2.3", diff --git a/package.json b/package.json index 2383db70e..90b0e4138 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@adobe/spacecat-shared-ahrefs-client": "1.10.7", "@adobe/spacecat-shared-athena-client": "1.9.7", "@adobe/spacecat-shared-brand-client": "1.1.38", - "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/f5aa0f15c554f5c31bdab9112e95011b6a723ec8/adobe-spacecat-shared-data-access-3.22.0.tgz", + "@adobe/spacecat-shared-data-access": "https://gist.githubusercontent.com/sandsinh/e6c7d5895ece4f0ad3b32a64701faf3b/raw/285c2ba0d9740c27994aa01b5df272138895c3b0/adobe-spacecat-shared-data-access-3.22.0.tgz", "@adobe/spacecat-shared-data-access-v2": "npm:@adobe/spacecat-shared-data-access@2.109.0", "@adobe/spacecat-shared-drs-client": "1.3.0", "@adobe/spacecat-shared-gpt-client": "1.6.19", From deffeeac0a23c9617592a7a2d7a2ebbcf4a546d6 Mon Sep 17 00:00:00 2001 From: Sandesh Sinha Date: Wed, 18 Mar 2026 12:32:44 +0530 Subject: [PATCH 16/16] fix: update summit plg logic --- src/controllers/opportunities.js | 3 ++- src/controllers/suggestions.js | 27 +++++++++++++------------- test/controllers/opportunities.test.js | 2 ++ test/controllers/suggestions.test.js | 14 ++++++------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/controllers/opportunities.js b/src/controllers/opportunities.js index 6342934a0..0e4d83081 100644 --- a/src/controllers/opportunities.js +++ b/src/controllers/opportunities.js @@ -158,7 +158,8 @@ function OpportunitiesController(ctx) { if (!oppty || oppty.getSiteId() !== siteId) { return notFound('Opportunity not found'); } - if (await getIsSummitPlgEnabled(site, ctx)) { + const clientType = context.pathInfo?.headers?.['x-client-type']; + if (clientType === 'sites-optimizer-ui' && await getIsSummitPlgEnabled(site, ctx)) { try { await grantSuggestionsForOpportunity(dataAccess, site, oppty); /* c8 ignore next 3 */ diff --git a/src/controllers/suggestions.js b/src/controllers/suggestions.js index 129f60878..0633f2046 100644 --- a/src/controllers/suggestions.js +++ b/src/controllers/suggestions.js @@ -189,15 +189,18 @@ function SuggestionsController(ctx, sqs, env) { /** * Filters suggestions to only granted ones when summit-plg is enabled for the site - * and useFilters is enabled. + * and the request originates from the sites-optimizer-ui client. * Returns all suggestions unchanged when either condition is not met. * @param {Object} site - Site entity. * @param {Array} suggestions - Suggestion entities to filter. - * @param {boolean} useFilters - Whether grant filtering is enabled. + * @param {Object} context - Request context. * @returns {Promise} Filtered suggestion entities. */ - const filterByGrantStatus = async (site, suggestions, useFilters = false) => { - if (!useFilters || !await getIsSummitPlgEnabled(site, ctx)) return suggestions; + const filterByGrantStatus = async (site, suggestions, context) => { + const clientType = context.pathInfo?.headers?.['x-client-type']; + if (clientType !== 'sites-optimizer-ui' || !await getIsSummitPlgEnabled(site, ctx)) { + return suggestions; + } try { const ids = suggestions.map((s) => s.getId()); const { grantedIds } = await SuggestionGrant.splitSuggestionsByGrantStatus(ids); @@ -219,7 +222,6 @@ function SuggestionsController(ctx, sqs, env) { const opptyId = context.params?.opportunityId; const viewParam = context.data?.view; const statusParam = context.data?.status; - const useFilters = context.data?.useFilters === 'true'; if (!isValidUUID(siteId)) { return badRequest('Site ID required'); @@ -263,7 +265,7 @@ function SuggestionsController(ctx, sqs, env) { (sugg) => statuses.includes(sugg.getStatus()), ); } - const grantedEntities = await filterByGrantStatus(site, suggestionEntities, useFilters); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities, context); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -284,7 +286,6 @@ function SuggestionsController(ctx, sqs, env) { const limit = parseInt(context.params?.limit, 10) || DEFAULT_PAGE_SIZE; const cursor = context.params?.cursor || null; const viewParam = context.data?.view; - const useFilters = context.data?.useFilters === 'true'; if (!isValidUUID(siteId)) { return badRequest('Site ID required'); @@ -323,7 +324,7 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const grantedEntities = await filterByGrantStatus(site, suggestionEntities, useFilters); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities, context); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -348,7 +349,6 @@ function SuggestionsController(ctx, sqs, env) { const opptyId = context.params?.opportunityId; const status = context.params?.status || undefined; const viewParam = context.data?.view; - const useFilters = context.data?.useFilters === 'true'; if (!isValidUUID(siteId)) { return badRequest('Site ID required'); @@ -380,7 +380,7 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const grantedEntities = await filterByGrantStatus(site, suggestionEntities, useFilters); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities, context); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -399,7 +399,6 @@ function SuggestionsController(ctx, sqs, env) { const limit = parseInt(context.params?.limit, 10) || DEFAULT_PAGE_SIZE; const cursor = context.params?.cursor || null; const viewParam = context.data?.view; - const useFilters = context.data?.useFilters === 'true'; if (!isValidUUID(siteId)) { return badRequest('Site ID required'); @@ -440,7 +439,7 @@ function SuggestionsController(ctx, sqs, env) { return notFound('Opportunity not found'); } } - const grantedEntities = await filterByGrantStatus(site, suggestionEntities, useFilters); + const grantedEntities = await filterByGrantStatus(site, suggestionEntities, context); const suggestions = grantedEntities.map( (sugg) => SuggestionDto.toJSON(sugg, view, opportunity), ); @@ -464,7 +463,6 @@ function SuggestionsController(ctx, sqs, env) { const opptyId = context.params?.opportunityId || undefined; const suggestionId = context.params?.suggestionId || undefined; const viewParam = context.data?.view; - const useFilters = context.data?.useFilters === 'true'; if (!isValidUUID(siteId)) { return badRequest('Site ID required'); @@ -498,7 +496,8 @@ function SuggestionsController(ctx, sqs, env) { if (!opportunity || opportunity.getSiteId() !== siteId) { return notFound(); } - if (useFilters && await getIsSummitPlgEnabled(site, ctx) + const clientType = context.pathInfo?.headers?.['x-client-type']; + if (clientType === 'sites-optimizer-ui' && await getIsSummitPlgEnabled(site, ctx) && !(await SuggestionGrant.isSuggestionGranted(suggestion.getId()))) { return notFound('Suggestion not found'); } diff --git a/test/controllers/opportunities.test.js b/test/controllers/opportunities.test.js index aa7d3dd0e..27480f51b 100644 --- a/test/controllers/opportunities.test.js +++ b/test/controllers/opportunities.test.js @@ -445,6 +445,7 @@ describe('Opportunities Controller', () => { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID, }, + pathInfo: { headers: { 'x-client-type': 'sites-optimizer-ui' } }, }); expect(response.status).to.equal(200); expect(mockContext.log.warn).to.have.been.calledOnce; @@ -497,6 +498,7 @@ describe('Opportunities Controller', () => { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID, }, + pathInfo: { headers: { 'x-client-type': 'sites-optimizer-ui' } }, }); expect(response.status).to.equal(200); expect(mockContext.log.warn).to.have.been.calledOnceWith( diff --git a/test/controllers/suggestions.test.js b/test/controllers/suggestions.test.js index c54d74e56..f76b86a1e 100644 --- a/test/controllers/suggestions.test.js +++ b/test/controllers/suggestions.test.js @@ -556,7 +556,7 @@ describe('Suggestions Controller', () => { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID, }, - data: { useFilters: 'true' }, + pathInfo: { headers: { 'x-client-type': 'sites-optimizer-ui' } }, ...context, })).to.be.rejectedWith('Failed to filter suggestions by grant status'); }); @@ -583,7 +583,7 @@ describe('Suggestions Controller', () => { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID, }, - data: { useFilters: 'true' }, + pathInfo: { headers: { 'x-client-type': 'sites-optimizer-ui' } }, ...context, }); expect(response.status).to.equal(200); @@ -592,7 +592,7 @@ describe('Suggestions Controller', () => { expect(mockSuggestionGrant.splitSuggestionsByGrantStatus).to.have.been.calledOnce; }); - it('skips grant filtering when useFilters is not set', async () => { + it('skips grant filtering when x-client-type is not sites-optimizer-ui', async () => { const ControllerWithSummitPlg = await esmock('../../src/controllers/suggestions.js', { '../../src/support/utils.js': { getIsSummitPlgEnabled: async () => true, @@ -634,7 +634,7 @@ describe('Suggestions Controller', () => { siteId: SITE_ID, opportunityId: OPPORTUNITY_ID, }, - data: { useFilters: 'true' }, + pathInfo: { headers: { 'x-client-type': 'sites-optimizer-ui' } }, ...context, })).to.be.rejectedWith('Failed to filter suggestions by grant status'); expect(mockLog.error).to.have.been.calledOnce; @@ -1723,7 +1723,7 @@ describe('Suggestions Controller', () => { expect(error).to.have.property('message', 'not found'); }); - it('getByID returns not found for summit-plg enabled site with ungranted suggestion when useFilters is true', async () => { + it('getByID returns not found for summit-plg enabled site with ungranted suggestion when x-client-type is sites-optimizer-ui', async () => { mockSuggestionGrant.isSuggestionGranted.resolves(false); const ControllerWithSummitPlg = await esmock('../../src/controllers/suggestions.js', { '../../src/support/utils.js': { @@ -1741,13 +1741,13 @@ describe('Suggestions Controller', () => { opportunityId: OPPORTUNITY_ID, suggestionId: SUGGESTION_IDS[0], }, - data: { useFilters: 'true' }, + pathInfo: { headers: { 'x-client-type': 'sites-optimizer-ui' } }, ...context, }); expect(response.status).to.equal(404); }); - it('getByID returns suggestion for summit-plg enabled site with ungranted suggestion when useFilters is not set', async () => { + it('getByID returns suggestion for summit-plg enabled site with ungranted suggestion when x-client-type is not sites-optimizer-ui', async () => { mockSuggestionGrant.isSuggestionGranted.resolves(false); const ControllerWithSummitPlg = await esmock('../../src/controllers/suggestions.js', { '../../src/support/utils.js': {