Skip to content

feat(llmo): add CloudFront wizard read endpoints#2654

Open
ABHA61 wants to merge 38 commits into
mainfrom
feat/llmo-edge-optimize-cloudfront-wizard
Open

feat(llmo): add CloudFront wizard read endpoints#2654
ABHA61 wants to merge 38 commits into
mainfrom
feat/llmo-edge-optimize-cloudfront-wizard

Conversation

@ABHA61

@ABHA61 ABHA61 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

What

Adds the read-only backend endpoints that power the CloudFront BYOCDN "Deploy routing in CloudFront" wizard in LLMO UI. The api-service assumes the customer's cross-account connector role server-side (no AWS credentials in the browser) and performs read-only CloudFront calls via that role.

Endpoints (all POST, all in INTERNAL_ROUTES — admin/IMS-only, not exposed to S2S)

  • POST /sites/:siteId/llmo/edge-optimize/connect — verify the connector role is assumable; returns { connected } so the wizard can poll while the customer is still creating the role.
  • POST /sites/:siteId/llmo/edge-optimize/distributions — list the account's CloudFront distributions.
  • POST /sites/:siteId/llmo/edge-optimize/prerequisites — confirm connector-role assumability + CloudFront read access.
  • POST /sites/:siteId/llmo/edge-optimize/origins — read a distribution's origins (and detect whether an Edge Optimize origin already exists).
  • POST /sites/:siteId/llmo/edge-optimize/behaviors — read a distribution's default + ordered cache behaviors.

New support module src/support/edge-optimize.js: assumeConnectorRole (STS AssumeRole with the per-session external ID), listCloudFrontDistributions, getDistributionConfig. Adds @aws-sdk/client-sts and @aws-sdk/client-cloudfront.

Stacked on #2642

This branch was cut from the bootstrap-url branch (#2642), so its diff also includes getEdgeOptimizeBootstrapUrl. Merge #2642 first (then this rebases down to just the read endpoints), or this PR can supersede it.

⚠️ TEMP testing defaults — MUST be removed before prod

Carried from #2642 (marked with TODO in getEdgeOptimizeBootstrapUrl):

  • EDGE_OPTIMIZE_TEMPLATE_BUCKET defaults to llmo-edgeoptimize-cf-template
  • EDGE_OPTIMIZE_TRUSTED_PRINCIPAL_ARN defaults to arn:aws:iam::682033462621:root (dev) — security-sensitive; must be set to Adobe's backend role ARN in prod.

Deferred (next phases)

Mutation steps are not in this PR: cache-policy detect/apply, Lambda@Edge, distribution associations, verify, revoke (these create/modify CloudFront resources and the long-running provisioning will be async).

Tests

  • Support module: 11 (mocked AWS SDK)
  • Controller edge-optimize: 69 (happy path; 400 invalid account / missing externalId / missing distributionId / AWS failure; 404 site-not-found; 403 no-access; 403 not-admin)
  • Routes + required-capabilities: green
  • OpenAPI documented; docs:lint valid

🤖 Generated with Claude Code

Akash Bhardwaj and others added 16 commits June 19, 2026 01:03
POST /sites/:siteId/llmo/edge-optimize-bootstrap-url returns a CloudFormation
quick-create URL with a server-side presigned template URL, so a customer can
create the cross-account Edge Optimize connector role in their own AWS account
without a public S3 bucket and without any S3 access of their own. Presigning is
done with the service execution role.

Includes route + capability registration, OpenAPI spec, and unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The getRouteHandlers "segregates static and dynamic routes" test asserts the
exact set of routes; add the new dynamic route to the expected list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hardcode EDGE_OPTIMIZE_TEMPLATE_BUCKET and EDGE_OPTIMIZE_TRUSTED_PRINCIPAL_ARN
fallbacks so the dev/ci branch deploy returns a quick-create URL before those
env vars are wired into Vault/secrets. Marked TEMPORARY / TODO REMOVE —
revert before merge/prod (values must come from env config).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p-url' into feat/llmo-edge-optimize-bootstrap-url
Use llmo-edgeoptimize-cf-template (in 682033462621, where the service deploys
and signs) so the dev role reads it same-account; stage customer fetches via
the presigned URL. Still TEMPORARY / TODO REMOVE before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p-url' into feat/llmo-edge-optimize-bootstrap-url
…ault

The TEMPORARY hardcoded EDGE_OPTIMIZE_TEMPLATE_BUCKET default makes the
bucket always set, so the 'not configured' guard can no longer be hit via
an empty env. Exercise the same guard via the missing S3 client instead.
TODO: restore the empty-bucket variant when the temp default is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tighten the default lifetime of the bootstrap template presigned URL from
1h to 15m. The customer opens the quick-create link immediately, so a
shorter TTL shrinks the leak window. A leaked URL only grants GetObject on
the single template object until expiry; still override via
EDGE_OPTIMIZE_PRESIGN_TTL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(Phase 2)

Backend for the CloudFront 'Deploy routing' wizard's read steps. The
api-service assumes the customer's cross-account connector role server-side
(no AWS creds in the browser):

- New src/support/edge-optimize.js: assumeConnectorRole (STS AssumeRole with
  the per-session external ID) + listCloudFrontDistributions.
- POST /sites/:siteId/llmo/edge-optimize/connect - verifies the role is
  assumable (returns { connected } so the UI can poll while the customer
  creates the role); POST .../edge-optimize/distributions - lists the
  account's CloudFront distributions. Both gated by site access + LLMO admin
  and added to INTERNAL_ROUTES (not exposed to S2S).
- Adds @aws-sdk/client-sts and @aws-sdk/client-cloudfront.
- Unit tests for the support module (mocked SDK) and both handlers; route +
  capability lists updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the already-shipped connect and distributions endpoints plus the
new read-only prerequisites, origins, and behaviors endpoints for the
CloudFront "Deploy routing" wizard. Adds shared connector/distribution
request schemas and per-endpoint response definitions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ints (Phase 2)

Add three read-only CloudFront wizard endpoints, mirroring the existing
connect/distributions handlers (12-digit accountId + externalId validation,
site/access/LLMO-admin gate, assumed-role calls, badRequest on failure):

- POST /sites/:siteId/llmo/edge-optimize/prerequisites -> checkEdgeOptimizePrerequisites
  reports connectorRole + cloudFrontRead checks (ok/false + detail, never 500)
- POST /sites/:siteId/llmo/edge-optimize/origins -> getEdgeOptimizeOrigins
  returns origins + hasEdgeOptimizeOrigin detection
- POST /sites/:siteId/llmo/edge-optimize/behaviors -> getEdgeOptimizeBehaviors
  returns default + ordered cache behaviors

Adds getDistributionConfig() support fn (GetDistributionConfigCommand) and
unit tests for the support fn, controller handlers, and route registration.
All three routes added to INTERNAL_ROUTES (admin/IMS-only, not S2S).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ABHA61 ABHA61 changed the title Feat/llmo edge optimize cloudfront wizard feat(llmo): add CloudFront wizard read endpoints Jun 20, 2026
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.73913% with 69 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/support/edge-optimize.js 95.24% 64 Missing ⚠️
src/controllers/llmo/llmo.js 99.32% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

Akash Bhardwaj and others added 2 commits June 20, 2026 19:45
Sync per-step endpoints that assume the customer connector role server-side
and perform one CloudFront write each (no AWS creds in the browser):

- create-origin: add the EdgeOptimize_Origin (env EDGE_OPTIMIZE_ORIGIN_DOMAIN,
  default dev.edgeoptimize.net) via UpdateDistribution (ETag IfMatch).
- create-function: create/update + publish the edgeoptimize-routing CF Function
  (bot-routing JS ported from the standalone wizard).
- apply-cache: add EO headers to the behavior's custom cache policy (common
  path; legacy ForwardedValues / managed-policy clone left as TODO).
