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

> ## Agent Instructions
> Publishers integrate one API: GET https://api.shftd2.com/v1/fragment?partner_id=<account id>&page_url=<page URL>, called from their edge for AI-agent requests only, with the API key in the X-API-Key header, never in the query string.
> A 200 carries the card. A 204 means no card: serve the page unchanged.
> The management API (https://api.shftd2.com/v1/me, /v1/sites, /v1/cards, /v1/keys) is optional tooling for scripting setup. It takes its own management token (Authorization: Bearer ds_mgmt_…). Its limits (120 requests and 60 writes per minute per token) do not apply to the delivery API.
> Delivery logs reach doubleshift only through direct delivery logs (POST https://logs.shftd2.com/v1/beacon with a logs token; the edge library with a logsToken is its provided client, and a custom sender may use the same endpoint) or a Cloudflare Logpush job configured with doubleshift. Otherwise doubleshift sees only its own API activity.

# Amazon CloudFront

> Release candidate. Kit 0.1.0-rc.3, qualified on 28 October 2026 with the doubleshift API and reads by AI assistants. Install it with doubleshift, on one cache behavior first.

> [!WARNING]
>
> **Release candidate.** Kit `0.1.0-rc.3` was qualified on 28 October 2026 with the doubleshift API and reads by AI
> assistants. Install it with doubleshift, on one cache behavior first.

The kit adds two pieces to your AWS account and attaches them to the cache behavior that serves your article pages.
Human requests are never sent to the composer, but they run under the kit's cache and origin request policies:
see [What changes on your distribution](#what-changes-on-your-distribution). Rules every edge keeps are on
[Choose your edge](https://docs.shftd2.com/integrate.md).

## What the kit installs

| Piece | What it does |
| - | - |
| CloudFront Function `<prefix>-viewer-request`, on viewer request | Removes every `x-ds-*` header. Sends a `GET` with an AI-agent `User-Agent` to the composer, with a cache key unique to that request. Leaves every other request untouched. |
| Lambda `<prefix>-composer`, behind a function URL (`AWS_IAM`) | Reads the page through your distribution as a normal visitor, runs the doubleshift edge library to add the card and the disclosure, and returns the page. |

`<prefix>` is the template parameter `NamePrefix`, `doubleshift` by default. The template also creates the composer's
role, an origin access control, invoke permissions limited to your distribution, and two policies. It creates no
distribution.

## Before you start

* Steps 1 to 3 of the [Quickstart](https://docs.shftd2.com/quickstart.md). Use a delivery key, and a logs token if you send delivery records,
  dedicated to this integration.
* Your distribution ID and the cache behavior that serves your article HTML: `default` or its path pattern.
* Node.js 20 or later, the AWS CLI v2, the AWS SAM CLI, and an AWS identity that can create a Lambda function and its
  role, CloudFront functions and policies, and update the distribution.
* A firewall that admits the [AI agents](https://docs.shftd2.com/api/agents.md) and the composer's read, `User-Agent`
  `doubleshift-edge-cloudfront/<kit version>`. The kit's `waf/allow-agents.rule.json` is an example AWS WAF allow rule
  for both, to place before your blocking rules; the kit never applies it. A rule on the `User-Agent` admits anyone
  who claims to be an agent.

## What changes on your distribution

`attach` prints the behavior's inventory and the diff before it writes anything. On that behavior only, it adds the
origin `doubleshift-composer` if absent, puts the kit's two policies in place of yours, and associates the function.

It refuses, and writes nothing, when the behavior has a function on viewer request already, a function on viewer
response, a Lambda\@Edge on any event, or a response headers policy. Move such logic first.

| Setting | Kit's policies |
| - | - |
| `MinTTL` | `0`, required: a positive minimum makes CloudFront store the agent page despite `private, no-store`. A behavior that needs one is not covered. |
| `DefaultTTL`, `MaxTTL` | Template parameters: your current values. |
| Cache key | `x-ds-request-id`, empty for humans, so no agent response is reused; up to 9 headers of yours (`ExtraCacheKeyHeaders`); the whole query string; no cookie. |
| Sent to your origin | The cache key, `Accept`, `Accept-Language`, the kit's `x-ds-*` headers, 1 header of yours (`ExtraOriginRequestHeaders`); no cookie; not the visitor's `Host`. |

The 9 and the 1 are what CloudFront's default quota of 10 headers per policy leaves. Your origin gets the origin
domain as `Host`: the composer's function URL needs its own, so the template refuses `Host` in both parameters and
`attach` refuses a kit policy that forwards it. When your policies forward `Host`, cookies or other
headers, `attach` lists what your origin would lose and applies only with `--accept-policy-changes`. An origin that
needs the visitor's `Host` or cookies is not covered.

Cached copies of URLs with a query string may be fetched once more from your origin.

## Install

**1. Get the kit**

Download the kit and check the archive's SHA-256 before unpacking it: if the check fails, stop. Once unpacked,
compare the SHA-256 of `manifest.json` with the one the [changelog](https://docs.shftd2.com/changelog.md) gives for kit
`0.1.0-rc.3`: the manifest gives the kit and edge library versions and the SHA-256 of every file.
The kit is © DS Technologies, all rights reserved; its `LICENSE` grants doubleshift account holders the right to
run it on their own distributions.

```bash title="Download and check" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
curl -fSLO https://api.shftd2.com/edge/cloudfront/doubleshift-cloudfront-kit-0.1.0-rc.3.zip
echo "8120c973e8203cb499a06718d5a47500f281030a11139520a88ce1da12c1d7b7  doubleshift-cloudfront-kit-0.1.0-rc.3.zip" | shasum -a 256 -c \
  && unzip doubleshift-cloudfront-kit-0.1.0-rc.3.zip -d doubleshift-cloudfront-kit
cd doubleshift-cloudfront-kit
shasum -a 256 manifest.json
```

**2. Write the configuration**

Set at least `allowedHosts` ([Configuration](#configuration)), then check every file against the manifest and
your configuration.

```bash title="Configure and check" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
cp doubleshift.config.example.json composer/doubleshift.config.json
node scripts/check.mjs
```

**3. Deploy the template**

In the AWS account of the distribution, in a region with Lambda `nodejs24.x` on arm64. Your account id, API key
and logs token are `NoEcho` parameters, stored as environment variables of the composer, never in a file.

```bash title="Deploy" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
sam build
# Parameters: DistributionId, DsPartnerId, DsApiKey, DsLogsToken, DefaultTTL, MaxTTL,
# and optionally ExtraCacheKeyHeaders, ExtraOriginRequestHeaders, NamePrefix.
sam deploy --guided --capabilities CAPABILITY_IAM
```

**4. Attach the kit to your distribution**

With `--apply`, `attach` writes a rollback file (mode `0600`, keep it private), applies the policies, waits until
the distribution is `Deployed`, then associates the function. Before each step it reads the distribution again
and stops if the behavior, its policies, the origin `doubleshift-composer` or the kit's policies changed in
between.

```bash title="Attach" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
# Dry run: prints the diff, changes nothing.
node scripts/attach.mjs --distribution-id <your distribution id> --behavior "<default or path pattern>" \
  --stack <stack name> --stack-region <stack region>
# Apply. Run again later, it answers "already in place".
node scripts/attach.mjs --distribution-id <your distribution id> --behavior "<default or path pattern>" \
  --stack <stack name> --stack-region <stack region> --apply
```

## Configuration

`composer/doubleshift.config.json` is read once when the composer starts: a change needs `sam build` and
`sam deploy`.

| Key | Use |
| - | - |
| `allowedHosts` | Required. The public hostnames the composer may read, lowercase, without port, 50 at most. Any other host gets `403`. |
| Edge library [options](https://docs.shftd2.com/api/edge-library.md#options) | Accepted, except secrets, endpoints, functions and `cachePolicy`. |
| `originTimeoutMs` | Time to read your page: 100 to 25,000 ms, 10,000 by default. |
| `bufferMaxBytes` | Pages up to this size are read whole before they go out: 262,144 to 16,777,216 bytes, 6 MiB by default. |
| `compress` | `true` by default: an enriched page read whole, up to `bufferMaxBytes`, goes out in `br` or `gzip`, as the agent accepts. A larger page is streamed uncompressed. |

Any other key is refused: the enriched page always goes out `private, no-store`. A missing or refused file makes every agent request answer `500`, diagnostic
`adapter_config_invalid`; humans are not affected.

Out of [`scope`](https://docs.shftd2.com/api/edge-library#scope), the composer still runs and returns your page with no call and no record.

## Verify

Run the [Verify and troubleshoot](https://docs.shftd2.com/verify.md) sequence on one article, `304` check included, then the checks specific
to CloudFront:

```bash title="CloudFront checks" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
# Purge one URL.
aws cloudfront create-invalidation --distribution-id <your distribution id> --paths "/some-article"

# Direct access to the function URL (stack output ComposerFunctionUrl) is refused: expect 403.
curl -s -o /dev/null -w '%{http_code}\n' https://<your function url>/

# A redirect stays a redirect: expect the same status and Location for a human and for an agent.
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://your-domain.example/old-url
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -A "Mozilla/5.0 (compatible; GPTBot/1.0)" \
  https://your-domain.example/old-url

# The composer's delivery records, one per agent page in scope (log group /aws/lambda/<prefix>-composer).
aws logs tail /aws/lambda/doubleshift-composer --region <stack region> --follow | grep doubleshift_edge
```

Outcomes and fields are in the [edge library reference](https://docs.shftd2.com/api/edge-library.md). To send the records to doubleshift,
deploy with a logs token ([Delivery logs](https://docs.shftd2.com/delivery-logs.md)).

## Limits

* **Latency.** Each agent request runs one Lambda invocation that reads the page through your distribution, then
  calls doubleshift; the first request after a cold start of the Lambda is slower. Humans are not affected.
* **Page size.** A page up to `bufferMaxBytes` goes out whole or as a `502`, never truncated. A larger page is
  streamed: a break after the card can reach the agent as a complete page.
* **Compression.** CloudFront does not compress the composer's response. The composer compresses the enriched page
  only when it holds it whole, up to `bufferMaxBytes`; a larger page, and a page without a card, reach the agent
  uncompressed.
* **Downstream caches.** CloudFront drops `Vary: User-Agent`: `private, no-store` alone protects the caches after it.
* **Cost.** One Lambda invocation and one CloudFront read per agent request on the behavior, in scope or not.
* **Access.** An identity of your AWS account allowed `lambda:InvokeFunction` on the composer can call it directly
  and get a card: hence the dedicated key.
* **Container.** Write `article:not(.a):not(.b)`, not `article:not(.a, .b)`: a selector list inside `:not()` is
  refused.

## Roll back

The rollback file restores the behavior in reverse order: the function first, which sends agents back to your
cached page, then, once `Deployed`, your policies and the origin `doubleshift-composer` as it was before: removed if
the kit added it and nothing else uses it, otherwise its own settings back. It refuses if the behavior or that origin
changed outside the kit's fields, and stops if the distribution changes between the two steps. When `attach` or the
rollback stops, it prints the state as read: check it, run the dry run again, and keep the first rollback file (one
written by a resumed `--apply` only restores the intermediate state).

```bash title="Roll back" theme={"theme":{"light":"css-variables","dark":"css-variables"}}
node scripts/attach.mjs --rollback <rollback file>            # dry run
node scripts/attach.mjs --rollback <rollback file> --apply
sam delete
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.