> ## Documentation Index
> Fetch the complete documentation index at: https://connect-docs.supertab.co/llms.txt
> Use this file to discover all available pages before exploring further.

# CloudFront

> Publish RSL license and deploy CAP enforcement on AWS CloudFront.

Supertab Connect integrates with AWS CloudFront for two purposes: serving your RSL license at `/license.xml` on your own domain, using a CloudFront Function to rewrite the path, and enforcing the Crawler Authentication Protocol (CAP) with one Lambda\@Edge function that runs before the cache.

***

## Publishing RSL License

Your RSL license needs to be accessible at `https://yourdomain.com/license.xml`. CloudFront proxies this path to the Supertab Connect origin using a CloudFront Function for URI rewriting, a new origin, and a dedicated cache behavior.

### URI Rewrite Function

Create a function with runtime `cloudfront-js-2.0`. This runs on viewer request and rewrites the URI before CloudFront selects the origin. It belongs to the `/license.xml` behavior only — CAP enforcement uses no CloudFront Function.

```javascript theme={null}
function handler(event) {
  var request = event.request;
  var merchantURN = "YOUR_WEBSITE_URN";
  request.uri = "/merchants/systems/" + merchantURN + request.uri;
  return request;
}
```

Publish the function after saving.

Creating this function from the CLI or Terraform rather than the console needs `cloudfront:CreateFunction`, `cloudfront:ListFunctions`, `cloudfront:GetFunction`, `cloudfront:DescribeFunction`, `cloudfront:UpdateFunction` and `cloudfront:PublishFunction`. The CAP policy further down does not include them, because CAP enforcement creates no function.

### Origin

Add an origin to your distribution:

```
Origin domain:  api-connect.supertab.co
Protocol:       HTTPS only
Name:           supertab-connect-origin
```

### Cache Behavior

Create a cache behavior for `/license.xml`:

| Setting                 | Value                              |
| ----------------------- | ---------------------------------- |
| Path pattern            | `/license.xml`                     |
| Origin                  | `supertab-connect-origin`          |
| Viewer protocol policy  | Redirect HTTP to HTTPS             |
| Cache policy            | CachingOptimized                   |
| Origin request policy   | `AllViewerExceptHostHeader`        |
| Viewer request function | Your published CloudFront Function |

<Note>
  Use `AllViewerExceptHostHeader`, not `AllViewer`. `AllViewer` forwards the viewer's `Host` header, which stops the Supertab Connect origin from routing the request and produces a `502`.
</Note>

The `/license.xml` behavior must sit above the default `*` behavior in the behaviors list. Deployment takes 10–15 minutes after saving.

<Accordion title="Terraform Alternative">
  If you manage your distribution with Terraform, use this configuration instead of the manual steps above.

  ```hcl theme={null}
  resource "aws_cloudfront_function" "supertab_rewrite_license_path" {
    name    = "supertab-rewrite-license-path"
    runtime = "cloudfront-js-2.0"
    publish = true
    comment = "Rewrites /license.xml to the Supertab Connect URN path"

    code = <<-EOT
  function handler(event) {
    var request = event.request;
    var merchantURN = "YOUR_WEBSITE_URN";
    request.uri = "/merchants/systems/" + merchantURN + request.uri;
    return request;
  }
  EOT
  }

  resource "aws_cloudfront_distribution" "your_distribution" {
    # ... your existing config ...

    origin {
      domain_name = "api-connect.supertab.co"
      origin_id   = "supertab-connect-origin"

      custom_origin_config {
        http_port              = 80
        https_port             = 443
        origin_protocol_policy = "https-only"
        origin_ssl_protocols   = ["TLSv1.2"]
      }
    }

    ordered_cache_behavior {
      path_pattern     = "/license.xml"
      allowed_methods  = ["GET", "HEAD"]
      cached_methods   = ["GET", "HEAD"]
      target_origin_id = "supertab-connect-origin"

      forwarded_values {
        query_string = false
        headers      = ["Origin"]
        cookies { forward = "none" }
      }

      function_association {
        event_type   = "viewer-request"
        function_arn = aws_cloudfront_function.supertab_rewrite_license_path.arn
      }

      viewer_protocol_policy = "redirect-to-https"
      min_ttl                = 0
      default_ttl            = 86400
      max_ttl                = 31536000
      compress               = true
    }

    depends_on = [aws_cloudfront_function.supertab_rewrite_license_path]
  }
  ```
