From c4e7c40a4d6f4d3c03ff168466eddd55699031d8 Mon Sep 17 00:00:00 2001 From: Akash Bhardwaj Date: Mon, 29 Jun 2026 19:44:32 +0530 Subject: [PATCH 1/3] fix(tokowaka-client): use a real browser UA for the verifyRouting human probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The human leg of verifyRouting used a bare `Mozilla/5.0`, which bot-detection can misclassify — making the human probe look bot-like and the verify falsely fail. Default to a realistic desktop Chrome User-Agent (overridable via an optional `humanUa` option). The bot probe (chatgpt-user) is unchanged. Co-Authored-By: Claude Opus 4.8 --- .../src/cdn/cloudfront/index.js | 10 +++++++-- .../test/cdn/cloudfront/index.test.js | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/spacecat-shared-tokowaka-client/src/cdn/cloudfront/index.js b/packages/spacecat-shared-tokowaka-client/src/cdn/cloudfront/index.js index 605f98a40..cf1de2b19 100644 --- a/packages/spacecat-shared-tokowaka-client/src/cdn/cloudfront/index.js +++ b/packages/spacecat-shared-tokowaka-client/src/cdn/cloudfront/index.js @@ -1199,6 +1199,11 @@ async function fetchEdgeOptimizeHeaders(url, userAgent) { } } +// A realistic desktop-browser User-Agent for the "human" probe. A bare `Mozilla/5.0` can be +// misclassified by bot-detection, making the human leg look bot-like and the verify falsely fail. +const DEFAULT_HUMAN_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + + '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'; + /** * Verify Edge Optimize routing end-to-end by fetching the distribution domain as an agentic bot * and as a human, then inspecting the `x-edgeoptimize-*` headers. Mirrors the standalone wizard's @@ -1207,15 +1212,16 @@ async function fetchEdgeOptimizeHeaders(url, userAgent) { * the page is NOT optimised, which is NOT success. * * @param {string} url - the URL to probe (typically `https:///`). + * @param {object} [options] + * @param {string} [options.humanUa] - User-Agent for the human probe (defaults to a browser UA). * @returns {Promise<{passed: boolean, requestId: string|null, * details: {bot: object, human: object}}>} */ -export async function verifyRouting(url) { +export async function verifyRouting(url, { humanUa = DEFAULT_HUMAN_UA } = {}) { if (!hasText(url)) { throw new Error('url is required'); } const botUa = 'chatgpt-user'; - const humanUa = 'Mozilla/5.0'; const [bot, human] = await Promise.all([ fetchEdgeOptimizeHeaders(url, botUa), fetchEdgeOptimizeHeaders(url, humanUa), diff --git a/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js b/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js index a7cac51c8..d280b56c6 100644 --- a/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js +++ b/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js @@ -2045,6 +2045,27 @@ describe('edge-optimize support', () => { expect(result.details.bot.status).to.equal(200); }); + it('uses a realistic browser User-Agent for the human probe by default', async () => { + fetchStub = sinon.stub(global, 'fetch'); + fetchStub.resolves(makeResponse(200, {})); + + await edgeOptimize.verifyRouting('https://d.cloudfront.net/'); + + // bot probe is the first fetch, human probe the second. + const humanUa = fetchStub.secondCall.args[1].headers['user-agent']; + expect(humanUa).to.include('Chrome/'); + expect(humanUa).to.not.equal('Mozilla/5.0'); + }); + + it('allows overriding the human User-Agent', async () => { + fetchStub = sinon.stub(global, 'fetch'); + fetchStub.resolves(makeResponse(200, {})); + + await edgeOptimize.verifyRouting('https://d.cloudfront.net/', { humanUa: 'CustomHumanUA/1.0' }); + + expect(fetchStub.secondCall.args[1].headers['user-agent']).to.equal('CustomHumanUA/1.0'); + }); + it('ignores non-edgeoptimize response headers', async () => { fetchStub = sinon.stub(global, 'fetch'); fetchStub.onFirstCall().resolves(makeResponse(200, { 'x-edgeoptimize-request-id': 'req-1', 'content-type': 'text/html' })); From 722c6431243dee6310116de9e1f6078f6e35b2ff Mon Sep 17 00:00:00 2001 From: Akash Bhardwaj Date: Thu, 2 Jul 2026 10:56:21 +0530 Subject: [PATCH 2/3] feat(tokowaka-client): attributable assume-role + resource ownership tags/guards - assumeConnectorRole accepts an operator (email) and sets it as the RoleSessionName so the customer's CloudTrail attributes each cross-account mutation to who ran it. - Tag the resources we create (Lambda, its exec role, the CloudFront function) with a fixed owner marker + createdBy; verify ownership before reusing/modifying any same-named resource (origin, cache clone, function, lambda, exec role) and refuse with an actionable "set up manually" error when it is not ours. - data-access: add optional edgeOptimizeConfig.externalId (org-level connector id). Co-Authored-By: Claude Opus 4.8 --- .../src/models/site/config.js | 3 + .../src/cdn/cloudfront/index.js | 107 +++++++- .../test/cdn/cloudfront/index.test.js | 237 +++++++++++++++--- 3 files changed, 307 insertions(+), 40 deletions(-) diff --git a/packages/spacecat-shared-data-access/src/models/site/config.js b/packages/spacecat-shared-data-access/src/models/site/config.js index c58e99442..3ab70c22d 100644 --- a/packages/spacecat-shared-data-access/src/models/site/config.js +++ b/packages/spacecat-shared-data-access/src/models/site/config.js @@ -449,6 +449,9 @@ export const configSchema = Joi.object({ ) .optional(), opted: Joi.number().optional(), + // Cross-account connector external ID, persisted at the Organization level (the shared Config + // backs Site + Organization) so an org's domains/accounts reuse one connector role. + externalId: Joi.string().uuid().optional(), stagingDomains: Joi.array().items( Joi.object({ domain: Joi.string().required(), diff --git a/packages/spacecat-shared-tokowaka-client/src/cdn/cloudfront/index.js b/packages/spacecat-shared-tokowaka-client/src/cdn/cloudfront/index.js index cf1de2b19..1d8e80d53 100644 --- a/packages/spacecat-shared-tokowaka-client/src/cdn/cloudfront/index.js +++ b/packages/spacecat-shared-tokowaka-client/src/cdn/cloudfront/index.js @@ -27,6 +27,7 @@ import { DescribeFunctionCommand, PublishFunctionCommand, UpdateDistributionCommand, + TagResourceCommand, } from '@aws-sdk/client-cloudfront'; import { IAMClient, @@ -103,6 +104,49 @@ const delay = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }); +// AWS RoleSessionName allows [\w+=,.@-] and 2-64 chars. Use the operator's email so the customer's +// CloudTrail attributes every mutation to the person who ran it; fall back to the generic name. +function sessionNameFor(operator) { + const cleaned = String(operator || '').replace(/[^\w+=,.@-]/g, '').slice(0, 64); + return cleaned.length >= 2 ? cleaned : SESSION_NAME; +} + +// Ownership marks on resources we create: a fixed owner flag for reuse + the operator as a +// breadcrumb. Tagged on the taggable resources (Lambda, its exec role, the CloudFront function); +// cache policy + origin (not taggable) use their name + Comment / EO-header markers instead. +const OWNER_TAG_KEY = 'ManagedBy'; +const OWNER_TAG_VALUE = 'adobe-llmo'; +const CREATED_BY_TAG_KEY = 'createdBy'; +const tagCreatedBy = (operator) => String(operator || 'unknown') + .replace(/[^\w.@+=:/ -]/g, '') + .slice(0, 256) || 'unknown'; +// IAM CreateRole + CloudFront TagResource take [{Key,Value}]; Lambda takes an object. +const ownerTagPairs = (operator) => [ + { Key: OWNER_TAG_KEY, Value: OWNER_TAG_VALUE }, + { Key: CREATED_BY_TAG_KEY, Value: tagCreatedBy(operator) }, +]; +const ownerTagsObject = (operator) => Object.fromEntries( + ownerTagPairs(operator).map((t) => [t.Key, t.Value]), +); + +// Substring stamped in the Comment of the CloudFront resources we create (function + cloned cache +// policy). Non-taggable/Comment-only resources use this as their ownership signal. +const OWNER_COMMENT_MARKER = 'managed by LLM Optimizer'; +// True when an IAM Role's tags (from GetRole) carry our fixed owner marker. +const roleTagsOwned = (tags = []) => tags.some( + (t) => t.Key === OWNER_TAG_KEY && t.Value === OWNER_TAG_VALUE, +); +// Guards reuse: never silently reuse or modify a same-named resource we did not create. Throws an +// actionable error the deploy step surfaces so the customer can finish setup manually. +function assertOwnedOrThrow(resourceLabel, name, isOurs) { + if (!isOurs) { + throw new Error( + `A CloudFront ${resourceLabel} named "${name}" already exists but was not created by ` + + 'LLM Optimizer. Remove or rename it, or complete the Edge Optimize setup manually.', + ); + } +} + /** * Assume the customer's cross-account connector role and return short-lived credentials. * @@ -116,6 +160,8 @@ const delay = (ms) => new Promise((resolve) => { * @param {string} params.externalId - external ID baked into the connector role trust policy. * @param {string} [params.roleName] - connector role name (defaults to the standard name). * @param {string} [params.region] - STS region. + * @param {string} [params.operator] - operator identity (email); used as the RoleSessionName so the + * customer's CloudTrail attributes each mutation to the person who ran it. * @returns {Promise<{roleArn: string, accountId: string, credentials: object}>} */ export async function assumeConnectorRole({ @@ -123,6 +169,7 @@ export async function assumeConnectorRole({ externalId, roleName = EDGE_OPTIMIZE_DEFAULT_ROLE_NAME, region = EDGE_OPTIMIZE_REGION, + operator, }) { if (!/^[0-9]{12}$/.test(String(accountId))) { throw new Error('accountId must be a 12-digit AWS account ID'); @@ -135,7 +182,7 @@ export async function assumeConnectorRole({ const sts = new STSClient({ region }); const response = await sts.send(new AssumeRoleCommand({ RoleArn: roleArn, - RoleSessionName: SESSION_NAME, + RoleSessionName: sessionNameFor(operator), ExternalId: externalId, DurationSeconds: SESSION_DURATION_SECONDS, })); @@ -323,6 +370,12 @@ export async function createOrigin( throw new Error(`An origin for ${originDomain} already exists as ${existingDomainOrigin.Id}; refusing to reuse a non-Edge Optimize origin`); } + if (existing) { + // Reuse only our origin: our reserved Id must point at the EO domain. + // If it targets something else, it was not created by us (or was repurposed) — do not reuse it. + assertOwnedOrThrow('origin', EDGE_OPTIMIZE_ORIGIN_ID, existing.DomainName === originDomain); + } + if (existing) { // Idempotent — but self-heal an origin created without the EO headers (earlier bug): patch its // CustomHeaders to the desired set when they differ. Never wipe headers if none were supplied. @@ -397,6 +450,7 @@ export async function createCloudFrontFunction( distributionId, targetedPaths = null, region = EDGE_OPTIMIZE_REGION, + operator = undefined, ) { if (!hasText(defaultOriginId)) { throw new Error('defaultOriginId is required'); @@ -414,12 +468,16 @@ export async function createCloudFrontFunction( // Look up the DEVELOPMENT stage to get its ETag (needed to update an existing function). let existingEtag = null; + let functionArn = null; + let existingComment = ''; try { const desc = await client.send(new DescribeFunctionCommand({ Name: functionName, Stage: 'DEVELOPMENT', })); existingEtag = desc.ETag; + functionArn = desc.FunctionSummary?.FunctionMetadata?.FunctionARN ?? null; + existingComment = String(desc.FunctionSummary?.FunctionConfig?.Comment || ''); } catch (err) { if (err.name !== 'NoSuchFunctionExists') { throw err; @@ -428,6 +486,9 @@ export async function createCloudFrontFunction( let etag; if (existingEtag) { + // Reuse only our function: it must carry our Comment marker (checked before the update below + // overwrites the Comment) — a same-named function without it is not ours. + assertOwnedOrThrow('function', functionName, existingComment.includes(OWNER_COMMENT_MARKER)); const updated = await client.send(new UpdateFunctionCommand({ Name: functionName, IfMatch: existingEtag, @@ -435,6 +496,7 @@ export async function createCloudFrontFunction( FunctionCode: code, })); etag = updated.ETag; + functionArn = updated.FunctionSummary?.FunctionMetadata?.FunctionARN ?? functionArn; } else { const created = await client.send(new CreateFunctionCommand({ Name: functionName, @@ -442,6 +504,7 @@ export async function createCloudFrontFunction( FunctionCode: code, })); etag = created.ETag; + functionArn = created.FunctionSummary?.FunctionMetadata?.FunctionARN ?? functionArn; } await client.send(new PublishFunctionCommand({ @@ -449,6 +512,19 @@ export async function createCloudFrontFunction( IfMatch: etag, })); + // Tag the function (CloudFront functions are taggable) so reuse can verify it is ours. + // Best-effort: the Comment marker still identifies it as ours if tagging is unavailable. + if (functionArn) { + try { + await client.send(new TagResourceCommand({ + Resource: functionArn, + Tags: { Items: ownerTagPairs(operator) }, + })); + } catch (e) { + // best-effort tagging; the Comment marker is the fallback ownership signal + } + } + return { name: functionName, created: !existingEtag, stage: 'LIVE' }; } @@ -602,6 +678,13 @@ export async function updateCacheSettings( let newPolicyId; let reused = false; if (existing) { + // Reuse only our clone: a policy matching the derived clone name must carry our Comment marker; + // if one with that exact name exists without it, it is not ours — refuse. + assertOwnedOrThrow( + 'cache policy', + clonedName, + String(existing.CachePolicy.CachePolicyConfig?.Comment || '').includes(OWNER_COMMENT_MARKER), + ); newPolicyId = existing.CachePolicy.Id; reused = true; } else { @@ -755,6 +838,7 @@ export async function createLambdaAtEdge( distributionId, originDomain = EDGE_OPTIMIZE_DEFAULT_ORIGIN_DOMAIN, retryDelayMs = 5000, + operator, } = {}, ) { if (!/^[0-9]{12}$/.test(String(accountId))) { @@ -778,6 +862,9 @@ export async function createLambdaAtEdge( const existing = await iam.send( new GetRoleCommand({ RoleName: roleName }), ); + // Reuse only our role: it must carry our owner tag before we modify its trust policy; + // a same-named role without it is not ours — refuse rather than repurpose it. + assertOwnedOrThrow('Lambda execution role', roleName, roleTagsOwned(existing.Role?.Tags)); roleArn = existing.Role.Arn; await iam.send(new UpdateAssumeRolePolicyCommand({ RoleName: roleName, @@ -791,6 +878,7 @@ export async function createLambdaAtEdge( RoleName: roleName, AssumeRolePolicyDocument: LAMBDA_TRUST_POLICY, Description: 'Execution role for EdgeOptimize Lambda@Edge function', + Tags: ownerTagPairs(operator), })); roleArn = created.Role.Arn; roleIsNew = true; @@ -845,6 +933,7 @@ export async function createLambdaAtEdge( Description: 'EdgeOptimize origin request/response handler (Lambda@Edge)', Timeout: 5, MemorySize: 128, + Tags: ownerTagsObject(operator), })); createdArn = created.FunctionArn; lastErr = null; @@ -885,6 +974,14 @@ export async function createLambdaAtEdge( }; } + // Reuse only our function: before reusing an existing Active one, confirm it runs as our exec + // role; if it points at a different role it was not created by us — refuse. + assertOwnedOrThrow( + 'Lambda@Edge function', + lambdaName, + String(cfg.Role || '').endsWith(`/${roleName}`), + ); + // Active and idle. If a numbered version already exists, reuse it (idempotent). const existingVersion = await getLatestLambdaVersion(lambda, lambdaName); if (existingVersion) { @@ -1334,7 +1431,7 @@ async function isBehaviorAlreadyAssociated(client, distributionId, pathPattern) export async function runDeployStep( credentials, { - distributionId, originId, behavior, originDomain, originHeaders, accountId, + distributionId, originId, behavior, originDomain, originHeaders, accountId, operator, }, region = EDGE_OPTIMIZE_REGION, ) { @@ -1368,7 +1465,7 @@ export async function runDeployStep( if (await isRoutingFunctionLive(client, distributionId)) { byKey('function').status = 'done'; } else { - await createCloudFrontFunction(credentials, originId, distributionId, null, region); + await createCloudFrontFunction(credentials, originId, distributionId, null, region, operator); byKey('function').status = 'done'; } } catch (err) { @@ -1407,7 +1504,9 @@ export async function runDeployStep( const created = await createLambdaAtEdge( credentials, accountId, - { region, distributionId, originDomain }, + { + region, distributionId, originDomain, operator, + }, ); if (created.status === 'ready') { lambdaVersionArn = created.versionArn; diff --git a/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js b/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js index d280b56c6..5b6ec8c5d 100644 --- a/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js +++ b/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js @@ -72,6 +72,7 @@ describe('edge-optimize support', () => { DescribeFunctionCommand: cfCommand('DescribeFunction'), PublishFunctionCommand: cfCommand('PublishFunction'), UpdateDistributionCommand: cfCommand('UpdateDistribution'), + TagResourceCommand: cfCommand('TagResource'), }, '@aws-sdk/client-iam': { IAMClient: function IAMClient(config) { @@ -415,7 +416,13 @@ describe('edge-optimize support', () => { }], })); const okRoleIam = { - GetRole: { Role: { Arn: 'arn:role', AssumeRolePolicyDocument: validTrust } }, + GetRole: { + Role: { + Arn: 'arn:role', + AssumeRolePolicyDocument: validTrust, + Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }], + }, + }, GetRolePolicy: { PolicyName: 'EdgeOptimizeLambdaLogging', PolicyDocument: '{}' }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {}, @@ -657,7 +664,7 @@ describe('edge-optimize support', () => { it('is idempotent when the origin already exists by id', async () => { cfSendStub.resolves({ - DistributionConfig: { Origins: { Quantity: 1, Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'x' }] } }, + DistributionConfig: { Origins: { Quantity: 1, Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'live.edgeoptimize.net' }] } }, ETag: 'etag-1', }); @@ -686,6 +693,23 @@ describe('edge-optimize support', () => { expect(cfSendStub.calledOnce).to.equal(true); }); + it('refuses to reuse an EdgeOptimize_Origin pointing at a foreign domain (not ours)', async () => { + cfSendStub.resolves({ + DistributionConfig: { Origins: { Quantity: 1, Items: [{ Id: 'EdgeOptimize_Origin', DomainName: 'someone-elses.example.com' }] } }, + ETag: 'etag-1', + }); + + let error; + try { + await edgeOptimize.createOrigin({}, 'E2EXAMPLE', 'dev.edgeoptimize.net'); + } catch (e) { + error = e; + } + + expect(error.message).to.include('was not created by LLM Optimizer'); + expect(error.message).to.include('setup manually'); + }); + it('patches the headers when the origin exists without them (self-heal)', async () => { cfSendStub.onFirstCall().resolves({ DistributionConfig: { @@ -804,7 +828,10 @@ describe('edge-optimize support', () => { }); it('updates and publishes when the function already exists', async () => { - cfSendStub.onFirstCall().resolves({ ETag: 'dev-etag' }); // DescribeFunction DEVELOPMENT + cfSendStub.onFirstCall().resolves({ + ETag: 'dev-etag', + FunctionSummary: { FunctionConfig: { Comment: 'EdgeOptimize agentic bot routing — managed by LLM Optimizer' } }, + }); // DescribeFunction DEVELOPMENT cfSendStub.onSecondCall().resolves({ ETag: 'updated-etag' }); // UpdateFunction cfSendStub.onThirdCall().resolves({}); // PublishFunction @@ -815,6 +842,50 @@ describe('edge-optimize support', () => { expect(cfSendStub.thirdCall.args[0].input.IfMatch).to.equal('updated-etag'); }); + it('tags a newly created function with the owner + createdBy tags', async () => { + cfSendStub.onFirstCall().rejects(Object.assign(new Error('nf'), { name: 'NoSuchFunctionExists' })); // DescribeFunction + cfSendStub.onSecondCall().resolves({ ETag: 'fn-etag', FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } }); // CreateFunction + cfSendStub.onThirdCall().resolves({}); // PublishFunction + cfSendStub.onCall(3).resolves({}); // TagResource + + await edgeOptimize.createCloudFrontFunction({}, 'origin-aem', 'E2EXAMPLE', null, undefined, 'jane@adobe.com'); + + const tagCall = cfSendStub.getCalls().find((c) => c.args[0].commandName === 'TagResource'); + expect(tagCall, 'TagResource should be called').to.not.equal(undefined); + expect(tagCall.args[0].input.Resource).to.equal('arn:cf-fn'); + expect(tagCall.args[0].input.Tags.Items).to.deep.include({ Key: 'ManagedBy', Value: 'adobe-llmo' }); + expect(tagCall.args[0].input.Tags.Items).to.deep.include({ Key: 'createdBy', Value: 'jane@adobe.com' }); + }); + + it('still succeeds when tagging the new function fails (best-effort)', async () => { + cfSendStub.onFirstCall().rejects(Object.assign(new Error('nf'), { name: 'NoSuchFunctionExists' })); // DescribeFunction + cfSendStub.onSecondCall().resolves({ ETag: 'fn-etag', FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } }); // CreateFunction + cfSendStub.onThirdCall().resolves({}); // PublishFunction + cfSendStub.onCall(3).rejects(new Error('AccessDenied: tagging not permitted')); // TagResource + + const result = await edgeOptimize.createCloudFrontFunction({}, 'origin-aem', 'E2EXAMPLE'); + + expect(result.created).to.equal(true); + expect(result.stage).to.equal('LIVE'); + }); + + it('refuses to reuse an existing function without our Comment marker (not ours)', async () => { + cfSendStub.onFirstCall().resolves({ + ETag: 'dev-etag', + FunctionSummary: { FunctionConfig: { Comment: 'some customer function' } }, + }); // DescribeFunction DEVELOPMENT + + let error; + try { + await edgeOptimize.createCloudFrontFunction({}, 'origin-aem', 'E2EXAMPLE'); + } catch (e) { + error = e; + } + + expect(error.message).to.include('was not created by LLM Optimizer'); + expect(cfSendStub.calledOnce).to.equal(true); // never reached UpdateFunction + }); + it('throws when defaultOriginId is missing', async () => { let error; try { @@ -1109,7 +1180,7 @@ describe('edge-optimize support', () => { }, ListCachePolicies: (cmd) => (cmd.input.Type === 'managed' ? { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-1' } }] } } - : { CachePolicyList: { Items: [{ CachePolicy: { Id: 'existing-eo', CachePolicyConfig: { Name: 'X-adobe-E2EXAMPLE' } } }] } }), + : { CachePolicyList: { Items: [{ CachePolicy: { Id: 'existing-eo', CachePolicyConfig: { Name: 'X-adobe-E2EXAMPLE', Comment: 'Cloned from Managed-X with Edge Optimize headers — managed by LLM Optimizer' } } }] } }), GetCachePolicy: { CachePolicy: { CachePolicyConfig: { Name: 'Managed-X', ParametersInCacheKeyAndForwardedToOrigin: {} } }, }, @@ -1124,6 +1195,32 @@ describe('edge-optimize support', () => { expect(lastCommand('CreateCachePolicy')).to.equal(undefined); // reused, not created }); + it('refuses to reuse a custom policy matching our clone name but lacking our marker (not ours)', async () => { + wireCloudFront({ + GetDistributionConfig: { + DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'managed-1' } }, + ETag: 'dist-etag', + }, + ListCachePolicies: (cmd) => (cmd.input.Type === 'managed' + ? { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-1' } }] } } + : { CachePolicyList: { Items: [{ CachePolicy: { Id: 'existing-eo', CachePolicyConfig: { Name: 'X-adobe-E2EXAMPLE', Comment: 'a customer policy' } } }] } }), + GetCachePolicy: { + CachePolicy: { CachePolicyConfig: { Name: 'Managed-X', ParametersInCacheKeyAndForwardedToOrigin: {} } }, + }, + }); + + let error; + try { + await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default'); + } catch (e) { + error = e; + } + + expect(error.message).to.include('was not created by LLM Optimizer'); + expect(lastCommand('CreateCachePolicy')).to.equal(undefined); + expect(lastCommand('UpdateDistribution')).to.equal(undefined); // never repointed the behavior + }); + it('handles a LEGACY behavior (ForwardedValues, no CachePolicyId)', async () => { wireCloudFront({ GetDistributionConfig: { @@ -1356,7 +1453,7 @@ describe('edge-optimize support', () => { it('creates the function (non-blocking) and returns provisioning when the role already exists', async () => { // Existing role + missing function: proceed to CreateFunction in the SAME call (unchanged). wireIam({ - GetRole: { Role: { Arn: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role' } }, + GetRole: { Role: { Arn: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {}, }); @@ -1379,7 +1476,7 @@ describe('edge-optimize support', () => { it('retries CreateFunction on role-propagation then succeeds', async () => { let createAttempts = 0; - wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); wireLambda({ GetFunctionConfiguration: () => notFound(), CreateFunction: () => { @@ -1400,7 +1497,7 @@ describe('edge-optimize support', () => { }); it('rethrows a non-role-propagation CreateFunction error', async () => { - wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); wireLambda({ GetFunctionConfiguration: () => notFound(), CreateFunction: () => Promise.reject(Object.assign(new Error('boom'), { name: 'SomethingElse' })), @@ -1416,7 +1513,7 @@ describe('edge-optimize support', () => { }); it('rethrows an InvalidParameterValue error with no message (not role propagation)', async () => { - wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); wireLambda({ GetFunctionConfiguration: () => notFound(), // name is InvalidParameterValueException but message is empty → `(message || '')` fallback, @@ -1439,7 +1536,7 @@ describe('edge-optimize support', () => { }); it('gives up after the retry budget on persistent role-propagation', async () => { - wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); wireLambda({ GetFunctionConfiguration: () => notFound(), CreateFunction: () => Promise.reject(Object.assign( @@ -1471,7 +1568,7 @@ describe('edge-optimize support', () => { }); it('rethrows an unexpected GetFunctionConfiguration error', async () => { - wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); wireLambda({ GetFunctionConfiguration: () => Promise.reject(Object.assign(new Error('throttled'), { name: 'TooManyRequestsException' })), }); @@ -1486,10 +1583,10 @@ describe('edge-optimize support', () => { }); it('returns provisioning (no mutation) while the function is still finalizing', async () => { - wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); wireLambda({ GetFunctionConfiguration: { - FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'InProgress', + FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'InProgress', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE', }, }); @@ -1501,10 +1598,10 @@ describe('edge-optimize support', () => { }); it('is idempotent: reuses the existing version when the function is idle', async () => { - wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); wireLambda({ GetFunctionConfiguration: { - FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful', + FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE', }, ListVersionsByFunction: { Versions: [{ Version: '$LATEST' }, { Version: '3', FunctionArn: 'arn:fn:3' }] }, }); @@ -1517,11 +1614,47 @@ describe('edge-optimize support', () => { expect(lastLambda('PublishVersion')).to.equal(undefined); // reused, not re-published }); + it('refuses to reuse an existing Active function whose exec-role is not ours (by role)', async () => { + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireLambda({ + GetFunctionConfiguration: { + FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful', Role: 'arn:aws:iam::120569600543:role/some-customer-role', + }, + }); + + let error; + try { + await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' }); + } catch (e) { + error = e; + } + + expect(error.message).to.include('was not created by LLM Optimizer'); + expect(lastLambda('PublishVersion')).to.equal(undefined); // never reused/published + }); + + it('refuses to modify an existing exec role that lacks our owner tag (not ours)', async () => { + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'team', Value: 'customer' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireLambda({ GetFunctionConfiguration: () => notFound() }); + + let error; + try { + await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' }); + } catch (e) { + error = e; + } + + expect(error.message).to.include('was not created by LLM Optimizer'); + const updatedTrust = iamSendStub.getCalls() + .find((c) => c.args[0].commandName === 'UpdateAssumeRolePolicy'); + expect(updatedTrust).to.equal(undefined); // never modified their role + }); + it('publishes a version when the function is idle but unpublished', async () => { - wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); wireLambda({ GetFunctionConfiguration: { - FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful', + FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE', }, ListVersionsByFunction: { Versions: [{ Version: '$LATEST' }] }, PublishVersion: { FunctionArn: 'arn:fn:1', Version: '1' }, @@ -1535,7 +1668,7 @@ describe('edge-optimize support', () => { }); it('treats a concurrent-create conflict as provisioning', async () => { - wireIam({ GetRole: { Role: { Arn: 'arn:role' } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); wireLambda({ GetFunctionConfiguration: () => notFound(), CreateFunction: () => Promise.reject(Object.assign(new Error('exists'), { name: 'ResourceConflictException' })), @@ -2241,7 +2374,7 @@ describe('edge-optimize support', () => { })); // IAM mock for an existing, correctly-configured role (roleExists + roleOk = true). const okRoleIam = (extra = {}) => ({ - GetRole: { Role: { Arn: 'arn:role', AssumeRolePolicyDocument: validTrust } }, + GetRole: { Role: { Arn: 'arn:role', AssumeRolePolicyDocument: validTrust, Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, GetRolePolicy: { PolicyName: 'EdgeOptimizeLambdaLogging', PolicyDocument: '{}' }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {}, @@ -2288,7 +2421,9 @@ describe('edge-optimize support', () => { GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'InProgress' } }, }); const readyLambda = () => ({ - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: 'arn:lambda:3', CodeSha256: 'sha' }] }, }); @@ -2470,7 +2605,9 @@ describe('edge-optimize support', () => { GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -2539,7 +2676,9 @@ describe('edge-optimize support', () => { GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -2594,7 +2733,9 @@ describe('edge-optimize support', () => { GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -2643,7 +2784,9 @@ describe('edge-optimize support', () => { UpdateDistribution: {}, // origin self-heal write (api-key header added) }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -2687,7 +2830,9 @@ describe('edge-optimize support', () => { GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -2733,7 +2878,9 @@ describe('edge-optimize support', () => { GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -2793,7 +2940,9 @@ describe('edge-optimize support', () => { GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'InProgress' } }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -2839,7 +2988,9 @@ describe('edge-optimize support', () => { GetDistribution: {}, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -2882,7 +3033,9 @@ describe('edge-optimize support', () => { GetDistribution: () => { throw new Error('get failed'); }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -3040,7 +3193,9 @@ describe('edge-optimize support', () => { }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -3191,7 +3346,9 @@ describe('edge-optimize support', () => { }, { // Active + idle, NO published version yet → createLambdaAtEdge must publish one. - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [] }, PublishVersion: { Version: '1', FunctionArn: lambdaVersionArn }, }, @@ -3237,7 +3394,7 @@ describe('edge-optimize support', () => { Statement: [{ Effect: 'Allow', Principal: { Service: 'lambda.amazonaws.com' }, Action: 'sts:AssumeRole' }], })); wire(readyDeployCf(), readyLambda(), { - GetRole: { Role: { Arn: 'arn:role', AssumeRolePolicyDocument: badTrust } }, + GetRole: { Role: { Arn: 'arn:role', AssumeRolePolicyDocument: badTrust, Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, GetRolePolicy: { PolicyName: 'EdgeOptimizeLambdaLogging', PolicyDocument: '{}' }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {}, @@ -3294,7 +3451,9 @@ describe('edge-optimize support', () => { GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'Deployed' } }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -3336,7 +3495,9 @@ describe('edge-optimize support', () => { GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'InProgress' } }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -3381,7 +3542,9 @@ describe('edge-optimize support', () => { }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -3427,7 +3590,9 @@ describe('edge-optimize support', () => { GetDistribution: { Distribution: { DomainName: 'd123.cloudfront.net', Status: 'InProgress' } }, }, { - GetFunctionConfiguration: { State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda' }, + GetFunctionConfiguration: { + State: 'Active', LastUpdateStatus: 'Successful', FunctionArn: 'arn:lambda', Role: 'arn:aws:iam::120569600543:role/edgeoptimize-origin-role-adobe-E2EXAMPLE123', + }, ListVersionsByFunction: { Versions: [{ Version: '3', FunctionArn: lambdaVersionArn, CodeSha256: 'sha' }] }, }, okRoleIam(), @@ -3886,7 +4051,7 @@ describe('edge-optimize support', () => { ListVersionsByFunction: { Versions: [] }, }, { - GetRole: { Role: { Arn: 'arn:role', AssumeRolePolicyDocument: encodeURIComponent('{"Statement":[]}') } }, + GetRole: { Role: { Arn: 'arn:role', AssumeRolePolicyDocument: encodeURIComponent('{"Statement":[]}'), Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, GetRolePolicy: throwNamed('NoSuchEntityException', 'no policy'), }, ); From 18f498e3ed85f1a0aa76b2e0dd0624b62b9837d0 Mon Sep 17 00:00:00 2001 From: Akshat Bhardwaj Date: Mon, 13 Jul 2026 22:32:46 +0530 Subject: [PATCH 3/3] fix(tokowaka-client): drop org externalId persistence field; cover ownership-guard branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The org-level externalId persistence (the edgeOptimizeConfig.externalId Joi field in data-access) is superseded by server-side derivation of the connector external ID from the org id in api-service (no persistence needed — see the CloudFront externalId change there), so the field is dead. Remove it. Also add the branch coverage the attributable-assume-role + resource-ownership code needs to keep tokowaka-client at 100% branches: - sessionNameFor with a valid operator (RoleSessionName = operator email) - tagCreatedBy falling back to 'unknown' when the operator sanitizes to empty - the function / cache-policy / Lambda reuse guards when the resource's ownership marker field (FunctionSummary / Comment / Role) is absent tokowaka-client: 970 passing, 100% branches. data-access: unchanged behavior, 2654 passing. Co-Authored-By: Claude Opus 4.8 --- .../src/models/site/config.js | 3 - .../test/cdn/cloudfront/index.test.js | 104 ++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/packages/spacecat-shared-data-access/src/models/site/config.js b/packages/spacecat-shared-data-access/src/models/site/config.js index ddeb63f06..d56f4d1f2 100644 --- a/packages/spacecat-shared-data-access/src/models/site/config.js +++ b/packages/spacecat-shared-data-access/src/models/site/config.js @@ -449,9 +449,6 @@ export const configSchema = Joi.object({ ) .optional(), opted: Joi.number().optional(), - // Cross-account connector external ID, persisted at the Organization level (the shared Config - // backs Site + Organization) so an org's domains/accounts reuse one connector role. - externalId: Joi.string().uuid().optional(), stagingDomains: Joi.array().items( Joi.object({ domain: Joi.string().required(), diff --git a/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js b/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js index 5b6ec8c5d..4b97e8e10 100644 --- a/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js +++ b/packages/spacecat-shared-tokowaka-client/test/cdn/cloudfront/index.test.js @@ -183,6 +183,20 @@ describe('edge-optimize support', () => { expect(error).to.be.an('error'); expect(error.message).to.include('no credentials'); }); + + it('uses the operator email as the RoleSessionName when provided (>= 2 valid chars)', async () => { + stsSendStub.resolves({ + Credentials: { AccessKeyId: 'A', SecretAccessKey: 'S', SessionToken: 'T' }, + }); + + await edgeOptimize.assumeConnectorRole({ + accountId: '120569600543', + externalId: 'ext', + operator: 'jane@adobe.com', + }); + + expect(stsSendStub.firstCall.args[0].input.RoleSessionName).to.equal('jane@adobe.com'); + }); }); describe('listDistributions', () => { @@ -886,6 +900,53 @@ describe('edge-optimize support', () => { expect(cfSendStub.calledOnce).to.equal(true); // never reached UpdateFunction }); + it('tags a new function with createdBy=unknown when the operator sanitizes to empty', async () => { + cfSendStub.onFirstCall().rejects(Object.assign(new Error('nf'), { name: 'NoSuchFunctionExists' })); // DescribeFunction + cfSendStub.onSecondCall().resolves({ ETag: 'fn-etag', FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:cf-fn' } } }); // CreateFunction + cfSendStub.onThirdCall().resolves({}); // PublishFunction + cfSendStub.onCall(3).resolves({}); // TagResource + + // '™©' is entirely stripped by the tag sanitizer -> falls back to 'unknown'. + await edgeOptimize.createCloudFrontFunction({}, 'origin-aem', 'E2EXAMPLE', null, undefined, '™©'); + + const tagCall = cfSendStub.getCalls().find((c) => c.args[0].commandName === 'TagResource'); + expect(tagCall.args[0].input.Tags.Items).to.deep.include({ Key: 'createdBy', Value: 'unknown' }); + }); + + it('refuses to reuse an existing function when DescribeFunction returns no FunctionSummary', async () => { + cfSendStub.onFirstCall().resolves({ ETag: 'dev-etag' }); // DescribeFunction: ETag but no FunctionSummary + + let error; + try { + await edgeOptimize.createCloudFrontFunction({}, 'origin-aem', 'E2EXAMPLE'); + } catch (e) { + error = e; + } + + expect(error.message).to.include('was not created by LLM Optimizer'); + expect(cfSendStub.calledOnce).to.equal(true); // never reached UpdateFunction + }); + + it('reuses our function and adopts the FunctionARN from the update response', async () => { + cfSendStub.onFirstCall().resolves({ + ETag: 'dev-etag', + FunctionSummary: { + FunctionMetadata: { FunctionARN: 'arn:old-fn' }, + FunctionConfig: { Comment: 'EdgeOptimize agentic bot routing — managed by LLM Optimizer' }, + }, + }); // DescribeFunction: ours, with an existing ARN + cfSendStub.onSecondCall().resolves({ + ETag: 'updated-etag', + FunctionSummary: { FunctionMetadata: { FunctionARN: 'arn:new-fn' } }, + }); // UpdateFunction returns a FunctionARN + cfSendStub.onThirdCall().resolves({}); // PublishFunction + + const result = await edgeOptimize.createCloudFrontFunction({}, 'origin-aem', 'E2EXAMPLE'); + + expect(result.created).to.equal(false); + expect(cfSendStub.secondCall.args[0].commandName).to.equal('UpdateFunction'); + }); + it('throws when defaultOriginId is missing', async () => { let error; try { @@ -1195,6 +1256,30 @@ describe('edge-optimize support', () => { expect(lastCommand('CreateCachePolicy')).to.equal(undefined); // reused, not created }); + it('refuses to reuse a custom policy matching our clone name that has no Comment (not ours)', async () => { + wireCloudFront({ + GetDistributionConfig: { + DistributionConfig: { DefaultCacheBehavior: { CachePolicyId: 'managed-1' } }, + ETag: 'dist-etag', + }, + ListCachePolicies: (cmd) => (cmd.input.Type === 'managed' + ? { CachePolicyList: { Items: [{ CachePolicy: { Id: 'managed-1' } }] } } + : { CachePolicyList: { Items: [{ CachePolicy: { Id: 'existing-eo', CachePolicyConfig: { Name: 'X-adobe-E2EXAMPLE' } } }] } }), // matches our clone name, but no Comment + GetCachePolicy: { + CachePolicy: { CachePolicyConfig: { Name: 'Managed-X', ParametersInCacheKeyAndForwardedToOrigin: {} } }, + }, + }); + + let error; + try { + await edgeOptimize.updateCacheSettings({}, 'E2EXAMPLE', 'default'); + } catch (e) { + error = e; + } + + expect(error.message).to.include('was not created by LLM Optimizer'); + }); + it('refuses to reuse a custom policy matching our clone name but lacking our marker (not ours)', async () => { wireCloudFront({ GetDistributionConfig: { @@ -1614,6 +1699,25 @@ describe('edge-optimize support', () => { expect(lastLambda('PublishVersion')).to.equal(undefined); // reused, not re-published }); + it('refuses to reuse an existing Active function whose config carries no Role (not ours)', async () => { + wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); + wireLambda({ + GetFunctionConfiguration: { + FunctionArn: 'arn:fn', State: 'Active', LastUpdateStatus: 'Successful', // no Role + }, + }); + + let error; + try { + await edgeOptimize.createLambdaAtEdge(creds, '120569600543', { distributionId: 'E2EXAMPLE' }); + } catch (e) { + error = e; + } + + expect(error.message).to.include('was not created by LLM Optimizer'); + expect(lastLambda('PublishVersion')).to.equal(undefined); + }); + it('refuses to reuse an existing Active function whose exec-role is not ours (by role)', async () => { wireIam({ GetRole: { Role: { Arn: 'arn:role', Tags: [{ Key: 'ManagedBy', Value: 'adobe-llmo' }] } }, UpdateAssumeRolePolicy: {}, PutRolePolicy: {} }); wireLambda({