/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 athttps://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 runtimecloudfront-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.
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./license.xml behavior must sit above the default * behavior in the behaviors list. Deployment takes 10–15 minutes after saving.
Terraform Alternative
Terraform Alternative
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 inOBSERVE 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.
Required IAM policy (replace YOUR_DISTRIBUTION_ID)
Required IAM policy (replace YOUR_DISTRIBUTION_ID)
Step 1: Build the Lambda Package
index.ts:
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.package.json:
dist/function.zip.
Step 2: Deploy to AWS
Deployment creates three resources: an IAM execution role (assumable by bothlambda.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.
deploy.sh
deploy.sh
deploy.sh in your supertab-verify directory and run it:
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).
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.config.json: re-applying it is how you back the change out.
config.json, edit only the associations on the behavior you are protecting:
- In
LambdaFunctionAssociations.Items, remove the old CAPorigin-requestentry and add the new CAPviewer-requestentry. Preserve unrelated origin-request, origin-response, and viewer-response Lambda associations. - Set
LambdaFunctionAssociations.Quantityto the resulting number of items. - 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.
update-distribution replaces the whole config, so send the edited file back with the ETag you saved:
Terraform
Terraform
Step 4: Verify
Four request shapes cover the behavior. Run them against the protected path. The SDK’s default detector treats a request with nosec-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.Enforcement Modes and Analytics
Setenforcement in the handler options:
Start in
OBSERVE while you validate the integration, then move to ENFORCE when you are ready to block.
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.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 stampingx-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:
- 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. - Keep the unedited
config.jsonfrom Step 3 so you can restore the previous associations if you need to back out. - Remove the CAP filtering CloudFront Function from viewer request, and delete the function once no behavior references it.
- 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.
- Attach the new published version ARN at viewer request (Step 3).
- Switch the behavior’s cache policy back to a standard one such as
CachingOptimized. The custom policy withx-license-authin the cache key is no longer needed; delete it once no behavior references it. - Re-run Step 4, then move to
ENFORCEonce 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.
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:https://yourdomain.com/license.xml.
Related Docs
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.