</Accordion>

***

## CAP Enforcement

CAP enforcement uses one Lambda\@Edge function running the Supertab Connect SDK, attached to the **viewer request** event of the behavior you want to protect. It does not require a CloudFront Function or a custom cache policy.

Viewer request fires before CloudFront's cache. The SDK handles every request on this behavior, including cache hits. Blocked responses are generated pre-cache, so CloudFront never stores them.

The Lambda is now on the path for all your traffic, not only licensed traffic. It fails open on its own errors, but a timeout, a throttle, or a bundle that fails to initialize returns a CloudFront error to the viewer. Deploy in `OBSERVE` first and watch Lambda Duration, Errors and Throttles in CloudWatch before switching to `ENFORCE`.

Lambda\@Edge functions must be deployed in **us-east-1**. CloudFront replicates the published version to edge locations from there.

<Note>
  Requires `@getsupertab/supertab-connect-sdk` **2.4.0 or later**. Earlier versions assume an origin-request trigger and return early for every request that does not carry an `x-license-auth` header — at viewer request that is every request, so the Lambda runs and enforces nothing. Check with `npm ls @getsupertab/supertab-connect-sdk`.
</Note>

### Prerequisites

You need Node.js 22+, npm, and the AWS CLI configured (`aws configure`, or `aws login` with AWS CLI 2.32.0 or later).

The deploying principal needs the permissions in the scoped policy below. The AWS-managed `PowerUserAccess` policy is not sufficient on its own: it does not grant the IAM role creation, policy attachment, or `iam:PassRole` permissions used here. CAP enforcement creates no CloudFront Function and no cache policy, so those permissions are not included.

<Accordion title="Required IAM policy (replace YOUR_DISTRIBUTION_ID)">
  ```json theme={null}
  {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "CLILogin",
        "Effect": "Allow",
        "Action": ["signin:AuthorizeOAuth2Access", "signin:CreateOAuth2Token"],
        "Resource": "*"
      },
      {
        "Sid": "IAMRole",
        "Effect": "Allow",
        "Action": ["iam:CreateRole", "iam:GetRole", "iam:AttachRolePolicy", "iam:PassRole"],
        "Resource": "arn:aws:iam::*:role/supertab-edge-verify*"
      },
      {
        "Sid": "AllowServiceLinkedRoleForLambdaEdge",
        "Effect": "Allow",
        "Action": "iam:CreateServiceLinkedRole",
        "Resource": "arn:aws:iam::*:role/aws-service-role/replicator.lambda.amazonaws.com/*",
        "Condition": {
          "StringEquals": {
            "iam:AWSServiceName": "replicator.lambda.amazonaws.com"
          }
        }
      },
      {
        "Sid": "LambdaEdge",
        "Effect": "Allow",
        "Action": [
          "lambda:CreateFunction",
          "lambda:UpdateFunctionCode",
          "lambda:UpdateFunctionConfiguration",
          "lambda:GetFunction",
          "lambda:GetFunctionConfiguration",
          "lambda:PublishVersion",
          "lambda:AddPermission",
          "lambda:EnableReplication*",
          "lambda:DisableReplication*"
        ],
        "Resource": "arn:aws:lambda:us-east-1:*:function:supertab-verify*"
      },
      {
        "Sid": "CloudFrontDistributionManage",
        "Effect": "Allow",
        "Action": [
          "cloudfront:GetDistribution",
          "cloudfront:GetDistributionConfig",
          "cloudfront:UpdateDistribution"
        ],
        "Resource": "arn:aws:cloudfront::*:distribution/YOUR_DISTRIBUTION_ID"
      },
      {
        "Sid": "CloudFrontInvalidate",
        "Effect": "Allow",
        "Action": ["cloudfront:CreateInvalidation", "cloudfront:GetInvalidation"],
        "Resource": "arn:aws:cloudfront::*:distribution/YOUR_DISTRIBUTION_ID"
      }
    ]
  }
  ```
</Accordion>

### Step 1: Build the Lambda Package