- create-lambda: create exec role (bounded IAM-propagation retry) + the
  edgeoptimize-origin Lambda@Edge and publish a version.
- apply-associations: wire the function (viewer-request) + Lambda (origin-
  request/response) onto the selected behavior.
- verify: server-side bot-vs-human probe; passed requires x-edgeoptimize-request-id
  (x-edgeoptimize-fo = failover, not success).

Adds @aws-sdk/client-iam + @aws-sdk/client-lambda. All AWS ops use ETag
read-modify-write. Embedded function/Lambda code ported verbatim from the
connect-aws-wizard; Lambda code inlined per the helix-deploy bundling rule.
Mocked-SDK unit tests for all 6 support fns + handlers; routes/capabilities
+ OpenAPI updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt-wizard' into feat/llmo-edge-optimize-cloudfront-wizard
The Default(*) behavior commonly uses an AWS-managed cache policy, which
cannot be updated (UpdateCachePolicy -> 'update is not allowed for this
policy'). applyEdgeOptimizeCacheHeaders now ports the full standalone-wizard
logic with all three scenarios:
- legacy (ForwardedValues, no CachePolicyId): add EO headers there + MinTTL 0
- custom policy: UpdateCachePolicy to add EO headers (existing path)
- managed policy: CLONE into a custom edgeoptimize-cache policy with the EO
  headers, then repoint the behavior to it (idempotent by name)

Adds GetCachePolicy/ListCachePolicies/CreateCachePolicy. Support tests
rewritten to dispatch by command name and cover all three scenarios.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Akash Bhardwaj and others added 2 commits June 22, 2026 15:00
The create-origin step created the EdgeOptimize_Origin without its custom
headers, so the routing function's request could not authenticate to Edge
Optimize or resolve the customer host - Verify never returned an
x-edgeoptimize-request-id.

- createEdgeOptimizeOrigin now sets x-edgeoptimize-api-key (site EO API key),
  x-forwarded-host (customer host), and optional x-edgeoptimize-fetcher-key,
  mirroring the standalone wizard + CloudFormation installer.
- Self-heals: an origin created header-less by the earlier version is patched
  in place on re-run (returns updated: true).
- Handler derives both server-side - api key from the tokowaka metaconfig
  (apiKeys[0]), forwarded host from calculateForwardedHost(site.baseURL) - so
  no new UI input; gateEdgeOptimizeWizard now returns the site to avoid a
  second fetch.
- Verify: documented the prod TODO (probe the customer's real domain, not the
  *.cloudfront.net domain) - behavior unchanged for dev testing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tighten the dev-only default for EDGE_OPTIMIZE_TRUSTED_PRINCIPAL_ARN from the
whole dev account (arn:aws:iam::682033462621:root) to the exact assuming
identity - the spacecat-api-service Lambda execution role
(arn:aws:iam::682033462621:role/spacecat-role-lambda-generic) - shrinking the
blast radius of the connector-role trust. No AWS-side change needed; the
assuming identity is already that role. Prod must still set this via env to the
prod execution role ARN (no in-code default) - tracked in the punch list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Akash Bhardwaj and others added 3 commits June 22, 2026 16:24
…oy orchestrator

Add POST /sites/:siteId/llmo/edge-optimize/deploy: a single endpoint the
wizard FE calls once then polls (~30s). Each call advances origin → function
→ cache → lambda → associate → verify as far as it safely can (well under the
~60s gateway timeout) and returns per-step status. Safe to call repeatedly:

- function step gated on DescribeFunction(Stage:LIVE) so it never re-publishes
- associate step gated on the target behavior already referencing our CF
  function + Lambda so it never re-issues UpdateDistribution
- origin + cache reuse the existing idempotent support fns
- lambda kicks off / polls without re-creating; sequence pauses while it
  provisions and resumes on the next poll
- verify is best-effort: in_progress (never error) until CloudFront propagates
- a step that throws is marked error on its own row; the response is still 200
  so the FE shows the failure and a re-poll retries idempotently

Orchestration lives in runEdgeOptimizeDeployStep (support/edge-optimize.js,
unit-testable without HTTP); the deployEdgeOptimize handler does validation +
gate + metaconfig/forwarded-host + a single assumeConnectorRole, then delegates.
Wires the route into routes/index.js + required-capabilities INTERNAL_ROUTES,
and the OpenAPI path + request schema. Adds support-fn and handler tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The one-time setupEsmock before() hook in edge-optimize.test.js exceeded the
30s hook timeout under the full CI suite (12k+ tests + nyc coverage + heap
pressure), failing the build even though it completes in ~1s locally. The
orchestrator's added code + tests (and a main merge) pushed cumulative suite
memory over the edge. Give the one-time hook generous headroom so suite growth
can't flake the whole build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Akash Bhardwaj and others added 3 commits June 22, 2026 21:33
The orchestrator only called createEdgeOptimizeLambda when the function did NOT
exist; once it existed (Pending -> Active) it merely status-checked. But
createEdgeOptimizeLambda is the only thing that PUBLISHES the numbered version
(Active + no version -> PublishVersion), and readiness requires a published
version -> the Deploy step hung at 'Lambda@Edge is still provisioning' forever.

Fix: call createEdgeOptimizeLambda on EVERY not-ready poll (it is idempotent +
non-blocking: creates when missing, no-ops while Pending, publishes once Active).
Added a unit test for the publish-on-Active path; updated the 'holds while
Pending' test (createEdgeOptimizeLambda now runs but still does not
CreateFunction/PublishVersion while Pending).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nt-wizard' into feat/llmo-edge-optimize-cloudfront-wizard
Comment thread docs/openapi/api.yaml
$ref: './llmo-api.yaml#/site-llmo-edge-optimize-config'
/sites/{siteId}/llmo/edge-optimize-config/stage:
$ref: './llmo-api.yaml#/site-llmo-edge-optimize-config-stage'
/sites/{siteId}/llmo/edge-optimize-bootstrap-url:

@nit23uec nit23uec Jun 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets have the namespace as: /sites/{siteId}/llmo/onboarding/cloudfront/
with the APIs as:
/bootstrap-url
/connect
/distributions, etc.

we wouldn't want to keep it limited to edgeoptimize as log forwarding will also use it.

@@ -0,0 +1,1346 @@
/*
* Copyright 2026 Adobe. All rights reserved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets move it to spacecat-shared.
we can have a new package spacecat-shared-aws-client.
or use tokowaka-client in short term before moving it to a new pkg- whatever suits you.


return `import cf from 'cloudfront';

function handler(event) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we can fetch this code from https://github.com/adobe/llmo-code-samples/tree/main/optimize-at-edge
it would keep it all at single place to manage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants