Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/controllers/llmo/llmo-akamai-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -814,8 +814,12 @@ export function redactPapiErrors(errors, extraSecrets = [], max = 25) {
* deploy, so it's a per-deploy fingerprint: deploy-status compares it between a version and its
* base to tell "this deploy's fresh write landed" (keys differ) from "the version is just an
* unwritten clone inheriting the previous onboard's rule" (keys identical).
* NEVER return this value to a client — it's a secret (redactSecrets scrubs it from responses); it
* is only compared server-side. Walks the whole tree for the first matching header.
* This value IS a secret (a Bot Manager/WAF allowlist key) and is redacted from every OTHER
* response that leaves the server (redactSecrets, redactPapiErrors). The one exception:
* deploy-status returns it directly, and ONLY when it has confirmed (via the freshWrite compare
* above) that the key belongs to THIS deploy — never a stale/inherited one. Do not return this
* value from any other call site without the same freshWrite guard. Walks the whole tree for the
* first matching header.
* @param {object} tree - a PAPI rule tree ({ rules: {...} })
* @returns {string|null} the fetcher-key header value, or null when the tree has no managed rule
*/
Expand Down
23 changes: 19 additions & 4 deletions src/controllers/llmo/llmo-akamai.js
Original file line number Diff line number Diff line change
Expand Up @@ -813,8 +813,14 @@ function LlmoAkamaiController(ctx) {
* even if THIS deploy's write never landed. When `baseVersion` is given, we also compare the
* per-deploy fetcher key between the two versions: a fresh deploy mints a new key, so a key that
* DIFFERS from the base's proves this deploy's write actually persisted (`freshWrite: true`); an
* identical key means the version is an unwritten clone (`freshWrite: false`). The key value is
* only compared server-side, never returned.
* identical key means the version is an unwritten clone (`freshWrite: false`).
*
* `fetcherKey` is returned in the response ONLY when `freshWrite !== false` (true, or undefined
* on an unambiguous first onboard) AND a fetcher-key header was actually found — i.e. only when
* the key is confirmed to belong to THIS deploy, never an inherited key from a prior onboarding,
* and never a bare `null`. This lets the Deploy step recover the key for Bot Manager allowlisting
* after its own deploy response was lost to the CDN timeout, without ever surfacing a
* stale/inherited/missing key.
*
* `validate` (optional, 'true'): also run PAPI validation on the checked version and return
* `activatable` (no blocking errors), `errorCount`, a bounded `errors` array, and `warningCount`.
Expand Down Expand Up @@ -883,17 +889,21 @@ function LlmoAkamaiController(ctx) {
const errorCount = validate ? (errors?.length ?? 0) : undefined;
const activatable = validate ? errorCount === 0 : undefined;

// The fetcher key found in the checked version, if any — null when the rule is absent, or
// present-but-missing/malformed the fetcher-key header (e.g. an old flat-layout rule).
// Computed once and reused below for both the freshWrite compare and the response.
const recoveredKey = deployed ? getManagedFetcherKey(ruleTree) : null;

// Re-onboard disambiguation: compare the per-deploy fetcher key against the base version the
// deploy cloned from. A fresh key proves THIS deploy's write persisted (not just an inherited
// clone). Only meaningful when a distinct baseVersion is supplied and the rule is present.
let freshWrite;
if (deployed && baseVersion !== undefined && baseVersion !== version) {
const targetKey = getManagedFetcherKey(ruleTree);
const { ruleTree: baseTree } = await client
.getRuleTree(propertyId, baseVersion, contractId, groupId);
const baseKey = getManagedFetcherKey(baseTree);
// Distinct (or the base had no managed key at all) ⇒ this deploy wrote fresh content.
freshWrite = targetKey !== null && targetKey !== baseKey;
freshWrite = recoveredKey !== null && recoveredKey !== baseKey;
}

log.info(auditLine(context, 'deploy-status', 'ok', {
Expand All @@ -911,6 +921,11 @@ function LlmoAkamaiController(ctx) {
// (fetcher key differs from the base clone); false ⇒ the version is an unwritten clone.
// undefined ⇒ not checked (first-onboard case, where `deployed` is already unambiguous).
...(freshWrite !== undefined ? { freshWrite } : {}),
// Recovered fetcher key, so a Deploy whose own response was lost to the CDN timeout can
Comment thread
adityamisra08 marked this conversation as resolved.
// still show it for Bot Manager allowlisting. NEVER when freshWrite === false — that
// version's key is an inherited clone from a prior onboarding, not this deploy's. Also
// omitted (not null) when the rule is present but has no usable fetcher-key header.
...(recoveredKey && freshWrite !== false ? { fetcherKey: recoveredKey } : {}),
// Present only when validate=true: whether the version can be activated + its error detail.
// `errors` is bounded (PAPI can return hundreds) and redacted: PAPI can echo a behavior's
// option values (e.g. headerValue) in structured detail on certain validation errors, so a
Expand Down
18 changes: 13 additions & 5 deletions test/controllers/llmo/llmo-akamai.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ describe('LlmoAkamaiController', () => {
expect(body.latestVersion).to.equal(7);
expect(body.deployed).to.equal(false);
expect(body.managedRulesPresent).to.deep.equal([]);
expect(body).to.not.have.property('fetcherKey');
expect(mockAkamaiClient.getRuleTree).to.have.been.calledWith(PROPERTY_ID, 7);
// Read-only: never creates a version or writes.
expect(mockAkamaiClient.createVersion).to.not.have.been.called;
Expand All @@ -579,6 +580,9 @@ describe('LlmoAkamaiController', () => {
expect(res.status).to.equal(200);
expect(body.deployed).to.equal(true);
expect(body.managedRulesPresent).to.deep.equal(['ABV - Optimize at Edge']);
// The rule is present but carries no fetcher-key header (DEPLOYED_TREE has none) — omit the
// field rather than returning a bare null, even though deployed is true.
expect(body).to.not.have.property('fetcherKey');
});

it('checks a specific version when one is supplied', async () => {
Expand Down Expand Up @@ -612,7 +616,7 @@ describe('LlmoAkamaiController', () => {
},
});

it('reports freshWrite:true when the fetcher key differs from the base (write landed)', async () => {
it('reports freshWrite:true and recovers the fetcher key when it differs from the base (write landed)', async () => {
mockAkamaiClient.getRuleTree.withArgs(PROPERTY_ID, 7)
.resolves({ ruleTree: treeWithFetcherKey('NEW_KEY'), ruleFormat: 'v', etag: 'e7' });
mockAkamaiClient.getRuleTree.withArgs(PROPERTY_ID, 6)
Expand All @@ -622,12 +626,12 @@ describe('LlmoAkamaiController', () => {
expect(res.status).to.equal(200);
expect(body.deployed).to.equal(true);
expect(body.freshWrite).to.equal(true);
// The secret fetcher-key value is compared server-side but never returned.
expect(JSON.stringify(body)).to.not.contain('NEW_KEY');
// This deploy's OWN key is recovered (confirmed fresh); the base's inherited key never is.
expect(body.fetcherKey).to.equal('NEW_KEY');
expect(JSON.stringify(body)).to.not.contain('OLD_KEY');
});

it('reports freshWrite:false when the version is an unwritten clone (same key as base)', async () => {
it('reports freshWrite:false and omits fetcherKey when the version is an unwritten clone (same key as base)', async () => {
mockAkamaiClient.getRuleTree.withArgs(PROPERTY_ID, 7)
.resolves({ ruleTree: treeWithFetcherKey('SAME_KEY'), ruleFormat: 'v', etag: 'e7' });
mockAkamaiClient.getRuleTree.withArgs(PROPERTY_ID, 6)
Expand All @@ -636,15 +640,19 @@ describe('LlmoAkamaiController', () => {
const body = await res.json();
expect(body.deployed).to.equal(true);
expect(body.freshWrite).to.equal(false);
// The key present is inherited from a prior onboarding, not this deploy's — never surfaced.
expect(body).to.not.have.property('fetcherKey');
expect(JSON.stringify(body)).to.not.contain('SAME_KEY');
});

it('omits freshWrite when no baseVersion is supplied', async () => {
it('omits freshWrite but recovers fetcherKey when no baseVersion is supplied (unambiguous first onboard)', async () => {
mockAkamaiClient.getRuleTree.withArgs(PROPERTY_ID, 7)
.resolves({ ruleTree: treeWithFetcherKey('K'), ruleFormat: 'v', etag: 'e7' });
const res = await controller.deployStatus(withData(propertyRef));
const body = await res.json();
expect(body.deployed).to.equal(true);
expect(body).to.not.have.property('freshWrite');
expect(body.fetcherKey).to.equal('K');
// No base comparison ⇒ only the target version was read.
expect(mockAkamaiClient.getRuleTree).to.have.been.calledOnceWith(PROPERTY_ID, 7);
});
Expand Down
Loading