```bash theme={null}
mkdir supertab-verify && cd supertab-verify
npm init -y
npm install @getsupertab/supertab-connect-sdk@^2.4.0
npm install -D esbuild typescript @types/aws-lambda
```

Create `index.ts`:

```typescript theme={null}
import {
  SupertabConnect,
  EnforcementMode,
  defaultBotDetector,
} from "@getsupertab/supertab-connect-sdk";
import type { CloudFrontRequestEvent, CloudFrontRequestResult } from "aws-lambda";

export async function handler(
  event: CloudFrontRequestEvent
): Promise<CloudFrontRequestResult> {
  const { config, request } = event.Records[0].cf;

  // At viewer request, this legacy header is client controlled. SDK 2.4.0 prefers it
  // over the actual CloudFront host and URI, so remove it before URL reconstruction.
  if (config.eventType === "viewer-request") {
    delete request.headers["x-original-request-url"];
  }

  return SupertabConnect.cloudfrontHandleRequests(event, {
    apiKey: "YOUR_MERCHANT_API_KEY",       // from your Supertab Connect dashboard
    enforcement: EnforcementMode.OBSERVE,  // switch to ENFORCE once Step 4 passes
    botDetector: defaultBotDetector,        // optional: use the SDK's built-in bot detector
    analyticsEnabled: true,                // attempt one bot-analytics event per request
    // Lambda@Edge has no waitUntil, so analytics and usage recording are awaited before the
    // response. This explicitly uses the SDK's default 2-second background-work budget.
    backgroundWorkTimeoutMs: 2000,
  });
}
```

The SDK reads the trigger from the event, so the handler is the same at either one.

<Note>
  Lambda\@Edge does not support environment variables. This example embeds the API key in the deployment bundle. Treat `dist/function.zip` as a secret: do not commit it or copy it outside your account. If you use a runtime secret lookup instead, account for its permissions, latency, availability, and regional behavior. Rotate an embedded key by rebuilding, publishing a new version, and updating the behavior to use it.
</Note>

Add build scripts to `package.json`:

```json theme={null}
{
  "scripts": {
    "build": "esbuild index.ts --bundle --platform=node --target=node22 --outfile=dist/index.js --format=cjs",
    "package": "cd dist && zip -r function.zip index.js",
    "bundle": "npm run build && npm run package"
  }
}
```

Build:

```bash theme={null}
npm run bundle
```

This produces `dist/function.zip`.

### Step 2: Deploy to AWS

Deployment creates three resources: an **IAM execution role** (assumable by both `lambda.amazonaws.com` and `edgelambda.amazonaws.com`, with `AWSLambdaBasicExecutionRole` for CloudWatch logging), the **Lambda function** in `us-east-1` (required for Lambda\@Edge — CloudFront replicates it globally from there), and a **published, numbered version** that CloudFront is granted permission to invoke at the edge. Lambda\@Edge cannot use `$LATEST`.

This guide configures a 5-second timeout to bound viewer latency. AWS allows Lambda\@Edge viewer events to run for up to 30 seconds, but viewer-request functions are limited to 128 MB of memory.

The script below does all three and is idempotent — re-run it after any handler change and it publishes a new version.

