Skip to content
Merged
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
77 changes: 71 additions & 6 deletions docs/javascripts/production-lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,25 +222,65 @@
return targets;
}

function validateLiveAdmissions(value, projection, targets) {
function isObjectReferenceRow(admission) {
return (
isObject(admission) &&
admission.kind === "qualification-release" &&
typeof admission.object_path === "string" &&
typeof admission.target !== "string"
);
}

function latestAdmissionsAreUntilRevoked(targets) {
for (const targetId of TARGET_IDS) {
const admission = targets.get(targetId)?.latest_admission;
if (
!isObject(admission) ||
admission.expires_at !== null ||
admission.revoked_at != null
) {
return false;
}
}
return true;
}

function issuedPolicyBindsOrUntilRevoked(value, projection, targets, policy) {
if (!DIGEST.test(value.policy_sha256)) return false;
if (value.policy_sha256 === projection.source.files.policy.sha256) return true;
return (
isObject(policy) &&
policy.admission_validity === "until_revoked" &&
latestAdmissionsAreUntilRevoked(targets)
);
}

function validateLiveAdmissions(value, projection, targets, policy = null) {
if (
!hasExactKeys(value, ["$schema", "schema_version", "policy_sha256", "admissions"]) ||
value.$schema !== "schemas/production-lifecycle-admissions.schema.json" ||
value.schema_version !== "openadapt.production-lifecycle-admissions/v1" ||
value.policy_sha256 !== projection.source.files.policy.sha256 ||
!Array.isArray(value.admissions)
!Array.isArray(value.admissions) ||
!issuedPolicyBindsOrUntilRevoked(value, projection, targets, policy)
) {
return false;
}
const byTarget = new Map(TARGET_IDS.map((id) => [id, []]));
let objectRefCount = 0;
for (const admission of value.admissions) {
if (!isObject(admission) || !byTarget.has(admission.target)) return false;
if (!isObject(admission)) return false;
if (isObjectReferenceRow(admission)) {
objectRefCount += 1;
continue;
}
if (!byTarget.has(admission.target)) return false;
byTarget.get(admission.target).push(admission);
}
for (const targetId of TARGET_IDS) {
const live = byTarget
.get(targetId)
.sort((left, right) => left?.release_identity?.sequence - right?.release_identity?.sequence);
if (live.length === 0 && objectRefCount > 0) continue;
if (!sameJson(live, targets.get(targetId).admission_history)) return false;
}
return true;
Expand Down Expand Up @@ -294,6 +334,7 @@
summaryUrl: null,
evidence: { class: admission.evidence_class },
release,
untilRevoked: true,
});
}

Expand Down Expand Up @@ -624,7 +665,28 @@
const expectedDigest = projection.source.files.admissions.sha256;
if ((await sha256(admissionsBytes)) !== expectedDigest) return null;
const liveAdmissions = JSON.parse(new TextDecoder().decode(admissionsBytes));
if (!validateLiveAdmissions(liveAdmissions, projection, targets)) return null;
let policy = null;
if (liveAdmissions.policy_sha256 !== projection.source.files.policy.sha256) {
const policyItem = projection.source.files.policy;
if (
!isObject(policyItem) ||
!isHttpsUrl(policyItem.url) ||
!DIGEST.test(policyItem.sha256)
) {
return null;
}
const policyBytes = await fetchBytes(fetchImpl, policyItem.url, now);
if (!policyBytes || (await sha256(policyBytes)) !== policyItem.sha256) {
return null;
}
policy = JSON.parse(new TextDecoder().decode(policyBytes));
if (!isObject(policy) || typeof policy.admission_validity !== "string") {
return null;
}
}
if (!validateLiveAdmissions(liveAdmissions, projection, targets, policy)) {
return null;
}

const candidates = new Map();
for (const [targetId, target] of targets) {
Expand All @@ -636,7 +698,9 @@
[...candidates.entries()].map(async ([targetId, active]) => [
targetId,
active,
await verifyTargetAuthorities(active, fetchImpl, now),
active.untilRevoked === true
? true
: await verifyTargetAuthorities(active, fetchImpl, now),
]),
);
const activeTargets = new Map(
Expand All @@ -660,6 +724,7 @@
return;
}
element.append("Production: ", active.releaseLabel, ". ");
if (!isHttpsUrl(active.summaryUrl)) return;
const link = element.ownerDocument.createElement("a");
link.href = active.summaryUrl;
link.rel = "noopener noreferrer";
Expand Down
163 changes: 163 additions & 0 deletions tests/js/production_lifecycle.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -704,3 +704,166 @@ test("a timestamped v2 expiry is not until-revoked", () => {
target.latest_admission.expires_at = "2026-09-09T18:24:25Z";
assert.equal(lifecycle.deriveTarget(target, committed, now), null);
});

const V2_SPEC = {
agent: { claimScope: "production_agent", releaseKind: "package" },
capture: { claimScope: "production_capture", releaseKind: "package" },
cloud: { claimScope: "production_cloud", releaseKind: "deployment" },
desktop: { claimScope: "production_desktop", releaseKind: "package" },
docs: { claimScope: "production_docs", releaseKind: "deployment" },
flow: { claimScope: "production_flow", releaseKind: "package" },
openadapt: { claimScope: "production_openadapt", releaseKind: "package" },
};

