Skip to main content
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.
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:

Cache Behavior

Create a cache behavior for /license.xml:
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.
The /license.xml behavior must sit above the default * behavior in the behaviors list. Deployment takes 10–15 minutes after saving.
If you manage your distribution with Terraform, use this configuration instead of the manual steps above.

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.
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.

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.

Step 1: Build the Lambda Package

Create index.ts:
The SDK reads the trigger from the event, so the handler is the same at either one.
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.
Add build scripts to package.json:
Build:
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.
Save it as deploy.sh in your supertab-verify directory and run it:
Save the version ARN from the output — you need it in Step 3.
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.

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. Save and wait for the distribution to deploy (10–15 minutes).
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.
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.
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.
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:
update-distribution replaces the whole config, so send the edited file back with the ETag you saved:

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.
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.
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.
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: Start in OBSERVE while you validate the integration, then move to ENFORCE when you are ready to block.
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.
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.
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.
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 → InvalidationsCreate invalidation, and enter:
Or via the CLI:
Invalidation completes in 5–15 minutes. Confirm the update at https://yourdomain.com/license.xml.

Deploy in Your CDN

CDN-agnostic guide covering RSL serving, CAP enforcement, and robots.txt.

Other CDNs

Generic CDN patterns for platforms not listed above.