<Accordion title="deploy.sh">
  ```bash theme={null}
  #!/bin/bash
  #
  # Creates or updates the Lambda@Edge function and publishes a numbered version.
  # Idempotent — re-run after any handler change.
  set -euo pipefail

  FUNCTION_NAME="supertab-verify"
  ROLE_NAME="supertab-edge-verify"
  REGION="us-east-1"   # Lambda@Edge must live here; CloudFront replicates from it
  TIMEOUT=5            # Guide choice to bound viewer latency; AWS allows up to 30s
  MEMORY=128           # viewer-request Lambda@Edge is capped at 128 MB

  if ! ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text); then
    echo "AWS CLI not configured. Run 'aws configure' first."
    exit 1
  fi
  if [ -z "$ACCOUNT_ID" ]; then
    echo "AWS CLI not configured. Run 'aws configure' first."
    exit 1
  fi

  echo "=== Deploying $FUNCTION_NAME (account $ACCOUNT_ID) ==="

  # 1. Execution role, assumable by Lambda and by the Lambda@Edge replicator.
  echo "Step 1/4 - IAM role..."
  TRUST_POLICY='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":["lambda.amazonaws.com","edgelambda.amazonaws.com"]},"Action":"sts:AssumeRole"}]}'

  ROLE_CREATED=false
  if aws iam get-role --role-name "$ROLE_NAME" > /dev/null 2>&1; then
    echo "  Role already exists: $ROLE_NAME"
  else
    aws iam create-role --role-name "$ROLE_NAME" \
      --assume-role-policy-document "$TRUST_POLICY" > /dev/null
    ROLE_CREATED=true
    echo "  Created $ROLE_NAME"
  fi

  # Re-applying a managed-policy attachment is safe and repairs an existing role
  # if the logging policy was removed or never attached.
  aws iam attach-role-policy --role-name "$ROLE_NAME" \
    --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
  echo "  Logging permissions attached"

  if [ "$ROLE_CREATED" = true ]; then
    echo "  Waiting 10s for IAM propagation..."
    sleep 10
  fi

  # 2. Function code and configuration.
  echo "Step 2/4 - Function code..."
  if aws lambda get-function --function-name "$FUNCTION_NAME" --region "$REGION" > /dev/null 2>&1; then
    aws lambda update-function-code --function-name "$FUNCTION_NAME" \
      --zip-file fileb://dist/function.zip --region "$REGION" > /dev/null
    aws lambda wait function-updated --function-name "$FUNCTION_NAME" --region "$REGION"
    # Re-apply the configuration so an older function uses this guide's timeout and
    # the viewer-request memory limit.
    aws lambda update-function-configuration --function-name "$FUNCTION_NAME" \
      --runtime nodejs22.x --timeout "$TIMEOUT" --memory-size "$MEMORY" \
      --region "$REGION" > /dev/null
    aws lambda wait function-updated --function-name "$FUNCTION_NAME" --region "$REGION"
    echo "  Updated: nodejs22.x, timeout ${TIMEOUT}s, memory ${MEMORY}MB"
  else
    aws lambda create-function --function-name "$FUNCTION_NAME" \
      --runtime nodejs22.x --handler index.handler \
      --role "arn:aws:iam::${ACCOUNT_ID}:role/${ROLE_NAME}" \
      --zip-file fileb://dist/function.zip \
      --timeout "$TIMEOUT" --memory-size "$MEMORY" --region "$REGION" > /dev/null
    aws lambda wait function-active --function-name "$FUNCTION_NAME" --region "$REGION"
    echo "  Function created"
  fi

  # 3. Published version — Lambda@Edge cannot run $LATEST.
  echo "Step 3/4 - Publishing version..."
  VERSION=$(aws lambda publish-version --function-name "$FUNCTION_NAME" \
    --region "$REGION" --query 'Version' --output text)
  echo "  Published version $VERSION"

  # 4. Let CloudFront fetch and invoke that version at edge locations.
  echo "Step 4/4 - Edge permissions..."
  for action in GetFunction InvokeFunction; do
    aws lambda add-permission --function-name "${FUNCTION_NAME}:${VERSION}" \
      --statement-id "cf-$(echo "$action" | tr '[:upper:]' '[:lower:]')-${VERSION}" \
      --action "lambda:${action}" --principal edgelambda.amazonaws.com \
      --region "$REGION" > /dev/null 2>&1 || true
  done

  echo ""
  echo "=== Done. Attach this ARN at viewer request in Step 3: ==="
  echo "  arn:aws:lambda:${REGION}:${ACCOUNT_ID}:function:${FUNCTION_NAME}:${VERSION}"
  ```
</Accordion>

Save it as `deploy.sh` in your `supertab-verify` directory and run it:

```bash theme={null}
chmod +x deploy.sh
./deploy.sh
```

Save the version ARN from the output — you need it in Step 3.

<Note>
  Prefer the console? Create the role with the Lambda + `edgelambda` trust policy shown in the script above, create the function (`nodejs22.x`, `us-east-1`, handler `index.handler`, timeout 5s, memory 128 MB), upload `dist/function.zip`, publish a numbered version, then grant `lambda:GetFunction` and `lambda:InvokeFunction` to the `edgelambda.amazonaws.com` principal on that version. If you update your handler later, publish a new version and update the behavior to use the new ARN.
</Note>

### Step 3: Attach the Lambda at Viewer Request

