Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 10 additions & 13 deletions docs/openapi/llmo-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1432,26 +1432,23 @@ site-llmo-edge-optimize-config:
site-llmo-edge-optimize-routing:
parameters:
- $ref: './parameters.yaml#/siteId'
- name: x-promise-token
in: header
required: true
description: Promise token exchanged for IMS user token to authorize the CDN API call.
schema:
type: string
post:
operationId: enableEdgeOptimize
summary: Update edge optimize routing for a site
description: |
Updates edge optimize routing for the site via the internal CDN API. Only available when
ENV is prod. The CDN API to call is selected by the required cdnType request body parameter.
Authenticated via standard auth (JWT/IMS) in the Authorization header; no promise token required.

Flow:
1. Validates request (x-promise-token header and cdnType required; enabled must be boolean if provided).
2. Probes the site with User-Agent `AdobeEdgeOptimize-Test`. If 2xx, flow continues with probe URL domain.
1. Validates request (cdnType required; enabled must be boolean if provided).
2. Loads site and checks access; extracts the site's organization IMS org ID for the CDN API.
3. Probes the site with User-Agent `AdobeEdgeOptimize-Test`. If 2xx, flow continues with probe URL domain.
If 301, the Location header is checked: the probe URL hostname and the Location URL hostname are compared
(lowercased, with leading www. stripped); subdomains are considered, so only the same host is allowed. If they
match, the domain from the Location URL is used for the CDN API; otherwise the flow fails. Other non-2xx responses fail.
3. Exchanges the promise token for IMS user token, then calls internal CDN API for the chosen cdnType to set routing for the domain.
4. Calls internal CDN API for the chosen cdnType with a static token from env (EDGE_OPTIMIZE_CDN_API_TOKEN)
in the Authorization header and the site's organization IMS org ID in the x-ims-org-id header.

tags:
- llmo
Expand Down Expand Up @@ -1495,7 +1492,7 @@ site-llmo-edge-optimize-routing:
type: string
example: aem-cs-fastly
'400':
description: Bad request (e.g. non-prod ENV, missing/invalid x-promise-token or cdnType, invalid enabled, probe failed, non-2xx/301, or 301 Location domain mismatch)
description: Bad request (e.g. non-prod ENV, missing/invalid cdnType, invalid enabled, site org missing or missing IMS org ID, probe failed, non-2xx/301, or 301 Location domain mismatch)
content:
application/json:
schema:
Expand All @@ -1504,15 +1501,15 @@ site-llmo-edge-optimize-routing:
message:
type: string
'401':
description: Authentication failed with upstream IMS service (missing or invalid promise token)
description: CDN API returned 401 Unauthorized
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: Authentication failed with upstream IMS service
example: User is not authorized to update CDN routing
'403':
$ref: './responses.yaml#/403'
'404':
Expand All @@ -1528,7 +1525,7 @@ site-llmo-edge-optimize-routing:
type: string
example: Upstream call failed with status 503
'503':
description: API not available (EDGE_OPTIMIZE_ROUTING_CONFIG not set or invalid, or missing cdnRoutingUrl for cdnType)
description: API not available (EDGE_OPTIMIZE_ROUTING_CONFIG or EDGE_OPTIMIZE_CDN_API_TOKEN not set or invalid, or missing cdnRoutingUrl for cdnType)
content:
application/json:
schema:
Expand Down
49 changes: 28 additions & 21 deletions src/controllers/llmo/llmo.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import crypto from 'crypto';
import { Entitlement as EntitlementModel } from '@adobe/spacecat-shared-data-access';
import TokowakaClient, { calculateForwardedHost } from '@adobe/spacecat-shared-tokowaka-client';
import AccessControlUtil from '../../support/access-control-util.js';
import { exchangePromiseToken } from '../../support/utils.js';
import { triggerBrandProfileAgent } from '../../support/brand-profile-trigger.js';
import {
applyFilters,
Expand Down Expand Up @@ -1261,20 +1260,21 @@ function LlmoController(ctx) {
/**
* POST /sites/{siteId}/llmo/edge-optimize-routing
* Updates edge optimize routing for the site via the internal CDN API.
* - Requires x-promise-token header and request body cdnType.
* - Extracts the site's organization IMS org ID and sends it as x-ims-org-id to the CDN API.
* - Uses static token from env (EDGE_OPTIMIZE_CDN_API_TOKEN) in CDN API Authorization header.
* - Probes the site with custom User-Agent (2xx continues; 301: if Location domain
* normalizes to same as probe URL domain, use Location domain for CDN API; otherwise break).
* - Exchanges promise token for IMS user token, then calls internal CDN API.
* @param {object} context - Request context (context.request for headers)
* @param {object} context - Request context
* @returns {Promise<Response>}
*/
const updateEdgeOptimizeCDNRouting = async (context) => {
const { log, dataAccess, env } = context;
const { siteId } = context.params;
const { Site } = dataAccess;
const { cdnType, enabled = true } = context.data || {};
const promiseToken = context.request?.headers?.get?.('x-promise-token');
log.info(`Edge optimize routing update request received for site ${siteId}`);
const profile = context.attributes?.authInfo?.getProfile?.();
const requester = profile?.email || 'unknown';
log.info(`Edge optimize routing update request received for site ${siteId} by ${requester}`);

if (env?.ENV && env.ENV !== 'prod') {
return createResponse(
Expand All @@ -1283,10 +1283,6 @@ function LlmoController(ctx) {
);
}

if (!hasText(promiseToken)) {
return badRequest('x-promise-token header is required and must be a non-empty string');
}

if (!hasText(cdnType)) {
return badRequest('cdnType is required and must be a non-empty string');
}
Expand Down Expand Up @@ -1316,6 +1312,15 @@ function LlmoController(ctx) {
);
}

const cdnApiToken = env?.EDGE_OPTIMIZE_CDN_API_TOKEN;
if (!hasText(cdnApiToken)) {
log.error('EDGE_OPTIMIZE_CDN_API_TOKEN is not set');
return createResponse(
{ message: 'API is missing mandatory environment variable' },
503,
);
}

const strategy = EDGE_OPTIMIZE_CDN_STRATEGIES[cdnTypeNormalized];

if (enabled !== undefined && typeof enabled !== 'boolean') {
Expand All @@ -1331,6 +1336,17 @@ function LlmoController(ctx) {
return forbidden('User does not have access to this site');
}

const org = await site.getOrganization();
if (!isObject(org)) {
log.error(`Site ${siteId} has no organization`);
return badRequest('Site organization is missing');
}
const imsOrgId = org.getImsOrgId?.();
if (!hasText(imsOrgId)) {
log.error(`Site ${siteId} organization has no IMS org ID`);
return badRequest('Site organization has no IMS org ID');
}

const overrideBaseURL = site.getConfig()?.getFetchConfig?.()?.overrideBaseURL;
const effectiveBaseUrl = isValidUrl(overrideBaseURL) ? overrideBaseURL : site.getBaseURL();
log.info(`Effective base URL for site ${siteId}: ${effectiveBaseUrl}`);
Expand Down Expand Up @@ -1377,16 +1393,6 @@ function LlmoController(ctx) {
return badRequest(msg);
}

let imsUserToken;
try {
log.debug(`Getting IMS user token for site ${siteId}`);
imsUserToken = await exchangePromiseToken(context, promiseToken);
log.info('IMS user token obtained successfully');
} catch (tokenError) {
log.warn(`Fetching IMS user token for site ${siteId} failed: ${tokenError.status} ${tokenError.message}`);
return createResponse({ message: 'Authentication failed with upstream IMS service' }, 401);
}

try {
const cdnUrl = strategy.buildUrl(cdnConfig, domain);
const cdnBody = strategy.buildBody(enabled);
Expand All @@ -1395,7 +1401,8 @@ function LlmoController(ctx) {
method: strategy.method,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${imsUserToken}`,
Authorization: `Bearer ${cdnApiToken}`,
'x-ims-org-id': imsOrgId,
},
body: JSON.stringify(cdnBody),
signal: AbortSignal.timeout(5000),
Expand Down
Loading
Loading