function v2Admission(targetId, overrides = {}) {
const spec = V2_SPEC[targetId];
const release =
spec.releaseKind === "package"
? { kind: "package", version: "1.2.3" }
: { kind: "deployment", deployment_id: `${targetId}-deploy` };
return {
target: targetId,
claim_scope: spec.claimScope,
verdict: "accepted",
expires_at: null,
revoked_at: null,
evidence_class: "remote-safe-synthetic",
issued_at: "2026-09-02T18:24:25Z",
release_identity: {
schema_version: "openadapt.monotonic-production-release/v1",
channel: "production",
sequence: 1,
previous_admission_sha256: null,
},
release,
...overrides,
};
}

function objectRef(index) {
return {
schema_version: "openadapt.production-evidence-object-reference/v2",
kind: "qualification-release",
object_path: `production-evidence/objects/sha256/${index}/obj.qualification-release.json`,
};
}

function makeUntilRevokedFixture(overrides = {}) {
const histories = Object.fromEntries(
lifecycle.TARGET_IDS.map((id) => [id, [v2Admission(id, overrides.admissions?.[id])]]),
);
const fixture = makeFixture(histories);
const policy = {
admission_validity: overrides.admissionValidity ?? "until_revoked",
};
const policyBytes = jsonBytes(policy);
const live = {
$schema: "schemas/production-lifecycle-admissions.schema.json",
schema_version: "openadapt.production-lifecycle-admissions/v1",
policy_sha256: overrides.issuedPolicySha256 ?? `sha256:${"a".repeat(64)}`,
admissions: overrides.liveAdmissions ?? lifecycle.TARGET_IDS.map((_, index) => objectRef(index)),
};
const bytes = jsonBytes(live);
fixture.live = live;
fixture.bytes = bytes;
fixture.policyBytes = policyBytes;
fixture.projection.source.files.admissions.sha256 = digest(bytes);
fixture.projection.source.files.policy.sha256 = digest(policyBytes);
return fixture;
}

function fetchUntilRevoked(fixture, options = {}) {
return async (url) => {
if (options.unavailable) return { ok: false };
if (url.startsWith(`${lifecycle.PROJECTION_URL}?openadapt_lifecycle_request=`)) {
return { ok: true, json: async () => structuredClone(fixture.projection) };
}
if (url.startsWith(`${lifecycle.ADMISSIONS_URL}?openadapt_lifecycle_request=`)) {
return byteResponse(options.bytes ?? fixture.bytes);
}
const policyUrl = fixture.projection.source.files.policy.url;
if (url === policyUrl || url.startsWith(`${policyUrl}?`)) {
if (options.policyUnavailable) return { ok: false };
return byteResponse(options.policyBytes ?? fixture.policyBytes);
}
return { ok: false };
};
}

const AFTER_ISSUE = Date.parse("2026-09-02T20:00:00Z");

test("until-revoked object-refs keep retained policy hashes active", async () => {
const fixture = makeUntilRevokedFixture();
const state = await lifecycle.load(fetchUntilRevoked(fixture), AFTER_ISSUE);

assert.ok(state?.activeTargets instanceof Map);
assert.deepEqual([...state.activeTargets.keys()].sort(), lifecycle.TARGET_IDS);
assert.equal(state.activeTargets.get("flow").untilRevoked, true);
assert.equal(state.activeTargets.get("flow").releaseLabel, "release 1.2.3");
assert.equal(state.defaultInstallVerified, true);
});

test("until-revoked policy mismatch fails closed without a parseable policy", async () => {
const fixture = makeUntilRevokedFixture();
assert.equal(
await lifecycle.load(
fetchUntilRevoked(fixture, { policyUnavailable: true }),
AFTER_ISSUE,
),
null,
);

const unparseable = Buffer.from("not-json");
fixture.projection.source.files.policy.sha256 = digest(unparseable);
assert.equal(
await lifecycle.load(
fetchUntilRevoked(fixture, { policyBytes: unparseable }),
AFTER_ISSUE,
),
null,
);

const bounded = makeUntilRevokedFixture({ admissionValidity: "bounded" });
assert.equal(await lifecycle.load(fetchUntilRevoked(bounded), AFTER_ISSUE), null);
});

test("until-revoked mismatch fails closed on expiry, revocation, or identity drift", async () => {
const expired = makeUntilRevokedFixture({
admissions: { flow: { expires_at: "2026-09-09T18:24:25Z" } },
});
assert.equal(await lifecycle.load(fetchUntilRevoked(expired), AFTER_ISSUE), null);

const revoked = makeUntilRevokedFixture({
admissions: { flow: { revoked_at: "2026-09-02T19:00:00Z" } },
});
assert.equal(await lifecycle.load(fetchUntilRevoked(revoked), AFTER_ISSUE), null);

const drifted = makeUntilRevokedFixture({
admissions: { flow: { claim_scope: "production_openadapt" } },
});
const driftedState = await lifecycle.load(fetchUntilRevoked(drifted), AFTER_ISSUE);
assert.equal(driftedState.activeTargets.has("flow"), false);
assert.equal(driftedState.activeTargets.size, 6);
assert.equal(driftedState.defaultInstallVerified, false);
});

test("object-ref rows are not product targets and unknown rows fail closed", async () => {
const fixture = makeUntilRevokedFixture({
liveAdmissions: [
...lifecycle.TARGET_IDS.map((_, index) => objectRef(index)),
{ kind: "not-a-target", object_path: "nope.json" },
],
});
assert.equal(await lifecycle.load(fetchUntilRevoked(fixture), AFTER_ISSUE), null);

const valid = makeUntilRevokedFixture();
assert.equal(
lifecycle.validateLiveAdmissions(
valid.live,
valid.projection,
lifecycle.validateProjection(valid.projection),
{ admission_validity: "until_revoked" },
),
true,
);
});