Edit the behavior for the path you want to protect — the default `*` behavior for all requests, or a specific path like `/articles/*`. Your `/license.xml` behavior is separate and sits above it, so the license itself never reaches the CAP Lambda.

Every request on the behavior you choose invokes the Lambda, including requests CloudFront would otherwise have served straight from cache. To reduce cost, attach the Lambda only to content paths.

| Setting                               | Value                                                                             |
| ------------------------------------- | --------------------------------------------------------------------------------- |
| Viewer request → CloudFront Functions | None. Remove any function associated here.                                        |
| Viewer request → Lambda\@Edge         | Published version ARN from Step 2, **Include body** off                           |
| Origin request → Lambda\@Edge         | None. Remove any CAP Lambda association here.                                     |
| Cache policy                          | Your existing policy. `CachingOptimized` is fine — no cache-key header is needed. |
| Origin request policy                 | Your existing policy. CAP no longer requires `AllViewerExceptHostHeader`.         |

Save and wait for the distribution to deploy (10–15 minutes).

<Warning>
  A behavior cannot combine a viewer-request Lambda\@Edge function with a CloudFront Function at either viewer request or viewer response. Do not move request-time authentication, redirects, or cache-key rewrites to origin request: origin-request code runs only on cache misses. Merge required request logic into the CAP viewer-request Lambda. For response logic, migrate it to a compatible Lambda\@Edge association or protect a separate behavior.
</Warning>

<Note>
  Check the distribution's **Default root object**. If your origin cannot serve `/` on its own — an S3 origin, for example — set it to `index.html`, with no leading slash, or requests for `/` reach a path the origin cannot answer.
</Note>

From the CLI instead, fetch the ETag and the config separately. Keep an unedited copy of `config.json`: re-applying it is how you back the change out.

```bash theme={null}
# ETag first, then the config. Fetched the other way round, a change made in between
# would be overwritten; this way the stale ETag makes the update fail instead.
ETAG=$(aws cloudfront get-distribution-config --id YOUR_DISTRIBUTION_ID \
  --query ETag --output text)
aws cloudfront get-distribution-config --id YOUR_DISTRIBUTION_ID \
  --query DistributionConfig --output json > config.json
```

In `config.json`, edit only the associations on the behavior you are protecting:

1. In `LambdaFunctionAssociations.Items`, remove the old CAP `origin-request` entry and add the new CAP `viewer-request` entry. Preserve unrelated origin-request, origin-response, and viewer-response Lambda associations.
2. Set `LambdaFunctionAssociations.Quantity` to the resulting number of items.
3. Remove viewer-request and viewer-response CloudFront Function entries from `FunctionAssociations`; neither can coexist with a viewer-request Lambda\@Edge function. Preserve required logic as described in the warning above.

If the behavior has no other Lambda associations or CloudFront Functions, the result is:

```json theme={null}
"LambdaFunctionAssociations": {
  "Quantity": 1,
  "Items": [
    { "LambdaFunctionARN": "YOUR_LAMBDA_VERSION_ARN", "EventType": "viewer-request", "IncludeBody": false }
  ]
},
"FunctionAssociations": { "Quantity": 0 }
```

`update-distribution` replaces the whole config, so send the edited file back with the ETag you saved:

```bash theme={null}
aws cloudfront update-distribution --id YOUR_DISTRIBUTION_ID \
  --if-match "$ETAG" --distribution-config file://config.json

aws cloudfront wait distribution-deployed --id YOUR_DISTRIBUTION_ID
```

<Accordion title="Terraform">
  ```hcl theme={null}
  resource "aws_cloudfront_distribution" "your_distribution" {
    # ... your existing config ...

    default_cache_behavior {
      # ... your existing config ...

      # Only one viewer-request association is allowed, so any function_association
      # on this behavior must be removed. Move required logic as described above.
      lambda_function_association {
        event_type   = "viewer-request"
        lambda_arn   = "YOUR_LAMBDA_VERSION_ARN" # published version, not $LATEST
        include_body = false
      }
    }
  }
  ```
</Accordion>

### Step 4: Verify

Four request shapes cover the behavior. Run them against the protected path.

The SDK's default detector treats a request with no `sec-ch-ua` and no `accept-language` as a bot, so check 1 sends a real browser header set — a bare `curl` is classified as a bot.

```bash theme={null}
# 1. Browser traffic.
curl -s -o /dev/null -w '%{http_code}\n' https://yourdomain.com/ \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36' \
  -H 'Accept-Language: en-US,en;q=0.9' \
  -H 'sec-ch-ua: "Chromium";v="140", "Google Chrome";v="140", "Not?A_Brand";v="24"'

# 2. An unlicensed crawler.
curl -si https://yourdomain.com/ -H 'User-Agent: GPTBot/1.0' | head -6

# 3. A valid license token passes through.
curl -s -o /dev/null -w '%{http_code}\n' https://yourdomain.com/ \
  -H "Authorization: License YOUR_LICENSE_TOKEN"

# 4. An invalid license token is blocked in every mode except DISABLED.
curl -si https://yourdomain.com/ -H 'Authorization: License in.valid.token' | head -6
```

| Check                 | `OBSERVE`                              | `ENFORCE`                        |
| --------------------- | -------------------------------------- | -------------------------------- |
| 1 — browser, no token | Passes through                         | Passes through                   |
| 2 — crawler, no token | Passes through; recording is attempted | `401`, `error="invalid_request"` |
| 3 — valid token       | Passes through                         | Passes through                   |
| 4 — invalid token     | `401`, `error="invalid_token"`         | `401`, `error="invalid_token"`   |

Blocked responses carry `WWW-Authenticate: License error="<code>", error_description="..."` and `Link: <https://yourdomain.com/license.xml>; rel="license"; type="application/rsl+xml"`, with a single line of plain text as the body. A token whose audience does not cover the requested URL is `403` `insufficient_scope`. A JWKS or verification-backend failure can produce `503` `server_error`, although a cached key may allow verification to continue. Analytics and usage-delivery failures are swallowed and do not produce that response.

Checks 1–3 return whatever status the cache or origin produces; they return `200` only if the test URL normally does. Check 2 is the one that depends on the mode. In `OBSERVE` it passes through and the SDK attempts to record the outcome for the dashboard; no result is added to the response headers.

<Note>
  Lambda\@Edge writes execution logs in the AWS Region where the function ran, which may be neither your own Region nor `us-east-1`. Use the Lambda\@Edge regional monitoring view, or look across CloudWatch Regions for a log group named `/aws/lambda/us-east-1.supertab-verify`.
</Note>

Responses generated by a viewer-request Lambda are not cached, so CAP configuration changes do not require an invalidation. If a pass-through request reaches a cached origin error, CloudFront's default error-cache duration is 10 seconds; wait for the configured error TTL or invalidate only the affected test path.

### Enforcement Modes and Analytics

Set `enforcement` in the handler options:

| Mode                | Behavior                                                                                      |
| ------------------- | --------------------------------------------------------------------------------------------- |
| `DISABLED`          | Skip verification entirely — all requests pass through                                        |
| `OBSERVE` (default) | Verify tokens and attempt to record outcomes; block invalid tokens, never block a missing one |
| `ENFORCE`           | Also block a bot request that carries no license token                                        |

Start in `OBSERVE` while you validate the integration, then move to `ENFORCE` when you are ready to block.

<Warning>
  At viewer request the Lambda sees unlicensed traffic, so bot detection applies to requests that carry no token at all. `ENFORCE` therefore blocks token-less crawlers — which the older origin-request setup let through, because the Lambda never saw them. Review your bot traffic in the dashboard before switching.
</Warning>

In this example, the mode is compiled into the bundle. Changing it means rebuilding, publishing a new version, and updating the behavior to use that version. Plan on 10–15 minutes of distribution deployment for each switch.

Analytics is off by default. `analyticsEnabled: true` attempts to send one analytics event per handled request for **bot classification** and traffic reporting. An event can be lost when delivery fails or the background-work budget expires.

Lambda\@Edge has no `waitUntil`, so the SDK waits for analytics and usage recording before returning, with a default 2-second budget. Set `backgroundWorkTimeoutMs` to a positive number of milliseconds to change it; missing, invalid, or non-positive values use 2000 ms. Set it to `Infinity` only when you deliberately want no background-work deadline. The budget is measured from handler entry and cancels analytics or usage delivery still in flight at the deadline. It does not cancel token verification or JWKS fetching, and it does not guarantee that the entire invocation finishes within that time.

<Note>
  CloudFront adds the `cloudfront-viewer-*` headers after the viewer request event, so at this trigger they do not exist and the analytics fields they populate — request country, ASN, TLS fingerprint, HTTP protocol — are recorded as null. User agent and client IP are available at viewer request, so bot classification and IP-based enrichment are unaffected.
</Note>

For requests the SDK allows, analytics records `status_code: null` and `status_source: "unobserved"` because the viewer-request handler returns before CloudFront or the origin produces a response. Responses blocked by the SDK have an observed status.

If dashboard events are missing, temporarily set `debug: true`, redeploy, and inspect the regional Lambda\@Edge logs. Check the ingest `accepted` verdict as well as background-timeout messages: an HTTP `200` from ingest can still report that the event was discarded. Turn debug logging off after troubleshooting.

### Cost and Latency

Every request invokes the Lambda, cache hits included, so Lambda\@Edge charges scale with total request volume rather than cache-miss rate. Latency depends on factors including cold starts, token verification, JWKS cache state, and analytics or usage delivery. Measure the protected paths from representative viewer locations and monitor regional Lambda Duration before enabling enforcement.

### Migrating from the Origin-Request Setup

Earlier versions of this page wired CAP as a viewer-request CloudFront Function stamping `x-license-auth`, plus an origin-request Lambda that only processed stamped requests. One viewer-request Lambda replaces both. Because only stamped requests reached the SDK, a crawler presenting no token could not be blocked in any mode, and analytics was never wired for CloudFront at all.

On the protected behavior:

1. Rebuild and redeploy the handler on SDK 2.4.0 or later, in `OBSERVE` (Steps 1 and 2). The existing function is updated in place and its timeout reset from 10 seconds to this guide's 5-second setting.
2. Keep the unedited `config.json` from Step 3 so you can restore the previous associations if you need to back out.
3. Remove the CAP filtering CloudFront Function from viewer request, and delete the function once no behavior references it.
4. Remove only the old CAP Lambda\@Edge association from origin request. Preserve unrelated Lambda associations. Leaving the CAP Lambda attached at both triggers runs the SDK twice on licensed requests.
5. Attach the new published version ARN at viewer request (Step 3).
6. Switch the behavior's cache policy back to a standard one such as `CachingOptimized`. The custom policy with `x-license-auth` in the cache key is no longer needed; delete it once no behavior references it.
7. Re-run Step 4, then move to `ENFORCE` once the recorded traffic looks right. No invalidation is required for Lambda-generated responses; invalidate only specific origin paths if you also need fresh origin content.

The origin request policy can stay as it is. CAP no longer requires `AllViewerExceptHostHeader`: at viewer request the Lambda reads the viewer's headers directly. The example handler removes `x-original-request-url` before calling SDK 2.4.0 because that legacy header is client controlled at this trigger.

Deleting the legacy CloudFront Function or cache policy during migration requires additional CloudFront management permissions, such as `cloudfront:DescribeFunction`, `cloudfront:DeleteFunction`, `cloudfront:GetCachePolicy`, and `cloudfront:DeleteCachePolicy`. They are not part of the CAP deployment policy above because the new setup creates neither resource.

***

## Purge Cached License

After you publish a new license version, CloudFront may keep serving the cached copy for up to 24 hours. Invalidate the license path to force a refresh.

In the console, open your distribution → **Invalidations** → **Create invalidation**, and enter:

```
/merchants/systems/YOUR_WEBSITE_URN/license.xml
```

Or via the CLI:

```bash theme={null}
aws cloudfront create-invalidation \
  --distribution-id YOUR_DISTRIBUTION_ID \
  --paths "/merchants/systems/YOUR_WEBSITE_URN/license.xml"
```

Invalidation completes in 5–15 minutes. Confirm the update at `https://yourdomain.com/license.xml`.

***

## Related Docs

<CardGroup cols={2}>
  <Card title="Deploy in Your CDN" icon="shield" href="/guides/deploy-cdn">
    CDN-agnostic guide covering RSL serving, CAP enforcement, and robots.txt.
  </Card>

  <Card title="Other CDNs" icon="server" href="/reference/others">
    Generic CDN patterns for platforms not listed above.
  </Card>
</CardGroup>
