diff --git a/docs/superpowers/plans/2026-09-01-duplicate-draft-consolidation.md b/docs/superpowers/plans/2026-09-01-duplicate-draft-consolidation.md new file mode 100644 index 000000000..c440dbe15 --- /dev/null +++ b/docs/superpowers/plans/2026-09-01-duplicate-draft-consolidation.md @@ -0,0 +1,1004 @@ +# Duplicate Draft Consolidation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build, merge, and operate a focused local CLI that proves the three v0.8.22 drafts equivalent, preserves Release `379991871`, safely removes only Releases `379982100` and `379986168`, and records a verifiable receipt. + +**Architecture:** Implement a standalone operator path that is unreachable from `.github/workflows/release.yml`. Strict schema, safe-file, evidence, authority, journal, adapter, and orchestration modules keep the destructive effect isolated behind an exact duplicate-ID boundary; production release parsers and attestation verification remain the source of truth for the 45-asset escrow. A hash-chained write-ahead journal makes interruption outcomes explicit and resumable while the required `main` freeze remains intact. + +**Tech Stack:** Node.js 24 ESM, built-in `node:test`, existing Dawn release readers/parsers, GitHub CLI authentication, GitHub REST API, npm registry reader, SHA-256 canonical JSON envelopes. + +--- + +## Execution setup + +Implement this prerequisite from current `origin/main`, not from the larger +single-owner abandonment branch. + +1. Use `superpowers:using-git-worktrees` to create a clean worktree on branch + `blove/duplicate-draft-consolidation` from the latest `origin/main`. +2. Bring only these approved documents into that worktree: + `docs/superpowers/specs/2026-09-01-duplicate-draft-consolidation-design.md` + and this plan. Do not bring abandonment implementation commits. +3. Verify the starting point with: + + ```bash + git merge-base --is-ancestor origin/main HEAD + git diff --name-only origin/main...HEAD + ``` + + Before implementation, the diff must contain only the two documentation + files. Release must remain `disabled_manually`; this setup does not mutate + GitHub or npm state. + +## File structure + +### Create + +- `scripts/release/duplicate-draft-consolidation-schema.mjs` — exact shared + record schemas, canonical JSON/envelope encoding, hash validation, size caps. +- `scripts/release/duplicate-draft-consolidation-files.mjs` — no-follow bounded + reads and atomic durable writes for private `.dawn` evidence and the tracked + receipt. +- `scripts/release/duplicate-draft-consolidation-evidence.mjs` — strict GitHub + Release/asset normalization, 45-asset hydration, production escrow and + attestation verification, three-way semantic/payload equality. +- `scripts/release/duplicate-draft-consolidation-adapters.mjs` — bounded local + Git/GitHub/npm/attestation adapters and the one exact Release DELETE effect. +- `scripts/release/duplicate-draft-consolidation-authority.mjs` — repository, + main, workflow/run, tag, npm, Release, and terminal direct-target observations. +- `scripts/release/duplicate-draft-consolidation-journal.mjs` — hash-chained + events, legal transitions, attempt bounds, resume decisions, final receipt. +- `scripts/release/duplicate-draft-consolidation.mjs` — `inspect`, `perform`, and + `verify` orchestration with no process-global dependencies. +- `scripts/release/duplicate-draft-consolidation-cli.mjs` — strict operator CLI, + production dependency composition, redacted output, exit codes. +- `scripts/release/test/support/duplicate-draft-consolidation-fixture.mjs` — one + realistic three-draft/45-asset fixture used across focused tests. +- `scripts/release/test/duplicate-draft-consolidation-schema.test.mjs` +- `scripts/release/test/duplicate-draft-consolidation-files.test.mjs` +- `scripts/release/test/duplicate-draft-consolidation-evidence.test.mjs` +- `scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs` +- `scripts/release/test/duplicate-draft-consolidation-authority.test.mjs` +- `scripts/release/test/duplicate-draft-consolidation-journal.test.mjs` +- `scripts/release/test/duplicate-draft-consolidation.test.mjs` +- `scripts/release/test/duplicate-draft-consolidation-cli.test.mjs` +- `scripts/release/test/duplicate-draft-consolidation-rehearsal.test.mjs` + +### Modify + +- `scripts/release/test/workflow-contracts.test.mjs` — prove the new CLI and + DELETE effect are unreachable from every workflow. +- `package.json` — add the local `release:consolidate-drafts` command only. + +### Explicitly unchanged + +- `.github/workflows/release.yml` +- `scripts/release/test/fixtures/release-script-hashes.json` +- `scripts/release/controller-schema.json` +- Vercel dependencies and the `vercel-native` CI lane + +The new operator CLI is not workflow-reachable, so it must not be added to the +release-path hash inventory. + +### Task 1: Lock dedicated limits and canonical envelope schemas + +**Files:** +- Create: `scripts/release/duplicate-draft-consolidation-schema.mjs` +- Test: `scripts/release/test/duplicate-draft-consolidation-schema.test.mjs` + +- [ ] **Step 1: Write failing limit tests** + +Import the dedicated consolidation limits from the new schema module and assert: + +```js +assert.deepEqual(DUPLICATE_DRAFT_CONSOLIDATION_LIMITS, { + proposedBytes: 4 * MEBIBYTE, + journalBytes: 72 * MEBIBYTE, + finalReceiptBytes: 96 * MEBIBYTE, + authorityStageBytes: 8 * MEBIBYTE, + survivorEvidenceBytes: 2 * MEBIBYTE, + journalEventReserveBytes: 8 * MEBIBYTE, + envelopeReserveBytes: MEBIBYTE, + maximumDeleteAttempts: 3, + maximumTargets: 2, + maximumOrphanAuthorityRecoveries: 1, + maximumAssetDownloads: 135, +}) +assert.ok( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes >= + (2 * 3 + 1 + 1) * + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalEventReserveBytes, +) +assert.ok( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes >= + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.survivorEvidenceBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.envelopeReserveBytes, +) +``` + +- [ ] **Step 2: Run the limit test and verify RED** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test scripts/release/test/duplicate-draft-consolidation-schema.test.mjs +``` + +Expected: FAIL because the dedicated schema module does not exist. + +- [ ] **Step 3: Add the exact limits and module-load invariants** + +Export the frozen object above from +`duplicate-draft-consolidation-schema.mjs`. Throw at module initialization if +either headroom relationship is false. Import existing `RELEASE_PAYLOAD_LIMITS` +for per-Release escrow caps, but keep `limits.mjs` and its workflow-reachable +pinned hash unchanged. + +- [ ] **Step 4: Write failing canonical-envelope tests** + +Cover all three exact top-level record schemas and the event envelope. Use the +field order from the approved design. Test: + +```js +const envelope = createConsolidationEnvelope("proposed", proposedRecord()) +const bytes = canonicalConsolidationEnvelopeBytes("proposed", envelope) +assert.deepEqual(parseConsolidationEnvelope("proposed", bytes), envelope) +assert.match(envelope.recordSha256, /^[0-9a-f]{64}$/u) +``` + +Mutate each required field, add an unknown field, reorder a fixed array, inject +a duplicate JSON key, change the digest, omit the final newline, pass invalid +UTF-8, and exceed the per-kind size bound. Each must throw before returning data. + +- [ ] **Step 5: Run the schema test and verify RED** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test scripts/release/test/duplicate-draft-consolidation-schema.test.mjs +``` + +Expected: FAIL because the schema module does not exist. + +- [ ] **Step 6: Implement the minimal strict codec** + +Export: + +```js +export function createConsolidationEnvelope(kind, record) +export function canonicalConsolidationEnvelopeBytes(kind, envelope) +export function parseConsolidationEnvelope(kind, bytes) +export function canonicalRecordSha256(record) +export function canonicalEventEnvelope(event, previousEventSha256) +export function parseJournalEventEnvelope(value, expectedSequence, previousEventSha256) +``` + +Use exact field arrays for every object in the design. Normalize into newly +constructed objects in canonical field order; do not canonicalize unknown input +by sorting arbitrary keys. Hash `JSON.stringify(record) + "\n"`, with the digest +outside the hashed record. Canonical-byte comparison must reject duplicate keys +because parsed-and-reencoded bytes differ. + +- [ ] **Step 7: Run focused tests and commit** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test scripts/release/test/duplicate-draft-consolidation-schema.test.mjs +``` + +Expected: PASS. + +Commit: + +```bash +git add scripts/release/duplicate-draft-consolidation-schema.mjs scripts/release/test/duplicate-draft-consolidation-schema.test.mjs +git commit -m "feat(release): define draft consolidation evidence" +``` + +### Task 2: Implement safe private evidence and tracked-receipt files + +**Files:** +- Create: `scripts/release/duplicate-draft-consolidation-files.mjs` +- Test: `scripts/release/test/duplicate-draft-consolidation-files.test.mjs` + +- [ ] **Step 1: Write adversarial failing tests** + +In a temporary repository, test private-file mode `0600`, tracked-file mode +`0644`, symlink rejection, non-regular-file rejection, wrong-owner injection, +hard-link rejection, group/other writable rejection, oversized input, pathname +replacement during read, same-size mutation during read, partial write failure, +and atomic replacement preserving the previous complete file. + +The core success assertion is: + +```js +await writePrivateEnvelope(target, bytes) +assert.equal((await stat(target)).mode & 0o777, 0o600) +assert.deepEqual(await readPrivateEnvelope(target, maximumBytes), bytes) +``` + +- [ ] **Step 2: Run the file test and verify RED** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test scripts/release/test/duplicate-draft-consolidation-files.test.mjs +``` + +Expected: FAIL because the file module does not exist. + +- [ ] **Step 3: Implement safe reads and atomic writes** + +Export: + +```js +export async function readPrivateEnvelope(filePath, maximumBytes, dependencies) +export async function writePrivateEnvelope(filePath, bytes, dependencies) +export async function readTrackedReceipt(filePath, maximumBytes, dependencies) +export async function writeTrackedReceipt(filePath, bytes, dependencies) +``` + +For `.dawn` files require no-follow support, one regular link, current effective +owner, and exact `0600`. For the tracked receipt require regular/no-follow, +current owner, nonexecutable, and no group/other write bits; accept `0644`. +Compare device, inode, size, link count, mtime, and ctime before/after reading and +revalidate the final pathname. Write a same-directory `wx` temporary file, +`fsync` it, rename atomically, then `fsync` the parent directory. Clean up only +the exact temporary pathname on failure. + +- [ ] **Step 4: Run focused tests and commit** + +Run the test above; expected PASS. + +Commit: + +```bash +git add scripts/release/duplicate-draft-consolidation-files.mjs scripts/release/test/duplicate-draft-consolidation-files.test.mjs +git commit -m "feat(release): secure consolidation evidence files" +``` + +### Task 3: Prove exact Release and 45-asset parity + +**Files:** +- Create: `scripts/release/duplicate-draft-consolidation-evidence.mjs` +- Create: `scripts/release/test/support/duplicate-draft-consolidation-fixture.mjs` +- Test: `scripts/release/test/duplicate-draft-consolidation-evidence.test.mjs` + +- [ ] **Step 1: Build a realistic failing fixture** + +Create three drafts with the approved IDs and opaque tags, distinct Release and +asset IDs, the same canonical `ESCROWED` body, one canonical +`release-record.json`, `manifest.json`, 21 package archives, and 22 replicated +multi-subject `.intoto.jsonl` bundles. The helper returns fake read adapters and +the expected 45-entry `{name, sha256}` projection. + +- [ ] **Step 2: Write failing evidence tests** + +Test the exact semantic Release projection and per-name asset projection. Cover +every excluded volatile field independently to prove it does not create false +inequality, then mutate each included field independently to prove it blocks. +Also cover: + +- malformed/noncanonical marker body; +- wrong release record or manifest bytes; +- package order/name/hash drift; +- missing, extra, duplicate, or non-`uploaded` asset; +- malformed GitHub digest or digest/download mismatch; +- bundle-set mismatch or failed attestation verification; +- payload over 64 MiB per Release, 192 MiB aggregate, or 135 downloads; +- fourth matching draft, published candidate Release, or wrong author. + +Use: + +```js +const result = await inspectEquivalentDrafts({ + candidate: CANDIDATE, + survivorId: "379991871", + duplicateIds: ["379982100", "379986168"], + releases: fixture.releases, + github: fixture.github, + attestations: fixture.attestations, +}) +assert.equal(result.releases.length, 3) +assert.equal(result.payloadProof.baseAssetSet.length, 45) +assert.equal(result.attestationVerification.subjects.length, 22) +``` + +- [ ] **Step 3: Run the evidence test and verify RED** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test scripts/release/test/duplicate-draft-consolidation-evidence.test.mjs +``` + +Expected: FAIL because the evidence module does not exist. + +- [ ] **Step 4: Implement evidence normalization and hydration** + +Export: + +```js +export async function inspectEquivalentDrafts(input) +export async function captureDirectTargetRead(input) +export function parseReleaseEvidence(value) +export function semanticReleaseProjection(value) +export function semanticAssetProjection(value) +export function assertEvidenceEqualsProposal(actual, proposed) +``` + +For each Release: + +1. Parse the body with `parseReleaseMarker` and compare it with + `canonicalReleaseBody`. +2. Download exactly 45 assets under existing release payload limits. +3. Parse `release-record.json` with `parseReleaseRecord` and require canonical + record bytes. +4. Parse canonical `manifest.json` with `parseSealedReleaseManifest`. +5. Parse marker attestations with `parseAttestationSet`. +6. Call `canonicalBaseAssetSet` with the actual record, manifest/package bytes, + marker attestation set, and all 22 bundles. +7. Call existing `verifyReleaseAttestationAnchor` with the replicated anchor + bundle and compare its returned attestation set/base digest to the marker and + canonical base set. +8. Compare every included semantic field and every same-name byte digest across + all three drafts. + +Do not export or duplicate the private attestation verifier from `metadata.mjs`; +the existing public anchor verifier already reaches the production verifier. + +- [ ] **Step 5: Run focused tests and commit** + +Run the evidence test; expected PASS. + +Commit: + +```bash +git add scripts/release/duplicate-draft-consolidation-evidence.mjs scripts/release/test/support/duplicate-draft-consolidation-fixture.mjs scripts/release/test/duplicate-draft-consolidation-evidence.test.mjs +git commit -m "feat(release): prove duplicate draft parity" +``` + +### Task 4: Add bounded production adapters and the isolated DELETE effect + +**Files:** +- Create: `scripts/release/duplicate-draft-consolidation-adapters.mjs` +- Test: `scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs` + +- [ ] **Step 1: Write failing adapter tests** + +Inject command and fetch fakes. Prove: + +- authentication comes from `gh auth token` or an existing safe token variable, + never argv/receipt/log output; +- GitHub reads use the trusted origin, API headers, 100-item pages, 100-page and + 10,000-record caps, and duplicate-ID rejection; +- local HEAD, symbolic branch, clean status, and `origin/main` use non-shell Git; +- repository/user/workflow/run/tag reads return exact normalized evidence; +- npm delegates to `createNpmReader` and preserves `ABSENT`/404/`E404`; +- only the approved duplicate IDs can reach DELETE; +- survivor, reordered/extra/missing ID, untrusted origin, malformed response, + 403/429/5xx, and abort-before-send are rejected; +- received 204 maps to `confirmed-204`, received 404 to + `response-404-ambiguous`, and timeout/transport loss to + `transport-ambiguous`. + +The destructive-boundary assertion must be explicit: + +```js +await assert.rejects( + () => writer.deleteDuplicate({ releaseId: "379991871" }), + /survivor|approved duplicate/u, +) +assert.equal(fetchCalls.length, 0) +``` + +- [ ] **Step 2: Run the adapter test and verify RED** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs +``` + +Expected: FAIL because the adapter module does not exist. + +- [ ] **Step 3: Implement the production composition** + +Export: + +```js +export async function createDuplicateDraftConsolidationAdapters(options) +export function createExactDuplicateDeleteEffect(options) +``` + +Reuse `createGitHubReader`, `createOwnerPreflightAdapters`, `createNpmReader`, +`createCliAttestationVerifier`, and the bounded process runner. Resolve the +GitHub token in memory through `gh auth token` only when no safe injected token +exists. The delete effect accepts the frozen survivor and ordered duplicate +set at construction and performs one `DELETE +/repos/cacheplane/dawnai/releases/{releaseId}` with a bounded timeout. It must +not expose create/update/publish/tag methods. + +- [ ] **Step 4: Run focused tests and commit** + +Run the adapter test; expected PASS. + +Commit: + +```bash +git add scripts/release/duplicate-draft-consolidation-adapters.mjs scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs +git commit -m "feat(release): isolate duplicate draft deletion" +``` + +### Task 5: Capture fresh authority with a terminal direct-target read + +**Files:** +- Create: `scripts/release/duplicate-draft-consolidation-authority.mjs` +- Test: `scripts/release/test/duplicate-draft-consolidation-authority.test.mjs` + +- [ ] **Step 1: Write failing authority tests** + +Model the exact `cacheplane/dawnai` repository/user identity, `main` SHA triple, +disabled Release workflow, empty nonterminal runs, annotated `v0.8.22`, ordered +21-package npm inventory, remaining draft set, payload proof, and direct target +read. Assert the final two network calls are direct Release-by-ID and full asset +enumeration; after they complete, orchestration may perform only the local +journal write before DELETE. + +Reject dirty checkout, non-`main` branch, mismatched HEAD/origin/GitHub SHA, +wrong actor/repository, active workflow, active run, moved/lightweight tag, +non-E404 npm result, missing/extra Release, target/list disagreement, stale npm +observation, and clock reversal. + +- [ ] **Step 2: Run the authority test and verify RED** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test scripts/release/test/duplicate-draft-consolidation-authority.test.mjs +``` + +Expected: FAIL because the authority module does not exist. + +- [ ] **Step 3: Implement staged authority capture** + +Export: + +```js +export async function captureNpmInventory(input) +export async function captureConsolidationAuthority(input) +export function assertFreshWriterAuthority(authority, proposal, now) +``` + +`captureConsolidationAuthority` must perform all broad reads first, then direct +GET of the target, then complete target asset enumeration. Store operation +start/completion timestamps and the canonical direct-evidence digest in +`targetRead`. Return with a sealed `networkEpoch`; the orchestrator consumes it +exactly once when persisting intent and invalidates it if any adapter read occurs +in between. Require the pre-delete npm inventory to be at most two minutes old. + +- [ ] **Step 4: Run focused tests and commit** + +Run the authority test; expected PASS. + +Commit: + +```bash +git add scripts/release/duplicate-draft-consolidation-authority.mjs scripts/release/test/duplicate-draft-consolidation-authority.test.mjs +git commit -m "feat(release): bind fresh consolidation authority" +``` + +### Task 6: Implement the hash-chained journal and resume state machine + +**Files:** +- Create: `scripts/release/duplicate-draft-consolidation-journal.mjs` +- Test: `scripts/release/test/duplicate-draft-consolidation-journal.test.mjs` + +- [ ] **Step 1: Write failing transition tests** + +Cover every event type and legal sequence from the design. Explicitly test: + +- previous-event digest, sequence, truncation, reordering, and mutation checks; +- fixed target order and no second-target event before first absence convergence; +- write-ahead intent before each DELETE; +- confirmed 204 then absence convergence; +- timeout/404 then absence convergence; +- intent with no outcome + present unchanged -> new attempt; +- recorded ambiguous outcome + six reads present unchanged -> new attempt; +- absent after unrecorded request -> `absent-ambiguous` + convergence; +- target change/publish/malformed -> stop; +- present after confirmed 204 -> stop; +- three-attempt cap; +- main SHA drift -> stop and preserve the journal; +- final authority only after both targets converge absent. + +- [ ] **Step 2: Run the journal test and verify RED** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test scripts/release/test/duplicate-draft-consolidation-journal.test.mjs +``` + +Expected: FAIL because the journal module does not exist. + +- [ ] **Step 3: Implement journal derivation and append operations** + +Export: + +```js +export function createConsolidationJournal(input) +export function parseConsolidationJournal(envelope) +export function deriveConsolidationState(journal) +export function appendJournalEvent(journal, type, payload, recordedAt) +export function nextResumeAction(state, liveTarget) +export function createFinalConsolidationReceipt(input) +``` + +Never mutate an existing event. Every append creates a new journal envelope and +is durably replaced through `writePrivateEnvelope`. Derive current state only by +replaying the exact event chain. `nextResumeAction` returns one of +`refresh-and-retry`, `reconcile-absence`, `complete`, or `stop`; it never calls a +writer. + +- [ ] **Step 4: Run focused tests and commit** + +Run the journal test; expected PASS. + +Commit: + +```bash +git add scripts/release/duplicate-draft-consolidation-journal.mjs scripts/release/test/duplicate-draft-consolidation-journal.test.mjs +git commit -m "feat(release): journal draft consolidation" +``` + +### Task 7: Implement read-only `inspect` and its CLI contract + +**Files:** +- Create: `scripts/release/duplicate-draft-consolidation.mjs` +- Create: `scripts/release/duplicate-draft-consolidation-cli.mjs` +- Test: `scripts/release/test/duplicate-draft-consolidation.test.mjs` +- Test: `scripts/release/test/duplicate-draft-consolidation-cli.test.mjs` +- Modify: `package.json` + +- [ ] **Step 1: Write failing inspect tests** + +Use injected adapters and a fake clock/waiter. Assert `inspect`: + +1. validates exact candidate and ID roles; +2. captures `inspect-initial` npm absence; +3. hydrates and verifies all 135 asset instances during the observation gap; +4. waits only the remainder required to reach 60 seconds; +5. captures `inspect-ready` npm absence and final authority metadata; +6. writes one canonical private proposed envelope; +7. returns/prints only its digest and safe summary; +8. makes zero writer calls. + +Reject every unknown/duplicate/missing flag and output path outside +`.dawn/release/`. + +- [ ] **Step 2: Run focused tests and verify RED** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test scripts/release/test/duplicate-draft-consolidation.test.mjs scripts/release/test/duplicate-draft-consolidation-cli.test.mjs +``` + +Expected: FAIL because inspect/CLI exports do not exist. + +- [ ] **Step 3: Implement `inspect` and strict argument parsing** + +Export: + +```js +export async function inspectDuplicateDrafts(input, dependencies) +export async function runDuplicateDraftConsolidationCli(options) +``` + +Add this package script: + +```json +"release:consolidate-drafts": "node scripts/release/duplicate-draft-consolidation-cli.mjs" +``` + +The exact production command is: + +```bash +pnpm release:consolidate-drafts inspect \ + --version 0.8.22 \ + --commit-sha 2a80deece2ff958fe7fde8fddeb4f99bed70a1c8 \ + --survivor 379991871 \ + --duplicates 379982100,379986168 \ + --output .dawn/release/duplicate-draft-consolidation.proposed.json +``` + +CLI exit codes are 0 success, 2 invalid invocation, and 1 failed evidence or +authority. Error output is one redacted line and never includes tokens, bodies, +asset bytes, or raw remote diagnostics. + +- [ ] **Step 4: Run focused tests and commit** + +Run both tests; expected PASS. + +Commit: + +```bash +git add package.json scripts/release/duplicate-draft-consolidation.mjs scripts/release/duplicate-draft-consolidation-cli.mjs scripts/release/test/duplicate-draft-consolidation.test.mjs scripts/release/test/duplicate-draft-consolidation-cli.test.mjs +git commit -m "feat(release): inspect duplicate drafts" +``` + +### Task 8: Implement one-target deletion, convergence, and retry + +**Files:** +- Modify: `scripts/release/duplicate-draft-consolidation.mjs` +- Modify: `scripts/release/test/duplicate-draft-consolidation.test.mjs` + +- [ ] **Step 1: Write failing mutation-kernel tests** + +For one target, assert exact call order: + +```text +fresh authority +direct target GET +complete target asset list +durable authority event +durable intent event +DELETE +durable outcome event (when observable) +bounded direct-GET/list convergence +durable convergence or retry event +``` + +Inject process loss after every durable boundary and before/after DELETE. Cover +204, 404, transport timeout, absent on resume, present unchanged on resume, +recorded ambiguity remaining present for the full window, reader disagreement, +rate limit/server error, and exhausted third attempt. Assert no survivor ID can +reach the writer under any state corruption. + +- [ ] **Step 2: Run the mutation test and verify RED** + +Run the focused orchestration test. Expected: FAIL because `performOneDeletion` +does not exist. + +- [ ] **Step 3: Implement the minimal mutation kernel** + +Add an internal/export-for-test function: + +```js +export async function performOneDuplicateDeletion(input, dependencies) +``` + +Use six complete read attempts under one 90-second wall-clock ceiling, give each +request the exact remaining timeout, and bound every backoff by its policy, +30 seconds, and the remaining shared budget. Retry DELETE only for an +ambiguous/unrecorded outcome whose target remains present and semantically +unchanged through the bounded window. Before recording the retry transition, +perform a completely fresh full authority capture, persist its actual current +45-asset target evidence, then append that same authority and consume its epoch +without intervening network. Any included Release or asset drift, clock +reversal, 403/429/5xx/timeout, or pagination failure stops rather than being +treated as absence. + +- [ ] **Step 4: Run focused tests and commit** + +Run the orchestration and journal tests; expected PASS. + +Commit: + +```bash +git add scripts/release/duplicate-draft-consolidation.mjs scripts/release/test/duplicate-draft-consolidation.test.mjs +git commit -m "feat(release): delete exact duplicate drafts" +``` + +### Task 9: Complete two-target `perform` and final receipt materialization + +**Files:** +- Modify: `scripts/release/duplicate-draft-consolidation.mjs` +- Modify: `scripts/release/duplicate-draft-consolidation-cli.mjs` +- Modify: `scripts/release/test/duplicate-draft-consolidation.test.mjs` +- Modify: `scripts/release/test/duplicate-draft-consolidation-cli.test.mjs` + +- [ ] **Step 1: Write failing end-to-end perform tests** + +Assert `perform`: + +- safely reads and verifies the reviewed proposed envelope; +- requires the exact confirmation containing its digest; +- requires clean merged `main` before the first writer and the same main SHA + before the second writer/finalization; +- appends `operation-started` and `perform-initial` evidence; +- repeats the 60-second npm/payload proof before target one; +- completes target one before any target-two authority event; +- captures fresh `pre-delete-2` authority before target two; +- records all six minimum npm stages plus retry stages; +- captures final one-survivor authority and npm E404; +- writes a complete tracked receipt only after final verification; +- never writes or mutates the survivor. + +Add failures for incorrect proposal digest, altered confirmation, main advance +after the first deletion, publication between deletions, changed survivor, +unexpected fourth draft, and receipt write failure after both deletions. The last +case must resume by rematerializing the same receipt without another DELETE. + +- [ ] **Step 2: Run focused tests and verify RED** + +Run the focused orchestration and CLI tests. Expected: FAIL because `perform` +is not wired. + +- [ ] **Step 3: Implement `perform` and CLI mode** + +Export: + +```js +export async function performDuplicateDraftConsolidation(input, dependencies) +``` + +The exact production shape is: + +```bash +pnpm release:consolidate-drafts perform \ + --proposal .dawn/release/duplicate-draft-consolidation.proposed.json \ + --journal .dawn/release/duplicate-draft-consolidation.journal.json \ + --receipt scripts/release/duplicate-draft-consolidation.json \ + --confirmation "CONSOLIDATE v0.8.22 2a80deece2ff958fe7fde8fddeb4f99bed70a1c8 SURVIVOR 379991871 DELETE 379982100,379986168 PROPOSAL " +``` + +There is no force, alternate survivor, reordered IDs, delete-all, SHA override, +or workflow mode. + +- [ ] **Step 4: Run focused tests and commit** + +Run schema, files, evidence, authority, journal, orchestration, and CLI tests; +expected PASS. + +Commit: + +```bash +git add scripts/release/duplicate-draft-consolidation.mjs scripts/release/duplicate-draft-consolidation-cli.mjs scripts/release/test/duplicate-draft-consolidation.test.mjs scripts/release/test/duplicate-draft-consolidation-cli.test.mjs +git commit -m "feat(release): perform draft consolidation" +``` + +### Task 10: Implement independent `verify` + +**Files:** +- Modify: `scripts/release/duplicate-draft-consolidation.mjs` +- Modify: `scripts/release/duplicate-draft-consolidation-cli.mjs` +- Modify: `scripts/release/test/duplicate-draft-consolidation.test.mjs` +- Modify: `scripts/release/test/duplicate-draft-consolidation-cli.test.mjs` + +- [ ] **Step 1: Write failing verify tests** + +Verify must safely read a normal `0644` tracked receipt, parse both embedded +envelopes, replay the event hash chain, prove both deleted IDs absent by direct +GET and complete list, re-download and verify the survivor's 45 assets, and +recheck main/workflow/runs/tag/final npm absence. Tamper every receipt layer and +assert rejection. + +Also assert the report says historical duplicate payload parity is supported by +the embedded pre-delete evidence plus current survivor, not independently +re-downloaded deleted bytes. + +- [ ] **Step 2: Run verify tests and verify RED** + +Expected: FAIL because verify mode is not implemented. + +- [ ] **Step 3: Implement verify and CLI mode** + +Export: + +```js +export async function verifyDuplicateDraftConsolidation(input, dependencies) +``` + +Production command: + +```bash +pnpm release:consolidate-drafts verify \ + --receipt scripts/release/duplicate-draft-consolidation.json +``` + +The receipt file is absent from the implementation PR and is created only by a +successful live operation. + +- [ ] **Step 4: Run focused tests and commit** + +Run focused tests; expected PASS. + +Commit: + +```bash +git add scripts/release/duplicate-draft-consolidation.mjs scripts/release/duplicate-draft-consolidation-cli.mjs scripts/release/test/duplicate-draft-consolidation.test.mjs scripts/release/test/duplicate-draft-consolidation-cli.test.mjs +git commit -m "feat(release): verify draft consolidation" +``` + +### Task 11: Add full rehearsal and prove workflow isolation + +**Files:** +- Create: `scripts/release/test/duplicate-draft-consolidation-rehearsal.test.mjs` +- Modify: `scripts/release/test/workflow-contracts.test.mjs` + +- [ ] **Step 1: Write the realistic rehearsal** + +Run `inspect -> perform -> verify` against the realistic three-draft fake with +distinct IDs and equal bytes. Rehearse clean completion and process loss at: + +- before first intent; +- after first intent but before DELETE; +- after server deletion but before response; +- after first convergence; +- after second intent; +- after second deletion but before receipt; +- after receipt write but before CLI success output. + +Assert every legal resume reaches exactly one unchanged survivor and exactly two +DELETE effects total unless a bounded retry is intentionally injected. + +- [ ] **Step 2: Write workflow-isolation assertions** + +Read all workflow sources and assert none contains +`duplicate-draft-consolidation`, `release:consolidate-drafts`, or a Release +DELETE endpoint. Re-run the existing release-controller reachability/hash tests +and assert `release-script-hashes.json` is unchanged. + +- [ ] **Step 3: Run rehearsal and verify RED/GREEN** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test scripts/release/test/duplicate-draft-consolidation.test.mjs scripts/release/test/duplicate-draft-consolidation-*.test.mjs scripts/release/test/workflow-contracts.test.mjs +``` + +Expected before completing fixtures: FAIL. Complete only fixture/harness code, +then rerun; expected PASS. + +- [ ] **Step 4: Commit** + +```bash +git add scripts/release/test/duplicate-draft-consolidation-rehearsal.test.mjs scripts/release/test/workflow-contracts.test.mjs +git commit -m "test(release): rehearse draft consolidation" +``` + +### Task 12: Run integration gates and independent review + +**Files:** +- Modify only files required by concrete test/review findings. + +- [ ] **Step 1: Run scoped format/lint without broad writes** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH pnpm exec biome check --config-path packages/config-biome/biome.json package.json scripts/release/duplicate-draft-consolidation*.mjs scripts/release/test/duplicate-draft-consolidation*.test.mjs scripts/release/test/support/duplicate-draft-consolidation-fixture.mjs scripts/release/test/workflow-contracts.test.mjs +``` + +Expected: PASS. If formatting is required, scope `--write` to only these files. + +- [ ] **Step 2: Run focused and full release-controller tests** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test scripts/release/test/duplicate-draft-consolidation.test.mjs scripts/release/test/duplicate-draft-consolidation-*.test.mjs +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH pnpm test:release-controller +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node scripts/check-docs.mjs +git diff --check +``` + +Expected: all PASS; controller-schema digest and release-path script hashes +remain unchanged. + +- [ ] **Step 3: Run the repository Definition of Done** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH DAWN_REQUIRE_DOCKER=1 pnpm ci:validate +``` + +Expected: PASS through lint, build, typecheck, tests, release inventory, +release-controller, docs, pack checks, TypeScript tooling, and all harness lanes. + +- [ ] **Step 4: Request technical review** + +Use `superpowers:requesting-code-review`. Review must check the exact head, +destructive boundary, event/journal state machine, safe files, immediate direct +read, retry cap, receipt limits, and workflow isolation. Also request the user's +GitHub review assistant on the PR, as previously agreed. + +- [ ] **Step 5: Address findings with `superpowers:receiving-code-review`** + +Reproduce every actionable finding with a failing test before changing code. +Rerun Steps 1–3 after the final fix. + +- [ ] **Step 6: Commit integration fixes** + +Use a factual commit message describing the concrete fix. The worktree must be +clean and the implementation PR must not contain the live receipt. + +### Task 13: Merge the focused prerequisite and run read-only `inspect` + +**Files:** +- Live local only: `.dawn/release/duplicate-draft-consolidation.proposed.json` + +- [ ] **Step 1: Merge only after exact-head checks pass** + +Release remains `disabled_manually`. Verify the PR changes only the focused +modules/tests/docs/package script, then merge. Pull the exact merged `main` into +the primary repository checkout and verify local HEAD, `origin/main`, and GitHub +main match. + +- [ ] **Step 2: Refresh read-only live state** + +Using `gh` and npm CLI reads, require the exact three approved drafts, 45 assets +each, annotated tag target, disabled Release workflow, no nonterminal Release +run, and npm E404 for all 21 packages. Stop on any drift. + +- [ ] **Step 3: Run `inspect` from exact merged main** + +Run the exact inspect command from Task 7. Review the canonical proposal and its +printed digest. No GitHub writer is invoked in this task. + +- [ ] **Step 4: Independently verify the proposed envelope** + +Run the focused schema/evidence verifier against the file, confirm survivor and +ordered duplicate IDs, then retain the private `0600` file for `perform`. + +### Task 14: Freeze main, perform the live consolidation, and verify + +**Files:** +- Live local: `.dawn/release/duplicate-draft-consolidation.journal.json` +- Create after success: `scripts/release/duplicate-draft-consolidation.json` + +- [ ] **Step 1: Begin the main-change freeze** + +Do not merge or push `main` until the receipt is durable. Recheck exact merged +main, clean checkout, Release disabled, zero active runs, tag identity, all npm +E404s, and the three-draft proposal. Stop if any check differs. + +- [ ] **Step 2: Run `perform` with the exact proposal digest** + +Substitute the actual digest into the Task 9 confirmation and run the command. +Do not retry manually after an ambiguous result; resume only through the same +CLI and journal. + +- [ ] **Step 3: Run independent `verify`** + +Run Task 10's verify command plus independent read-only `gh`/npm CLI checks. +Require only survivor `379991871`, both deleted IDs 404/list-absent, unchanged +45 survivor assets, unchanged tag, disabled Release, zero active runs, and all +npm versions absent. + +- [ ] **Step 4: End the main-change freeze** + +End it only after the final receipt is canonical and verify passes. If main +moved during the window, preserve proposal/journal and stop for the reviewed +successor-controller migration; do not use an override. + +### Task 15: Publish the receipt and resume the release program + +**Files:** +- Add: `scripts/release/duplicate-draft-consolidation.json` +- Modify only if required: release runbook blocker/status text. + +- [ ] **Step 1: Create a focused receipt branch from the verified checkout** + +Commit only the canonical receipt and accurate runbook status. Do not include +`.dawn` private files or asset payloads. + +- [ ] **Step 2: Re-run receipt and release-integrity verification** + +Run verify, focused tests, full release-controller tests, docs check, and +`git diff --check`. Request review and merge the receipt follow-up. + +- [ ] **Step 3: Rebase the larger abandonment branch** + +Rebase `blove/single-owner-release-abandonment` on the new `main`, resolve only +real overlaps, and rerun its Task 12 integration/Definition-of-Done gate. + +- [ ] **Step 4: Resume the approved release sequence** + +Continue trusted-publisher cutover, abandon v0.8.22, cut v0.8.23 with +provenance, run the full smoke tests including the real Vercel deployment lane, +and verify production. No compatibility shim, dependency override, Vercel CLI +removal, or CI-lane removal is part of this plan. diff --git a/docs/superpowers/specs/2026-09-01-duplicate-draft-consolidation-design.md b/docs/superpowers/specs/2026-09-01-duplicate-draft-consolidation-design.md new file mode 100644 index 000000000..ec0c00a44 --- /dev/null +++ b/docs/superpowers/specs/2026-09-01-duplicate-draft-consolidation-design.md @@ -0,0 +1,689 @@ +# Duplicate Draft Consolidation Design + +**Date:** 2026-09-01 +**Status:** Implementation complete; live execution pending +**Repository:** `cacheplane/dawnai` +**Candidate:** `v0.8.22` at `2a80deece2ff958fe7fde8fddeb4f99bed70a1c8` + +## Summary + +The v0.8.22 recovery is blocked because GitHub currently contains three mutable, +marker-identical `ESCROWED` drafts for one candidate. Dawn deliberately refuses +to choose between duplicate managed Releases. Before the npm trust cutover or +prepublication abandonment can proceed, the duplicate set must converge to one +unambiguous managed draft. + +The consolidation preserves Release `379991871`, which the previously approved +immutable-draft recovery design explicitly identified as the existing v0.8.22 +draft. A dedicated local operator CLI will prove complete parity, delete only +Releases `379982100` and `379986168`, verify the unchanged survivor, and emit a +canonical public receipt. The Release workflow remains disabled throughout the +live consolidation. + +This is single-owner recovery. It provides deterministic evidence, exhaustive +read-before-write checks, resumability, and a stable survivor, but it does not +claim dual control or a GitHub-side atomic compare-and-delete primitive. + +## Current Evidence + +Read-only observations on 2026-09-01 found these managed drafts: + +| Role | Release ID | Opaque `tag_name` | +| --- | ---: | --- | +| survivor | `379991871` | `untagged-be0ff4bee4ba43b521a9` | +| duplicate | `379982100` | `untagged-a13939767dd2419ade01` | +| duplicate | `379986168` | `untagged-20706099efa3c38335a8` | + +All three are mutable drafts named `Dawn v0.8.22`, target `main`, contain the +same canonical `ESCROWED` body, identify the same candidate and annotated tag, +and expose the same 45-name asset projection. The Release and asset IDs differ, +as expected for independent GitHub objects. Live state must be re-read before +any action; this table is evidence, not continuing authority. + +The survivor is not chosen by numeric ordering or most-recent mutation. Release +`379991871` is pinned because the earlier approved recovery design and plan +explicitly named it as the draft to converge and preserve. + +## Goals + +- Converge exactly three equivalent v0.8.22 managed drafts to exactly one. +- Preserve Release `379991871`, its opaque draft identity, body, and all 45 + assets without mutation. +- Delete only Releases `379982100` and `379986168` after exhaustive equality + and live-authority checks. +- Emit a canonical, bounded, self-verifying receipt suitable for source control. +- Make interruption after the first deletion safely resumable while the + required main-change freeze remains intact. +- Keep Release disabled and repeatedly observe npm absence throughout + consolidation. This excludes the active Release automation but cannot prevent + an out-of-band publisher from racing the operator. +- Restore the exactly-one-draft precondition required by the abandonment + controller and subsequent trust cutover. + +## Non-goals + +- Do not abandon v0.8.22 in this operation. +- Do not publish, retag, rename, edit, or recreate any Release. +- Do not delete or reuse the annotated `v0.8.22` tag. +- Do not add a third Release workflow operation or temporarily enable Release. +- Do not teach ordinary candidate discovery to choose among duplicates. +- Do not retain duplicate asset payloads in Git; the payload-identical survivor is + the recovery source. Deleting a duplicate permanently removes its GitHub + Release and asset identities, URLs, timestamps, and service history; the + operator evidence retains the last observed identities and payload digests, + not a recreatable GitHub object. +- Do not begin the npm trusted-publisher cutover. + +## Considered Approaches + +### 1. Preserve the recorded survivor and delete exact duplicates — selected + +This restores the controller's one-object invariant with the smallest live +change. It retains the draft already named by the prior recovery design and +removes only redundant GitHub objects after byte-level parity checks. + +### 2. Abandon all three drafts + +This would leave three terminal records for one candidate, complicate discovery, +and preserve the ambiguity the controller is designed to reject. It also turns +one release incident into three apparently authoritative release records. + +### 3. Edit two drafts so they no longer look managed + +Removing or changing markers would be a fragile manual repair. It would retain +misleading drafts, require mutation of body evidence, and create behavior that a +future controller change could accidentally rediscover. + +## Architecture + +### Dedicated operator command + +Add a dedicated CLI and production module for this one recovery class. It is +not a dormant abandonment command and is not reachable from `release.yml`. + +The CLI has three modes: + +- `inspect`: read and compare the exact live set; write a canonical proposed + consolidation record without mutating GitHub. +- `perform`: repeat all live reads, validate the proposed record, use a durable + write-ahead journal while deleting the two exact duplicate IDs in + deterministic order, and write a final receipt. +- `verify`: strictly parse and independently verify a final receipt against + current GitHub state. + +The implementation should separate strict parsing/classification, GitHub/npm +adapters, and mutation orchestration so tests exercise production logic without +shelling around it. + +### Exact command contract + +The survivor and duplicate IDs are explicit inputs, not discovered by sorting: + +```text +survivor: 379991871 +duplicates: 379982100,379986168 +``` + +`perform` requires the canonical SHA-256 of the reviewed `inspect` record and an +exact confirmation string containing the version, candidate SHA, survivor ID, +and ordered duplicate IDs. Both values are persisted before any write. The CLI +rejects unknown, duplicate, reordered, missing, or additional IDs. The survivor +ID is excluded from every deletion adapter by type and runtime checks. + +For this incident the exact string is: + +```text +CONSOLIDATE v0.8.22 2a80deece2ff958fe7fde8fddeb4f99bed70a1c8 SURVIVOR 379991871 DELETE 379982100,379986168 PROPOSAL <64-lowercase-hex-digest> +``` + +The command runs only from a clean checkout of the exact merged focused +consolidation change. Local Git HEAD, `origin/main`, and the GitHub +default-branch SHA must agree before the first writer and before the second +writer. + +### GitHub and npm adapters + +Use bounded, paginated production readers for Releases, assets, annotated tags, +workflow states, workflow runs, and asset downloads. Authentication remains in +the environment or the existing `gh` session; live credentials never appear in +argv, receipts, logs, or source files. Tests use only neutral synthetic secret +values that do not resemble provider-issued credentials. + +Release, asset, and workflow-run enumeration uses `per_page=100`, follows only +validated GitHub Link relations, accepts at most 100 pages and 10,000 raw +records, rejects duplicate IDs across pages, and requires stable reported totals +where the endpoint supplies them. + +The only writer deletes one exact Release ID. It accepts the statically validated +duplicate-ID set and cannot receive the survivor ID. GitHub documents `204` and +`404` as the delete endpoint outcomes and does not document conditional +`DELETE` for this endpoint, so the design does not claim compare-and-delete: + +- +- + +## Equality Contract + +Before deletion, exactly three managed v0.8.22 drafts must exist and no +published Release may match the candidate. Parsers require the known fields and +their types but tolerate additive GitHub response fields. Equality is defined +only by the following explicit semantic projections. + +The Release projection is: + +```text +name, target_commitish, draft, immutable, prerelease, published_at, +canonical body bytes, body SHA-256, author login/id/node_id +``` + +It must be identical for all three, with `draft: true`, `immutable: false`, +`prerelease: false`, `published_at: null`, target `main`, and the expected +single-owner author. The body must round-trip through `parseReleaseMarker` and +`canonicalReleaseBody` and bind the exact `ESCROWED` marker, candidate +version/SHA, annotated tag, revision, sealed-manifest digest, +release-record digest, canonical `baseAssetSetSha256`, and 22-subject +attestation set. + +Release `id`, `node_id`, opaque `tag_name`, derived URLs, and +`created_at`/`updated_at` are validated and recorded but intentionally excluded +from equality. Unmentioned optional or additive API fields are not compared. + +The per-name asset projection is: + +```text +name, label, state, content_type, size, digest, +uploader login/id/node_id, downloaded SHA-256 +``` + +It must be identical for all three and each downloaded byte sequence must be +equal. Asset `id`, `node_id`, derived URLs, timestamps, and `download_count` are +validated and recorded but excluded from equality. Missing required fields, +duplicate names/IDs, non-`uploaded` state, absent or malformed GitHub SHA-256, +and unknown assets fail closed. + +The 45-asset namespace is the existing production escrow contract, not a new +consolidation-specific digest: + +- one canonical `release-record.json`, parsed by `parseReleaseRecord` and + compared to `canonicalReleaseRecordBytes`; +- 22 ordered subjects: canonical `manifest.json` plus the 21 fixed-group + package archives, with the manifest parsed by `parseSealedReleaseManifest`; +- 22 ordered `.intoto.jsonl` bundle assets bound by `parseAttestationSet` and + verified through the production attestation-bundle verifier; +- the exact ordered `{name, sha256}` projection produced by + `canonicalBaseAssetSet`, whose digest must equal marker + `baseAssetSetSha256`. + +The implementation reuses these production parsers and verification routines; +if an internal verifier must become exported, that export is part of the +focused change. A separately named `consolidationPayloadSha256` may hash the +canonical three-Release equality projection for proposed-record binding, but it is +never substituted for `baseAssetSetSha256`. + +The annotated `v0.8.22` tag must remain annotated and peel to +`2a80deece2ff958fe7fde8fddeb4f99bed70a1c8`. All 21 fixed-group npm `0.8.22` +endpoints must return exact E404 in two complete observations at least 60 +seconds apart. Release must be `disabled_manually`, and no nonterminal Release +run may exist. + +Freshness follows the existing abandonment chronology. The first npm inventory +is captured before the heavy work; downloads and byte verification occur during +the observation gap; the second complete inventory is captured at least 60 +seconds later. Final main/workflow/run/tag/Release reads follow it, and the +first `DELETE` must begin while that second observation is no more than two +minutes old. After first-delete convergence, a fresh complete npm E404 inventory +is required immediately before the second writer. A final complete E404 +inventory is required after both deletions. These checks observe absence; they +do not reserve the npm namespace or exclude an out-of-band publisher. + +All downloaded bytes and aggregate reads are bounded by the existing 64 MiB +per-Release escrow limit and an explicit 192 MiB three-Release aggregate +ceiling. At most 45 assets per Release and 135 asset downloads overall are +accepted. Pagination exhaustion, +duplicate IDs/names, missing digests, invalid base64, unknown assets, or a fourth +matching draft blocks the operation. + +## Proposed Record, Journal, and Final Receipt + +The three files have distinct exact-schema envelopes: + +```text +.dawn/release/duplicate-draft-consolidation.proposed.json +.dawn/release/duplicate-draft-consolidation.journal.json +scripts/release/duplicate-draft-consolidation.json +``` + +Each envelope is `{ "record": , "recordSha256": }`. +`recordSha256` is SHA-256 over the canonical UTF-8 JSON bytes of `record` plus +one newline; the digest field is outside the hashed projection. The proposed, +journal, and receipt records use separate schema identifiers and exact field +sets. All arrays have fixed canonical order, timestamps are canonical UTC, and +the maximum serialized sizes are 4 MiB for proposed, 72 MiB for journal, and 96 +MiB for final receipt. Unknown/missing fields, noncanonical bytes, invalid +UTF-8, duplicate keys, digest mismatch, or excessive size are rejected. + +The limit module also defines 8 MiB for one authority stage, 2 MiB for one +survivor evidence record, 8 MiB journal-event reserve, and 1 MiB +canonical-envelope reserve. Initialization and tests require: + +```text +journalBytes >= ((targets * maximumAttempts) + finalStages + + maximumOrphanAuthorityRecoveries) * + authorityStageBytes + journalEventReserveBytes + +finalReceiptBytes >= proposedBytes + journalBytes + + authorityStageBytes + survivorEvidenceBytes + + envelopeReserveBytes +``` + +With two targets, three attempts each, one final stage, and exactly one orphan +authority recovery for the whole operation, the journal minimum is 72 MiB. A +second orphan-authority recovery stops without appending. The chosen 96 MiB +final cap preserves 9 MiB of additional headroom over its 87 MiB minimum. Tests +exercise the maximum eight-authority-stage history and both relationships. + +### Exact shared records + +These field lists are normative, including field order. Every object is exact; +nullable values remain present as `null`. + +```text +repository: {name, id, defaultBranch, actor} +actor: {login, id} +controller: {headSha, originMainSha, githubMainSha} +candidate: {version, commitSha, tag} +roles: {survivor, duplicates} +confirmation: {version, commitSha, survivor, duplicates, template} +annotatedTag: {name, objectSha, targetSha, objectType, observedAt} + +workflowAuthority: + {workflowId, path, state, query, nonterminalRuns, observedAt} +workflow query: {statuses, perPage, maximumPages} +workflow run: + {id, runAttempt, status, event, headSha, headBranch} + +npmInventory: {stage, startedAt, completedAt, packages} +npm package observation: + {name, version, status, httpStatus, code, observedAt} + +releaseEvidence: + {role, id, nodeId, tagName, createdAt, updatedAt, semantic, assets} +release semantic: + {name, targetCommitish, draft, immutable, prerelease, publishedAt, + body, bodySha256, author} +release author / asset uploader: {login, id, nodeId} +assetEvidence: + {id, nodeId, name, label, state, contentType, size, digest, uploader, + createdAt, updatedAt, downloadCount, downloadSha256} + +payloadProof: + {baseAssetSet, baseAssetSetSha256, consolidationPayloadSha256, + attestationVerification} +baseAssetSet entry: {name, sha256} +attestationVerification: {status, subjects} +verified subject: {name, sha256} + +authorityStage: + {stage, controller, annotatedTag, workflowAuthority, npmInventory, + releases, payloadProof, targetRead, observedAt} +targetRead: + {releaseGetStartedAt, releaseGetCompletedAt, assetsListStartedAt, + assetsListCompletedAt, evidence, evidenceSha256} +``` + +IDs use canonical positive decimal strings; SHAs and digests use canonical +lowercase hexadecimal. `controller` requires all three SHAs to match. +`roles.survivor` and `roles.duplicates` are the fixed Release IDs in approved +order. `workflowAuthority.state` must be `disabled_manually`, `path` must be +`.github/workflows/release.yml`, and `nonterminalRuns` must be empty after the +bounded query. The query is exactly statuses `["in_progress","pending", +"queued","requested","waiting"]`, `perPage: 100`, and `maximumPages: 100`. +Every npm package observation must be exact `ABSENT`, HTTP 404, code `E404`, +in canonical fixed-group order. + +Release/asset evidence includes service identity and volatile fields for honest +recording, while only the semantic projections in Equality Contract are used +for parity. `baseAssetSet` is the exact 45-entry ordered `{name, sha256}` array +from `canonicalBaseAssetSet`; `attestationVerification` is exactly `VERIFIED` +plus the ordered 22-subject `{name, sha256}` result from the production +verifier. Proposed npm stages are exactly `inspect-initial` and +`inspect-ready`. Perform stages are exactly `perform-initial`, +`pre-delete-1`, `pre-delete-2`, and `final`; the final receipt retains all six +stage-labeled inventories at minimum. A resumed attempt may append another +`perform-initial` plus the target's `pre-delete-1` or `pre-delete-2` inventory, +distinguished by event sequence and attempt number. + +`targetRead` is `null` only for the `final` authority stage. For every +pre-delete stage it embeds the complete direct-ID Release and paginated asset +evidence plus the digest of those canonical evidence bytes. Its timestamps must +be monotone, its evidence must equal the target entry semantically, and its +asset identities and metadata must be the latest accepted target view. + +### Proposed envelope + +The proposed `record` fields are exactly: + +```text +schemaVersion, repository, controller, candidate, roles, confirmation, +annotatedTag, workflowAuthority, npmInventories, releases, payloadProof, +inspectedAt +``` + +`confirmation.template` retains the literal `<64-lowercase-hex-digest>` +placeholder, avoiding self-reference to the proposed digest. `npmInventories` +contains the two inspect stages. `releases` contains survivor then duplicates. +The proposed record contains no credentials or full asset payloads. + +### Hash-chained journal envelope + +The journal `record` fields are exactly: + +```text +schemaVersion, repository, candidate, proposedRecordSha256, +confirmationSha256, deletionOrder, events, updatedAt +``` + +`events` is an append-only array of envelopes +`{ "event": , "eventSha256": }`. The event digest is over canonical +event bytes plus one newline. Every event is exactly +`{schemaVersion, sequence, previousEventSha256, type, recordedAt, payload}`; +sequence starts at one and the first previous digest is `null`. Later events +must bind the immediately preceding event digest. Event types and exact payloads +are: + +```text +operation-started: + {proposedRecordSha256, confirmationSha256, controllerSha, deletionOrder} +npm-observed: + {targetReleaseId, attemptNumber, inventory} +delete-authority-observed: + {targetReleaseId, attemptNumber, authority} +delete-intent: + {targetReleaseId, attemptNumber, authorityEventSha256} +delete-outcome: + {targetReleaseId, attemptNumber, classification, httpStatus, observedAt} +resume-reconciliation: + {targetReleaseId, attemptNumber, classification, releaseEvidence, observedAt} +absence-converged: + {targetReleaseId, attemptNumber, basis, directGet404At, listAbsentAt, + attempts, completedAt} +final-authority-observed: + {authority} +``` + +Allowed `delete-outcome.classification` values are `confirmed-204`, +`transport-ambiguous`, `response-404-ambiguous`, and `response-hard-failure`; +`httpStatus` is 204, null, 404, or the observed hard response status respectively +(`response-hard-failure` also uses null when the response shape cannot be safely +classified). Allowed resume classifications are +`present-unchanged-retryable` and `absent-ambiguous`; `releaseEvidence` is the +current complete target evidence for the former and `null` for the latter. +Allowed convergence `basis` values are `confirmed-204` and `ambiguous`. +All proposed, journal, final, and journal-event `schemaVersion` fields are the +integer `1`, interpreted by their distinct strict parser contexts. +`attemptNumber` is a canonical integer from 1 through 3; no target may receive +more than three `delete-intent` events. + +Every delete attempt embeds a complete immediately preceding +`authorityStage`—including fresh controller, workflow/run, tag, npm, remaining +Release/asset, and payload evidence—in `delete-authority-observed`. The intent +binds that event digest and is atomically durable before the request. A received +204 produces `confirmed-204`. Timeout, transport failure, or a received 404 is +ambiguous. A redirect, 403, 429, 5xx, or malformed response produces a durable +`response-hard-failure` and stops permanently before another writer. If the +process disappears before recording an outcome, resume reads the target: + +- present and semantically identical: append + `present-unchanged-retryable` only after refreshing every authority source + and fully hydrating the current 45-asset target evidence. Persist that actual + fresh evidence in the reconciliation, append the same captured authority for + the new attempt with no intervening network, persist a new intent, and only + then retry DELETE. If the prior + complete npm inventory is more than two minutes old, append a new + `perform-initial` inventory, wait at least 60 seconds while repeating the + heavy payload checks, then capture the target's new `pre-delete-1` or + `pre-delete-2` inventory; +- absent: append `absent-ambiguous` and perform bounded absence convergence; +- changed, published, or malformed: stop without another write. + +The same state decision applies after a durably recorded ambiguous outcome. If +the six-read/90-second window repeatedly observes the target present and +semantically unchanged, with complete enumeration still including it and every +other authority input unchanged, perform one full fresh authority capture, +append `present-unchanged-retryable` with that capture's actual current target +evidence, and bind the same capture to the fully fresh numbered attempt. If it +becomes absent, append +`absence-converged`. A changed target, reader disagreement/error, or a third +ambiguous attempt that remains present stops. A target present after a recorded +`confirmed-204` also stops; it is not eligible for retry. + +Only `absence-converged` completes a target. The next target cannot receive an +authority or intent event until the preceding target has that terminal event. +The hash chain is the complete transition history verified on every resume. + +### Final receipt envelope + +The final `record` fields are exactly: + +```text +schemaVersion, proposedEnvelope, journalEnvelope, finalAuthority, +finalSurvivor, completedAt +``` + +The embedded values are the complete canonical envelopes, not bare records or +summary hashes. `finalAuthority` is the exact `final` authority stage and +`finalSurvivor` is the same exact survivor `releaseEvidence` contained within +it. Canonical outcome is never renamed in the receipt: deletion certainty is +derived from the recorded outcome/resume classification plus the required +`absence-converged` event. + +The `.dawn` proposed and journal files are opened no-follow, must be regular +files owned by the operator, use mode `0600`, and are replaced through a +same-directory temporary file + fsync + atomic rename + parent-directory fsync. +Creation refuses an existing symlink or unsafe path. The tracked receipt uses a +separate source-file rule: no-follow regular file, expected owner, nonexecutable, +and no group/other write bits; ordinary Git mode `0644` is accepted. + +`verify` proves envelope/hash-chain integrity, current survivor equality, +deleted-ID absence, and current authority postconditions. It cannot +independently re-download deleted historical bytes; those are supported by the +embedded pre-delete observations and the survivor's still-live identical +payload. The first public durable copy is the focused receipt follow-up commit. + +## Mutation Flow + +1. Begin the operational main-change freeze: the sole owner makes no merge or + direct push to `main` until the final receipt is durable. Re-read + local/remote/main SHA identity and require a clean merged checkout. +2. Validate the reviewed proposed envelope and exact confirmation. Append + `operation-started` to a new journal. +3. Require Release `disabled_manually`, zero nonterminal Release runs, and the + exact three-ID managed set. Capture and append the complete + `perform-initial` npm E404 inventory bound to target `379982100`, attempt 1. +4. During the at-least-60-second gap, download all 135 asset instances, apply + production escrow/attestation verification, and prove the exact equality + projections against the proposal. +5. Capture the `pre-delete-1` complete npm E404 inventory and all other fresh + authority. After every other network read, directly GET Release `379982100` + by ID and completely enumerate its assets; record this terminal target read + in `delete-authority-observed` and require it semantically equal to the + proposal. Then perform only the local atomic journal write for + `delete-intent` before issuing DELETE—no intervening network request—while + the npm inventory is no more than two minutes old. +6. Delete Release `379982100`; append the exact outcome or reconcile an + interrupted intent. Run bounded read-only convergence; append + `absence-converged` if absent, or use the bounded new-attempt path if an + ambiguous outcome remains present and unchanged. +7. Require direct GET of `379982100` to return 404 and a complete paginated + Release enumeration to exclude it, while the survivor and Release + `379986168` remain projection/payload-identical to the proposal. +8. Capture complete `pre-delete-2` controller/workflow/run/tag/npm and remaining + Release/asset evidence. End the authority stage with direct GET of Release + `379986168` plus its complete asset enumeration. Append the resulting + `delete-authority-observed`, then perform only the local atomic intent write + before DELETE. Any intervening network request invalidates the authority + stage and requires a fresh one. +9. Delete Release `379986168`, append or reconcile the exact outcome, and + require the same two-source bounded absence convergence while the survivor + remains unchanged. +10. Capture and append the complete `final` authority stage. Require only + survivor `379991871`, unchanged body and 45 downloaded assets, unchanged + annotated tag, Release still disabled, no nonterminal run, and a final + complete npm E404 inventory. +11. Atomically write the final canonical receipt, independently verify its + envelopes/hash chain/current-state claims, then end the main-change freeze. + +Convergence retries only reads: at most six complete direct-GET/list attempts +within 90 seconds, with bounded backoff no longer than 30 seconds. `403`, `429`, +`5xx`, timeouts, pagination failure, +or changed/discordant evidence blocks the next writer. An exact unchanged target +after an ambiguous outcome may retry only under the three-attempt protocol +above; exhausting it blocks. GitHub does not provide this +design with an atomic multi-Release transaction or a documented conditional +delete. Single-operator exclusivity and disabled Release automation minimize +the GET-to-DELETE race but do not eliminate it; the receipt records the last +observation, not a server-side compare-and-swap. Exact-ID writers, immediate +full re-reads, deterministic order, the write-ahead journal, and postconditions +make uncertainty visible rather than guessed away. + +The main-change freeze is a required operational precondition, not an API lock. +The command rechecks all three main SHAs before each DELETE and at finalization. +If `main` advances after the first deletion, the command stops and no longer +claims automatic resumability. The operator preserves the proposed/journal +files and requests a focused reviewed migration change. That change must name +the exact old and successor controller SHAs, parse and verify the old envelopes +and event chain without rewriting them, prove the successor preserves this +command's schemas and safety behavior, emit a migration event/receipt binding +both SHAs, and repeat the full fresh authority sequence before authorizing a +remaining delete. There is no unreviewed SHA override or automatic migration. + +## Interruption and Recovery + +While the main-change freeze remains intact, the operation is resumable only +through the exact same controller CLI and journal. Resume first validates the +proposed envelope, confirmation digest, repository/controller identity, and +complete hash-chained event history; it never infers permission to delete from +current absence alone. If the freeze was violated, only the reviewed migration +procedure above may restore authority. + +- Before any intent: the journal may be discarded and `perform` rerun. The + proposal is reference evidence rather than fresh mutation authority; all live + parity and authority reads are repeated by `perform`, and any mismatch stops. +- After `delete-intent` with no later event, run the bounded direct GET and + complete paginated-list reads. If they repeatedly prove the target present + and semantically unchanged, perform one full fresh Task 5 authority capture, + append `present-unchanged-retryable` with that capture's actual target + evidence, append the same captured authority for the new attempt, and then + persist intent, mint the one-use permit, and DELETE with no intervening + network. If the bounded reads prove absence, append `absent-ambiguous` and + reconcile. Any other state stops. +- After an ambiguous `delete-outcome`, preserve its exact classification. If + direct GET returns 404 and complete paginated enumeration excludes the ID, + reconcile absence. If the bounded reads instead repeatedly prove the target + present and exactly unchanged, perform one full fresh Task 5 authority + capture, append `present-unchanged-retryable` with that capture's actual + target evidence, append the same captured authority for the fully fresh + numbered attempt, and then persist intent, mint the one-use permit, and + DELETE with no intervening network, subject to the three-attempt cap. Any + other result stops. A `confirmed-204` target that is present stops. +- After resolving `379982100`: require Release `379986168` and the survivor still + exactly match the proposal, repeat all authority checks including fresh npm + absence, then continue with the second deletion. +- After both deletions but before receipt write: require the exact final state and + materialize the same final receipt. +- If the survivor is missing or changed, a deleted duplicate reappears, an + unexpected draft appears, publication begins, or evidence differs: stop. Do + not recreate, edit, or delete anything else automatically. + +The survivor's verified payload bytes are the recovery source for the removed +duplicate payloads. Deleted Release/asset identities and service history are +not recoverable; the receipt preserves only their last observed values and byte +digests. No automatic duplicate recreation is part of this recovery. + +## Failure and Stop Conditions + +Stop before mutation or before the next deletion on any of these conditions: + +- survivor or duplicate ID mismatch; +- additional, missing, published, immutable, or malformed candidate Release; +- metadata, body, marker, manifest, asset namespace, size, digest, or downloaded + byte mismatch; +- moved, lightweight, missing, or retargeted annotated tag; +- any npm result other than exact E404; +- Release not disabled or any nonterminal Release run; +- local, origin, or GitHub main SHA disagreement; +- actor/repository mismatch, pagination overflow, aggregate byte overflow, or + malformed API response; +- deletion response ambiguity that is neither reconciled absent nor proven + present-and-unchanged within the bounded window, exhaustion of three attempts, + or any ambiguous result followed by changed remaining objects; +- proposed/journal/receipt drift or noncanonical bytes. + +There is no force mode, survivor override, ID auto-selection, delete-all option, +inline manual repair path, tag mutation, or best-effort continuation. The +reviewed successor-controller migration is a new focused recovery change, not a +runtime bypass. + +## Testing and Rehearsal + +Implementation follows test-first development and must cover: + +- exact three-way equality and aggregate byte comparison; +- body/marker/manifest/attestation/asset mismatches; +- extra, missing, reordered, duplicate, or unknown Release/asset IDs; +- survivor ID reaching the deletion adapter; +- active runs, workflow-state drift, main drift, tag drift, npm publication, and + pagination/byte-bound failures; +- runner loss before deletion, after the first deletion, after the second + deletion, and before final receipt materialization; +- request timeout and process loss after server-side delete but before response + or journal update, preserving the distinction between `confirmed-204` and + reconciled ambiguity; +- direct-GET/list disagreement, delayed delete convergence, exhausted read-only + retries, and every `403`/`429`/`5xx`/timeout stop path; +- crash after intent while the target remains unchanged, safe fresh-authority + retry as a new attempt, crash after server-side absence, and hash-chain + tampering/reordering/truncation; +- main advance before either writer and after the first deletion, proving the + current CLI stops and cannot accept an unreviewed successor SHA; +- idempotent resume from each legal journal state; +- rejection of every illegal partial state; +- proposed/journal/final exact schemas, hash projections, canonicalization, + safe-file behavior, atomic transitions, tamper detection, and independent + live verify without claiming historical-byte revalidation; +- a realistic three-draft fake with distinct Release and asset IDs but exact + same-name bytes; +- proof that Release stays disabled and the survivor is never written. + +The full release-controller suite, docs checks, repository Definition of Done, +and Docker-required validation run before any live consolidation. The dedicated +CLI and every workflow-reachable dependency remain content-pinned where the +release integrity policy requires it. + +## Delivery and Live Sequence + +The duplicate repair is a focused prerequisite, not a new operation bundled +into the larger single-owner abandonment branch. This explicitly supersedes the +old Task 13/14 ordering while the duplicate blocker exists. + +1. Branch from current `main` and implement only the dedicated consolidation + CLI, minimum shared production-parser exports, tests, and this design/plan. +2. Rehearse it locally and run the focused checks plus the repository Definition + of Done, including `DAWN_REQUIRE_DOCKER=1 pnpm ci:validate`. +3. Review and merge that focused prerequisite while Release remains disabled. +4. From a clean checkout of the exact merged `main`, run `inspect`; review and + retain its proposed envelope; then run `perform` with its digest and the + exact confirmation. +5. Run `verify` and independent read-only GitHub/npm checks against the + one-survivor state. +6. Commit the final receipt in a focused follow-up change and rerun the relevant + release-integrity verification. This commit makes the embedded pre-delete + evidence public; it does not make deleted GitHub service identities + recreatable. +7. Rebase the larger single-owner abandonment work on that main, rerun its + integration gate, then resume its trust cutover and abandonment tasks. +8. Only after those gates abandon v0.8.22, cut v0.8.23 with provenance, run the + complete smoke tests, and verify production. + +No step in this design removes the Vercel CLI, the real `vercel-native` lane, or +the later independent production deployment verification. diff --git a/package.json b/package.json index be8b3d3f7..2c99049dc 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "published:smoke": "node scripts/published-artifact-smoke.mjs", "published:verify": "node scripts/published-artifact-verify.mjs", "release:observe": "node scripts/release/cli.mjs observe", + "release:consolidate-drafts": "node scripts/release/duplicate-draft-consolidation-cli.mjs", "release:preflight": "node scripts/release/preflight.mjs", "release:rehearse": "node scripts/release/test/support/fault-harness.mjs", "sync:chart-appversion": "node scripts/sync-chart-appversion.mjs", diff --git a/scripts/release/duplicate-draft-consolidation-adapters.mjs b/scripts/release/duplicate-draft-consolidation-adapters.mjs new file mode 100644 index 000000000..e1fbcda5c --- /dev/null +++ b/scripts/release/duplicate-draft-consolidation-adapters.mjs @@ -0,0 +1,3206 @@ +import { AsyncLocalStorage } from "node:async_hooks" +import { createHash } from "node:crypto" +import path from "node:path" +import { isDeepStrictEqual, types as utilTypes } from "node:util" + +import { normalizeAdapterEnvelope, snapshotJson } from "./adapter-normalize.mjs" +import { createGitHubReader as defaultCreateGitHubReader } from "./adapters/github.mjs" +import { createHttpGet } from "./adapters/http.mjs" +import { createNpmReader as defaultCreateNpmReader } from "./adapters/npm.mjs" +import { createCliAttestationVerifier as defaultCreateCliAttestationVerifier } from "./artifact-store.mjs" +import { captureConsolidationAuthorityCore } from "./duplicate-draft-consolidation-authority-core.mjs" +import { + assertEvidenceEqualsProposal, + captureDirectTargetRead, +} from "./duplicate-draft-consolidation-evidence.mjs" +import { + readPrivateEnvelope, + writePrivateEnvelope, +} from "./duplicate-draft-consolidation-files.mjs" +import { + appendJournalEvent, + deriveConsolidationState, + parseConsolidationJournal, +} from "./duplicate-draft-consolidation-journal.mjs" +import { + classifyConsolidationReleases, + consolidationStageRule, +} from "./duplicate-draft-consolidation-release-classifier.mjs" +import { + canonicalConsolidationEnvelopeBytes, + canonicalRecordSha256, + createConsolidationEnvelope, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS, + parseConsolidationEnvelope, +} from "./duplicate-draft-consolidation-schema.mjs" +import { CANONICAL_RELEASE_PACKAGE_ORDER } from "./manifest.mjs" +import { createOwnerPreflightAdapters as defaultCreateOwnerPreflightAdapters } from "./preflight-owner-adapters.mjs" +import { createReleasePreparationRunner as defaultCreateReleasePreparationRunner } from "./process-runner.mjs" + +const REPOSITORY = "cacheplane/dawnai" +const OWNER = "cacheplane" +const REPO = "dawnai" +const API_ORIGIN = "https://api.github.com" +const API_VERSION = "2022-11-28" +const JSON_ACCEPT = "application/vnd.github+json" +const USER_AGENT = "dawn-duplicate-draft-consolidation/1" +const RELEASE_WORKFLOW = ".github/workflows/release.yml" +const APPROVED_TAG = "v0.8.22" +const APPROVED_CANDIDATE = Object.freeze({ + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + tag: APPROVED_TAG, +}) +const SURVIVOR_ID = "379991871" +const DUPLICATE_IDS = Object.freeze(["379982100", "379986168"]) +const MAX_PAGES = 100 +const MAX_RECORDS = 10_000 +const MAX_TOKEN_BYTES = 4_096 +const MAX_DIRECT_JSON_BYTES = 8 * 1024 * 1024 +const DELETE_TIMEOUT_MS = 15_000 +const MAX_DELETE_TIMEOUT_MS = 60_000 +const MAX_LINK_HEADER_BYTES = 16_384 +const TIMESTAMP_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/u +const SHA_PATTERN = /^[0-9a-f]{40}$/u +const ID_PATTERN = /^[1-9][0-9]*$/u +const NATIVE_DATE = Date +const NATIVE_DATE_NOW = Date.now +const NATIVE_DATE_TO_ISO_STRING = Date.prototype.toISOString +const NONTERMINAL_STATUS_ORDER = Object.freeze([ + "in_progress", + "pending", + "queued", + "requested", + "waiting", +]) +const PAGINATION_RELATIONS = new Set(["first", "last", "next", "prev"]) +const ROOT_OPTION_FIELDS = new Set(["cwd", "token", "environment", "dependencies", "requestBudget"]) +const DEPENDENCY_FIELDS = new Set([ + "fetchImpl", + "run", + "now", + "createGitHubReader", + "createOwnerPreflightAdapters", + "createNpmReader", + "createCliAttestationVerifier", + "createReleasePreparationRunner", +]) +const DELETE_OPTION_FIELDS = new Set([ + "repository", + "apiOrigin", + "survivorId", + "duplicateIds", + "token", + "fetchImpl", + "timeoutMs", + "now", +]) +const DELETE_CALL_FIELDS = new Set(["releaseId", "signal", "permit"]) +const SAFE_ENVIRONMENT_NAMES = new Set([ + "CI", + "COLORTERM", + "COMSPEC", + "FORCE_COLOR", + "GITHUB_ACTIONS", + "HOME", + "LANG", + "LC_ALL", + "PATH", + "Path", + "PATHEXT", + "SYSTEMROOT", + "TEMP", + "TMP", + "TMPDIR", + "TERM", + "USERPROFILE", +]) +const WINDOWS_SAFE_ENVIRONMENT_NAMES = new Map( + [...SAFE_ENVIRONMENT_NAMES].map((name) => [name.toUpperCase(), name === "Path" ? "PATH" : name]), +) +const WINDOWS_ENVIRONMENT_MARKERS = new Set(["COMSPEC", "PATHEXT", "SYSTEMROOT"]) +const DELETE_WRITER_IDENTITIES = new WeakMap() +const DELETE_PERMIT_BINDINGS = new WeakMap() + +export async function createDuplicateDraftConsolidationAdapters(options) { + const root = exactDataOptions(options, ROOT_OPTION_FIELDS, "Adapter options") + const cwd = normalizedRoot(required(root, "cwd", "Adapter root")) + const requestBudget = Object.hasOwn(root, "requestBudget") + ? normalizeRequestBudget(root.requestBudget) + : null + const environment = Object.hasOwn(root, "environment") + ? snapshotEnvironment(root.environment) + : snapshotRuntimeEnvironment(process.env) + const dependencies = exactDataOptions( + Object.hasOwn(root, "dependencies") ? root.dependencies : {}, + DEPENDENCY_FIELDS, + "Adapter dependencies", + ) + const fetchImpl = dependencyFunction(dependencies, "fetchImpl", fetch) + const now = dependencyFunction(dependencies, "now", () => new Date().toISOString()) + const networkGuard = createNetworkGuard({ cwd, now }) + const guardedFetch = (url, init) => + networkGuard.runRequest("network transport", () => + fetchImpl(url, requestBudget === null ? init : applyRequestBudget(init, requestBudget)), + ) + const createGitHubReader = dependencyFunction( + dependencies, + "createGitHubReader", + defaultCreateGitHubReader, + ) + const createOwnerPreflightAdapters = dependencyFunction( + dependencies, + "createOwnerPreflightAdapters", + defaultCreateOwnerPreflightAdapters, + ) + const createNpmReader = dependencyFunction( + dependencies, + "createNpmReader", + defaultCreateNpmReader, + ) + const createCliAttestationVerifier = dependencyFunction( + dependencies, + "createCliAttestationVerifier", + defaultCreateCliAttestationVerifier, + ) + const createReleasePreparationRunner = dependencyFunction( + dependencies, + "createReleasePreparationRunner", + defaultCreateReleasePreparationRunner, + ) + const run = Object.hasOwn(dependencies, "run") + ? dependencies.run + : createReleasePreparationRunner({ + commandTimeoutMs: 15_000, + overallTimeoutMs: 10 * 60_000, + maxOutputBytes: 2 * 1024 * 1024, + }) + assertFunction(run, "Adapter command runner") + + const safeEnvironment = subprocessEnvironment(environment) + const injectedToken = Object.hasOwn(root, "token") + ? root.token + : Object.hasOwn(environment, "GH_TOKEN") + ? environment.GH_TOKEN + : environment.GITHUB_TOKEN + const token = + injectedToken === undefined + ? await resolveGhToken({ cwd, environment: safeEnvironment, run }) + : canonicalToken(injectedToken) + const authenticatedFetch = githubFetch(guardedFetch) + const timestampNow = networkGuard.now + const wallNow = () => Date.parse(timestampNow()) + const githubReader = createGitHubReader({ + owner: OWNER, + repo: REPO, + token, + apiOrigin: API_ORIGIN, + fetchImpl: authenticatedFetch, + maxPages: MAX_PAGES, + maxRecords: MAX_RECORDS, + now: wallNow, + ...(requestBudget === null ? {} : { timeoutMs: requestBudget.timeoutMs }), + }) + const rawGithub = createIncidentGitHubReader({ + reader: githubReader, + fetchImpl: authenticatedFetch, + token, + now: timestampNow, + wallNow, + }) + const github = guardNetworkFacade(rawGithub, networkGuard, "GitHub reader") + const ownerRun = (command, args, options) => + run(command, args, { ...options, env: { ...safeEnvironment } }) + const ownerAdapters = createOwnerPreflightAdapters({ + cwd, + environment: safeEnvironment, + run: ownerRun, + }) + const rawLocal = createLocalGitReader({ + cwd, + environment: safeEnvironment, + run, + ownerAdapters, + }) + const local = guardNetworkFacade(rawLocal, networkGuard, "local Git reader") + const npmReader = createNpmReader({ fetchImpl: guardedFetch }) + const observePackageVersion = bindMethod( + npmReader, + "observePackageVersion", + "npm package-version reader", + ) + const npm = deepFreeze({ + async observePackageVersion(input) { + const result = await networkGuard.runRequest( + "npm package-version reader", + () => observePackageVersion(input), + [input], + ) + return deepFreeze( + normalizeAdapterEnvelope(containsProxy(result) ? null : result, { + source: "npm", + operation: "package-version", + payloadKey: "package", + }), + ) + }, + }) + const runGh = async (args) => { + if (!safeStringArray(args)) throw new TypeError("Attestation command arguments are invalid") + await networkGuard.runRequest("attestation verifier", () => + executeExact(run, "gh", args, { + cwd, + env: { ...safeEnvironment, GH_TOKEN: token }, + }), + ) + } + const attestationVerifier = createCliAttestationVerifier({ + repository: REPOSITORY, + token, + runGh, + }) + const verifyAttestations = bindMethod(attestationVerifier, "verify", "attestation verifier") + const attestations = deepFreeze({ + async verify(input) { + return networkGuard.runRequest( + "attestation verifier", + async () => normalizeAttestationResult(await verifyAttestations(input)), + [input], + ) + }, + }) + const rawWriter = createExactDuplicateDeleteEffect({ + repository: REPOSITORY, + apiOrigin: API_ORIGIN, + survivorId: SURVIVOR_ID, + duplicateIds: DUPLICATE_IDS, + token, + fetchImpl: guardedFetch, + timeoutMs: DELETE_TIMEOUT_MS, + now: timestampNow, + }) + networkGuard.bindWriter(rawWriter) + const writer = deepFreeze({ + async deleteDuplicate(input) { + const call = exactDataOptions( + input, + new Set(["releaseId", "signal", "permit"]), + "Guarded delete call options", + ) + const releaseId = canonicalStringId(required(call, "releaseId", "Delete Release ID")) + const permit = required(call, "permit", "Delete permit") + return networkGuard.runDelete(permit, releaseId, () => + rawWriter.deleteDuplicate({ + releaseId, + permit, + ...(call.signal === undefined ? {} : { signal: call.signal }), + }), + ) + }, + }) + + const adapters = { local, github, npm, attestations, writer } + Object.defineProperty( + adapters, + "captureConsolidationAuthority", + hiddenMethod((input) => { + return captureConsolidationAuthorityCore( + input, + networkGuard.createAuthorityCapability(rawGithub, adapters), + ) + }), + ) + Object.defineProperty( + adapters, + "captureInspectionTerminal", + hiddenMethod((input) => networkGuard.captureInspectionTerminal(rawGithub, input)), + ) + Object.defineProperty( + adapters, + "assertInspectionTerminalSealed", + hiddenMethod(() => networkGuard.assertInspectionTerminalSealed()), + ) + Object.freeze(adapters) + return adapters +} + +export function createExactDuplicateDeleteEffect(options) { + const value = exactDataOptions(options, DELETE_OPTION_FIELDS, "Delete effect options") + if (required(value, "repository", "Delete repository") !== REPOSITORY) { + throw new TypeError("Delete repository is not the approved incident repository") + } + if (required(value, "apiOrigin", "Delete API origin") !== API_ORIGIN) { + throw new TypeError("Delete API origin is not the approved trusted origin") + } + const survivorId = canonicalStringId(required(value, "survivorId", "Delete survivor ID")) + if (survivorId !== SURVIVOR_ID) { + throw new TypeError("Delete survivor is not the approved survivor") + } + const duplicateIds = snapshotStringArray( + required(value, "duplicateIds", "Delete duplicate IDs"), + "Delete duplicate IDs", + ).map(canonicalStringId) + if (!arraysEqual(duplicateIds, DUPLICATE_IDS) || duplicateIds.includes(survivorId)) { + throw new TypeError("Delete duplicate IDs are not the approved ordered duplicate set") + } + const approved = new Set(duplicateIds) + const token = canonicalToken(required(value, "token", "Delete token")) + const fetchImpl = requiredFunction(value, "fetchImpl", "Delete fetch implementation") + const timeoutMs = boundedInteger( + required(value, "timeoutMs", "Delete timeout"), + 1, + MAX_DELETE_TIMEOUT_MS, + "Delete timeout", + ) + const now = requiredFunction(value, "now", "Delete clock") + const writerIdentity = Object.freeze({}) + + const writer = deepFreeze({ + async deleteDuplicate(input) { + const call = exactDataOptions(input, DELETE_CALL_FIELDS, "Delete call options") + const releaseId = canonicalStringId(required(call, "releaseId", "Delete Release ID")) + const permit = required(call, "permit", "Delete permit") + const permitBinding = + permit !== null && typeof permit === "object" + ? DELETE_PERMIT_BINDINGS.get(permit) + : undefined + if ( + permitBinding === undefined || + permitBinding.writerIdentity !== writerIdentity || + permitBinding.releaseId !== releaseId || + permitBinding.used || + !permitBinding.armed + ) { + if (permitBinding !== undefined) permitBinding.used = true + throw new Error("Delete requires an armed one-use guard-minted permit") + } + permitBinding.armed = false + permitBinding.used = true + if ( + typeof permitBinding.assertActiveLease !== "function" || + typeof permitBinding.verifyPreSend !== "function" + ) { + throw new Error("Delete permit has no active journal transaction lease") + } + permitBinding.assertActiveLease() + if (releaseId === survivorId || !approved.has(releaseId)) { + throw new TypeError("Release ID is the survivor or is not an approved duplicate") + } + const callerSignal = call.signal + if (callerSignal !== undefined) assertAbortSignal(callerSignal) + if (callerSignal?.aborted === true) { + throw new Error("Duplicate Release deletion was aborted before send") + } + + permitBinding.assertActiveLease() + const beforeVerification = canonicalTimestamp(callClock(now)) + if (beforeVerification > permitBinding.authorityExpiresAt) { + throw new Error("Delete permit expired before final committed-state verification") + } + await permitBinding.verifyPreSend() + permitBinding.assertActiveLease() + const observedAt = canonicalTimestamp(callClock(now)) + if (observedAt > permitBinding.authorityExpiresAt) { + throw new Error("Delete permit expired at the writer's final pre-send clock") + } + const deadline = deleteDeadline(timeoutMs, callerSignal) + let response + try { + const pendingResponse = fetchImpl( + `${API_ORIGIN}/repos/${REPOSITORY}/releases/${releaseId}`, + { + method: "DELETE", + redirect: "manual", + headers: githubHeaders(token), + signal: deadline.signal, + }, + ) + response = await deadline.race(pendingResponse) + } catch { + deadline.dispose() + return deleteOutcome("transport-ambiguous", null, observedAt) + } + let normalized + try { + normalized = await deleteResponse(response, deadline) + } catch { + return deleteOutcome("response-hard-failure", null, observedAt) + } finally { + deadline.dispose() + } + if (normalized.status === 204) { + return deleteOutcome("confirmed-204", 204, observedAt) + } + if (normalized.status === 404) { + return deleteOutcome("response-404-ambiguous", 404, observedAt) + } + return deleteOutcome("response-hard-failure", normalized.status, observedAt) + }, + }) + DELETE_WRITER_IDENTITIES.set(writer, writerIdentity) + return writer +} + +function createIncidentGitHubReader({ reader, fetchImpl, token, now, wallNow }) { + const getRef = bindMethod(reader, "getRef", "GitHub reader") + const getGitTag = bindMethod(reader, "getGitTag", "GitHub reader") + const getWorkflow = bindMethod(reader, "getWorkflow", "GitHub reader") + const listReleases = bindMethod(reader, "listReleases", "GitHub reader") + const getRelease = bindMethod(reader, "getRelease", "GitHub reader") + const listReleaseAssets = bindMethod(reader, "listReleaseAssets", "GitHub reader") + const downloadReleaseAsset = bindMethod(reader, "downloadReleaseAsset", "GitHub reader") + const http = createHttpGet({ + fetchImpl, + timeoutMs: DELETE_TIMEOUT_MS, + maxResponseBytes: MAX_DIRECT_JSON_BYTES, + }) + + return deepFreeze({ + async getRepository() { + const body = await readDirectJson(http, BASE_URL(), token, "repository") + if (!isPlainRecord(body) || body.full_name !== REPOSITORY || body.default_branch !== "main") { + throw new TypeError("GitHub repository evidence is malformed") + } + return deepFreeze({ + name: REPOSITORY, + id: canonicalId(body.id), + defaultBranch: "main", + }) + }, + async getAuthenticatedUser() { + const body = await readDirectJson(http, `${API_ORIGIN}/user`, token, "authenticated user") + if (!isPlainRecord(body) || !safeLogin(body.login)) { + throw new TypeError("GitHub authenticated-user evidence is malformed") + } + return deepFreeze({ login: body.login, id: canonicalId(body.id) }) + }, + async getDefaultBranchSha() { + const value = presentValue(await getRef({ ref: "heads/main" }), "ref") + if ( + !isPlainRecord(value) || + value.ref !== "refs/heads/main" || + !isPlainRecord(value.object) || + value.object.type !== "commit" || + !isSha(value.object.sha) + ) { + throw new TypeError("GitHub default-branch evidence is malformed") + } + return value.object.sha + }, + async getWorkflowState() { + const value = presentValue(await getWorkflow({ workflow: "release.yml" }), "workflow") + if ( + !isPlainRecord(value) || + value.path !== RELEASE_WORKFLOW || + !safeBoundedString(value.state, 128) + ) { + throw new TypeError("GitHub workflow evidence is malformed") + } + return deepFreeze({ + workflowId: canonicalId(value.id), + path: RELEASE_WORKFLOW, + state: value.state, + }) + }, + async listNonterminalWorkflowRuns(input) { + const query = exactWorkflowRunQuery(input) + const runs = await readNonterminalWorkflowRuns(http, token, wallNow, query) + return deepFreeze({ query, runs }) + }, + async getAnnotatedTag(input) { + const call = exactDataOptions(input, new Set(["name"]), "Annotated-tag options") + if (required(call, "name", "Annotated tag name") !== APPROVED_TAG) { + throw new TypeError("Annotated tag is not the approved incident tag") + } + const ref = presentValue(await getRef({ ref: `tags/${APPROVED_TAG}` }), "ref") + if ( + !isPlainRecord(ref) || + ref.ref !== `refs/tags/${APPROVED_TAG}` || + !isPlainRecord(ref.object) || + ref.object.type !== "tag" || + !isSha(ref.object.sha) + ) { + throw new TypeError("GitHub annotated-tag ref evidence is malformed") + } + const tag = presentValue(await getGitTag({ tagSha: ref.object.sha }), "git-tag") + if ( + !isPlainRecord(tag) || + tag.sha !== ref.object.sha || + tag.tag !== APPROVED_TAG || + !isPlainRecord(tag.object) || + tag.object.type !== "commit" || + !isSha(tag.object.sha) + ) { + throw new TypeError("GitHub annotated-tag object evidence is malformed") + } + return deepFreeze({ + name: APPROVED_TAG, + objectSha: ref.object.sha, + targetSha: tag.object.sha, + objectType: "tag", + observedAt: now(), + }) + }, + async listReleases() { + return rejectDuplicateIds(await listReleases(), "releases", "DUPLICATE_RELEASE_ID") + }, + async getRelease(input) { + return normalizedGitHubEnvelope(await getRelease(input), "release", "value") + }, + async listReleaseAssets(input) { + return rejectDuplicateIds( + await listReleaseAssets(input), + "release-assets", + "DUPLICATE_ASSET_ID", + ) + }, + async downloadReleaseAsset(input) { + return normalizedGitHubEnvelope( + await downloadReleaseAsset(input), + "release-asset-download", + "contentBase64", + ) + }, + }) +} + +function exactWorkflowRunQuery(value) { + if (!isPlainRecord(value) || !Object.isFrozen(value)) { + throw new TypeError("Workflow-run query must be an exact deeply frozen object") + } + const query = exactDataOptions( + value, + new Set(["statuses", "perPage", "maximumPages"]), + "Workflow-run query", + ) + if (utilTypes.isProxy(query.statuses) || !Object.isFrozen(query.statuses)) { + throw new TypeError("Workflow-run query must be an exact deeply frozen object") + } + const statuses = snapshotStringArray(query.statuses, "Workflow-run query statuses") + if ( + !arraysEqual(statuses, NONTERMINAL_STATUS_ORDER) || + query.perPage !== 100 || + query.maximumPages !== MAX_PAGES + ) { + throw new TypeError("Workflow-run query does not match the exact status or page bounds") + } + return deepFreeze({ statuses, perPage: 100, maximumPages: MAX_PAGES }) +} + +function guardNetworkFacade(source, guard, label) { + const facade = {} + for (const name of Object.keys(source)) { + const method = bindMethod(source, name, label) + facade[name] = (...args) => guard.runRequest(`${label} ${name}`, () => method(...args), args) + } + return deepFreeze(facade) +} + +function expectedAuthorityTrace(proposal, stage) { + const exact = (label, args, validate, validateArgs) => ({ + label, + args: snapshotJson(args), + validate, + ...(validateArgs === undefined ? {} : { validateArgs }), + }) + const equals = (expected, label) => (actual) => { + if (!isDeepStrictEqual(actual, expected)) { + throw new Error(`${label} differs from the authority proposal`) + } + } + const steps = [ + exact("local Git reader readState", [], (actual) => { + if ( + actual.headSha !== proposal.controller.headSha || + actual.originMainSha !== proposal.controller.originMainSha || + actual.branch !== "main" || + actual.porcelainStatus !== "" + ) { + throw new Error("Local capture differs from the authority proposal") + } + }), + exact("GitHub reader getRepository", [], (actual) => { + equals( + { + name: proposal.repository.name, + id: proposal.repository.id, + defaultBranch: proposal.repository.defaultBranch, + }, + "Repository capture", + )(actual) + }), + exact( + "GitHub reader getAuthenticatedUser", + [], + equals(proposal.repository.actor, "Actor capture"), + ), + exact( + "GitHub reader getDefaultBranchSha", + [], + equals(proposal.controller.githubMainSha, "GitHub main capture"), + ), + exact("GitHub reader getWorkflowState", [], (actual) => { + const { workflowId, path: workflowPath, state: workflowState } = proposal.workflowAuthority + equals({ workflowId, path: workflowPath, state: workflowState }, "Workflow capture")(actual) + }), + exact( + "GitHub reader listNonterminalWorkflowRuns", + [proposal.workflowAuthority.query], + (actual) => { + equals( + { + query: proposal.workflowAuthority.query, + runs: proposal.workflowAuthority.nonterminalRuns, + }, + "Workflow-run capture", + )(actual) + }, + ), + exact("GitHub reader getAnnotatedTag", [{ name: proposal.candidate.tag }], (actual) => { + for (const name of ["name", "objectSha", "targetSha", "objectType"]) { + if (actual[name] !== proposal.annotatedTag[name]) { + throw new Error("Annotated-tag capture differs from the proposal") + } + } + }), + exact("GitHub reader listReleases", [], (actual, session) => { + if ( + actual.status !== "PRESENT" || + actual.operation !== "releases" || + actual.httpStatus !== 200 || + actual.code !== null || + !Array.isArray(actual.value) + ) { + throw new Error("Release-list capture is malformed") + } + const classification = classifyConsolidationReleases(actual.value, proposal, stage) + session.keyResults.set("releases", actual) + session.keyResults.set("release-classification", classification) + appendExpectedPayloadTrace( + session.expected, + proposal, + stage, + classification.selected, + exact, + equals, + ) + }), + ] + for (const name of CANONICAL_RELEASE_PACKAGE_ORDER) { + steps.push( + exact( + "npm package-version reader", + [{ name, version: proposal.candidate.version }], + equals( + { + status: "ABSENT", + operation: "package-version", + httpStatus: 404, + code: "E404", + }, + "npm capture", + ), + ), + ) + } + return steps +} + +function appendExpectedPayloadTrace(steps, proposal, stage, rawReleases, exact, equals) { + const { releaseIds: remainingIds, targetReleaseId } = consolidationStageRule(stage) + const fullyEnumerated = [] + for (const releaseId of remainingIds) { + const release = proposal.releases.find(({ id }) => id === releaseId) + const rawRelease = rawReleases.find(({ id }) => String(id) === releaseId) + if (release === undefined || rawRelease === undefined) { + throw new Error("Authority proposal is missing a required Release") + } + steps.push( + exact("GitHub reader listReleaseAssets", [{ releaseId }], (actual, session) => { + if ( + actual.status !== "PRESENT" || + actual.operation !== "release-assets" || + actual.httpStatus !== 200 || + actual.code !== null || + !Array.isArray(actual.value) + ) { + throw new Error("Complete Release asset-list capture is malformed") + } + fullyEnumerated.push({ ...rawRelease, assets: actual.value }) + session.keyResults.set(`release-assets:${releaseId}`, actual) + if (fullyEnumerated.length === remainingIds.length) { + appendExpectedPayloadDownloads( + session.expected, + proposal, + stage, + fullyEnumerated, + exact, + equals, + targetReleaseId, + ) + } + }), + ) + } +} + +function appendExpectedPayloadDownloads( + steps, + proposal, + stage, + rawReleases, + exact, + equals, + targetReleaseId, +) { + for (const rawRelease of rawReleases) { + const releaseId = canonicalId(rawRelease.id) + const release = proposal.releases.find(({ id }) => id === releaseId) + if (release === undefined) { + throw new Error("Complete asset enumeration contains an unexpected Release") + } + const orderedAssets = stage === "pre-delete-2" ? release.assets : rawRelease.assets + const finalAssetsByName = + stage === "final" ? new Map(release.assets.map((asset) => [asset.name, asset])) : null + if ( + finalAssetsByName !== null && + (orderedAssets.length !== finalAssetsByName.size || + new Set(orderedAssets.map(({ name }) => name)).size !== finalAssetsByName.size) + ) { + throw new Error("Final Release asset names differ from the proposal") + } + for (const rawAsset of orderedAssets) { + const asset = + finalAssetsByName === null + ? release.assets.find(({ id }) => id === String(rawAsset.id)) + : finalAssetsByName.get(rawAsset.name) + if (asset === undefined) { + throw new Error("Broad Release contains an unexpected asset") + } + if (stage === "final") assertFinalTraceAsset(rawAsset, asset) + const currentAssetId = stage === "final" ? canonicalId(rawAsset.id) : asset.id + steps.push( + exact( + "GitHub reader downloadReleaseAsset", + [ + { + releaseId, + assetId: currentAssetId, + maximumBytes: asset.size, + }, + ], + (actual, session) => { + assertCapturedDownload(actual, asset) + if (!session.downloads.has(releaseId)) session.downloads.set(releaseId, new Map()) + session.downloads + .get(releaseId) + .set(asset.name, Buffer.from(actual.contentBase64, "base64")) + }, + ), + ) + } + if (stage === "pre-delete-1" || stage === "final") { + steps.push( + exact( + "attestation verifier", + null, + (actual) => { + equals(proposal.payloadProof.attestationVerification, "Attestation capture")(actual) + }, + (args, session) => assertCapturedAttestationArgs(args, proposal, releaseId, session), + ), + ) + } + } + if (stage !== "final") { + steps.push( + exact("terminal Release GET", [{ releaseId: targetReleaseId }], (actual, session) => { + session.keyResults.set("terminal-release", actual) + }), + exact("terminal asset enumeration", [{ releaseId: targetReleaseId }], (actual, session) => { + session.keyResults.set("terminal-assets", actual) + }), + ) + } +} + +function assertFinalTraceAsset(actual, proposed) { + if ( + actual.name !== proposed.name || + actual.label !== proposed.label || + actual.state !== proposed.state || + actual.content_type !== proposed.contentType || + actual.size !== proposed.size || + actual.digest !== proposed.digest || + actual.uploader?.login !== proposed.uploader.login || + String(actual.uploader?.id) !== proposed.uploader.id || + actual.uploader?.node_id !== proposed.uploader.nodeId + ) { + throw new Error("Current final Release asset semantics differ from the proposal") + } +} + +function assertCapturedDownload(actual, asset) { + if ( + actual.status !== "PRESENT" || + actual.operation !== "release-asset-download" || + actual.httpStatus !== 200 || + actual.code !== null || + typeof actual.contentBase64 !== "string" + ) { + throw new Error("Release download capture is malformed") + } + const bytes = Buffer.from(actual.contentBase64, "base64") + if ( + bytes.byteLength !== asset.size || + bytes.toString("base64") !== actual.contentBase64 || + createHash("sha256").update(bytes).digest("hex") !== asset.downloadSha256 + ) { + throw new Error("Release download capture differs from the proposal") + } +} + +function assertCapturedAttestationArgs(args, proposal, releaseId, session) { + if (!Array.isArray(args) || args.length !== 1) { + throw new Error("Attestation capture arguments are missing or ambiguous") + } + const downloads = session.downloads.get(releaseId) + if (downloads === undefined) { + throw new Error("Attestation capture is missing guarded Release downloads") + } + const normalized = normalizeTraceValue(args[0]) + const subjects = proposal.payloadProof.attestationVerification.subjects + const recordBytes = downloads.get("release-record.json") + if (recordBytes === undefined) { + throw new Error("Attestation capture is missing the guarded release record") + } + let record + try { + record = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(recordBytes)) + } catch { + throw new Error("Guarded release record is not canonical JSON") + } + const expectedFiles = subjects.map(({ name }) => { + const bytes = downloads.get(name) + if (bytes === undefined) throw new Error("Attestation subject was not downloaded by the guard") + return { name, bytes: { bufferBase64: bytes.toString("base64") } } + }) + const expectedBundles = subjects.map(({ name }) => { + const bundleName = `${name}.intoto.jsonl` + const bytes = downloads.get(bundleName) + if (bytes === undefined) throw new Error("Attestation bundle was not downloaded by the guard") + const expectedDigest = proposal.payloadProof.baseAssetSet.find( + (entry) => entry.name === bundleName, + )?.sha256 + if ( + expectedDigest === undefined || + createHash("sha256").update(bytes).digest("hex") !== expectedDigest + ) { + throw new Error("Attestation bundle digest differs from the proposal") + } + return { + name: bundleName, + bytes: { bufferBase64: bytes.toString("base64") }, + } + }) + const expected = { + source: "escrow", + record, + subjects, + files: expectedFiles, + bundles: expectedBundles, + } + if (!isDeepStrictEqual(normalized, expected)) { + throw new Error("Attestation capture arguments differ from guarded proposal evidence") + } +} + +function canonicalTraceEntry(label, args, result) { + return Object.freeze({ + label, + argsSha256: traceSha256(normalizeTraceValue(args)), + resultSha256: traceSha256(result), + }) +} + +function traceSha256(value) { + return createHash("sha256").update(JSON.stringify(value), "utf8").digest("hex") +} + +function normalizeTraceValue(value, ancestors = new Set()) { + if (Buffer.isBuffer(value)) { + return { bufferBase64: value.toString("base64") } + } + if ( + value === null || + typeof value === "string" || + typeof value === "boolean" || + (typeof value === "number" && Number.isFinite(value)) + ) { + return value + } + if (typeof value !== "object" || utilTypes.isProxy(value) || ancestors.has(value)) { + throw new TypeError("Authority trace contains an invalid value") + } + ancestors.add(value) + try { + if (Array.isArray(value)) { + return value.map((entry) => normalizeTraceValue(entry, ancestors)) + } + if ( + Object.getPrototypeOf(value) !== Object.prototype && + Object.getPrototypeOf(value) !== null + ) { + throw new TypeError("Authority trace contains a non-plain value") + } + const output = {} + for (const name of Object.keys(value).sort()) { + const descriptor = Object.getOwnPropertyDescriptor(value, name) + if ( + descriptor?.enumerable !== true || + !("value" in descriptor) || + descriptor.get !== undefined || + descriptor.set !== undefined + ) { + throw new TypeError("Authority trace contains an unsafe field") + } + output[name] = normalizeTraceValue(descriptor.value, ancestors) + } + return output + } finally { + ancestors.delete(value) + } +} + +function assertCapturedClockBinding(capture, authority) { + const clocks = capture.clockReads + const npm = authority.npmInventory + if ( + clocks.length < 25 || + authority.workflowAuthority.observedAt !== clocks[0] || + npm.startedAt !== clocks[1] || + npm.completedAt !== clocks[CANONICAL_RELEASE_PACKAGE_ORDER.length + 2] || + authority.observedAt !== clocks.at(-1) || + npm.packages.some((entry, index) => entry.observedAt !== clocks[index + 2]) + ) { + throw new Error("Authority timestamps differ from the guard-owned trace") + } + if (capture.targetReleaseId !== null) { + const terminalClocks = clocks.slice(-5, -1) + const target = authority.targetRead + if ( + target === null || + !isDeepStrictEqual(terminalClocks, [ + target.releaseGetStartedAt, + target.releaseGetCompletedAt, + target.assetsListStartedAt, + target.assetsListCompletedAt, + ]) + ) { + throw new Error("Terminal chronology differs from the guard-owned trace") + } + } +} + +function assertCapturedAuthorityProjection(capture, authority) { + const local = capture.keyResults.get("local Git reader readState") + const main = capture.keyResults.get("GitHub reader getDefaultBranchSha") + const tag = capture.keyResults.get("GitHub reader getAnnotatedTag") + const workflow = capture.keyResults.get("GitHub reader getWorkflowState") + const runs = capture.keyResults.get("GitHub reader listNonterminalWorkflowRuns") + if ( + local === undefined || + main === undefined || + tag === undefined || + workflow === undefined || + runs === undefined || + !isDeepStrictEqual(authority.controller, { + headSha: local.headSha, + originMainSha: local.originMainSha, + githubMainSha: main, + }) || + !isDeepStrictEqual(authority.annotatedTag, tag) || + !isDeepStrictEqual(authority.workflowAuthority, { + ...workflow, + query: runs.query, + nonterminalRuns: runs.runs, + observedAt: capture.clockReads[0], + }) + ) { + throw new Error("Authority projection differs from exact guard-recorded results") + } +} + +async function validateCapturedReleaseEvidence(capture, authority) { + const proposal = capture.proposedEnvelope.record + const expectedIds = consolidationStageRule(capture.stage).releaseIds + if ( + !Array.isArray(authority.releases) || + !isDeepStrictEqual( + authority.releases.map(({ id }) => id), + expectedIds, + ) + ) { + throw new Error("Captured authority has the wrong remaining Releases") + } + const releaseEnvelope = capture.keyResults.get("releases") + if (releaseEnvelope === undefined) { + throw new Error("Captured authority has no broad Release enumeration") + } + const rawReleases = releaseEnvelope.value + const capturedClassification = capture.keyResults.get("release-classification") + const classification = classifyConsolidationReleases(rawReleases, proposal, capture.stage) + if ( + capturedClassification === undefined || + capturedClassification.enumerationSha256 !== classification.enumerationSha256 + ) { + throw new Error("Captured full Release enumeration classification differs from its trace") + } + for (const authorityRelease of authority.releases) { + const proposed = proposal.releases.find(({ id }) => id === authorityRelease.id) + const raw = classification.selected.find(({ id }) => String(id) === authorityRelease.id) + if (proposed === undefined || raw === undefined) { + throw new Error("Captured Release is absent from broad enumeration") + } + assertEvidenceEqualsProposal(authorityRelease, proposed) + await captureDirectTargetRead({ + candidate: proposal.candidate, + releaseId: authorityRelease.id, + role: authorityRelease.role, + expectedEvidence: authorityRelease, + github: Object.freeze({ + async getRelease() { + return { + status: "PRESENT", + operation: "release", + httpStatus: 200, + code: null, + value: raw, + } + }, + async listReleaseAssets() { + return { + status: "PRESENT", + operation: "release-assets", + httpStatus: 200, + code: null, + value: raw.assets, + } + }, + }), + now: () => authority.observedAt, + }) + } + if (capture.targetReleaseId !== null) { + const releaseResult = capture.keyResults.get("terminal-release") + const assetsResult = capture.keyResults.get("terminal-assets") + const targetAuthority = authority.releases.find(({ id }) => id === capture.targetReleaseId) + if ( + releaseResult === undefined || + assetsResult === undefined || + targetAuthority === undefined + ) { + throw new Error("Captured terminal Release evidence is incomplete") + } + const terminal = await captureDirectTargetRead({ + candidate: proposal.candidate, + releaseId: capture.targetReleaseId, + role: "duplicate", + expectedEvidence: targetAuthority, + github: Object.freeze({ + async getRelease() { + return releaseResult + }, + async listReleaseAssets() { + return assetsResult + }, + }), + now: () => authority.observedAt, + }) + if (!isDeepStrictEqual(terminal.evidence, authority.targetRead.evidence)) { + throw new Error("Terminal evidence differs from captured authority") + } + } + return classification.enumerationSha256 +} + +function createNetworkGuard({ cwd, now }) { + const context = new AsyncLocalStorage() + const journalPath = path.join( + cwd, + ".dawn", + "release", + "duplicate-draft-consolidation.journal.json", + ) + const permitRecords = new WeakMap() + let state = "open" + let activeRequests = 0 + let sequence = 0 + let terminal = null + let capture = null + let lastClockMillis = null + let boundWriterIdentity = null + let guardFacade + let lastTerminalClockMillis = null + + const terminalNow = () => { + const millis = Reflect.apply(NATIVE_DATE_NOW, NATIVE_DATE, []) + if ( + !Number.isSafeInteger(millis) || + (lastTerminalClockMillis !== null && millis < lastTerminalClockMillis) + ) { + throw new TypeError("Inspection terminal clock failed closed") + } + lastTerminalClockMillis = millis + return Reflect.apply(NATIVE_DATE_TO_ISO_STRING, Reflect.construct(NATIVE_DATE, [millis]), []) + } + + const trustedNow = () => { + const owner = context.getStore() + if ( + capture !== null && + (state === "capturing" || state === "terminal") && + owner !== capture.token + ) { + invalidate() + throw new TypeError("Trusted adapter clock is outside authority capture") + } + let timestamp + try { + timestamp = canonicalTimestamp(callClock(now)) + } catch { + throw new TypeError("Trusted adapter clock failed closed") + } + const millis = Date.parse(timestamp) + if (lastClockMillis !== null && millis < lastClockMillis) { + throw new TypeError("Trusted adapter clock is not monotone") + } + lastClockMillis = millis + return timestamp + } + + const invalidate = () => { + if (terminal !== null) terminal.invalidated = true + if (state !== "deleting" && state !== "spent") state = "invalidated" + } + + const runRequest = async (label, operation, traceArgs) => { + const owner = context.getStore() + const captureOwner = capture !== null && owner === capture.token + const terminalOwner = state === "terminal" && terminal !== null && owner === terminal.token + const deleteOwner = state === "deleting" && owner === terminal?.deleteToken + const allowedDeleteRequest = + deleteOwner && (label === "approved DELETE" || label === "network transport") + if (state !== "open" && !captureOwner && !terminalOwner && !allowedDeleteRequest) { + invalidate() + throw new Error(`${label} rejected by the sealed network epoch`) + } + const traced = traceArgs !== undefined && capture !== null + if (traced) { + if ( + (!captureOwner && !terminalOwner) || + capture.inFlight || + capture.nextStep >= capture.expected.length + ) { + invalidate() + throw new Error(`${label} rejected by the authority capture trace`) + } + const expected = capture.expected[capture.nextStep] + if ( + expected.label !== label || + (expected.args !== null && !isDeepStrictEqual(snapshotJson(traceArgs), expected.args)) + ) { + invalidate() + throw new Error(`${label} is out of order in the authority capture trace`) + } + if (expected.validateArgs !== undefined) { + expected.validateArgs(traceArgs, capture) + } + capture.inFlight = true + } + sequence += 1 + activeRequests += 1 + try { + const result = await operation() + if (deleteOwner && terminal?.invalidated) { + throw new Error(`${label} completed after a reentrant adapter invalidated DELETE`) + } + if (traced) { + if (capture.invalidated || state === "invalidated") { + throw new Error(`${label} completed after authority capture invalidation`) + } + const expected = capture.expected[capture.nextStep] + const resultSnapshot = snapshotJson(result) + expected.validate(resultSnapshot, capture) + if (!capture.keyResults.has(label)) { + capture.keyResults.set(label, resultSnapshot) + } + capture.entries.push(canonicalTraceEntry(label, traceArgs, resultSnapshot)) + capture.nextStep += 1 + capture.inFlight = false + } + return result + } catch (error) { + if (traced) { + capture.inFlight = false + invalidate() + } + throw error + } finally { + activeRequests -= 1 + } + } + + const assertSealed = (session) => { + if ( + terminal !== session || + session.invalidated || + state !== "sealed" || + activeRequests !== 0 || + sequence !== session.sealedSequence + ) { + throw new Error("Adapter network epoch is no longer sealed") + } + } + + const sealedEpoch = (session) => { + const capability = {} + const descriptors = { + now: hiddenMethod(trustedNow), + journalPath: hiddenValue(journalPath), + validate: hiddenMethod(() => assertSealed(session)), + bindAuthority: hiddenMethod((input) => bindCapturedAuthority(session, input)), + toJSON: hiddenMethod(() => { + throw new TypeError("Adapter network epoch capability cannot be serialized") + }), + } + Object.defineProperties(capability, descriptors) + return Object.freeze(capability) + } + + const bindCapturedAuthority = async (session, input) => { + assertSealed(session) + if ( + capture === null || + capture.invalidated || + capture.bound !== null || + capture.nextStep !== capture.expected.length || + capture.inFlight + ) { + invalidate() + throw new Error("Authority capture trace is not sealable") + } + const value = exactDataOptions( + input, + new Set(["authority", "proposal", "acceptTransitionBoundary"]), + "Captured authority binding", + ) + const authority = snapshotJson(required(value, "authority", "Captured authority")) + const proposedEnvelope = createConsolidationEnvelope( + "proposed", + snapshotJson(required(value, "proposal", "Captured proposal")), + ) + const acceptTransitionBoundary = requiredFunction( + value, + "acceptTransitionBoundary", + "Task6 transition boundary receiver", + ) + if ( + proposedEnvelope.recordSha256 !== capture.proposedEnvelope.recordSha256 || + authority.stage !== capture.stage || + (authority.targetRead?.evidence?.id ?? null) !== capture.targetReleaseId + ) { + invalidate() + throw new Error("Captured authority identity differs from its trace") + } + assertCapturedClockBinding(capture, authority) + assertCapturedAuthorityProjection(capture, authority) + const releaseEnumerationSha256 = await validateCapturedReleaseEvidence(capture, authority) + capture.bound = Object.freeze({ + traceSha256: traceSha256(capture.entries), + authoritySha256: canonicalRecordSha256(authority), + proposalSha256: proposedEnvelope.recordSha256, + targetReadSha256: traceSha256(normalizeTraceValue(authority.targetRead)), + releaseEnumerationSha256, + }) + const transitionBoundary = (input) => { + if (session !== terminal || guardFacade === undefined) { + throw new Error("Authority capture epoch is no longer current") + } + return guardFacade.armTask6Transition(input) + } + Object.freeze(transitionBoundary) + acceptTransitionBoundary(transitionBoundary) + return undefined + } + + const sealCapturedWithoutTarget = (session) => { + if ( + capture !== session || + state !== "capturing" || + session.invalidated || + session.inFlight || + session.nextStep !== session.expected.length || + activeRequests !== 0 || + session.targetReleaseId !== null + ) { + invalidate() + throw new Error("Final authority capture trace is incomplete") + } + terminal = { + token: Object.freeze({}), + deleteToken: Object.freeze({}), + releaseId: null, + nextStep: 0, + completedSteps: 0, + inFlight: false, + invalidated: false, + sealedSequence: sequence, + } + state = "sealed" + return sealedEpoch(terminal) + } + + const beginAuthorityCapture = (facade, rawGithub, input) => { + const value = exactDataOptions( + input, + new Set(["stage", "proposal", "targetReleaseId"]), + "Authority capture options", + ) + const stage = required(value, "stage", "Authority capture stage") + if (!new Set(["pre-delete-1", "pre-delete-2", "final"]).has(stage)) { + throw new TypeError("Authority capture stage is invalid") + } + const proposedEnvelope = createConsolidationEnvelope( + "proposed", + snapshotJson(required(value, "proposal", "Authority capture proposal")), + ) + const targetValue = required(value, "targetReleaseId", "Authority capture target") + const targetReleaseId = targetValue === null ? null : canonicalStringId(targetValue) + const expectedTarget = consolidationStageRule(stage).targetReleaseId + if ( + targetReleaseId !== expectedTarget || + state !== "open" || + activeRequests !== 0 || + capture !== null + ) { + invalidate() + throw new Error("Authority capture cannot start from this adapter state") + } + const session = { + token: Object.freeze({}), + stage, + targetReleaseId, + proposedEnvelope, + expected: expectedAuthorityTrace(proposedEnvelope.record, stage), + nextStep: 0, + inFlight: false, + entries: [], + clockReads: [], + keyResults: new Map(), + downloads: new Map(), + invalidated: false, + bound: null, + } + capture = session + state = "capturing" + const scope = (source) => { + const scoped = {} + for (const name of Object.keys(source)) { + scoped[name] = (...args) => context.run(session.token, () => source[name](...args)) + } + return deepFreeze(scoped) + } + const capability = {} + Object.defineProperties(capability, { + local: hiddenValue(scope(facade.local)), + github: hiddenValue(scope(facade.github)), + npm: hiddenValue(scope(facade.npm)), + attestations: hiddenValue(scope(facade.attestations)), + now: hiddenMethod(() => + context.run(session.token, () => { + const timestamp = trustedNow() + session.clockReads.push(timestamp) + return timestamp + }), + ), + beginTerminalRead: hiddenMethod((terminalInput) => + context.run(session.token, () => beginTerminalRead(rawGithub, terminalInput)), + ), + sealWithoutTarget: hiddenMethod(() => + context.run(session.token, () => sealCapturedWithoutTarget(session)), + ), + abort: hiddenMethod(() => invalidate()), + toJSON: hiddenMethod(() => { + throw new TypeError("Authority capture capability cannot be serialized") + }), + }) + return Object.freeze(capability) + } + + const beginTerminalRead = (rawGithub, input) => { + const call = exactDataOptions(input, new Set(["releaseId"]), "Terminal read options") + const releaseId = canonicalStringId(required(call, "releaseId", "Terminal Release ID")) + if (!DUPLICATE_IDS.includes(releaseId)) { + invalidate() + throw new TypeError("Terminal Release ID is not an approved duplicate") + } + const captureOwner = capture !== null && context.getStore() === capture.token + if ((state !== "open" && !(state === "capturing" && captureOwner)) || activeRequests !== 0) { + invalidate() + throw new Error("Terminal network read cannot absorb a concurrent request") + } + const session = { + token: Object.freeze({}), + deleteToken: Object.freeze({}), + releaseId, + nextStep: 0, + completedSteps: 0, + inFlight: false, + invalidated: false, + sealedSequence: null, + } + terminal = session + state = "terminal" + const terminalStep = async (step, name, options, operation) => { + let call + try { + call = exactDataOptions(options, new Set(["releaseId"]), `Terminal ${name} options`) + const requestedReleaseId = canonicalStringId( + required(call, "releaseId", `Terminal ${name} Release ID`), + ) + if (requestedReleaseId !== session.releaseId) { + throw new TypeError("Terminal request target differs from its session") + } + } catch { + session.invalidated = true + state = "invalidated" + throw new TypeError("Terminal request options failed closed") + } + if ( + state !== "terminal" || + terminal !== session || + session.invalidated || + session.nextStep !== step || + session.inFlight + ) { + session.invalidated = true + state = "invalidated" + throw new Error("Terminal network read order is invalid") + } + session.inFlight = true + session.nextStep = step + 1 + try { + const result = await context.run(session.token, () => + runRequest(`terminal ${name}`, () => operation(call), [call]), + ) + if ( + state !== "terminal" || + terminal !== session || + session.invalidated || + !session.inFlight || + session.nextStep !== step + 1 + ) { + throw new Error("Terminal network read was invalidated while pending") + } + session.inFlight = false + session.completedSteps = step + 1 + return result + } catch { + session.inFlight = false + session.invalidated = true + state = "invalidated" + throw new Error(`Terminal ${name} failed closed`) + } + } + const github = deepFreeze({ + getRelease(options) { + return terminalStep(0, "Release GET", options, (owned) => rawGithub.getRelease(owned)) + }, + listReleaseAssets(options) { + return terminalStep(1, "asset enumeration", options, (owned) => + rawGithub.listReleaseAssets(owned), + ) + }, + }) + const capability = {} + Object.defineProperties(capability, { + github: hiddenValue(github), + seal: hiddenMethod(() => { + if ( + state !== "terminal" || + terminal !== session || + session.invalidated || + session.nextStep !== 2 || + session.completedSteps !== 2 || + session.inFlight || + activeRequests !== 0 + ) { + session.invalidated = true + state = "invalidated" + throw new Error("Terminal network read did not complete exactly") + } + if ( + capture !== null && + (capture.invalidated || capture.inFlight || capture.nextStep !== capture.expected.length) + ) { + session.invalidated = true + state = "invalidated" + throw new Error("Authority capture trace is incomplete") + } + state = "sealed" + session.sealedSequence = sequence + return sealedEpoch(session) + }), + abort: hiddenMethod(() => { + session.invalidated = true + state = "invalidated" + }), + toJSON: hiddenMethod(() => { + throw new TypeError("Terminal network capability cannot be serialized") + }), + }) + return Object.freeze(capability) + } + + const captureInspectionTerminal = async (rawGithub, input) => { + let session + try { + const value = exactDataOptions( + input, + new Set(["candidate", "releases"]), + "Inspection terminal options", + ) + const candidate = snapshotJson(required(value, "candidate", "Inspection candidate")) + const releases = snapshotJson(required(value, "releases", "Inspection releases")) + if ( + !isDeepStrictEqual(candidate, APPROVED_CANDIDATE) || + !Array.isArray(releases) || + releases.length !== 3 || + !isDeepStrictEqual( + releases.map(({ role, id }) => ({ role, id })), + [ + { role: "survivor", id: SURVIVOR_ID }, + { role: "duplicate", id: DUPLICATE_IDS[0] }, + { role: "duplicate", id: DUPLICATE_IDS[1] }, + ], + ) || + state !== "open" || + activeRequests !== 0 || + capture !== null || + terminal !== null + ) { + throw new Error("Inspection terminal cannot start from this adapter state") + } + + session = { + token: Object.freeze({}), + deleteToken: Object.freeze({}), + releaseId: null, + nextStep: 0, + completedSteps: 0, + inFlight: false, + invalidated: false, + sealedSequence: null, + inspection: true, + } + terminal = session + state = "terminal" + + const terminalStep = async (step, name, options, operation) => { + const call = exactDataOptions( + options, + new Set(["releaseId"]), + `Inspection terminal ${name} options`, + ) + const releaseId = canonicalStringId( + required(call, "releaseId", `Inspection terminal ${name} Release ID`), + ) + const expectedReleaseId = [ + SURVIVOR_ID, + SURVIVOR_ID, + DUPLICATE_IDS[0], + DUPLICATE_IDS[0], + DUPLICATE_IDS[1], + DUPLICATE_IDS[1], + ][step] + if ( + releaseId !== expectedReleaseId || + state !== "terminal" || + terminal !== session || + session.invalidated || + session.nextStep !== step || + session.inFlight + ) { + throw new Error("Inspection terminal read order is invalid") + } + session.inFlight = true + try { + const result = await runRequest(`inspection terminal ${name}`, () => operation(call)) + if ( + state !== "terminal" || + terminal !== session || + session.invalidated || + !session.inFlight + ) { + throw new Error("Inspection terminal read was invalidated") + } + session.nextStep = step + 1 + session.completedSteps = step + 1 + session.inFlight = false + return result + } catch (error) { + session.inFlight = false + throw error + } + } + + const github = deepFreeze({ + getRelease(options) { + return terminalStep(session.nextStep, "Release GET", options, (owned) => + rawGithub.getRelease(owned), + ) + }, + listReleaseAssets(options) { + return terminalStep(session.nextStep, "asset enumeration", options, (owned) => + rawGithub.listReleaseAssets(owned), + ) + }, + }) + const evidence = [] + await context.run(session.token, async () => { + for (const expectedEvidence of releases) { + const direct = await captureDirectTargetRead({ + candidate, + releaseId: expectedEvidence.id, + role: expectedEvidence.role, + expectedEvidence, + github, + now: terminalNow, + }) + evidence.push(direct.evidence) + } + }) + if ( + session.invalidated || + session.inFlight || + session.nextStep !== 6 || + session.completedSteps !== 6 || + activeRequests !== 0 + ) { + throw new Error("Inspection terminal did not complete exactly") + } + const completedAt = terminalNow() + state = "sealed" + session.sealedSequence = sequence + return deepFreeze({ releases: evidence, completedAt }) + } catch { + if (session !== undefined) session.invalidated = true + invalidate() + throw new Error("Inspection terminal failed closed") + } + } + + guardFacade = Object.freeze({ + now: trustedNow, + runRequest, + bindWriter(writer) { + const identity = + writer !== null && typeof writer === "object" + ? DELETE_WRITER_IDENTITIES.get(writer) + : undefined + if (identity === undefined || boundWriterIdentity !== null) { + invalidate() + throw new TypeError("Adapter network guard delete writer binding failed") + } + boundWriterIdentity = identity + }, + captureInspectionTerminal, + assertInspectionTerminalSealed() { + const session = terminal + if (session === null || session.inspection !== true) { + throw new Error("Inspection terminal is not sealed") + } + assertSealed(session) + }, + armTask6Transition(input) { + try { + const session = terminal + if (session === null || capture === null || capture.bound === null || capture.invalidated) { + throw new Error("Task6 transition has no bound authority capture trace") + } + assertSealed(session) + if ( + session.releaseId === null || + !DUPLICATE_IDS.includes(session.releaseId) || + session.completedSteps !== 2 || + session.inFlight || + boundWriterIdentity === null + ) { + throw new Error("Task6 transition requires a completed approved terminal target read") + } + const binding = exactDataOptions( + input, + new Set([ + "targetReleaseId", + "authority", + "proposedEnvelope", + "confirmation", + "predecessorJournal", + "predecessorHead", + "committedJournal", + "committedHead", + ]), + "Task6 journal transition", + ) + const targetReleaseId = canonicalStringId( + required(binding, "targetReleaseId", "Task6 transition target"), + ) + if (targetReleaseId !== session.releaseId) { + throw new Error("Task6 transition target differs from the terminal session") + } + const predecessorJournal = required( + binding, + "predecessorJournal", + "Task6 predecessor journal", + ) + const committedJournal = required(binding, "committedJournal", "Task6 committed journal") + const journalHeadPath = `${journalPath.slice(0, -"journal.json".length)}journal.head.json` + const predecessorHead = required( + binding, + "predecessorHead", + "Task6 predecessor journal head", + ) + const committedHead = required(binding, "committedHead", "Task6 committed journal head") + const predecessorProvenance = authenticatedPrivateRead(predecessorJournal, journalPath) + const committedProvenance = authenticatedPrivateRead(committedJournal, journalPath) + authenticatedPrivateRead(predecessorHead, journalHeadPath) + authenticatedPrivateRead(committedHead, journalHeadPath) + if ( + predecessorProvenance.identity.dev !== committedProvenance.identity.dev || + predecessorProvenance.identity.ino === committedProvenance.identity.ino + ) { + throw new Error("Task6 journal transition did not replace one authenticated predecessor") + } + const predecessorEnvelope = parseConsolidationJournal(predecessorJournal) + const committedEnvelope = parseConsolidationJournal(committedJournal) + const proposedEnvelope = parseConsolidationEnvelope( + "proposed", + canonicalConsolidationEnvelopeBytes( + "proposed", + required(binding, "proposedEnvelope", "Task6 proposed envelope"), + ), + ) + const confirmation = required(binding, "confirmation", "Task6 confirmation") + assertExactIncidentConfirmation(confirmation, proposedEnvelope) + const confirmationSha256 = createHash("sha256").update(confirmation, "utf8").digest("hex") + const predecessorState = deriveConsolidationState(predecessorEnvelope) + const authority = required(binding, "authority", "Task6 authority") + assertTransitionAuthorityMatchesProposal( + authority, + proposedEnvelope.record, + targetReleaseId, + ) + const releaseEnvelope = capture.keyResults.get("releases") + if (releaseEnvelope === undefined) { + throw new Error("Task6 transition has no guarded Release enumeration") + } + const releaseClassification = classifyConsolidationReleases( + releaseEnvelope.value, + proposedEnvelope.record, + capture.stage, + ) + if ( + capture.bound.traceSha256 !== traceSha256(capture.entries) || + capture.bound.authoritySha256 !== canonicalRecordSha256(authority) || + capture.bound.proposalSha256 !== proposedEnvelope.recordSha256 || + capture.bound.releaseEnumerationSha256 !== releaseClassification.enumerationSha256 || + capture.bound.targetReadSha256 !== traceSha256(normalizeTraceValue(authority.targetRead)) + ) { + throw new Error("Task6 authority differs from the sealed guard-owned trace") + } + const expectedPredecessorHead = canonicalJournalHeadBytes(journalPath, predecessorEnvelope) + const expectedCommittedHead = canonicalJournalHeadBytes(journalPath, committedEnvelope) + if ( + !predecessorHead.equals(expectedPredecessorHead) || + !committedHead.equals(expectedCommittedHead) + ) { + throw new Error("Task6 authenticated journal heads do not bind the legal append") + } + if ( + predecessorEnvelope.record.proposedRecordSha256 !== proposedEnvelope.recordSha256 || + predecessorEnvelope.record.confirmationSha256 !== confirmationSha256 || + predecessorState.controllerSha !== proposedEnvelope.record.controller.headSha || + predecessorState.phase !== "delete-authority-observed" || + predecessorState.currentTargetReleaseId !== targetReleaseId || + !isDeepStrictEqual(predecessorState.lastAuthority, authority) + ) { + throw new Error( + "Task6 transition predecessor does not bind confirmation, controller, target, and authority", + ) + } + const expectedCommitted = appendJournalEvent( + predecessorEnvelope, + "delete-intent", + { + targetReleaseId, + attemptNumber: predecessorState.attemptNumber, + authorityEventSha256: predecessorState.lastEventSha256, + }, + committedEnvelope.record.updatedAt, + ) + if ( + !committedJournal.equals( + canonicalConsolidationEnvelopeBytes("journal", expectedCommitted), + ) || + !isDeepStrictEqual(committedEnvelope, expectedCommitted) + ) { + throw new Error("Task6 committed journal is not exactly one legal intent append") + } + const authorityExpiresAt = new Date( + Date.parse(authority.npmInventory.completedAt) + 120_000, + ).toISOString() + if (trustedNow() > authorityExpiresAt) { + throw new Error("Task6 authority expired before permit issuance") + } + const permit = {} + Object.defineProperty(permit, "toJSON", { + ...hiddenMethod(() => { + throw new TypeError("Delete permit capability cannot be serialized") + }), + }) + Object.freeze(permit) + permitRecords.set(permit, { + session, + targetReleaseId, + used: false, + committedJournal, + committedHead, + authorityExpiresAt, + }) + DELETE_PERMIT_BINDINGS.set(permit, { + writerIdentity: boundWriterIdentity, + releaseId: targetReleaseId, + authorityExpiresAt, + armed: false, + used: false, + }) + state = "permitted" + return permit + } catch (error) { + invalidate() + throw error + } + }, + runDelete: async (permit, releaseId, operation) => { + const record = + permit !== null && typeof permit === "object" ? permitRecords.get(permit) : undefined + const permitBinding = + permit !== null && typeof permit === "object" + ? DELETE_PERMIT_BINDINGS.get(permit) + : undefined + if ( + record === undefined || + record.used || + record.session !== terminal || + record.targetReleaseId !== releaseId || + permitBinding === undefined || + permitBinding.writerIdentity !== boundWriterIdentity || + permitBinding.releaseId !== releaseId || + permitBinding.used || + permitBinding.armed || + state !== "permitted" + ) { + invalidate() + throw new Error("Delete requires the valid one-use adapter-bound permit") + } + record.used = true + permitBinding.used = true + let transactionEntered = false + try { + const journalHeadPath = `${journalPath.slice(0, -"journal.json".length)}journal.head.json` + try { + return await writePrivateEnvelope.withExclusiveTransaction( + journalPath, + async (assertActiveLease) => { + transactionEntered = true + const verifyCommittedState = async () => { + assertActiveLease() + let currentJournal + try { + currentJournal = await readPrivateEnvelope( + journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ) + if ( + !sameAuthenticatedPrivateRead( + currentJournal, + record.committedJournal, + journalPath, + ) + ) { + throw new Error("Committed journal changed") + } + } catch { + throw consolidationVerificationError("JOURNAL") + } + try { + const currentHead = await readPrivateEnvelope(journalHeadPath, 16 * 1024) + if ( + !sameAuthenticatedPrivateRead( + currentHead, + record.committedHead, + journalHeadPath, + ) + ) { + throw new Error("Committed journal head changed") + } + } catch { + throw consolidationVerificationError("HEAD") + } + assertActiveLease() + } + const beforeRead = trustedNow() + if (beforeRead > record.authorityExpiresAt) { + throw new Error("Delete permit expired with its absolute npm authority") + } + await verifyCommittedState() + const immediatelyBeforeSend = trustedNow() + if (immediatelyBeforeSend > record.authorityExpiresAt) { + throw new Error("Delete permit expired with its absolute npm authority") + } + permitBinding.assertActiveLease = assertActiveLease + permitBinding.verifyPreSend = verifyCommittedState + permitBinding.armed = true + permitBinding.used = false + state = "deleting" + const outcome = await context.run(record.session.deleteToken, () => + runRequest("approved DELETE", operation), + ) + await verifyCommittedState() + return outcome + }, + ) + } catch (error) { + if (!transactionEntered) { + throw consolidationVerificationError("LOCK") + } + throw error + } + } finally { + state = "spent" + } + }, + createAuthorityCapability(rawGithub, facade) { + const capability = {} + Object.defineProperties(capability, { + now: hiddenMethod(trustedNow), + journalPath: hiddenValue(journalPath), + validateFacade: hiddenMethod((candidate) => { + if (candidate !== facade) { + invalidate() + throw new TypeError("Adapter authority capability is not bound to this facade") + } + }), + beginAuthorityCapture: hiddenMethod((input) => + beginAuthorityCapture(facade, rawGithub, input), + ), + beginTerminalRead: hiddenMethod((input) => beginTerminalRead(rawGithub, input)), + sealWithoutTarget: hiddenMethod(() => { + if (state !== "open" || activeRequests !== 0) { + invalidate() + throw new Error("Final network epoch cannot absorb a concurrent request") + } + terminal = { + token: Object.freeze({}), + deleteToken: Object.freeze({}), + releaseId: null, + nextStep: 0, + completedSteps: 0, + inFlight: false, + invalidated: false, + sealedSequence: sequence, + } + state = "sealed" + return sealedEpoch(terminal) + }), + toJSON: hiddenMethod(() => { + throw new TypeError("Adapter authority capability cannot be serialized") + }), + }) + return Object.freeze(capability) + }, + }) + return guardFacade +} + +function hiddenMethod(value) { + Object.freeze(value) + return { value, enumerable: false, writable: false, configurable: false } +} + +function hiddenValue(value) { + return { value, enumerable: false, writable: false, configurable: false } +} + +function consolidationVerificationError(subject) { + const code = subject === "LOCK" ? "JOURNAL_LOCK" : `COMMITTED_${subject}` + return new Error( + `ERR_CONSOLIDATION_${code}_VERIFICATION: Committed ${subject.toLowerCase()} verification failed`, + ) +} + +function authenticatedPrivateRead(value, expectedPath) { + const authenticate = Object.getOwnPropertyDescriptor(readPrivateEnvelope, "authenticate")?.value + if (typeof authenticate !== "function") { + throw new TypeError("Private no-follow read verifier is unavailable") + } + return authenticate(value, expectedPath) +} + +function sameAuthenticatedPrivateRead(actual, expected, expectedPath) { + const actualIdentity = authenticatedPrivateRead(actual, expectedPath).identity + const expectedIdentity = authenticatedPrivateRead(expected, expectedPath).identity + return actual.equals(expected) && sameFileIdentity(actualIdentity, expectedIdentity) +} + +function sameFileIdentity(left, right) { + return ["ctimeNs", "dev", "ino", "mtimeNs", "nlink", "size"].every( + (name) => left[name] === right[name], + ) +} + +function canonicalJournalHeadBytes(journalPath, journal) { + return Buffer.from( + `${JSON.stringify({ + schemaVersion: 1, + journalPath, + repository: journal.record.repository, + proposedRecordSha256: journal.record.proposedRecordSha256, + journalRecordSha256: journal.recordSha256, + lastEventSha256: journal.record.events.at(-1).eventSha256, + sequence: journal.record.events.length, + updatedAt: journal.record.updatedAt, + })}\n`, + "utf8", + ) +} + +function createLocalGitReader({ cwd, environment, run, ownerAdapters }) { + const git = ownDataObject( + requiredMember(ownerAdapters, "git", "Owner preflight adapters"), + "Owner Git adapter", + ) + const headSha = bindMethod(git, "headSha", "Owner Git adapter") + return deepFreeze({ + async readState() { + const head = await headSha() + if (!isSha(head)) throw new TypeError("Local Git HEAD SHA is malformed") + const branchResult = await executeExact( + run, + "git", + ["symbolic-ref", "--quiet", "--short", "HEAD"], + { cwd, env: environment }, + ) + const branch = singleLine(branchResult.stdout, "Local Git symbolic branch") + if (branch !== "main") throw new TypeError("Local Git symbolic branch must be main") + const statusResult = await executeExact( + run, + "git", + ["status", "--porcelain=v1", "--untracked-files=all"], + { cwd, env: environment }, + ) + if (statusResult.stdout !== "") { + throw new TypeError("Local Git porcelain status must be clean") + } + const originResult = await executeExact( + run, + "git", + ["rev-parse", "--verify", "refs/remotes/origin/main^{commit}"], + { cwd, env: environment }, + ) + const originMainSha = singleLine(originResult.stdout, "Local Git origin/main SHA") + if (!isSha(originMainSha)) throw new TypeError("Local Git origin/main SHA is malformed") + return deepFreeze({ + headSha: head, + branch, + porcelainStatus: "", + originMainSha, + }) + }, + }) +} + +async function readNonterminalWorkflowRuns(http, token, now, query) { + const runs = [] + const rawIds = new Set() + const requestedStatuses = new Set(query.statuses) + const budget = workflowReadBudget(now) + let total = null + let pages = 1 + for (let page = 1; page <= pages; page += 1) { + const url = workflowRunsUrl(page, query.perPage) + const result = await readDirectJsonResult( + http, + url, + token, + "workflow runs", + remainingWorkflowRequestBudget(budget), + ) + consumeWorkflowResponseBudget(budget, result.bodyBytes) + const { body } = result + if ( + !isPlainRecord(body) || + !Number.isSafeInteger(body.total_count) || + body.total_count < 0 || + body.total_count > MAX_RECORDS || + !Array.isArray(body.workflow_runs) || + body.workflow_runs.length > query.perPage + ) { + throw new TypeError("GitHub workflow-run total or record bound is malformed") + } + if (page === 1) { + total = body.total_count + pages = Math.max(1, Math.ceil(total / query.perPage)) + if (pages > query.maximumPages) { + throw new TypeError("GitHub workflow-run page bound exceeded") + } + } else if (body.total_count !== total) { + throw new TypeError("GitHub workflow-run total is unstable") + } + const expected = + total === 0 ? 0 : page < pages ? query.perPage : total - (pages - 1) * query.perPage + if (body.workflow_runs.length !== expected) { + throw new TypeError("GitHub workflow-run page total is inconsistent") + } + const nextUrl = workflowNextUrl(result.link) + if (page < pages) { + if (nextUrl === null) + throw new TypeError("GitHub workflow-run pagination is missing Link next") + if (nextUrl !== workflowRunsUrl(page + 1, query.perPage)) { + throw new TypeError("GitHub workflow-run Link next URL is not the expected trusted page") + } + } else if (nextUrl !== null) { + throw new TypeError("GitHub workflow-run pagination has an unexpected Link next") + } + for (const run of body.workflow_runs) { + if (!isPlainRecord(run)) throw new TypeError("GitHub workflow run is malformed") + const id = canonicalId(run.id) + if (rawIds.has(id)) throw new TypeError("GitHub workflow runs contain a duplicate ID") + rawIds.add(id) + if (!requestedStatuses.has(run.status)) continue + if ( + !Number.isSafeInteger(run.run_attempt) || + run.run_attempt < 1 || + !safeBoundedString(run.event, 256) || + !isSha(run.head_sha) || + !safeBoundedString(run.head_branch, 1_024) + ) { + throw new TypeError("GitHub nonterminal workflow run is malformed") + } + runs.push({ + id, + runAttempt: run.run_attempt, + status: run.status, + event: run.event, + headSha: run.head_sha, + headBranch: run.head_branch, + }) + } + } + if (rawIds.size !== total) throw new TypeError("GitHub workflow-run raw total is inconsistent") + return deepFreeze( + runs.sort((left, right) => + left.id === right.id + ? left.runAttempt - right.runAttempt + : BigInt(left.id) < BigInt(right.id) + ? -1 + : 1, + ), + ) +} + +async function readDirectJson(http, url, token, label) { + return (await readDirectJsonResult(http, url, token, label)).body +} + +async function readDirectJsonResult(http, url, token, label, requestBudget) { + const result = await http.getJson({ + url, + headers: githubHeaders(token), + ...(requestBudget === undefined ? {} : requestBudget), + }) + if (result.status !== "OK" || result.httpStatus < 200 || result.httpStatus >= 300) { + throw new Error(`GitHub ${label} read failed closed`) + } + if (!isPlainRecord(result.body)) throw new TypeError(`GitHub ${label} response is malformed`) + return { + body: result.body, + link: result.headers.link, + bodyBytes: result.bodyBytes, + } +} + +function workflowReadBudget(now) { + const startedAt = workflowClockMillis(now) + return { + deadline: startedAt + DELETE_TIMEOUT_MS, + remainingBytes: MAX_DIRECT_JSON_BYTES, + now, + } +} + +function remainingWorkflowRequestBudget(budget) { + const timeoutMs = budget.deadline - workflowClockMillis(budget.now) + if (timeoutMs < 1) throw new Error("GitHub workflow-run operation deadline exceeded") + if (budget.remainingBytes < 1) { + throw new Error("GitHub workflow-run operation byte budget exceeded") + } + return { + timeoutMs: Math.min(timeoutMs, DELETE_TIMEOUT_MS), + maxResponseBytes: budget.remainingBytes, + } +} + +function consumeWorkflowResponseBudget(budget, bodyBytes) { + if (!Number.isSafeInteger(bodyBytes) || bodyBytes < 0 || bodyBytes > budget.remainingBytes) { + throw new Error("GitHub workflow-run operation byte budget exceeded") + } + budget.remainingBytes -= bodyBytes + if (budget.deadline <= workflowClockMillis(budget.now)) { + throw new Error("GitHub workflow-run operation deadline exceeded") + } +} + +function workflowClockMillis(now) { + const value = now() + if (!Number.isSafeInteger(value)) throw new TypeError("GitHub workflow-run clock is invalid") + return value +} + +function workflowRunsUrl(page, perPage = 100) { + return `${API_ORIGIN}/repos/${REPOSITORY}/actions/workflows/${encodeURIComponent(RELEASE_WORKFLOW)}/runs?per_page=${perPage}&page=${page}` +} + +function workflowNextUrl(value) { + if (value === null) return null + const graph = exactLinkGraph(value) + if (graph === null) { + throw new TypeError("GitHub workflow-run Link header is malformed") + } + const next = [] + const relations = new Set() + const targetRelations = new Map() + for (const entry of graph) { + const url = exactWorkflowPageUrl(entry.url) + if (relations.has(entry.relation)) { + throw new TypeError("GitHub workflow-run Link graph is contradictory") + } + relations.add(entry.relation) + addTargetRelation(targetRelations, url, entry.relation) + if (entry.relation === "next") next.push(url) + } + if (!hasCompatibleSharedLinkTargets(targetRelations)) { + throw new TypeError("GitHub workflow-run Link graph is contradictory") + } + return next[0] ?? null +} + +function exactWorkflowPageUrl(value) { + let url + try { + url = new URL(value) + } catch { + throw new TypeError("GitHub workflow-run Link URL is malformed") + } + const expectedPath = new URL(workflowRunsUrl(1)).pathname + const entries = [...url.searchParams] + if ( + url.origin !== API_ORIGIN || + url.protocol !== "https:" || + url.username !== "" || + url.password !== "" || + url.hash !== "" || + url.pathname !== expectedPath || + entries.length !== 2 || + url.searchParams.getAll("per_page").length !== 1 || + url.searchParams.get("per_page") !== "100" || + url.searchParams.getAll("page").length !== 1 || + !ID_PATTERN.test(url.searchParams.get("page")) + ) { + throw new TypeError("GitHub workflow-run Link URL is not trusted") + } + return url.href +} + +function normalizeAttestationResult(value) { + if (containsProxy(value)) throw new TypeError("Attestation evidence is malformed") + let snapshot + try { + snapshot = snapshotJson(value) + } catch { + throw new TypeError("Attestation evidence is malformed") + } + if ( + !isPlainRecord(snapshot) || + !hasExactKeys(snapshot, ["status", "subjects"]) || + !["VERIFIED", "INVALID"].includes(snapshot.status) || + !Array.isArray(snapshot.subjects) || + snapshot.subjects.length > 22 || + (snapshot.status === "INVALID" && snapshot.subjects.length !== 0) + ) { + throw new TypeError("Attestation evidence is malformed") + } + const names = new Set() + const subjects = snapshot.subjects.map((subject) => { + if ( + !isPlainRecord(subject) || + !hasExactKeys(subject, ["name", "sha256"]) || + !safeBoundedString(subject.name, 256) || + !/^[0-9a-f]{64}$/u.test(subject.sha256) || + names.has(subject.name) + ) { + throw new TypeError("Attestation subject evidence is malformed") + } + names.add(subject.name) + return { name: subject.name, sha256: subject.sha256 } + }) + return deepFreeze({ status: snapshot.status, subjects }) +} + +function containsProxy(value, seen = new Set()) { + if (value === null || typeof value !== "object" || seen.has(value)) return false + if (utilTypes.isProxy(value)) return true + seen.add(value) + for (const key of Reflect.ownKeys(value)) { + const descriptor = Object.getOwnPropertyDescriptor(value, key) + if (isEnumerableData(descriptor) && containsProxy(descriptor.value, seen)) return true + } + return false +} + +function hasExactKeys(value, expected) { + const actual = Object.keys(value).sort() + const sortedExpected = [...expected].sort() + return ( + actual.length === sortedExpected.length && + actual.every((name, index) => name === sortedExpected[index]) + ) +} + +function githubFetch(fetchImpl) { + const authorizedDownloadHops = new Set() + return async (url, init) => { + let parsed + try { + parsed = new URL(url) + } catch { + throw new TypeError("GitHub request URL is invalid") + } + const apiRequest = parsed.origin === API_ORIGIN + const authorizedDownloadHop = !apiRequest && authorizedDownloadHops.delete(parsed.href) + if ( + parsed.protocol !== "https:" || + parsed.username !== "" || + parsed.password !== "" || + parsed.hash !== "" || + (!apiRequest && !authorizedDownloadHop) + ) { + throw new TypeError("GitHub request origin is not trusted") + } + const headerEntries = Object.entries(init.headers ?? {}) + const forwardedHeaders = authorizedDownloadHop + ? Object.fromEntries(headerEntries.filter(([name]) => name.toLowerCase() !== "authorization")) + : { ...init.headers } + const response = await fetchImpl(parsed.href, { + ...init, + headers: { ...forwardedHeaders, "User-Agent": USER_AGENT }, + }) + if (apiRequest) { + authorizeProductionDownloadHop(parsed, init, response, authorizedDownloadHops) + return enforcePaginationLinkGraph(response, parsed) + } + return response + } +} + +function authorizeProductionDownloadHop(requestUrl, init, response, authorizedDownloadHops) { + const headers = Object.entries(init.headers ?? {}) + if ( + init.method !== "GET" || + init.redirect !== "manual" || + requestUrl.search !== "" || + headers.length !== 3 || + exactHeaderValue(headers, "accept") !== "application/octet-stream" || + exactHeaderValue(headers, "x-github-api-version") !== API_VERSION || + !/^Bearer [^\s]+$/u.test(exactHeaderValue(headers, "authorization") ?? "") || + !/^\/repos\/cacheplane\/dawnai\/releases\/assets\/[1-9][0-9]*$/u.test(requestUrl.pathname) + ) { + return + } + let status + let location + try { + status = response?.status + location = response?.headers?.get("location") + } catch { + return + } + if ( + status !== 302 || + typeof location !== "string" || + location.length === 0 || + Buffer.byteLength(location, "utf8") > MAX_LINK_HEADER_BYTES + ) { + return + } + const normalized = normalizedAbsoluteUrl(location) + if (normalized !== null) authorizedDownloadHops.add(normalized) +} + +function exactHeaderValue(entries, expectedName) { + const matches = entries.filter(([name]) => name.toLowerCase() === expectedName) + return matches.length === 1 && typeof matches[0][1] === "string" ? matches[0][1] : null +} + +function enforcePaginationLinkGraph(response, requestUrl) { + let link + try { + link = response?.headers?.get("link") + } catch { + return response + } + if (link === null || validPaginationLinkGraph(link, requestUrl)) return response + try { + const headers = new Headers(response.headers) + headers.set("Link", "malformed") + return { status: response.status, headers, body: response.body } + } catch { + return response + } +} + +function validPaginationLinkGraph(value, requestUrl) { + const graph = exactLinkGraph(value) + if (graph === null) return false + const relations = new Set() + const targetRelations = new Map() + for (const entry of graph) { + const url = normalizedAbsoluteUrl(entry.url) + if ( + url === null || + relations.has(entry.relation) || + (entry.relation !== "next" && exactPaginationLinkUrl(url, requestUrl) === null) + ) { + return false + } + relations.add(entry.relation) + addTargetRelation(targetRelations, url, entry.relation) + } + return hasCompatibleSharedLinkTargets(targetRelations) +} + +function addTargetRelation(targetRelations, url, relation) { + const relations = targetRelations.get(url) ?? new Set() + relations.add(relation) + targetRelations.set(url, relations) +} + +function hasCompatibleSharedLinkTargets(targetRelations) { + for (const relations of targetRelations.values()) { + if (relations.size === 1) continue + if ( + relations.size !== 2 || + !( + (relations.has("next") && relations.has("last")) || + (relations.has("prev") && relations.has("first")) + ) + ) { + return false + } + } + return true +} + +function normalizedAbsoluteUrl(value) { + try { + return new URL(value).href + } catch { + return null + } +} + +function exactLinkGraph(value) { + if ( + typeof value !== "string" || + value.length === 0 || + Buffer.byteLength(value, "utf8") > MAX_LINK_HEADER_BYTES + ) { + return null + } + const entries = [] + for (const part of value.split(",")) { + const match = /^\s*<([^<>\s]+)>\s*;\s*rel="([A-Za-z][A-Za-z0-9._ -]*)"\s*$/u.exec(part) + if (match === null) return null + const relations = match[2].split(/ +/u) + if ( + relations.length !== 1 || + !PAGINATION_RELATIONS.has(relations[0]) || + !/^[A-Za-z][A-Za-z0-9._-]*$/u.test(relations[0]) + ) { + return null + } + entries.push({ url: match[1], relation: relations[0] }) + } + return entries.length === 0 ? null : entries +} + +function exactPaginationLinkUrl(value, requestUrl) { + try { + const url = new URL(value) + const current = new URL(requestUrl) + if ( + url.origin !== API_ORIGIN || + url.protocol !== "https:" || + url.username !== "" || + url.password !== "" || + url.hash !== "" || + url.pathname !== current.pathname + ) { + return null + } + const currentQuery = uniqueQuery(current.searchParams) + const linkQuery = uniqueQuery(url.searchParams) + if ( + currentQuery === null || + linkQuery === null || + linkQuery.size !== currentQuery.size + (currentQuery.has("page") ? 0 : 1) || + linkQuery.get("per_page") !== "100" || + !ID_PATTERN.test(linkQuery.get("page")) + ) { + return null + } + for (const [name, queryValue] of currentQuery) { + if (name !== "page" && linkQuery.get(name) !== queryValue) return null + } + return url.href + } catch { + return null + } +} + +function uniqueQuery(searchParams) { + const values = new Map() + for (const [name, value] of searchParams) { + if (values.has(name)) return null + values.set(name, value) + } + return values +} + +function githubHeaders(token) { + return { + Accept: JSON_ACCEPT, + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": API_VERSION, + "User-Agent": USER_AGENT, + } +} + +function normalizedGitHubEnvelope(value, operation, payloadKey) { + return deepFreeze( + normalizeAdapterEnvelope(value, { + source: "github", + operation, + payloadKey, + }), + ) +} + +function presentValue(value, operation) { + const result = normalizedGitHubEnvelope(value, operation, "value") + if (result.status !== "PRESENT") { + throw new Error(`GitHub ${operation} read failed closed`) + } + return result.value +} + +function rejectDuplicateIds(value, operation, code) { + const result = normalizedGitHubEnvelope(value, operation, "value") + if (result.status !== "PRESENT") return result + if (!Array.isArray(result.value)) { + return deepFreeze({ + status: "ERROR", + operation, + httpStatus: result.httpStatus, + code: "MALFORMED_SCHEMA", + }) + } + const ids = new Set() + for (const record of result.value) { + let id + try { + id = canonicalId(record?.id) + } catch { + return deepFreeze({ + status: "ERROR", + operation, + httpStatus: result.httpStatus, + code: "MALFORMED_SCHEMA", + }) + } + if (ids.has(id)) { + return deepFreeze({ + status: "ERROR", + operation, + httpStatus: result.httpStatus, + code, + }) + } + ids.add(id) + } + return result +} + +async function resolveGhToken({ cwd, environment, run }) { + let result + try { + result = await executeExact(run, "gh", ["auth", "token"], { + cwd, + env: environment, + }) + } catch { + throw new Error("GitHub authentication token resolution failed") + } + if (Buffer.byteLength(result.stdout, "utf8") > MAX_TOKEN_BYTES + 2) { + throw new TypeError("GitHub authentication token output is invalid") + } + const raw = result.stdout.endsWith("\r\n") + ? result.stdout.slice(0, -2) + : result.stdout.endsWith("\n") + ? result.stdout.slice(0, -1) + : result.stdout + return canonicalToken(raw) +} + +async function executeExact(run, command, args, options) { + let result + try { + result = await run(command, [...args], { + cwd: options.cwd, + env: { ...options.env }, + }) + } catch { + throw new Error("Bounded adapter command failed") + } + const value = exactDataOptions( + result, + new Set(["exitCode", "stdout", "stderr"]), + "Adapter command result", + ) + if ( + !Number.isSafeInteger(value.exitCode) || + value.exitCode !== 0 || + typeof value.stdout !== "string" || + typeof value.stderr !== "string" + ) { + throw new TypeError("Adapter command result is malformed") + } + return value +} + +function deleteDeadline(timeoutMs, callerSignal) { + const controller = new AbortController() + let rejectAbort + const abortPromise = new Promise((_resolve, reject) => { + rejectAbort = reject + }) + let settled = false + const abort = () => { + if (settled) return + settled = true + controller.abort() + rejectAbort(new Error("Delete deadline expired")) + } + const onCallerAbort = () => abort() + callerSignal?.addEventListener("abort", onCallerAbort, { once: true }) + const timer = setTimeout(abort, timeoutMs) + return { + signal: controller.signal, + race(promise) { + return Promise.race([Promise.resolve(promise), abortPromise]) + }, + dispose() { + settled = true + clearTimeout(timer) + callerSignal?.removeEventListener("abort", onCallerAbort) + }, + } +} + +async function deleteResponse(response, deadline) { + if (utilTypes.isProxy(response) || response === null || typeof response !== "object") { + throw new TypeError("GitHub DELETE response is malformed") + } + let status + let headers + let body + try { + status = response.status + headers = response.headers + body = response.body + } catch { + throw new TypeError("GitHub DELETE response is malformed") + } + const malformed = + !Number.isInteger(status) || + status < 100 || + status > 599 || + headers === null || + typeof headers !== "object" || + typeof headers.get !== "function" + await cancelDeleteResponseBody(body, deadline) + if (malformed) { + throw new TypeError("GitHub DELETE response is malformed") + } + return { status } +} + +async function cancelDeleteResponseBody(body, deadline) { + if (body === null) return + if (utilTypes.isProxy(body) || typeof body !== "object") { + throw new TypeError("GitHub DELETE response body is malformed") + } + let cancel + try { + cancel = body.cancel + } catch { + throw new TypeError("GitHub DELETE response body is malformed") + } + if (typeof cancel !== "function" || utilTypes.isProxy(cancel)) { + throw new TypeError("GitHub DELETE response body is malformed") + } + try { + await deadline.race(Promise.resolve().then(() => cancel.call(body))) + } catch { + throw new Error("GitHub DELETE response body cancellation failed closed") + } +} + +function deleteOutcome(classification, httpStatus, observedAt) { + return deepFreeze({ + classification, + httpStatus, + observedAt, + }) +} + +function callClock(now) { + try { + return now() + } catch { + throw new TypeError("Adapter clock is invalid") + } +} + +function canonicalTimestamp(value) { + if ( + typeof value !== "string" || + !TIMESTAMP_PATTERN.test(value) || + Number.isNaN(Date.parse(value)) || + new Date(value).toISOString() !== value + ) { + throw new TypeError("Adapter clock timestamp is invalid") + } + return value +} + +function canonicalToken(value) { + if ( + typeof value !== "string" || + value.length === 0 || + Buffer.byteLength(value, "utf8") > MAX_TOKEN_BYTES || + hasControlCharacters(value) || + /\s/u.test(value) + ) { + throw new TypeError("GitHub authentication token is invalid") + } + return value +} + +function canonicalId(value) { + const normalized = Number.isSafeInteger(value) && value > 0 ? String(value) : value + if (typeof normalized !== "string" || !ID_PATTERN.test(normalized)) { + throw new TypeError("Identifier must be a canonical positive decimal string") + } + return normalized +} + +function canonicalStringId(value) { + if (typeof value !== "string" || !ID_PATTERN.test(value)) { + throw new TypeError("Identifier must be a canonical positive decimal string") + } + return value +} + +function normalizedRoot(value) { + if ( + typeof value !== "string" || + !path.isAbsolute(value) || + path.resolve(value) !== value || + hasControlCharacters(value) + ) { + throw new TypeError("Adapter root is invalid") + } + return value +} + +function snapshotEnvironment(value) { + const input = ownDataObject(value, "Adapter environment") + return snapshotEnvironmentFields(input) +} + +function snapshotRuntimeEnvironment(value) { + if (value === null || typeof value !== "object" || utilTypes.isProxy(value)) { + throw new TypeError("Runtime environment is invalid") + } + return snapshotEnvironmentFields(value, process.platform === "win32") +} + +function snapshotEnvironmentFields(input, windowsRuntime = false) { + const output = Object.create(null) + const keys = Reflect.ownKeys(input) + for (const key of keys) { + if (typeof key !== "string") throw new TypeError("Adapter environment contains a symbol") + const descriptor = Object.getOwnPropertyDescriptor(input, key) + if (!isEnumerableData(descriptor) || typeof descriptor.value !== "string") { + throw new TypeError("Adapter environment contains an unsafe field") + } + } + const logicalNames = new Set( + keys.flatMap((key) => { + const logicalName = typeof key === "string" ? asciiEnvironmentName(key) : null + return logicalName === null ? [] : [logicalName] + }), + ) + const windowsShaped = + windowsRuntime || [...WINDOWS_ENVIRONMENT_MARKERS].every((name) => logicalNames.has(name)) + for (const key of keys) { + const value = Object.getOwnPropertyDescriptor(input, key).value + if (key === "GH_TOKEN" || key === "GITHUB_TOKEN") { + output[key] = value + continue + } + if (!windowsShaped) { + if (SAFE_ENVIRONMENT_NAMES.has(key)) output[key] = value + continue + } + const logicalName = asciiEnvironmentName(key) + const canonicalName = + logicalName === null ? undefined : WINDOWS_SAFE_ENVIRONMENT_NAMES.get(logicalName) + if (canonicalName === undefined) continue + if (Object.hasOwn(output, canonicalName) && output[canonicalName] !== value) { + throw new TypeError("Adapter Windows environment contains conflicting aliases") + } + output[canonicalName] = value + } + return Object.freeze({ ...output }) +} + +function subprocessEnvironment(environment) { + const output = Object.create(null) + for (const name of SAFE_ENVIRONMENT_NAMES) { + if (name !== "PATH" && name !== "Path" && typeof environment[name] === "string") { + output[name] = environment[name] + } + } + if (typeof environment.PATH === "string") output.PATH = environment.PATH + else if (typeof environment.Path === "string") output.Path = environment.Path + output.NO_COLOR = "1" + return Object.freeze({ ...output }) +} + +function asciiEnvironmentName(value) { + return /^[A-Za-z_]+$/u.test(value) ? value.toUpperCase() : null +} + +function exactDataOptions(value, allowed, label) { + const input = ownDataObject(value, label) + const keys = Reflect.ownKeys(input) + for (const key of keys) { + if (typeof key !== "string" || !allowed.has(key)) { + throw new TypeError(`${label} contains an unknown or symbol field`) + } + if (!isEnumerableData(Object.getOwnPropertyDescriptor(input, key))) { + throw new TypeError(`${label} contains an accessor or hidden field`) + } + } + const output = Object.create(null) + for (const key of keys) { + output[key] = Object.getOwnPropertyDescriptor(input, key).value + } + return Object.freeze({ ...output }) +} + +function ownDataObject(value, label) { + if ( + utilTypes.isProxy(value) || + value === null || + typeof value !== "object" || + Array.isArray(value) || + ![Object.prototype, null].includes(Object.getPrototypeOf(value)) + ) { + throw new TypeError(`${label} must be a non-proxy plain object`) + } + return value +} + +function snapshotStringArray(value, label) { + if ( + utilTypes.isProxy(value) || + !Array.isArray(value) || + Object.getPrototypeOf(value) !== Array.prototype + ) { + throw new TypeError(`${label} must be a plain array`) + } + const keys = Reflect.ownKeys(value) + if (keys.length !== value.length + 1 || keys.at(-1) !== "length") { + throw new TypeError(`${label} contains hidden, symbol, or sparse fields`) + } + const output = [] + for (let index = 0; index < value.length; index += 1) { + const descriptor = Object.getOwnPropertyDescriptor(value, String(index)) + if (!isEnumerableData(descriptor) || typeof descriptor.value !== "string") { + throw new TypeError(`${label} contains an accessor or invalid entry`) + } + output.push(descriptor.value) + } + return output +} + +function bindMethod(value, name, label) { + const object = ownDataObject(value, label) + const descriptor = Object.getOwnPropertyDescriptor(object, name) + if ( + !isEnumerableData(descriptor) || + typeof descriptor.value !== "function" || + utilTypes.isProxy(descriptor.value) + ) { + throw new TypeError(`${label} method ${name} is invalid`) + } + return async (...args) => descriptor.value.apply(object, args) +} + +function dependencyFunction(dependencies, name, fallback) { + if (!Object.hasOwn(dependencies, name)) return fallback + const value = dependencies[name] + assertFunction(value, `Adapter dependency ${name}`) + return value +} + +function requiredFunction(value, name, label) { + const member = required(value, name, label) + assertFunction(member, label) + return member +} + +function assertFunction(value, label) { + if (typeof value !== "function" || utilTypes.isProxy(value)) { + throw new TypeError(`${label} is invalid`) + } +} + +function required(value, name, label) { + if (!Object.hasOwn(value, name)) throw new TypeError(`${label} is required`) + return value[name] +} + +function requiredMember(value, name, label) { + const object = ownDataObject(value, label) + const descriptor = Object.getOwnPropertyDescriptor(object, name) + if (!isEnumerableData(descriptor)) throw new TypeError(`${label} member ${name} is invalid`) + return descriptor.value +} + +function isEnumerableData(descriptor) { + return ( + descriptor?.enumerable === true && + "value" in descriptor && + descriptor.get === undefined && + descriptor.set === undefined + ) +} + +function isPlainRecord(value) { + return ( + !utilTypes.isProxy(value) && + value !== null && + typeof value === "object" && + !Array.isArray(value) && + [Object.prototype, null].includes(Object.getPrototypeOf(value)) + ) +} + +function safeLogin(value) { + return typeof value === "string" && /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/u.test(value) +} + +function safeBoundedString(value, maximumBytes) { + return ( + typeof value === "string" && + value.length > 0 && + Buffer.byteLength(value, "utf8") <= maximumBytes && + !hasControlCharacters(value) + ) +} + +function hasControlCharacters(value) { + return [...value].some((character) => { + const codePoint = character.codePointAt(0) + return codePoint <= 31 || (codePoint >= 127 && codePoint <= 159) + }) +} + +function assertExactIncidentConfirmation(value, proposedEnvelope) { + if (typeof value !== "string" || hasControlCharacters(value)) { + throw new TypeError("Task6 confirmation must be an exact control-free string") + } + const { candidate, roles } = proposedEnvelope.record + const expected = `CONSOLIDATE v${candidate.version} ${candidate.commitSha} SURVIVOR ${roles.survivor} DELETE ${roles.duplicates.join(",")} PROPOSAL ${proposedEnvelope.recordSha256}` + if (value !== expected) { + throw new Error("Task6 confirmation does not exactly bind the proposal") + } +} + +function assertTransitionAuthorityMatchesProposal(authority, proposal, targetReleaseId) { + const expectedStage = targetReleaseId === DUPLICATE_IDS[0] ? "pre-delete-1" : "pre-delete-2" + const stableTag = ({ observedAt: _observedAt, ...value }) => value + const stableWorkflow = ({ observedAt: _observedAt, ...value }) => value + if ( + authority.stage !== expectedStage || + !isDeepStrictEqual(authority.controller, proposal.controller) || + !isDeepStrictEqual(stableTag(authority.annotatedTag), stableTag(proposal.annotatedTag)) || + !isDeepStrictEqual( + stableWorkflow(authority.workflowAuthority), + stableWorkflow(proposal.workflowAuthority), + ) || + !isDeepStrictEqual(authority.payloadProof, proposal.payloadProof) + ) { + throw new Error( + "Task6 authority controller, tag, workflow, or payload differs from the proposal", + ) + } + for (const release of authority.releases) { + const proposed = proposal.releases.find(({ id }) => id === release.id) + if (proposed === undefined) { + throw new Error("Task6 authority contains an unproposed Release") + } + assertEvidenceEqualsProposal(release, proposed) + } + if ( + authority.targetRead?.evidence.id !== targetReleaseId || + authority.npmInventory.stage !== expectedStage || + authority.npmInventory.packages.some((entry) => entry.version !== proposal.candidate.version) + ) { + throw new Error("Task6 authority target or npm evidence differs from the proposal") + } +} + +function singleLine(value, label) { + if (typeof value !== "string") throw new TypeError(`${label} is malformed`) + const normalized = value.endsWith("\n") ? value.slice(0, -1) : value + if ( + !safeBoundedString(normalized, 1_024) || + normalized.includes("\n") || + normalized.includes("\r") + ) { + throw new TypeError(`${label} is malformed`) + } + return normalized +} + +function normalizeRequestBudget(value) { + if (!Object.isFrozen(value)) { + throw new TypeError("Convergence request budget must be frozen") + } + const budget = exactDataOptions( + value, + new Set(["operation", "timeoutMs", "signal"]), + "Convergence request budget", + ) + if (budget.operation !== "release" && budget.operation !== "releases") { + throw new TypeError("Convergence request operation is invalid") + } + const timeoutMs = boundedInteger(budget.timeoutMs, 1, 90_000, "Convergence request timeout") + assertAbortSignal(budget.signal, "Convergence request abort signal") + return Object.freeze({ operation: budget.operation, timeoutMs, signal: budget.signal }) +} + +function applyRequestBudget(init, requestBudget) { + if (init === null || typeof init !== "object" || Array.isArray(init)) { + throw new TypeError("Budgeted network request options are invalid") + } + const signal = + init.signal === undefined + ? requestBudget.signal + : AbortSignal.any([init.signal, requestBudget.signal]) + return { ...init, signal } +} + +function assertAbortSignal(value, label = "Delete abort signal") { + if ( + utilTypes.isProxy(value) || + value === null || + typeof value !== "object" || + typeof value.aborted !== "boolean" || + typeof value.addEventListener !== "function" || + typeof value.removeEventListener !== "function" + ) { + throw new TypeError(`${label} is invalid`) + } +} + +function safeStringArray(value) { + return ( + Array.isArray(value) && + value.length <= 64 && + value.every((entry) => typeof entry === "string" && !entry.includes("\u0000")) + ) +} + +function isSha(value) { + return typeof value === "string" && SHA_PATTERN.test(value) +} + +function boundedInteger(value, minimum, maximum, label) { + if (!Number.isSafeInteger(value) || value < minimum || value > maximum) { + throw new TypeError(`${label} is invalid`) + } + return value +} + +function arraysEqual(left, right) { + return left.length === right.length && left.every((entry, index) => entry === right[index]) +} + +function BASE_URL() { + return `${API_ORIGIN}/repos/${REPOSITORY}` +} + +function deepFreeze(value, seen = new Set()) { + if ( + (typeof value !== "object" && typeof value !== "function") || + value === null || + seen.has(value) + ) { + return value + } + seen.add(value) + for (const key of Reflect.ownKeys(value)) { + const descriptor = Object.getOwnPropertyDescriptor(value, key) + if (descriptor !== undefined && "value" in descriptor && descriptor.enumerable) { + deepFreeze(descriptor.value, seen) + } + } + return Object.freeze(value) +} diff --git a/scripts/release/duplicate-draft-consolidation-authority-core.mjs b/scripts/release/duplicate-draft-consolidation-authority-core.mjs new file mode 100644 index 000000000..031e10848 --- /dev/null +++ b/scripts/release/duplicate-draft-consolidation-authority-core.mjs @@ -0,0 +1,1625 @@ +import { createHash } from "node:crypto" +import { isDeepStrictEqual, types as utilTypes } from "node:util" + +import { + assertEvidenceEqualsProposal, + captureDirectTargetRead, + inspectEquivalentDrafts, + inspectFinalSurvivor, +} from "./duplicate-draft-consolidation-evidence.mjs" +import { + readPrivateEnvelope, + writePrivateEnvelope, +} from "./duplicate-draft-consolidation-files.mjs" +import { + appendJournalEvent, + deriveConsolidationState, + parseConsolidationJournal, +} from "./duplicate-draft-consolidation-journal.mjs" +import { + classifyConsolidationReleases, + consolidationStageRule, +} from "./duplicate-draft-consolidation-release-classifier.mjs" +import { + canonicalConsolidationEnvelopeBytes, + canonicalEventEnvelope, + canonicalRecordSha256, + createConsolidationEnvelope, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS, +} from "./duplicate-draft-consolidation-schema.mjs" +import { CANONICAL_RELEASE_PACKAGE_ORDER } from "./manifest.mjs" + +const REPOSITORY = "cacheplane/dawnai" +const REPOSITORY_ID = "1210070282" +const ACTOR = Object.freeze({ login: "blove", id: "61436" }) +const CANDIDATE = Object.freeze({ + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + tag: "v0.8.22", +}) +const SURVIVOR_ID = "379991871" +const DUPLICATE_IDS = Object.freeze(["379982100", "379986168"]) +const WORKFLOW_PATH = ".github/workflows/release.yml" +const WORKFLOW_STATUSES = Object.freeze([ + "in_progress", + "pending", + "queued", + "requested", + "waiting", +]) +const WORKFLOW_RUN_QUERY = Object.freeze({ + statuses: WORKFLOW_STATUSES, + perPage: 100, + maximumPages: 100, +}) +const NPM_STAGES = new Set([ + "inspect-initial", + "inspect-ready", + "perform-initial", + "pre-delete-1", + "pre-delete-2", + "final", +]) +const AUTHORITY_STAGES = new Set(["pre-delete-1", "pre-delete-2", "final"]) +const MAXIMUM_NPM_OPERATION_MS = 120_000 +const MAXIMUM_WRITER_AGE_MS = 120_000 +const TIMESTAMP_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/u +const SHA_PATTERN = /^[0-9a-f]{40}$/u +const ID_PATTERN = /^[1-9][0-9]*$/u + +export async function captureNpmInventory(input) { + const value = exactInput(input, ["stage", "candidate", "npm", "now"], "npm inventory input") + const stage = dataString(value, "stage", "npm inventory stage") + if (!NPM_STAGES.has(stage)) throw new TypeError("npm inventory stage is invalid") + const candidate = normalizeCandidate(dataValue(value, "candidate", "npm candidate")) + const npm = bindBoundary( + dataValue(value, "npm", "npm reader"), + ["observePackageVersion"], + "npm reader", + ) + const now = dataFunction(value, "now", "npm inventory clock") + + const startedAt = callTimestamp(now, "npm inventory start") + const packages = [] + let previousObservedAt = startedAt + for (const name of CANONICAL_RELEASE_PACKAGE_ORDER) { + const result = snapshotPlain( + await npm.observePackageVersion({ name, version: candidate.version }), + "npm package-version evidence", + ) + assertExactKeys( + result, + ["status", "operation", "httpStatus", "code"], + "npm package-version evidence", + ) + if ( + result.status !== "ABSENT" || + result.operation !== "package-version" || + result.httpStatus !== 404 || + result.code !== "E404" + ) { + throw new Error("npm package-version absence evidence is incomplete or ambiguous") + } + const observedAt = callTimestamp(now, "npm package observation") + assertTimestampOrder(previousObservedAt, observedAt, "npm package observation") + previousObservedAt = observedAt + packages.push({ + name, + version: candidate.version, + status: "ABSENT", + httpStatus: 404, + code: "E404", + observedAt, + }) + } + const completedAt = callTimestamp(now, "npm inventory completion") + assertTimestampOrder(packages.at(-1).observedAt, completedAt, "npm inventory completion") + if (Date.parse(completedAt) - Date.parse(startedAt) > MAXIMUM_NPM_OPERATION_MS) { + throw new Error("npm inventory operation exceeded its duration bound") + } + return deepFreeze({ stage, startedAt, completedAt, packages }) +} + +export async function captureConsolidationAuthorityCore(input, authorityCapability) { + const context = normalizeCaptureInput(input, authorityCapability) + const proposal = normalizeProposal(context.proposal) + assertProductionProposal(proposal) + const stageRule = consolidationStageRule(context.stage) + if (context.targetReleaseId !== stageRule.targetReleaseId) { + throw new Error("Authority target is not the approved current next duplicate") + } + const adapters = context.adapters.authorityEpoch.beginAuthorityCapture({ + stage: context.stage, + proposal, + targetReleaseId: context.targetReleaseId, + }) + + const localState = snapshotPlain(await adapters.local.readState(), "local checkout state") + assertExactKeys( + localState, + ["headSha", "branch", "porcelainStatus", "originMainSha"], + "local checkout state", + ) + if ( + localState.branch !== "main" || + localState.porcelainStatus !== "" || + !SHA_PATTERN.test(localState.headSha) || + localState.headSha !== localState.originMainSha + ) { + throw new Error("Local checkout must be clean symbolic main at exact origin/main") + } + + const repository = snapshotPlain(await adapters.github.getRepository(), "GitHub repository") + const actor = snapshotPlain(await adapters.github.getAuthenticatedUser(), "GitHub actor") + const githubMainSha = await adapters.github.getDefaultBranchSha() + const workflow = snapshotPlain(await adapters.github.getWorkflowState(), "Release workflow") + const nonterminalRunRead = snapshotPlain( + await adapters.github.listNonterminalWorkflowRuns(WORKFLOW_RUN_QUERY), + "nonterminal workflow-run read", + ) + const nonterminalRuns = normalizeNonterminalRunRead(nonterminalRunRead, WORKFLOW_RUN_QUERY) + const annotatedTag = snapshotPlain( + await adapters.github.getAnnotatedTag({ name: CANDIDATE.tag }), + "annotated tag", + ) + const releaseEnvelope = snapshotPlain(await adapters.github.listReleases(), "Release enumeration") + const listedReleases = presentValue(releaseEnvelope, "releases") + if (!Array.isArray(listedReleases)) throw new TypeError("Release enumeration is malformed") + + assertRepositoryAuthority({ repository, actor, proposal }) + if ( + typeof githubMainSha !== "string" || + githubMainSha !== localState.headSha || + githubMainSha !== proposal.controller.githubMainSha + ) { + throw new Error("Controller HEAD, origin/main, and GitHub main SHAs must match") + } + if ( + localState.headSha !== proposal.controller.headSha || + localState.originMainSha !== proposal.controller.originMainSha + ) { + throw new Error("Current controller SHA authority differs from the proposal") + } + const workflowAuthority = normalizeWorkflowAuthority({ + workflow, + nonterminalRuns, + query: WORKFLOW_RUN_QUERY, + observedAt: callTimestamp(adapters.now, "workflow authority observation"), + }) + const currentTag = normalizeAnnotatedTag(annotatedTag) + assertStableTagAndWorkflow(currentTag, workflowAuthority, proposal) + + const npmInventory = await captureNpmInventory({ + stage: context.stage, + candidate: proposal.candidate, + npm: adapters.npm.source, + now: adapters.now, + }) + const selectedRaw = classifyConsolidationReleases( + listedReleases, + proposal, + context.stage, + ).selected + const broadEvidence = await hydrateListedEvidence({ + stage: context.stage, + selectedRaw, + proposal, + github: adapters.github, + attestations: adapters.attestations, + contextNow: adapters.now, + }) + let releases = broadEvidence.releases + const payloadProof = broadEvidence.payloadProof + + let targetRead = null + let adapterEpoch + if (stageRule.targetReleaseId !== null) { + const targetIndex = releases.findIndex(({ id }) => id === stageRule.targetReleaseId) + if (targetIndex < 0) throw new Error("Authority target is absent from the Release list") + const terminal = adapters.beginTerminalRead({ + releaseId: stageRule.targetReleaseId, + }) + try { + targetRead = await captureDirectTargetRead({ + candidate: proposal.candidate, + releaseId: stageRule.targetReleaseId, + role: "duplicate", + expectedEvidence: proposal.releases.find(({ id }) => id === stageRule.targetReleaseId), + github: terminal.github, + now: adapters.now, + }) + adapterEpoch = terminal.seal() + } catch { + terminal.abort() + throw new Error("Terminal target read failed closed") + } + if (!isDeepStrictEqual(targetRead.evidence, releases[targetIndex])) { + throw new Error("Direct target evidence disagrees with the complete Release list") + } + releases = releases.with(targetIndex, targetRead.evidence) + } else { + try { + adapterEpoch = adapters.sealWithoutTarget() + } catch { + throw new Error("Final network epoch failed closed") + } + } + const observedAt = callTimestamp(adapters.now, "authority observation") + const authority = normalizeAuthorityStage({ + stage: context.stage, + controller: { + headSha: localState.headSha, + originMainSha: localState.originMainSha, + githubMainSha, + }, + annotatedTag: currentTag, + workflowAuthority, + npmInventory, + releases, + payloadProof, + targetRead, + observedAt, + }) + assertAuthorityTemporalOrder(authority, observedAt) + assertAuthorityAgainstProposal(authority, proposal) + if (stageRule.targetReleaseId !== null) { + assertFreshWriterAuthority(authority, proposal, observedAt) + } + + let transitionBoundary = null + const acceptTransitionBoundary = (boundary) => { + if ( + transitionBoundary !== null || + typeof boundary !== "function" || + utilTypes.isProxy(boundary) || + !Object.isFrozen(boundary) + ) { + throw new TypeError("Task6 transition boundary registration failed") + } + transitionBoundary = boundary + } + Object.freeze(acceptTransitionBoundary) + try { + await adapterEpoch.bindAuthority({ + authority, + proposal, + acceptTransitionBoundary, + }) + if (transitionBoundary === null) throw new Error("Task6 transition boundary was not registered") + adapterEpoch.validate() + } catch { + throw new Error("Network epoch was invalidated after terminal completion") + } + const networkEpoch = createNetworkEpoch({ + authority, + proposal, + targetReleaseId: stageRule.targetReleaseId, + adapterEpoch, + transitionBoundary, + }) + const result = { authority } + Object.defineProperty(result, "networkEpoch", { + value: networkEpoch, + enumerable: false, + writable: false, + configurable: false, + }) + return Object.freeze(result) +} + +export function assertFreshWriterAuthority(authority, proposal, now) { + const normalizedProposal = normalizeProposal(proposal) + assertProductionProposal(normalizedProposal) + const stage = ownDataString(authority, "stage", "writer authority stage") + if (stage !== "pre-delete-1" && stage !== "pre-delete-2") { + throw new Error("Writer authority must be a pre-delete stage") + } + const normalizedAuthority = normalizeAuthorityStage(authority) + const currentTimestamp = canonicalTimestamp(now, "writer authority clock") + assertAuthorityTemporalOrder(normalizedAuthority, currentTimestamp) + assertAuthorityAgainstProposal(normalizedAuthority, normalizedProposal) + const age = + Date.parse(currentTimestamp) - Date.parse(normalizedAuthority.npmInventory.completedAt) + if (age < 0) throw new Error("Writer authority contains a future npm observation") + if (age > MAXIMUM_WRITER_AGE_MS) { + throw new Error("Writer authority npm inventory is stale beyond 120000ms") + } + return normalizedAuthority +} + +function normalizeCaptureInput(input, authorityCapability) { + const value = exactInput( + input, + ["stage", "proposal", "targetReleaseId", "adapters"], + "authority capture input", + ) + const stage = dataString(value, "stage", "authority stage") + if (!AUTHORITY_STAGES.has(stage)) throw new TypeError("Authority stage is invalid") + const target = dataValue(value, "targetReleaseId", "authority target") + const targetReleaseId = target === null ? null : canonicalId(target, "authority target") + const rawAdapters = dataValue(value, "adapters", "consolidation adapters") + const adapters = bindAdapterFacade(rawAdapters, authorityCapability) + return { + stage, + proposal: snapshotPlain(dataValue(value, "proposal", "proposal"), "proposal"), + targetReleaseId, + adapters, + } +} + +function normalizeProposal(value) { + return deepFreeze( + createConsolidationEnvelope("proposed", snapshotPlain(value, "proposal")).record, + ) +} + +function assertProductionProposal(proposal) { + if ( + proposal.repository.name !== REPOSITORY || + proposal.repository.id !== REPOSITORY_ID || + proposal.repository.defaultBranch !== "main" || + !isDeepStrictEqual(proposal.repository.actor, ACTOR) || + !isDeepStrictEqual(proposal.candidate, CANDIDATE) || + proposal.roles.survivor !== SURVIVOR_ID || + !isDeepStrictEqual(proposal.roles.duplicates, DUPLICATE_IDS) + ) { + throw new Error("Proposal does not bind the approved production incident identity") + } +} + +function assertRepositoryAuthority({ repository, actor, proposal }) { + assertExactKeys(repository, ["name", "id", "defaultBranch"], "GitHub repository") + assertExactKeys(actor, ["login", "id"], "GitHub actor") + if ( + repository.name !== REPOSITORY || + repository.id !== REPOSITORY_ID || + repository.defaultBranch !== "main" || + actor.login !== ACTOR.login || + actor.id !== ACTOR.id || + !isDeepStrictEqual({ ...repository, actor }, proposal.repository) + ) { + throw new Error("GitHub repository or actor identity is not approved") + } +} + +function normalizeNonterminalRunRead(value, executedQuery) { + assertExactKeys(value, ["query", "runs"], "nonterminal workflow-run read") + const echoedQuery = snapshotPlain(value.query, "nonterminal workflow-run query echo") + assertExactKeys( + echoedQuery, + ["statuses", "perPage", "maximumPages"], + "nonterminal workflow-run query echo", + ) + if (!isDeepStrictEqual(echoedQuery, executedQuery)) { + throw new Error("Nonterminal workflow-run query echo differs from the executed query") + } + if (!Array.isArray(value.runs)) { + throw new TypeError("Nonterminal workflow-run result is malformed") + } + return value.runs +} + +function normalizeWorkflowAuthority({ workflow, nonterminalRuns, query, observedAt }) { + assertExactKeys(workflow, ["workflowId", "path", "state"], "Release workflow") + if ( + !ID_PATTERN.test(workflow.workflowId) || + workflow.path !== WORKFLOW_PATH || + workflow.state !== "disabled_manually" + ) { + throw new Error("Release workflow authority is missing, malformed, or active") + } + if (!Array.isArray(nonterminalRuns) || nonterminalRuns.length !== 0) { + throw new Error("Release workflow has a nonterminal or duplicate run") + } + return { + workflowId: workflow.workflowId, + path: WORKFLOW_PATH, + state: "disabled_manually", + query: { + statuses: [...query.statuses], + perPage: query.perPage, + maximumPages: query.maximumPages, + }, + nonterminalRuns: [], + observedAt, + } +} + +function normalizeAnnotatedTag(value) { + assertExactKeys( + value, + ["name", "objectSha", "targetSha", "objectType", "observedAt"], + "annotated tag", + ) + if ( + value.name !== CANDIDATE.tag || + !SHA_PATTERN.test(value.objectSha) || + value.targetSha !== CANDIDATE.commitSha || + value.objectType !== "tag" + ) { + throw new Error("Candidate tag is moved, lightweight, or malformed") + } + return { + name: value.name, + objectSha: value.objectSha, + targetSha: value.targetSha, + objectType: value.objectType, + observedAt: canonicalTimestamp(value.observedAt, "annotated tag observation"), + } +} + +function assertStableTagAndWorkflow(tag, workflow, proposal) { + for (const field of ["name", "objectSha", "targetSha", "objectType"]) { + if (tag[field] !== proposal.annotatedTag[field]) { + throw new Error("Current annotated tag differs from the approved proposal") + } + } + for (const field of ["workflowId", "path", "state"]) { + if (workflow[field] !== proposal.workflowAuthority[field]) { + throw new Error("Current Release workflow differs from the approved proposal") + } + } +} + +async function hydrateListedEvidence({ + stage, + selectedRaw, + proposal, + github, + attestations, + contextNow, +}) { + const fullyEnumerated = await hydrateCompleteAssetEnumerations(selectedRaw, github.source) + if (stage === "pre-delete-1") { + const inspected = await inspectEquivalentDrafts({ + candidate: proposal.candidate, + survivorId: proposal.roles.survivor, + duplicateIds: proposal.roles.duplicates, + releases: fullyEnumerated, + github: github.source, + attestations: attestations.source, + }) + if (!isDeepStrictEqual(inspected.payloadProof, proposal.payloadProof)) { + throw new Error("Current production payload proof differs from the proposal") + } + return inspected + } + if (stage === "final") { + const inspected = await inspectFinalSurvivor({ + candidate: proposal.candidate, + survivorId: proposal.roles.survivor, + duplicateIds: proposal.roles.duplicates, + releases: fullyEnumerated, + github: github.source, + attestations: attestations.source, + }) + if ( + !isDeepStrictEqual(inspected.payloadProof.baseAssetSet, proposal.payloadProof.baseAssetSet) || + inspected.payloadProof.baseAssetSetSha256 !== proposal.payloadProof.baseAssetSetSha256 || + !isDeepStrictEqual( + inspected.payloadProof.attestationVerification, + proposal.payloadProof.attestationVerification, + ) + ) { + throw new Error("Current final survivor payload proof differs from the proposal") + } + return deepFreeze({ releases: inspected.releases, payloadProof: proposal.payloadProof }) + } + const releases = [] + for (const raw of fullyEnumerated) { + const id = canonicalId(raw.id, "listed Release id") + const expectedEvidence = proposal.releases.find((release) => release.id === id) + if (expectedEvidence === undefined) + throw new Error("Listed Release is absent from the proposal") + const read = await captureDirectTargetRead({ + candidate: proposal.candidate, + releaseId: id, + role: expectedEvidence.role, + expectedEvidence, + github: Object.freeze({ + async getRelease() { + return { + status: "PRESENT", + operation: "release", + httpStatus: 200, + code: null, + value: raw, + } + }, + async listReleaseAssets() { + return { + status: "PRESENT", + operation: "release-assets", + httpStatus: 200, + code: null, + value: raw.assets, + } + }, + }), + now: contextNow, + }) + releases.push(read.evidence) + } + await verifyCurrentPayloadDownloads(releases, github.source) + return deepFreeze({ releases, payloadProof: proposal.payloadProof }) +} + +async function hydrateCompleteAssetEnumerations(selectedRaw, github) { + const releases = [] + for (const raw of selectedRaw) { + const releaseId = canonicalId(raw.id, "listed Release id") + const envelope = snapshotPlain( + await github.listReleaseAssets({ releaseId }), + "complete Release asset enumeration", + ) + const assets = presentValue(envelope, "release-assets") + if (!Array.isArray(assets)) { + throw new TypeError("Complete Release asset enumeration is malformed") + } + releases.push({ ...raw, assets }) + } + return releases +} + +async function verifyCurrentPayloadDownloads(releases, github) { + let downloads = 0 + for (const release of releases) { + for (const asset of release.assets) { + downloads += 1 + if (downloads > 135) { + throw new Error("Current Release payload exceeded the download-count bound") + } + const envelope = snapshotPlain( + await github.downloadReleaseAsset({ + releaseId: release.id, + assetId: asset.id, + maximumBytes: asset.size, + }), + "current Release asset download", + ) + assertExactKeys( + envelope, + ["status", "operation", "httpStatus", "code", "contentBase64"], + "current Release asset download", + ) + if ( + envelope.status !== "PRESENT" || + envelope.operation !== "release-asset-download" || + envelope.httpStatus !== 200 || + envelope.code !== null || + typeof envelope.contentBase64 !== "string" + ) { + throw new Error("Current Release asset download is unavailable or ambiguous") + } + const maximumCharacters = Math.ceil(asset.size / 3) * 4 + if (envelope.contentBase64.length > maximumCharacters) { + throw new Error("Current Release asset download exceeds its declared size") + } + const bytes = Buffer.from(envelope.contentBase64, "base64") + if ( + bytes.byteLength !== asset.size || + bytes.toString("base64") !== envelope.contentBase64 || + createHash("sha256").update(bytes).digest("hex") !== asset.downloadSha256 + ) { + throw new Error("Current Release asset bytes differ from the proven proposal") + } + } + } +} + +function normalizeAuthorityStage(value) { + const stage = ownDataString(value, "stage", "authority stage") + const event = + stage === "final" + ? { + schemaVersion: 1, + sequence: 1, + previousEventSha256: null, + type: "final-authority-observed", + recordedAt: ownDataString(value, "observedAt", "authority observation"), + payload: { authority: value }, + } + : { + schemaVersion: 1, + sequence: 1, + previousEventSha256: null, + type: "delete-authority-observed", + recordedAt: ownDataString(value, "observedAt", "authority observation"), + payload: { + targetReleaseId: consolidationStageRule(stage).targetReleaseId, + attemptNumber: 1, + authority: value, + }, + } + return deepFreeze(canonicalEventEnvelope(event, null).event.payload.authority) +} + +function assertAuthorityAgainstProposal(authority, proposal) { + const rule = consolidationStageRule(authority.stage) + if (!isDeepStrictEqual(authority.controller, proposal.controller)) { + throw new Error("Authority controller differs from the proposal") + } + assertStableTagAndWorkflow(authority.annotatedTag, authority.workflowAuthority, proposal) + if ( + authority.npmInventory.stage !== authority.stage || + authority.npmInventory.packages.some( + (entry, index) => + entry.name !== CANONICAL_RELEASE_PACKAGE_ORDER[index] || + entry.version !== proposal.candidate.version, + ) + ) { + throw new Error("Authority npm inventory does not bind the proposal candidate") + } + if ( + !isDeepStrictEqual( + authority.releases.map(({ id }) => id), + rule.releaseIds, + ) + ) { + throw new Error("Authority remaining Release identities are invalid") + } + for (const release of authority.releases) { + const proposed = proposal.releases.find(({ id }) => id === release.id) + if (proposed === undefined) throw new Error("Authority Release is absent from the proposal") + assertEvidenceEqualsProposal(release, proposed) + } + if (!isDeepStrictEqual(authority.payloadProof, proposal.payloadProof)) { + throw new Error("Authority payload proof differs from the proposal") + } + if (rule.targetReleaseId === null) { + if (authority.targetRead !== null) + throw new Error("Final authority must not contain a target read") + } else if ( + authority.targetRead === null || + authority.targetRead.evidence.id !== rule.targetReleaseId || + !isDeepStrictEqual( + authority.targetRead.evidence, + authority.releases.find(({ id }) => id === rule.targetReleaseId), + ) + ) { + throw new Error("Authority direct target is not the approved current next duplicate") + } +} + +function assertAuthorityTemporalOrder(authority, ceiling) { + const ceilingTimestamp = canonicalTimestamp(ceiling, "authority time ceiling") + const inventory = authority.npmInventory + assertTimestampOrder( + authority.annotatedTag.observedAt, + authority.workflowAuthority.observedAt, + "authority observation phase", + ) + assertTimestampOrder( + authority.workflowAuthority.observedAt, + inventory.startedAt, + "authority observation phase", + ) + let previousNpmTimestamp = inventory.startedAt + for (const observation of inventory.packages) { + assertTimestampOrder(previousNpmTimestamp, observation.observedAt, "npm observation") + previousNpmTimestamp = observation.observedAt + } + assertTimestampOrder(previousNpmTimestamp, inventory.completedAt, "npm inventory") + for (const observedAt of [ + authority.annotatedTag.observedAt, + authority.workflowAuthority.observedAt, + inventory.completedAt, + authority.observedAt, + ]) { + assertTimestampOrder(observedAt, ceilingTimestamp, "authority observation") + } + for (const release of authority.releases) { + assertTimestampOrder(release.createdAt, release.updatedAt, "Release service observation") + assertTimestampOrder(release.updatedAt, ceilingTimestamp, "Release service observation") + for (const asset of release.assets) { + assertTimestampOrder(asset.createdAt, asset.updatedAt, "asset service observation") + assertTimestampOrder(asset.updatedAt, ceilingTimestamp, "asset service observation") + } + } + if (authority.targetRead !== null) { + assertTargetReadChronology(authority, inventory.completedAt) + } else { + assertTimestampOrder(inventory.completedAt, authority.observedAt, "authority observation phase") + } +} + +function assertTargetReadChronology(authority, npmCompletedAt) { + const chronology = [ + npmCompletedAt, + authority.targetRead.releaseGetStartedAt, + authority.targetRead.releaseGetCompletedAt, + authority.targetRead.assetsListStartedAt, + authority.targetRead.assetsListCompletedAt, + authority.observedAt, + ] + for (let index = 1; index < chronology.length; index += 1) { + assertTimestampOrder( + chronology[index - 1], + chronology[index], + "terminal target-read chronology", + ) + } +} + +function createNetworkEpoch({ + authority, + proposal, + targetReleaseId, + adapterEpoch, + transitionBoundary, +}) { + const authoritySha256 = canonicalRecordSha256(authority) + const proposalSha256 = canonicalRecordSha256(proposal) + let consumed = false + const capability = {} + Object.defineProperties(capability, { + consume: { + enumerable: false, + configurable: false, + writable: false, + async value(input) { + if (consumed) throw new Error("Network epoch has already been consumed") + consumed = true + const value = exactInput( + input, + [ + "authority", + "proposal", + "confirmation", + "targetReleaseId", + "intentPath", + "currentJournal", + ], + "network epoch consumption", + ) + assertAdapterEpochSealed(adapterEpoch) + const currentTimestamp = readTrustedEpochClock(adapterEpoch) + const consumedAuthority = assertFreshWriterAuthority( + dataValue(value, "authority", "epoch authority"), + dataValue(value, "proposal", "epoch proposal"), + currentTimestamp, + ) + const consumedProposal = normalizeProposal(dataValue(value, "proposal", "epoch proposal")) + const proposedEnvelope = createConsolidationEnvelope("proposed", consumedProposal) + const confirmation = dataString(value, "confirmation", "operator confirmation") + assertExactIncidentConfirmation(confirmation, proposedEnvelope) + const consumedTarget = canonicalId( + dataValue(value, "targetReleaseId", "epoch target"), + "epoch target", + ) + if ( + canonicalRecordSha256(consumedAuthority) !== authoritySha256 || + canonicalRecordSha256(consumedProposal) !== proposalSha256 || + consumedTarget !== targetReleaseId + ) { + throw new Error("Network epoch authority, proposal, or target binding changed") + } + const intentPath = dataString(value, "intentPath", "journal intent path") + if (intentPath !== adapterEpoch.journalPath) { + throw new Error("Journal intent path is not the adapter-owned private path") + } + const expectedCurrent = parseConsolidationJournal( + dataValue(value, "currentJournal", "current journal"), + ) + const expectedConfirmationSha256 = createHash("sha256") + .update(confirmation, "utf8") + .digest("hex") + const expectedCurrentState = deriveConsolidationState(expectedCurrent) + if ( + expectedCurrent.record.proposedRecordSha256 !== proposalSha256 || + expectedCurrent.record.confirmationSha256 !== expectedConfirmationSha256 || + expectedCurrentState.controllerSha !== consumedAuthority.controller.headSha + ) { + throw new Error("Current journal does not bind proposal confirmation and controller") + } + return writePrivateEnvelope.withExclusiveTransaction(intentPath, async () => { + let currentBytes + try { + currentBytes = await readPrivateEnvelope( + intentPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ) + } catch { + throw new Error( + "Exact current private journal could not be authenticated; do not DELETE", + ) + } + const currentEnvelope = parseConsolidationJournal(currentBytes) + const expectedCurrentBytes = canonicalConsolidationEnvelopeBytes( + "journal", + expectedCurrent, + ) + if ( + !currentBytes.equals(expectedCurrentBytes) || + !isDeepStrictEqual(currentEnvelope, expectedCurrent) + ) { + throw new Error("Current journal file differs from the authenticated expected history") + } + const journalHeadPath = `${intentPath.slice(0, -"journal.json".length)}journal.head.json` + let currentHeadBytes + try { + currentHeadBytes = await reconcileJournalHead({ + journalHeadPath, + journalPath: intentPath, + journal: currentEnvelope, + }) + } catch { + throw new Error( + "Durable journal head anchor is missing, ahead, divergent, or unsafe; do not DELETE", + ) + } + if ( + expectedCurrentState.phase !== "delete-authority-observed" || + expectedCurrentState.currentTargetReleaseId !== consumedTarget || + !isDeepStrictEqual(expectedCurrentState.lastAuthority, consumedAuthority) + ) { + throw new Error("Current journal is not the exact legal delete-authority predecessor") + } + const beforeWriteTimestamp = readTrustedEpochClock(adapterEpoch) + assertFreshWriterAuthority(consumedAuthority, consumedProposal, beforeWriteTimestamp) + assertAdapterEpochSealed(adapterEpoch) + const intentEnvelope = appendJournalEvent( + currentEnvelope, + "delete-intent", + { + targetReleaseId: consumedTarget, + attemptNumber: expectedCurrentState.attemptNumber, + authorityEventSha256: expectedCurrentState.lastEventSha256, + }, + beforeWriteTimestamp, + ) + const intentBytes = canonicalConsolidationEnvelopeBytes("journal", intentEnvelope) + try { + await writePrivateEnvelope(intentPath, intentBytes, undefined, currentBytes) + } catch { + throw new Error( + "Journal intent may already be durable; persistence failed, so do not DELETE or reconsume", + ) + } + try { + const committedJournal = await readPrivateEnvelope( + intentPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ) + if (!committedJournal.equals(intentBytes)) { + throw new Error("Committed journal bytes differ from the legal intent") + } + const parsedCommitted = parseConsolidationJournal(committedJournal) + if (!isDeepStrictEqual(parsedCommitted, intentEnvelope)) { + throw new Error("Committed journal envelope differs from the legal intent") + } + const committedHeadBytes = canonicalJournalHeadBytes(intentPath, intentEnvelope) + await writePrivateEnvelope( + journalHeadPath, + committedHeadBytes, + undefined, + currentHeadBytes, + ) + const committedHead = await readPrivateEnvelope(journalHeadPath, 16 * 1024) + if (!committedHead.equals(committedHeadBytes)) { + throw new Error("Committed journal head differs from the legal intent") + } + const completedTimestamp = readTrustedEpochClock(adapterEpoch) + assertFreshWriterAuthority(consumedAuthority, consumedProposal, completedTimestamp) + adapterEpoch.validate() + return transitionBoundary({ + targetReleaseId: consumedTarget, + authority: consumedAuthority, + proposedEnvelope, + confirmation, + predecessorJournal: currentBytes, + predecessorHead: currentHeadBytes, + committedJournal, + committedHead, + }) + } catch { + throw new Error( + "Journal intent may already be durable; post-write authority failed, so do not DELETE or reconsume", + ) + } + }) + }, + }, + toJSON: { + enumerable: false, + configurable: false, + writable: false, + value() { + throw new TypeError("Network epoch capability cannot be serialized") + }, + }, + }) + return Object.freeze(capability) +} + +function assertAdapterEpochSealed(adapterEpoch) { + try { + adapterEpoch.validate() + } catch { + throw new Error("Adapter network epoch is invalid or no longer sealed") + } +} + +function readTrustedEpochClock(adapterEpoch) { + try { + return canonicalTimestamp(adapterEpoch.now(), "trusted adapter clock") + } catch { + throw new TypeError("Trusted adapter clock failed closed") + } +} + +async function reconcileJournalHead({ journalHeadPath, journalPath, journal }) { + let headBytes + try { + headBytes = await readPrivateEnvelope(journalHeadPath, 16 * 1024) + } catch (error) { + if (!hasErrorCode(error, "ENOENT")) throw error + if (!isExactJournalGenesis(journal)) { + throw new Error("Missing durable journal head may bootstrap only exact operation genesis") + } + await writePrivateEnvelope( + journalHeadPath, + canonicalJournalHeadBytes(journalPath, journal), + undefined, + null, + ) + return readPrivateEnvelope(journalHeadPath, 16 * 1024) + } + const head = parseJournalHead(headBytes, journalPath) + if (journalHeadMatches(head, journal)) return headBytes + if (head.sequence + 1 !== journal.record.events.length) { + throw new Error("Journal and durable head have divergent sequence lineage") + } + const predecessor = createConsolidationEnvelope("journal", { + ...journal.record, + events: journal.record.events.slice(0, -1), + updatedAt: journal.record.events.at(-2).event.recordedAt, + }) + parseConsolidationJournal(predecessor) + if (!journalHeadMatches(head, predecessor)) { + throw new Error("Journal is not one legal append ahead of its durable head") + } + await writePrivateEnvelope( + journalHeadPath, + canonicalJournalHeadBytes(journalPath, journal), + undefined, + headBytes, + ) + return readPrivateEnvelope(journalHeadPath, 16 * 1024) +} + +function isExactJournalGenesis(journal) { + return ( + journal.record.events.length === 1 && + journal.record.events[0].event.type === "operation-started" && + journal.record.events[0].event.sequence === 1 && + journal.record.events[0].event.previousEventSha256 === null + ) +} + +function canonicalJournalHeadBytes(journalPath, journal) { + return Buffer.from( + `${JSON.stringify({ + schemaVersion: 1, + journalPath, + repository: journal.record.repository, + proposedRecordSha256: journal.record.proposedRecordSha256, + journalRecordSha256: journal.recordSha256, + lastEventSha256: journal.record.events.at(-1).eventSha256, + sequence: journal.record.events.length, + updatedAt: journal.record.updatedAt, + })}\n`, + "utf8", + ) +} + +function parseJournalHead(bytes, expectedJournalPath) { + let value + try { + value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) + } catch { + throw new Error("Journal head is not canonical UTF-8 JSON") + } + assertExactKeys( + value, + [ + "schemaVersion", + "journalPath", + "repository", + "proposedRecordSha256", + "journalRecordSha256", + "lastEventSha256", + "sequence", + "updatedAt", + ], + "journal head", + ) + if ( + value.schemaVersion !== 1 || + value.journalPath !== expectedJournalPath || + !Number.isSafeInteger(value.sequence) || + value.sequence < 1 || + !/^[0-9a-f]{64}$/u.test(value.proposedRecordSha256) || + !/^[0-9a-f]{64}$/u.test(value.journalRecordSha256) || + !/^[0-9a-f]{64}$/u.test(value.lastEventSha256) || + canonicalTimestamp(value.updatedAt, "journal head timestamp") !== value.updatedAt || + !Buffer.from(`${JSON.stringify(value)}\n`, "utf8").equals(bytes) + ) { + throw new Error("Journal head fields or canonical bytes are invalid") + } + return value +} + +function journalHeadMatches(head, journal) { + return ( + head.journalRecordSha256 === journal.recordSha256 && + head.proposedRecordSha256 === journal.record.proposedRecordSha256 && + head.lastEventSha256 === journal.record.events.at(-1).eventSha256 && + head.sequence === journal.record.events.length && + head.updatedAt === journal.record.updatedAt && + isDeepStrictEqual(head.repository, journal.record.repository) + ) +} + +function hasErrorCode(error, code) { + if (error !== null && typeof error === "object") { + if (error.code === code) return true + if (hasErrorCode(error.cause, code)) return true + if (Array.isArray(error.errors) && error.errors.some((entry) => hasErrorCode(entry, code))) + return true + } + return false +} + +function exactInput(value, expectedKeys, label) { + if (!isPlainObject(value) || utilTypes.isProxy(value)) + throw new TypeError(`${label} must be a plain non-proxy object`) + assertExactKeys(value, expectedKeys, label, { allowFunctions: true }) + return value +} + +function bindAdapterFacade(value, authorityCapability) { + if ( + !isPlainObject(value) || + utilTypes.isProxy(value) || + !Object.isFrozen(value) || + Object.getOwnPropertySymbols(value).length !== 0 + ) { + throw new TypeError("consolidation adapters must be an immutable plain non-proxy facade") + } + const descriptors = Object.getOwnPropertyDescriptors(value) + const expected = new Set([ + "local", + "github", + "npm", + "attestations", + "writer", + "captureConsolidationAuthority", + "captureInspectionTerminal", + "assertInspectionTerminalSealed", + ]) + if ( + Object.keys(descriptors).length !== expected.size || + Object.keys(descriptors).some((key) => !expected.has(key)) + ) { + throw new TypeError("consolidation adapter facade fields are invalid") + } + for (const name of ["local", "github", "npm", "attestations", "writer"]) { + const descriptor = descriptors[name] + if ( + descriptor?.enumerable !== true || + !("value" in descriptor) || + descriptor.get !== undefined || + descriptor.set !== undefined + ) { + throw new TypeError("consolidation adapter facade fields are invalid") + } + } + const captureDescriptor = descriptors.captureConsolidationAuthority + if ( + captureDescriptor?.enumerable !== false || + captureDescriptor.writable !== false || + captureDescriptor.configurable !== false || + typeof captureDescriptor.value !== "function" + ) { + throw new TypeError("adapter safe authority capture descriptor is invalid") + } + for (const name of ["captureInspectionTerminal", "assertInspectionTerminalSealed"]) { + const descriptor = descriptors[name] + if ( + descriptor?.enumerable !== false || + descriptor.writable !== false || + descriptor.configurable !== false || + typeof descriptor.value !== "function" || + utilTypes.isProxy(descriptor.value) || + !Object.isFrozen(descriptor.value) + ) { + throw new TypeError("adapter inspection terminal descriptor is invalid") + } + } + const authorityEpoch = bindAuthorityCapability(authorityCapability, value) + return Object.freeze({ + local: bindBoundary(descriptors.local.value, ["readState"], "local Git reader"), + github: bindBoundary( + descriptors.github.value, + [ + "getRepository", + "getAuthenticatedUser", + "getDefaultBranchSha", + "getWorkflowState", + "listNonterminalWorkflowRuns", + "getAnnotatedTag", + "listReleases", + "getRelease", + "listReleaseAssets", + "downloadReleaseAsset", + ], + "GitHub authority reader", + ), + npm: bindBoundary(descriptors.npm.value, ["observePackageVersion"], "npm authority reader"), + attestations: bindBoundary( + descriptors.attestations.value, + ["verify"], + "attestation authority reader", + ), + writer: bindBoundary(descriptors.writer.value, ["deleteDuplicate"], "duplicate delete writer"), + authorityEpoch, + now: authorityEpoch.now, + }) +} + +function bindAuthorityCapability(value, facade) { + assertHiddenCapability( + value, + [ + "now", + "journalPath", + "validateFacade", + "beginAuthorityCapture", + "beginTerminalRead", + "sealWithoutTarget", + "toJSON", + ], + "adapter authority capability", + ) + const now = hiddenDataFunction(value, "now", "adapter authority clock") + const validateFacade = hiddenDataFunction(value, "validateFacade", "adapter facade validator") + const beginTerminalRead = hiddenDataFunction( + value, + "beginTerminalRead", + "terminal-read capability", + ) + const beginAuthorityCapture = hiddenDataFunction( + value, + "beginAuthorityCapture", + "authority-capture capability", + ) + const sealWithoutTarget = hiddenDataFunction(value, "sealWithoutTarget", "final-stage capability") + try { + validateFacade.call(value, facade) + } catch { + throw new TypeError("adapter authority capability binding failed closed") + } + return Object.freeze({ + now: () => { + try { + return now.call(value) + } catch { + throw new TypeError("Trusted adapter clock failed closed") + } + }, + journalPath: hiddenDataValue(value, "journalPath", "adapter journal path"), + beginAuthorityCapture(input) { + try { + return bindAuthorityCapture(beginAuthorityCapture.call(value, input)) + } catch { + throw new Error("Authority capture session failed closed") + } + }, + beginTerminalRead(input) { + try { + return bindTerminalCapability(beginTerminalRead.call(value, input)) + } catch { + throw new Error("Terminal network epoch failed closed") + } + }, + sealWithoutTarget() { + try { + return bindFinalEpoch(sealWithoutTarget.call(value)) + } catch { + throw new Error("Final network epoch failed closed") + } + }, + }) +} + +function bindAuthorityCapture(value) { + assertHiddenCapability( + value, + [ + "local", + "github", + "npm", + "attestations", + "now", + "beginTerminalRead", + "sealWithoutTarget", + "abort", + "toJSON", + ], + "authority capture capability", + ) + const beginTerminalRead = hiddenDataFunction( + value, + "beginTerminalRead", + "captured terminal-read capability", + ) + const sealWithoutTarget = hiddenDataFunction( + value, + "sealWithoutTarget", + "captured final-stage capability", + ) + const abort = hiddenDataFunction(value, "abort", "authority capture abort") + const now = hiddenDataFunction(value, "now", "authority capture clock") + return Object.freeze({ + local: bindBoundary( + hiddenDataValue(value, "local", "captured local reader"), + ["readState"], + "authority captured local Git reader", + ), + github: bindBoundary( + hiddenDataValue(value, "github", "captured GitHub reader"), + [ + "getRepository", + "getAuthenticatedUser", + "getDefaultBranchSha", + "getWorkflowState", + "listNonterminalWorkflowRuns", + "getAnnotatedTag", + "listReleases", + "getRelease", + "listReleaseAssets", + "downloadReleaseAsset", + ], + "authority captured GitHub Release reader", + ), + npm: bindBoundary( + hiddenDataValue(value, "npm", "captured npm reader"), + ["observePackageVersion"], + "authority captured npm reader", + ), + attestations: bindBoundary( + hiddenDataValue(value, "attestations", "captured attestation verifier"), + ["verify"], + "authority captured attestation verifier", + ), + now: () => now.call(value), + beginTerminalRead(input) { + return bindTerminalCapability(beginTerminalRead.call(value, input)) + }, + sealWithoutTarget() { + return bindSealedEpoch(sealWithoutTarget.call(value)) + }, + abort: () => abort.call(value), + }) +} + +function bindFinalEpoch(value) { + assertHiddenCapability( + value, + ["now", "journalPath", "validate", "bindAuthority", "toJSON"], + "final adapter epoch", + ) + const now = hiddenDataFunction(value, "now", "final adapter clock") + const validate = hiddenDataFunction(value, "validate", "final epoch validator") + const bindAuthority = hiddenDataFunction(value, "bindAuthority", "captured authority binder") + return Object.freeze({ + now: () => now.call(value), + journalPath: hiddenDataValue(value, "journalPath", "final journal path"), + validate: () => validate.call(value), + bindAuthority: (input) => bindAuthority.call(value, input), + }) +} + +function bindTerminalCapability(value) { + assertHiddenCapability( + value, + ["github", "seal", "abort", "toJSON"], + "terminal network capability", + ) + const github = bindBoundary( + hiddenDataValue(value, "github", "terminal GitHub reader"), + ["getRelease", "listReleaseAssets"], + "terminal GitHub reader", + ).source + const seal = hiddenDataFunction(value, "seal", "terminal epoch seal") + const abort = hiddenDataFunction(value, "abort", "terminal epoch abort") + return Object.freeze({ + github, + seal() { + try { + return bindSealedEpoch(seal.call(value)) + } catch { + throw new Error("Terminal network epoch seal failed closed") + } + }, + abort() { + try { + abort.call(value) + } catch { + throw new Error("Terminal network epoch abort failed closed") + } + }, + }) +} + +function bindSealedEpoch(value) { + assertHiddenCapability( + value, + ["now", "journalPath", "validate", "bindAuthority", "toJSON"], + "sealed adapter epoch", + ) + const now = hiddenDataFunction(value, "now", "sealed adapter clock") + const validate = hiddenDataFunction(value, "validate", "sealed epoch validator") + const bindAuthority = hiddenDataFunction(value, "bindAuthority", "captured authority binder") + return Object.freeze({ + now: () => now.call(value), + journalPath: hiddenDataValue(value, "journalPath", "sealed journal path"), + validate: () => validate.call(value), + bindAuthority: (input) => bindAuthority.call(value, input), + }) +} + +function assertHiddenCapability(value, expectedKeys, label) { + if ( + !isPlainObject(value) || + utilTypes.isProxy(value) || + !Object.isFrozen(value) || + Object.getOwnPropertySymbols(value).length !== 0 + ) { + throw new TypeError(`${label} is invalid`) + } + const descriptors = Object.getOwnPropertyDescriptors(value) + const expected = new Set(expectedKeys) + if ( + Object.keys(descriptors).length !== expected.size || + Object.keys(descriptors).some((key) => !expected.has(key)) || + Object.values(descriptors).some( + (descriptor) => + descriptor.enumerable !== false || + descriptor.writable !== false || + descriptor.configurable !== false || + !("value" in descriptor), + ) + ) { + throw new TypeError(`${label} fields or descriptors are invalid`) + } +} + +function hiddenDataValue(value, name, label) { + const descriptor = Object.getOwnPropertyDescriptor(value, name) + if ( + descriptor?.enumerable !== false || + descriptor.writable !== false || + descriptor.configurable !== false || + !("value" in descriptor) + ) { + throw new TypeError(`${label} is invalid`) + } + return descriptor.value +} + +function hiddenDataFunction(value, name, label) { + const result = hiddenDataValue(value, name, label) + if (typeof result !== "function" || utilTypes.isProxy(result)) { + throw new TypeError(`${label} must be a non-proxy function`) + } + return result +} + +function bindBoundary(value, methods, label) { + if (!isPlainObject(value) || utilTypes.isProxy(value) || !Object.isFrozen(value)) { + throw new TypeError(`${label} must be an immutable plain non-proxy object`) + } + assertExactKeys(value, methods, label, { allowFunctions: true }) + const bound = {} + for (const method of methods) { + const fn = dataFunction(value, method, `${label} method`) + bound[method] = async (...args) => { + try { + return await fn.apply(value, args) + } catch { + throw new Error(`${label} operation failed closed`) + } + } + } + const source = Object.freeze(bound) + return Object.freeze({ ...source, source }) +} + +function dataValue(value, name, label) { + const descriptor = Object.getOwnPropertyDescriptor(value, name) + if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) { + throw new TypeError(`${label} must be an enumerable data property`) + } + return descriptor.value +} + +function dataString(value, name, label) { + const result = dataValue(value, name, label) + if (typeof result !== "string") throw new TypeError(`${label} must be a string`) + return result +} + +function dataFunction(value, name, label) { + const result = dataValue(value, name, label) + if (typeof result !== "function" || utilTypes.isProxy(result)) { + throw new TypeError(`${label} must be a non-proxy function`) + } + return result +} + +function ownDataString(value, name, label) { + if (!isPlainObject(value) || utilTypes.isProxy(value)) + throw new TypeError(`${label} object is invalid`) + return dataString(value, name, label) +} + +function assertExactKeys(value, expected, label, { allowFunctions = false } = {}) { + if (!isPlainObject(value) || utilTypes.isProxy(value)) + throw new TypeError(`${label} must be a plain object`) + if (Object.getOwnPropertySymbols(value).length !== 0) + throw new TypeError(`${label} contains symbol properties`) + const descriptors = Object.getOwnPropertyDescriptors(value) + const keys = Object.keys(descriptors) + const expectedSet = new Set(expected) + if ( + keys.length !== expected.length || + keys.some((key) => !expectedSet.has(key)) || + keys.some((key) => { + const descriptor = descriptors[key] + return ( + !descriptor.enumerable || + !("value" in descriptor) || + (!allowFunctions && typeof descriptor.value === "function") + ) + }) + ) { + throw new TypeError(`${label} fields or descriptors are invalid`) + } +} + +function snapshotPlain(value, label) { + if (utilTypes.isProxy(value)) throw new TypeError(`${label} must not be a proxy`) + if (Array.isArray(value)) { + const descriptors = Object.getOwnPropertyDescriptors(value) + const symbols = Object.getOwnPropertySymbols(value) + if (symbols.length !== 0) throw new TypeError(`${label} contains symbol properties`) + const keys = Object.keys(descriptors).filter((key) => key !== "length") + if (keys.length !== value.length || keys.some((key, index) => key !== String(index))) { + throw new TypeError(`${label} must be a dense canonical array`) + } + return keys.map((key) => { + const descriptor = descriptors[key] + if (!descriptor.enumerable || !("value" in descriptor)) + throw new TypeError(`${label} contains an accessor`) + return snapshotPlain(descriptor.value, `${label}[${key}]`) + }) + } + if (value !== null && typeof value === "object") { + if (!isPlainObject(value)) throw new TypeError(`${label} must contain only plain objects`) + const descriptors = Object.getOwnPropertyDescriptors(value) + if (Object.getOwnPropertySymbols(value).length !== 0) + throw new TypeError(`${label} contains symbol properties`) + const result = {} + for (const [key, descriptor] of Object.entries(descriptors)) { + if ( + !descriptor.enumerable || + !("value" in descriptor) || + typeof descriptor.value === "function" + ) { + throw new TypeError(`${label} contains hidden, accessor, or function properties`) + } + result[key] = snapshotPlain(descriptor.value, `${label}.${key}`) + } + return result + } + if (typeof value === "symbol" || typeof value === "function" || typeof value === "bigint") { + throw new TypeError(`${label} contains a non-data value`) + } + return value +} + +function presentValue(value, operation) { + assertExactKeys( + value, + ["status", "operation", "httpStatus", "code", "value"], + `${operation} envelope`, + ) + if ( + value.status !== "PRESENT" || + value.operation !== operation || + value.httpStatus !== 200 || + value.code !== null + ) { + throw new Error(`${operation} evidence is unavailable or ambiguous`) + } + return value.value +} + +function normalizeCandidate(value) { + const candidate = snapshotPlain(value, "candidate") + assertExactKeys(candidate, ["version", "commitSha", "tag"], "candidate") + if (!isDeepStrictEqual(candidate, CANDIDATE)) { + throw new Error("Candidate is not the approved v0.8.22 incident") + } + return candidate +} + +function canonicalId(value, label) { + const result = typeof value === "number" && Number.isSafeInteger(value) ? String(value) : value + if (typeof result !== "string" || !ID_PATTERN.test(result)) + throw new TypeError(`${label} is invalid`) + return result +} + +function callTimestamp(now, label) { + let value + try { + value = now() + } catch { + throw new TypeError(`${label} clock failed`) + } + return canonicalTimestamp(value, label) +} + +function canonicalTimestamp(value, label) { + if ( + typeof value !== "string" || + !TIMESTAMP_PATTERN.test(value) || + !Number.isFinite(Date.parse(value)) || + new Date(Date.parse(value)).toISOString() !== value + ) { + throw new TypeError(`${label} must be a canonical timestamp`) + } + return value +} + +function assertExactIncidentConfirmation(value, proposedEnvelope) { + if ( + typeof value !== "string" || + Buffer.byteLength(value, "utf8") > 512 || + [...value].some((character) => { + const codePoint = character.codePointAt(0) + return codePoint <= 31 || (codePoint >= 127 && codePoint <= 159) + }) + ) { + throw new TypeError("Operator confirmation must be an exact control-free string") + } + const { candidate, roles } = proposedEnvelope.record + const expected = `CONSOLIDATE v${candidate.version} ${candidate.commitSha} SURVIVOR ${roles.survivor} DELETE ${roles.duplicates.join(",")} PROPOSAL ${proposedEnvelope.recordSha256}` + if (value !== expected) { + throw new Error("Operator confirmation does not exactly bind the proposal") + } +} + +function assertTimestampOrder(earlier, later, label) { + const first = canonicalTimestamp(earlier, label) + const second = canonicalTimestamp(later, label) + if (Date.parse(second) < Date.parse(first)) + throw new Error(`${label} timestamps are not monotone`) +} + +function isPlainObject(value) { + if (value === null || typeof value !== "object" || Array.isArray(value)) return false + const prototype = Object.getPrototypeOf(value) + return prototype === Object.prototype || prototype === null +} + +function deepFreeze(value) { + if (value !== null && typeof value === "object" && !Object.isFrozen(value)) { + for (const child of Object.values(value)) deepFreeze(child) + Object.freeze(value) + } + return value +} diff --git a/scripts/release/duplicate-draft-consolidation-authority.mjs b/scripts/release/duplicate-draft-consolidation-authority.mjs new file mode 100644 index 000000000..6e3da1627 --- /dev/null +++ b/scripts/release/duplicate-draft-consolidation-authority.mjs @@ -0,0 +1,49 @@ +import { types as utilTypes } from "node:util" + +export { + assertFreshWriterAuthority, + captureNpmInventory, +} from "./duplicate-draft-consolidation-authority-core.mjs" + +export async function captureConsolidationAuthority(input) { + if ( + input === null || + typeof input !== "object" || + utilTypes.isProxy(input) || + ![Object.prototype, null].includes(Object.getPrototypeOf(input)) + ) { + throw new TypeError("authority capture input must be a plain non-proxy object") + } + const adapterDescriptor = Object.getOwnPropertyDescriptor(input, "adapters") + if ( + adapterDescriptor === undefined || + !("value" in adapterDescriptor) || + adapterDescriptor.get !== undefined || + adapterDescriptor.set !== undefined + ) { + throw new TypeError("authority capture input adapters descriptor is unavailable") + } + const adapters = adapterDescriptor.value + if ( + adapters === null || + typeof adapters !== "object" || + utilTypes.isProxy(adapters) || + !Object.isFrozen(adapters) + ) { + throw new TypeError("authority capture adapters are invalid") + } + const captureDescriptor = Object.getOwnPropertyDescriptor( + adapters, + "captureConsolidationAuthority", + ) + if ( + captureDescriptor?.enumerable !== false || + captureDescriptor.writable !== false || + captureDescriptor.configurable !== false || + typeof captureDescriptor.value !== "function" || + utilTypes.isProxy(captureDescriptor.value) + ) { + throw new TypeError("safe authority capture entrypoint is unavailable") + } + return Reflect.apply(captureDescriptor.value, adapters, [input]) +} diff --git a/scripts/release/duplicate-draft-consolidation-cli.mjs b/scripts/release/duplicate-draft-consolidation-cli.mjs new file mode 100644 index 000000000..ba2018948 --- /dev/null +++ b/scripts/release/duplicate-draft-consolidation-cli.mjs @@ -0,0 +1,458 @@ +import { EventEmitter } from "node:events" +import path from "node:path" +import { Writable } from "node:stream" +import { + clearTimeout as clearTimer, + setImmediate as scheduleImmediate, + setTimeout as startTimer, +} from "node:timers" +import { setTimeout as waitFor } from "node:timers/promises" +import { fileURLToPath } from "node:url" +import { types as utilTypes } from "node:util" +import { + inspectDuplicateDrafts, + performDuplicateDraftConsolidation, + verifyDuplicateDraftConsolidation, +} from "./duplicate-draft-consolidation.mjs" +import { createDuplicateDraftConsolidationAdapters } from "./duplicate-draft-consolidation-adapters.mjs" + +const INSPECT_EXPECTED = Object.freeze([ + "inspect", + "--version", + "0.8.22", + "--commit-sha", + "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + "--survivor", + "379991871", + "--duplicates", + "379982100,379986168", + "--output", + ".dawn/release/duplicate-draft-consolidation.proposed.json", +]) +const PERFORM_PREFIX = Object.freeze([ + "perform", + "--proposal", + ".dawn/release/duplicate-draft-consolidation.proposed.json", + "--journal", + ".dawn/release/duplicate-draft-consolidation.journal.json", + "--receipt", + "scripts/release/duplicate-draft-consolidation.json", + "--confirmation", +]) +const CONFIRMATION_PATTERN = + /^CONSOLIDATE v0\.8\.22 2a80deece2ff958fe7fde8fddeb4f99bed70a1c8 SURVIVOR 379991871 DELETE 379982100,379986168 PROPOSAL ([0-9a-f]{64})$/u +const VERIFY_EXPECTED = Object.freeze([ + "verify", + "--receipt", + "scripts/release/duplicate-draft-consolidation.json", +]) + +export async function runDuplicateDraftConsolidationCli(options = {}) { + let invocation + try { + invocation = normalizeOptions(options) + const input = parseArguments(invocation.argv) + const now = invocation.dependencies.now ?? (() => new Date().toISOString()) + const wait = + invocation.dependencies.wait ?? + ((milliseconds, { signal }) => waitFor(milliseconds, undefined, { signal })) + const createAdapters = + invocation.dependencies.createAdapters ?? createDuplicateDraftConsolidationAdapters + const inspect = invocation.dependencies.inspect ?? inspectDuplicateDrafts + const perform = invocation.dependencies.perform ?? performDuplicateDraftConsolidation + const verify = invocation.dependencies.verify ?? verifyDuplicateDraftConsolidation + for (const operation of [now, wait, createAdapters, inspect, perform, verify]) { + if (typeof operation !== "function" || utilTypes.isProxy(operation)) + throw new InvocationError() + } + const repositoryRootIdentity = await inspectionRootCapture()(invocation.cwd) + let result + if (input.mode === "inspect") { + result = await inspect(input.value, { + repositoryRoot: invocation.cwd, + adapters: await createAdapters({ + cwd: invocation.cwd, + environment: invocation.environment, + dependencies: { now }, + }), + now, + wait, + repositoryRootIdentity, + }) + } else if (input.mode === "perform") { + result = await perform(input.value, { + repositoryRoot: invocation.cwd, + createAdapters: (requestBudget) => + createAdapters({ + cwd: invocation.cwd, + environment: invocation.environment, + dependencies: { now }, + ...(requestBudget === undefined ? {} : { requestBudget }), + }), + now, + wait, + }) + } else { + result = await verify(input.value, { + repositoryRoot: invocation.cwd, + createAdapters: () => + createAdapters({ + cwd: invocation.cwd, + environment: invocation.environment, + dependencies: { now }, + }), + }) + } + const summary = + input.mode === "inspect" + ? safeSummary(result, input.value) + : input.mode === "perform" + ? safePerformSummary(result) + : safeVerifySummary(result) + if (!(await writeSink(invocation.stdout, `${JSON.stringify(summary)}\n`))) { + await writeSink(invocation.stderr, `Duplicate-draft ${operationLabel(input.mode)} failed.\n`) + return 1 + } + return 0 + } catch (error) { + const target = invocation?.stderr ?? safeInvocationStderr(options) ?? bindSink(process.stderr) + await writeSink( + target, + error instanceof InvocationError + ? "Invalid duplicate-draft consolidation invocation.\n" + : `Duplicate-draft ${operationLabel(invocation?.mode)} failed.\n`, + ) + return error instanceof InvocationError ? 2 : 1 + } +} + +function inspectionRootCapture() { + const descriptor = Object.getOwnPropertyDescriptor( + inspectDuplicateDrafts, + "captureRepositoryRoot", + ) + if ( + descriptor?.enumerable !== false || + descriptor.writable !== false || + descriptor.configurable !== false || + typeof descriptor.value !== "function" || + utilTypes.isProxy(descriptor.value) || + !Object.isFrozen(descriptor.value) + ) { + throw new Error("Inspection root capture is unavailable") + } + return descriptor.value +} + +async function writeSink(sink, chunk) { + try { + await sink.write(chunk) + return true + } catch { + return false + } +} + +function safeInvocationStderr(options) { + if (options === null || typeof options !== "object" || utilTypes.isProxy(options)) return null + const descriptor = Object.getOwnPropertyDescriptor(options, "stderr") + if (descriptor?.enumerable !== true || !("value" in descriptor)) return null + try { + return bindSink(descriptor.value) + } catch { + return null + } +} + +function parseArguments(argv) { + const snapshot = snapshotArguments(argv) + if (snapshot.length === INSPECT_EXPECTED.length) { + for (const [index, expected] of INSPECT_EXPECTED.entries()) { + if (snapshot[index] !== expected) throw new InvocationError() + } + return { + mode: "inspect", + value: { + version: INSPECT_EXPECTED[2], + commitSha: INSPECT_EXPECTED[4], + survivor: INSPECT_EXPECTED[6], + duplicates: INSPECT_EXPECTED[8].split(","), + output: INSPECT_EXPECTED[10], + }, + } + } + if (snapshot.length === PERFORM_PREFIX.length + 1) { + for (const [index, expected] of PERFORM_PREFIX.entries()) { + if (snapshot[index] !== expected) throw new InvocationError() + } + const confirmation = snapshot.at(-1) + const match = CONFIRMATION_PATTERN.exec(confirmation) + if (match === null) throw new InvocationError() + return { + mode: "perform", + value: { + proposal: PERFORM_PREFIX[2], + proposalSha256: match[1], + journal: PERFORM_PREFIX[4], + receipt: PERFORM_PREFIX[6], + confirmation, + }, + } + } + if (snapshot.length === VERIFY_EXPECTED.length) { + for (const [index, expected] of VERIFY_EXPECTED.entries()) { + if (snapshot[index] !== expected) throw new InvocationError() + } + return { + mode: "verify", + value: { receipt: VERIFY_EXPECTED[2] }, + } + } + throw new InvocationError() +} + +function operationLabel(mode) { + if (mode === "perform") return "perform" + return mode === "verify" ? "verify" : "inspection" +} + +function snapshotArguments(argv) { + if ( + !Array.isArray(argv) || + utilTypes.isProxy(argv) || + Object.getOwnPropertySymbols(argv).length !== 0 || + Object.getOwnPropertyNames(argv).length !== argv.length + 1 + ) + throw new InvocationError() + const output = [] + for (let index = 0; index < argv.length; index += 1) { + const descriptor = Object.getOwnPropertyDescriptor(argv, String(index)) + if (descriptor?.enumerable !== true || !("value" in descriptor)) throw new InvocationError() + const value = descriptor.value + if (typeof value !== "string") throw new InvocationError() + output.push(value) + } + return output +} + +function normalizeOptions(options) { + const values = snapshotDataOptions(options, [ + "argv", + "cwd", + "environment", + "stdout", + "stderr", + "dependencies", + ]) + const cwd = values.cwd ?? process.cwd() + if (typeof cwd !== "string" || !path.isAbsolute(cwd) || path.normalize(cwd) !== cwd) + throw new InvocationError() + const dependencies = snapshotDataOptions(values.dependencies ?? {}, [ + "createAdapters", + "inspect", + "perform", + "verify", + "now", + "wait", + ]) + const stdout = bindSink(values.stdout ?? process.stdout) + const stderr = bindSink(values.stderr ?? process.stderr) + const result = { + argv: values.argv ?? process.argv.slice(2), + cwd, + environment: values.environment ?? process.env, + stdout, + stderr, + dependencies, + } + try { + const parsed = parseArguments(result.argv) + result.mode = parsed.mode + } catch { + result.mode = null + } + return result +} + +function bindSink(value) { + if (value === null || typeof value !== "object" || utilTypes.isProxy(value)) { + throw new InvocationError() + } + let owner = value + let descriptor + while (owner !== null) { + descriptor = Object.getOwnPropertyDescriptor(owner, "write") + if (descriptor !== undefined) break + owner = Object.getPrototypeOf(owner) + } + if ( + descriptor === undefined || + !("value" in descriptor) || + typeof descriptor.value !== "function" || + utilTypes.isProxy(descriptor.value) + ) { + throw new InvocationError() + } + const write = descriptor.value + const isNodeWritable = value instanceof Writable && write === Writable.prototype.write + return Object.freeze({ + write(chunk) { + if (isNodeWritable) return writeNodeWritable(value, write, chunk) + return Reflect.apply(write, value, [chunk]) + }, + }) +} + +function writeNodeWritable(stream, write, chunk) { + return new Promise((resolve, reject) => { + let settled = false + let scheduled = false + let succeeded = false + const cleanup = () => { + Reflect.apply(EventEmitter.prototype.removeListener, stream, ["error", onError]) + clearTimer(deadline) + } + const finish = () => { + if (settled) return + settled = true + cleanup() + if (succeeded) resolve() + else reject(new Error("Output sink failed")) + } + const schedule = (success) => { + if (!success) succeeded = false + else if (!scheduled) succeeded = true + if (scheduled) return + scheduled = true + scheduleImmediate(finish) + } + const onError = () => schedule(false) + Reflect.apply(EventEmitter.prototype.on, stream, ["error", onError]) + const deadline = startTimer(() => schedule(false), 5_000) + deadline.unref?.() + try { + const result = Reflect.apply(write, stream, [ + chunk, + (error) => { + schedule(error === undefined || error === null) + }, + ]) + if (result !== null && (typeof result === "object" || typeof result === "function")) { + Promise.resolve(result).catch(onError) + } + } catch { + schedule(false) + } + }) +} + +function snapshotDataOptions(value, allowed) { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + utilTypes.isProxy(value) || + ![Object.prototype, null].includes(Object.getPrototypeOf(value)) || + Object.getOwnPropertySymbols(value).length !== 0 + ) + throw new InvocationError() + const names = Object.getOwnPropertyNames(value) + if (names.some((name) => !allowed.includes(name))) throw new InvocationError() + const output = {} + for (const name of names) { + const descriptor = Object.getOwnPropertyDescriptor(value, name) + if ( + descriptor?.enumerable !== true || + !("value" in descriptor) || + descriptor.get !== undefined || + descriptor.set !== undefined + ) + throw new InvocationError() + output[name] = descriptor.value + } + return output +} + +function safeSummary(value, input) { + if (value === null || typeof value !== "object" || !/^[0-9a-f]{64}$/u.test(value.proposalSha256)) + throw new Error("Inspection summary is invalid") + for (const field of ["version", "commitSha", "survivor", "output"]) { + if (value[field] !== input[field]) throw new Error("Inspection summary identity is invalid") + } + if ( + !Array.isArray(value.duplicates) || + value.duplicates.length !== 2 || + value.duplicates.some((id, index) => id !== input.duplicates[index]) + ) { + throw new Error("Inspection summary duplicate identity is invalid") + } + return { + proposalSha256: value.proposalSha256, + version: input.version, + commitSha: input.commitSha, + survivor: input.survivor, + duplicates: [...input.duplicates], + output: input.output, + } +} + +function safePerformSummary(value) { + if ( + value === null || + typeof value !== "object" || + value.status !== "complete" || + value.survivor !== "379991871" || + value.receipt !== "scripts/release/duplicate-draft-consolidation.json" || + !/^[0-9a-f]{64}$/u.test(value.receiptSha256) || + !Array.isArray(value.deleted) || + value.deleted.length !== 2 || + value.deleted[0] !== "379982100" || + value.deleted[1] !== "379986168" + ) { + throw new Error("Perform summary is invalid") + } + return { + status: value.status, + survivor: value.survivor, + deleted: [...value.deleted], + receipt: value.receipt, + receiptSha256: value.receiptSha256, + } +} + +function safeVerifySummary(value) { + const historicalParity = + "Historical duplicate payload parity is supported by embedded pre-delete evidence plus the currently reverified survivor; deleted bytes were not independently re-downloaded." + if ( + value === null || + typeof value !== "object" || + value.status !== "verified" || + value.survivor !== "379991871" || + value.receipt !== "scripts/release/duplicate-draft-consolidation.json" || + value.historicalParity !== historicalParity || + !/^[0-9a-f]{64}$/u.test(value.receiptSha256) || + !Array.isArray(value.deleted) || + value.deleted.length !== 2 || + value.deleted[0] !== "379982100" || + value.deleted[1] !== "379986168" + ) { + throw new Error("Verify summary is invalid") + } + return { + status: value.status, + survivor: value.survivor, + deleted: [...value.deleted], + receipt: value.receipt, + receiptSha256: value.receiptSha256, + historicalParity, + } +} + +class InvocationError extends Error {} + +if ( + process.argv[1] !== undefined && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + process.exitCode = await runDuplicateDraftConsolidationCli() +} diff --git a/scripts/release/duplicate-draft-consolidation-evidence.mjs b/scripts/release/duplicate-draft-consolidation-evidence.mjs new file mode 100644 index 000000000..e6d130c44 --- /dev/null +++ b/scripts/release/duplicate-draft-consolidation-evidence.mjs @@ -0,0 +1,1156 @@ +import { createHash } from "node:crypto" +import { isDeepStrictEqual, types as utilTypes } from "node:util" + +import { DUPLICATE_DRAFT_CONSOLIDATION_LIMITS } from "./duplicate-draft-consolidation-schema.mjs" +import { RELEASE_PAYLOAD_LIMITS } from "./limits.mjs" +import { canonicalManifestBytes, parseSealedReleaseManifest } from "./manifest.mjs" +import { + canonicalBaseAssetSet, + canonicalReleaseBody, + parseAttestationSet, + parseReleaseMarker, + verifyReleaseAttestationAnchor, +} from "./metadata.mjs" +import { + canonicalReleaseRecordBytes, + parseReleaseRecord, + releaseRecordSha256, +} from "./release-record.mjs" + +const APPROVED_VERSION = "0.8.22" +const APPROVED_COMMIT_SHA = "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8" +const APPROVED_TAG = "v0.8.22" +const APPROVED_SURVIVOR_ID = "379991871" +const APPROVED_DUPLICATE_IDS = Object.freeze(["379982100", "379986168"]) +const APPROVED_AUTHOR = Object.freeze({ + login: "blove", + id: "61436", + nodeId: "MDQ6VXNlcjYxNDM2", +}) +const RELEASE_EVIDENCE_FIELDS = Object.freeze([ + "role", + "id", + "nodeId", + "tagName", + "createdAt", + "updatedAt", + "semantic", + "assets", +]) +const RELEASE_SEMANTIC_FIELDS = Object.freeze([ + "name", + "targetCommitish", + "draft", + "immutable", + "prerelease", + "publishedAt", + "body", + "bodySha256", + "author", +]) +const SERVICE_IDENTITY_FIELDS = Object.freeze(["login", "id", "nodeId"]) +const ASSET_EVIDENCE_FIELDS = Object.freeze([ + "id", + "nodeId", + "name", + "label", + "state", + "contentType", + "size", + "digest", + "uploader", + "createdAt", + "updatedAt", + "downloadCount", + "downloadSha256", +]) +const SHA256_PATTERN = /^[0-9a-f]{64}$/u +const DIGEST_PATTERN = /^sha256:([0-9a-f]{64})$/u +const ID_PATTERN = /^[1-9][0-9]*$/u +const TIMESTAMP_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/u +const GITHUB_TIMESTAMP_PATTERN = + /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{3})?Z$/u +const AGGREGATE_ESCROW_BYTES = 3 * RELEASE_PAYLOAD_LIMITS.escrowBytes + +export async function inspectEquivalentDrafts(input) { + const context = snapshotInspectionInput(input) + return inspectManagedDraftSet(context, [context.survivorId, ...context.duplicateIds]) +} + +export async function inspectEquivalentRemainingDrafts(input) { + const { stage, ...context } = snapshotRemainingInspectionInput(input) + const orderedIds = + stage === "pre-delete-1" + ? [context.survivorId, ...context.duplicateIds] + : [context.survivorId, context.duplicateIds[1]] + return inspectManagedDraftSet(context, orderedIds) +} + +export async function inspectFinalSurvivor(input) { + const context = snapshotInspectionInput(input) + return inspectManagedDraftSet(context, [context.survivorId]) +} + +async function inspectManagedDraftSet(context, orderedIds) { + const managed = candidateReleases(context.releases, context.candidate) + if (managed.published.length !== 0) { + throw new Error("A published Release already matches the candidate") + } + if (managed.drafts.length !== orderedIds.length) { + throw new Error(`Exactly ${orderedIds.length} managed candidate drafts are required`) + } + + const byId = new Map( + managed.drafts.map((entry) => [canonicalId(entry.release.id, "Release id"), entry]), + ) + if (byId.size !== orderedIds.length || orderedIds.some((id) => !byId.has(id))) { + throw new Error("Managed Release roles do not match the approved exact IDs and order") + } + const selected = orderedIds.map((id, index) => ({ + ...byId.get(id), + role: index === 0 ? "survivor" : "duplicate", + })) + preflightDownloads( + selected.map(({ release, marker }) => ({ + release, + expectedNames: markerAssetNames(marker), + })), + context.accounting, + ) + + const counter = { + downloads: context.accounting.downloadedAssets, + bytes: context.accounting.downloadedBytes, + } + const releases = [] + const hydration = [] + for (const selectedRelease of selected) { + const result = await hydrateRelease({ + ...selectedRelease, + candidate: context.candidate, + github: context.github, + attestations: context.attestations, + counter, + }) + releases.push(result.evidence) + hydration.push(result) + } + assertManagedParity(releases) + for (let index = 1; index < hydration.length; index += 1) { + if ( + hydration[index].baseAssetSetSha256 !== hydration[0].baseAssetSetSha256 || + !isDeepStrictEqual(hydration[index].baseAssetSet, hydration[0].baseAssetSet) || + !isDeepStrictEqual(hydration[index].verifiedSubjects, hydration[0].verifiedSubjects) + ) { + throw new Error("Candidate draft production payload proofs are not equal") + } + } + if ( + counter.downloads !== context.accounting.downloadedAssets + orderedIds.length * 45 || + counter.downloads > DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumAssetDownloads || + counter.bytes > AGGREGATE_ESCROW_BYTES + ) { + throw new Error("Duplicate draft download aggregate exceeded its exact bound") + } + + const payloadProjection = releases.map((release) => ({ + release: semanticReleaseProjection(release), + assets: release.assets.map(semanticAssetProjection), + })) + return deepFreeze({ + releases, + payloadProof: { + baseAssetSet: hydration[0].baseAssetSet, + baseAssetSetSha256: hydration[0].baseAssetSetSha256, + consolidationPayloadSha256: canonicalSha256(payloadProjection), + attestationVerification: { + status: "VERIFIED", + subjects: hydration[0].verifiedSubjects, + }, + }, + }) +} + +export async function captureDirectTargetRead(input) { + const value = snapshotPlain(input, "direct target read input", { + allowFunctions: true, + }) + assertExactKeys( + value, + [ + "candidate", + "releaseId", + "role", + "expectedEvidence", + "github", + ...(Object.hasOwn(value, "now") ? ["now"] : []), + ], + "direct target read input", + ) + const candidate = parseCandidate(value.candidate) + const releaseId = canonicalId(value.releaseId, "Direct target Release id") + const role = value.role + if (role !== "survivor" && role !== "duplicate") { + throw new TypeError("Direct target Release role is invalid") + } + const expectedEvidence = parseReleaseEvidence(value.expectedEvidence) + if (expectedEvidence.id !== releaseId || expectedEvidence.role !== role) { + throw new Error("Direct target expected evidence identity is invalid") + } + const github = bindBoundary( + value.github, + ["getRelease", "listReleaseAssets"], + "GitHub direct target reader", + ) + const now = value.now === undefined ? () => new Date().toISOString() : value.now + if (typeof now !== "function") throw new TypeError("Direct target clock is invalid") + + const releaseGetStartedAt = canonicalTimestamp(now(), "Release GET start") + const releaseEnvelope = await github.getRelease({ releaseId }) + const releaseGetCompletedAt = canonicalTimestamp(now(), "Release GET completion") + assertMonotone(releaseGetStartedAt, releaseGetCompletedAt, "Release GET") + const release = exactPresentValue(releaseEnvelope, "release") + if (canonicalId(release.id, "Direct target Release id") !== releaseId) { + throw new Error("Direct Release-by-ID read returned the wrong identity") + } + const assetsListStartedAt = canonicalTimestamp(now(), "Asset list start") + assertMonotone(releaseGetCompletedAt, assetsListStartedAt, "Direct target read") + const assetsEnvelope = await github.listReleaseAssets({ releaseId }) + const assetsListCompletedAt = canonicalTimestamp(now(), "Asset list completion") + assertMonotone(assetsListStartedAt, assetsListCompletedAt, "Asset list") + const assets = exactPresentValue(assetsEnvelope, "release-assets") + if (!Array.isArray(assets)) throw new TypeError("Direct target asset enumeration is invalid") + const directRelease = { ...snapshotPlain(release, "direct Release"), assets } + preflightDownloads([ + { + release: directRelease, + expectedNames: expectedEvidence.assets.map(({ name }) => name), + }, + ]) + const directEvidence = buildDirectEvidence({ + release: directRelease, + role, + candidate, + expectedEvidence, + }) + const evidence = assertEvidenceEqualsProposal(directEvidence, expectedEvidence) + return deepFreeze({ + releaseGetStartedAt, + releaseGetCompletedAt, + assetsListStartedAt, + assetsListCompletedAt, + evidence, + evidenceSha256: canonicalSha256(evidence), + }) +} + +export function parseReleaseEvidence(value) { + value = snapshotPlain(value, "Release evidence") + assertExactKeys(value, RELEASE_EVIDENCE_FIELDS, "Release evidence") + if (value.role !== "survivor" && value.role !== "duplicate") { + throw new TypeError("Release evidence role is invalid") + } + if (!Array.isArray(value.assets) || value.assets.length !== 45) { + throw new TypeError("Release evidence must contain exactly 45 assets") + } + const assets = value.assets.map(parseAssetEvidence) + if ( + new Set(assets.map(({ id }) => id)).size !== 45 || + new Set(assets.map(({ name }) => name)).size !== 45 + ) { + throw new TypeError("Release evidence asset identities must be unique") + } + const aggregateSize = assets.reduce( + (total, { size }) => checkedAdd(total, size, "Release evidence payload"), + 0, + ) + if (aggregateSize > RELEASE_PAYLOAD_LIMITS.escrowBytes) { + throw new TypeError("Release evidence exceeds the escrow payload limit") + } + return deepFreeze({ + role: value.role, + id: evidenceId(value.id, "Release id"), + nodeId: nonemptyString(value.nodeId, "Release node id"), + tagName: nonemptyString(value.tagName, "Release tag name"), + createdAt: canonicalTimestamp(value.createdAt, "Release creation timestamp"), + updatedAt: canonicalTimestamp(value.updatedAt, "Release update timestamp"), + semantic: parseReleaseSemantic(value.semantic), + assets, + }) +} + +export function semanticReleaseProjection(value) { + const evidence = parseReleaseEvidence(value) + return deepFreeze(snapshotPlain(evidence.semantic, "Release semantic projection")) +} + +export function semanticAssetProjection(value) { + const asset = parseAssetEvidence(snapshotPlain(value, "Asset evidence")) + return deepFreeze({ + name: asset.name, + label: asset.label, + state: asset.state, + contentType: asset.contentType, + size: asset.size, + digest: asset.digest, + uploader: asset.uploader, + downloadSha256: asset.downloadSha256, + }) +} + +export function assertEvidenceEqualsProposal(actual, proposed) { + const normalizedActual = parseReleaseEvidence(actual) + const normalizedProposed = parseReleaseEvidence(proposed) + if ( + !isDeepStrictEqual( + semanticReleaseProjection(normalizedActual), + semanticReleaseProjection(normalizedProposed), + ) + ) { + throw new Error("Direct Release semantic evidence does not equal the proposal") + } + const proposedAssets = new Map( + normalizedProposed.assets.map((asset) => [asset.name, semanticAssetProjection(asset)]), + ) + if ( + proposedAssets.size !== normalizedActual.assets.length || + normalizedActual.assets.some( + (asset) => !isDeepStrictEqual(semanticAssetProjection(asset), proposedAssets.get(asset.name)), + ) + ) { + throw new Error("Direct Release asset evidence does not equal the proposal") + } + return normalizedActual +} + +function buildDirectEvidence({ release, role, candidate, expectedEvidence }) { + validateRawReleasePolicy(release, candidate) + parseCandidateMarker(release.body, candidate, "Direct target Release") + const expectedByName = new Map(expectedEvidence.assets.map((asset) => [asset.name, asset])) + const latestByName = new Map() + for (const rawAsset of release.assets) { + const descriptor = parseRawAsset(rawAsset) + const expected = expectedByName.get(descriptor.name) + if (expected === undefined) { + throw new Error("Direct target asset list contains an unknown asset") + } + if (descriptor.digest !== `sha256:${expected.downloadSha256}`) { + throw new Error("Direct target asset digest does not match the proven downloaded payload") + } + latestByName.set(descriptor.name, { + ...descriptor, + downloadSha256: expected.downloadSha256, + }) + } + if ( + latestByName.size !== expectedByName.size || + [...expectedByName.keys()].some((name) => !latestByName.has(name)) + ) { + throw new Error("Direct target asset list is incomplete") + } + return parseReleaseEvidence({ + role, + id: canonicalId(release.id, "Direct target Release id"), + nodeId: nonemptyString(release.node_id, "Direct target Release node id"), + tagName: nonemptyString(release.tag_name, "Direct target Release tag name"), + createdAt: githubTimestamp(release.created_at, "Direct target Release creation timestamp"), + updatedAt: githubTimestamp(release.updated_at, "Direct target Release update timestamp"), + semantic: { + name: nonemptyString(release.name, "Direct target Release name"), + targetCommitish: nonemptyString( + release.target_commitish, + "Direct target Release target commitish", + ), + draft: release.draft, + immutable: release.immutable, + prerelease: release.prerelease, + publishedAt: release.published_at, + body: release.body, + bodySha256: sha256(Buffer.from(release.body, "utf8")), + author: parseRawIdentity(release.author, "Direct target Release author"), + }, + assets: expectedEvidence.assets.map(({ name }) => latestByName.get(name)), + }) +} + +function snapshotInspectionInput(input) { + const value = snapshotPlain(input, "duplicate draft evidence input", { + allowFunctions: true, + }) + assertExactKeys( + value, + [ + "candidate", + "survivorId", + "duplicateIds", + "releases", + "github", + "attestations", + ...(Object.hasOwn(value, "accounting") ? ["accounting"] : []), + ], + "duplicate draft evidence input", + ) + const candidate = parseCandidate(value.candidate) + const survivorId = canonicalId(value.survivorId, "Survivor Release id") + if (survivorId !== APPROVED_SURVIVOR_ID) { + throw new Error("Survivor Release id is not approved") + } + if ( + !Array.isArray(value.duplicateIds) || + value.duplicateIds.length !== 2 || + !value.duplicateIds.every((id, index) => id === APPROVED_DUPLICATE_IDS[index]) + ) { + throw new Error("Duplicate Release roles are not in approved order") + } + if (!Array.isArray(value.releases)) throw new TypeError("Release enumeration is invalid") + const accounting = normalizeAccounting(value.accounting) + return { + candidate, + survivorId, + duplicateIds: [...APPROVED_DUPLICATE_IDS], + releases: value.releases, + github: bindBoundary(value.github, ["downloadReleaseAsset"], "GitHub asset reader"), + attestations: bindBoundary(value.attestations, ["verify"], "Attestation verifier"), + accounting, + } +} + +function snapshotRemainingInspectionInput(input) { + const value = snapshotPlain(input, "remaining duplicate draft evidence input", { + allowFunctions: true, + }) + assertExactKeys( + value, + [ + "stage", + "candidate", + "survivorId", + "duplicateIds", + "releases", + "github", + "attestations", + ...(Object.hasOwn(value, "accounting") ? ["accounting"] : []), + ], + "remaining duplicate draft evidence input", + ) + if (value.stage !== "pre-delete-1" && value.stage !== "pre-delete-2") { + throw new TypeError("Remaining duplicate draft evidence stage is invalid") + } + return { + stage: value.stage, + ...snapshotInspectionInput({ + candidate: value.candidate, + survivorId: value.survivorId, + duplicateIds: value.duplicateIds, + releases: value.releases, + github: value.github, + attestations: value.attestations, + ...(Object.hasOwn(value, "accounting") ? { accounting: value.accounting } : {}), + }), + } +} + +function normalizeAccounting(value) { + if (value === undefined) { + return Object.freeze({ downloadedAssets: 0, downloadedBytes: 0 }) + } + assertExactKeys(value, ["downloadedAssets", "downloadedBytes"], "duplicate draft accounting") + const downloadedAssets = nonnegativeInteger(value.downloadedAssets, "Downloaded asset accounting") + const downloadedBytes = nonnegativeInteger(value.downloadedBytes, "Downloaded byte accounting") + if ( + downloadedAssets > DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumAssetDownloads || + downloadedBytes > AGGREGATE_ESCROW_BYTES + ) { + throw new Error("Duplicate draft accounting already exceeds its aggregate limit") + } + return Object.freeze({ downloadedAssets, downloadedBytes }) +} + +function parseCandidate(value) { + value = snapshotPlain(value, "candidate identity") + assertExactKeys(value, ["version", "commitSha", "tag"], "candidate identity") + if ( + value.version !== APPROVED_VERSION || + value.commitSha !== APPROVED_COMMIT_SHA || + value.tag !== APPROVED_TAG + ) { + throw new Error("Candidate identity is not the approved v0.8.22 candidate") + } + return deepFreeze(value) +} + +function candidateReleases(releases, candidate) { + const drafts = [] + const published = [] + const releaseIds = new Set() + for (const [index, source] of releases.entries()) { + const release = snapshotPlain(source, `GitHub Release ${index}`) + const id = canonicalId(release.id, `GitHub Release ${index} id`) + if (releaseIds.has(id)) throw new Error("GitHub Release enumeration contains duplicate IDs") + releaseIds.add(id) + let marker = null + try { + marker = parseReleaseMarker(release.body) + } catch { + marker = null + } + const markerMatches = + marker !== null && + marker.version === candidate.version && + marker.commitSha === candidate.commitSha && + marker.tag === candidate.tag + const exactTagMatches = release.tag_name === candidate.tag + if (!markerMatches && !exactTagMatches) { + if ([APPROVED_SURVIVOR_ID, ...APPROVED_DUPLICATE_IDS].includes(id) && marker === null) { + throw new Error("Approved managed candidate draft has a malformed Release marker") + } + continue + } + if (release.draft !== true || release.immutable !== false || release.published_at !== null) { + published.push(release) + continue + } + if (marker === null) { + throw new Error("Managed candidate draft has a malformed Release marker") + } + drafts.push({ release, marker }) + } + return { drafts, published } +} + +function markerAssetNames(marker) { + return [ + "release-record.json", + ...marker.attestationSet.subjects.map(({ subjectName }) => subjectName), + ...marker.attestationSet.subjects.map(({ bundleName }) => bundleName), + ] +} + +function assetCategory(name) { + if (name === "release-record.json") { + return { + kind: "record", + label: "Release record", + maximumBytes: RELEASE_PAYLOAD_LIMITS.releaseRecordBytes, + } + } + if (name === "manifest.json") { + return { + kind: "manifest", + label: "Release manifest", + maximumBytes: RELEASE_PAYLOAD_LIMITS.manifestBytes, + } + } + if (name.endsWith(".intoto.jsonl")) { + return { + kind: "bundle", + label: "Attestation bundle", + maximumBytes: RELEASE_PAYLOAD_LIMITS.attestationBundleBytes, + } + } + if (name.endsWith(".tgz")) { + return { + kind: "package", + label: "Release package tarball", + maximumBytes: RELEASE_PAYLOAD_LIMITS.tarballBytes, + } + } + throw new Error("Release asset is outside the canonical namespace") +} + +function preflightDownloads(entries, accounting = { downloadedAssets: 0, downloadedBytes: 0 }) { + let aggregate = accounting.downloadedBytes + let downloads = accounting.downloadedAssets + for (const [releaseIndex, entry] of entries.entries()) { + const { release, expectedNames } = entry + if (!Array.isArray(release.assets) || release.assets.length !== 45) { + throw new Error(`Release ${releaseIndex} must expose exactly 45 assets`) + } + if ( + !Array.isArray(expectedNames) || + expectedNames.length !== 45 || + new Set(expectedNames).size !== 45 + ) { + throw new Error("Release canonical asset namespace is invalid") + } + const expected = new Set(expectedNames) + let releaseBytes = 0 + let preparedBytes = 0 + let bundleBytes = 0 + const ids = new Set() + const names = new Set() + for (const asset of release.assets) { + const id = canonicalId(asset.id, "Release asset id") + const name = nonemptyString(asset.name, "Release asset name") + if (ids.has(id) || names.has(name)) { + throw new Error("Release asset enumeration contains a duplicate identity") + } + ids.add(id) + names.add(name) + if (!expected.has(name)) { + throw new Error("Release asset is outside the canonical namespace") + } + const size = nonnegativeInteger(asset.size, "Release asset size") + const category = assetCategory(name) + if (size > category.maximumBytes) { + throw new Error(`${category.label} exceeds its namespace payload limit`) + } + if (category.kind === "package") { + preparedBytes = checkedAdd(preparedBytes, size, "Prepared package payload") + } + if (category.kind === "bundle") { + bundleBytes = checkedAdd(bundleBytes, size, "Attestation bundle payload") + } + releaseBytes = checkedAdd(releaseBytes, size, "Release escrow payload") + downloads += 1 + } + if (names.size !== expected.size || expectedNames.some((name) => !names.has(name))) { + throw new Error("Release asset namespace is incomplete") + } + if (preparedBytes > RELEASE_PAYLOAD_LIMITS.preparedTarballsBytes) { + throw new Error("Prepared package payload exceeds its aggregate limit") + } + if (bundleBytes > RELEASE_PAYLOAD_LIMITS.attestationBundlesBytes) { + throw new Error("Attestation bundle payload exceeds its aggregate limit") + } + if (releaseBytes > RELEASE_PAYLOAD_LIMITS.escrowBytes) { + throw new Error("Release asset evidence exceeds the escrow payload limit") + } + aggregate = checkedAdd(aggregate, releaseBytes, "Aggregate Release escrow payload") + } + if ( + aggregate > AGGREGATE_ESCROW_BYTES || + downloads > DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumAssetDownloads + ) { + throw new Error("Duplicate draft aggregate payload or download limit was exceeded") + } +} + +async function hydrateRelease({ release, marker, role, candidate, github, attestations, counter }) { + validateRawReleasePolicy(release, candidate) + const parsedMarker = marker ?? parseCandidateMarker(release.body, candidate, "Managed Release") + const expectedNames = markerAssetNames(parsedMarker) + const observedNames = new Set(release.assets.map(({ name }) => name)) + if ( + release.assets.length !== expectedNames.length || + observedNames.size !== expectedNames.length || + expectedNames.some((name) => !observedNames.has(name)) + ) { + throw new Error("Release assets do not match the exact canonical 45-name set") + } + const assetsByName = new Map() + const downloaded = new Map() + for (const rawAsset of release.assets) { + const descriptor = parseRawAsset(rawAsset) + counter.downloads += 1 + if (counter.downloads > DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumAssetDownloads) { + throw new Error("Release asset download count exceeds 135") + } + const envelope = await github.downloadReleaseAsset({ + releaseId: canonicalId(release.id, "Release id"), + assetId: descriptor.id, + maximumBytes: descriptor.size, + }) + const bytes = exactDownloadBytes(envelope, descriptor.size) + if (bytes.byteLength !== descriptor.size) { + throw new Error("Downloaded asset bytes conflict with declared size") + } + counter.bytes = checkedAdd(counter.bytes, bytes.byteLength, "Downloaded Release payload") + if (counter.bytes > AGGREGATE_ESCROW_BYTES) { + throw new Error("Downloaded Release payload exceeds 192 MiB") + } + const downloadSha256 = sha256(bytes) + if (descriptor.digest !== `sha256:${downloadSha256}`) { + throw new Error("GitHub asset digest does not match downloaded bytes") + } + downloaded.set(descriptor.name, bytes) + assetsByName.set(descriptor.name, { ...descriptor, downloadSha256 }) + } + + const recordBytes = downloaded.get("release-record.json") + const record = parseReleaseRecord(recordBytes) + if (!Buffer.from(recordBytes).equals(canonicalReleaseRecordBytes(record))) { + throw new Error("Release record bytes are not canonical") + } + if (record.version !== candidate.version || record.commitSha !== candidate.commitSha) { + throw new Error("Release record does not bind the approved candidate") + } + if (parsedMarker.releaseRecordSha256 !== releaseRecordSha256(record)) { + throw new Error("Release marker record digest does not bind the canonical release record") + } + const manifestBytes = downloaded.get("manifest.json") + const manifest = parseSealedReleaseManifest(manifestBytes, { candidate }) + if (!Buffer.from(manifestBytes).equals(canonicalManifestBytes(manifest))) { + throw new Error("Sealed Release manifest bytes are not canonical") + } + const productionCandidate = { + version: candidate.version, + commitSha: candidate.commitSha, + } + const attestationSet = parseAttestationSet(parsedMarker.attestationSet, { + candidate: productionCandidate, + manifest, + repository: "cacheplane/dawnai", + }) + const subjectFiles = attestationSet.subjects.map(({ subjectName }) => ({ + name: subjectName, + bytes: Buffer.from(downloaded.get(subjectName)), + })) + const bundles = attestationSet.subjects.map(({ bundleName }) => ({ + name: bundleName, + bytes: Buffer.from(downloaded.get(bundleName)), + })) + const artifact = { manifest, files: subjectFiles } + const base = canonicalBaseAssetSet({ + record, + artifact, + attestationSet, + bundles, + }) + if (base.sha256 !== parsedMarker.baseAssetSetSha256) { + throw new Error("Canonical base asset set digest does not match the Release marker") + } + const anchored = await verifyReleaseAttestationAnchor({ + candidate: productionCandidate, + record, + artifact, + bundleBytes: bundles[0].bytes, + attestations, + }) + if ( + anchored.baseAssetSetSha256 !== base.sha256 || + !isDeepStrictEqual(anchored.attestationSet, attestationSet) + ) { + throw new Error("Verified attestation anchor does not match the canonical escrow") + } + if (release.body !== canonicalReleaseBody({ marker: parsedMarker, manifest })) { + throw new Error("Managed Release body bytes are not canonical") + } + const assets = base.assets.map(({ name }) => assetsByName.get(name)) + if (assets.some((asset) => asset === undefined)) { + throw new Error("Canonical base asset evidence is incomplete") + } + const semantic = { + name: nonemptyString(release.name, "Release name"), + targetCommitish: nonemptyString(release.target_commitish, "Release target commitish"), + draft: release.draft, + immutable: release.immutable, + prerelease: release.prerelease, + publishedAt: release.published_at, + body: release.body, + bodySha256: sha256(Buffer.from(release.body, "utf8")), + author: parseRawIdentity(release.author, "Release author"), + } + const evidence = parseReleaseEvidence({ + role, + id: canonicalId(release.id, "Release id"), + nodeId: nonemptyString(release.node_id, "Release node id"), + tagName: nonemptyString(release.tag_name, "Release tag name"), + createdAt: githubTimestamp(release.created_at, "Release creation timestamp"), + updatedAt: githubTimestamp(release.updated_at, "Release update timestamp"), + semantic, + assets, + }) + return { + evidence, + baseAssetSet: base.assets.map(({ name, sha256: digest }) => ({ + name, + sha256: digest, + })), + baseAssetSetSha256: base.sha256, + verifiedSubjects: attestationSet.subjects.map(({ subjectName, subjectSha256 }) => ({ + name: subjectName, + sha256: subjectSha256, + })), + } +} + +function parseCandidateMarker(body, candidate, label) { + let marker + try { + marker = parseReleaseMarker(body) + } catch (error) { + throw new Error(`${label} has a malformed or noncanonical marker`, { + cause: error, + }) + } + if ( + marker.phase !== "ESCROWED" || + marker.revision !== 2 || + marker.version !== candidate.version || + marker.commitSha !== candidate.commitSha || + marker.tag !== candidate.tag + ) { + throw new Error(`${label} marker does not bind the approved ESCROWED candidate`) + } + return marker +} + +function validateRawReleasePolicy(release, candidate) { + if ( + release.name !== `Dawn v${candidate.version}` || + release.target_commitish !== "main" || + release.draft !== true || + release.immutable !== false || + release.prerelease !== false || + release.published_at !== null + ) { + throw new Error("Managed candidate Release is not the expected mutable draft") + } + const author = parseRawIdentity(release.author, "Release author") + if (!isDeepStrictEqual(author, APPROVED_AUTHOR)) { + throw new Error("Managed candidate Release author is not approved") + } +} + +function parseRawAsset(value) { + value = snapshotPlain(value, "GitHub Release asset") + const required = [ + "id", + "node_id", + "name", + "label", + "state", + "content_type", + "size", + "digest", + "uploader", + "created_at", + "updated_at", + "download_count", + ] + for (const field of required) { + if (!Object.hasOwn(value, field)) + throw new TypeError(`GitHub Release asset is missing ${field}`) + } + if (value.state !== "uploaded") throw new Error("Release asset state is not uploaded") + const digest = nonemptyString(value.digest, "Asset service digest") + if (!DIGEST_PATTERN.test(digest)) throw new TypeError("Asset service digest is malformed") + return { + id: canonicalId(value.id, "Asset id"), + nodeId: nonemptyString(value.node_id, "Asset node id"), + name: nonemptyString(value.name, "Asset name"), + label: value.label === null ? null : stringValue(value.label, "Asset label"), + state: value.state, + contentType: nonemptyString(value.content_type, "Asset content type"), + size: nonnegativeInteger(value.size, "Asset size"), + digest, + uploader: parseRawIdentity(value.uploader, "Asset uploader"), + createdAt: githubTimestamp(value.created_at, "Asset creation timestamp"), + updatedAt: githubTimestamp(value.updated_at, "Asset update timestamp"), + downloadCount: nonnegativeInteger(value.download_count, "Asset download count"), + } +} + +function parseRawIdentity(value, label) { + value = snapshotPlain(value, label) + for (const field of ["login", "id", "node_id"]) { + if (!Object.hasOwn(value, field)) throw new TypeError(`${label} is missing ${field}`) + } + return { + login: nonemptyString(value.login, `${label} login`), + id: canonicalId(value.id, `${label} id`), + nodeId: nonemptyString(value.node_id, `${label} node id`), + } +} + +function parseReleaseSemantic(value) { + value = snapshotPlain(value, "Release semantic evidence") + assertExactKeys(value, RELEASE_SEMANTIC_FIELDS, "Release semantic evidence") + if ( + value.draft !== true || + value.immutable !== false || + value.prerelease !== false || + value.publishedAt !== null + ) { + throw new TypeError("Release semantic evidence is not a mutable draft") + } + const body = stringValue(value.body, "Release body") + const bodySha256 = sha256Value(value.bodySha256, "Release body digest") + if (sha256(Buffer.from(body, "utf8")) !== bodySha256) { + throw new TypeError("Release body digest does not match its canonical bytes") + } + return { + name: nonemptyString(value.name, "Release name"), + targetCommitish: exactString(value.targetCommitish, "main", "Release target commitish"), + draft: true, + immutable: false, + prerelease: false, + publishedAt: null, + body, + bodySha256, + author: parseServiceIdentity(value.author, "Release author"), + } +} + +function parseAssetEvidence(value) { + value = snapshotPlain(value, "Asset evidence") + assertExactKeys(value, ASSET_EVIDENCE_FIELDS, "Asset evidence") + const digest = nonemptyString(value.digest, "Asset service digest") + const match = DIGEST_PATTERN.exec(digest) + if (match === null) throw new TypeError("Asset service digest is malformed") + const downloadSha256 = sha256Value(value.downloadSha256, "Downloaded asset digest") + if (match[1] !== downloadSha256) { + throw new TypeError("Asset service digest and downloaded digest differ") + } + if (value.state !== "uploaded") throw new TypeError("Asset evidence state is not uploaded") + const size = nonnegativeInteger(value.size, "Asset size") + if (size > RELEASE_PAYLOAD_LIMITS.tarballBytes) + throw new TypeError("Asset size exceeds its limit") + return { + id: evidenceId(value.id, "Asset id"), + nodeId: nonemptyString(value.nodeId, "Asset node id"), + name: nonemptyString(value.name, "Asset name"), + label: value.label === null ? null : stringValue(value.label, "Asset label"), + state: "uploaded", + contentType: nonemptyString(value.contentType, "Asset content type"), + size, + digest, + uploader: parseServiceIdentity(value.uploader, "Asset uploader"), + createdAt: canonicalTimestamp(value.createdAt, "Asset creation timestamp"), + updatedAt: canonicalTimestamp(value.updatedAt, "Asset update timestamp"), + downloadCount: nonnegativeInteger(value.downloadCount, "Asset download count"), + downloadSha256, + } +} + +function parseServiceIdentity(value, label) { + value = snapshotPlain(value, label) + assertExactKeys(value, SERVICE_IDENTITY_FIELDS, label) + return { + login: nonemptyString(value.login, `${label} login`), + id: evidenceId(value.id, `${label} id`), + nodeId: nonemptyString(value.nodeId, `${label} node id`), + } +} + +function assertManagedParity(releases) { + const releaseProjection = semanticReleaseProjection(releases[0]) + const assetProjection = releases[0].assets.map(semanticAssetProjection) + for (const release of releases.slice(1)) { + if ( + !isDeepStrictEqual(semanticReleaseProjection(release), releaseProjection) || + !isDeepStrictEqual(release.assets.map(semanticAssetProjection), assetProjection) + ) { + throw new Error("Managed candidate Release or asset semantic parity failed") + } + } +} + +function exactPresentValue(envelope, operation) { + const value = snapshotPlain(envelope, `GitHub ${operation} observation`) + assertExactKeys( + value, + ["status", "operation", "httpStatus", "code", "value"], + `GitHub ${operation} observation`, + ) + if ( + value.status !== "PRESENT" || + value.operation !== operation || + value.httpStatus !== 200 || + value.code !== null + ) { + throw new Error(`GitHub ${operation} observation is not exact`) + } + return value.value +} + +function exactDownloadBytes(envelope, maximumBytes) { + const value = snapshotPlain(envelope, "GitHub asset download observation") + assertExactKeys( + value, + ["status", "operation", "httpStatus", "code", "contentBase64"], + "GitHub asset download observation", + ) + if ( + value.status !== "PRESENT" || + value.operation !== "release-asset-download" || + value.httpStatus !== 200 || + value.code !== null || + typeof value.contentBase64 !== "string" + ) { + throw new Error("GitHub asset download observation is not exact") + } + const maximumBase64Characters = Math.ceil(maximumBytes / 3) * 4 + if (value.contentBase64.length > maximumBase64Characters) { + throw new Error("GitHub asset download base64 exceeds its declared-size bound") + } + const bytes = Buffer.from(value.contentBase64, "base64") + if (bytes.toString("base64") !== value.contentBase64) { + throw new Error("GitHub asset download base64 is noncanonical") + } + return bytes +} + +function bindBoundary(value, methods, label) { + if (!isPlainObject(value) || utilTypes.isProxy(value)) throw new TypeError(`${label} is invalid`) + const bound = {} + for (const method of methods) { + const descriptor = Object.getOwnPropertyDescriptor(value, method) + if (!isEnumerableData(descriptor) || typeof descriptor.value !== "function") { + throw new TypeError(`${label} method ${method} is invalid`) + } + bound[method] = descriptor.value.bind(value) + } + return Object.freeze(bound) +} + +function snapshotPlain(value, label, { allowFunctions = false } = {}) { + return snapshotValue(value, label, new Set(), allowFunctions) +} + +function snapshotValue(value, label, ancestors, allowFunctions) { + if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value + if (typeof value === "function" && allowFunctions) return value + if (typeof value !== "object" || utilTypes.isProxy(value)) { + throw new TypeError(`${label} must be plain data`) + } + if (ancestors.has(value)) throw new TypeError(`${label} contains a cycle`) + const next = new Set(ancestors) + next.add(value) + if (Array.isArray(value)) { + const keys = Reflect.ownKeys(value) + const expected = Array.from({ length: value.length }, (_, index) => String(index)) + if ( + keys.length !== expected.length + 1 || + keys.at(-1) !== "length" || + expected.some((key, index) => keys[index] !== key) + ) { + throw new TypeError(`${label} must be a dense plain array`) + } + return expected.map((key) => { + const descriptor = Object.getOwnPropertyDescriptor(value, key) + if (!isEnumerableData(descriptor)) throw new TypeError(`${label} contains an accessor`) + return snapshotValue(descriptor.value, label, next, allowFunctions) + }) + } + if (!isPlainObject(value)) throw new TypeError(`${label} must be a plain object`) + const result = {} + for (const key of Reflect.ownKeys(value)) { + if (typeof key !== "string") throw new TypeError(`${label} contains a symbol field`) + const descriptor = Object.getOwnPropertyDescriptor(value, key) + if (!isEnumerableData(descriptor)) + throw new TypeError(`${label} contains an accessor or hidden field`) + result[key] = snapshotValue(descriptor.value, `${label}.${key}`, next, allowFunctions) + } + return result +} + +function isPlainObject(value) { + if (value === null || typeof value !== "object") return false + const prototype = Object.getPrototypeOf(value) + return prototype === Object.prototype || prototype === null +} + +function isEnumerableData(descriptor) { + return ( + descriptor?.enumerable === true && + "value" in descriptor && + descriptor.get === undefined && + descriptor.set === undefined + ) +} + +function assertExactKeys(value, expected, label) { + if (!isPlainObject(value) || !isDeepStrictEqual(Object.keys(value), expected)) { + throw new TypeError(`${label} has an invalid exact field schema`) + } +} + +function canonicalId(value, label) { + const normalized = Number.isSafeInteger(value) && value > 0 ? String(value) : value + if (typeof normalized !== "string" || !ID_PATTERN.test(normalized)) { + throw new TypeError(`${label} must be a positive decimal identity`) + } + return normalized +} + +function evidenceId(value, label) { + if (typeof value !== "string" || !ID_PATTERN.test(value)) { + throw new TypeError(`${label} must be a canonical positive decimal string`) + } + return value +} + +function nonnegativeInteger(value, label) { + if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${label} is invalid`) + return value +} + +function checkedAdd(left, right, label) { + const result = left + right + if (!Number.isSafeInteger(result) || result < 0) + throw new Error(`${label} exceeds safe accounting`) + return result +} + +function nonemptyString(value, label) { + if (typeof value !== "string" || value.length === 0) throw new TypeError(`${label} is invalid`) + return value +} + +function stringValue(value, label) { + if (typeof value !== "string") throw new TypeError(`${label} is invalid`) + return value +} + +function sha256Value(value, label) { + if (typeof value !== "string" || !SHA256_PATTERN.test(value)) + throw new TypeError(`${label} is invalid`) + return value +} + +function exactString(value, expected, label) { + if (value !== expected) throw new TypeError(`${label} must be ${expected}`) + return value +} + +function canonicalTimestamp(value, label) { + if ( + typeof value !== "string" || + !TIMESTAMP_PATTERN.test(value) || + new Date(value).toISOString() !== value + ) { + throw new TypeError(`${label} is not a canonical timestamp`) + } + return value +} + +function githubTimestamp(value, label) { + if (typeof value !== "string" || !GITHUB_TIMESTAMP_PATTERN.test(value)) { + throw new TypeError(`${label} is not an exact GitHub timestamp`) + } + const normalized = value.endsWith(".000Z") + ? value + : value.includes(".") + ? value + : `${value.slice(0, -1)}.000Z` + try { + if (new Date(normalized).toISOString() !== normalized) { + throw new TypeError(`${label} has an invalid GitHub calendar value`) + } + } catch { + throw new TypeError(`${label} has an invalid GitHub calendar value`) + } + return normalized +} + +function assertMonotone(left, right, label) { + if (right < left) throw new Error(`${label} timestamps are not monotone`) +} + +function canonicalSha256(value) { + return sha256(Buffer.from(`${JSON.stringify(value)}\n`, "utf8")) +} + +function sha256(bytes) { + return createHash("sha256").update(bytes).digest("hex") +} + +function deepFreeze(value) { + if (value !== null && typeof value === "object" && !Object.isFrozen(value)) { + for (const child of Object.values(value)) deepFreeze(child) + Object.freeze(value) + } + return value +} diff --git a/scripts/release/duplicate-draft-consolidation-files.mjs b/scripts/release/duplicate-draft-consolidation-files.mjs new file mode 100644 index 000000000..916bad481 --- /dev/null +++ b/scripts/release/duplicate-draft-consolidation-files.mjs @@ -0,0 +1,1252 @@ +import { AsyncLocalStorage } from "node:async_hooks" +import { createHash, randomUUID as defaultRandomUUID } from "node:crypto" +import { constants as fsConstants } from "node:fs" +import * as defaultFileSystem from "node:fs/promises" +import path from "node:path" +import { types as utilTypes } from "node:util" + +import { DUPLICATE_DRAFT_CONSOLIDATION_LIMITS } from "./duplicate-draft-consolidation-schema.mjs" + +const MAXIMUM_PATH_BYTES = 4096 +const IO_CHUNK_BYTES = 64 * 1024 +const PRIVATE_MODE = 0o600 +const TRACKED_MODE = 0o644 +const DEPENDENCY_FIELDS = Object.freeze(["effectiveUserId", "fileSystem", "randomUUID"]) +const READ_FILE_SYSTEM_METHODS = Object.freeze(["lstat", "open"]) +const WRITE_FILE_SYSTEM_METHODS = Object.freeze([...READ_FILE_SYSTEM_METHODS, "rename", "unlink"]) +const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u +const PRIVATE_READ_PROVENANCE = new WeakMap() +const PRIVATE_TRANSACTION_CONTEXT = new AsyncLocalStorage() +const JOURNAL_BASENAME = "duplicate-draft-consolidation.journal.json" +const JOURNAL_HEAD_BASENAME = "duplicate-draft-consolidation.journal.head.json" +const LOCK_RECORD_BYTES = 2048 +const LOCK_TIMESTAMP_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/u +const PROCESS_START_IDENTITY = `${process.pid}:${Math.trunc(Date.now() - process.uptime() * 1000)}` + +// Approved threat model: one operator and cooperative in-scope writers. Every +// consolidation journal/head publication must hold this module's lease. An +// arbitrary external filesystem writer is out of scope; no cross-process +// lockfile protocol can make its rename atomic with ours. Identity checks make +// such interference fail closed when observed, but are not a general CAS claim. + +const POLICIES = Object.freeze({ + private: Object.freeze({ label: "private envelope", mode: PRIVATE_MODE }), + tracked: Object.freeze({ label: "tracked receipt", mode: TRACKED_MODE }), +}) + +export async function readPrivateEnvelope(filePath, maximumBytes, dependencies) { + return readEvidence(filePath, maximumBytes, dependencies, POLICIES.private) +} + +export async function writePrivateEnvelope(filePath, bytes, dependencies, expectedCurrent) { + const target = snapshotPath(filePath) + const lockTarget = privateLockTarget(target) + if (lockTarget !== null && expectedCurrent !== undefined) { + assertActivePrivateLease(lockTarget) + return writeEvidence(target, bytes, dependencies, POLICIES.private, expectedCurrent) + } + return withPrivateWriteLock(target, dependencies, () => + writeEvidence(target, bytes, dependencies, POLICIES.private, expectedCurrent), + ) +} + +Object.defineProperty(readPrivateEnvelope, "authenticate", { + value(value, expectedPath) { + const target = snapshotPath(expectedPath) + const provenance = + value !== null && typeof value === "object" ? PRIVATE_READ_PROVENANCE.get(value) : undefined + if ( + provenance === undefined || + provenance.path !== target || + !Buffer.isBuffer(value) || + sha256(value) !== provenance.sha256 + ) { + throw new TypeError("Authenticated private read provenance or byte digest is invalid") + } + return provenance + }, + enumerable: false, + writable: false, + configurable: false, +}) + +Object.defineProperty(writePrivateEnvelope, "withExclusiveTransaction", { + value(filePath, operation, dependencies) { + const target = snapshotPath(filePath) + if (typeof operation !== "function" || utilTypes.isProxy(operation)) { + throw new TypeError("Private envelope transaction callback is invalid") + } + return withPrivateWriteLock(target, dependencies, operation) + }, + enumerable: false, + writable: false, + configurable: false, +}) + +export async function readTrackedReceipt(filePath, maximumBytes, dependencies) { + return readEvidence(filePath, maximumBytes, dependencies, POLICIES.tracked) +} + +export async function writeTrackedReceipt(filePath, bytes, dependencies) { + return writeEvidence(filePath, bytes, dependencies, POLICIES.tracked) +} + +async function readEvidence(filePath, maximumBytes, dependencies, policy) { + const target = snapshotPath(filePath) + const maximum = snapshotMaximumBytes(maximumBytes) + const runtime = snapshotDependencies(dependencies, false) + requireNoFollowSupport(false) + const parentPath = path.dirname(target) + const parentChain = await captureParentChain(runtime.operations, parentPath, policy.label) + const parentGuard = await openParentGuard( + runtime.operations, + parentPath, + parentChain, + policy.label, + false, + ) + let handle + let primaryError = null + let result = null + try { + await assertParentChainCurrent(runtime.operations, parentChain, policy.label) + handle = await openNoFollow(runtime.operations, target, fsConstants.O_RDONLY, policy.label) + const handleOperations = snapshotHandleOperations(handle, ["close", "read", "stat"]) + const before = await handleOperations.stat({ bigint: true }) + assertSourcePolicy(before, runtime.effectiveUserId, policy) + if ( + before.size < 0n || + before.size > BigInt(maximum) || + before.size > BigInt(Number.MAX_SAFE_INTEGER) + ) { + throw new Error(`${capitalize(policy.label)} exceeds its byte bound`) + } + const bytes = Buffer.allocUnsafe(Number(before.size)) + let offset = 0 + while (offset < bytes.byteLength) { + const requested = Math.min(IO_CHUNK_BYTES, bytes.byteLength - offset) + const readResult = await handleOperations.read(bytes, offset, requested, offset) + const bytesRead = resultCount(readResult, "bytesRead", requested, `${policy.label} read`) + if (bytesRead === 0) break + offset += bytesRead + if (offset > maximum) throw new Error(`${capitalize(policy.label)} exceeds its byte bound`) + } + const after = await handleOperations.stat({ bigint: true }) + const current = await runtime.operations.lstat(target, { bigint: true }) + if ( + offset !== bytes.byteLength || + !sameFileState(before, after) || + current.isSymbolicLink() || + !sameFileState(after, current) + ) { + throw new Error(`${capitalize(policy.label)} changed while it was read`) + } + assertSourcePolicy(after, runtime.effectiveUserId, policy) + assertSourcePolicy(current, runtime.effectiveUserId, policy) + await assertParentChainCurrent(runtime.operations, parentChain, policy.label) + result = Buffer.from(bytes) + if (policy === POLICIES.private) recordPrivateRead(result, target, after) + } catch (error) { + primaryError = error + } + + const closeErrors = [] + if (handle !== undefined) { + try { + await snapshotHandleOperations(handle, ["close"]).close() + } catch (error) { + closeErrors.push(error) + } + } + try { + await parentGuard.operations.close() + } catch (error) { + closeErrors.push(error) + } + throwCombined(primaryError, closeErrors, `${capitalize(policy.label)} read failed during cleanup`) + return result +} + +async function writeEvidence(filePath, inputBytes, dependencies, policy, expectedCurrent) { + const target = snapshotPath(filePath) + const bytes = snapshotWriteBytes(inputBytes, policy.label) + const expected = snapshotExpectedCurrent(expectedCurrent, policy, target) + const runtime = snapshotDependencies(dependencies, true) + requireNoFollowSupport(true) + const identifier = runtime.randomUUID() + if (typeof identifier !== "string" || !UUID_PATTERN.test(identifier)) { + throw new TypeError(`${capitalize(policy.label)} temporary identity is invalid`) + } + + const parentPath = path.dirname(target) + const parentChain = await captureParentChain(runtime.operations, parentPath, policy.label) + const parentGuard = await openParentGuard( + runtime.operations, + parentPath, + parentChain, + policy.label, + true, + ) + const temporaryPath = path.join( + parentPath, + `.${path.basename(target)}.${process.pid}.${identifier}.tmp`, + ) + let existingIdentity + let temporaryHandle + let temporaryOperations + let temporaryIdentity + let temporaryCreated = false + let renamed = false + let publishedIdentity + let primaryError = null + + try { + await assertParentChainCurrent(runtime.operations, parentChain, policy.label) + existingIdentity = await inspectExistingDestination( + runtime, + target, + policy, + parentChain, + expected, + ) + temporaryHandle = await openNoFollow( + runtime.operations, + temporaryPath, + fsConstants.O_RDWR | fsConstants.O_CREAT | fsConstants.O_EXCL, + policy.label, + policy.mode, + ) + temporaryCreated = true + temporaryOperations = snapshotHandleOperations(temporaryHandle, [ + "chmod", + "close", + "read", + "stat", + "sync", + "write", + ]) + await temporaryOperations.chmod(policy.mode) + temporaryIdentity = await temporaryOperations.stat({ bigint: true }) + assertTemporaryFile(temporaryIdentity, runtime.effectiveUserId, policy, 0) + + let offset = 0 + while (offset < bytes.byteLength) { + const requested = Math.min(IO_CHUNK_BYTES, bytes.byteLength - offset) + const writeResult = await temporaryOperations.write(bytes, offset, requested, offset) + const bytesWritten = resultCount( + writeResult, + "bytesWritten", + requested, + `${policy.label} write`, + ) + if (bytesWritten === 0) { + throw new Error(`${capitalize(policy.label)} temporary write made no progress`) + } + offset += bytesWritten + } + await temporaryOperations.sync() + temporaryIdentity = await temporaryOperations.stat({ bigint: true }) + assertTemporaryFile(temporaryIdentity, runtime.effectiveUserId, policy, bytes.byteLength) + await assertParentChainCurrent(runtime.operations, parentChain, policy.label) + await assertDestinationUnchanged(runtime, target, existingIdentity, policy) + await assertTemporaryPathCurrent( + runtime.operations, + temporaryPath, + temporaryIdentity, + runtime.effectiveUserId, + policy, + ) + await runtime.operations.rename(temporaryPath, target) + renamed = true + try { + publishedIdentity = await verifyPublishedBytes( + runtime.operations, + temporaryOperations, + target, + temporaryIdentity, + bytes, + runtime.effectiveUserId, + policy, + false, + ) + await temporaryOperations.sync() + publishedIdentity = await verifyPublishedBytes( + runtime.operations, + temporaryOperations, + target, + publishedIdentity, + bytes, + runtime.effectiveUserId, + policy, + true, + ) + await parentGuard.operations.sync() + await assertParentChainCurrent(runtime.operations, parentChain, policy.label) + await verifyPublishedBytes( + runtime.operations, + temporaryOperations, + target, + publishedIdentity, + bytes, + runtime.effectiveUserId, + policy, + true, + ) + await temporaryOperations.close() + temporaryHandle = undefined + } catch (error) { + throw new Error( + `${capitalize(policy.label)} publication or durability is ambiguous after atomic replacement`, + { cause: error }, + ) + } + } catch (error) { + primaryError = error + } + + const secondaryErrors = [] + let retainedState = null + if (temporaryHandle !== undefined) { + try { + const operations = temporaryOperations ?? snapshotHandleOperations(temporaryHandle, ["close"]) + await operations.close() + } catch (error) { + secondaryErrors.push(error) + } + } + if (temporaryCreated && !renamed) { + retainedState = await observeRetainedTemporary( + runtime.operations, + temporaryPath, + temporaryIdentity, + ) + if (primaryError !== null) { + primaryError = new Error( + retainedTemporaryMessage(policy.label, temporaryPath, retainedState), + { cause: primaryError }, + ) + } + } + try { + await parentGuard.operations.close() + } catch (error) { + secondaryErrors.push(error) + } + if (renamed && primaryError === null && secondaryErrors.length > 0) { + primaryError = new Error( + `${capitalize(policy.label)} publication completed but descriptor cleanup status is ambiguous`, + { cause: secondaryErrors.shift() }, + ) + } + + throwCombined( + primaryError, + secondaryErrors, + renamed + ? `${capitalize(policy.label)} publication or durability is ambiguous and descriptor cleanup also failed` + : `${capitalize(policy.label)} write and retained-path inspection both failed`, + ) + return Buffer.from(bytes) +} + +function snapshotDependencies(dependencies, needsWrite) { + if (dependencies === undefined) dependencies = Object.create(null) + if ( + dependencies === null || + typeof dependencies !== "object" || + utilTypes.isProxy(dependencies) || + ![Object.prototype, null].includes(Object.getPrototypeOf(dependencies)) + ) { + throw new TypeError("Consolidation file dependencies are unsafe") + } + const values = Object.create(null) + for (const key of Reflect.ownKeys(dependencies)) { + const descriptor = + typeof key === "string" ? Object.getOwnPropertyDescriptor(dependencies, key) : undefined + if ( + typeof key !== "string" || + !DEPENDENCY_FIELDS.includes(key) || + descriptor === undefined || + !descriptor.enumerable || + !("value" in descriptor) + ) { + throw new TypeError("Consolidation file dependencies contain an unsafe field") + } + values[key] = descriptor.value + } + + const fileSystem = values.fileSystem ?? defaultFileSystem + const operations = Object.create(null) + const fileSystemMethods = needsWrite ? WRITE_FILE_SYSTEM_METHODS : READ_FILE_SYSTEM_METHODS + for (const method of fileSystemMethods) { + operations[method] = dataMethod(fileSystem, method, "filesystem").bind(fileSystem) + } + const effectiveUserId = values.effectiveUserId ?? defaultEffectiveUserId + if (typeof effectiveUserId !== "function" || utilTypes.isProxy(effectiveUserId)) { + throw new TypeError("Consolidation file effective-user dependency is unsafe") + } + const currentUserId = effectiveUserId() + if (!Number.isSafeInteger(currentUserId) || currentUserId < 0) { + throw new TypeError("Consolidation file effective user is unavailable") + } + const randomUUID = values.randomUUID ?? defaultRandomUUID + if (needsWrite && (typeof randomUUID !== "function" || utilTypes.isProxy(randomUUID))) { + throw new TypeError("Consolidation file random identity dependency is unsafe") + } + return Object.freeze({ + effectiveUserId: BigInt(currentUserId), + operations: Object.freeze(operations), + randomUUID, + }) +} + +function defaultEffectiveUserId() { + if (typeof process.geteuid !== "function") { + throw new TypeError("Consolidation file effective user is unavailable") + } + return process.geteuid() +} + +function snapshotHandleOperations(handle, names) { + if (handle === null || (typeof handle !== "object" && typeof handle !== "function")) { + throw new TypeError("Consolidation file descriptor is unsafe") + } + const operations = Object.create(null) + for (const name of names) operations[name] = dataMethod(handle, name, "descriptor").bind(handle) + return Object.freeze(operations) +} + +function dataMethod(object, name, label) { + if ( + object === null || + (typeof object !== "object" && typeof object !== "function") || + utilTypes.isProxy(object) + ) { + throw new TypeError(`Consolidation file ${label} is unsafe`) + } + let current = object + while (current !== null) { + if (utilTypes.isProxy(current)) throw new TypeError(`Consolidation file ${label} is unsafe`) + const descriptor = Object.getOwnPropertyDescriptor(current, name) + if (descriptor !== undefined) { + if (!("value" in descriptor) || typeof descriptor.value !== "function") { + throw new TypeError(`Consolidation file ${label} must expose ${name}`) + } + return descriptor.value + } + current = Object.getPrototypeOf(current) + } + throw new TypeError(`Consolidation file ${label} must expose ${name}`) +} + +function snapshotPath(value) { + if ( + typeof value !== "string" || + value.length === 0 || + value.includes("\0") || + hasControlCharacters(value) || + Buffer.byteLength(value, "utf8") > MAXIMUM_PATH_BYTES || + !path.isAbsolute(value) || + path.resolve(value) !== value || + value === path.parse(value).root + ) { + throw new TypeError("Consolidation evidence file path is invalid") + } + return value +} + +function snapshotMaximumBytes(value) { + if (!Number.isSafeInteger(value) || value < 0) { + throw new TypeError("Consolidation evidence byte bound is invalid") + } + return value +} + +function snapshotWriteBytes(value, label) { + if ( + !(value instanceof Uint8Array) || + utilTypes.isProxy(value) || + (!Buffer.isBuffer(value) && Object.getPrototypeOf(value) !== Uint8Array.prototype) || + (typeof SharedArrayBuffer === "function" && value.buffer instanceof SharedArrayBuffer) + ) { + throw new TypeError(`${capitalize(label)} bytes must be one owned byte array`) + } + if (value.byteLength > DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes) { + throw new Error(`${capitalize(label)} exceeds its write byte bound`) + } + try { + return Buffer.from(value) + } catch { + throw new TypeError(`${capitalize(label)} bytes could not be safely copied`) + } +} + +function snapshotExpectedCurrent(value, policy, target) { + if (value === undefined) return null + if (policy !== POLICIES.private) { + throw new TypeError("Only private envelopes support authenticated replacement") + } + if (value === null) return Object.freeze({ absent: true }) + const provenance = + value !== null && typeof value === "object" ? PRIVATE_READ_PROVENANCE.get(value) : undefined + if ( + provenance === undefined || + provenance.path !== target || + !Buffer.isBuffer(value) || + sha256(value) !== provenance.sha256 + ) { + throw new TypeError( + "Authenticated private replacement requires the exact no-follow read result", + ) + } + return Object.freeze({ + bytes: Buffer.from(value), + identity: provenance.identity, + }) +} + +function recordPrivateRead(bytes, target, status) { + const provenance = Object.freeze({ + path: target, + identity: fileIdentity(status), + sha256: sha256(bytes), + }) + PRIVATE_READ_PROVENANCE.set(bytes, provenance) + return bytes +} + +function sha256(bytes) { + return createHash("sha256").update(bytes).digest("hex") +} + +async function withPrivateWriteLock(target, dependencies, operation) { + const lockTarget = privateLockTarget(target) + if (lockTarget === null) return operation() + const active = PRIVATE_TRANSACTION_CONTEXT.getStore() + if (active?.active === true && active.lockTarget === lockTarget) { + return operation(() => assertActivePrivateLease(lockTarget, active)) + } + if (active?.active === true) { + throw new Error("Private envelope transaction cannot acquire another lock") + } + const runtime = snapshotDependencies(dependencies, true) + const identifier = runtime.randomUUID() + if (typeof identifier !== "string" || !UUID_PATTERN.test(identifier)) { + throw new TypeError("Private envelope lock identity is invalid") + } + const parentPath = path.dirname(lockTarget) + const parentChain = await captureParentChain( + runtime.operations, + parentPath, + "private envelope lock", + ) + const parentGuard = await openParentGuard( + runtime.operations, + parentPath, + parentChain, + "private envelope lock", + true, + ) + let handle + let operations + let identity + let primaryError = null + let result + try { + try { + handle = await openNoFollow( + runtime.operations, + lockTarget, + fsConstants.O_RDWR | fsConstants.O_CREAT | fsConstants.O_EXCL, + "private envelope lock", + PRIVATE_MODE, + ) + } catch (error) { + if (errorCode(error) !== "EEXIST") throw error + await quarantineProvablyDeadLock({ + runtime, + lockTarget, + identifier, + parentGuard, + }) + handle = await openNoFollow( + runtime.operations, + lockTarget, + fsConstants.O_RDWR | fsConstants.O_CREAT | fsConstants.O_EXCL, + "private envelope lock", + PRIVATE_MODE, + ) + } + operations = snapshotHandleOperations(handle, ["chmod", "close", "stat", "sync", "write"]) + await operations.chmod(PRIVATE_MODE) + const lockBytes = canonicalPrivateLockBytes({ + lockTarget, + nonce: identifier, + }) + const written = await operations.write(lockBytes, 0, lockBytes.byteLength, 0) + if ( + resultCount(written, "bytesWritten", lockBytes.byteLength, "lock write") !== + lockBytes.byteLength + ) { + throw new Error("Private envelope lock write is incomplete") + } + await operations.sync() + identity = await operations.stat({ bigint: true }) + assertTemporaryFile(identity, runtime.effectiveUserId, POLICIES.private, lockBytes.byteLength) + const current = await runtime.operations.lstat(lockTarget, { + bigint: true, + }) + if (!sameFileState(identity, current)) + throw new Error("Private envelope lock path changed during acquisition") + await parentGuard.operations.sync() + const lease = { active: true, id: identifier, lockTarget } + result = await PRIVATE_TRANSACTION_CONTEXT.run(lease, async () => { + try { + return await operation(() => assertActivePrivateLease(lockTarget, lease)) + } finally { + lease.active = false + } + }) + } catch (error) { + primaryError = error + } + + const cleanupErrors = [] + if (operations !== undefined) { + try { + await operations.close() + handle = undefined + } catch (error) { + cleanupErrors.push(error) + } + } + if (identity !== undefined) { + try { + const current = await runtime.operations.lstat(lockTarget, { + bigint: true, + }) + if (!sameFileState(identity, current)) { + throw new Error("Private envelope lock ownership changed; retained fail-closed") + } + const releasedPath = `${lockTarget}.${identifier}.released` + await runtime.operations.rename(lockTarget, releasedPath) + const released = await runtime.operations.lstat(releasedPath, { + bigint: true, + }) + if (!sameFileObject(identity, released)) { + throw new Error("Private envelope lock release identity changed; retained fail-closed") + } + await runtime.operations.unlink(releasedPath) + await parentGuard.operations.sync() + } catch (error) { + cleanupErrors.push(error) + } + } + try { + await parentGuard.operations.close() + } catch (error) { + cleanupErrors.push(error) + } + throwCombined( + primaryError, + cleanupErrors, + "Private envelope transaction and lock cleanup both failed", + ) + return result +} + +async function quarantineProvablyDeadLock({ runtime, lockTarget, identifier, parentGuard }) { + const before = await runtime.operations.lstat(lockTarget, { bigint: true }) + assertPrivateLockFile(before, runtime.effectiveUserId) + if (before.size <= 0n || before.size > BigInt(LOCK_RECORD_BYTES)) { + throw new Error("Existing private lock record has an invalid byte length") + } + const handle = await openNoFollow( + runtime.operations, + lockTarget, + fsConstants.O_RDONLY, + "existing private envelope lock", + ) + const operations = snapshotHandleOperations(handle, ["close", "read", "stat"]) + let closed = false + try { + const opened = await operations.stat({ bigint: true }) + assertPrivateLockFile(opened, runtime.effectiveUserId) + if (!sameFileState(before, opened)) { + throw new Error("Existing private lock changed before recovery read") + } + const first = await readExactHandleBytes( + operations, + Number(opened.size), + "existing private lock", + ) + const afterFirst = await operations.stat({ bigint: true }) + const second = await readExactHandleBytes( + operations, + Number(opened.size), + "existing private lock repeat", + ) + const afterSecond = await operations.stat({ bigint: true }) + const current = await runtime.operations.lstat(lockTarget, { + bigint: true, + }) + if ( + !first.equals(second) || + !sameFileState(opened, afterFirst) || + !sameFileState(afterFirst, afterSecond) || + !sameFileState(afterSecond, current) + ) { + throw new Error("Existing private lock changed during recovery read") + } + const record = parseCanonicalPrivateLock(first, lockTarget) + assertProvablyDeadProcess(record) + await operations.close() + closed = true + const finalCurrent = await runtime.operations.lstat(lockTarget, { + bigint: true, + }) + if (!sameFileState(current, finalCurrent)) { + throw new Error("Existing private lock changed before quarantine") + } + const quarantinePath = `${lockTarget}.${identifier}.quarantine` + await runtime.operations.rename(lockTarget, quarantinePath) + const quarantined = await runtime.operations.lstat(quarantinePath, { + bigint: true, + }) + assertPrivateLockFile(quarantined, runtime.effectiveUserId) + if (!sameFileObject(finalCurrent, quarantined)) { + throw new Error("Quarantined private lock identity changed during recovery") + } + await parentGuard.operations.sync() + } finally { + if (!closed) await operations.close() + } +} + +function canonicalPrivateLockBytes({ lockTarget, nonce }) { + return Buffer.from( + `${JSON.stringify({ + schemaVersion: 1, + pid: process.pid, + processStartIdentity: PROCESS_START_IDENTITY, + nonce, + path: lockTarget, + createdAt: new Date().toISOString(), + })}\n`, + "utf8", + ) +} + +function parseCanonicalPrivateLock(bytes, lockTarget) { + let record + try { + record = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) + } catch { + throw new Error("Existing private lock record is malformed") + } + const expectedKeys = [ + "schemaVersion", + "pid", + "processStartIdentity", + "nonce", + "path", + "createdAt", + ] + const keys = record !== null && typeof record === "object" ? Reflect.ownKeys(record) : [] + if ( + record === null || + typeof record !== "object" || + utilTypes.isProxy(record) || + Object.getPrototypeOf(record) !== Object.prototype || + keys.length !== expectedKeys.length || + keys.some((key, index) => key !== expectedKeys[index]) || + record.schemaVersion !== 1 || + !Number.isSafeInteger(record.pid) || + record.pid <= 0 || + (record.processStartIdentity !== null && + (typeof record.processStartIdentity !== "string" || + record.processStartIdentity.length === 0 || + Buffer.byteLength(record.processStartIdentity, "utf8") > 256)) || + typeof record.nonce !== "string" || + !UUID_PATTERN.test(record.nonce) || + record.path !== lockTarget || + typeof record.createdAt !== "string" || + !LOCK_TIMESTAMP_PATTERN.test(record.createdAt) || + Number.isNaN(Date.parse(record.createdAt)) + ) { + throw new Error("Existing private lock record is invalid") + } + const canonical = Buffer.from(`${JSON.stringify(record)}\n`, "utf8") + if (!bytes.equals(canonical)) { + throw new Error("Existing private lock record is not canonical") + } + return record +} + +function assertProvablyDeadProcess(record) { + try { + process.kill(record.pid, 0) + } catch (error) { + if (errorCode(error) === "ESRCH") return + throw new Error("Existing private lock owner status is unknown", { + cause: error, + }) + } + throw new Error("Existing private lock owner is live or its PID may have been reused") +} + +function assertPrivateLockFile(status, expectedUserId) { + if (!status.isFile() || status.isSymbolicLink()) { + throw new Error("Existing private lock must be a regular no-follow file") + } + if (status.nlink !== 1n) { + throw new Error("Existing private lock must have exactly one link") + } + if (status.uid !== expectedUserId) { + throw new Error("Existing private lock must have the current effective owner") + } + if (Number(status.mode & 0o7777n) !== PRIVATE_MODE) { + throw new Error("Existing private lock must have exact mode 0600") + } +} + +async function readExactHandleBytes(operations, size, label) { + const bytes = Buffer.allocUnsafe(size) + let offset = 0 + while (offset < size) { + const requested = Math.min(IO_CHUNK_BYTES, size - offset) + const result = await operations.read(bytes, offset, requested, offset) + const count = resultCount(result, "bytesRead", requested, `${label} read`) + if (count === 0) break + offset += count + } + if (offset !== size) throw new Error(`${capitalize(label)} read is incomplete`) + return bytes +} + +function assertActivePrivateLease(lockTarget, expectedLease) { + const lease = PRIVATE_TRANSACTION_CONTEXT.getStore() + if ( + lease === undefined || + lease.active !== true || + lease.lockTarget !== lockTarget || + (expectedLease !== undefined && lease !== expectedLease) + ) { + throw new Error("Authenticated journal access requires the active exact transaction lease") + } + return lease +} + +function privateLockTarget(target) { + const basename = path.basename(target) + if (basename !== JOURNAL_BASENAME && basename !== JOURNAL_HEAD_BASENAME) return null + return path.join(path.dirname(target), `.${JOURNAL_BASENAME}.lock`) +} + +function requireNoFollowSupport(needsWrite) { + const required = [fsConstants.O_RDONLY, fsConstants.O_NOFOLLOW, fsConstants.O_DIRECTORY] + if (needsWrite) { + required.push(fsConstants.O_RDWR, fsConstants.O_CREAT, fsConstants.O_EXCL) + } + if ( + required.some((value) => !Number.isInteger(value)) || + fsConstants.O_NOFOLLOW === 0 || + fsConstants.O_DIRECTORY === 0 + ) { + throw new TypeError("Consolidation evidence no-follow containment is unavailable") + } +} + +async function captureParentChain(operations, parentPath, label) { + const parsed = path.parse(parentPath) + const relative = parentPath.slice(parsed.root.length) + const components = relative.length === 0 ? [] : relative.split(path.sep) + const chain = [] + let current = parsed.root + for (const component of components) { + current = path.join(current, component) + const status = await operations.lstat(current, { bigint: true }) + if (!status.isDirectory() || status.isSymbolicLink()) { + throw new Error(`${capitalize(label)} has an unsafe parent symlink or component`) + } + chain.push(Object.freeze({ dev: status.dev, ino: status.ino, path: current })) + } + if (chain.length === 0) { + const status = await operations.lstat(parsed.root, { bigint: true }) + if (!status.isDirectory() || status.isSymbolicLink()) { + throw new Error(`${capitalize(label)} has an unsafe parent component`) + } + chain.push(Object.freeze({ dev: status.dev, ino: status.ino, path: parsed.root })) + } + return Object.freeze(chain) +} + +async function assertParentChainCurrent(operations, chain, label) { + for (const expected of chain) { + const current = await operations.lstat(expected.path, { bigint: true }) + if ( + !current.isDirectory() || + current.isSymbolicLink() || + current.dev !== expected.dev || + current.ino !== expected.ino + ) { + throw new Error(`${capitalize(label)} parent path changed during containment`) + } + } +} + +async function openParentGuard(operations, parentPath, chain, label, needsSync) { + const handle = await openNoFollow( + operations, + parentPath, + fsConstants.O_RDONLY | fsConstants.O_DIRECTORY, + `${label} parent`, + ) + const handleOperations = snapshotHandleOperations(handle, [ + "close", + "stat", + ...(needsSync ? ["sync"] : []), + ]) + try { + const status = await handleOperations.stat({ bigint: true }) + const expected = chain.at(-1) + if (!status.isDirectory() || status.dev !== expected.dev || status.ino !== expected.ino) { + throw new Error(`${capitalize(label)} parent path changed while it was opened`) + } + await assertParentChainCurrent(operations, chain, label) + return Object.freeze({ handle, operations: handleOperations }) + } catch (error) { + await handleOperations.close() + throw error + } +} + +async function openNoFollow(operations, filePath, flags, label, mode) { + try { + return await operations.open(filePath, flags | fsConstants.O_NOFOLLOW, mode) + } catch (error) { + const code = errorCode(error) + if (code === "ELOOP" || code === "ENOTDIR") { + throw new Error(`${capitalize(label)} must be a no-follow regular path`, { + cause: error, + }) + } + throw error + } +} + +function assertSourcePolicy(status, expectedUserId, policy) { + if (!status.isFile()) throw new Error(`${capitalize(policy.label)} must be a regular file`) + if (status.nlink !== 1n) throw new Error(`${capitalize(policy.label)} must have exactly one link`) + if (status.uid !== expectedUserId) { + throw new Error(`${capitalize(policy.label)} must have the current effective owner`) + } + const mode = Number(status.mode & 0o7777n) + if (policy === POLICIES.private) { + if (mode !== PRIVATE_MODE) { + throw new Error(`${capitalize(policy.label)} mode must be exactly 0600`) + } + return + } + if ((mode & 0o7000) !== 0) { + throw new Error(`${capitalize(policy.label)} mode must not contain special permission bits`) + } + if ((mode & 0o111) !== 0) { + throw new Error(`${capitalize(policy.label)} mode must be nonexecutable`) + } + if ((mode & 0o022) !== 0) { + throw new Error(`${capitalize(policy.label)} mode must not be group or other writable`) + } +} + +function assertTemporaryFile(status, expectedUserId, policy, expectedBytes) { + assertSourcePolicy(status, expectedUserId, policy) + if (status.size !== BigInt(expectedBytes)) { + throw new Error(`${capitalize(policy.label)} temporary write is incomplete`) + } +} + +async function inspectExistingDestination(runtime, target, policy, parentChain, expected) { + let handle + try { + handle = await openNoFollow(runtime.operations, target, fsConstants.O_RDONLY, policy.label) + } catch (error) { + if (errorCode(error) === "ENOENT" && (expected === null || expected?.absent === true)) + return null + if (errorCode(error) === "ENOENT") { + throw new Error(`${capitalize(policy.label)} authenticated current file is missing`, { + cause: error, + }) + } + throw error + } + const operations = snapshotHandleOperations(handle, [ + "close", + "stat", + ...(expected === null ? [] : ["read"]), + ]) + try { + const status = await operations.stat({ bigint: true }) + assertSourcePolicy(status, runtime.effectiveUserId, policy) + if (expected?.absent === true) { + throw new Error(`${capitalize(policy.label)} appeared before authenticated creation`) + } + if (expected !== null && !sameIdentityRecord(expected.identity, status)) { + throw new Error( + `${capitalize(policy.label)} no longer identifies the authenticated current file`, + ) + } + if (expected !== null) { + if (status.size !== BigInt(expected.bytes.byteLength)) { + throw new Error( + `${capitalize(policy.label)} current bytes differ before authenticated replacement`, + ) + } + const observed = Buffer.allocUnsafe(expected.bytes.byteLength) + let offset = 0 + while (offset < observed.byteLength) { + const requested = Math.min(IO_CHUNK_BYTES, observed.byteLength - offset) + const readResult = await operations.read(observed, offset, requested, offset) + const bytesRead = resultCount( + readResult, + "bytesRead", + requested, + `${policy.label} authenticated current read`, + ) + if (bytesRead === 0) break + offset += bytesRead + } + const afterRead = await operations.stat({ bigint: true }) + if ( + offset !== observed.byteLength || + !observed.equals(expected.bytes) || + !sameFileState(status, afterRead) + ) { + throw new Error( + `${capitalize(policy.label)} current bytes changed before authenticated replacement`, + ) + } + } + const current = await runtime.operations.lstat(target, { bigint: true }) + if (current.isSymbolicLink() || !sameFileState(status, current)) { + throw new Error(`${capitalize(policy.label)} destination changed during inspection`) + } + assertSourcePolicy(current, runtime.effectiveUserId, policy) + await assertParentChainCurrent(runtime.operations, parentChain, policy.label) + return fileIdentity(status) + } finally { + await operations.close() + } +} + +async function assertDestinationUnchanged(runtime, target, expected, policy) { + let current + try { + current = await runtime.operations.lstat(target, { bigint: true }) + } catch (error) { + if (expected === null && errorCode(error) === "ENOENT") return + throw error + } + if (expected === null || current.isSymbolicLink() || !sameIdentityRecord(expected, current)) { + throw new Error(`${capitalize(policy.label)} destination changed before atomic replacement`) + } + assertSourcePolicy(current, runtime.effectiveUserId, policy) +} + +async function assertTemporaryPathCurrent( + operations, + temporaryPath, + expected, + expectedUserId, + policy, +) { + const current = await operations.lstat(temporaryPath, { bigint: true }) + if (current.isSymbolicLink() || !sameFileState(expected, current)) { + throw new Error(`${capitalize(policy.label)} temporary path changed before publication`) + } + assertTemporaryFile(current, expectedUserId, policy, Number(expected.size)) +} + +async function verifyPublishedBytes( + operations, + descriptor, + target, + expected, + intendedBytes, + expectedUserId, + policy, + includeChangeMetadata, +) { + const before = await descriptor.stat({ bigint: true }) + if ( + before.dev !== expected.dev || + before.ino !== expected.ino || + before.size !== expected.size || + before.mtimeNs !== expected.mtimeNs || + (includeChangeMetadata && !sameIdentityRecord(expected, before)) + ) { + throw new Error(`${capitalize(policy.label)} changed during atomic publication`) + } + assertTemporaryFile(before, expectedUserId, policy, intendedBytes.byteLength) + const observedBytes = Buffer.allocUnsafe(intendedBytes.byteLength) + let offset = 0 + while (offset < observedBytes.byteLength) { + const requested = Math.min(IO_CHUNK_BYTES, observedBytes.byteLength - offset) + const readResult = await descriptor.read(observedBytes, offset, requested, offset) + const bytesRead = resultCount( + readResult, + "bytesRead", + requested, + `${policy.label} publication read`, + ) + if (bytesRead === 0) break + offset += bytesRead + } + const after = await descriptor.stat({ bigint: true }) + const current = await operations.lstat(target, { bigint: true }) + if ( + offset !== intendedBytes.byteLength || + !observedBytes.equals(intendedBytes) || + !sameFileState(before, after) || + current.isSymbolicLink() || + !sameFileState(after, current) + ) { + throw new Error(`${capitalize(policy.label)} bytes changed during atomic publication`) + } + assertSourcePolicy(after, expectedUserId, policy) + assertSourcePolicy(current, expectedUserId, policy) + return fileIdentity(after) +} + +async function observeRetainedTemporary(operations, temporaryPath, expected) { + if (expected === undefined) return "unobservable" + const first = await retainedPathStatus(operations, temporaryPath) + if (first === "missing" || first === "unobservable") return first + const second = await retainedPathStatus(operations, temporaryPath) + if (second === "missing" || second === "unobservable") return second + if ( + !first.isSymbolicLink() && + first.dev === expected.dev && + first.ino === expected.ino && + sameFileState(first, second) + ) { + return "owned" + } + return "replaced" +} + +async function retainedPathStatus(operations, temporaryPath) { + try { + return await operations.lstat(temporaryPath, { bigint: true }) + } catch (error) { + return errorCode(error) === "ENOENT" ? "missing" : "unobservable" + } +} + +function retainedTemporaryMessage(label, temporaryPath, state) { + const prefix = `${capitalize(label)} failed before publication;` + if (state === "owned") { + return `${prefix} operation-owned temporary artifact ${temporaryPath} was retained for safe inspection` + } + if (state === "missing") { + return `${prefix} the operation-owned temporary artifact is no longer present at ${temporaryPath}; no pathname was removed` + } + if (state === "replaced") { + return `${prefix} temporary pathname ${temporaryPath} no longer identifies the operation-owned artifact; the replacement was left untouched` + } + return `${prefix} temporary pathname ${temporaryPath} could not be identified safely and was left untouched` +} + +function sameFileState(before, after) { + return ( + after.isFile() && + after.dev === before.dev && + after.ino === before.ino && + after.size === before.size && + after.nlink === before.nlink && + after.mtimeNs === before.mtimeNs && + after.ctimeNs === before.ctimeNs + ) +} + +function sameFileObject(before, after) { + return ( + after.isFile() && + after.dev === before.dev && + after.ino === before.ino && + after.size === before.size && + after.nlink === before.nlink + ) +} + +function fileIdentity(status) { + return Object.freeze({ + ctimeNs: status.ctimeNs, + dev: status.dev, + ino: status.ino, + mtimeNs: status.mtimeNs, + nlink: status.nlink, + size: status.size, + }) +} + +function sameIdentityRecord(expected, current) { + return ( + current.isFile() && + current.dev === expected.dev && + current.ino === expected.ino && + current.size === expected.size && + current.nlink === expected.nlink && + current.mtimeNs === expected.mtimeNs && + current.ctimeNs === expected.ctimeNs + ) +} + +function resultCount(result, field, requested, label) { + if (result === null || typeof result !== "object" || utilTypes.isProxy(result)) { + throw new TypeError(`${capitalize(label)} result is unsafe`) + } + const descriptor = Object.getOwnPropertyDescriptor(result, field) + if ( + descriptor === undefined || + !("value" in descriptor) || + !Number.isSafeInteger(descriptor.value) || + descriptor.value < 0 || + descriptor.value > requested + ) { + throw new TypeError(`${capitalize(label)} result is invalid`) + } + return descriptor.value +} + +function errorCode(error) { + if (error === null || typeof error !== "object" || utilTypes.isProxy(error)) return undefined + let current = error + while (current !== null) { + const descriptor = Object.getOwnPropertyDescriptor(current, "code") + if (descriptor !== undefined) return "value" in descriptor ? descriptor.value : undefined + current = Object.getPrototypeOf(current) + } + return undefined +} + +function throwCombined(primaryError, secondaryErrors, message) { + if (primaryError !== null && secondaryErrors.length > 0) { + throw new AggregateError([primaryError, ...secondaryErrors], message) + } + if (primaryError !== null) throw primaryError + if (secondaryErrors.length > 1) throw new AggregateError(secondaryErrors, message) + if (secondaryErrors.length === 1) throw secondaryErrors[0] +} + +function capitalize(value) { + return `${value[0].toUpperCase()}${value.slice(1)}` +} + +function hasControlCharacters(value) { + for (const character of value) { + const codePoint = character.codePointAt(0) + if (codePoint <= 0x1f || codePoint === 0x7f) return true + } + return false +} diff --git a/scripts/release/duplicate-draft-consolidation-journal.mjs b/scripts/release/duplicate-draft-consolidation-journal.mjs new file mode 100644 index 000000000..c710b2657 --- /dev/null +++ b/scripts/release/duplicate-draft-consolidation-journal.mjs @@ -0,0 +1,809 @@ +import { isDeepStrictEqual, types as utilTypes } from "node:util" + +import { assertEvidenceEqualsProposal } from "./duplicate-draft-consolidation-evidence.mjs" +import { + canonicalConsolidationEnvelopeBytes, + canonicalEventEnvelope, + createConsolidationEnvelope, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS, + parseConsolidationEnvelope, +} from "./duplicate-draft-consolidation-schema.mjs" + +const MAXIMUM_DELETE_ATTEMPTS = 3 +const RETRY_OBSERVATION_GAP_MS = 60_000 +const MAXIMUM_ORPHAN_AUTHORITY_RECOVERIES = + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumOrphanAuthorityRecoveries +if (MAXIMUM_ORPHAN_AUTHORITY_RECOVERIES !== 1) { + throw new Error("Journal orphan-authority recovery bound must remain exactly one") +} +const APPROVED_DUPLICATE_IDS = Object.freeze(["379982100", "379986168"]) +const SHA256_PATTERN = /^[0-9a-f]{64}$/u +const TIMESTAMP_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/u +const JOURNAL_STATES = new WeakSet() + +export function createConsolidationJournal(input) { + const value = exactInput( + input, + ["proposedEnvelope", "confirmationSha256", "recordedAt"], + "journal creation input", + ) + const proposedEnvelope = normalizeEnvelope( + "proposed", + dataValue(value, "proposedEnvelope", "proposed envelope"), + ) + const confirmationSha256 = canonicalSha256( + dataValue(value, "confirmationSha256", "confirmation digest"), + "confirmation digest", + ) + const recordedAt = canonicalTimestamp( + dataValue(value, "recordedAt", "journal creation timestamp"), + "journal creation timestamp", + ) + const event = canonicalEventEnvelope( + { + schemaVersion: 1, + sequence: 1, + previousEventSha256: null, + type: "operation-started", + recordedAt, + payload: { + proposedRecordSha256: proposedEnvelope.recordSha256, + confirmationSha256, + controllerSha: proposedEnvelope.record.controller.headSha, + deletionOrder: [...proposedEnvelope.record.roles.duplicates], + }, + }, + null, + ) + const journal = createConsolidationEnvelope("journal", { + schemaVersion: 1, + repository: proposedEnvelope.record.repository, + candidate: proposedEnvelope.record.candidate, + proposedRecordSha256: proposedEnvelope.recordSha256, + confirmationSha256, + deletionOrder: [...proposedEnvelope.record.roles.duplicates], + events: [event], + updatedAt: recordedAt, + }) + return freezeEnvelope(journal) +} + +export function parseConsolidationJournal(envelope) { + const parsed = normalizeEnvelope("journal", envelope) + replayJournal(parsed) + return freezeEnvelope(parsed) +} + +export function deriveConsolidationState(journal) { + const parsed = parseConsolidationJournal(journal) + const state = replayJournal(parsed) + const result = deepFreeze({ + phase: state.phase, + controllerSha: state.controllerSha, + deletionOrder: [...state.deletionOrder], + completedTargets: [...state.completedTargets], + currentTargetReleaseId: state.currentTargetReleaseId, + attemptNumber: state.attemptNumber, + lastOutcomeClassification: state.lastOutcomeClassification, + lastAuthority: state.lastAuthority, + lastRetryNpmInventory: state.lastRetryNpmInventory, + pendingRetryFromAttempt: state.pendingRetryFromAttempt, + lastEventSha256: parsed.record.events.at(-1).eventSha256, + journalRecordSha256: parsed.recordSha256, + }) + JOURNAL_STATES.add(result) + return result +} + +export function appendJournalEvent(journal, type, payload, recordedAt) { + const parsed = parseConsolidationJournal(journal) + if (typeof type !== "string" || type.length === 0) { + throw new TypeError("Journal event type must be a nonempty string") + } + const timestamp = canonicalTimestamp(recordedAt, "journal event timestamp") + const previous = parsed.record.events.at(-1) + if (Date.parse(timestamp) < Date.parse(previous.event.recordedAt)) { + throw new Error("Journal event timestamps must be monotone") + } + const eventEnvelope = canonicalEventEnvelope( + { + schemaVersion: 1, + sequence: parsed.record.events.length + 1, + previousEventSha256: previous.eventSha256, + type, + recordedAt: timestamp, + payload, + }, + previous.eventSha256, + ) + const candidate = createConsolidationEnvelope("journal", { + ...parsed.record, + events: [...parsed.record.events, eventEnvelope], + updatedAt: timestamp, + }) + replayJournal(candidate) + return freezeEnvelope(candidate) +} + +export function nextResumeAction(state, liveTarget) { + if (!JOURNAL_STATES.has(state)) { + throw new TypeError("Resume state must come from journal replay") + } + const live = exactInput(liveTarget, Object.keys(liveTarget), "live target observation") + const classification = dataString(live, "classification", "live target classification") + if (state.phase === "final-authority-observed") return "complete" + if (state.phase === "target-converged") { + return state.completedTargets.length === state.deletionOrder.length + ? "complete" + : "refresh-and-retry" + } + if (state.phase === "resume-present") { + return state.attemptNumber >= MAXIMUM_DELETE_ATTEMPTS ? "stop" : "refresh-and-retry" + } + if (state.phase === "resume-absent") return "reconcile-absence" + if (state.phase !== "delete-intent" && state.phase !== "delete-outcome") { + return "stop" + } + if (classification === "absent") return "reconcile-absence" + if (classification !== "present-unchanged") return "stop" + if ( + (state.phase === "delete-outcome" && + !isAmbiguousOutcomeClassification(state.lastOutcomeClassification)) || + state.attemptNumber >= MAXIMUM_DELETE_ATTEMPTS + ) { + return "stop" + } + const evidence = dataValue(live, "releaseEvidence", "live Release evidence") + const expected = state.lastAuthority?.targetRead?.evidence + if (expected === undefined) { + return "stop" + } + try { + assertEvidenceEqualsProposal(evidence, expected) + } catch { + return "stop" + } + if (state.phase === "delete-outcome") { + const observations = dataValue(live, "observations", "live unchanged observation count") + if (!Number.isSafeInteger(observations) || observations !== 6) return "stop" + } + return "refresh-and-retry" +} + +function isAmbiguousOutcomeClassification(value) { + return value === "transport-ambiguous" || value === "response-404-ambiguous" +} + +export function createFinalConsolidationReceipt(input) { + const value = exactInput( + input, + ["proposedEnvelope", "journalEnvelope", "finalAuthority", "completedAt"], + "final receipt input", + ) + const proposedEnvelope = normalizeEnvelope( + "proposed", + dataValue(value, "proposedEnvelope", "final proposal envelope"), + ) + const journalEnvelope = parseConsolidationJournal( + dataValue(value, "journalEnvelope", "final journal envelope"), + ) + const state = deriveConsolidationState(journalEnvelope) + if ( + state.phase !== "final-authority-observed" || + state.completedTargets.length !== journalEnvelope.record.deletionOrder.length + ) { + throw new Error("Final receipt requires both targets converged and final authority recorded") + } + if ( + journalEnvelope.record.proposedRecordSha256 !== proposedEnvelope.recordSha256 || + !isDeepStrictEqual(journalEnvelope.record.repository, proposedEnvelope.record.repository) || + !isDeepStrictEqual(journalEnvelope.record.candidate, proposedEnvelope.record.candidate) + ) { + throw new Error("Final journal does not bind the proposed envelope") + } + const finalAuthority = snapshotPlain( + dataValue(value, "finalAuthority", "final authority"), + "final authority", + ) + if (!isDeepStrictEqual(finalAuthority, state.lastAuthority)) { + throw new Error("Final authority differs from the journal terminal event") + } + assertFinalAuthorityMatchesProposal(finalAuthority, proposedEnvelope.record) + const completedAt = canonicalTimestamp( + dataValue(value, "completedAt", "receipt completion timestamp"), + "receipt completion timestamp", + ) + if (Date.parse(completedAt) < Date.parse(journalEnvelope.record.updatedAt)) { + throw new Error("Receipt completion precedes the completed journal") + } + return freezeEnvelope( + createConsolidationEnvelope("final", { + schemaVersion: 1, + proposedEnvelope, + journalEnvelope, + finalAuthority, + finalSurvivor: finalAuthority.releases[0], + completedAt, + }), + ) +} + +function replayJournal(journal) { + const events = journal.record.events + if (events.length === 0) throw new Error("Journal is truncated before start") + const started = events[0].event + if (started.type !== "operation-started") { + throw new Error("Journal must begin with operation-started") + } + if ( + started.payload.proposedRecordSha256 !== journal.record.proposedRecordSha256 || + started.payload.confirmationSha256 !== journal.record.confirmationSha256 || + !isDeepStrictEqual(started.payload.deletionOrder, journal.record.deletionOrder) + ) { + throw new Error("Operation start does not bind the journal header") + } + if (!isDeepStrictEqual(journal.record.deletionOrder, APPROVED_DUPLICATE_IDS)) { + throw new Error("Journal target order is not approved") + } + if (journal.record.updatedAt !== events.at(-1).event.recordedAt) { + throw new Error("Journal update timestamp does not bind its final event") + } + + const state = { + phase: "operation-started", + controllerSha: started.payload.controllerSha, + deletionOrder: journal.record.deletionOrder, + completedTargets: [], + currentTargetReleaseId: journal.record.deletionOrder[0], + attemptNumber: 1, + lastOutcomeClassification: null, + lastAuthority: null, + lastAuthorityEventSha256: null, + lastRetryEvidence: null, + lastRetryNpmInventory: null, + pendingRetryFromAttempt: null, + orphanAuthorityRecoveries: 0, + } + assertEventTemporalOrder(started) + let previousTimestamp = started.recordedAt + for (let index = 1; index < events.length; index += 1) { + const envelope = events[index] + const event = envelope.event + if (Date.parse(event.recordedAt) < Date.parse(previousTimestamp)) { + throw new Error("Journal event timestamps are not monotone") + } + previousTimestamp = event.recordedAt + assertEventTemporalOrder(event) + applyEvent(state, event, envelope.eventSha256) + } + return state +} + +function assertEventTemporalOrder(event) { + const eventTime = event.recordedAt + if (event.type === "npm-observed") { + assertNpmTemporalOrder(event.payload.inventory, eventTime) + return + } + if (event.type === "delete-authority-observed" || event.type === "final-authority-observed") { + assertAuthorityEventTemporalOrder(event.payload.authority, eventTime) + return + } + if (event.type === "delete-outcome" || event.type === "resume-reconciliation") { + assertNotAfter(event.payload.observedAt, eventTime, `${event.type} observation`) + if (event.payload.releaseEvidence !== null && event.payload.releaseEvidence !== undefined) { + assertReleaseEvidenceTemporalOrder(event.payload.releaseEvidence, event.payload.observedAt) + } + return + } + if (event.type === "absence-converged") { + assertOrderedTimestamps( + [ + event.payload.directGet404At, + event.payload.listAbsentAt, + event.payload.completedAt, + eventTime, + ], + "absence convergence", + ) + } +} + +function assertAuthorityEventTemporalOrder(authority, eventTime) { + assertNotAfter(authority.observedAt, eventTime, "authority observation") + assertNotAfter( + authority.annotatedTag.observedAt, + authority.observedAt, + "annotated tag observation", + ) + assertNotAfter( + authority.workflowAuthority.observedAt, + authority.observedAt, + "workflow observation", + ) + assertNpmTemporalOrder(authority.npmInventory, authority.observedAt) + for (const release of authority.releases) { + assertReleaseEvidenceTemporalOrder(release, authority.observedAt) + } + if (authority.targetRead !== null) { + assertOrderedTimestamps( + [ + authority.npmInventory.completedAt, + authority.targetRead.releaseGetStartedAt, + authority.targetRead.releaseGetCompletedAt, + authority.targetRead.assetsListStartedAt, + authority.targetRead.assetsListCompletedAt, + authority.observedAt, + ], + "authority target read", + ) + } +} + +function assertNpmTemporalOrder(inventory, upperBound) { + assertNotAfter(inventory.startedAt, inventory.completedAt, "npm inventory interval") + for (const entry of inventory.packages) { + assertOrderedTimestamps( + [inventory.startedAt, entry.observedAt, inventory.completedAt], + "npm package observation", + ) + } + assertNotAfter(inventory.completedAt, upperBound, "npm inventory completion") +} + +function assertReleaseEvidenceTemporalOrder(release, upperBound) { + assertOrderedTimestamps([release.createdAt, release.updatedAt, upperBound], "Release evidence") + for (const asset of release.assets) { + assertOrderedTimestamps( + [asset.createdAt, asset.updatedAt, upperBound], + "Release asset evidence", + ) + } +} + +function assertNotAfter(earlier, later, label) { + assertOrderedTimestamps([earlier, later], label) +} + +function assertOrderedTimestamps(timestamps, label) { + const values = timestamps.map((timestamp) => Date.parse(timestamp)) + if ( + values.some((value) => !Number.isFinite(value)) || + values.some((value, index) => index > 0 && value < values[index - 1]) + ) { + throw new Error(`${label} timestamps are contradictory`) + } +} + +function applyEvent(state, event, eventSha256) { + if (state.phase === "final-authority-observed") { + throw new Error("Final authority must be the journal's terminal event") + } + if (event.type === "npm-observed") { + const preparingRetry = + state.phase === "delete-intent" || + (state.phase === "delete-outcome" && + isAmbiguousOutcomeClassification(state.lastOutcomeClassification)) + const expectedAttempt = + state.phase === "resume-present" || preparingRetry + ? state.attemptNumber + 1 + : state.attemptNumber + assertCurrentTarget(state, event.payload, "npm observation") + if (event.payload.attemptNumber !== expectedAttempt) { + throw new Error("npm observation is not for the next legal attempt") + } + if (expectedAttempt > MAXIMUM_DELETE_ATTEMPTS) { + throw new Error("Delete attempts are exhausted at the reviewed maximum") + } + if ( + ![ + "operation-started", + "target-converged", + "resume-present", + "delete-intent", + "delete-outcome", + ].includes(state.phase) || + (state.phase === "delete-outcome" && + !isAmbiguousOutcomeClassification(state.lastOutcomeClassification)) + ) { + throw new Error("npm observation is not legal in the current journal state") + } + if (event.payload.inventory.stage !== "perform-initial") { + throw new Error("Journal npm events must record perform-initial inventory") + } + state.phase = "npm-observed" + state.lastRetryNpmInventory = event.payload.inventory + state.pendingRetryFromAttempt = preparingRetry ? state.attemptNumber : null + if (!preparingRetry) state.attemptNumber = expectedAttempt + return + } + if (event.type === "delete-authority-observed") { + const supersedingOrphan = state.phase === "delete-authority-observed" + const expectedAttempt = + state.phase === "resume-present" ? state.attemptNumber + 1 : state.attemptNumber + assertCurrentTarget(state, event.payload, "delete authority") + if (event.payload.attemptNumber !== expectedAttempt) { + throw new Error("Delete authority attempt is not the next legal attempt") + } + if ( + ![ + "operation-started", + "npm-observed", + "target-converged", + "resume-present", + "delete-authority-observed", + ].includes(state.phase) + ) { + throw new Error("Delete authority is not legal in the current state") + } + if (state.phase === "npm-observed" && state.pendingRetryFromAttempt !== null) { + throw new Error("Stale retry npm evidence requires reconciliation before authority") + } + if (expectedAttempt > MAXIMUM_DELETE_ATTEMPTS) { + throw new Error("Delete attempts are exhausted at the reviewed maximum") + } + const orphanAuthorityRecoveries = supersedingOrphan + ? state.orphanAuthorityRecoveries + 1 + : state.orphanAuthorityRecoveries + if (orphanAuthorityRecoveries > MAXIMUM_ORPHAN_AUTHORITY_RECOVERIES) { + throw new Error("Orphan delete authority recoveries exceed their global bound") + } + const targetIndex = state.deletionOrder.indexOf(state.currentTargetReleaseId) + const expectedStage = targetIndex === 0 ? "pre-delete-1" : "pre-delete-2" + if (event.payload.authority.stage !== expectedStage) { + throw new Error("Delete authority stage differs from fixed target order") + } + if (supersedingOrphan) { + assertOrphanAuthorityMatches(event.payload.authority, state.lastAuthority) + } + if ( + event.payload.authority.controller.headSha !== state.controllerSha || + event.payload.authority.controller.originMainSha !== state.controllerSha || + event.payload.authority.controller.githubMainSha !== state.controllerSha + ) { + throw new Error("Controller main SHA drifted from operation-started") + } + if (state.lastRetryEvidence !== null) { + assertEvidenceEqualsProposal( + event.payload.authority.targetRead.evidence, + state.lastRetryEvidence, + ) + } + if ( + state.lastRetryNpmInventory !== null && + Date.parse(event.payload.authority.npmInventory.startedAt) - + Date.parse(state.lastRetryNpmInventory.completedAt) < + RETRY_OBSERVATION_GAP_MS + ) { + throw new Error("Retry authority began before the sixty-second npm observation gap") + } + state.phase = "delete-authority-observed" + state.attemptNumber = expectedAttempt + state.lastOutcomeClassification = null + state.lastAuthority = event.payload.authority + state.lastAuthorityEventSha256 = eventSha256 + state.lastRetryEvidence = null + state.lastRetryNpmInventory = null + state.pendingRetryFromAttempt = null + state.orphanAuthorityRecoveries = orphanAuthorityRecoveries + return + } + if (event.type === "delete-intent") { + assertCurrentAttempt(state, event.payload, "delete intent") + if ( + state.phase !== "delete-authority-observed" || + event.payload.authorityEventSha256 !== state.lastAuthorityEventSha256 + ) { + throw new Error("Delete intent must immediately bind the preceding authority digest") + } + state.phase = "delete-intent" + return + } + if (event.type === "delete-outcome") { + assertCurrentAttempt(state, event.payload, "delete outcome") + if (state.phase !== "delete-intent") { + throw new Error("Delete outcome requires a durable preceding intent") + } + state.phase = "delete-outcome" + state.lastOutcomeClassification = event.payload.classification + return + } + if (event.type === "resume-reconciliation") { + const preparedByStaleNpm = + state.phase === "npm-observed" && state.pendingRetryFromAttempt !== null + if (preparedByStaleNpm) { + assertCurrentTarget(state, event.payload, "resume reconciliation") + if (event.payload.attemptNumber !== state.pendingRetryFromAttempt) { + throw new Error("Retry reconciliation does not bind the stale npm source attempt") + } + } else { + assertCurrentAttempt(state, event.payload, "resume reconciliation") + } + if ( + state.phase !== "delete-intent" && + !preparedByStaleNpm && + !( + state.phase === "delete-outcome" && + isAmbiguousOutcomeClassification(state.lastOutcomeClassification) + ) + ) { + throw new Error("Resume reconciliation is not legal after this outcome") + } + if ( + event.payload.classification === "present-unchanged-retryable" && + state.lastAuthority?.targetRead?.evidence === undefined + ) { + throw new Error("Retryable target evidence has no preceding delete authority") + } + if (event.payload.classification === "present-unchanged-retryable") { + assertEvidenceEqualsProposal( + event.payload.releaseEvidence, + state.lastAuthority.targetRead.evidence, + ) + state.lastRetryEvidence = event.payload.releaseEvidence + } else { + state.lastRetryEvidence = null + } + state.phase = + event.payload.classification === "present-unchanged-retryable" + ? "resume-present" + : "resume-absent" + state.attemptNumber = event.payload.attemptNumber + state.pendingRetryFromAttempt = null + return + } + if (event.type === "absence-converged") { + assertCurrentAttempt(state, event.payload, "absence convergence") + const confirmed = + state.phase === "delete-outcome" && + state.lastOutcomeClassification === "confirmed-204" && + event.payload.basis === "confirmed-204" + const ambiguous = + event.payload.basis === "ambiguous" && + (state.phase === "resume-absent" || + (state.phase === "delete-outcome" && + isAmbiguousOutcomeClassification(state.lastOutcomeClassification))) + if (!confirmed && !ambiguous) { + throw new Error("Absence convergence basis does not match delete history") + } + state.completedTargets.push(state.currentTargetReleaseId) + state.currentTargetReleaseId = state.deletionOrder[state.completedTargets.length] ?? null + state.attemptNumber = 1 + state.lastOutcomeClassification = null + state.lastAuthority = null + state.lastAuthorityEventSha256 = null + state.lastRetryEvidence = null + state.lastRetryNpmInventory = null + state.pendingRetryFromAttempt = null + state.phase = "target-converged" + return + } + if (event.type === "final-authority-observed") { + if ( + state.phase !== "target-converged" || + state.completedTargets.length !== state.deletionOrder.length || + state.currentTargetReleaseId !== null + ) { + throw new Error("Final authority requires both targets converged absent") + } + if ( + event.payload.authority.controller.headSha !== state.controllerSha || + event.payload.authority.controller.originMainSha !== state.controllerSha || + event.payload.authority.controller.githubMainSha !== state.controllerSha + ) { + throw new Error("Final authority controller drifted from operation-started") + } + state.phase = "final-authority-observed" + state.lastAuthority = event.payload.authority + return + } + throw new Error(`Illegal journal event ${event.type}`) +} + +function assertOrphanAuthorityMatches(actual, previous) { + if ( + previous === null || + actual.stage !== previous.stage || + !isDeepStrictEqual(actual.controller, previous.controller) || + !isDeepStrictEqual( + withoutObservedAt(actual.annotatedTag), + withoutObservedAt(previous.annotatedTag), + ) || + !isDeepStrictEqual( + withoutObservedAt(actual.workflowAuthority), + withoutObservedAt(previous.workflowAuthority), + ) || + !isDeepStrictEqual( + stableNpmInventory(actual.npmInventory), + stableNpmInventory(previous.npmInventory), + ) || + !isDeepStrictEqual(actual.payloadProof, previous.payloadProof) || + actual.releases.length !== previous.releases.length || + !isDeepStrictEqual( + actual.releases.map(({ id }) => id), + previous.releases.map(({ id }) => id), + ) + ) { + throw new Error("Orphan authority recovery drifted from prior authority") + } + for (let index = 0; index < actual.releases.length; index += 1) { + assertEvidenceEqualsProposal(actual.releases[index], previous.releases[index]) + } + assertEvidenceEqualsProposal(actual.targetRead.evidence, previous.targetRead.evidence) +} + +function withoutObservedAt({ observedAt: _observedAt, ...value }) { + return value +} + +function stableNpmInventory({ startedAt: _startedAt, completedAt: _completedAt, ...inventory }) { + return { + ...inventory, + packages: inventory.packages.map(({ observedAt: _observedAt, ...entry }) => entry), + } +} + +function assertFinalAuthorityMatchesProposal(authority, proposal) { + if ( + authority.stage !== "final" || + !isDeepStrictEqual(authority.controller, proposal.controller) + ) { + throw new Error("Final authority controller differs from the proposal") + } + const stableTag = ({ observedAt: _observedAt, ...value }) => value + const stableWorkflow = ({ observedAt: _observedAt, ...value }) => value + if ( + !isDeepStrictEqual(stableTag(authority.annotatedTag), stableTag(proposal.annotatedTag)) || + !isDeepStrictEqual( + stableWorkflow(authority.workflowAuthority), + stableWorkflow(proposal.workflowAuthority), + ) + ) { + throw new Error("Final authority tag or workflow differs from the proposal") + } + if ( + authority.releases.length !== 1 || + authority.releases[0].id !== proposal.roles.survivor || + proposal.roles.duplicates.some((id) => authority.releases.some((release) => release.id === id)) + ) { + throw new Error("Final authority does not contain only the survivor") + } + const proposedSurvivor = proposal.releases.find(({ id }) => id === proposal.roles.survivor) + if (proposedSurvivor === undefined) { + throw new Error("Proposal survivor evidence is missing") + } + assertEvidenceEqualsProposal(authority.releases[0], proposedSurvivor) + if (!isDeepStrictEqual(authority.payloadProof, proposal.payloadProof)) { + throw new Error("Final authority payload proof differs from the proposal") + } + if ( + authority.npmInventory.stage !== "final" || + authority.npmInventory.packages.some((entry) => entry.version !== proposal.candidate.version) + ) { + throw new Error("Final authority npm evidence differs from the candidate") + } +} + +function assertCurrentAttempt(state, payload, label) { + assertCurrentTarget(state, payload, label) + if (payload.attemptNumber !== state.attemptNumber) { + throw new Error(`${label} does not match the current delete attempt`) + } +} + +function assertCurrentTarget(state, payload, label) { + if ( + state.currentTargetReleaseId === null || + payload.targetReleaseId !== state.currentTargetReleaseId + ) { + throw new Error(`${label} violates the fixed target convergence order`) + } +} + +function normalizeEnvelope(kind, value) { + if (value instanceof Uint8Array) { + return parseConsolidationEnvelope(kind, Buffer.from(value)) + } + return parseConsolidationEnvelope(kind, canonicalConsolidationEnvelopeBytes(kind, value)) +} + +function freezeEnvelope(value) { + return deepFreeze(value) +} + +function exactInput(value, expectedKeys, label) { + if (!isPlainObject(value) || utilTypes.isProxy(value)) { + throw new TypeError(`${label} must be a plain non-proxy object`) + } + const descriptors = Object.getOwnPropertyDescriptors(value) + const keys = Object.keys(descriptors) + if ( + Object.getOwnPropertySymbols(value).length !== 0 || + keys.length !== expectedKeys.length || + keys.some((key) => !expectedKeys.includes(key)) || + keys.some((key) => { + const descriptor = descriptors[key] + return !descriptor.enumerable || !("value" in descriptor) + }) + ) { + throw new TypeError(`${label} fields are invalid`) + } + return value +} + +function dataValue(value, name, label) { + const descriptor = Object.getOwnPropertyDescriptor(value, name) + if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) { + throw new TypeError(`${label} must be an enumerable data property`) + } + return descriptor.value +} + +function dataString(value, name, label) { + const result = dataValue(value, name, label) + if (typeof result !== "string") throw new TypeError(`${label} must be a string`) + return result +} + +function canonicalSha256(value, label) { + if (typeof value !== "string" || !SHA256_PATTERN.test(value)) { + throw new TypeError(`${label} must be a canonical SHA-256 digest`) + } + return value +} + +function canonicalTimestamp(value, label) { + if ( + typeof value !== "string" || + !TIMESTAMP_PATTERN.test(value) || + !Number.isFinite(Date.parse(value)) || + new Date(Date.parse(value)).toISOString() !== value + ) { + throw new TypeError(`${label} must be a canonical UTC timestamp`) + } + return value +} + +function snapshotPlain(value, label) { + if (utilTypes.isProxy(value)) throw new TypeError(`${label} must not be a proxy`) + if (Array.isArray(value)) { + if ( + Object.getPrototypeOf(value) !== Array.prototype || + Object.keys(value).length !== value.length + ) { + throw new TypeError(`${label} must be a dense array`) + } + return value.map((entry, index) => snapshotPlain(entry, `${label}[${index}]`)) + } + if (value !== null && typeof value === "object") { + if (!isPlainObject(value)) throw new TypeError(`${label} must be plain data`) + const result = {} + for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) { + if (!descriptor.enumerable || !("value" in descriptor)) { + throw new TypeError(`${label} contains a hidden or accessor field`) + } + result[key] = snapshotPlain(descriptor.value, `${label}.${key}`) + } + return result + } + if (["bigint", "function", "symbol", "undefined"].includes(typeof value)) { + throw new TypeError(`${label} contains a non-JSON value`) + } + return value +} + +function isPlainObject(value) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return false + } + const prototype = Object.getPrototypeOf(value) + return prototype === Object.prototype || prototype === null +} + +function deepFreeze(value) { + if (value !== null && typeof value === "object" && !Object.isFrozen(value)) { + for (const child of Object.values(value)) deepFreeze(child) + Object.freeze(value) + } + return value +} diff --git a/scripts/release/duplicate-draft-consolidation-release-classifier.mjs b/scripts/release/duplicate-draft-consolidation-release-classifier.mjs new file mode 100644 index 000000000..786061224 --- /dev/null +++ b/scripts/release/duplicate-draft-consolidation-release-classifier.mjs @@ -0,0 +1,159 @@ +import { snapshotJson } from "./adapter-normalize.mjs" +import { canonicalRecordSha256 } from "./duplicate-draft-consolidation-schema.mjs" +import { parseReleaseMarker } from "./metadata.mjs" + +const APPROVED_CANDIDATE = Object.freeze({ + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + tag: "v0.8.22", +}) +const SURVIVOR_ID = "379991871" +const DUPLICATE_IDS = Object.freeze(["379982100", "379986168"]) +const INCIDENT_IDS = Object.freeze([SURVIVOR_ID, ...DUPLICATE_IDS]) +const ID_PATTERN = /^[1-9][0-9]*$/u +const DAWN_MARKER_IDENTITY = "DAWN_RELEASE_CONTROLLER_MARKER" + +const STAGE_RULES = Object.freeze({ + "pre-delete-1": Object.freeze({ + releaseIds: INCIDENT_IDS, + targetReleaseId: DUPLICATE_IDS[0], + }), + "pre-delete-2": Object.freeze({ + releaseIds: Object.freeze([SURVIVOR_ID, DUPLICATE_IDS[1]]), + targetReleaseId: DUPLICATE_IDS[1], + }), + final: Object.freeze({ + releaseIds: Object.freeze([SURVIVOR_ID]), + targetReleaseId: null, + }), +}) + +export function consolidationStageRule(stage) { + const rule = STAGE_RULES[stage] + if (rule === undefined) throw new TypeError("Consolidation authority stage is invalid") + return rule +} + +export function classifyConsolidationReleases(rawReleases, proposal, stage) { + const rule = consolidationStageRule(stage) + assertApprovedProposalIdentity(proposal) + const releases = snapshotJson(rawReleases) + if (!Array.isArray(releases)) throw new TypeError("Release enumeration must be an array") + + const expected = new Set(rule.releaseIds) + const selected = new Map() + const seenIds = new Set() + const entries = [] + for (const [index, release] of releases.entries()) { + const id = canonicalId(release?.id, `GitHub Release ${index} id`) + if (seenIds.has(id)) throw new Error("GitHub Release enumeration contains a duplicate ID") + seenIds.add(id) + + let marker = null + try { + marker = parseReleaseMarker(release.body) + } catch { + marker = null + } + const exactMarker = markerMatches(marker, APPROVED_CANDIDATE) + const exactTag = release.tag_name === APPROVED_CANDIDATE.tag + const incidentId = INCIDENT_IDS.includes(id) + const suspiciousMarkerBody = isSuspiciousMarkerBody(release.body, APPROVED_CANDIDATE) + const partialMarker = markerSharesCandidateIdentity(marker, APPROVED_CANDIDATE) + const managed = exactMarker || exactTag || incidentId || partialMarker || suspiciousMarkerBody + + if (!managed) { + entries.push({ index, id, classification: "unrelated", release }) + continue + } + if (!exactMarker) { + throw new Error("Managed candidate Release marker or tag identity is malformed or ambiguous") + } + if (release.draft !== true || release.immutable !== false || release.published_at !== null) { + throw new Error("Managed candidate Release is published or not an exact draft") + } + if (!expected.has(id)) { + throw new Error( + "Release enumeration contains a managed candidate contrary to the authority stage", + ) + } + selected.set(id, release) + entries.push({ index, id, classification: "managed", release }) + } + + if (selected.size !== rule.releaseIds.length || rule.releaseIds.some((id) => !selected.has(id))) { + throw new Error("Release enumeration is missing an exact remaining managed candidate draft") + } + const enumerationRecord = { + stage, + candidate: APPROVED_CANDIDATE, + expectedReleaseIds: rule.releaseIds, + entries, + } + return deepFreeze({ + selected: rule.releaseIds.map((id) => selected.get(id)), + enumerationSha256: canonicalRecordSha256(enumerationRecord), + }) +} + +function assertApprovedProposalIdentity(proposal) { + if ( + proposal === null || + typeof proposal !== "object" || + proposal.candidate?.version !== APPROVED_CANDIDATE.version || + proposal.candidate?.commitSha !== APPROVED_CANDIDATE.commitSha || + proposal.candidate?.tag !== APPROVED_CANDIDATE.tag || + proposal.roles?.survivor !== SURVIVOR_ID || + !Array.isArray(proposal.roles?.duplicates) || + proposal.roles.duplicates.length !== DUPLICATE_IDS.length || + proposal.roles.duplicates.some((id, index) => id !== DUPLICATE_IDS[index]) || + !Array.isArray(proposal.releases) || + proposal.releases.length !== INCIDENT_IDS.length || + proposal.releases.some(({ id }, index) => id !== INCIDENT_IDS[index]) + ) { + throw new Error("Release classification proposal is not the approved incident identity") + } +} + +function markerMatches(marker, candidate) { + return ( + marker !== null && + marker.version === candidate.version && + marker.commitSha === candidate.commitSha && + marker.tag === candidate.tag + ) +} + +function markerSharesCandidateIdentity(marker, candidate) { + return ( + marker !== null && + (marker.version === candidate.version || + marker.commitSha === candidate.commitSha || + marker.tag === candidate.tag) + ) +} + +function isSuspiciousMarkerBody(body, candidate) { + return ( + typeof body === "string" && + (body.includes(DAWN_MARKER_IDENTITY) || + body.includes(candidate.commitSha) || + body.includes(candidate.tag)) + ) +} + +function canonicalId(value, label) { + const id = typeof value === "number" ? String(value) : value + if (typeof id !== "string" || !ID_PATTERN.test(id)) { + throw new TypeError(`${label} must be a canonical positive decimal id`) + } + return id +} + +function deepFreeze(value) { + if (value !== null && typeof value === "object" && !Object.isFrozen(value)) { + for (const child of Object.values(value)) deepFreeze(child) + Object.freeze(value) + } + return value +} diff --git a/scripts/release/duplicate-draft-consolidation-schema.mjs b/scripts/release/duplicate-draft-consolidation-schema.mjs new file mode 100644 index 000000000..1b6e5e1dc --- /dev/null +++ b/scripts/release/duplicate-draft-consolidation-schema.mjs @@ -0,0 +1,1386 @@ +import { createHash } from "node:crypto" +import { types as utilTypes } from "node:util" + +import { RELEASE_PAYLOAD_LIMITS } from "./limits.mjs" +import { CANONICAL_RELEASE_PACKAGE_ORDER } from "./manifest.mjs" + +const MEBIBYTE = 1024 * 1024 + +export const DUPLICATE_DRAFT_CONSOLIDATION_LIMITS = Object.freeze({ + proposedBytes: 4 * MEBIBYTE, + journalBytes: 72 * MEBIBYTE, + finalReceiptBytes: 96 * MEBIBYTE, + authorityStageBytes: 8 * MEBIBYTE, + survivorEvidenceBytes: 2 * MEBIBYTE, + journalEventReserveBytes: 8 * MEBIBYTE, + envelopeReserveBytes: MEBIBYTE, + maximumDeleteAttempts: 3, + maximumTargets: 2, + maximumOrphanAuthorityRecoveries: 1, + maximumAssetDownloads: 135, +}) + +if ( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes < + (DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumTargets * + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumDeleteAttempts + + 1 + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumOrphanAuthorityRecoveries) * + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalEventReserveBytes || + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes < + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.survivorEvidenceBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.envelopeReserveBytes || + RELEASE_PAYLOAD_LIMITS.escrowBytes <= 0 +) { + throw new Error("Duplicate-draft consolidation limits do not preserve required headroom") +} + +const ENVELOPE_FIELDS = Object.freeze(["record", "recordSha256"]) +const EVENT_ENVELOPE_FIELDS = Object.freeze(["event", "eventSha256"]) +const EVENT_FIELDS = Object.freeze([ + "schemaVersion", + "sequence", + "previousEventSha256", + "type", + "recordedAt", + "payload", +]) +const AUTHORITY_EVENT_TYPES = new Set(["delete-authority-observed", "final-authority-observed"]) +const WORKFLOW_STATUSES = Object.freeze([ + "in_progress", + "pending", + "queued", + "requested", + "waiting", +]) +const APPROVED_SURVIVOR_ID = "379991871" +const APPROVED_DUPLICATE_IDS = Object.freeze(["379982100", "379986168"]) +const INSPECT_STAGES = Object.freeze(["inspect-initial", "inspect-ready"]) +const PERFORM_STAGES = new Set(["perform-initial", "pre-delete-1", "pre-delete-2", "final"]) +const RECORD_FIELDS = Object.freeze({ + proposed: Object.freeze([ + "schemaVersion", + "repository", + "controller", + "candidate", + "roles", + "confirmation", + "annotatedTag", + "workflowAuthority", + "npmInventories", + "releases", + "payloadProof", + "inspectedAt", + ]), + journal: Object.freeze([ + "schemaVersion", + "repository", + "candidate", + "proposedRecordSha256", + "confirmationSha256", + "deletionOrder", + "events", + "updatedAt", + ]), + final: Object.freeze([ + "schemaVersion", + "proposedEnvelope", + "journalEnvelope", + "finalAuthority", + "finalSurvivor", + "completedAt", + ]), +}) +const KIND_LIMITS = Object.freeze({ + proposed: DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes, + journal: DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + final: DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes, +}) +const SHA256_PATTERN = /^[0-9a-f]{64}$/u +const GIT_SHA_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u +const POSITIVE_DECIMAL_PATTERN = /^[1-9][0-9]*$/u +const VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u +const TIMESTAMP_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/u +const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true }) +const CANONICAL_BUDGETS = [] + +export function createConsolidationEnvelope(kind, record) { + return withCanonicalBudget(kindLimit(kind), `${kind} envelope`, () => { + chargeCanonicalBytes( + Buffer.byteLength('{"record":', "utf8") + + Buffer.byteLength(`,"recordSha256":"${"0".repeat(64)}"}\n`, "utf8"), + ) + const normalized = normalizeRecord(kind, record) + const envelope = { + record: normalized, + recordSha256: canonicalRecordSha256(normalized), + } + assertWithinKindLimit(kind, Buffer.byteLength(`${JSON.stringify(envelope)}\n`, "utf8")) + return envelope + }) +} + +export function canonicalConsolidationEnvelopeBytes(kind, envelope) { + const normalized = normalizeEnvelope(kind, envelope) + const bytes = Buffer.from(`${JSON.stringify(normalized)}\n`, "utf8") + assertWithinKindLimit(kind, bytes.byteLength) + return bytes +} + +export function parseConsolidationEnvelope(kind, bytes) { + const maximum = kindLimit(kind) + if (!(bytes instanceof Uint8Array)) throw new TypeError("Envelope bytes must be a byte array") + if (bytes.byteLength > maximum) throw new Error(`${kind} envelope exceeds its byte limit`) + if (bytes.byteLength >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) { + throw new TypeError("Envelope must not contain a UTF-8 byte-order mark") + } + + let source + try { + source = UTF8_DECODER.decode(bytes) + } catch { + throw new TypeError("Envelope is not valid UTF-8") + } + let value + try { + value = JSON.parse(source) + } catch { + throw new TypeError("Envelope is not valid JSON") + } + const normalized = normalizeEnvelope(kind, value) + const canonical = `${JSON.stringify(normalized)}\n` + if (source !== canonical) { + throw new TypeError("Envelope bytes are not canonical") + } + return normalized +} + +export function canonicalRecordSha256(record) { + const source = JSON.stringify(record) + if (source === undefined) throw new TypeError("Record is not JSON serializable") + return createHash("sha256").update(`${source}\n`, "utf8").digest("hex") +} + +export function canonicalEventEnvelope(event, previousEventSha256) { + return withCanonicalBudget( + journalEventEnvelopeBudget(ownDataDiscriminator(event, "type")), + "journal event envelope", + () => { + chargeCanonicalBytes( + Buffer.byteLength('{"event":', "utf8") + + Buffer.byteLength(`,"eventSha256":"${"0".repeat(64)}"}`, "utf8"), + ) + const expectedSequence = assertPositiveInteger( + ownDataDiscriminator(event, "sequence"), + "Journal event sequence", + ) + const normalizedEvent = normalizeEvent(event, expectedSequence, previousEventSha256) + return { + event: normalizedEvent, + eventSha256: canonicalRecordSha256(normalizedEvent), + } + }, + ) +} + +export function parseJournalEventEnvelope(value, expectedSequence, previousEventSha256) { + return withCanonicalBudget( + journalEventEnvelopeBudget(ownDataDiscriminator(ownDataDiscriminator(value, "event"), "type")), + "journal event envelope", + () => { + value = assertExactFields(value, EVENT_ENVELOPE_FIELDS, "Journal event envelope") + const event = normalizeEvent(value.event, expectedSequence, previousEventSha256) + const eventSha256 = assertSha256(value.eventSha256, "Journal event digest") + if (eventSha256 !== canonicalRecordSha256(event)) { + throw new TypeError("Journal event digest does not match its canonical event") + } + return { event, eventSha256 } + }, + ) +} + +function normalizeEnvelope(kind, value) { + return withCanonicalBudget(kindLimit(kind), `${kind} envelope`, () => { + chargeCurrentCanonicalBytes(1) + value = assertExactFields(value, ENVELOPE_FIELDS, `${kind} envelope`) + const record = normalizeRecord(kind, value.record) + const recordSha256 = assertSha256(value.recordSha256, `${kind} record digest`) + if (recordSha256 !== canonicalRecordSha256(record)) { + throw new TypeError(`${kind} envelope digest does not match its canonical record`) + } + const normalized = { record, recordSha256 } + assertCanonicalValueByteLength(normalized, kindLimit(kind), `${kind} envelope`) + return normalized + }) +} + +function normalizeRecord(kind, value) { + return withCanonicalBudget(kindLimit(kind), `${kind} record`, () => { + if (kind === "proposed") return normalizeProposedRecord(value) + if (kind === "journal") return normalizeJournalRecord(value) + return normalizeFinalRecord(value) + }) +} + +function normalizeProposedRecord(value) { + value = assertExactFields(value, RECORD_FIELDS.proposed, "Proposed record") + assertSchemaVersion(value.schemaVersion, "Proposed record") + const repository = normalizeRepository(value.repository) + const controller = normalizeController(value.controller) + const candidate = normalizeCandidate(value.candidate) + const roles = normalizeRoles(value.roles) + const confirmation = normalizeConfirmation(value.confirmation) + const annotatedTag = normalizeAnnotatedTag(value.annotatedTag) + const workflowAuthority = normalizeWorkflowAuthority(value.workflowAuthority) + const npmInventories = assertArray(value.npmInventories, "Proposed npm inventories", { + exactLength: 2, + }).map(normalizeNpmInventory) + const releases = assertArray(value.releases, "Proposed releases", { + exactLength: 3, + }).map(normalizeReleaseEvidence) + const payloadProof = normalizePayloadProof(value.payloadProof) + const inspectedAt = assertTimestamp(value.inspectedAt, "Proposed inspection timestamp") + + assertArrayEqual( + npmInventories.map(({ stage }) => stage), + INSPECT_STAGES, + "Proposed npm inventory stages", + ) + for (const inventory of npmInventories) { + if (inventory.packages.some(({ version }) => version !== candidate.version)) { + throw new TypeError("Proposed npm observations must identify the candidate version") + } + } + assertIdentityContract(candidate, roles, confirmation) + if (annotatedTag.name !== candidate.tag || annotatedTag.targetSha !== candidate.commitSha) { + throw new TypeError("Annotated tag does not identify the proposed candidate") + } + const expectedReleaseIds = [roles.survivor, ...roles.duplicates] + assertArrayEqual( + releases.map(({ id }) => id), + expectedReleaseIds, + "Proposed release order", + ) + assertArrayEqual( + releases.map(({ role }) => role), + ["survivor", ...roles.duplicates.map(() => "duplicate")], + "Proposed release roles", + ) + if ( + releases.reduce((total, release) => total + release.assets.length, 0) > + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumAssetDownloads + ) { + throw new TypeError("Proposed release evidence exceeds the asset download limit") + } + assertPayloadMatchesReleases(payloadProof, releases) + + return { + schemaVersion: 1, + repository, + controller, + candidate, + roles, + confirmation, + annotatedTag, + workflowAuthority, + npmInventories, + releases, + payloadProof, + inspectedAt, + } +} + +function normalizeJournalRecord(value) { + value = assertExactFields(value, RECORD_FIELDS.journal, "Journal record") + assertSchemaVersion(value.schemaVersion, "Journal record") + const repository = normalizeRepository(value.repository) + const candidate = normalizeCandidate(value.candidate) + const proposedRecordSha256 = assertSha256( + value.proposedRecordSha256, + "Journal proposed record digest", + ) + const confirmationSha256 = assertSha256(value.confirmationSha256, "Journal confirmation digest") + const deletionOrder = normalizeIdentityArray( + value.deletionOrder, + "Journal deletion order", + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumTargets, + ) + if (deletionOrder.length !== DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumTargets) { + throw new TypeError("Journal deletion order must contain exactly two targets") + } + assertArrayEqual(deletionOrder, APPROVED_DUPLICATE_IDS, "Approved journal deletion order") + const rawEvents = assertArray(value.events, "Journal events", { + maximumLength: Math.floor(DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes / 64), + }) + const events = [] + let previousEventSha256 = null + for (let index = 0; index < rawEvents.length; index += 1) { + const envelope = parseJournalEventEnvelope(rawEvents[index], index + 1, previousEventSha256) + validateJournalEventTarget(envelope.event, deletionOrder) + events.push(envelope) + previousEventSha256 = envelope.eventSha256 + } + const intentCounts = new Map() + for (const { event } of events) { + if (event.type !== "delete-intent") continue + const count = (intentCounts.get(event.payload.targetReleaseId) ?? 0) + 1 + if (count > DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumDeleteAttempts) { + throw new TypeError("Journal target exceeds the maximum number of delete intents") + } + intentCounts.set(event.payload.targetReleaseId, count) + } + if (events.length > 0) { + const first = events[0].event + if ( + first.type !== "operation-started" || + first.payload.proposedRecordSha256 !== proposedRecordSha256 || + first.payload.confirmationSha256 !== confirmationSha256 + ) { + throw new TypeError("Journal must begin with its bound operation-started event") + } + assertArrayEqual(first.payload.deletionOrder, deletionOrder, "Journal operation deletion order") + } + const updatedAt = assertTimestamp(value.updatedAt, "Journal update timestamp") + return { + schemaVersion: 1, + repository, + candidate, + proposedRecordSha256, + confirmationSha256, + deletionOrder, + events, + updatedAt, + } +} + +function normalizeFinalRecord(value) { + value = assertExactFields(value, RECORD_FIELDS.final, "Final receipt record") + assertSchemaVersion(value.schemaVersion, "Final receipt record") + const proposedEnvelope = normalizeEnvelope("proposed", value.proposedEnvelope) + const journalEnvelope = normalizeEnvelope("journal", value.journalEnvelope) + if (journalEnvelope.record.proposedRecordSha256 !== proposedEnvelope.recordSha256) { + throw new TypeError("Final receipt journal does not bind the embedded proposal") + } + const finalAuthority = normalizeAuthorityStage(value.finalAuthority) + if (finalAuthority.stage !== "final" || finalAuthority.targetRead !== null) { + throw new TypeError("Final receipt authority must be the final no-target-read stage") + } + const finalSurvivor = normalizeReleaseEvidence(value.finalSurvivor) + if ( + finalAuthority.releases.length !== 1 || + finalAuthority.releases[0].role !== "survivor" || + JSON.stringify(finalAuthority.releases[0]) !== JSON.stringify(finalSurvivor) + ) { + throw new TypeError("Final survivor must exactly match the final authority survivor") + } + const completedAt = assertTimestamp(value.completedAt, "Final receipt completion timestamp") + return { + schemaVersion: 1, + proposedEnvelope, + journalEnvelope, + finalAuthority, + finalSurvivor, + completedAt, + } +} + +function normalizeRepository(value) { + value = assertExactFields(value, ["name", "id", "defaultBranch", "actor"], "Repository") + return { + name: assertNonemptyString(value.name, "Repository name"), + id: assertId(value.id, "Repository id"), + defaultBranch: assertNonemptyString(value.defaultBranch, "Repository default branch"), + actor: normalizeActor(value.actor, "Repository actor"), + } +} + +function normalizeActor(value, label) { + value = assertExactFields(value, ["login", "id"], label) + return { + login: assertNonemptyString(value.login, `${label} login`), + id: assertId(value.id, `${label} id`), + } +} + +function normalizeController(value) { + value = assertExactFields(value, ["headSha", "originMainSha", "githubMainSha"], "Controller") + const headSha = assertGitSha(value.headSha, "Controller head SHA") + const originMainSha = assertGitSha(value.originMainSha, "Controller origin/main SHA") + const githubMainSha = assertGitSha(value.githubMainSha, "Controller GitHub main SHA") + if (headSha !== originMainSha || headSha !== githubMainSha) { + throw new TypeError("Controller SHAs must be identical") + } + return { headSha, originMainSha, githubMainSha } +} + +function normalizeCandidate(value) { + value = assertExactFields(value, ["version", "commitSha", "tag"], "Candidate") + const version = assertMatchingString(value.version, VERSION_PATTERN, "Candidate version") + const commitSha = assertGitSha(value.commitSha, "Candidate commit SHA") + const tag = assertNonemptyString(value.tag, "Candidate tag") + if (tag !== `v${version}`) throw new TypeError("Candidate tag must match its version") + return { version, commitSha, tag } +} + +function normalizeRoles(value) { + value = assertExactFields(value, ["survivor", "duplicates"], "Release roles") + const survivor = assertId(value.survivor, "Survivor Release id") + const duplicates = normalizeIdentityArray( + value.duplicates, + "Duplicate Release ids", + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumTargets, + ) + if ( + duplicates.length !== DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumTargets || + duplicates.includes(survivor) || + survivor !== APPROVED_SURVIVOR_ID + ) { + throw new TypeError("Release roles must identify one survivor and two ordered duplicates") + } + assertArrayEqual(duplicates, APPROVED_DUPLICATE_IDS, "Approved duplicate Release order") + return { survivor, duplicates } +} + +function normalizeConfirmation(value) { + value = assertExactFields( + value, + ["version", "commitSha", "survivor", "duplicates", "template"], + "Confirmation", + ) + const template = assertNonemptyString(value.template, "Confirmation template") + if ((template.match(/<64-lowercase-hex-digest>/gu) ?? []).length !== 1) { + throw new TypeError("Confirmation template must retain the digest placeholder") + } + return { + version: assertMatchingString(value.version, VERSION_PATTERN, "Confirmation version"), + commitSha: assertGitSha(value.commitSha, "Confirmation commit SHA"), + survivor: assertId(value.survivor, "Confirmation survivor id"), + duplicates: normalizeIdentityArray( + value.duplicates, + "Confirmation duplicate ids", + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumTargets, + ), + template, + } +} + +function normalizeAnnotatedTag(value) { + value = assertExactFields( + value, + ["name", "objectSha", "targetSha", "objectType", "observedAt"], + "Annotated tag", + ) + const objectType = assertNonemptyString(value.objectType, "Annotated tag object type") + if (objectType !== "tag") throw new TypeError("Candidate tag must be an annotated tag object") + return { + name: assertNonemptyString(value.name, "Annotated tag name"), + objectSha: assertGitSha(value.objectSha, "Annotated tag object SHA"), + targetSha: assertGitSha(value.targetSha, "Annotated tag target SHA"), + objectType, + observedAt: assertTimestamp(value.observedAt, "Annotated tag observation timestamp"), + } +} + +function normalizeWorkflowAuthority(value) { + value = assertExactFields( + value, + ["workflowId", "path", "state", "query", "nonterminalRuns", "observedAt"], + "Workflow authority", + ) + const query = assertExactFields( + value.query, + ["statuses", "perPage", "maximumPages"], + "Workflow query", + ) + const statuses = assertArray(query.statuses, "Workflow query statuses", { + exactLength: WORKFLOW_STATUSES.length, + }).map((status) => assertNonemptyString(status, "Workflow query status")) + assertArrayEqual(statuses, WORKFLOW_STATUSES, "Workflow query statuses") + if (query.perPage !== 100 || query.maximumPages !== 100) { + throw new TypeError("Workflow query bounds must be the reviewed 100-by-100 values") + } + const nonterminalRuns = assertArray(value.nonterminalRuns, "Nonterminal workflow runs", { + exactLength: 0, + }).map(normalizeWorkflowRun) + if (nonterminalRuns.length !== 0) throw new TypeError("Nonterminal workflow runs must be empty") + if (value.path !== ".github/workflows/release.yml" || value.state !== "disabled_manually") { + throw new TypeError("Release workflow must remain disabled at its canonical path") + } + return { + workflowId: assertId(value.workflowId, "Workflow id"), + path: value.path, + state: value.state, + query: { statuses, perPage: 100, maximumPages: 100 }, + nonterminalRuns, + observedAt: assertTimestamp(value.observedAt, "Workflow authority observation timestamp"), + } +} + +function normalizeWorkflowRun(value) { + value = assertExactFields( + value, + ["id", "runAttempt", "status", "event", "headSha", "headBranch"], + "Workflow run", + ) + return { + id: assertId(value.id, "Workflow run id"), + runAttempt: assertPositiveInteger(value.runAttempt, "Workflow run attempt"), + status: assertNonemptyString(value.status, "Workflow run status"), + event: assertNonemptyString(value.event, "Workflow run event"), + headSha: assertGitSha(value.headSha, "Workflow run head SHA"), + headBranch: assertNonemptyString(value.headBranch, "Workflow run head branch"), + } +} + +function normalizeNpmInventory(value) { + value = assertExactFields( + value, + ["stage", "startedAt", "completedAt", "packages"], + "npm inventory", + ) + const packages = assertArray(value.packages, "npm package observations", { + exactLength: CANONICAL_RELEASE_PACKAGE_ORDER.length, + }).map(normalizeNpmObservation) + assertArrayEqual( + packages.map(({ name }) => name), + CANONICAL_RELEASE_PACKAGE_ORDER, + "npm package order", + ) + return { + stage: assertNonemptyString(value.stage, "npm inventory stage"), + startedAt: assertTimestamp(value.startedAt, "npm inventory start timestamp"), + completedAt: assertTimestamp(value.completedAt, "npm inventory completion timestamp"), + packages, + } +} + +function normalizeNpmObservation(value) { + value = assertExactFields( + value, + ["name", "version", "status", "httpStatus", "code", "observedAt"], + "npm package observation", + ) + if (value.status !== "ABSENT" || value.httpStatus !== 404 || value.code !== "E404") { + throw new TypeError("npm package observation must be exact ABSENT/404/E404 evidence") + } + return { + name: assertNonemptyString(value.name, "npm package name"), + version: assertMatchingString(value.version, VERSION_PATTERN, "npm package version"), + status: value.status, + httpStatus: value.httpStatus, + code: value.code, + observedAt: assertTimestamp(value.observedAt, "npm observation timestamp"), + } +} + +function normalizeReleaseEvidence(value) { + const role = ownDataDiscriminator(value, "role") + if (role === "survivor") { + return withCanonicalBudget( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.survivorEvidenceBytes, + "survivor evidence", + () => normalizeReleaseEvidenceValue(value), + ) + } + return normalizeReleaseEvidenceValue(value) +} + +function normalizeReleaseEvidenceValue(value) { + value = assertExactFields( + value, + ["role", "id", "nodeId", "tagName", "createdAt", "updatedAt", "semantic", "assets"], + "Release evidence", + ) + if (!new Set(["survivor", "duplicate"]).has(value.role)) { + throw new TypeError("Release evidence has an invalid role") + } + const assets = assertArray(value.assets, "Release assets", { + exactLength: 45, + }).map(normalizeAssetEvidence) + if (assets.length !== 45 || new Set(assets.map(({ id }) => id)).size !== 45) { + throw new TypeError("Release evidence must contain exactly 45 uniquely identified assets") + } + assertUniqueNames( + assets.map(({ name }) => name), + "Release asset names", + ) + const aggregateSize = assets.reduce((total, { size }) => total + size, 0) + if (!Number.isSafeInteger(aggregateSize) || aggregateSize > RELEASE_PAYLOAD_LIMITS.escrowBytes) { + throw new TypeError("Release asset evidence exceeds the escrow payload limit") + } + const normalized = { + role: value.role, + id: assertId(value.id, "Release id"), + nodeId: assertNonemptyString(value.nodeId, "Release node id"), + tagName: assertNonemptyString(value.tagName, "Release tag name"), + createdAt: assertTimestamp(value.createdAt, "Release creation timestamp"), + updatedAt: assertTimestamp(value.updatedAt, "Release update timestamp"), + semantic: normalizeReleaseSemantic(value.semantic), + assets, + } + if (normalized.role === "survivor") { + assertCanonicalValueByteLength( + normalized, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.survivorEvidenceBytes, + "Survivor Release evidence", + ) + } + return normalized +} + +function normalizeReleaseSemantic(value) { + value = assertExactFields( + value, + [ + "name", + "targetCommitish", + "draft", + "immutable", + "prerelease", + "publishedAt", + "body", + "bodySha256", + "author", + ], + "Release semantic evidence", + ) + if ( + value.draft !== true || + value.immutable !== false || + value.prerelease !== false || + value.publishedAt !== null + ) { + throw new TypeError("Managed duplicate Release must remain a mutable non-prerelease draft") + } + const body = assertString(value.body, "Release body") + const bodySha256 = assertSha256(value.bodySha256, "Release body digest") + const targetCommitish = assertString(value.targetCommitish, "Release target commitish") + if (targetCommitish !== "main") { + throw new TypeError('Release target commitish must be exactly "main"') + } + return { + name: assertNonemptyString(value.name, "Release name"), + targetCommitish, + draft: value.draft, + immutable: value.immutable, + prerelease: value.prerelease, + publishedAt: null, + body, + bodySha256, + author: normalizeServiceIdentity(value.author, "Release author"), + } +} + +function normalizeServiceIdentity(value, label) { + value = assertExactFields(value, ["login", "id", "nodeId"], label) + return { + login: assertNonemptyString(value.login, `${label} login`), + id: assertId(value.id, `${label} id`), + nodeId: assertNonemptyString(value.nodeId, `${label} node id`), + } +} + +function normalizeAssetEvidence(value) { + value = assertExactFields( + value, + [ + "id", + "nodeId", + "name", + "label", + "state", + "contentType", + "size", + "digest", + "uploader", + "createdAt", + "updatedAt", + "downloadCount", + "downloadSha256", + ], + "Asset evidence", + ) + const digest = assertNonemptyString(value.digest, "Asset service digest") + if (!/^sha256:[0-9a-f]{64}$/u.test(digest)) { + throw new TypeError("Asset service digest must be canonical sha256 evidence") + } + const name = assertNonemptyString(value.name, "Asset name") + if (Buffer.byteLength(name, "utf8") > RELEASE_PAYLOAD_LIMITS.archiveFilenameBytes) { + throw new TypeError("Asset name exceeds the release archive filename limit") + } + const size = assertNonnegativeInteger(value.size, "Asset size") + if (size > RELEASE_PAYLOAD_LIMITS.tarballBytes) { + throw new TypeError("Asset size exceeds the release per-asset payload limit") + } + return { + id: assertId(value.id, "Asset id"), + nodeId: assertNonemptyString(value.nodeId, "Asset node id"), + name, + label: value.label === null ? null : assertString(value.label, "Asset label"), + state: assertNonemptyString(value.state, "Asset state"), + contentType: assertNonemptyString(value.contentType, "Asset content type"), + size, + digest, + uploader: normalizeServiceIdentity(value.uploader, "Asset uploader"), + createdAt: assertTimestamp(value.createdAt, "Asset creation timestamp"), + updatedAt: assertTimestamp(value.updatedAt, "Asset update timestamp"), + downloadCount: assertNonnegativeInteger(value.downloadCount, "Asset download count"), + downloadSha256: assertSha256(value.downloadSha256, "Downloaded asset digest"), + } +} + +function normalizePayloadProof(value) { + value = assertExactFields( + value, + ["baseAssetSet", "baseAssetSetSha256", "consolidationPayloadSha256", "attestationVerification"], + "Payload proof", + ) + const baseAssetSet = assertArray(value.baseAssetSet, "Base asset set", { + exactLength: 45, + }).map(normalizeNamedDigest) + if (baseAssetSet.length !== 45) + throw new TypeError("Base asset set must contain exactly 45 assets") + assertUniqueNames( + baseAssetSet.map(({ name }) => name), + "Base asset set names", + ) + const attestationVerification = assertExactFields( + value.attestationVerification, + ["status", "subjects"], + "Attestation verification", + ) + if (attestationVerification.status !== "VERIFIED") { + throw new TypeError("Attestation verification must be VERIFIED") + } + const subjects = assertArray(attestationVerification.subjects, "Attestation subjects", { + exactLength: 22, + }).map(normalizeNamedDigest) + if (subjects.length !== 22) throw new TypeError("Attestation verification requires 22 subjects") + assertUniqueNames( + subjects.map(({ name }) => name), + "Attestation subject names", + ) + return { + baseAssetSet, + baseAssetSetSha256: assertSha256(value.baseAssetSetSha256, "Base asset set digest"), + consolidationPayloadSha256: assertSha256( + value.consolidationPayloadSha256, + "Consolidation payload digest", + ), + attestationVerification: { status: "VERIFIED", subjects }, + } +} + +function normalizeNamedDigest(value) { + value = assertExactFields(value, ["name", "sha256"], "Named digest") + return { + name: assertNonemptyString(value.name, "Named digest name"), + sha256: assertSha256(value.sha256, "Named digest SHA-256"), + } +} + +function normalizeAuthorityStage(value) { + return withCanonicalBudget( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes, + "authority stage", + () => { + chargeCurrentCanonicalBytes(1) + return normalizeAuthorityStageValue(value) + }, + ) +} + +function normalizeAuthorityStageValue(value) { + value = assertExactFields( + value, + [ + "stage", + "controller", + "annotatedTag", + "workflowAuthority", + "npmInventory", + "releases", + "payloadProof", + "targetRead", + "observedAt", + ], + "Authority stage", + ) + const stage = assertNonemptyString(value.stage, "Authority stage name") + if (!PERFORM_STAGES.has(stage)) throw new TypeError("Authority stage name is invalid") + const npmInventory = normalizeNpmInventory(value.npmInventory) + if (npmInventory.stage !== stage) + throw new TypeError("Authority stage and npm inventory stage differ") + const releases = assertArray(value.releases, "Authority releases", { + maximumLength: 3, + }).map(normalizeReleaseEvidence) + if ( + releases.reduce((total, release) => total + release.assets.length, 0) > + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumAssetDownloads + ) { + throw new TypeError("Authority stage exceeds the asset download limit") + } + if (releases.length === 0 || releases[0].role !== "survivor") { + throw new TypeError("Authority releases must begin with the survivor") + } + const payloadProof = normalizePayloadProof(value.payloadProof) + assertPayloadMatchesReleases(payloadProof, releases) + const targetRead = value.targetRead === null ? null : normalizeTargetRead(value.targetRead) + if ((stage === "final") !== (targetRead === null)) { + throw new TypeError("targetRead is null only for final authority") + } + if (stage === "final" && releases.length !== 1) { + throw new TypeError("Final authority must contain only the survivor") + } + if ( + targetRead !== null && + !releases.some( + (release) => + release.id === targetRead.evidence.id && + JSON.stringify(release) === JSON.stringify(targetRead.evidence), + ) + ) { + throw new TypeError("Target read evidence must exactly match an authority Release") + } + const normalized = { + stage, + controller: normalizeController(value.controller), + annotatedTag: normalizeAnnotatedTag(value.annotatedTag), + workflowAuthority: normalizeWorkflowAuthority(value.workflowAuthority), + npmInventory, + releases, + payloadProof, + targetRead, + observedAt: assertTimestamp(value.observedAt, "Authority observation timestamp"), + } + assertCanonicalValueByteLength( + normalized, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes, + "Authority stage", + ) + return normalized +} + +function normalizeTargetRead(value) { + value = assertExactFields( + value, + [ + "releaseGetStartedAt", + "releaseGetCompletedAt", + "assetsListStartedAt", + "assetsListCompletedAt", + "evidence", + "evidenceSha256", + ], + "Target read", + ) + const evidence = normalizeReleaseEvidence(value.evidence) + const evidenceSha256 = assertSha256(value.evidenceSha256, "Target read evidence digest") + if (evidenceSha256 !== canonicalRecordSha256(evidence)) { + throw new TypeError("Target read evidence digest does not match") + } + const timestamps = [ + assertTimestamp(value.releaseGetStartedAt, "Release GET start timestamp"), + assertTimestamp(value.releaseGetCompletedAt, "Release GET completion timestamp"), + assertTimestamp(value.assetsListStartedAt, "Asset list start timestamp"), + assertTimestamp(value.assetsListCompletedAt, "Asset list completion timestamp"), + ] + for (let index = 1; index < timestamps.length; index += 1) { + if (timestamps[index] < timestamps[index - 1]) { + throw new TypeError("Target read timestamps must be monotone") + } + } + return { + releaseGetStartedAt: timestamps[0], + releaseGetCompletedAt: timestamps[1], + assetsListStartedAt: timestamps[2], + assetsListCompletedAt: timestamps[3], + evidence, + evidenceSha256, + } +} + +function normalizeEvent(value, expectedSequence, previousEventSha256) { + value = assertExactFields(value, EVENT_FIELDS, "Journal event") + assertSchemaVersion(value.schemaVersion, "Journal event") + const sequence = assertPositiveInteger(expectedSequence, "Expected journal event sequence") + if (value.sequence !== sequence) throw new TypeError("Journal event sequence is not contiguous") + const expectedPrevious = + previousEventSha256 === null + ? null + : assertSha256(previousEventSha256, "Previous journal event digest") + if (value.previousEventSha256 !== expectedPrevious) { + throw new TypeError("Journal event does not bind the immediately previous digest") + } + if (sequence === 1 && expectedPrevious !== null) { + throw new TypeError("First journal event must have no previous digest") + } + if (sequence > 1 && expectedPrevious === null) { + throw new TypeError("Later journal events must bind a previous digest") + } + const type = assertNonemptyString(value.type, "Journal event type") + return { + schemaVersion: 1, + sequence, + previousEventSha256: expectedPrevious, + type, + recordedAt: assertTimestamp(value.recordedAt, "Journal event timestamp"), + payload: normalizeEventPayload(type, value.payload), + } +} + +function normalizeEventPayload(type, value) { + if (type === "operation-started") { + value = assertExactFields( + value, + ["proposedRecordSha256", "confirmationSha256", "controllerSha", "deletionOrder"], + "operation-started payload", + ) + const deletionOrder = normalizeIdentityArray( + value.deletionOrder, + "Operation deletion order", + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumTargets, + ) + if (deletionOrder.length !== DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumTargets) { + throw new TypeError("Operation deletion order must contain two targets") + } + assertArrayEqual(deletionOrder, APPROVED_DUPLICATE_IDS, "Approved operation deletion order") + return { + proposedRecordSha256: assertSha256(value.proposedRecordSha256, "Proposed record digest"), + confirmationSha256: assertSha256(value.confirmationSha256, "Confirmation digest"), + controllerSha: assertGitSha(value.controllerSha, "Operation controller SHA"), + deletionOrder, + } + } + if (type === "npm-observed") { + value = assertExactFields( + value, + ["targetReleaseId", "attemptNumber", "inventory"], + "npm-observed payload", + ) + const inventory = normalizeNpmInventory(value.inventory) + if (!PERFORM_STAGES.has(inventory.stage)) { + throw new TypeError("npm-observed event has an invalid perform-stage inventory") + } + return { + targetReleaseId: assertId(value.targetReleaseId, "npm-observed target Release id"), + attemptNumber: assertDeleteAttempt(value.attemptNumber), + inventory, + } + } + if (type === "delete-authority-observed") { + value = assertExactFields( + value, + ["targetReleaseId", "attemptNumber", "authority"], + "delete-authority-observed payload", + ) + const targetReleaseId = assertId(value.targetReleaseId, "Authority target Release id") + const authority = normalizeAuthorityStage(value.authority) + if (authority.targetRead?.evidence.id !== targetReleaseId) { + throw new TypeError("Delete authority target read must identify the deletion target") + } + return { + targetReleaseId, + attemptNumber: assertDeleteAttempt(value.attemptNumber), + authority, + } + } + if (type === "delete-intent") { + value = assertExactFields( + value, + ["targetReleaseId", "attemptNumber", "authorityEventSha256"], + "delete-intent payload", + ) + return { + targetReleaseId: assertId(value.targetReleaseId, "Delete intent target Release id"), + attemptNumber: assertDeleteAttempt(value.attemptNumber), + authorityEventSha256: assertSha256( + value.authorityEventSha256, + "Delete authority event digest", + ), + } + } + if (type === "delete-outcome") return normalizeDeleteOutcome(value) + if (type === "resume-reconciliation") return normalizeResumeReconciliation(value) + if (type === "absence-converged") return normalizeAbsenceConverged(value) + if (type === "final-authority-observed") { + value = assertExactFields(value, ["authority"], "final-authority-observed payload") + const authority = normalizeAuthorityStage(value.authority) + if (authority.stage !== "final") + throw new TypeError("Final authority event must contain final authority") + return { authority } + } + throw new TypeError("Unknown journal event type") +} + +function normalizeDeleteOutcome(value) { + value = assertExactFields( + value, + ["targetReleaseId", "attemptNumber", "classification", "httpStatus", "observedAt"], + "delete-outcome payload", + ) + const triplets = new Map([ + ["confirmed-204", 204], + ["transport-ambiguous", null], + ["response-404-ambiguous", 404], + ]) + const hardFailure = + value.classification === "response-hard-failure" && + (value.httpStatus === null || + (Number.isInteger(value.httpStatus) && + value.httpStatus >= 100 && + value.httpStatus <= 599 && + value.httpStatus !== 204 && + value.httpStatus !== 404)) + if ( + !hardFailure && + (!triplets.has(value.classification) || triplets.get(value.classification) !== value.httpStatus) + ) { + throw new TypeError("Delete outcome classification and HTTP status are inconsistent") + } + return { + targetReleaseId: assertId(value.targetReleaseId, "Delete outcome target Release id"), + attemptNumber: assertDeleteAttempt(value.attemptNumber), + classification: value.classification, + httpStatus: value.httpStatus, + observedAt: assertTimestamp(value.observedAt, "Delete outcome timestamp"), + } +} + +function normalizeResumeReconciliation(value) { + value = assertExactFields( + value, + ["targetReleaseId", "attemptNumber", "classification", "releaseEvidence", "observedAt"], + "resume-reconciliation payload", + ) + const targetReleaseId = assertId(value.targetReleaseId, "Resume target Release id") + let releaseEvidence + if (value.classification === "present-unchanged-retryable") { + releaseEvidence = normalizeReleaseEvidence(value.releaseEvidence) + if (releaseEvidence.id !== targetReleaseId) { + throw new TypeError("Resume Release evidence does not identify its target") + } + } else if (value.classification === "absent-ambiguous" && value.releaseEvidence === null) { + releaseEvidence = null + } else { + throw new TypeError("Resume classification and Release evidence are inconsistent") + } + return { + targetReleaseId, + attemptNumber: assertDeleteAttempt(value.attemptNumber), + classification: value.classification, + releaseEvidence, + observedAt: assertTimestamp(value.observedAt, "Resume reconciliation timestamp"), + } +} + +function normalizeAbsenceConverged(value) { + value = assertExactFields( + value, + [ + "targetReleaseId", + "attemptNumber", + "basis", + "directGet404At", + "listAbsentAt", + "attempts", + "completedAt", + ], + "absence-converged payload", + ) + if (!new Set(["confirmed-204", "ambiguous"]).has(value.basis)) { + throw new TypeError("Absence convergence basis is invalid") + } + const attempts = assertPositiveInteger(value.attempts, "Absence convergence attempts") + if (attempts > 6) throw new TypeError("Absence convergence exceeds six read attempts") + return { + targetReleaseId: assertId(value.targetReleaseId, "Absence target Release id"), + attemptNumber: assertDeleteAttempt(value.attemptNumber), + basis: value.basis, + directGet404At: assertTimestamp(value.directGet404At, "Direct GET absence timestamp"), + listAbsentAt: assertTimestamp(value.listAbsentAt, "Release list absence timestamp"), + attempts, + completedAt: assertTimestamp(value.completedAt, "Absence convergence timestamp"), + } +} + +function validateJournalEventTarget(event, deletionOrder) { + const targetReleaseId = event.payload.targetReleaseId + if (targetReleaseId !== undefined && !deletionOrder.includes(targetReleaseId)) { + throw new TypeError("Journal event target is not in the fixed deletion order") + } +} + +function assertIdentityContract(candidate, roles, confirmation) { + if ( + candidate.version !== confirmation.version || + candidate.commitSha !== confirmation.commitSha || + roles.survivor !== confirmation.survivor + ) { + throw new TypeError("Confirmation does not bind the proposed candidate and survivor") + } + assertArrayEqual(confirmation.duplicates, roles.duplicates, "Confirmation duplicate identities") +} + +function assertPayloadMatchesReleases(payloadProof, releases) { + const expected = payloadProof.baseAssetSet + for (const release of releases) { + assertArrayEqual( + release.assets.map(({ name }) => name), + expected.map(({ name }) => name), + "Release assets and payload proof names", + ) + assertArrayEqual( + release.assets.map(({ downloadSha256 }) => downloadSha256), + expected.map(({ sha256 }) => sha256), + "Release assets and payload proof digests", + ) + } +} + +function normalizeIdentityArray(value, label, exactLength) { + const identities = assertArray(value, label, { exactLength }).map((entry) => + assertId(entry, label), + ) + if (new Set(identities).size !== identities.length) + throw new TypeError(`${label} contains duplicates`) + return identities +} + +function withCanonicalBudget(maximum, label, operation) { + const budget = { label, maximum, used: 0 } + CANONICAL_BUDGETS.push(budget) + try { + return operation() + } finally { + CANONICAL_BUDGETS.pop() + } +} + +function chargeCanonicalBytes(byteLength) { + if (!Number.isSafeInteger(byteLength) || byteLength < 0) { + throw new TypeError("Canonical byte accounting received an invalid length") + } + for (const budget of CANONICAL_BUDGETS) { + budget.used += byteLength + if (!Number.isSafeInteger(budget.used) || budget.used > budget.maximum) { + throw new TypeError(`Canonical value exceeds its cumulative ${budget.label} budget`) + } + } +} + +function chargeCurrentCanonicalBytes(byteLength) { + const current = CANONICAL_BUDGETS.at(-1) + if (current === undefined) return + current.used += byteLength + if (!Number.isSafeInteger(current.used) || current.used > current.maximum) { + throw new TypeError(`Canonical value exceeds its cumulative ${current.label} budget`) + } +} + +function chargeCanonicalPrimitive(value, arrayEntry) { + if (value !== null && typeof value === "object") return + let source + try { + source = JSON.stringify(value) + } catch { + throw new TypeError("Canonical value contains a non-JSON primitive") + } + if (source === undefined) { + if (arrayEntry) chargeCanonicalBytes(4) + return + } + chargeCanonicalBytes(Buffer.byteLength(source, "utf8")) +} + +function ownDataDiscriminator(value, field) { + if (value === null || typeof value !== "object" || utilTypes.isProxy(value)) { + return undefined + } + const descriptor = Object.getOwnPropertyDescriptor(value, field) + return descriptor !== undefined && "value" in descriptor ? descriptor.value : undefined +} + +function journalEventEnvelopeBudget(type) { + return AUTHORITY_EVENT_TYPES.has(type) + ? DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalEventReserveBytes + : DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalEventReserveBytes +} + +function assertExactFields(value, fields, label) { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + utilTypes.isProxy(value) + ) { + throw new TypeError(`${label} must be an object`) + } + const prototype = Object.getPrototypeOf(value) + if (prototype !== Object.prototype && prototype !== null) { + throw new TypeError(`${label} must be a plain object`) + } + const keys = Reflect.ownKeys(value) + if ( + keys.length !== fields.length || + keys.some((key) => typeof key !== "string" || !fields.includes(key)) + ) { + throw new TypeError(`${label} must contain exactly: ${fields.join(", ")}`) + } + const descriptors = Object.getOwnPropertyDescriptors(value) + const snapshot = {} + chargeCanonicalBytes(2 + Math.max(0, fields.length - 1)) + for (const field of fields) { + const descriptor = descriptors[field] + if (descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor)) { + throw new TypeError(`${label} fields must be enumerable data properties`) + } + snapshot[field] = descriptor.value + chargeCanonicalBytes(Buffer.byteLength(JSON.stringify(field), "utf8") + 1) + chargeCanonicalPrimitive(descriptor.value, false) + } + return snapshot +} + +function assertSchemaVersion(value, label) { + if (value !== 1) throw new TypeError(`${label} schemaVersion must be integer 1`) +} + +function assertArray(value, label, { exactLength, maximumLength } = {}) { + if ( + !Array.isArray(value) || + utilTypes.isProxy(value) || + Object.getPrototypeOf(value) !== Array.prototype + ) { + throw new TypeError(`${label} must be a dense array`) + } + const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length") + const length = lengthDescriptor?.value + if ( + !Number.isSafeInteger(length) || + length < 0 || + (exactLength !== undefined && length !== exactLength) || + (maximumLength !== undefined && length > maximumLength) + ) { + throw new TypeError(`${label} has an invalid cardinality`) + } + const keys = Reflect.ownKeys(value) + if (keys.length !== length + 1 || keys.at(-1) !== "length") { + throw new TypeError(`${label} must contain only canonical numeric indices`) + } + const descriptors = Object.getOwnPropertyDescriptors(value) + const snapshot = new Array(length) + chargeCanonicalBytes(2 + Math.max(0, length - 1)) + for (let index = 0; index < length; index += 1) { + const key = String(index) + if (keys[index] !== key) { + throw new TypeError(`${label} must contain every canonical numeric index`) + } + const descriptor = descriptors[key] + if (descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor)) { + throw new TypeError(`${label} entries must be enumerable data properties`) + } + snapshot[index] = descriptor.value + chargeCanonicalPrimitive(descriptor.value, true) + } + return snapshot +} + +function assertArrayEqual(actual, expected, label) { + if ( + actual.length !== expected.length || + actual.some((entry, index) => entry !== expected[index]) + ) { + throw new TypeError(`${label} is not canonical`) + } +} + +function assertUniqueNames(values, label) { + if (new Set(values).size !== values.length) throw new TypeError(`${label} must be unique`) +} + +function assertString(value, label) { + if (typeof value !== "string") throw new TypeError(`${label} must be a string`) + return value +} + +function assertNonemptyString(value, label) { + const string = assertString(value, label) + if (string.length === 0) throw new TypeError(`${label} must not be empty`) + return string +} + +function assertMatchingString(value, pattern, label) { + const string = assertNonemptyString(value, label) + if (!pattern.test(string)) throw new TypeError(`${label} is not canonical`) + return string +} + +function assertId(value, label) { + return assertMatchingString(value, POSITIVE_DECIMAL_PATTERN, label) +} + +function assertSha256(value, label) { + return assertMatchingString(value, SHA256_PATTERN, label) +} + +function assertGitSha(value, label) { + return assertMatchingString(value, GIT_SHA_PATTERN, label) +} + +function assertTimestamp(value, label) { + const timestamp = assertMatchingString(value, TIMESTAMP_PATTERN, label) + try { + const canonical = new Date(timestamp).toISOString() + if (canonical !== timestamp) { + throw new TypeError(`${label} is not a valid UTC instant`) + } + return canonical + } catch { + throw new TypeError(`${label} is not a valid UTC instant`) + } +} + +function assertPositiveInteger(value, label) { + if (!Number.isSafeInteger(value) || value < 1) + throw new TypeError(`${label} must be a positive integer`) + return value +} + +function assertNonnegativeInteger(value, label) { + if (!Number.isSafeInteger(value) || value < 0) { + throw new TypeError(`${label} must be a nonnegative integer`) + } + return value +} + +function assertDeleteAttempt(value) { + const attempt = assertPositiveInteger(value, "Delete attempt number") + if (attempt > DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.maximumDeleteAttempts) { + throw new TypeError("Delete attempt number exceeds the reviewed maximum") + } + return attempt +} + +function assertCanonicalValueByteLength(value, maximum, label) { + const byteLength = Buffer.byteLength(`${JSON.stringify(value)}\n`, "utf8") + if (byteLength > maximum) { + throw new TypeError(`${label} exceeds its ${maximum}-byte limit`) + } +} + +function kindLimit(kind) { + const maximum = KIND_LIMITS[kind] + if (maximum === undefined) throw new TypeError("Unknown consolidation envelope kind") + return maximum +} + +function assertWithinKindLimit(kind, byteLength) { + const maximum = kindLimit(kind) + if (byteLength > maximum) throw new Error(`${kind} envelope exceeds its byte limit`) +} diff --git a/scripts/release/duplicate-draft-consolidation.mjs b/scripts/release/duplicate-draft-consolidation.mjs new file mode 100644 index 000000000..2e69bf21f --- /dev/null +++ b/scripts/release/duplicate-draft-consolidation.mjs @@ -0,0 +1,2353 @@ +import { createHash } from "node:crypto" +import { lstat, mkdir, realpath } from "node:fs/promises" +import path from "node:path" +import { isDeepStrictEqual, types as utilTypes } from "node:util" + +import { + captureConsolidationAuthority, + captureNpmInventory, +} from "./duplicate-draft-consolidation-authority.mjs" +import { + assertEvidenceEqualsProposal, + inspectEquivalentDrafts, + inspectEquivalentRemainingDrafts, + semanticAssetProjection, + semanticReleaseProjection, +} from "./duplicate-draft-consolidation-evidence.mjs" +import { + readPrivateEnvelope, + readTrackedReceipt, + writePrivateEnvelope, + writeTrackedReceipt, +} from "./duplicate-draft-consolidation-files.mjs" +import { + appendJournalEvent, + createConsolidationJournal, + createFinalConsolidationReceipt, + deriveConsolidationState, + nextResumeAction, + parseConsolidationJournal, +} from "./duplicate-draft-consolidation-journal.mjs" +import { classifyConsolidationReleases } from "./duplicate-draft-consolidation-release-classifier.mjs" +import { + canonicalConsolidationEnvelopeBytes, + canonicalEventEnvelope, + canonicalRecordSha256, + createConsolidationEnvelope, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS, + parseConsolidationEnvelope, +} from "./duplicate-draft-consolidation-schema.mjs" +import { parseReleaseMarker } from "./metadata.mjs" + +const REPOSITORY = Object.freeze({ + name: "cacheplane/dawnai", + id: "1210070282", + defaultBranch: "main", + actor: Object.freeze({ login: "blove", id: "61436" }), +}) +const CANDIDATE = Object.freeze({ + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + tag: "v0.8.22", +}) +const SURVIVOR = "379991871" +const DUPLICATES = Object.freeze(["379982100", "379986168"]) +const OUTPUT = ".dawn/release/duplicate-draft-consolidation.proposed.json" +const JOURNAL_OUTPUT = ".dawn/release/duplicate-draft-consolidation.journal.json" +const RECEIPT_OUTPUT = "scripts/release/duplicate-draft-consolidation.json" +const OBSERVATION_GAP_MS = 60_000 +const MAXIMUM_RETRY_NPM_AGE_MS = 120_000 +const CONVERGENCE_CEILING_MS = 90_000 +const CONVERGENCE_BACKOFF_MS = Object.freeze([1_000, 5_000, 15_000, 30_000, 30_000]) +const MAXIMUM_CONVERGENCE_ATTEMPTS = 6 +const NATIVE_DATE = Date +const NATIVE_PERFORMANCE_NOW = performance.now.bind(performance) +const FAULT_BOUNDARIES = new Set([ + "after-authority-journal", + "after-authority-head", + "after-npm-journal", + "after-npm-head", + "after-intent-journal", + "after-intent-head", + "before-delete", + "after-delete", + "after-outcome-journal", + "after-outcome-head", + "after-resume-journal", + "after-resume-head", + "after-convergence-journal", + "after-convergence-head", +]) +const ROOT_GUARDS = new WeakMap() +const WORKFLOW_QUERY = deepFreeze({ + statuses: ["in_progress", "pending", "queued", "requested", "waiting"], + perPage: 100, + maximumPages: 100, +}) +const HISTORICAL_PARITY_REPORT = + "Historical duplicate payload parity is supported by embedded pre-delete evidence plus the currently reverified survivor; deleted bytes were not independently re-downloaded." + +export async function verifyDuplicateDraftConsolidation(input, dependencies) { + try { + const context = await normalizeVerifyInvocation(input, dependencies) + assertCompleteVerificationReceipt(context.receipt) + const sourceAdapters = await context.createAdapters() + const adapters = bindAdapters(sourceAdapters) + + for (const releaseId of DUPLICATES) { + const direct = exactPlain( + await adapters.github.getRelease({ releaseId }), + ["status", "operation", "httpStatus", "code"], + "deleted Release direct read", + ) + if ( + direct.status !== "AMBIGUOUS" || + direct.operation !== "release" || + direct.httpStatus !== 404 || + direct.code !== "NOT_FOUND" + ) { + throw new Error("Deleted Release direct absence is not proven") + } + } + + const enumerated = await readReleaseEnumeration({ adapters }) + classifyConsolidationReleases( + enumerated, + context.receipt.record.proposedEnvelope.record, + "final", + ) + const current = await captureConsolidationAuthority({ + stage: "final", + proposal: context.receipt.record.proposedEnvelope.record, + targetReleaseId: null, + adapters: sourceAdapters, + }) + assertFreshFinalAuthority( + current.authority, + context.receipt.record.finalAuthority, + context.receipt.record.proposedEnvelope.record, + ) + + return deepFreeze({ + status: "verified", + survivor: SURVIVOR, + deleted: [...DUPLICATES], + receipt: RECEIPT_OUTPUT, + receiptSha256: context.receipt.recordSha256, + historicalParity: HISTORICAL_PARITY_REPORT, + }) + } catch { + throw new Error("Duplicate-draft verify failed.") + } +} + +async function normalizeVerifyInvocation(input, dependencies) { + const value = exactPlain(input, ["receipt"], "verify input") + if (value.receipt !== RECEIPT_OUTPUT) { + throw new TypeError("Verify input does not identify the approved receipt") + } + const runtime = exactOptionalFields( + dependencies, + ["repositoryRoot", "createAdapters"], + [], + "verify dependencies", + ) + if ( + typeof runtime.repositoryRoot !== "string" || + !path.isAbsolute(runtime.repositoryRoot) || + path.normalize(runtime.repositoryRoot) !== runtime.repositoryRoot || + (await realpath(runtime.repositoryRoot)) !== runtime.repositoryRoot + ) { + throw new TypeError("Verify repository root is invalid") + } + const receiptPath = approvedPerformPath(runtime.repositoryRoot, value.receipt, RECEIPT_OUTPUT) + const receipt = parseConsolidationEnvelope( + "final", + await readTrackedReceipt(receiptPath, DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes), + ) + return Object.freeze({ + repositoryRoot: runtime.repositoryRoot, + receiptPath, + receipt, + createAdapters: safeFunction(runtime.createAdapters, "verify adapter factory"), + }) +} + +function assertCompleteVerificationReceipt(receipt) { + const proposal = receipt.record.proposedEnvelope + const journal = parseConsolidationJournal(receipt.record.journalEnvelope) + const expectedConfirmation = exactConfirmation(proposal) + const expectedConfirmationSha256 = createHash("sha256") + .update(expectedConfirmation, "utf8") + .digest("hex") + if ( + !isDeepStrictEqual(proposal.record.repository, REPOSITORY) || + !isDeepStrictEqual(proposal.record.candidate, CANDIDATE) || + proposal.record.roles.survivor !== SURVIVOR || + !safeArrayEquals(proposal.record.roles.duplicates, DUPLICATES) || + !isDeepStrictEqual(proposal.record.confirmation, { + version: CANDIDATE.version, + commitSha: CANDIDATE.commitSha, + survivor: SURVIVOR, + duplicates: [...DUPLICATES], + template: "<64-lowercase-hex-digest>", + }) || + proposal.record.releases.length !== 3 || + !isDeepStrictEqual( + proposal.record.releases.map(({ id }) => id), + [SURVIVOR, ...DUPLICATES], + ) || + !isDeepStrictEqual(journal.record.repository, REPOSITORY) || + !isDeepStrictEqual(journal.record.candidate, CANDIDATE) || + journal.record.proposedRecordSha256 !== proposal.recordSha256 || + journal.record.confirmationSha256 !== expectedConfirmationSha256 || + !safeArrayEquals(journal.record.deletionOrder, DUPLICATES) + ) { + throw new Error("Receipt does not bind the approved consolidation identity") + } + if ( + proposal.record.controller.headSha !== proposal.record.controller.originMainSha || + proposal.record.controller.headSha !== proposal.record.controller.githubMainSha || + proposal.record.controller.headSha === CANDIDATE.commitSha + ) { + throw new Error("Receipt controller identity is invalid") + } + assertHistoricalEvidenceBinding(journal, proposal) + assertMandatoryPerformHistory(journal, proposal) + const state = deriveConsolidationState(journal) + if ( + state.phase !== "final-authority-observed" || + !safeArrayEquals(state.completedTargets, DUPLICATES) || + !isDeepStrictEqual(state.lastAuthority, receipt.record.finalAuthority) + ) { + throw new Error("Receipt journal is not a complete consolidation history") + } + const reconstructed = createFinalConsolidationReceipt({ + proposedEnvelope: proposal, + journalEnvelope: journal, + finalAuthority: receipt.record.finalAuthority, + completedAt: receipt.record.completedAt, + }) + if ( + !canonicalConsolidationEnvelopeBytes("final", reconstructed).equals( + canonicalConsolidationEnvelopeBytes("final", receipt), + ) + ) { + throw new Error("Receipt is not the exact canonical terminal result") + } +} + +function assertHistoricalEvidenceBinding(journal, proposalEnvelope) { + const proposal = proposalEnvelope.record + assertProposalPayloadProof(proposal) + const proposedById = new Map(proposal.releases.map((release) => [release.id, release])) + const stablePackages = stableNpmObservation(proposal.npmInventories[0]).packages + + for (const { event } of journal.record.events) { + if (event.type === "npm-observed") { + if ( + event.payload.inventory.stage !== "perform-initial" || + !isDeepStrictEqual(stableNpmObservation(event.payload.inventory).packages, stablePackages) + ) { + throw new Error("Journal npm evidence differs from the reviewed proposal") + } + continue + } + if (event.type === "resume-reconciliation") { + if (event.payload.classification === "present-unchanged-retryable") { + assertHistoricalRelease( + event.payload.releaseEvidence, + proposedById.get(event.payload.targetReleaseId), + "Retry reconciliation", + ) + } + continue + } + if (event.type === "delete-authority-observed") { + const expectedStage = + event.payload.targetReleaseId === DUPLICATES[0] ? "pre-delete-1" : "pre-delete-2" + const expectedIds = + expectedStage === "pre-delete-1" ? [SURVIVOR, ...DUPLICATES] : [SURVIVOR, DUPLICATES[1]] + assertHistoricalAuthority( + event.payload.authority, + proposal, + proposedById, + expectedStage, + expectedIds, + ) + const targetRead = event.payload.authority.targetRead + if ( + targetRead === null || + targetRead.evidence.id !== event.payload.targetReleaseId || + targetRead.evidenceSha256 !== canonicalRecordSha256(targetRead.evidence) + ) { + throw new Error("Delete authority target read is not the exact reviewed target") + } + assertHistoricalRelease( + targetRead.evidence, + proposedById.get(event.payload.targetReleaseId), + "Delete authority target read", + ) + continue + } + if (event.type === "final-authority-observed") { + assertHistoricalAuthority(event.payload.authority, proposal, proposedById, "final", [ + SURVIVOR, + ]) + } + } +} + +function assertProposalPayloadProof(proposal) { + const releases = proposal.releases + const releaseProjection = semanticReleaseProjection(releases[0]) + const assetProjection = releases[0].assets.map(semanticAssetProjection) + for (const release of releases) { + if ( + release.semantic.name !== `Dawn v${CANDIDATE.version}` || + !isDeepStrictEqual(semanticReleaseProjection(release), releaseProjection) || + !isDeepStrictEqual(release.assets.map(semanticAssetProjection), assetProjection) + ) { + throw new Error("Proposal does not prove exact three-way Release payload parity") + } + } + + const marker = parseReleaseMarker(releases[0].semantic.body) + const markerSubjects = marker.attestationSet?.subjects.map(({ subjectName, subjectSha256 }) => ({ + name: subjectName, + sha256: subjectSha256, + })) + const markerBaseAssetSet = + marker.attestationSet === null + ? null + : [ + { name: "release-record.json", sha256: marker.releaseRecordSha256 }, + ...marker.attestationSet.subjects.map(({ subjectName, subjectSha256 }) => ({ + name: subjectName, + sha256: subjectSha256, + })), + ...marker.attestationSet.subjects.map(({ bundleName, bundleSha256 }) => ({ + name: bundleName, + sha256: bundleSha256, + })), + ] + const proof = proposal.payloadProof + const payloadProjection = releases.map((release) => ({ + release: semanticReleaseProjection(release), + assets: release.assets.map(semanticAssetProjection), + })) + if ( + marker.phase !== "ESCROWED" || + marker.version !== CANDIDATE.version || + marker.commitSha !== CANDIDATE.commitSha || + marker.tag !== CANDIDATE.tag || + marker.attestationSet?.repository !== REPOSITORY.name || + marker.baseAssetSetSha256 !== proof.baseAssetSetSha256 || + !isDeepStrictEqual(markerBaseAssetSet, proof.baseAssetSet) || + !isDeepStrictEqual(markerSubjects, proof.attestationVerification.subjects) || + proof.baseAssetSetSha256 !== canonicalRecordSha256(proof.baseAssetSet) || + proof.consolidationPayloadSha256 !== canonicalRecordSha256(payloadProjection) + ) { + throw new Error("Proposal payload proof is not bound to its canonical Release evidence") + } +} + +function assertHistoricalAuthority(authority, proposal, proposedById, stage, expectedIds) { + if ( + authority.stage !== stage || + !isDeepStrictEqual( + authority.releases.map(({ id }) => id), + expectedIds, + ) || + !isDeepStrictEqual(authority.controller, proposal.controller) || + !isDeepStrictEqual( + withoutObservationTime(authority.annotatedTag), + withoutObservationTime(proposal.annotatedTag), + ) || + !isDeepStrictEqual( + withoutObservationTime(authority.workflowAuthority), + withoutObservationTime(proposal.workflowAuthority), + ) || + !isDeepStrictEqual( + stableNpmObservation(authority.npmInventory).packages, + stableNpmObservation(proposal.npmInventories[0]).packages, + ) || + !isDeepStrictEqual(authority.payloadProof, proposal.payloadProof) || + (stage === "final") !== (authority.targetRead === null) + ) { + throw new Error("Historical authority differs from the reviewed stage evidence") + } + for (const release of authority.releases) { + assertHistoricalRelease(release, proposedById.get(release.id), "Historical authority") + } +} + +function assertHistoricalRelease(actual, proposed, label) { + if (proposed === undefined || actual.id !== proposed.id || actual.role !== proposed.role) { + throw new Error(`${label} identifies an unreviewed Release`) + } + assertEvidenceEqualsProposal(actual, proposed) +} + +export async function performDuplicateDraftConsolidation(input, dependencies) { + try { + const context = await normalizePerformInvocation(input, dependencies) + let current = await loadOrCreatePerformJournal(context) + assertPerformJournalBinding(context, current.journal) + let state = deriveConsolidationState(current.journal) + const resumedFromFinalAuthority = state.phase === "final-authority-observed" + + if (state.phase === "operation-started") { + const inventory = await context.capturePerformInitial(context) + current = await appendDurableEvent( + context, + current.journal, + "npm-observed", + { + targetReleaseId: DUPLICATES[0], + attemptNumber: 1, + inventory, + }, + inventory.completedAt, + "npm", + ) + state = deriveConsolidationState(current.journal) + } + assertMandatoryPerformHistory(current.journal, context.proposal) + if (state.phase === "npm-observed") { + if (state.lastRetryNpmInventory === null) { + throw new Error("Perform initial npm evidence is unavailable") + } + await context.verifyPerformInitial(context, state.lastRetryNpmInventory) + } + + for (const targetReleaseId of DUPLICATES) { + state = deriveConsolidationState(current.journal) + if (state.completedTargets.includes(targetReleaseId)) continue + if (state.currentTargetReleaseId !== targetReleaseId) { + throw new Error("Perform journal target order is invalid") + } + await context.performOneDeletion( + { + proposedEnvelope: context.proposal, + confirmation: context.confirmation, + targetReleaseId, + journalPath: context.journalPath, + }, + { + createAdapters: context.createAdapters, + wait: context.wait, + }, + ) + current = await loadCurrentJournal(context.journalPath) + state = deriveConsolidationState(current.journal) + if (!state.completedTargets.includes(targetReleaseId)) { + throw new Error("Perform target did not durably converge") + } + } + + state = deriveConsolidationState(current.journal) + if (state.phase === "target-converged") { + const finalAuthority = await context.captureFinalAuthority(context) + current = await appendDurableEvent( + context, + current.journal, + "final-authority-observed", + { authority: finalAuthority }, + finalAuthority.observedAt, + "final-authority", + ) + state = deriveConsolidationState(current.journal) + } + if (state.phase !== "final-authority-observed" || state.lastAuthority === null) { + throw new Error("Perform journal is not ready for its final receipt") + } + assertMandatoryPerformHistory(current.journal, context.proposal) + const receipt = createFinalConsolidationReceipt({ + proposedEnvelope: context.proposal, + journalEnvelope: current.journal, + finalAuthority: state.lastAuthority, + completedAt: state.lastAuthority.observedAt, + }) + const receiptBytes = canonicalConsolidationEnvelopeBytes("final", receipt) + const receiptAlreadyDurable = await exactExistingReceipt(context.receiptPath, receiptBytes) + if (resumedFromFinalAuthority) { + const freshFinalAuthority = await context.captureFinalAuthority(context) + assertFreshFinalAuthority(freshFinalAuthority, state.lastAuthority, context.proposal.record) + } + if (receiptAlreadyDurable) return completedPerformResult(receipt) + await context.publishReceipt(context.receiptPath, receiptBytes) + return completedPerformResult(receipt) + } catch { + throw new Error("Duplicate-draft perform failed.") + } +} + +function completedPerformResult(receipt) { + return deepFreeze({ + status: "complete", + survivor: SURVIVOR, + deleted: [...DUPLICATES], + receipt: RECEIPT_OUTPUT, + receiptSha256: receipt.recordSha256, + }) +} + +async function exactExistingReceipt(receiptPath, expectedBytes) { + let current + try { + current = await readTrackedReceipt( + receiptPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes, + ) + } catch (error) { + if (error?.code === "ENOENT") return false + throw error + } + if (!current.equals(expectedBytes)) { + throw new Error("Existing consolidation receipt differs from the canonical result") + } + parseConsolidationEnvelope("final", current) + return true +} + +function assertMandatoryPerformHistory(journal, proposal) { + const parsed = parseConsolidationJournal(journal) + if ( + proposal.record.npmInventories.length !== 2 || + proposal.record.npmInventories[0].stage !== "inspect-initial" || + proposal.record.npmInventories[1].stage !== "inspect-ready" + ) { + throw new Error("Proposal does not contain the mandatory inspection npm stages") + } + const events = parsed.record.events.map(({ event }) => event) + const initial = events[1] + if ( + initial?.type !== "npm-observed" || + initial.payload.targetReleaseId !== DUPLICATES[0] || + initial.payload.attemptNumber !== 1 || + initial.payload.inventory.stage !== "perform-initial" + ) { + throw new Error("Journal omitted the mandatory perform-initial proof") + } + const authorityStages = [] + for (let index = 2; index < events.length; index += 1) { + const event = events[index] + if (event.type === "npm-observed" && event.payload.inventory.stage !== "perform-initial") { + throw new Error("Retry npm history contains an invalid stage") + } + if (event.type === "delete-authority-observed") { + authorityStages.push(event.payload.authority.stage) + } + if (event.type === "final-authority-observed") authorityStages.push("final") + } + const state = deriveConsolidationState(parsed) + if ( + state.phase === "final-authority-observed" && + !isDeepStrictEqual( + authorityStages.filter((stage, index) => stage !== authorityStages[index - 1]), + ["pre-delete-1", "pre-delete-2", "final"], + ) + ) { + throw new Error("Final history does not contain all mandatory npm authority stages") + } +} + +function assertFreshFinalAuthority(value, recorded, proposal) { + const normalized = canonicalEventEnvelope( + { + schemaVersion: 1, + sequence: 1, + previousEventSha256: null, + type: "final-authority-observed", + recordedAt: value?.observedAt, + payload: { authority: value }, + }, + null, + ).event.payload.authority + if ( + normalized.stage !== "final" || + normalized.targetRead !== null || + !isDeepStrictEqual(normalized.controller, proposal.controller) || + !isDeepStrictEqual( + withoutObservationTime(normalized.annotatedTag), + withoutObservationTime(proposal.annotatedTag), + ) || + !isDeepStrictEqual( + withoutObservationTime(normalized.workflowAuthority), + withoutObservationTime(proposal.workflowAuthority), + ) || + !isDeepStrictEqual( + stableNpmObservation(normalized.npmInventory), + stableNpmObservation(recorded.npmInventory), + ) || + normalized.releases.length !== 1 || + normalized.releases[0].id !== SURVIVOR || + !isDeepStrictEqual(normalized.payloadProof, proposal.payloadProof) || + Date.parse(normalized.observedAt) < Date.parse(recorded.observedAt) + ) { + throw new Error("Fresh final authority drifted from the completed operation") + } + assertEvidenceEqualsProposal(normalized.releases[0], proposal.releases[0]) +} + +function withoutObservationTime({ observedAt: _observedAt, ...value }) { + return value +} + +function stableNpmObservation({ startedAt: _startedAt, completedAt: _completedAt, ...inventory }) { + return { + ...inventory, + packages: inventory.packages.map(({ observedAt: _observedAt, ...entry }) => entry), + } +} + +async function normalizePerformInvocation(input, dependencies) { + const value = exactPlain( + input, + ["proposal", "proposalSha256", "journal", "receipt", "confirmation"], + "perform input", + ) + if ( + value.proposal !== OUTPUT || + value.journal !== JOURNAL_OUTPUT || + value.receipt !== RECEIPT_OUTPUT || + !/^[0-9a-f]{64}$/u.test(value.proposalSha256) || + typeof value.confirmation !== "string" + ) { + throw new TypeError("Perform input does not identify the approved operation") + } + const runtime = exactOptionalFields( + dependencies, + ["repositoryRoot", "createAdapters", "now", "wait"], + [ + "capturePerformInitial", + "verifyPerformInitial", + "performOneDeletion", + "captureFinalAuthority", + "publishReceipt", + ], + "perform dependencies", + ) + if ( + typeof runtime.repositoryRoot !== "string" || + !path.isAbsolute(runtime.repositoryRoot) || + path.normalize(runtime.repositoryRoot) !== runtime.repositoryRoot || + (await realpath(runtime.repositoryRoot)) !== runtime.repositoryRoot + ) { + throw new TypeError("Perform repository root is invalid") + } + const proposalPath = approvedPerformPath(runtime.repositoryRoot, value.proposal, OUTPUT) + const journalPath = approvedPerformPath(runtime.repositoryRoot, value.journal, JOURNAL_OUTPUT) + const receiptPath = approvedPerformPath(runtime.repositoryRoot, value.receipt, RECEIPT_OUTPUT) + const proposal = parseConsolidationEnvelope( + "proposed", + await readPrivateEnvelope(proposalPath, DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes), + ) + const confirmation = exactConfirmation(proposal) + if (proposal.recordSha256 !== value.proposalSha256 || value.confirmation !== confirmation) { + throw new Error("Perform confirmation does not bind the reviewed proposal") + } + const createAdapters = safeFunction(runtime.createAdapters, "perform adapter factory") + const now = trustedClock(safeFunction(runtime.now, "perform clock")) + const wait = safeFunction(runtime.wait, "perform waiter") + const context = { + repositoryRoot: runtime.repositoryRoot, + proposal, + confirmation, + proposalPath, + journalPath, + receiptPath, + createAdapters, + now, + wait, + faultAt: null, + } + context.capturePerformInitial = + runtime.capturePerformInitial === undefined + ? defaultCapturePerformInitial + : safeFunction(runtime.capturePerformInitial, "perform initial capture") + context.verifyPerformInitial = + runtime.verifyPerformInitial === undefined + ? defaultVerifyPerformInitial + : safeFunction(runtime.verifyPerformInitial, "perform initial verifier") + context.performOneDeletion = + runtime.performOneDeletion === undefined + ? performOneDuplicateDeletion + : safeFunction(runtime.performOneDeletion, "perform target deletion") + context.captureFinalAuthority = + runtime.captureFinalAuthority === undefined + ? defaultCaptureFinalAuthority + : safeFunction(runtime.captureFinalAuthority, "perform final authority") + context.publishReceipt = + runtime.publishReceipt === undefined + ? writeTrackedReceipt + : safeFunction(runtime.publishReceipt, "perform receipt publisher") + return Object.freeze(context) +} + +function approvedPerformPath(repositoryRoot, relativePath, expected) { + if (relativePath !== expected) throw new TypeError("Perform path is not approved") + const absolute = path.join(repositoryRoot, ...relativePath.split("/")) + if (path.relative(repositoryRoot, absolute) !== relativePath.split("/").join(path.sep)) { + throw new TypeError("Perform path escapes the repository") + } + return absolute +} + +function exactConfirmation(proposal) { + return `CONSOLIDATE v${proposal.record.candidate.version} ${proposal.record.candidate.commitSha} SURVIVOR ${proposal.record.roles.survivor} DELETE ${proposal.record.roles.duplicates.join(",")} PROPOSAL ${proposal.recordSha256}` +} + +async function loadOrCreatePerformJournal(context) { + try { + return await loadCurrentJournal(context.journalPath) + } catch (error) { + if (error?.code !== "ENOENT") throw error + } + const headPath = context.journalPath.replace(/journal\.json$/u, "journal.head.json") + try { + await readPrivateEnvelope(headPath, 16 * 1024) + throw new Error("An orphan consolidation journal head blocks genesis") + } catch (error) { + if (error?.code !== "ENOENT") throw error + } + const recordedAt = timestamp(context.now, "operation start clock") + const journal = createConsolidationJournal({ + proposedEnvelope: context.proposal, + confirmationSha256: createHash("sha256").update(context.confirmation, "utf8").digest("hex"), + recordedAt, + }) + await writePrivateEnvelope( + context.journalPath, + canonicalConsolidationEnvelopeBytes("journal", journal), + ) + await writePrivateEnvelope(headPath, canonicalJournalHeadBytes(context.journalPath, journal)) + return loadCurrentJournal(context.journalPath) +} + +function assertPerformJournalBinding(context, journal) { + const state = deriveConsolidationState(journal) + const confirmationSha256 = createHash("sha256").update(context.confirmation, "utf8").digest("hex") + if ( + journal.record.proposedRecordSha256 !== context.proposal.recordSha256 || + journal.record.confirmationSha256 !== confirmationSha256 || + state.controllerSha !== context.proposal.record.controller.headSha || + !safeArrayEquals(journal.record.deletionOrder, DUPLICATES) + ) { + throw new Error("Journal does not bind the exact approved perform operation") + } +} + +async function defaultCapturePerformInitial(context) { + const adapters = bindAdapters(await context.createAdapters()) + return captureNpmInventory({ + stage: "perform-initial", + candidate: CANDIDATE, + npm: adapters.npm, + now: context.now, + }) +} + +async function defaultVerifyPerformInitial(context, inventory) { + const adapters = bindAdapters(await context.createAdapters()) + const releases = await readReleaseEnumeration({ adapters }) + const inspected = await inspectEquivalentDrafts({ + candidate: CANDIDATE, + survivorId: SURVIVOR, + duplicateIds: DUPLICATES, + releases, + github: adapters.github, + attestations: adapters.attestations, + }) + for (let index = 0; index < inspected.releases.length; index += 1) { + assertEvidenceEqualsProposal(inspected.releases[index], context.proposal.record.releases[index]) + } + if (!isDeepStrictEqual(inspected.payloadProof, context.proposal.record.payloadProof)) { + throw new Error("Perform payload proof differs from the proposal") + } + const elapsed = + Date.parse(timestamp(context.now, "perform payload clock")) - Date.parse(inventory.completedAt) + if (elapsed < 0) throw new Error("Perform observation clock reversed") + const remaining = Math.max(0, OBSERVATION_GAP_MS - elapsed) + if (remaining > 0) { + const signal = AbortSignal.timeout(remaining + 5_000) + await context.wait(remaining, { signal }) + } + if ( + Date.parse(timestamp(context.now, "perform ready clock")) - Date.parse(inventory.completedAt) < + OBSERVATION_GAP_MS + ) { + throw new Error("Perform observation gap did not reach sixty seconds") + } +} + +async function defaultCaptureFinalAuthority(context) { + const sourceAdapters = await context.createAdapters() + const captured = await captureConsolidationAuthority({ + stage: "final", + proposal: context.proposal.record, + targetReleaseId: null, + adapters: sourceAdapters, + }) + return captured.authority +} + +export async function performOneDuplicateDeletion(input, dependencies) { + try { + const context = normalizeDeletionInvocation(input, dependencies) + let current = await loadCurrentJournal(context.journalPath) + assertDeletionBinding(context, current.journal) + const completed = completedDeletionResult(context, current.journal) + if (completed !== null) return completed + let preparedAttempt = null + + for (;;) { + const state = deriveConsolidationState(current.journal) + if (state.currentTargetReleaseId !== context.targetReleaseId) { + throw new Error("Journal current target differs from the requested target") + } + + if ( + state.phase === "delete-outcome" && + state.lastOutcomeClassification === "response-hard-failure" + ) { + throw new Error("A hard GitHub DELETE response is terminal and cannot be retried") + } + + if (["delete-intent", "delete-outcome", "resume-absent"].includes(state.phase)) { + const observation = await observeConvergence(context, state) + const resolved = await resolveConvergence(context, current.journal, state, observation) + if (resolved.result !== null) return resolved.result + current = { journal: resolved.journal } + preparedAttempt = resolved.preparedAttempt + continue + } + + if (state.phase === "npm-observed" && state.pendingRetryFromAttempt !== null) { + const resolved = await completeStaleRetryPreparation(context, current.journal, state) + current = { journal: resolved.journal } + preparedAttempt = resolved.preparedAttempt + continue + } + + if ( + ![ + "operation-started", + "npm-observed", + "target-converged", + "resume-present", + "delete-authority-observed", + ].includes(state.phase) + ) { + throw new Error("Journal is not at a legal one-target mutation state") + } + + const prepared = preparedAttempt ?? (await captureFreshDeleteAuthority(context, state)) + preparedAttempt = null + const { adapters, captured } = prepared + const attemptNumber = + state.phase === "resume-present" ? state.attemptNumber + 1 : state.attemptNumber + current = await appendDurableEvent( + context, + current.journal, + "delete-authority-observed", + { + targetReleaseId: context.targetReleaseId, + attemptNumber, + authority: captured.authority, + }, + captured.authority.observedAt, + "authority", + ) + const permit = await captured.networkEpoch.consume({ + authority: captured.authority, + proposal: context.proposal.record, + confirmation: context.confirmation, + targetReleaseId: context.targetReleaseId, + intentPath: context.journalPath, + currentJournal: current.journal, + }) + current = await loadCurrentJournal(context.journalPath) + const intentState = deriveConsolidationState(current.journal) + if ( + intentState.phase !== "delete-intent" || + intentState.currentTargetReleaseId !== context.targetReleaseId || + intentState.attemptNumber !== attemptNumber + ) { + throw new Error("Durable delete intent did not become current") + } + injectFault(context, "before-delete") + const outcome = exactPlain( + await adapters.writer.deleteDuplicate({ + releaseId: context.targetReleaseId, + permit, + }), + ["classification", "httpStatus", "observedAt"], + "delete outcome", + ) + injectFault(context, "after-delete") + current = await appendDurableEvent( + context, + current.journal, + "delete-outcome", + { + targetReleaseId: context.targetReleaseId, + attemptNumber, + classification: outcome.classification, + httpStatus: outcome.httpStatus, + observedAt: outcome.observedAt, + }, + outcome.observedAt, + "outcome", + ) + const outcomeState = deriveConsolidationState(current.journal) + if (outcomeState.lastOutcomeClassification === "response-hard-failure") { + throw new Error("A hard GitHub DELETE response is terminal and cannot be retried") + } + const observation = await observeConvergence(context, outcomeState) + const resolved = await resolveConvergence(context, current.journal, outcomeState, observation) + if (resolved.result !== null) return resolved.result + current = { journal: resolved.journal } + preparedAttempt = resolved.preparedAttempt + } + } catch { + throw new Error("One duplicate deletion failed.") + } +} + +async function captureFreshDeleteAuthority(context, state) { + const sourceAdapters = await context.createAdapters() + const adapters = bindAdapters(sourceAdapters) + const captured = await captureConsolidationAuthority({ + stage: deletionStage(state), + proposal: context.proposal.record, + targetReleaseId: context.targetReleaseId, + adapters: sourceAdapters, + }) + return Object.freeze({ adapters, captured }) +} + +async function captureStaleRetryInventory(context, journal, state) { + const sourceAdapters = await context.createAdapters() + const adapters = bindAdapters(sourceAdapters) + const inventory = await captureNpmInventory({ + stage: "perform-initial", + candidate: CANDIDATE, + npm: adapters.npm, + now: context.retryWallNow, + }) + return appendDurableEvent( + context, + journal, + "npm-observed", + { + targetReleaseId: context.targetReleaseId, + attemptNumber: state.attemptNumber + 1, + inventory, + }, + inventory.completedAt, + "npm", + ) +} + +function normalizeDeletionInvocation(input, dependencies) { + const value = exactPlain( + input, + ["proposedEnvelope", "confirmation", "targetReleaseId", "journalPath"], + "one-target deletion input", + ) + if ( + typeof value.targetReleaseId !== "string" || + !DUPLICATES.includes(value.targetReleaseId) || + value.targetReleaseId === SURVIVOR + ) { + throw new TypeError("Deletion target is not an approved duplicate") + } + if ( + typeof value.journalPath !== "string" || + !path.isAbsolute(value.journalPath) || + path.normalize(value.journalPath) !== value.journalPath || + path.basename(value.journalPath) !== "duplicate-draft-consolidation.journal.json" + ) { + throw new TypeError("Deletion journal path is invalid") + } + if (typeof value.confirmation !== "string") { + throw new TypeError("Deletion confirmation is invalid") + } + const proposal = parseConsolidationEnvelope( + "proposed", + canonicalConsolidationEnvelopeBytes("proposed", value.proposedEnvelope), + ) + const runtime = exactOptionalFields( + dependencies, + ["createAdapters", "wait"], + ["faultAt", "monotonicTimeline", "wallClockTimeline"], + "one-target deletion dependencies", + ) + const createAdapters = safeFunction(runtime.createAdapters, "deletion adapter factory") + const wait = safeFunction(runtime.wait, "convergence waiter") + const convergenceAuditNow = deletionMonotonicClock(runtime.monotonicTimeline) + const retryWallNow = deletionWallClock(runtime.wallClockTimeline) + if ( + runtime.faultAt !== undefined && + (typeof runtime.faultAt !== "string" || !FAULT_BOUNDARIES.has(runtime.faultAt)) + ) { + throw new TypeError("Deletion fault boundary is invalid") + } + return Object.freeze({ + proposal, + confirmation: value.confirmation, + targetReleaseId: value.targetReleaseId, + journalPath: value.journalPath, + createAdapters, + wait, + convergenceAuditNow, + retryWallNow, + faultAt: runtime.faultAt ?? null, + }) +} + +function assertDeletionBinding(context, journal) { + const state = deriveConsolidationState(journal) + const confirmationSha256 = createHash("sha256").update(context.confirmation, "utf8").digest("hex") + if ( + journal.record.proposedRecordSha256 !== context.proposal.recordSha256 || + journal.record.confirmationSha256 !== confirmationSha256 || + state.controllerSha !== context.proposal.record.controller.headSha || + !safeArrayEquals(journal.record.deletionOrder, DUPLICATES) || + (state.currentTargetReleaseId !== context.targetReleaseId && + !state.completedTargets.includes(context.targetReleaseId)) + ) { + throw new Error("Journal does not bind the exact approved deletion") + } +} + +function completedDeletionResult(context, journal) { + const state = deriveConsolidationState(journal) + if (!state.completedTargets.includes(context.targetReleaseId)) return null + const converged = journal.record.events.findLast( + ({ event }) => + event.type === "absence-converged" && + event.payload.targetReleaseId === context.targetReleaseId, + ) + if (converged === undefined) { + throw new Error("Completed target lacks its durable convergence event") + } + return deepFreeze({ + status: "converged", + targetReleaseId: context.targetReleaseId, + attemptNumber: converged.event.payload.attemptNumber, + basis: converged.event.payload.basis, + }) +} + +function deletionStage(state) { + const index = state.deletionOrder.indexOf(state.currentTargetReleaseId) + if (index === 0) return "pre-delete-1" + if (index === 1) return "pre-delete-2" + throw new Error("Journal target is outside the fixed deletion order") +} + +async function resolveConvergence(context, journal, state, observation) { + if (observation.classification === "absent") { + let current = { journal } + let currentState = state + if (currentState.phase === "delete-intent") { + if (nextResumeAction(currentState, { classification: "absent" }) !== "reconcile-absence") { + throw new Error("Journal rejected absent intent reconciliation") + } + current = await appendDurableEvent( + context, + current.journal, + "resume-reconciliation", + { + targetReleaseId: context.targetReleaseId, + attemptNumber: currentState.attemptNumber, + classification: "absent-ambiguous", + releaseEvidence: null, + observedAt: observation.completedAt, + }, + observation.completedAt, + "resume", + ) + currentState = deriveConsolidationState(current.journal) + } + if (currentState.phase !== "resume-absent" && currentState.phase !== "delete-outcome") { + throw new Error("Absence is not legal in the current journal phase") + } + const basis = + currentState.phase === "delete-outcome" && + currentState.lastOutcomeClassification === "confirmed-204" + ? "confirmed-204" + : "ambiguous" + current = await appendDurableEvent( + context, + current.journal, + "absence-converged", + { + targetReleaseId: context.targetReleaseId, + attemptNumber: currentState.attemptNumber, + basis, + directGet404At: observation.directGet404At, + listAbsentAt: observation.listAbsentAt, + attempts: observation.attempts, + completedAt: observation.completedAt, + }, + observation.completedAt, + "convergence", + ) + return { + journal: current.journal, + result: deepFreeze({ + status: "converged", + targetReleaseId: context.targetReleaseId, + attemptNumber: currentState.attemptNumber, + basis, + }), + } + } + + const liveTarget = { + classification: "present-unchanged", + releaseEvidence: observation.releaseEvidence, + ...(state.phase === "delete-outcome" ? { observations: observation.attempts } : {}), + } + if (nextResumeAction(state, liveTarget) !== "refresh-and-retry") { + throw new Error("Present target is not eligible for another delete attempt") + } + if (retryNpmInventoryIsStale(context, state)) { + const current = await captureStaleRetryInventory(context, journal, state) + return completeStaleRetryPreparation( + context, + current.journal, + deriveConsolidationState(current.journal), + ) + } + const preparedAttempt = await captureFreshDeleteAuthority(context, state) + const current = await appendRetryReconciliation(context, journal, state, preparedAttempt) + return { journal: current.journal, result: null, preparedAttempt } +} + +function retryNpmInventoryIsStale(context, state) { + const completedAt = state.lastAuthority?.npmInventory?.completedAt + if (completedAt === undefined) { + throw new Error("Retry freshness has no preceding npm inventory") + } + const current = timestampValue(context.retryWallNow(), "retry npm freshness clock") + const age = Date.parse(current) - Date.parse(completedAt) + if (age < 0) { + throw new Error("Retry freshness clock precedes the prior npm inventory") + } + return age > MAXIMUM_RETRY_NPM_AGE_MS +} + +async function completeStaleRetryPreparation(context, journal, state) { + if ( + state.phase !== "npm-observed" || + state.pendingRetryFromAttempt === null || + state.lastRetryNpmInventory === null + ) { + throw new Error("Stale retry preparation is not durable in the journal") + } + const sourceAdapters = await context.createAdapters() + const adapters = bindAdapters(sourceAdapters) + const releases = await readReleaseEnumeration({ adapters }) + const stage = deletionStage(state) + const inspected = await inspectEquivalentRemainingDrafts({ + stage, + candidate: CANDIDATE, + survivorId: SURVIVOR, + duplicateIds: DUPLICATES, + releases, + github: adapters.github, + attestations: adapters.attestations, + }) + assertRetryPayloadMatchesProposal(inspected, context.proposal.record, stage) + const gapStartedAt = Date.parse(state.lastRetryNpmInventory.completedAt) + const afterVerification = Date.parse( + timestampValue(context.retryWallNow(), "retry payload verification clock"), + ) + const elapsed = afterVerification - gapStartedAt + if (elapsed < 0) throw new Error("Retry observation clock reversed") + const remaining = Math.max(0, OBSERVATION_GAP_MS - elapsed) + if (remaining > 0) { + const timeoutMs = remaining + 5_000 + const signal = AbortSignal.timeout(timeoutMs) + await context.wait(remaining, { signal, timeoutMs }) + } + const readyBoundary = Date.parse( + timestampValue(context.retryWallNow(), "retry observation boundary clock"), + ) + if (readyBoundary - gapStartedAt < OBSERVATION_GAP_MS) { + throw new Error("Retry observation gap did not reach sixty seconds") + } + const preparedAttempt = await captureFreshDeleteAuthority(context, state) + const current = await appendRetryReconciliation(context, journal, state, preparedAttempt) + return { journal: current.journal, result: null, preparedAttempt } +} + +function assertRetryPayloadMatchesProposal(inspected, proposal, stage) { + const proposedReleases = + stage === "pre-delete-1" ? proposal.releases : [proposal.releases[0], proposal.releases[2]] + if (!Array.isArray(inspected.releases) || inspected.releases.length !== proposedReleases.length) { + throw new Error("Retry payload verification returned incomplete Releases") + } + for (let index = 0; index < proposedReleases.length; index += 1) { + if (inspected.releases[index].id !== proposedReleases[index].id) { + throw new Error("Retry payload verification changed fixed Release order") + } + assertEvidenceEqualsProposal(inspected.releases[index], proposedReleases[index]) + } + const payloadProofMatches = + isDeepStrictEqual(inspected.payloadProof.baseAssetSet, proposal.payloadProof.baseAssetSet) && + inspected.payloadProof.baseAssetSetSha256 === proposal.payloadProof.baseAssetSetSha256 && + isDeepStrictEqual( + inspected.payloadProof.attestationVerification, + proposal.payloadProof.attestationVerification, + ) && + (stage === "pre-delete-2" || + inspected.payloadProof.consolidationPayloadSha256 === + proposal.payloadProof.consolidationPayloadSha256) + if (!payloadProofMatches) { + throw new Error("Retry payload proof differs from the reviewed proposal") + } +} + +function appendRetryReconciliation(context, journal, state, preparedAttempt) { + return appendDurableEvent( + context, + journal, + "resume-reconciliation", + { + targetReleaseId: context.targetReleaseId, + attemptNumber: + state.pendingRetryFromAttempt === null + ? state.attemptNumber + : state.pendingRetryFromAttempt, + classification: "present-unchanged-retryable", + releaseEvidence: preparedAttempt.captured.authority.targetRead.evidence, + observedAt: preparedAttempt.captured.authority.observedAt, + }, + preparedAttempt.captured.authority.observedAt, + "resume", + ) +} + +async function observeConvergence(context, state) { + const budget = createConvergenceBudget(NATIVE_PERFORMANCE_NOW, context.convergenceAuditNow) + let lastPresent = null + for (let attempt = 1; attempt <= MAXIMUM_CONVERGENCE_ATTEMPTS; attempt += 1) { + budget.checkpoint() + const direct = await runConvergenceRequest(context, budget, "release", (adapters) => + adapters.github.getRelease({ + releaseId: context.targetReleaseId, + }), + ) + const directCompletedAt = currentIsoTimestamp() + const list = exactPlain( + await runConvergenceRequest(context, budget, "releases", (adapters) => + adapters.github.listReleases(), + ), + ["status", "operation", "httpStatus", "code", "value"], + "convergence Release enumeration", + ) + const listCompletedAt = currentIsoTimestamp() + if ( + list.status !== "PRESENT" || + list.operation !== "releases" || + list.httpStatus !== 200 || + list.code !== null || + !Array.isArray(list.value) + ) { + throw new Error("Convergence Release enumeration is incomplete") + } + const directStatus = safeDataValue(direct, "status") + if (directStatus === "PRESENT") { + const present = exactPlain( + direct, + ["status", "operation", "httpStatus", "code", "value"], + "convergence direct Release read", + ) + if (present.operation !== "release" || present.httpStatus !== 200 || present.code !== null) { + throw new Error("Convergence direct Release read is malformed") + } + const classified = classifyConsolidationReleases( + list.value, + context.proposal.record, + deletionStage(state), + ) + validateRemainingConvergence(classified.selected, context) + const listed = classified.selected.find(({ id }) => String(id) === context.targetReleaseId) + if (listed === undefined || !isDeepStrictEqual(present.value, listed)) { + throw new Error("Convergence direct and list readers disagree") + } + lastPresent = validatePresentConvergence(present.value, state) + } else { + const absent = exactPlain( + direct, + ["status", "operation", "httpStatus", "code"], + "convergence direct Release absence", + ) + if ( + absent.status !== "AMBIGUOUS" || + absent.operation !== "release" || + absent.httpStatus !== 404 || + typeof absent.code !== "string" + ) { + throw new Error("Convergence direct read is not exact 404 evidence") + } + const classified = classifyConsolidationReleases( + list.value, + context.proposal.record, + nextDeletionStage(state), + ) + validateRemainingConvergence(classified.selected, context) + return deepFreeze({ + classification: "absent", + directGet404At: directCompletedAt, + listAbsentAt: listCompletedAt, + attempts: attempt, + completedAt: monotoneEventTimestamp( + state, + laterTimestamp(directCompletedAt, listCompletedAt), + ), + }) + } + if (attempt === MAXIMUM_CONVERGENCE_ATTEMPTS) break + await runConvergenceWait(context, budget, CONVERGENCE_BACKOFF_MS[attempt - 1]) + } + if (lastPresent === null) { + throw new Error("Convergence did not produce complete target evidence") + } + return deepFreeze({ + classification: "present-unchanged", + releaseEvidence: lastPresent, + attempts: MAXIMUM_CONVERGENCE_ATTEMPTS, + completedAt: monotoneEventTimestamp(state, currentIsoTimestamp()), + }) +} + +function validatePresentConvergence(rawRelease, state) { + const expected = state.lastAuthority?.targetRead?.evidence + if (expected === undefined) { + throw new Error("Present convergence has no recorded authority evidence") + } + assertRawReleaseSemanticEqualsProposal(rawRelease, expected) + return expected +} + +function validateRemainingConvergence(rawReleases, context) { + for (const rawRelease of rawReleases) { + const releaseId = String(rawRelease.id) + const expected = context.proposal.record.releases.find(({ id }) => id === releaseId) + if (expected === undefined) { + throw new Error("Convergence includes an unproposed managed Release") + } + assertRawReleaseSemanticEqualsProposal(rawRelease, expected) + } +} + +function assertRawReleaseSemanticEqualsProposal(rawRelease, expected) { + if (!safeRecord(rawRelease)) { + throw new TypeError("Convergence Release evidence is invalid") + } + const author = safeDataValue(rawRelease, "author") + if (!safeRecord(author)) { + throw new TypeError("Convergence Release author is invalid") + } + const id = safeDataValue(rawRelease, "id") + const semantic = { + name: safeDataValue(rawRelease, "name"), + targetCommitish: safeDataValue(rawRelease, "target_commitish"), + draft: safeDataValue(rawRelease, "draft"), + immutable: safeDataValue(rawRelease, "immutable"), + prerelease: safeDataValue(rawRelease, "prerelease"), + publishedAt: safeDataValue(rawRelease, "published_at"), + body: safeDataValue(rawRelease, "body"), + bodySha256: createHash("sha256") + .update(safeDataValue(rawRelease, "body"), "utf8") + .digest("hex"), + author: { + login: safeDataValue(author, "login"), + id: String(safeDataValue(author, "id")), + nodeId: safeDataValue(author, "node_id"), + }, + } + if (String(id) !== expected.id || !isDeepStrictEqual(semantic, expected.semantic)) { + throw new Error("Convergence Release semantic evidence changed") + } +} + +function nextDeletionStage(state) { + return state.currentTargetReleaseId === DUPLICATES[0] ? "pre-delete-2" : "final" +} + +async function appendDurableEvent(context, expectedJournal, type, payload, recordedAt, boundary) { + return writePrivateEnvelope.withExclusiveTransaction(context.journalPath, async () => { + const current = await loadCurrentJournalLocked(context.journalPath) + if (current.journal.recordSha256 !== expectedJournal.recordSha256) { + throw new Error("Journal changed before its legal append") + } + const timestamp = monotoneJournalTimestamp(current.journal, recordedAt) + const appended = appendJournalEvent(current.journal, type, payload, timestamp) + const bytes = canonicalConsolidationEnvelopeBytes("journal", appended) + await writePrivateEnvelope(context.journalPath, bytes, undefined, current.bytes) + injectFault(context, `after-${boundary}-journal`) + const durable = await readPrivateEnvelope( + context.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ) + if (!durable.equals(bytes)) { + throw new Error("Durable journal differs from its legal append") + } + const headBytes = canonicalJournalHeadBytes(context.journalPath, appended) + await writePrivateEnvelope(current.headPath, headBytes, undefined, current.headBytes) + injectFault(context, `after-${boundary}-head`) + const durableHead = await readPrivateEnvelope(current.headPath, 16 * 1024) + if (!durableHead.equals(headBytes)) { + throw new Error("Durable journal head differs from its legal append") + } + return { journal: parseConsolidationJournal(durable) } + }) +} + +async function loadCurrentJournal(journalPath) { + return writePrivateEnvelope.withExclusiveTransaction(journalPath, () => + loadCurrentJournalLocked(journalPath), + ) +} + +async function loadCurrentJournalLocked(journalPath) { + const bytes = await readPrivateEnvelope( + journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ) + const journal = parseConsolidationJournal(bytes) + const headPath = journalPath.replace(/journal\.json$/u, "journal.head.json") + let headBytes = await readPrivateEnvelope(headPath, 16 * 1024) + const expected = canonicalJournalHeadBytes(journalPath, journal) + if (!headBytes.equals(expected)) { + const predecessor = predecessorJournal(journal) + if ( + predecessor === null || + !headBytes.equals(canonicalJournalHeadBytes(journalPath, predecessor)) + ) { + throw new Error("Journal head is divergent from the durable journal") + } + await writePrivateEnvelope(headPath, expected, undefined, headBytes) + headBytes = await readPrivateEnvelope(headPath, 16 * 1024) + if (!headBytes.equals(expected)) { + throw new Error("Journal head reconciliation was not durable") + } + } + return { bytes, journal, headPath, headBytes } +} + +function predecessorJournal(journal) { + if (journal.record.events.length <= 1) return null + const events = journal.record.events.slice(0, -1) + return createConsolidationEnvelope("journal", { + ...journal.record, + events, + updatedAt: events.at(-1).event.recordedAt, + }) +} + +function canonicalJournalHeadBytes(journalPath, journal) { + return Buffer.from( + `${JSON.stringify({ + schemaVersion: 1, + journalPath, + repository: journal.record.repository, + proposedRecordSha256: journal.record.proposedRecordSha256, + journalRecordSha256: journal.recordSha256, + lastEventSha256: journal.record.events.at(-1).eventSha256, + sequence: journal.record.events.length, + updatedAt: journal.record.updatedAt, + })}\n`, + "utf8", + ) +} + +function injectFault(context, boundary) { + if (context.faultAt === boundary) { + throw new Error("Injected consolidation process loss") + } +} + +function safeDataValue(value, field) { + if (!safeRecord(value)) throw new TypeError("Adapter result is invalid") + return dataValue(value, field) +} + +function monotoneJournalTimestamp(journal, value) { + const candidate = timestampValue(value, "journal append timestamp") + return Date.parse(candidate) < Date.parse(journal.record.updatedAt) + ? journal.record.updatedAt + : candidate +} + +function monotoneEventTimestamp(state, value) { + const authorityTime = state.lastAuthority?.observedAt + return authorityTime !== undefined && Date.parse(value) < Date.parse(authorityTime) + ? authorityTime + : value +} + +function laterTimestamp(first, second) { + return Date.parse(first) >= Date.parse(second) ? first : second +} + +function currentIsoTimestamp() { + return new NATIVE_DATE().toISOString() +} + +function deletionMonotonicClock(timeline) { + if (timeline === undefined) return null + const descriptors = + Array.isArray(timeline) && !utilTypes.isProxy(timeline) + ? Object.getOwnPropertyDescriptors(timeline) + : null + if ( + descriptors === null || + !Object.isFrozen(timeline) || + timeline.length < 2 || + timeline.length > 256 || + !isDeepStrictEqual(Object.keys(descriptors), [ + ...Array.from({ length: timeline.length }, (_, index) => String(index)), + "length", + ]) + ) { + throw new TypeError("Deletion monotonic test timeline is invalid") + } + const values = [] + for (let index = 0; index < timeline.length; index += 1) { + const descriptor = descriptors[String(index)] + if ( + descriptor?.enumerable !== true || + !("value" in descriptor) || + descriptor.get !== undefined || + descriptor.set !== undefined || + !Number.isSafeInteger(descriptor.value) || + descriptor.value < 0 + ) { + throw new TypeError("Deletion monotonic test timeline is invalid") + } + values.push(descriptor.value) + } + let index = 0 + return Object.freeze(() => { + if (index >= values.length) { + throw new Error("Deletion monotonic test timeline is exhausted") + } + const value = values[index] + index += 1 + return value + }) +} + +function deletionWallClock(timeline) { + if (timeline === undefined) { + let previous = null + return Object.freeze(() => { + const current = currentIsoTimestamp() + if (previous !== null && Date.parse(current) < Date.parse(previous)) { + throw new Error("Deletion wall clock reversed") + } + previous = current + return current + }) + } + const descriptors = + Array.isArray(timeline) && !utilTypes.isProxy(timeline) + ? Object.getOwnPropertyDescriptors(timeline) + : null + if ( + descriptors === null || + !Object.isFrozen(timeline) || + timeline.length < 1 || + timeline.length > 256 || + !isDeepStrictEqual(Object.keys(descriptors), [ + ...Array.from({ length: timeline.length }, (_, index) => String(index)), + "length", + ]) + ) { + throw new TypeError("Deletion wall-clock test timeline is invalid") + } + const values = [] + for (let index = 0; index < timeline.length; index += 1) { + const descriptor = descriptors[String(index)] + if ( + descriptor?.enumerable !== true || + !("value" in descriptor) || + descriptor.get !== undefined || + descriptor.set !== undefined + ) { + throw new TypeError("Deletion wall-clock test timeline is invalid") + } + values.push(timestampValue(descriptor.value, "deletion wall-clock timeline")) + } + let index = 0 + let previous = null + return Object.freeze(() => { + if (index >= values.length) { + throw new Error("Deletion wall-clock test timeline is exhausted") + } + const current = values[index] + index += 1 + if (previous !== null && Date.parse(current) < Date.parse(previous)) { + throw new Error("Deletion wall clock reversed") + } + previous = current + return current + }) +} + +function createConvergenceBudget(trustedNow, auditNow) { + const clocks = [monotonicBudgetClock(trustedNow)] + if (auditNow !== null) clocks.push(monotonicBudgetClock(auditNow)) + const checkpoint = () => { + for (const clock of clocks) clock.read() + } + return Object.freeze({ + checkpoint, + start() { + const remaining = Math.min( + ...clocks.map((clock) => Math.floor(clock.deadline - clock.read())), + ) + if (remaining <= 0) { + throw new Error("Convergence wall-clock ceiling expired") + } + return remaining + }, + complete: checkpoint, + }) +} + +function monotonicBudgetClock(now) { + let previous = now() + const deadline = previous + CONVERGENCE_CEILING_MS + return Object.freeze({ + deadline, + read() { + const current = now() + if (current < previous) { + throw new Error("Convergence monotonic clock reversed") + } + if (current > deadline) { + throw new Error("Convergence wall-clock ceiling expired") + } + previous = current + return current + }, + }) +} + +async function runConvergenceRequest(context, budget, operation, request) { + const timeoutMs = budget.start() + const signal = AbortSignal.timeout(timeoutMs) + const requestBudget = Object.freeze({ operation, timeoutMs, signal }) + const value = await raceConvergenceOperation( + () => + Promise.resolve(context.createAdapters(requestBudget)).then((source) => + request(bindAdapters(source)), + ), + signal, + ) + budget.complete() + return value +} + +async function runConvergenceWait(context, budget, policyDelayMs) { + const timeoutMs = budget.start() + const delay = Math.min(policyDelayMs, 30_000, timeoutMs) + if (delay <= 0) { + throw new Error("Convergence backoff exceeded its bounded window") + } + const signal = AbortSignal.timeout(timeoutMs) + await raceConvergenceOperation(() => context.wait(delay, { signal, timeoutMs }), signal) + budget.complete() +} + +async function raceConvergenceOperation(operation, signal) { + return new Promise((resolve, reject) => { + let settled = false + const finish = (callback, value) => { + if (settled) return + settled = true + signal.removeEventListener("abort", onAbort) + callback(value) + } + const onAbort = () => finish(reject, new Error("Convergence wall-clock ceiling expired")) + signal.addEventListener("abort", onAbort, { once: true }) + Promise.resolve() + .then(operation) + .then( + (value) => finish(resolve, value), + (error) => finish(reject, error), + ) + }) +} + +export async function inspectDuplicateDrafts(input, dependencies) { + let context + try { + context = normalizeInvocation(input, dependencies) + } catch { + throw new TypeError("Duplicate-draft inspection input is invalid.") + } + + try { + const rootGuard = + context.repositoryRootIdentity ?? (await captureRepositoryRoot(context.repositoryRoot)) + const initialMetadata = await captureMetadata(context) + const initialInventory = await captureNpmInventory({ + stage: "inspect-initial", + candidate: CANDIDATE, + npm: context.adapters.npm, + now: context.now, + }) + const gapStartedAt = Date.parse(initialInventory.completedAt) + + const releases = await readReleaseEnumeration(context) + const inspected = await inspectEquivalentDrafts({ + candidate: CANDIDATE, + survivorId: SURVIVOR, + duplicateIds: DUPLICATES, + releases, + github: context.adapters.github, + attestations: context.adapters.attestations, + }) + + const afterVerification = timestamp(context.now, "inspection clock") + const elapsed = Date.parse(afterVerification) - gapStartedAt + if (elapsed < 0) throw new Error("Inspection clock is not monotone") + const remaining = Math.max(0, OBSERVATION_GAP_MS - elapsed) + if (remaining > 0) { + const signal = AbortSignal.timeout(remaining + 5_000) + await context.wait(remaining, { signal }) + } + const readyBoundary = timestamp(context.now, "ready boundary clock") + if (Date.parse(readyBoundary) - gapStartedAt < OBSERVATION_GAP_MS) { + throw new Error("Observation gap did not reach sixty seconds") + } + + const readyInventory = await captureNpmInventory({ + stage: "inspect-ready", + candidate: CANDIDATE, + npm: context.adapters.npm, + now: context.now, + }) + if (Date.parse(readyInventory.startedAt) - gapStartedAt < OBSERVATION_GAP_MS) { + throw new Error("Ready inventory began before the observation gap closed") + } + const finalReleaseEnumeration = await readReleaseEnumeration(context) + const finalMetadata = await captureMetadata(context) + assertStableMetadata(initialMetadata, finalMetadata) + const preliminaryEnvelope = proposalEnvelope({ + metadata: finalMetadata, + npmInventories: [initialInventory, readyInventory], + releases: inspected.releases, + payloadProof: inspected.payloadProof, + inspectedAt: timestamp(context.now, "preliminary inspection clock"), + }) + classifyConsolidationReleases( + finalReleaseEnumeration, + preliminaryEnvelope.record, + "pre-delete-1", + ) + const terminal = exactPlain( + await context.adapters.captureInspectionTerminal({ + candidate: CANDIDATE, + releases: inspected.releases, + }), + ["releases", "completedAt"], + "inspection terminal", + ) + if (!Array.isArray(terminal.releases) || terminal.releases.length !== 3) { + throw new Error("Inspection terminal evidence is incomplete") + } + terminal.completedAt = timestampValue(terminal.completedAt, "inspection terminal completion") + context.adapters.assertInspectionTerminalSealed() + + const envelope = proposalEnvelope({ + metadata: finalMetadata, + npmInventories: [initialInventory, readyInventory], + releases: terminal.releases, + payloadProof: inspected.payloadProof, + inspectedAt: terminal.completedAt, + }) + const bytes = canonicalConsolidationEnvelopeBytes("proposed", envelope) + const absoluteOutput = context.absoluteOutput + await revalidateRepositoryRoot(rootGuard) + await writePrivateEnvelope(absoluteOutput, bytes) + return deepFreeze({ + proposalSha256: envelope.recordSha256, + version: CANDIDATE.version, + commitSha: CANDIDATE.commitSha, + survivor: SURVIVOR, + duplicates: [...DUPLICATES], + output: OUTPUT, + }) + } catch { + throw new Error("Duplicate-draft inspection failed.") + } +} + +Object.defineProperty(inspectDuplicateDrafts, "captureRepositoryRoot", { + value: Object.freeze(captureRepositoryRoot), + enumerable: false, + writable: false, + configurable: false, +}) + +async function readReleaseEnumeration(context) { + const envelope = exactPlain( + await context.adapters.github.listReleases(), + ["status", "operation", "httpStatus", "code", "value"], + "Release enumeration", + ) + if ( + envelope.status !== "PRESENT" || + envelope.operation !== "releases" || + envelope.httpStatus !== 200 || + envelope.code !== null || + !Array.isArray(envelope.value) + ) { + throw new Error("Release enumeration is incomplete") + } + return envelope.value +} + +function proposalEnvelope({ metadata, npmInventories, releases, payloadProof, inspectedAt }) { + return createConsolidationEnvelope("proposed", { + schemaVersion: 1, + repository: metadata.repository, + controller: metadata.controller, + candidate: CANDIDATE, + roles: { survivor: SURVIVOR, duplicates: DUPLICATES }, + confirmation: { + version: CANDIDATE.version, + commitSha: CANDIDATE.commitSha, + survivor: SURVIVOR, + duplicates: DUPLICATES, + template: "<64-lowercase-hex-digest>", + }, + annotatedTag: metadata.annotatedTag, + workflowAuthority: metadata.workflowAuthority, + npmInventories, + releases, + payloadProof, + inspectedAt, + }) +} + +async function captureMetadata(context) { + const local = exactPlain( + await context.adapters.local.readState(), + ["headSha", "branch", "porcelainStatus", "originMainSha"], + "local checkout", + ) + if ( + !/^[0-9a-f]{40}$/u.test(local.headSha) || + local.headSha === CANDIDATE.commitSha || + local.originMainSha === CANDIDATE.commitSha || + local.originMainSha !== local.headSha || + local.branch !== "main" || + local.porcelainStatus !== "" + ) { + throw new Error("Local checkout authority is invalid") + } + + const repository = exactPlain( + await context.adapters.github.getRepository(), + ["name", "id", "defaultBranch"], + "repository authority", + ) + const actor = exactPlain( + await context.adapters.github.getAuthenticatedUser(), + ["login", "id"], + "actor authority", + ) + if (!isDeepStrictEqual({ ...repository, actor }, REPOSITORY)) { + throw new Error("Repository or actor authority is invalid") + } + const githubMainSha = await context.adapters.github.getDefaultBranchSha() + if (githubMainSha === CANDIDATE.commitSha || githubMainSha !== local.headSha) { + throw new Error("GitHub main authority is invalid") + } + const workflow = exactPlain( + await context.adapters.github.getWorkflowState(), + ["workflowId", "path", "state"], + "workflow authority", + ) + if ( + !/^[1-9][0-9]*$/u.test(workflow.workflowId) || + workflow.path !== ".github/workflows/release.yml" || + workflow.state !== "disabled_manually" + ) { + throw new Error("Release workflow authority is invalid") + } + const runRead = exactPlain( + await context.adapters.github.listNonterminalWorkflowRuns(WORKFLOW_QUERY), + ["query", "runs"], + "workflow-run authority", + ) + if ( + !isDeepStrictEqual(runRead.query, WORKFLOW_QUERY) || + !Array.isArray(runRead.runs) || + runRead.runs.length !== 0 + ) { + throw new Error("Release workflow has nonterminal runs") + } + const annotatedTag = exactPlain( + await context.adapters.github.getAnnotatedTag({ name: CANDIDATE.tag }), + ["name", "objectSha", "targetSha", "objectType", "observedAt"], + "annotated-tag authority", + ) + if ( + annotatedTag.name !== CANDIDATE.tag || + !/^[0-9a-f]{40}$/u.test(annotatedTag.objectSha) || + annotatedTag.targetSha !== CANDIDATE.commitSha || + annotatedTag.objectType !== "tag" + ) { + throw new Error("Annotated tag authority is invalid") + } + annotatedTag.observedAt = timestampValue(annotatedTag.observedAt, "tag timestamp") + const observedAt = timestamp(context.now, "workflow observation clock") + return deepFreeze({ + repository: { ...repository, actor }, + controller: { + headSha: local.headSha, + originMainSha: local.originMainSha, + githubMainSha, + }, + annotatedTag, + workflowAuthority: { + ...workflow, + query: WORKFLOW_QUERY, + nonterminalRuns: [], + observedAt, + }, + }) +} + +function assertStableMetadata(initial, final) { + const stableInitial = structuredClone(initial) + const stableFinal = structuredClone(final) + delete stableInitial.annotatedTag.observedAt + delete stableFinal.annotatedTag.observedAt + delete stableInitial.workflowAuthority.observedAt + delete stableFinal.workflowAuthority.observedAt + if (!isDeepStrictEqual(stableInitial, stableFinal)) { + throw new Error("Authority changed during the observation gap") + } +} + +function normalizeInvocation(input, dependencies) { + const normalizedInput = exactPlain( + input, + ["version", "commitSha", "survivor", "duplicates", "output"], + "inspection input", + ) + if ( + normalizedInput.version !== CANDIDATE.version || + normalizedInput.commitSha !== CANDIDATE.commitSha || + normalizedInput.survivor !== SURVIVOR || + normalizedInput.output !== OUTPUT || + !safeArrayEquals(normalizedInput.duplicates, DUPLICATES) + ) { + throw new TypeError("Inspection does not identify the approved incident") + } + const normalizedDependencies = exactOptionalFields( + dependencies, + ["repositoryRoot", "adapters", "now", "wait"], + ["repositoryRootIdentity"], + "inspection dependencies", + ) + if ( + typeof normalizedDependencies.repositoryRoot !== "string" || + !path.isAbsolute(normalizedDependencies.repositoryRoot) || + path.normalize(normalizedDependencies.repositoryRoot) !== normalizedDependencies.repositoryRoot + ) { + throw new TypeError("Repository root is not canonical") + } + const adapters = bindAdapters(normalizedDependencies.adapters) + const now = trustedClock(safeFunction(normalizedDependencies.now, "inspection clock")) + const wait = safeFunction(normalizedDependencies.wait, "inspection waiter") + const repositoryRootIdentity = bindRepositoryRootIdentity( + normalizedDependencies.repositoryRootIdentity, + normalizedDependencies.repositoryRoot, + ) + const absoluteOutput = path.join(normalizedDependencies.repositoryRoot, ...OUTPUT.split("/")) + if ( + path.relative(normalizedDependencies.repositoryRoot, absoluteOutput) !== + OUTPUT.split("/").join(path.sep) || + path.basename(absoluteOutput) !== "duplicate-draft-consolidation.proposed.json" + ) { + throw new TypeError("Proposal output is outside the approved path") + } + return { + adapters, + now, + wait, + absoluteOutput, + repositoryRootIdentity, + repositoryRoot: normalizedDependencies.repositoryRoot, + } +} + +function bindAdapters(value) { + if (!safeRecord(value) || !Object.isFrozen(value)) throw new TypeError("Adapters are invalid") + const names = Object.keys(value) + if (!isDeepStrictEqual(names, ["local", "github", "npm", "attestations", "writer"])) { + throw new TypeError("Adapter facade is not exact") + } + if ( + !isDeepStrictEqual(Object.getOwnPropertyNames(value), [ + ...names, + "captureConsolidationAuthority", + "captureInspectionTerminal", + "assertInspectionTerminalSealed", + ]) || + Object.getOwnPropertySymbols(value).length !== 0 + ) { + throw new TypeError("Adapter facade hidden fields are invalid") + } + const captureAuthority = hiddenAdapterMethod(value, "captureConsolidationAuthority") + const captureInspectionTerminal = hiddenAdapterMethod(value, "captureInspectionTerminal") + const assertInspectionTerminalSealed = hiddenAdapterMethod( + value, + "assertInspectionTerminalSealed", + ) + const adapters = { + local: bindFacade(dataValue(value, "local"), ["readState"], "local adapter"), + github: bindFacade( + dataValue(value, "github"), + [ + "getRepository", + "getAuthenticatedUser", + "getDefaultBranchSha", + "getWorkflowState", + "listNonterminalWorkflowRuns", + "getAnnotatedTag", + "listReleases", + "getRelease", + "listReleaseAssets", + "downloadReleaseAsset", + ], + "GitHub adapter", + ), + npm: bindFacade(dataValue(value, "npm"), ["observePackageVersion"], "npm adapter"), + attestations: bindFacade(dataValue(value, "attestations"), ["verify"], "attestation adapter"), + writer: bindFacade(dataValue(value, "writer"), ["deleteDuplicate"], "writer adapter"), + } + for (const [name, operation] of [ + ["captureConsolidationAuthority", captureAuthority], + ["captureInspectionTerminal", captureInspectionTerminal], + ["assertInspectionTerminalSealed", assertInspectionTerminalSealed], + ]) { + Object.defineProperty(adapters, name, { + value: (...args) => Reflect.apply(operation, value, args), + enumerable: false, + writable: false, + configurable: false, + }) + Object.freeze(adapters[name]) + } + return Object.freeze(adapters) +} + +function hiddenAdapterMethod(value, name) { + const descriptor = Object.getOwnPropertyDescriptor(value, name) + if ( + descriptor?.enumerable !== false || + descriptor.writable !== false || + descriptor.configurable !== false || + typeof descriptor.value !== "function" || + utilTypes.isProxy(descriptor.value) || + !Object.isFrozen(descriptor.value) + ) { + throw new TypeError("Adapter hidden entrypoint is invalid") + } + return descriptor.value +} + +async function captureRepositoryRoot(repositoryRoot) { + if ( + typeof repositoryRoot !== "string" || + !path.isAbsolute(repositoryRoot) || + path.normalize(repositoryRoot) !== repositoryRoot || + (await realpath(repositoryRoot)) !== repositoryRoot + ) { + throw new Error("Repository root is not physically canonical") + } + const effectiveUserId = currentEffectiveUserId() + const root = await captureDirectoryIdentity(repositoryRoot, effectiveUserId, false) + const dawnPath = path.join(repositoryRoot, ".dawn") + const releasePath = path.join(dawnPath, "release") + if (path.relative(repositoryRoot, releasePath) !== ".dawn/release") { + throw new Error("Proposal directory is outside the repository root") + } + const dawn = await captureDirectoryIdentity(dawnPath, effectiveUserId, true) + const release = await captureDirectoryIdentity(releasePath, effectiveUserId, true) + if (!dawn.exists && release.exists) { + throw new Error("Proposal directory containment is invalid") + } + const capability = {} + Object.defineProperty(capability, "toJSON", { + value() { + throw new TypeError("Repository-root identity cannot be serialized") + }, + enumerable: false, + writable: false, + configurable: false, + }) + Object.freeze(capability.toJSON) + Object.freeze(capability) + ROOT_GUARDS.set( + capability, + Object.freeze({ + repositoryRoot, + effectiveUserId, + root, + dawn, + release, + }), + ) + return capability +} + +function bindRepositoryRootIdentity(value, repositoryRoot) { + if (value === undefined) return undefined + const record = value !== null && typeof value === "object" ? ROOT_GUARDS.get(value) : undefined + if (record === undefined || record.repositoryRoot !== repositoryRoot) { + throw new TypeError("Repository-root identity is invalid") + } + return value +} + +async function revalidateRepositoryRoot(capability) { + const record = ROOT_GUARDS.get(capability) + if (record === undefined) throw new Error("Repository-root identity is invalid") + if ((await realpath(record.repositoryRoot)) !== record.repositoryRoot) { + throw new Error("Repository root changed before proposal publication") + } + await assertDirectoryIdentity(record.repositoryRoot, record.root, record.effectiveUserId) + const currentComponents = [] + for (const [target, expected] of [ + [path.join(record.repositoryRoot, ".dawn"), record.dawn], + [path.join(record.repositoryRoot, ".dawn", "release"), record.release], + ]) { + if (expected.exists) { + currentComponents.push([ + target, + await assertDirectoryIdentity(target, expected, record.effectiveUserId), + ]) + continue + } + await assertDirectoryAbsent(target) + await mkdir(target, { mode: 0o700 }) + currentComponents.push([ + target, + await captureDirectoryIdentity(target, record.effectiveUserId, false), + ]) + } + await assertDirectoryIdentity(record.repositoryRoot, record.root, record.effectiveUserId) + for (const [target, identity] of currentComponents) { + await assertDirectoryIdentity(target, identity, record.effectiveUserId) + } +} + +async function captureDirectoryIdentity(target, effectiveUserId, allowAbsent) { + let status + try { + status = await lstat(target, { bigint: true }) + } catch (error) { + if (allowAbsent && error?.code === "ENOENT") { + return Object.freeze({ exists: false }) + } + throw error + } + if ( + status.isSymbolicLink() || + !status.isDirectory() || + status.uid !== effectiveUserId || + (status.mode & 0o022n) !== 0n || + (await realpath(target)) !== target + ) { + throw new Error("Repository path identity is unsafe") + } + const current = await lstat(target, { bigint: true }) + if ( + current.isSymbolicLink() || + !current.isDirectory() || + current.dev !== status.dev || + current.ino !== status.ino || + current.mode !== status.mode || + current.uid !== status.uid + ) { + throw new Error("Repository path identity changed during validation") + } + return Object.freeze({ + exists: true, + dev: current.dev, + ino: current.ino, + mode: current.mode, + uid: current.uid, + }) +} + +async function assertDirectoryIdentity(target, expected, effectiveUserId) { + const observed = await captureDirectoryIdentity(target, effectiveUserId, false) + if ( + !expected.exists || + observed.dev !== expected.dev || + observed.ino !== expected.ino || + observed.mode !== expected.mode || + observed.uid !== expected.uid + ) { + throw new Error("Repository path identity changed before publication") + } + return observed +} + +async function assertDirectoryAbsent(target) { + try { + await lstat(target, { bigint: true }) + } catch (error) { + if (error?.code === "ENOENT") return + throw error + } + throw new Error("Repository path appeared before publication") +} + +function currentEffectiveUserId() { + if (typeof process.geteuid !== "function") { + throw new Error("Repository owner identity is unavailable") + } + const value = process.geteuid() + if (!Number.isSafeInteger(value) || value < 0) { + throw new Error("Repository owner identity is invalid") + } + return BigInt(value) +} + +function bindFacade(value, methods, label) { + if ( + !safeRecord(value) || + !Object.isFrozen(value) || + !isDeepStrictEqual([...Object.keys(value)].sort(), [...methods].sort()) || + !isDeepStrictEqual([...Object.getOwnPropertyNames(value)].sort(), [...methods].sort()) || + Object.getOwnPropertySymbols(value).length !== 0 + ) { + throw new TypeError(`${label} is invalid`) + } + const facade = {} + for (const method of methods) { + const operation = safeFunction(dataValue(value, method), `${label} method`) + facade[method] = (...args) => Reflect.apply(operation, value, args) + } + return Object.freeze(facade) +} + +function exactPlain(value, fields, label) { + if (!safeRecord(value) || Object.getOwnPropertySymbols(value).length !== 0) { + throw new TypeError(`${label} must be a plain object`) + } + const names = Object.getOwnPropertyNames(value) + if (!isDeepStrictEqual(names, fields)) throw new TypeError(`${label} fields are invalid`) + const output = {} + for (const field of fields) output[field] = dataValue(value, field) + return output +} + +function exactOptionalFields(value, fields, optionalFields, label) { + if (!safeRecord(value) || Object.getOwnPropertySymbols(value).length !== 0) { + throw new TypeError(`${label} must be a plain object`) + } + const names = Object.getOwnPropertyNames(value) + const expected = [...fields, ...optionalFields.filter((field) => names.includes(field))] + if (!isDeepStrictEqual(names, expected)) throw new TypeError(`${label} fields are invalid`) + const output = {} + for (const field of names) output[field] = dataValue(value, field) + return output +} + +function dataValue(value, field) { + const descriptor = Object.getOwnPropertyDescriptor(value, field) + if ( + descriptor?.enumerable !== true || + !("value" in descriptor) || + descriptor.get !== undefined || + descriptor.set !== undefined + ) { + throw new TypeError("Required data property is unsafe") + } + return descriptor.value +} + +function safeRecord(value) { + return ( + value !== null && + typeof value === "object" && + !utilTypes.isProxy(value) && + [Object.prototype, null].includes(Object.getPrototypeOf(value)) + ) +} + +function safeFunction(value, label) { + if (typeof value !== "function" || utilTypes.isProxy(value)) + throw new TypeError(`${label} is invalid`) + return value +} + +function safeArrayEquals(value, expected) { + if ( + !Array.isArray(value) || + utilTypes.isProxy(value) || + value.length !== expected.length || + Object.getOwnPropertySymbols(value).length !== 0 + ) + return false + if (!isDeepStrictEqual(Object.getOwnPropertyNames(value), ["0", "1", "length"])) return false + return expected.every((entry, index) => dataValue(value, String(index)) === entry) +} + +function timestamp(now, label) { + return timestampValue(Reflect.apply(now, undefined, []), label) +} + +function trustedClock(source) { + let previous = null + const clock = () => { + const value = timestampValue(Reflect.apply(source, undefined, []), "trusted inspection clock") + const current = Date.parse(value) + if (previous !== null && current < previous) { + throw new TypeError("Trusted inspection clock is not monotone") + } + previous = current + return value + } + return Object.freeze(clock) +} + +function timestampValue(value, label) { + if ( + typeof value !== "string" || + !/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/u.test(value) || + !Number.isFinite(Date.parse(value)) + ) { + throw new TypeError(`${label} is invalid`) + } + return value +} + +function deepFreeze(value) { + if (value !== null && typeof value === "object" && !Object.isFrozen(value)) { + for (const child of Object.values(value)) deepFreeze(child) + Object.freeze(value) + } + return value +} diff --git a/scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs b/scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs new file mode 100644 index 000000000..107fd2d17 --- /dev/null +++ b/scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs @@ -0,0 +1,2054 @@ +import assert from "node:assert/strict" +import { readFile, rm } from "node:fs/promises" +import test from "node:test" + +import { + createDuplicateDraftConsolidationAdapters, + createExactDuplicateDeleteEffect, +} from "../duplicate-draft-consolidation-adapters.mjs" +import { createAuthorizedDeleteHarness } from "./support/duplicate-draft-consolidation-authorized-delete.mjs" + +const REPOSITORY = "cacheplane/dawnai" +const API_ORIGIN = "https://api.github.com" +const BASE = `${API_ORIGIN}/repos/${REPOSITORY}` +const SURVIVOR = "379991871" +const DUPLICATES = Object.freeze(["379982100", "379986168"]) +const TOKEN = "github_test_token_123456789" +const HEAD_SHA = "0123456789abcdef0123456789abcdef01234567" +const TAG_OBJECT_SHA = "123456789abcdef0123456789abcdef012345678" +const USER_AGENT = "dawn-duplicate-draft-consolidation/1" +const NOW = "2026-09-01T12:34:56.789Z" +const TEMPORARY_ROOTS = [] +test.after(async () => { + await Promise.all(TEMPORARY_ROOTS.map((root) => rm(root, { recursive: true, force: true }))) +}) + +test("composition exposes only one safe authority capture entrypoint and no raw authority surface", async () => { + const recording = recordingFetch([ + jsonResponse({ + id: 1_210_070_282, + full_name: REPOSITORY, + default_branch: "main", + }), + ]) + const commandCalls = [] + const adapters = await createAdapters({ + token: TOKEN, + fetchImpl: recording.fetchImpl, + run: commandRunner(commandCalls), + }) + + assert.deepEqual(await adapters.github.getRepository(), { + name: REPOSITORY, + id: "1210070282", + defaultBranch: "main", + }) + assert.equal( + commandCalls.some(([command, args]) => command === "gh" && args[0] === "auth"), + false, + ) + assert.equal(recording.calls.length, 1) + assert.deepEqual(recording.calls[0].init.headers, githubHeaders()) + assert.equal(recording.calls[0].url, `${BASE}`) + assert.equal(JSON.stringify(adapters).includes(TOKEN), false) + assert.deepEqual(Object.keys(adapters).sort(), [ + "attestations", + "github", + "local", + "npm", + "writer", + ]) + assert.deepEqual(Reflect.ownKeys(adapters).sort(), [ + "assertInspectionTerminalSealed", + "attestations", + "captureConsolidationAuthority", + "captureInspectionTerminal", + "github", + "local", + "npm", + "writer", + ]) + assert.equal(typeof adapters.captureConsolidationAuthority, "function") + assert.equal(typeof adapters.captureInspectionTerminal, "function") + assert.equal(typeof adapters.assertInspectionTerminalSealed, "function") + for (const forbidden of [ + "authorityEpoch", + "beginAuthorityCapture", + "beginTerminalRead", + "bindAuthority", + "acceptTransitionBoundary", + "armTransition", + "armTask6Transition", + ]) { + assert.equal(Reflect.ownKeys(adapters).includes(forbidden), false) + assert.equal(Reflect.get(adapters, forbidden), undefined) + } + assert.deepEqual(Reflect.ownKeys(new Proxy(adapters, {})), Reflect.ownKeys(adapters)) + assert.equal(JSON.stringify(adapters).includes("authority"), false) +}) + +test("a convergence budget aborts the underlying production GitHub request", async () => { + const controller = new AbortController() + const requestBudget = Object.freeze({ + operation: "release", + timeoutMs: 10_000, + signal: controller.signal, + }) + let entered + const requestEntered = new Promise((resolve) => { + entered = resolve + }) + let receivedSignal + const adapters = await createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + token: TOKEN, + environment: { HOME: "/home/release", PATH: "/tools" }, + requestBudget, + dependencies: { + now: () => NOW, + run: commandRunner([]), + async fetchImpl(_url, init) { + receivedSignal = init.signal + entered() + return new Promise((_resolve, reject) => { + init.signal.addEventListener("abort", () => reject(init.signal.reason), { once: true }) + }) + }, + }, + }) + + const pending = adapters.github.getRelease({ releaseId: DUPLICATES[0] }) + await requestEntered + controller.abort() + const result = await pending + + assert.equal(receivedSignal.aborted, true) + assert.equal(result.status, "AMBIGUOUS") + assert.equal(result.operation, "release") + assert.equal(result.code, "ABORTED") +}) + +test("workflow-run reads require and return the exact frozen executed query", async () => { + const recording = recordingFetch([jsonResponse({ total_count: 0, workflow_runs: [] })]) + const adapters = await createAdapters({ + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + }) + const query = workflowQuery() + const result = await adapters.github.listNonterminalWorkflowRuns(query) + assert.deepEqual(result, { query: workflowQuery(), runs: [] }) + assert.notEqual(result.query, query) + assert.notEqual(result.query.statuses, query.statuses) + assert.equal(Object.isFrozen(result), true) + assert.equal(Object.isFrozen(result.query), true) + assert.equal(Object.isFrozen(result.query.statuses), true) + assert.equal(new URL(recording.calls[0].url).search, "?per_page=100&page=1") + + for (const invalid of [ + undefined, + { ...query }, + Object.freeze({ ...query, statuses: [...query.statuses] }), + Object.freeze({ ...query, perPage: 99 }), + Object.freeze({ ...query, maximumPages: 99 }), + Object.freeze({ + ...query, + statuses: Object.freeze(query.statuses.slice(1)), + }), + Object.freeze({ ...query, statuses: new Proxy(query.statuses, {}) }), + ]) { + await assert.rejects( + adapters.github.listNonterminalWorkflowRuns(invalid), + /query|frozen|status|page|option|exact/iu, + ) + } + assert.equal(recording.calls.length, 1) +}) + +test("composition resolves safe environment credentials before gh auth token", async () => { + for (const [name, value] of [ + ["GH_TOKEN", "gh_environment_token"], + ["GITHUB_TOKEN", "github_environment_token"], + ]) { + const recording = recordingFetch([ + jsonResponse({ + id: 1_210_070_282, + full_name: REPOSITORY, + default_branch: "main", + }), + ]) + const calls = [] + const adapters = await createAdapters({ + token: undefined, + environment: { HOME: "/home/release", PATH: "/tools", [name]: value }, + fetchImpl: recording.fetchImpl, + run: commandRunner(calls), + }) + + await adapters.github.getRepository() + assert.equal( + calls.some(([command, args]) => command === "gh" && args[0] === "auth"), + false, + ) + assert.equal(recording.calls[0].init.headers.Authorization, `Bearer ${value}`) + assert.equal(JSON.stringify(calls).includes(value), false) + assert.equal(JSON.stringify(adapters).includes(value), false) + } +}) + +test("composition falls back to one bounded non-shell gh auth token command", async () => { + const calls = [] + const recording = recordingFetch([ + jsonResponse({ + id: 1_210_070_282, + full_name: REPOSITORY, + default_branch: "main", + }), + ]) + const adapters = await createAdapters({ + token: undefined, + environment: { + HOME: "/home/release", + PATH: "/tools", + NODE_OPTIONS: "--require /tmp/unsafe.cjs", + UNRELATED_SECRET: "must-not-leak", + }, + fetchImpl: recording.fetchImpl, + run: commandRunner(calls, { authToken: TOKEN }), + }) + + await adapters.github.getRepository() + assert.deepEqual(calls[0], [ + "gh", + ["auth", "token"], + { + cwd: "/workspace", + env: { HOME: "/home/release", PATH: "/tools", NO_COLOR: "1" }, + }, + ]) + assert.equal(calls[0][2].shell, undefined) + assert.equal(JSON.stringify(calls).includes(TOKEN), false) + assert.equal(JSON.stringify(calls).includes("must-not-leak"), false) + assert.equal(JSON.stringify(calls).includes("unsafe"), false) + assert.equal(recording.calls[0].init.headers.Authorization, `Bearer ${TOKEN}`) +}) + +test("command environment canonicalizes bounded Windows runtime aliases", async () => { + const calls = [] + const environment = { + ci: "true", + ColorTerm: "truecolor", + ComSpec: "C:\\Windows\\System32\\cmd.exe", + Force_Color: "1", + Github_Actions: "true", + Home: "C:\\Users\\release", + lang: "en_US.UTF-8", + Lc_All: "C", + Path: "C:\\tools", + path: "C:\\tools", + PathExt: ".COM;.EXE;.BAT;.CMD", + SystemRoot: "C:\\Windows", + Temp: "C:\\Temp", + term: "xterm-256color", + tmp: "C:\\Tmp", + TmpDir: "C:\\TmpDir", + UserProfile: "C:\\Users\\release", + Gh_ToKeN: "must-not-be-a-token", + Node_Options: "--require C:\\unsafe.cjs", + Unrelated_Secret: "must-not-leak", + } + const adapters = await createAdapters({ + token: undefined, + environment, + fetchImpl: assert.fail, + run: commandRunner(calls, { authToken: TOKEN }), + }) + + await adapters.local.readState() + const expectedEnvironment = { + CI: "true", + COLORTERM: "truecolor", + COMSPEC: "C:\\Windows\\System32\\cmd.exe", + FORCE_COLOR: "1", + GITHUB_ACTIONS: "true", + HOME: "C:\\Users\\release", + LANG: "en_US.UTF-8", + LC_ALL: "C", + PATH: "C:\\tools", + PATHEXT: ".COM;.EXE;.BAT;.CMD", + SYSTEMROOT: "C:\\Windows", + TEMP: "C:\\Temp", + TERM: "xterm-256color", + TMP: "C:\\Tmp", + TMPDIR: "C:\\TmpDir", + USERPROFILE: "C:\\Users\\release", + NO_COLOR: "1", + } + for (const [, , options] of calls) { + assert.deepEqual(options.env, expectedEnvironment) + assert.equal(Object.hasOwn(options.env, "Path"), false) + assert.equal(JSON.stringify(options.env).includes("must-not-be-a-token"), false) + assert.equal(JSON.stringify(options.env).includes("unsafe"), false) + assert.equal(JSON.stringify(options.env).includes("must-not-leak"), false) + } +}) + +test("Windows environment rejects conflicting case aliases before command invocation", async () => { + const logicalNames = [ + "CI", + "COLORTERM", + "COMSPEC", + "FORCE_COLOR", + "GITHUB_ACTIONS", + "HOME", + "LANG", + "LC_ALL", + "PATH", + "PATHEXT", + "SYSTEMROOT", + "TEMP", + "TERM", + "TMP", + "TMPDIR", + "USERPROFILE", + ] + for (const name of logicalNames) { + const commandCalls = [] + const fetchCalls = [] + const alias = name === "PATH" ? "Path" : name.toLowerCase() + const environment = { + ComSpec: "C:\\Windows\\System32\\cmd.exe", + PathExt: ".COM;.EXE;.BAT;.CMD", + SystemRoot: "C:\\Windows", + [name]: "first", + [alias]: "second", + } + + await assert.rejects( + createAdapters({ + environment, + fetchImpl: (...args) => fetchCalls.push(args), + run: async (...args) => { + commandCalls.push(args) + return { exitCode: 0, stdout: "", stderr: "" } + }, + }), + /environment|alias|conflict|duplicate|unsafe/iu, + name, + ) + assert.equal(commandCalls.length, 0, name) + assert.equal(fetchCalls.length, 0, name) + } +}) + +test("POSIX environment keeps approved names case-sensitive", async () => { + const calls = [] + const adapters = await createAdapters({ + token: undefined, + environment: { + HOME: "/home/release", + home: "/unsafe-home", + PATH: "/usr/bin", + Path: "/opt/legacy-bin", + path: "/unsafe-bin", + SYSTEMROOT: "posix-data", + temp: "/unsafe-temp", + Gh_ToKeN: "must-not-be-a-token", + }, + fetchImpl: assert.fail, + run: commandRunner(calls, { authToken: TOKEN }), + }) + + await adapters.local.readState() + for (const [, , options] of calls) { + assert.deepEqual(options.env, { + HOME: "/home/release", + PATH: "/usr/bin", + SYSTEMROOT: "posix-data", + NO_COLOR: "1", + }) + } + + const pathOnlyCalls = [] + const pathOnly = await createAdapters({ + environment: { HOME: "/home/release", Path: "/opt/legacy-bin" }, + fetchImpl: assert.fail, + run: commandRunner(pathOnlyCalls), + }) + await pathOnly.local.readState() + for (const [, , options] of pathOnlyCalls) { + assert.deepEqual(options.env, { + HOME: "/home/release", + Path: "/opt/legacy-bin", + NO_COLOR: "1", + }) + } +}) + +test("token inputs and command output are strictly bounded and never echoed in errors", async () => { + for (const token of [null, 42, "", "bad\ntoken", "bad\u0000token", "x".repeat(4_097)]) { + await assert.rejects( + createAdapters({ token, fetchImpl: assert.fail, run: assert.fail }), + (error) => typeof token !== "string" || token.length === 0 || !String(error).includes(token), + ) + } + + for (const stdout of ["", "bad\ntoken\n", `${"x".repeat(4_097)}\n`]) { + await assert.rejects( + createAdapters({ + token: undefined, + fetchImpl: assert.fail, + run: async () => ({ exitCode: 0, stdout, stderr: TOKEN }), + }), + (error) => + (stdout.length === 0 || !String(error).includes(stdout)) && !String(error).includes(TOKEN), + ) + } + + const source = await readFile( + new URL("../duplicate-draft-consolidation-adapters.mjs", import.meta.url), + "utf8", + ) + assert.equal(source.includes(TOKEN), false) +}) + +test("options and dependencies are exact descriptor-safe snapshots", async () => { + let invoked = 0 + const accessor = {} + Object.defineProperty(accessor, "token", { + enumerable: true, + get() { + invoked += 1 + return TOKEN + }, + }) + await assert.rejects( + createDuplicateDraftConsolidationAdapters(accessor), + /accessor|descriptor|option|unsafe/iu, + ) + assert.equal(invoked, 0) + + const dependencyAccessor = {} + Object.defineProperty(dependencyAccessor, "fetchImpl", { + enumerable: true, + get() { + invoked += 1 + return assert.fail + }, + }) + await assert.rejects( + createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + token: TOKEN, + dependencies: dependencyAccessor, + }), + /dependenc|accessor|descriptor|unsafe/iu, + ) + assert.equal(invoked, 0) + + for (const value of [ + { cwd: "/workspace", token: TOKEN, extra: true }, + Object.assign(Object.create({ inherited: true }), { + cwd: "/workspace", + token: TOKEN, + }), + Object.assign({ cwd: "/workspace", token: TOKEN }, { [Symbol("hidden")]: true }), + ]) { + await assert.rejects(createDuplicateDraftConsolidationAdapters(value)) + } + + const hidden = { cwd: "/workspace", token: TOKEN } + Object.defineProperty(hidden, "extra", { value: true }) + await assert.rejects(createDuplicateDraftConsolidationAdapters(hidden)) + + const proxy = new Proxy( + { cwd: "/workspace", token: TOKEN }, + { + ownKeys() { + invoked += 1 + return ["cwd", "token"] + }, + }, + ) + await assert.rejects(createDuplicateDraftConsolidationAdapters(proxy), /proxy|unsafe|option/iu) + assert.equal(invoked, 0) + + await assert.rejects( + createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + token: TOKEN, + dependencies: { fetchImpl: undefined }, + }), + /dependenc|fetch|function|invalid/iu, + ) + for (const dependencies of [null, undefined]) { + await assert.rejects( + createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + token: TOKEN, + dependencies, + }), + /dependenc|plain object|invalid/iu, + ) + } +}) + +test("tokens reject whitespace even when it is not an HTTP control character", async () => { + for (const token of ["bad token", " leading", "trailing ", "bad\u00a0token"]) { + await assert.rejects( + createAdapters({ token, fetchImpl: assert.fail, run: assert.fail }), + /token|invalid/iu, + ) + } +}) + +test("composition delegates to the required factories with fixed bounded identities", async () => { + const calls = [] + const github = githubBoundary() + const npm = { observePackageVersion: async (input) => input } + const attestations = { verify: async (input) => input } + const owner = { git: { headSha: async () => HEAD_SHA } } + const adapters = await createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + token: TOKEN, + environment: { HOME: "/home/release", PATH: "/tools" }, + dependencies: { + fetchImpl: assert.fail, + run: commandRunner([]), + now: () => NOW, + createGitHubReader(options) { + calls.push(["github", options]) + return github + }, + createOwnerPreflightAdapters(options) { + calls.push(["owner", options]) + return owner + }, + createNpmReader(options) { + calls.push(["npm", options]) + return npm + }, + createCliAttestationVerifier(options) { + calls.push(["attestations", options]) + return attestations + }, + }, + }) + + assert.deepEqual(Object.keys(calls[0][1]).sort(), [ + "apiOrigin", + "fetchImpl", + "maxPages", + "maxRecords", + "now", + "owner", + "repo", + "token", + ]) + assert.equal(calls[0][1].owner, "cacheplane") + assert.equal(calls[0][1].repo, "dawnai") + assert.equal(calls[0][1].apiOrigin, API_ORIGIN) + assert.equal(calls[0][1].maxPages, 100) + assert.equal(calls[0][1].maxRecords, 10_000) + assert.equal(calls[1][0], "owner") + assert.deepEqual(Object.keys(calls[1][1]).sort(), ["cwd", "environment", "run"]) + assert.equal(calls[2][0], "npm") + assert.deepEqual(Object.keys(calls[2][1]).sort(), ["fetchImpl"]) + assert.equal(calls[3][0], "attestations") + assert.deepEqual(Object.keys(calls[3][1]).sort(), ["repository", "runGh", "token"]) + assert.notEqual(adapters.github.listReleases, github.listReleases) + assert.notEqual(adapters.npm.observePackageVersion, npm.observePackageVersion) + assert.notEqual(adapters.attestations.verify, attestations.verify) +}) + +test("GitHub reads use exact trusted endpoints, headers, pagination, and normalized evidence", async () => { + const secondReleasePage = `${BASE}/releases?per_page=100&page=2` + const recording = recordingFetch([ + jsonResponse({ + id: 1_210_070_282, + full_name: REPOSITORY, + default_branch: "main", + }), + jsonResponse({ id: 61_436, login: "blove" }), + jsonResponse({ + ref: "refs/heads/main", + object: { type: "commit", sha: HEAD_SHA }, + }), + jsonResponse({ + id: 12_345, + path: ".github/workflows/release.yml", + state: "disabled_manually", + }), + jsonResponse({ total_count: 1, workflow_runs: [workflowRun()] }), + jsonResponse({ + ref: "refs/tags/v0.8.22", + object: { type: "tag", sha: TAG_OBJECT_SHA }, + }), + jsonResponse({ + sha: TAG_OBJECT_SHA, + tag: "v0.8.22", + object: { type: "commit", sha: HEAD_SHA }, + }), + jsonResponse([{ id: 2, name: "second" }], 200, { + Link: `<${secondReleasePage}>; rel="next"`, + }), + jsonResponse([{ id: 1, name: "first" }]), + jsonResponse({ id: Number(SURVIVOR), draft: true }), + jsonResponse([{ id: 91, name: "manifest.json" }]), + binaryResponse(Buffer.from("asset")), + ]) + const adapters = await createAdapters({ + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + }) + + assert.deepEqual(await adapters.github.getRepository(), { + name: REPOSITORY, + id: "1210070282", + defaultBranch: "main", + }) + assert.deepEqual(await adapters.github.getAuthenticatedUser(), { + login: "blove", + id: "61436", + }) + assert.equal(await adapters.github.getDefaultBranchSha(), HEAD_SHA) + assert.deepEqual(await adapters.github.getWorkflowState(), { + workflowId: "12345", + path: ".github/workflows/release.yml", + state: "disabled_manually", + }) + assert.deepEqual(await adapters.github.listNonterminalWorkflowRuns(workflowQuery()), { + query: workflowQuery(), + runs: [normalizedWorkflowRun()], + }) + assert.deepEqual(await adapters.github.getAnnotatedTag({ name: "v0.8.22" }), { + name: "v0.8.22", + objectSha: TAG_OBJECT_SHA, + targetSha: HEAD_SHA, + objectType: "tag", + observedAt: NOW, + }) + assert.deepEqual((await adapters.github.listReleases()).value, [ + { id: 1, name: "first" }, + { id: 2, name: "second" }, + ]) + assert.equal( + (await adapters.github.getRelease({ releaseId: SURVIVOR })).value.id, + Number(SURVIVOR), + ) + assert.equal((await adapters.github.listReleaseAssets({ releaseId: SURVIVOR })).value[0].id, 91) + assert.equal( + Buffer.from( + ( + await adapters.github.downloadReleaseAsset({ + assetId: "91", + maximumBytes: 5, + }) + ).contentBase64, + "base64", + ).toString(), + "asset", + ) + + assert.deepEqual( + recording.calls.map(({ url }) => url), + [ + BASE, + `${API_ORIGIN}/user`, + `${BASE}/git/ref/heads%2Fmain`, + `${BASE}/actions/workflows/release.yml`, + `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=1`, + `${BASE}/git/ref/tags%2Fv0.8.22`, + `${BASE}/git/tags/${TAG_OBJECT_SHA}`, + `${BASE}/releases?per_page=100`, + secondReleasePage, + `${BASE}/releases/${SURVIVOR}`, + `${BASE}/releases/${SURVIVOR}/assets?per_page=100`, + `${BASE}/releases/assets/91`, + ], + ) + for (const { init } of recording.calls) { + assert.equal(init.redirect, "manual") + assert.equal(init.headers["User-Agent"], USER_AGENT) + assert.equal(init.headers.Authorization, `Bearer ${TOKEN}`) + assert.equal( + init.headers.Accept, + init.method === "GET" && init.headers.Accept === "application/octet-stream" + ? "application/octet-stream" + : "application/vnd.github+json", + ) + assert.equal(init.headers["X-GitHub-Api-Version"], "2022-11-28") + } +}) + +test("asset downloads preserve the production one-hop signed-host boundary", async () => { + const signedUrl = + "https://release-assets.githubusercontent.com/github-production-release-asset/1210070282/91/manifest.json?sp=r&sv=2025-01-05&sig=exact-signature" + const recording = recordingFetch([ + redirectResponse(signedUrl), + binaryResponse(Buffer.from("asset")), + ]) + const adapters = await createAdapters({ + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + }) + + assert.equal( + Buffer.from( + ( + await adapters.github.downloadReleaseAsset({ + assetId: "91", + maximumBytes: 5, + }) + ).contentBase64, + "base64", + ).toString(), + "asset", + ) + assert.deepEqual(recording.calls, [ + { + url: `${BASE}/releases/assets/91`, + init: { + method: "GET", + headers: { + Accept: "application/octet-stream", + Authorization: `Bearer ${TOKEN}`, + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": USER_AGENT, + }, + redirect: "manual", + signal: recording.calls[0].init.signal, + }, + }, + { + url: signedUrl, + init: { + method: "GET", + headers: { "User-Agent": USER_AGENT }, + redirect: "manual", + signal: recording.calls[1].init.signal, + }, + }, + ]) + assert.equal(recording.calls[0].init.headers.Authorization, `Bearer ${TOKEN}`) + assert.equal(Object.hasOwn(recording.calls[1].init.headers, "Authorization"), false) + + for (const location of [ + "https://evil.example/github-production-release-asset/1210070282/91/manifest.json?sig=x", + "https://release-assets.githubusercontent.com.evil.example/asset?sig=x", + "https://release-assets.githubusercontent.com/asset#fragment", + ]) { + const unsafe = recordingFetch([redirectResponse(location)]) + const unsafeAdapters = await createAdapters({ + fetchImpl: unsafe.fetchImpl, + run: commandRunner([]), + }) + const result = await unsafeAdapters.github.downloadReleaseAsset({ + assetId: "91", + maximumBytes: 5, + }) + assert.equal(result.status, "ERROR") + assert.equal(result.code, "UNSAFE_DOWNLOAD_URL") + assert.equal(unsafe.calls.length, 1) + } + + const extraHop = recordingFetch([ + redirectResponse(signedUrl), + redirectResponse(`${signedUrl}&retry=1`), + ]) + const extraHopAdapters = await createAdapters({ + fetchImpl: extraHop.fetchImpl, + run: commandRunner([]), + }) + const extraHopResult = await extraHopAdapters.github.downloadReleaseAsset({ + assetId: "91", + maximumBytes: 5, + }) + assert.equal(extraHopResult.status, "ERROR") + assert.equal(extraHopResult.code, "REDIRECT") + assert.equal(extraHop.calls.length, 2) + assert.equal(Object.hasOwn(extraHop.calls[1].init.headers, "Authorization"), false) + + const source = await readFile( + new URL("../duplicate-draft-consolidation-adapters.mjs", import.meta.url), + "utf8", + ) + for (const duplicatedAuthority of [ + "objects.githubusercontent.com", + "release-assets.githubusercontent.com", + "productionresultssa", + ]) { + assert.equal(source.includes(duplicatedAuthority), false) + } +}) + +test("signed download transport rejects caller-driven second hops", async () => { + const calls = [] + const signedUrl = + "https://release-assets.githubusercontent.com/github-production-release-asset/1210070282/91/manifest.json?sig=exact" + const adapters = await createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + token: TOKEN, + environment: { HOME: "/home/release", PATH: "/tools" }, + dependencies: { + fetchImpl: async (...args) => { + calls.push(args) + return binaryResponse(Buffer.from("asset")) + }, + run: commandRunner([]), + now: () => NOW, + createGitHubReader({ fetchImpl }) { + return { + ...githubBoundary(), + async downloadReleaseAsset() { + await fetchImpl(signedUrl, { + method: "GET", + headers: {}, + redirect: "manual", + }) + return { + status: "PRESENT", + operation: "release-asset-download", + httpStatus: 200, + code: null, + contentBase64: "YXNzZXQ=", + } + }, + } + }, + }, + }) + + await assert.rejects( + () => adapters.github.downloadReleaseAsset({ assetId: "91", maximumBytes: 5 }), + /flow|hop|origin|trusted|download/iu, + ) + assert.equal(calls.length, 0) + + const seeded = recordingFetch([redirectResponse(signedUrl), binaryResponse(Buffer.from("asset"))]) + const seededAdapters = await createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + token: TOKEN, + environment: { HOME: "/home/release", PATH: "/tools" }, + dependencies: { + fetchImpl: seeded.fetchImpl, + run: commandRunner([]), + now: () => NOW, + createGitHubReader({ fetchImpl }) { + return { + ...githubBoundary(), + async downloadReleaseAsset() { + await fetchImpl(`${BASE}/releases/assets/91`, { + method: "GET", + headers: {}, + redirect: "manual", + }) + await fetchImpl(signedUrl, { + method: "GET", + headers: {}, + redirect: "manual", + }) + return { + status: "PRESENT", + operation: "release-asset-download", + httpStatus: 200, + code: null, + contentBase64: "YXNzZXQ=", + } + }, + } + }, + }, + }) + await assert.rejects( + () => + seededAdapters.github.downloadReleaseAsset({ + assetId: "91", + maximumBytes: 5, + }), + /flow|hop|origin|trusted|download/iu, + ) + assert.equal(seeded.calls.length, 1) +}) + +test("authorized download hops strip credentials at the transport boundary", async () => { + const signedUrl = + "https://release-assets.githubusercontent.com/github-production-release-asset/1210070282/91/manifest.json?sig=exact" + const recording = recordingFetch([ + redirectResponse(signedUrl), + binaryResponse(Buffer.from("asset")), + ]) + const adapters = await createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + token: TOKEN, + environment: { HOME: "/home/release", PATH: "/tools" }, + dependencies: { + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + now: () => NOW, + createGitHubReader({ fetchImpl }) { + return { + ...githubBoundary(), + async downloadReleaseAsset() { + const first = await fetchImpl(`${BASE}/releases/assets/91`, { + method: "GET", + headers: { + Accept: "application/octet-stream", + Authorization: `Bearer ${TOKEN}`, + "X-GitHub-Api-Version": "2022-11-28", + }, + redirect: "manual", + }) + await fetchImpl(first.headers.get("location"), { + method: "GET", + headers: { + Accept: "application/octet-stream", + Authorization: `Bearer ${TOKEN}`, + }, + redirect: "manual", + }) + return { + status: "PRESENT", + operation: "release-asset-download", + httpStatus: 200, + code: null, + contentBase64: "YXNzZXQ=", + } + }, + } + }, + }, + }) + + assert.equal( + ( + await adapters.github.downloadReleaseAsset({ + assetId: "91", + maximumBytes: 5, + }) + ).contentBase64, + "YXNzZXQ=", + ) + assert.equal(recording.calls.length, 2) + assert.equal(Object.hasOwn(recording.calls[1].init.headers, "Authorization"), false) + assert.equal(recording.calls[1].init.headers.Accept, "application/octet-stream") + assert.equal(recording.calls[1].init.headers["User-Agent"], USER_AGENT) +}) + +test("release and asset readers reject duplicate numeric identities across pages", async () => { + for (const [method, firstUrl, secondUrl, invoke, code] of [ + [ + "release", + `${BASE}/releases?per_page=100`, + `${BASE}/releases?per_page=100&page=2`, + (github) => github.listReleases(), + "DUPLICATE_RELEASE_ID", + ], + [ + "asset", + `${BASE}/releases/${SURVIVOR}/assets?per_page=100`, + `${BASE}/releases/${SURVIVOR}/assets?per_page=100&page=2`, + (github) => github.listReleaseAssets({ releaseId: SURVIVOR }), + "DUPLICATE_ASSET_ID", + ], + ]) { + const recording = recordingFetch([ + jsonResponse([{ id: 7, name: `${method}-one` }], 200, { + Link: `<${secondUrl}>; rel="next"`, + }), + jsonResponse([{ id: 7, name: `${method}-two` }]), + ]) + const adapters = await createAdapters({ + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + }) + assert.deepEqual(await invoke(adapters.github), { + status: "ERROR", + operation: method === "release" ? "releases" : "release-assets", + httpStatus: 200, + code, + }) + assert.deepEqual( + recording.calls.map(({ url }) => url), + [firstUrl, secondUrl], + ) + } +}) + +test("release and asset pagination accept compatible shared Link targets", async () => { + for (const [operation, firstUrl, secondUrl, invoke] of [ + [ + "releases", + `${BASE}/releases?per_page=100&page=1`, + `${BASE}/releases?per_page=100&page=2`, + (github) => github.listReleases(), + ], + [ + "release-assets", + `${BASE}/releases/${SURVIVOR}/assets?per_page=100&page=1`, + `${BASE}/releases/${SURVIVOR}/assets?per_page=100&page=2`, + (github) => github.listReleaseAssets({ releaseId: SURVIVOR }), + ], + ]) { + const recording = recordingFetch([ + jsonResponse([{ id: 2, name: "second" }], 200, { + Link: `<${secondUrl}>; rel="next", <${secondUrl}>; rel="last"`, + }), + jsonResponse([{ id: 1, name: "first" }], 200, { + Link: `<${firstUrl}>; rel="prev", <${firstUrl}>; rel="first"`, + }), + ]) + const adapters = await createAdapters({ + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + }) + assert.deepEqual(await invoke(adapters.github), { + status: "PRESENT", + operation, + httpStatus: 200, + code: null, + value: [ + { id: 1, name: "first" }, + { id: 2, name: "second" }, + ], + }) + assert.equal(recording.calls.length, 2) + } +}) + +test("release and asset pagination reject incompatible complete Link graphs", async () => { + for (const [operation, page2, page3, invoke] of [ + [ + "releases", + `${BASE}/releases?per_page=100&page=2`, + `${BASE}/releases?per_page=100&page=3`, + (github) => github.listReleases(), + ], + [ + "release-assets", + `${BASE}/releases/${SURVIVOR}/assets?per_page=100&page=2`, + `${BASE}/releases/${SURVIVOR}/assets?per_page=100&page=3`, + (github) => github.listReleaseAssets({ releaseId: SURVIVOR }), + ], + ]) { + for (const link of [ + `<${page2}>; rel="next", <${page2}>; rel="prev"`, + `<${page2}>; rel="next", <${page2}>; rel="first"`, + `<${page2}>; rel="last", <${page2}>; rel="prev"`, + `<${page2}>; rel="last", <${page2}>; rel="first"`, + `<${page2}>; rel="next", <${page3}>; rel="next"`, + `<${page2}>; rel="next last"`, + ]) { + const recording = recordingFetch([jsonResponse([], 200, { Link: link })]) + const adapters = await createAdapters({ + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + }) + assert.deepEqual(await invoke(adapters.github), { + status: "ERROR", + operation, + httpStatus: 200, + code: "MALFORMED_LINK_HEADER", + }) + assert.equal(recording.calls.length, 1) + } + } +}) + +test("GitHub reader preserves fail-closed pagination and transport classifications", async () => { + const unsafeNext = [ + `https://evil.example/repos/cacheplane/dawnai/releases?per_page=100&page=2`, + `${BASE}/issues?per_page=100&page=2`, + `${BASE}/releases?per_page=100&page=2&extra=true`, + ] + for (const next of unsafeNext) { + const adapters = await createAdapters({ + fetchImpl: async () => jsonResponse([], 200, { Link: `<${next}>; rel="next"` }), + run: commandRunner([]), + }) + assert.equal((await adapters.github.listReleases()).code, "UNSAFE_PAGINATION_URL") + } + + const repeated = `${BASE}/releases?per_page=100&page=2` + const adapters = await createAdapters({ + fetchImpl: async () => jsonResponse([], 200, { Link: `<${repeated}>; rel="next"` }), + run: commandRunner([]), + }) + assert.equal((await adapters.github.listReleases()).code, "PAGINATION_LOOP") + + for (const response of [ + jsonResponse({ message: "forbidden" }, 403), + jsonResponse({ message: "rate limited" }, 429), + jsonResponse({ message: "server" }, 503), + new Response("not-json", { + status: 200, + headers: { "content-type": "application/json" }, + }), + new Response(null, { + status: 302, + headers: { location: `${BASE}/releases` }, + }), + ]) { + const reader = await createAdapters({ + fetchImpl: async () => response, + run: commandRunner([]), + }) + assert.notEqual((await reader.github.listReleases()).status, "PRESENT") + } +}) + +test("workflow-run enumeration rejects unstable totals, duplicate IDs, and bounds", async () => { + const page = Array.from({ length: 100 }, (_unused, index) => workflowRun(index + 1)) + const next = `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=2` + for (const responses of [ + [ + jsonResponse({ total_count: 101, workflow_runs: page }, 200, { + Link: `<${next}>; rel="next"`, + }), + jsonResponse({ total_count: 102, workflow_runs: [workflowRun(101)] }), + ], + [ + jsonResponse({ total_count: 101, workflow_runs: page }, 200, { + Link: `<${next}>; rel="next"`, + }), + jsonResponse({ total_count: 101, workflow_runs: [workflowRun(1)] }), + ], + [jsonResponse({ total_count: 10_001, workflow_runs: [] })], + ]) { + const recording = recordingFetch(responses) + const adapters = await createAdapters({ + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + }) + await assert.rejects( + adapters.github.listNonterminalWorkflowRuns(workflowQuery()), + /total|duplicate|record|bound/iu, + ) + } +}) + +test("workflow-run enumeration requires one exact trusted Link next relation", async () => { + const page = Array.from({ length: 100 }, (_unused, index) => workflowRun(index + 1)) + const endpoint = `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=2` + for (const link of [ + null, + `; rel="next"`, + `<${BASE}/issues?per_page=100&page=2>; rel="next"`, + `<${endpoint}&extra=true>; rel="next"`, + `<${endpoint}>; rel="next prev"`, + `<${endpoint}>; rel="next", <${endpoint}>; rel="prev"`, + `<${endpoint}>; rel="next", <${endpoint}>; rel="first"`, + `<${endpoint}>; rel="last", <${endpoint}>; rel="prev"`, + `<${endpoint}>; rel="last", <${endpoint}>; rel="first"`, + `<${endpoint}>; rel="next last"`, + `<${endpoint}>; rel="next", <${endpoint}>; rel="next"`, + `<${endpoint}>; rel="next", malformed`, + ]) { + const first = jsonResponse( + { total_count: 101, workflow_runs: page }, + 200, + link === null ? {} : { Link: link }, + ) + const adapters = await createAdapters({ + fetchImpl: recordingFetch([first]).fetchImpl, + run: commandRunner([]), + }) + await assert.rejects( + adapters.github.listNonterminalWorkflowRuns(workflowQuery()), + /Link|pagination|next|trusted|URL/iu, + ) + } +}) + +test("workflow-run pagination accepts compatible next-last and prev-first aliases", async () => { + const firstPageUrl = `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=1` + const secondPageUrl = `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=2` + const page = Array.from({ length: 100 }, (_unused, index) => workflowRun(index + 1)) + const recording = recordingFetch([ + jsonResponse({ total_count: 101, workflow_runs: page }, 200, { + Link: `<${secondPageUrl}>; rel="next", <${secondPageUrl}>; rel="last"`, + }), + jsonResponse({ total_count: 101, workflow_runs: [workflowRun(101)] }, 200, { + Link: `<${firstPageUrl}>; rel="prev", <${firstPageUrl}>; rel="first"`, + }), + ]) + const adapters = await createAdapters({ + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + }) + + const result = await adapters.github.listNonterminalWorkflowRuns(workflowQuery()) + assert.deepEqual(result.query, workflowQuery()) + assert.equal(result.runs.length, 101) + assert.deepEqual(result.runs[0], normalizedWorkflowRun("1")) + assert.deepEqual(result.runs.at(-1), normalizedWorkflowRun("101")) + assert.deepEqual( + recording.calls.map(({ url }) => url), + [firstPageUrl, secondPageUrl], + ) +}) + +test("workflow-run pagination enforces one cumulative raw-byte budget", async () => { + const firstPage = Array.from({ length: 100 }, (_unused, index) => workflowRun(index + 1)) + const secondPage = [workflowRun(101)] + const secondPageUrl = `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=2` + const padding = "x".repeat(4_500_000) + const recording = recordingFetch([ + jsonResponse({ total_count: 101, workflow_runs: firstPage, padding }, 200, { + Link: `<${secondPageUrl}>; rel="next"`, + }), + jsonResponse({ total_count: 101, workflow_runs: secondPage, padding }), + ]) + const adapters = await createAdapters({ + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + }) + + await assert.rejects( + adapters.github.listNonterminalWorkflowRuns(workflowQuery()), + /byte|size|large|budget|failed closed/iu, + ) + assert.equal(recording.calls.length, 2) +}) + +test("workflow-run pagination enforces one cumulative wall-clock deadline", async () => { + const firstPage = Array.from({ length: 100 }, (_unused, index) => workflowRun(index + 1)) + const secondPageUrl = `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=2` + let clockMillis = Date.parse(NOW) + const recording = recordingFetch([ + jsonResponse({ total_count: 101, workflow_runs: firstPage }, 200, { + Link: `<${secondPageUrl}>; rel="next"`, + }), + jsonResponse({ total_count: 101, workflow_runs: [workflowRun(101)] }), + ]) + const fetchImpl = async (...args) => { + const response = await recording.fetchImpl(...args) + clockMillis += 15_001 + return response + } + const adapters = await createAdapters({ + fetchImpl, + run: commandRunner([]), + now: () => new Date(clockMillis).toISOString(), + }) + + await assert.rejects( + adapters.github.listNonterminalWorkflowRuns(workflowQuery()), + /deadline|time|budget|failed closed/iu, + ) + assert.equal(recording.calls.length, 1) +}) + +test("local Git reads use exact argv arrays and reject detached, dirty, or malformed output", async () => { + const calls = [] + const adapters = await createAdapters({ + fetchImpl: assert.fail, + run: commandRunner(calls), + }) + assert.deepEqual(await adapters.local.readState(), { + headSha: HEAD_SHA, + branch: "main", + porcelainStatus: "", + originMainSha: HEAD_SHA, + }) + assert.deepEqual( + calls.map(([command, args]) => [command, args]), + [ + ["git", ["rev-parse", "--verify", "HEAD^{commit}"]], + ["git", ["symbolic-ref", "--quiet", "--short", "HEAD"]], + ["git", ["status", "--porcelain=v1", "--untracked-files=all"]], + ["git", ["rev-parse", "--verify", "refs/remotes/origin/main^{commit}"]], + ], + ) + for (const [, , options] of calls) { + assert.equal(options.cwd, "/workspace") + assert.equal(options.shell, undefined) + assert.equal(options.env.GH_TOKEN, undefined) + } + + for (const overrides of [ + { branch: "" }, + { branch: "main\nforged" }, + { status: "?? secret.txt\n" }, + { originMainSha: HEAD_SHA.toUpperCase() }, + ]) { + const invalid = await createAdapters({ + fetchImpl: assert.fail, + run: commandRunner([], overrides), + }) + await assert.rejects(invalid.local.readState(), /branch|clean|status|SHA|malformed/iu) + } +}) + +test("npm and attestation operations delegate through owned narrow wrappers", async () => { + const npmResult = { + status: "ABSENT", + operation: "package-version", + httpStatus: 404, + code: "E404", + } + const attestationResult = { + status: "VERIFIED", + subjects: [{ name: "manifest.json", sha256: "a".repeat(64) }], + } + const calls = [] + const adapters = await createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + token: TOKEN, + dependencies: { + fetchImpl: assert.fail, + run: commandRunner([]), + createNpmReader() { + return { + async observePackageVersion(input) { + calls.push(["npm", input]) + return npmResult + }, + } + }, + createCliAttestationVerifier() { + return { + async verify(input) { + calls.push(["attestations", input]) + return attestationResult + }, + } + }, + }, + }) + const npmInput = { name: "@dawn-ai/sdk", version: "0.8.22" } + const attestationInput = { + source: "escrow", + record: {}, + subjects: [], + files: [], + bundles: [], + } + const observedNpm = await adapters.npm.observePackageVersion(npmInput) + const observedAttestations = await adapters.attestations.verify(attestationInput) + assert.deepEqual(observedNpm, npmResult) + assert.deepEqual(observedAttestations, attestationResult) + assert.notEqual(observedNpm, npmResult) + assert.notEqual(observedAttestations, attestationResult) + assert.equal(deeplyFrozen(observedNpm), true) + assert.equal(deeplyFrozen(observedAttestations), true) + assert.deepEqual(calls, [ + ["npm", npmInput], + ["attestations", attestationInput], + ]) + assert.deepEqual(Object.keys(adapters.npm), ["observePackageVersion"]) + assert.deepEqual(Object.keys(adapters.attestations), ["verify"]) +}) + +test("delegated npm and attestation results reject hostile mutable evidence", async () => { + let invoked = 0 + const npmAccessor = {} + Object.defineProperty(npmAccessor, "status", { + enumerable: true, + get() { + invoked += 1 + return "ABSENT" + }, + }) + const attestationAccessor = {} + Object.defineProperty(attestationAccessor, "status", { + enumerable: true, + get() { + invoked += 1 + return "VERIFIED" + }, + }) + const adapters = await createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + token: TOKEN, + dependencies: { + fetchImpl: assert.fail, + run: commandRunner([]), + createNpmReader: () => ({ + observePackageVersion: async () => npmAccessor, + }), + createCliAttestationVerifier: () => ({ + verify: async () => attestationAccessor, + }), + }, + }) + assert.deepEqual(await adapters.npm.observePackageVersion({}), { + status: "ERROR", + operation: "malformed-envelope", + httpStatus: null, + code: "MALFORMED_ENVELOPE", + }) + await assert.rejects(adapters.attestations.verify({}), /attestation|evidence|malformed/iu) + assert.equal(invoked, 0) + + for (const result of [ + { status: "VERIFIED", subjects: [], extra: true }, + { + status: "VERIFIED", + subjects: [{ name: "manifest.json", sha256: "A".repeat(64) }], + }, + new Proxy({ status: "INVALID", subjects: [] }, {}), + ]) { + const hostile = await createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + token: TOKEN, + dependencies: { + fetchImpl: assert.fail, + run: commandRunner([]), + createCliAttestationVerifier: () => ({ verify: async () => result }), + }, + }) + await assert.rejects(hostile.attestations.verify({}), /attestation|evidence|malformed/iu) + } +}) + +test("delete boundary rejects every non-approved construction or call before fetch", async () => { + const invalidConstructions = [ + { repository: "cacheplane/other" }, + { apiOrigin: "http://api.github.com" }, + { apiOrigin: "https://evil.example" }, + { survivorId: DUPLICATES[0] }, + { survivorId: Number(SURVIVOR) }, + { duplicateIds: [...DUPLICATES].reverse() }, + { duplicateIds: [Number(DUPLICATES[0]), DUPLICATES[1]] }, + { duplicateIds: [DUPLICATES[0], Number(DUPLICATES[1])] }, + { duplicateIds: [DUPLICATES[0]] }, + { duplicateIds: [...DUPLICATES, "1"] }, + { duplicateIds: [DUPLICATES[0], DUPLICATES[0]] }, + { duplicateIds: [DUPLICATES[0], SURVIVOR] }, + { token: "bad\ntoken" }, + ] + for (const override of invalidConstructions) { + const fetchCalls = [] + assert.throws( + () => + createWriter({ + ...override, + fetchImpl: (...args) => fetchCalls.push(args), + }), + /approved|canonical|duplicate|survivor|repository|origin|token|invalid/iu, + ) + assert.equal(fetchCalls.length, 0) + } + + const fetchCalls = [] + const writer = await createGuardedWriter({ + fetchImpl: (...args) => fetchCalls.push(args), + }) + await assert.rejects( + () => writer.deleteDuplicate({ releaseId: "379991871" }), + /survivor|approved duplicate|permit/iu, + ) + assert.equal(fetchCalls.length, 0) + for (const releaseId of [379982100, "0379982100", "+379982100", "379982100 ", "1", null]) { + await assert.rejects( + () => writer.deleteDuplicate({ releaseId }), + /canonical|approved|duplicate|invalid|permit/iu, + ) + assert.equal(fetchCalls.length, 0) + } + await assert.rejects( + () => writer.deleteDuplicate({ releaseId: DUPLICATES[0], extra: true }), + /field|option|invalid/iu, + ) + assert.equal(fetchCalls.length, 0) + + const controller = new AbortController() + controller.abort() + await assert.rejects( + () => + writer.deleteDuplicate({ + releaseId: DUPLICATES[0], + signal: controller.signal, + }), + /abort/iu, + ) + assert.equal(fetchCalls.length, 0) +}) + +test("standalone delete effects reject without a guard-minted permit before fetch", async () => { + let fetches = 0 + const writer = createWriter({ + fetchImpl: async () => { + fetches += 1 + return new Response(null, { status: 204 }) + }, + }) + await assert.rejects( + writer.deleteDuplicate({ releaseId: DUPLICATES[0] }), + /guard|permit|one-use/iu, + ) + assert.equal(fetches, 0) +}) + +test("delete performs exactly one bodyless non-redirected DELETE and classifies actual 204", async () => { + const calls = [] + const writer = await createGuardedWriter({ + fetchImpl: async (url, init) => { + calls.push({ url, init }) + return { status: 204, ok: false, headers: new Headers(), body: null } + }, + }) + assert.deepEqual(await writer.deleteDuplicate({ releaseId: DUPLICATES[0] }), { + classification: "confirmed-204", + httpStatus: 204, + observedAt: NOW, + }) + assert.equal(calls.length, 1) + assert.equal(calls[0].url, `${BASE}/releases/${DUPLICATES[0]}`) + assert.equal(calls[0].init.method, "DELETE") + assert.equal(calls[0].init.redirect, "manual") + assert.equal(Object.hasOwn(calls[0].init, "body"), false) + assert.deepEqual(calls[0].init.headers, githubHeaders()) + assert.equal(calls[0].init.signal instanceof AbortSignal, true) +}) + +test("delete classifies received 404 and cancels any response body", async () => { + let cancelled = 0 + const writer = await createGuardedWriter({ + fetchImpl: async () => ({ + status: 404, + ok: true, + headers: new Headers({ "content-type": "application/json" }), + body: { + cancel: async () => { + cancelled += 1 + }, + }, + }), + }) + assert.deepEqual(await writer.deleteDuplicate({ releaseId: DUPLICATES[0] }), { + classification: "response-404-ambiguous", + httpStatus: 404, + observedAt: NOW, + }) + assert.equal(cancelled, 1) +}) + +test("delete cancels bodies on 204 and hard HTTP failure responses", async () => { + for (const status of [204, 500]) { + let cancelled = 0 + const writer = await createGuardedWriter({ + fetchImpl: async () => ({ + status, + headers: new Headers(), + body: { + cancel: async () => { + cancelled += 1 + }, + }, + }), + }) + if (status === 204) { + assert.equal((await writer.deleteDuplicate({ releaseId: DUPLICATES[0] })).httpStatus, 204) + } else { + assert.deepEqual(await writer.deleteDuplicate({ releaseId: DUPLICATES[0] }), { + classification: "response-hard-failure", + httpStatus: 500, + observedAt: NOW, + }) + } + assert.equal(cancelled, 1) + } +}) + +test("delete fails closed when a response body cannot be boundedly canceled", async () => { + const malformed = await createGuardedWriter({ + fetchImpl: async () => ({ status: 204, headers: new Headers(), body: {} }), + }) + assert.deepEqual(await malformed.deleteDuplicate({ releaseId: DUPLICATES[0] }), { + classification: "response-hard-failure", + httpStatus: null, + observedAt: NOW, + }) + + for (const status of [204, 404, 500]) { + for (const cancel of [ + async () => { + throw new Error(`${TOKEN} cancel failed`) + }, + ]) { + const writer = await createGuardedWriter({ + fetchImpl: async () => ({ + status, + headers: new Headers(), + body: { cancel }, + }), + }) + const outcome = await writer.deleteDuplicate({ releaseId: DUPLICATES[0] }) + assert.deepEqual(outcome, { + classification: "response-hard-failure", + httpStatus: null, + observedAt: NOW, + }) + assert.equal(JSON.stringify(outcome).includes(TOKEN), false) + } + } +}) + +test("delete classifies caller abort after send and transport loss as ambiguous", async () => { + const controller = new AbortController() + let sent = false + let signalSent + const sentPromise = new Promise((resolve) => { + signalSent = resolve + }) + const aborted = await createGuardedWriter({ + fetchImpl: async (_url, init) => { + sent = true + signalSent() + return new Promise((_resolve, reject) => { + init.signal.addEventListener( + "abort", + () => reject(new DOMException("aborted", "AbortError")), + { once: true }, + ) + }) + }, + }) + const pending = aborted.deleteDuplicate({ + releaseId: DUPLICATES[0], + signal: controller.signal, + }) + await sentPromise + assert.equal(sent, true) + controller.abort() + assert.equal((await pending).classification, "transport-ambiguous") + + const lost = await createGuardedWriter({ + fetchImpl: async () => { + throw new Error(`${TOKEN} socket lost`) + }, + }) + const outcome = await lost.deleteDuplicate({ releaseId: DUPLICATES[0] }) + assert.deepEqual(outcome, { + classification: "transport-ambiguous", + httpStatus: null, + observedAt: NOW, + }) + assert.equal(JSON.stringify(outcome).includes(TOKEN), false) +}) + +test("delete returns terminal outcomes for explicit HTTP failures, redirects, and malformed responses", async () => { + for (const response of [ + { status: 403, ok: false, headers: new Headers(), body: null }, + { status: 429, ok: false, headers: new Headers(), body: null }, + { status: 500, ok: false, headers: new Headers(), body: null }, + { + status: 302, + ok: false, + headers: new Headers({ location: `${BASE}/releases/1` }), + body: null, + }, + { status: "204", ok: true, headers: new Headers(), body: null }, + null, + ]) { + const writer = await createGuardedWriter({ + fetchImpl: async () => response, + }) + assert.deepEqual(await writer.deleteDuplicate({ releaseId: DUPLICATES[0] }), { + classification: "response-hard-failure", + httpStatus: Number.isInteger(response?.status) ? response.status : null, + observedAt: NOW, + }) + } +}) + +test("delete outcomes require a canonical clock value", async () => { + for (const now of [ + () => "2026-09-01T12:34:56Z", + () => "invalid", + () => 0, + () => { + throw new Error(TOKEN) + }, + ]) { + let fetchCalls = 0 + const writer = await createGuardedWriter({ + fetchImpl: async () => { + fetchCalls += 1 + return { status: 204, headers: new Headers(), body: null } + }, + now, + }) + await assert.rejects( + () => writer.deleteDuplicate({ releaseId: DUPLICATES[0] }), + (error) => !String(error).includes(TOKEN), + ) + assert.equal(fetchCalls, 0) + } +}) + +test("delete rechecks authority and prevalidates outcome time immediately before send", async () => { + let clockCalls = 0 + let fetchCalls = 0 + const writer = await createGuardedWriter({ + now: () => { + clockCalls += 1 + if (clockCalls > 4) throw new Error("clock must not run after send") + return NOW + }, + fetchImpl: async () => { + fetchCalls += 1 + assert.equal(clockCalls, 4) + return { status: 204, headers: new Headers(), body: null } + }, + }) + + assert.deepEqual(await writer.deleteDuplicate({ releaseId: DUPLICATES[0] }), { + classification: "confirmed-204", + httpStatus: 204, + observedAt: NOW, + }) + assert.equal(fetchCalls, 1) + assert.equal(clockCalls, 4) +}) + +test("final clock failure creates no deadline or later unhandled failure", async (t) => { + for (const failure of ["throws", "invalid"]) { + await t.test(failure, async () => { + let clockCalls = 0 + let fetchCalls = 0 + const harness = await createAuthorizedDeleteHarness({ + deleteNow: () => { + clockCalls += 1 + if (clockCalls !== 4) return NOW + if (failure === "throws") { + throw new Error(`${TOKEN} final clock failed`) + } + return "invalid" + }, + fetchImpl: () => { + fetchCalls += 1 + return new Response(null, { status: 204 }) + }, + }) + TEMPORARY_ROOTS.push(harness.root) + const timers = interceptDeleteDeadline() + const unhandled = [] + const uncaught = [] + const onUnhandled = (error) => unhandled.push(error) + const onUncaught = (error) => uncaught.push(error) + process.on("unhandledRejection", onUnhandled) + process.on("uncaughtException", onUncaught) + try { + await assert.rejects( + harness.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATES[0], + permit: harness.permit, + }), + (error) => !String(error).includes(TOKEN), + ) + timers.expire() + await new Promise((resolve) => setImmediate(resolve)) + assert.equal(fetchCalls, 0) + assert.equal(clockCalls, 4) + assert.equal(timers.scheduled(), 0) + assert.equal(timers.cleared(), 0) + assert.deepEqual(unhandled, []) + assert.deepEqual(uncaught, []) + } finally { + process.off("unhandledRejection", onUnhandled) + process.off("uncaughtException", onUncaught) + timers.restore() + } + }) + } +}) + +test("a synchronous fetch failure disposes its delete deadline", async () => { + const harness = await createAuthorizedDeleteHarness({ + deleteNow: () => NOW, + fetchImpl: () => { + throw new Error(`${TOKEN} synchronous transport failure`) + }, + }) + TEMPORARY_ROOTS.push(harness.root) + const timers = interceptDeleteDeadline() + const unhandled = [] + const onUnhandled = (error) => unhandled.push(error) + process.on("unhandledRejection", onUnhandled) + try { + assert.deepEqual( + await harness.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATES[0], + permit: harness.permit, + }), + { + classification: "transport-ambiguous", + httpStatus: null, + observedAt: NOW, + }, + ) + timers.expire() + await new Promise((resolve) => setImmediate(resolve)) + assert.equal(timers.scheduled(), 1) + assert.equal(timers.cleared(), 1) + assert.deepEqual(unhandled, []) + } finally { + process.off("unhandledRejection", onUnhandled) + timers.restore() + } +}) + +test("composition and delete writer are deeply frozen owned capability sets", async () => { + const adapters = await createAdapters({ + fetchImpl: assert.fail, + run: commandRunner([]), + }) + assert.equal(deeplyFrozen(adapters), true) + assert.equal(deeplyFrozen(adapters.writer), true) + assert.deepEqual(Object.keys(adapters.writer), ["deleteDuplicate"]) + assert.deepEqual(Object.keys(adapters.github).sort(), [ + "downloadReleaseAsset", + "getAnnotatedTag", + "getAuthenticatedUser", + "getDefaultBranchSha", + "getRelease", + "getRepository", + "getWorkflowState", + "listNonterminalWorkflowRuns", + "listReleaseAssets", + "listReleases", + ]) + assert.equal(JSON.stringify(adapters).includes(TOKEN), false) + assert.equal(JSON.stringify(adapters).includes("function"), false) +}) + +function createAdapters(options = {}) { + const { + environment = { HOME: "/home/release", PATH: "/tools" }, + fetchImpl, + run, + now = () => NOW, + } = options + return createDuplicateDraftConsolidationAdapters({ + cwd: "/workspace", + ...(Object.hasOwn(options, "token") + ? options.token === undefined + ? {} + : { token: options.token } + : { token: TOKEN }), + environment, + dependencies: { + fetchImpl, + run, + now, + }, + }) +} + +function createWriter(overrides = {}) { + return createExactDuplicateDeleteEffect({ + repository: REPOSITORY, + apiOrigin: API_ORIGIN, + survivorId: SURVIVOR, + duplicateIds: DUPLICATES, + token: TOKEN, + fetchImpl: async () => ({ + status: 204, + headers: new Headers(), + body: null, + }), + timeoutMs: 100, + now: () => NOW, + ...overrides, + }) +} + +async function createGuardedWriter(overrides = {}) { + const fetchImpl = overrides.fetchImpl ?? (async () => new Response(null, { status: 204 })) + const now = overrides.now ?? (() => NOW) + return Object.freeze({ + async deleteDuplicate(input) { + const harness = await createAuthorizedDeleteHarness({ + fetchImpl, + deleteNow: now, + }) + TEMPORARY_ROOTS.push(harness.root) + return harness.adapters.writer.deleteDuplicate({ + ...input, + permit: harness.permit, + }) + }, + }) +} + +function interceptDeleteDeadline() { + const originalSetTimeout = globalThis.setTimeout + const originalClearTimeout = globalThis.clearTimeout + const timer = Object.freeze({}) + let callback = null + let scheduled = 0 + let cleared = 0 + globalThis.setTimeout = (operation, _delay, ...args) => { + scheduled += 1 + callback = () => operation(...args) + return timer + } + globalThis.clearTimeout = (candidate) => { + assert.equal(candidate, timer) + cleared += 1 + } + return { + cleared: () => cleared, + expire: () => callback?.(), + restore() { + globalThis.setTimeout = originalSetTimeout + globalThis.clearTimeout = originalClearTimeout + }, + scheduled: () => scheduled, + } +} + +function workflowQuery() { + return Object.freeze({ + statuses: Object.freeze(["in_progress", "pending", "queued", "requested", "waiting"]), + perPage: 100, + maximumPages: 100, + }) +} + +function githubBoundary() { + return { + getRef: async () => present("ref", {}), + getGitTag: async () => present("git-tag", {}), + getWorkflow: async () => present("workflow", {}), + listReleases: async () => present("releases", []), + getRelease: async () => present("release", {}), + listReleaseAssets: async () => present("release-assets", []), + downloadReleaseAsset: async () => ({ + status: "PRESENT", + operation: "release-asset-download", + httpStatus: 200, + code: null, + contentBase64: "", + }), + } +} + +function present(operation, value) { + return { status: "PRESENT", operation, httpStatus: 200, code: null, value } +} + +function commandRunner(calls, overrides = {}) { + return async (command, args, options) => { + if (command === "gh" && args[0] === "auth") { + const token = overrides.authToken ?? TOKEN + calls.push([command, args, options]) + return { exitCode: 0, stdout: `${token}\n`, stderr: "" } + } + calls.push([command, args, options]) + if (args[0] === "rev-parse" && args.at(-1).startsWith("HEAD")) { + return { + exitCode: 0, + stdout: `${overrides.headSha ?? HEAD_SHA}\n`, + stderr: "", + } + } + if (args[0] === "symbolic-ref") { + return { + exitCode: 0, + stdout: `${overrides.branch ?? "main"}\n`, + stderr: "", + } + } + if (args[0] === "status") { + return { exitCode: 0, stdout: overrides.status ?? "", stderr: "" } + } + if (args[0] === "rev-parse" && args.at(-1).startsWith("refs/remotes/origin/main")) { + return { + exitCode: 0, + stdout: `${overrides.originMainSha ?? HEAD_SHA}\n`, + stderr: "", + } + } + throw new Error(`Unexpected command ${command}`) + } +} + +function recordingFetch(responses) { + const calls = [] + let index = 0 + return { + calls, + async fetchImpl(url, init) { + calls.push({ url, init }) + const response = responses[index] + index += 1 + if (response === undefined) throw new Error(`Unexpected fetch ${url}`) + return response + }, + } +} + +function jsonResponse(value, status = 200, headers = {}) { + return new Response(JSON.stringify(value), { + status, + headers: { "content-type": "application/json", ...headers }, + }) +} + +function binaryResponse(value, status = 200) { + return new Response(value, { + status, + headers: { "content-type": "application/octet-stream" }, + }) +} + +function redirectResponse(location) { + return new Response(null, { status: 302, headers: { location } }) +} + +function githubHeaders() { + return { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${TOKEN}`, + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": USER_AGENT, + } +} + +function workflowRun(id = 41) { + return { + id, + run_attempt: 2, + status: "queued", + event: "workflow_dispatch", + head_sha: HEAD_SHA, + head_branch: "main", + } +} + +function normalizedWorkflowRun(id = "41") { + return { + id, + runAttempt: 2, + status: "queued", + event: "workflow_dispatch", + headSha: HEAD_SHA, + headBranch: "main", + } +} + +function deeplyFrozen(value, seen = new Set()) { + if ( + (typeof value !== "object" && typeof value !== "function") || + value === null || + seen.has(value) + ) { + return true + } + seen.add(value) + if (!Object.isFrozen(value)) return false + for (const key of Reflect.ownKeys(value)) { + const descriptor = Object.getOwnPropertyDescriptor(value, key) + if ( + descriptor !== undefined && + "value" in descriptor && + !deeplyFrozen(descriptor.value, seen) + ) { + return false + } + } + return true +} diff --git a/scripts/release/test/duplicate-draft-consolidation-authority.test.mjs b/scripts/release/test/duplicate-draft-consolidation-authority.test.mjs new file mode 100644 index 000000000..5ae3c0a3e --- /dev/null +++ b/scripts/release/test/duplicate-draft-consolidation-authority.test.mjs @@ -0,0 +1,2303 @@ +import assert from "node:assert/strict" +import { createHash } from "node:crypto" +import { unlinkSync } from "node:fs" +import { mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import test from "node:test" +import { + createDuplicateDraftConsolidationAdapters, + createExactDuplicateDeleteEffect, +} from "../duplicate-draft-consolidation-adapters.mjs" +import { + assertFreshWriterAuthority, + captureConsolidationAuthority, + captureNpmInventory, +} from "../duplicate-draft-consolidation-authority.mjs" +import { inspectEquivalentDrafts } from "../duplicate-draft-consolidation-evidence.mjs" +import { + readPrivateEnvelope, + writePrivateEnvelope, +} from "../duplicate-draft-consolidation-files.mjs" +import { + appendJournalEvent, + createConsolidationJournal, + deriveConsolidationState, + parseConsolidationJournal, +} from "../duplicate-draft-consolidation-journal.mjs" +import { + canonicalConsolidationEnvelopeBytes, + canonicalEventEnvelope, + canonicalRecordSha256, + createConsolidationEnvelope, +} from "../duplicate-draft-consolidation-schema.mjs" +import { CANONICAL_RELEASE_PACKAGE_ORDER } from "../manifest.mjs" +import { + createDuplicateDraftConsolidationFixture, + DUPLICATE_DRAFT_CANDIDATE, + DUPLICATE_DRAFT_IDS, + DUPLICATE_DRAFT_SURVIVOR_ID, +} from "./support/duplicate-draft-consolidation-fixture.mjs" + +const REPOSITORY_ID = "1210070282" +const ACTOR = Object.freeze({ login: "blove", id: "61436" }) +const TAG_OBJECT_SHA = "a".repeat(40) +const WORKFLOW_ID = "202458345" +const BASE_TIME = Date.parse("2026-09-01T12:00:00.000Z") +const TEMPORARY_ROOTS = [] +test.after(async () => { + await Promise.all(TEMPORARY_ROOTS.map((root) => rm(root, { recursive: true, force: true }))) +}) +const EXACT_WORKFLOW_RUN_QUERY = Object.freeze({ + statuses: Object.freeze(["in_progress", "pending", "queued", "requested", "waiting"]), + perPage: 100, + maximumPages: 100, +}) + +test("inspection terminal captures three exact direct reads, owns completion time, and permanently seals adapters", async () => { + const fixture = await authorityFixture() + let injectedClockCallsAfterSixthRead = 0 + let injectedNetworkAttemptsAfterSixthRead = 0 + fixture.setClock(() => { + if ( + fixture.networkOperations.filter( + (operation) => operation.startsWith("get-release:") || operation.startsWith("list-assets:"), + ).length >= 6 + ) { + injectedClockCallsAfterSixthRead += 1 + injectedNetworkAttemptsAfterSixthRead += 1 + void fixture.adapters.github.getRepository().catch(() => {}) + throw new Error("injected clock ran after the sixth terminal read") + } + return new Date(BASE_TIME).toISOString() + }) + const nativeStartedAt = Date.now() + const terminal = await fixture.adapters.captureInspectionTerminal({ + candidate: fixture.proposal.candidate, + releases: fixture.proposal.releases, + }) + + assert.deepEqual(Reflect.ownKeys(terminal), ["releases", "completedAt"]) + assert.deepEqual( + terminal.releases.map(({ role, id }) => ({ role, id })), + [ + { role: "survivor", id: DUPLICATE_DRAFT_SURVIVOR_ID }, + { role: "duplicate", id: DUPLICATE_DRAFT_IDS[0] }, + { role: "duplicate", id: DUPLICATE_DRAFT_IDS[1] }, + ], + ) + assert.match( + terminal.completedAt, + /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/u, + ) + assert.equal(Date.parse(terminal.completedAt) >= nativeStartedAt, true) + assert.equal(Date.parse(terminal.completedAt) <= Date.now(), true) + assert.equal(injectedClockCallsAfterSixthRead, 0) + assert.equal(injectedNetworkAttemptsAfterSixthRead, 0) + assert.equal(Object.isFrozen(terminal), true) + assert.equal(Object.isFrozen(terminal.releases), true) + assert.deepEqual(fixture.networkOperations, [ + `get-release:${DUPLICATE_DRAFT_SURVIVOR_ID}`, + `list-assets:${DUPLICATE_DRAFT_SURVIVOR_ID}`, + `get-release:${DUPLICATE_DRAFT_IDS[0]}`, + `list-assets:${DUPLICATE_DRAFT_IDS[0]}`, + `get-release:${DUPLICATE_DRAFT_IDS[1]}`, + `list-assets:${DUPLICATE_DRAFT_IDS[1]}`, + ]) + assert.equal(Reflect.ownKeys(terminal).includes("permit"), false) + assert.equal(fixture.adapters.assertInspectionTerminalSealed(), undefined) + await assert.rejects(fixture.adapters.github.getRepository(), /sealed|terminal|epoch/iu) + await assert.rejects( + fixture.adapters.captureInspectionTerminal({ + candidate: fixture.proposal.candidate, + releases: fixture.proposal.releases, + }), + /sealed|terminal|epoch|state/iu, + ) + assert.throws(() => fixture.adapters.assertInspectionTerminalSealed(), /sealed|terminal|epoch/iu) + assert.equal( + fixture.networkOperations.some((operation) => operation.startsWith("delete:")), + false, + ) +}) + +test("inspection terminal rejects a reordered target set before any direct read", async () => { + const fixture = await authorityFixture() + await assert.rejects( + fixture.adapters.captureInspectionTerminal({ + candidate: fixture.proposal.candidate, + releases: [...fixture.proposal.releases].reverse(), + }), + /terminal|failed|state/iu, + ) + assert.deepEqual(fixture.networkOperations, []) + await assert.rejects(fixture.adapters.github.getRepository(), /sealed|terminal|epoch|invalid/iu) +}) + +test("captures exact pre-delete authority and leaves direct GET plus asset enumeration terminal", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + + assert.deepEqual( + captured.authority.releases.map(({ role, id }) => ({ role, id })), + [ + { role: "survivor", id: DUPLICATE_DRAFT_SURVIVOR_ID }, + { role: "duplicate", id: DUPLICATE_DRAFT_IDS[0] }, + { role: "duplicate", id: DUPLICATE_DRAFT_IDS[1] }, + ], + ) + assert.equal(captured.authority.stage, "pre-delete-1") + assert.equal(captured.authority.targetRead.evidence.id, DUPLICATE_DRAFT_IDS[0]) + assert.deepEqual(fixture.networkOperations.slice(-2), [ + `get-release:${DUPLICATE_DRAFT_IDS[0]}`, + `list-assets:${DUPLICATE_DRAFT_IDS[0]}`, + ]) + assert.equal( + fixture.networkOperations.filter((entry) => entry.startsWith("download:")).length, + 135, + ) + assert.equal(Object.isFrozen(captured.authority), true) + assert.equal(Object.isFrozen(captured.authority.releases[0].assets), true) + assert.equal(Object.isSealed(captured.networkEpoch), true) + assert.deepEqual(Object.keys(captured.networkEpoch), []) + assert.equal(JSON.stringify(captured).includes("networkEpoch"), false) + assert.equal(JSON.stringify(captured.authority).includes("consume"), false) + assert.throws(() => JSON.stringify(captured.networkEpoch), /serialize|capability|epoch/iu) + + const consumption = await journalIntentConsumption(captured, fixture) + const permit = await captured.networkEpoch.consume(consumption) + assert.equal(Object.isFrozen(permit), true) + assert.deepEqual(Object.keys(permit), []) + assert.throws(() => JSON.stringify(permit), /serialize|permit|capability/iu) + const persistedJournal = parseConsolidationJournal( + await readPrivateEnvelope(fixture.journalPath, 64 * 1024 * 1024), + ) + assert.equal(persistedJournal.record.events.at(-1).event.type, "delete-intent") + assert.equal( + persistedJournal.record.events.length, + consumption.currentJournal.record.events.length + 1, + ) + assert.equal((await stat(fixture.journalPath)).mode & 0o777, 0o600) + assert.deepEqual( + await fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + { + classification: "confirmed-204", + httpStatus: 204, + observedAt: new Date(fixture.nowMs).toISOString(), + }, + ) + await assert.rejects(captured.networkEpoch.consume(consumption), /consumed|epoch/iu) + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + /permit|one-use|valid/iu, + ) +}) + +test("invalidates the one-use epoch after any intervening adapter read", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + await assert.rejects(fixture.adapters.github.getRepository(), /sealed|epoch|rejected/iu) + + await assert.rejects( + captured.networkEpoch.consume(await journalIntentConsumption(captured, fixture)), + /epoch|intervening|read/iu, + ) + assert.equal( + parseConsolidationJournal( + await readPrivateEnvelope(fixture.journalPath, 64 * 1024 * 1024), + ).record.events.at(-1).event.type, + "delete-authority-observed", + ) +}) + +test("cannot authorize DELETE without the exact current private journal", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + await rm(fixture.journalPath) + + await assert.rejects( + captured.networkEpoch.consume(consumption), + /journal|ENOENT|durable|current|private/iu, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +}) + +test("requires the exact v-prefixed incident confirmation string instead of a variant", async () => { + for (const variant of ["spacing", "newline", "template-object", "unprefixed", "wrong-prefix"]) { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + const confirmation = + variant === "unprefixed" + ? consumption.confirmation.replace("CONSOLIDATE v", "CONSOLIDATE ") + : variant === "wrong-prefix" + ? consumption.confirmation.replace("CONSOLIDATE v", "CONSOLIDATE version-") + : variant === "spacing" + ? consumption.confirmation.replace(" SURVIVOR ", " SURVIVOR ") + : variant === "newline" + ? `${consumption.confirmation}\n` + : JSON.stringify(fixture.proposal.confirmation) + await assert.rejects( + captured.networkEpoch.consume({ ...consumption, confirmation }), + /confirmation|exact|consumed|epoch/iu, + ) + } + + const secondFixture = await authorityFixture() + const secondCapture = await captureConsolidationAuthority(secondFixture.input) + const second = await journalIntentConsumption(secondCapture, secondFixture) + const templateDigestJournal = createConsolidationJournal({ + proposedEnvelope: createConsolidationEnvelope("proposed", secondFixture.proposal), + confirmationSha256: canonicalRecordSha256(secondFixture.proposal.confirmation), + recordedAt: secondCapture.authority.observedAt, + }) + const withAuthority = appendJournalEvent( + templateDigestJournal, + "delete-authority-observed", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + authority: secondCapture.authority, + }, + secondCapture.authority.observedAt, + ) + await writePrivateEnvelope( + secondFixture.journalPath, + canonicalConsolidationEnvelopeBytes("journal", withAuthority), + ) + await assert.rejects( + secondCapture.networkEpoch.consume({ + ...second, + currentJournal: withAuthority, + }), + /confirmation|digest|journal|bind/iu, + ) +}) + +test("rejects unrelated valid journal replacement without overwriting it", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + let unrelated = createConsolidationJournal({ + proposedEnvelope: createConsolidationEnvelope("proposed", fixture.proposal), + confirmationSha256: "d".repeat(64), + recordedAt: captured.authority.observedAt, + }) + unrelated = appendJournalEvent( + unrelated, + "delete-authority-observed", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + authority: captured.authority, + }, + captured.authority.observedAt, + ) + const unrelatedBytes = canonicalConsolidationEnvelopeBytes("journal", unrelated) + await writePrivateEnvelope(fixture.journalPath, unrelatedBytes) + + await assert.rejects( + captured.networkEpoch.consume(consumption), + /confirmation|journal|current|replace|history|binding/iu, + ) + assert.deepEqual(await readFile(fixture.journalPath), unrelatedBytes) +}) + +test("rejects confirmation and operation-controller mismatch before journal replacement", async (t) => { + for (const mismatch of ["confirmation", "controller"]) { + await t.test(mismatch, async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + const changed = structuredClone(consumption.currentJournal) + if (mismatch === "confirmation") { + changed.record.confirmationSha256 = "e".repeat(64) + changed.record.events[0].event.payload.confirmationSha256 = "e".repeat(64) + } else { + changed.record.events[0].event.payload.controllerSha = "f".repeat(40) + } + changed.record.events = rebuildEventChain(changed.record.events) + const envelope = createConsolidationEnvelope("journal", changed.record) + const changedBytes = canonicalConsolidationEnvelopeBytes("journal", envelope) + await writePrivateEnvelope(fixture.journalPath, changedBytes) + await assert.rejects( + captured.networkEpoch.consume({ + ...consumption, + currentJournal: envelope, + }), + /confirmation|controller|journal|proposal|authority|drift/iu, + ) + assert.deepEqual(await readFile(fixture.journalPath), changedBytes) + }) + } +}) + +test("rejects an illegal intent append from a journal without current delete authority", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + const operationOnly = createConsolidationJournal({ + proposedEnvelope: createConsolidationEnvelope("proposed", fixture.proposal), + confirmationSha256: canonicalRecordSha256(fixture.proposal.confirmation), + recordedAt: captured.authority.observedAt, + }) + const operationOnlyBytes = canonicalConsolidationEnvelopeBytes("journal", operationOnly) + await writePrivateEnvelope(fixture.journalPath, operationOnlyBytes) + + await assert.rejects( + captured.networkEpoch.consume({ + ...consumption, + currentJournal: operationOnly, + }), + /authority|journal|state|intent|bind/iu, + ) + assert.deepEqual(await readFile(fixture.journalPath), operationOnlyBytes) +}) + +test("rejects canonical journal truncation or divergence against the durable head anchor", async (t) => { + for (const mode of ["truncated", "divergent"]) { + await t.test(mode, async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + await writePrivateEnvelope( + fixture.journalHeadPath, + journalHeadBytes(fixture, consumption.currentJournal), + ) + let changed + if (mode === "truncated") { + changed = createConsolidationJournal({ + proposedEnvelope: createConsolidationEnvelope("proposed", fixture.proposal), + confirmationSha256: consumption.currentJournal.record.confirmationSha256, + recordedAt: captured.authority.observedAt, + }) + } else { + const divergent = structuredClone(consumption.currentJournal) + divergent.record.events[0].event.payload.confirmationSha256 = "e".repeat(64) + divergent.record.confirmationSha256 = "e".repeat(64) + divergent.record.events = rebuildEventChain(divergent.record.events) + changed = createConsolidationEnvelope("journal", divergent.record) + } + const changedBytes = canonicalConsolidationEnvelopeBytes("journal", changed) + await writePrivateEnvelope(fixture.journalPath, changedBytes) + await assert.rejects( + captured.networkEpoch.consume({ + ...consumption, + currentJournal: changed, + }), + /head|anchor|lineage|truncat|diverg|confirmation/iu, + ) + assert.deepEqual(await readFile(fixture.journalPath), changedBytes) + }) + } +}) + +test("recovers an anchor behind by exactly one legal append and advances it with intent", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + const predecessor = createConsolidationJournal({ + proposedEnvelope: createConsolidationEnvelope("proposed", fixture.proposal), + confirmationSha256: consumption.currentJournal.record.confirmationSha256, + recordedAt: captured.authority.observedAt, + }) + await writePrivateEnvelope(fixture.journalHeadPath, journalHeadBytes(fixture, predecessor)) + + await captured.networkEpoch.consume(consumption) + const committed = parseConsolidationJournal( + await readPrivateEnvelope(fixture.journalPath, 64 * 1024 * 1024), + ) + assert.equal(committed.record.events.at(-1).event.type, "delete-intent") + assert.deepEqual(await readFile(fixture.journalHeadPath), journalHeadBytes(fixture, committed)) +}) + +test("repairs the intent-written anchor crash window without issuing another permit", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + await writePrivateEnvelope( + fixture.journalHeadPath, + journalHeadBytes(fixture, consumption.currentJournal), + ) + const state = deriveConsolidationState(consumption.currentJournal) + const intent = appendJournalEvent( + consumption.currentJournal, + "delete-intent", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + authorityEventSha256: state.lastEventSha256, + }, + captured.authority.observedAt, + ) + await writePrivateEnvelope( + fixture.journalPath, + canonicalConsolidationEnvelopeBytes("journal", intent), + ) + + await assert.rejects( + captured.networkEpoch.consume({ + ...consumption, + currentJournal: intent, + }), + /predecessor|authority|state|legal/iu, + ) + assert.deepEqual(await readFile(fixture.journalHeadPath), journalHeadBytes(fixture, intent)) +}) + +test("burns a delayed permit at the absolute npm-authority expiry with zero DELETE fetches", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const permit = await captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + fixture.setClock(() => + new Date(Date.parse(captured.authority.npmInventory.completedAt) + 120_001).toISOString(), + ) + + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + /expired|fresh|authority|permit/iu, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +}) + +test("the raw writer enforces expiry on its final pre-send clock", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const permit = await captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + const expiry = Date.parse(captured.authority.npmInventory.completedAt) + 120_000 + let reads = 0 + fixture.setClock(() => { + reads += 1 + return new Date(expiry + (reads >= 3 ? 1 : 0)).toISOString() + }) + + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + /expired|final|pre-send|authority|permit/iu, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +}) + +test("a microtask expiry during awaited verification burns zero DELETE fetches", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const permit = await captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + const expiry = Date.parse(captured.authority.npmInventory.completedAt) + 120_000 + let reads = 0 + let expired = false + fixture.setClock(() => { + reads += 1 + if (reads === 3) queueMicrotask(() => (expired = true)) + return new Date(expiry + (expired ? 1 : 0)).toISOString() + }) + + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + /expired|final|pre-send|authority|permit/iu, + ) + assert.equal(reads, 4) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +}) + +test("the final guard clock invokes transport before its scheduled microtask", async () => { + let betweenFinalClockAndFetch = false + const fixture = await authorityFixture({ + deleteHook: () => assert.equal(betweenFinalClockAndFetch, false), + }) + const captured = await captureConsolidationAuthority(fixture.input) + const permit = await captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + const expiry = Date.parse(captured.authority.npmInventory.completedAt) + 120_000 + let reads = 0 + fixture.setClock(() => { + reads += 1 + if (reads === 4) { + queueMicrotask(() => (betweenFinalClockAndFetch = true)) + } + return new Date(expiry).toISOString() + }) + + assert.deepEqual( + await fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + { + classification: "confirmed-204", + httpStatus: 204, + observedAt: new Date(expiry).toISOString(), + }, + ) + assert.equal(reads, 4) +}) + +test("pre-send journal, head, and lock failures are stable and path-free", async (t) => { + for (const failure of ["journal", "head", "lock"]) { + await t.test(failure, async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const permit = await captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + if (failure === "journal") await rm(fixture.journalPath) + if (failure === "head") await rm(fixture.journalHeadPath) + if (failure === "lock") { + const lockPath = path.join( + path.dirname(fixture.journalPath), + `.${path.basename(fixture.journalPath)}.lock`, + ) + await writeFile(lockPath, "secret-root-path-content\n", { + mode: 0o600, + }) + } + + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + (error) => { + const diagnostic = `${error.message}\n${error.stack ?? ""}` + assert.match( + error.message, + /ERR_CONSOLIDATION_(COMMITTED_JOURNAL|COMMITTED_HEAD|JOURNAL_LOCK)_VERIFICATION/iu, + ) + assert.equal(diagnostic.includes(fixture.root), false) + assert.equal(diagnostic.includes("secret-root-path-content"), false) + assert.equal(Object.hasOwn(error, "cause"), false) + return true + }, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) + }) + } +}) + +test("missing or replaced committed journal heads burn the permit before DELETE", async (t) => { + for (const mutation of ["missing", "replaced"]) { + await t.test(mutation, async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const permit = await captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + if (mutation === "missing") { + await rm(fixture.journalHeadPath) + } else { + const bytes = await readPrivateEnvelope(fixture.journalHeadPath, 16 * 1024) + await writePrivateEnvelope(fixture.journalHeadPath, bytes) + } + + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + /head|anchor|identity|missing|replace/iu, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) + }) + } +}) + +test("removing the journal head from the raw writer clock prevents DELETE send", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const permit = await captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + const captureCompletedAt = Date.parse(captured.authority.observedAt) + let reads = 0 + fixture.setClock(() => { + reads += 1 + if (reads === 3) unlinkSync(fixture.journalHeadPath) + return new Date(captureCompletedAt + reads).toISOString() + }) + + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + /head|journal|identity|lease|missing/iu, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +}) + +test("removing the journal head inside fetch prevents confirmed classification", async () => { + let fixture + fixture = await authorityFixture({ + deleteHook: async () => rm(fixture.journalHeadPath), + }) + const captured = await captureConsolidationAuthority(fixture.input) + const permit = await captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + /head|journal|identity|lease|missing/iu, + ) + assert.equal(fixture.networkOperations.filter((entry) => entry.startsWith("delete:")).length, 1) +}) + +test("a nested adapter call during DELETE invalidates an outer 204", async () => { + let fixture + fixture = await authorityFixture({ + deleteHook: async () => fixture.adapters.github.getRepository(), + }) + const captured = await captureConsolidationAuthority(fixture.input) + const permit = await captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + /reentrant|invalid|sealed|delete|adapter/iu, + ) +}) + +test("rejects replacement of the committed journal before DELETE even when bytes match", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const permit = await captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + const committed = await readPrivateEnvelope(fixture.journalPath, 64 * 1024 * 1024) + await writePrivateEnvelope(fixture.journalPath, committed) + + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + /journal|identity|replace|permit|current/iu, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +}) + +test("rejects a constant-counter bypass and concurrent terminal-read race", async () => { + const constantCounter = await authorityFixture() + constantCounter.input.networkReadCount = () => 0 + await assert.rejects( + captureConsolidationAuthority(constantCounter.input), + /input|field|adapter/iu, + ) + + let releaseTerminal + let signalTerminal + const terminalEntered = new Promise((resolve) => { + signalTerminal = resolve + }) + const terminalGate = new Promise((resolve) => { + releaseTerminal = resolve + }) + const raced = await authorityFixture({ + terminalGate: { entered: signalTerminal, wait: terminalGate }, + }) + const pendingCapture = captureConsolidationAuthority(raced.input) + await terminalEntered + await assert.rejects( + raced.adapters.github.getRepository(), + /terminal|epoch|concurrent|rejected/iu, + ) + releaseTerminal() + await assert.rejects(pendingCapture, /terminal|epoch|failed/iu) +}) + +test("a full public capture exposes no raw trace, callback, or transition boundary", async () => { + const victim = await authorityFixture() + const captured = await captureConsolidationAuthority(victim.input) + const forbidden = new Set([ + "authorityEpoch", + "beginAuthorityCapture", + "beginTerminalRead", + "bindAuthority", + "acceptTransitionBoundary", + "armTransition", + "armTask6Transition", + "trace", + ]) + for (const value of [victim.adapters, captured, captured.networkEpoch]) { + for (const key of Reflect.ownKeys(new Proxy(value, {}))) { + assert.equal(forbidden.has(key), false) + } + } + await assert.rejects( + victim.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit: Object.freeze({}), + }), + /permit|guard|one-use|valid/iu, + ) + assert.equal( + victim.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +}) + +test("caller reads outside capture cannot substitute for the private trace", async () => { + const fixture = await authorityFixture() + await fixture.adapters.github.getRepository() + const captured = await captureConsolidationAuthority(fixture.input) + assert.equal(captured.authority.stage, "pre-delete-1") + assert.equal(Object.hasOwn(fixture.adapters, "beginAuthorityCapture"), false) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +}) + +test("concurrent high-level captures cannot share one private trace", async () => { + const fixture = await authorityFixture() + const results = await Promise.allSettled([ + captureConsolidationAuthority(fixture.input), + captureConsolidationAuthority(fixture.input), + ]) + assert.equal( + results.some(({ status }) => status === "rejected"), + true, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +}) + +test("safe authority capture rejects a complete Release trace that violates its stage", async (t) => { + await t.test("pre-delete-2 rejects the first target reappearing", async () => { + const source = await authorityFixture() + const fixture = await authorityFixture({ stage: "pre-delete-2" }) + fixture.remainingReleases.push( + structuredClone( + source.remainingReleases.find(({ id }) => String(id) === DUPLICATE_DRAFT_IDS[0]), + ), + ) + await assertRawReleaseTraceRejected(fixture) + }) + + await t.test("rejects a fourth managed candidate draft", async () => { + const fixture = await authorityFixture() + const extra = structuredClone(fixture.remainingReleases[0]) + extra.id = 999_999_999 + extra.node_id = "RE_kwDO-managed-extra" + fixture.remainingReleases.push(extra) + await assertRawReleaseTraceRejected(fixture) + }) + + await t.test("rejects a published candidate", async () => { + const fixture = await authorityFixture() + fixture.remainingReleases[0].published_at = "2026-09-01T11:59:00Z" + await assertRawReleaseTraceRejected(fixture) + }) + + await t.test("rejects malformed and duplicate expected entries", async (t) => { + await t.test("malformed managed marker", async () => { + const fixture = await authorityFixture() + fixture.remainingReleases[0].body = "malformed managed candidate" + await assertRawReleaseTraceRejected(fixture) + }) + + await t.test("duplicate expected id", async () => { + const fixture = await authorityFixture() + fixture.remainingReleases.push(structuredClone(fixture.remainingReleases[1])) + await assertRawReleaseTraceRejected(fixture) + }) + }) + + await t.test("an authority cannot omit an extra managed entry", async () => { + const fixture = await authorityFixture() + const extra = structuredClone(fixture.remainingReleases[2]) + extra.id = 999_999_998 + extra.node_id = "RE_kwDO-omitted-managed-extra" + fixture.remainingReleases.push(extra) + await assertRawReleaseTraceRejected(fixture) + }) + + for (const body of [ + "", + "prefix\u0000DAWN_RELEASE_CONTROLLER_MARKER-near-prefix", + `incident ${DUPLICATE_DRAFT_CANDIDATE.commitSha}`, + ]) { + await t.test("rejects a suspicious malformed marker on an unexpected id", async () => { + const fixture = await authorityFixture() + const suspicious = structuredClone(fixture.remainingReleases[0]) + suspicious.id = 999_999_997 + suspicious.node_id = "RE_kwDO-suspicious-unexpected" + suspicious.tag_name = "unrelated-tag" + suspicious.body = body + fixture.remainingReleases.push(suspicious) + await assertRawReleaseTraceRejected(fixture) + }) + } + + await t.test("allows an ordinary unrelated Release", async () => { + const fixture = await authorityFixture() + fixture.remainingReleases.push({ + id: 999_999_996, + node_id: "RE_kwDO-ordinary-unrelated", + tag_name: "v0.8.21", + name: "ordinary unrelated release", + target_commitish: "main", + draft: false, + immutable: true, + prerelease: false, + published_at: "2026-08-01T00:00:00Z", + created_at: "2026-08-01T00:00:00Z", + updated_at: "2026-08-01T00:00:00Z", + body: "ordinary unrelated body", + author: { login: "other", id: 1, node_id: "U_other" }, + assets: [], + }) + await captureConsolidationAuthority(fixture.input) + }) +}) + +test("caller attestation activity cannot enter the private authority trace", async () => { + const fixture = await authorityFixture() + await assert.rejects( + fixture.adapters.attestations.verify({ arbitrary: "caller-authored" }), + /attestation|bundle|record|argument|input|verify/iu, + ) + assert.equal(Reflect.ownKeys(fixture.adapters).includes("beginAuthorityCapture"), false) + assert.equal(Reflect.ownKeys(fixture.adapters).includes("authorityEpoch"), false) +}) + +test("a missing durable head bootstraps only exact operation genesis", async (t) => { + await t.test("post-genesis authority history rejects", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + await rm(fixture.journalHeadPath, { force: true }) + await assert.rejects( + captured.networkEpoch.consume(consumption), + /head|anchor|genesis|missing|history/iu, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) + }) + + await t.test("exact genesis creates and binds the head before stopping", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + const genesis = createConsolidationJournal({ + proposedEnvelope: createConsolidationEnvelope("proposed", fixture.proposal), + confirmationSha256: consumption.currentJournal.record.confirmationSha256, + recordedAt: captured.authority.observedAt, + }) + await writePrivateEnvelope( + fixture.journalPath, + canonicalConsolidationEnvelopeBytes("journal", genesis), + ) + await rm(fixture.journalHeadPath, { force: true }) + await assert.rejects( + captured.networkEpoch.consume({ + ...consumption, + currentJournal: genesis, + }), + /authority|predecessor|state|intent/iu, + ) + assert.deepEqual(await readFile(fixture.journalHeadPath), journalHeadBytes(fixture, genesis)) + }) + + await t.test("deleting both files cannot reset post-genesis history", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + await writePrivateEnvelope( + fixture.journalHeadPath, + journalHeadBytes(fixture, consumption.currentJournal), + ) + await rm(fixture.journalPath) + await rm(fixture.journalHeadPath) + await writePrivateEnvelope( + fixture.journalPath, + canonicalConsolidationEnvelopeBytes("journal", consumption.currentJournal), + ) + await assert.rejects( + captured.networkEpoch.consume(consumption), + /head|anchor|genesis|missing|history/iu, + ) + }) + + await t.test("same-binding divergent history cannot create a new head", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + const divergent = appendJournalEvent( + consumption.currentJournal, + "delete-intent", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + authorityEventSha256: deriveConsolidationState(consumption.currentJournal).lastEventSha256, + }, + captured.authority.observedAt, + ) + await writePrivateEnvelope( + fixture.journalPath, + canonicalConsolidationEnvelopeBytes("journal", divergent), + ) + await rm(fixture.journalHeadPath, { force: true }) + await assert.rejects( + captured.networkEpoch.consume({ + ...consumption, + currentJournal: divergent, + }), + /head|anchor|genesis|missing|history/iu, + ) + }) +}) + +test("real Task4 composition binds the executed workflow query into Task5 authority", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + assert.deepEqual(captured.authority.workflowAuthority.query, EXACT_WORKFLOW_RUN_QUERY) + assert.match(fixture.workflowRunUrls[0], /per_page=100&page=1/u) +}) + +test("captures exact ordered npm absence evidence with bounded canonical timestamps", async () => { + let nowMs = BASE_TIME + const calls = [] + const inventory = await captureNpmInventory({ + stage: "pre-delete-1", + candidate: DUPLICATE_DRAFT_CANDIDATE, + npm: Object.freeze({ + async observePackageVersion(input) { + calls.push(structuredClone(input)) + nowMs += 1 + return { + status: "ABSENT", + operation: "package-version", + httpStatus: 404, + code: "E404", + } + }, + }), + now: () => new Date(nowMs).toISOString(), + }) + + assert.deepEqual( + inventory.packages.map(({ name }) => name), + CANONICAL_RELEASE_PACKAGE_ORDER, + ) + assert.deepEqual( + calls, + CANONICAL_RELEASE_PACKAGE_ORDER.map((name) => ({ + name, + version: "0.8.22", + })), + ) + assert.equal( + inventory.packages.every(({ status }) => status === "ABSENT"), + true, + ) + assert.equal(Object.isFrozen(inventory.packages), true) +}) + +test("rejects every non-exact npm observation and a reversed or overlong operation clock", async (t) => { + for (const [name, result] of [ + [ + "present", + { + status: "PRESENT", + operation: "package-version", + httpStatus: 200, + code: null, + }, + ], + [ + "ambiguous", + { + status: "AMBIGUOUS", + operation: "package-version", + httpStatus: 404, + code: "E404", + }, + ], + [ + "wrong status", + { + status: "ABSENT", + operation: "package-version", + httpStatus: 500, + code: "E404", + }, + ], + [ + "wrong code", + { + status: "ABSENT", + operation: "package-version", + httpStatus: 404, + code: "HTTP_404", + }, + ], + [ + "wrong operation", + { + status: "ABSENT", + operation: "package-metadata", + httpStatus: 404, + code: "E404", + }, + ], + ]) { + await t.test(name, async () => { + await assert.rejects( + captureNpmInventory({ + stage: "pre-delete-1", + candidate: DUPLICATE_DRAFT_CANDIDATE, + npm: Object.freeze({ + async observePackageVersion() { + return result + }, + }), + now: () => "2026-09-01T12:00:00.000Z", + }), + /npm|absence|E404|package-version/iu, + ) + }) + } + + for (const [name, times] of [ + ["reversal", [BASE_TIME + 1, BASE_TIME]], + ["overlong", [BASE_TIME, BASE_TIME + 120_001]], + ]) { + await t.test(name, async () => { + let index = 0 + await assert.rejects( + captureNpmInventory({ + stage: "pre-delete-1", + candidate: DUPLICATE_DRAFT_CANDIDATE, + npm: Object.freeze({ + async observePackageVersion() { + return absent() + }, + }), + now: () => new Date(times[Math.min(index++, times.length - 1)]).toISOString(), + }), + /clock|timestamp|duration|monotone/iu, + ) + }) + } + + await t.test("pairwise package observation reversal", async () => { + let call = 0 + await assert.rejects( + captureNpmInventory({ + stage: "pre-delete-1", + candidate: DUPLICATE_DRAFT_CANDIDATE, + npm: Object.freeze({ + async observePackageVersion() { + return absent() + }, + }), + now: () => { + call += 1 + const offset = call === 2 ? 10 : call === 3 ? 5 : 10 + return new Date(BASE_TIME + (call === 1 ? 0 : offset)).toISOString() + }, + }), + /monotone|observation|timestamp/iu, + ) + }) +}) + +test("rejects invalid repository, checkout, workflow, tag, actor, and SHA authority before a writer exists", async (t) => { + const cases = [ + ["dirty checkout", (fixture) => (fixture.localState.porcelainStatus = " M package.json")], + ["non-main", (fixture) => (fixture.localState.branch = "release")], + ["detached", (fixture) => (fixture.localState.branch = null)], + ["origin mismatch", (fixture) => (fixture.localState.originMainSha = "b".repeat(40))], + ["GitHub mismatch", (fixture) => (fixture.githubMainSha.value = "b".repeat(40))], + ["repository", (fixture) => (fixture.repository.name = "cacheplane/other")], + ["repository id", (fixture) => (fixture.repository.id = "1")], + ["actor", (fixture) => (fixture.actor.login = "someone-else")], + ["actor id", (fixture) => (fixture.actor.id = "1")], + ["workflow state", (fixture) => (fixture.workflow.state = "active")], + ["workflow path", (fixture) => (fixture.workflow.path = ".github/workflows/ci.yml")], + ["active run", (fixture) => fixture.nonterminalRuns.push({ id: "1" })], + ["moved tag", (fixture) => (fixture.annotatedTag.targetSha = "b".repeat(40))], + ["lightweight tag", (fixture) => (fixture.annotatedTag.objectType = "commit")], + ] + + for (const [name, mutate] of cases) { + await t.test(name, async () => { + const fixture = await authorityFixture() + mutate(fixture) + await assert.rejects( + captureConsolidationAuthority(fixture.input), + /repository|actor|checkout|branch|clean|SHA|workflow|run|tag|authority|local Git reader/iu, + ) + }) + } +}) + +test("rejects wrong stage sets, missing or changed drafts, and target disagreement", async (t) => { + for (const [name, mutate] of [ + ["wrong target", (fixture) => (fixture.input.targetReleaseId = DUPLICATE_DRAFT_IDS[1])], + ["missing draft", (fixture) => fixture.remainingReleases.splice(1, 1)], + [ + "extra managed draft", + (fixture) => + fixture.remainingReleases.push({ + ...structuredClone(fixture.remainingReleases[1]), + id: 999999999, + }), + ], + [ + "published draft", + (fixture) => (fixture.remainingReleases[1].published_at = "2026-09-01T12:00:00Z"), + ], + ["changed body", (fixture) => (fixture.remainingReleases[1].name = "changed")], + [ + "target/list disagreement", + (fixture) => (fixture.directRelease.updated_at = "2026-09-01T11:59:59Z"), + ], + ]) { + await t.test(name, async () => { + const fixture = await authorityFixture() + mutate(fixture) + await assert.rejects( + captureConsolidationAuthority(fixture.input), + /release|draft|target|proposal|parity|managed|identity|digest/iu, + ) + }) + } +}) + +test("epoch rejects proposal drift before invoking the journal-intent writer", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const driftedProposal = structuredClone(fixture.proposal) + driftedProposal.inspectedAt = "2026-09-01T11:59:59.000Z" + const consumption = await journalIntentConsumption(captured, fixture, { + proposal: driftedProposal, + }) + + await assert.rejects(captured.networkEpoch.consume(consumption), /proposal|binding|changed/iu) + await assert.rejects(captured.networkEpoch.consume(consumption), /consumed|epoch/iu) + assert.equal( + parseConsolidationJournal( + await readPrivateEnvelope(fixture.journalPath, 64 * 1024 * 1024), + ).record.events.at(-1).event.type, + "delete-authority-observed", + ) +}) + +test("epoch attempts burn before binding and trusted-clock failures", async (t) => { + const invalidCases = [ + [ + "wrong authority", + ({ authority, consumption }) => { + authority.controller.headSha = "b".repeat(40) + consumption.authority = authority + }, + ], + [ + "wrong target", + ({ consumption }) => { + consumption.targetReleaseId = DUPLICATE_DRAFT_IDS[1] + }, + ], + [ + "wrong path", + ({ consumption }) => { + consumption.intentPath = `${consumption.intentPath}.other` + }, + ], + [ + "current journal digest drift", + ({ consumption }) => { + consumption.currentJournal = structuredClone(consumption.currentJournal) + consumption.currentJournal.record.updatedAt = "2026-09-01T11:59:59.000Z" + }, + ], + ] + for (const [name, mutate] of invalidCases) { + await t.test(name, async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const authority = structuredClone(captured.authority) + const consumption = await journalIntentConsumption(captured, fixture) + mutate({ authority, consumption }) + await assert.rejects( + captured.networkEpoch.consume(consumption), + /authority|binding|canonical|envelope|epoch|JSON|path|proposal|sha|target/iu, + ) + assert.equal( + parseConsolidationJournal( + await readPrivateEnvelope(fixture.journalPath, 64 * 1024 * 1024), + ).record.events.at(-1).event.type, + "delete-authority-observed", + ) + await assert.rejects(captured.networkEpoch.consume(consumption), /consumed|epoch/iu) + }) + } + + for (const [name, clock] of [ + ["invalid", () => "2026-09-01T12:00:00Z"], + [ + "throwing", + () => { + throw new Error("secret-clock-token") + }, + ], + [ + "stale", + (captured) => () => + new Date(Date.parse(captured.authority.npmInventory.completedAt) + 120_001).toISOString(), + ], + [ + "future/reversed", + (captured) => () => new Date(Date.parse(captured.authority.observedAt) - 1).toISOString(), + ], + ]) { + await t.test(`${name} trusted clock`, async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + fixture.setClock(clock.length === 0 ? clock : clock(captured)) + await assert.rejects( + captured.networkEpoch.consume(await journalIntentConsumption(captured, fixture)), + (error) => { + assert.equal(error.message.includes("secret-clock-token"), false) + return /clock|fresh|future|monotone|stale|epoch/iu.test(error.message) + }, + ) + assert.equal( + parseConsolidationJournal( + await readPrivateEnvelope(fixture.journalPath, 64 * 1024 * 1024), + ).record.events.at(-1).event.type, + "delete-authority-observed", + ) + await assert.rejects( + captured.networkEpoch.consume(await journalIntentConsumption(captured, fixture)), + /consumed|epoch/iu, + ) + }) + } + + for (const forbidden of ["writeIntent", "now"]) { + await t.test(`caller ${forbidden} cannot substitute for owned authority`, async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const consumption = await journalIntentConsumption(captured, fixture) + consumption[forbidden] = + forbidden === "writeIntent" ? async () => {} : "2099-01-01T00:00:00.000Z" + await assert.rejects( + captured.networkEpoch.consume(consumption), + /field|descriptor|consumption/iu, + ) + assert.equal( + parseConsolidationJournal( + await readPrivateEnvelope(fixture.journalPath, 64 * 1024 * 1024), + ).record.events.at(-1).event.type, + "delete-authority-observed", + ) + await assert.rejects(captured.networkEpoch.consume(consumption), /consumed|epoch/iu) + }) + } +}) + +test("epoch fails closed when freshness expires during the real durable write", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + let calls = 0 + fixture.setClock(() => { + calls += 1 + const offset = calls <= 2 ? 0 : 120_001 + return new Date(Date.parse(captured.authority.npmInventory.completedAt) + offset).toISOString() + }) + await assert.rejects( + captured.networkEpoch.consume(await journalIntentConsumption(captured, fixture)), + /epoch|invalid|durable|do not DELETE/iu, + ) + await readFile(fixture.journalPath) + await assert.rejects( + captured.networkEpoch.consume(await journalIntentConsumption(captured, fixture)), + /consumed|epoch/iu, + ) +}) + +test("post-write trusted-clock failures are redacted and report possibly durable intent", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + let calls = 0 + fixture.setClock(() => { + calls += 1 + if (calls === 3) throw new Error("secret-post-write-clock") + return captured.authority.observedAt + }) + await assert.rejects( + captured.networkEpoch.consume(await journalIntentConsumption(captured, fixture)), + (error) => { + assert.equal(error.message.includes("secret-post-write-clock"), false) + return /may already be durable.*do not DELETE/iu.test(error.message) + }, + ) + await readFile(fixture.journalPath) +}) + +test("raw reads and DELETE attempts during intent persistence invalidate the permit", async (t) => { + for (const effect of ["read", "delete"]) { + await t.test(effect, async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const pending = captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + if (effect === "read") { + await assert.rejects(fixture.adapters.github.getRepository(), /sealed|epoch|rejected/iu) + } else { + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit: Object.freeze({}), + }), + /permit|one-use|valid/iu, + ) + } + await assert.rejects(pending, /epoch|invalid|may already be durable|do not DELETE/iu) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) + }) + } +}) + +test("a permit is unforgeably bound to its composed delete writer", async () => { + const fixture = await authorityFixture() + const captured = await captureConsolidationAuthority(fixture.input) + const permit = await captured.networkEpoch.consume( + await journalIntentConsumption(captured, fixture), + ) + let standaloneFetches = 0 + const standalone = createExactDuplicateDeleteEffect({ + repository: "cacheplane/dawnai", + apiOrigin: "https://api.github.com", + survivorId: DUPLICATE_DRAFT_SURVIVOR_ID, + duplicateIds: DUPLICATE_DRAFT_IDS, + token: "github_test_token_123456789", + fetchImpl: async () => { + standaloneFetches += 1 + return new Response(null, { status: 204 }) + }, + timeoutMs: 15_000, + now: () => captured.authority.observedAt, + }) + await assert.rejects( + standalone.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + /guard|permit|one-use/iu, + ) + assert.equal(standaloneFetches, 0) + await assert.rejects( + fixture.adapters.writer.deleteDuplicate({ + releaseId: DUPLICATE_DRAFT_IDS[0], + permit, + }), + /guard|permit|one-use|valid/iu, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +}) + +test("captures pre-delete-2 and final authority with their exact remaining-set rules", async () => { + const second = await authorityFixture({ stage: "pre-delete-2" }) + const secondCapture = await captureConsolidationAuthority(second.input) + assert.deepEqual( + secondCapture.authority.releases.map(({ id }) => id), + [DUPLICATE_DRAFT_SURVIVOR_ID, DUPLICATE_DRAFT_IDS[1]], + ) + assert.equal(secondCapture.authority.targetRead.evidence.id, DUPLICATE_DRAFT_IDS[1]) + assert.equal(second.networkOperations.filter((entry) => entry.startsWith("download:")).length, 90) + + let finalAttestationCalls = 0 + let finalAttestationInput + const final = await authorityFixture({ + stage: "final", + attestationVerify(input) { + finalAttestationCalls += 1 + finalAttestationInput = structuredClone(input) + return { status: "VERIFIED", subjects: structuredClone(input.subjects) } + }, + }) + const finalCapture = await captureConsolidationAuthority(final.input) + assert.deepEqual( + finalCapture.authority.releases.map(({ id }) => id), + [DUPLICATE_DRAFT_SURVIVOR_ID], + ) + assert.equal(finalCapture.authority.targetRead, null) + assert.equal(final.networkOperations.filter((entry) => entry.startsWith("download:")).length, 45) + assert.equal(finalAttestationCalls, 1) + assert.deepEqual(Object.keys(finalAttestationInput), [ + "source", + "record", + "subjects", + "files", + "bundles", + ]) + assert.equal(finalAttestationInput.source, "escrow") + assert.equal(finalAttestationInput.subjects.length, 22) + assert.deepEqual( + finalAttestationInput.files.map(({ name }) => name), + finalAttestationInput.subjects.map(({ name }) => name), + ) + assert.deepEqual( + finalAttestationInput.bundles.map(({ name }) => name), + finalAttestationInput.subjects.map(({ name }) => `${name}.intoto.jsonl`), + ) + assert.equal(final.networkOperations.at(-1).startsWith("download:"), true) +}) + +test("final authority rejects failed or wrong-subject production attestation after 45 bounded downloads", async (t) => { + for (const [name, attestationVerify] of [ + ["invalid", () => ({ status: "INVALID", subjects: [] })], + [ + "wrong subjects", + (input) => ({ + status: "VERIFIED", + subjects: input.subjects.map((subject, index) => + index === 0 ? { ...subject, sha256: "f".repeat(64) } : { ...subject }, + ), + }), + ], + ]) { + await t.test(name, async () => { + const fixture = await authorityFixture({ stage: "final", attestationVerify }) + await assert.rejects(captureConsolidationAuthority(fixture.input), /authority|attestation/iu) + assert.equal( + fixture.networkOperations.filter((entry) => entry.startsWith("download:")).length, + 45, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) + }) + } +}) + +test("production final authority downloads current asset identities while preserving semantic equality", async () => { + const fixture = await authorityFixture({ + stage: "final", + finalAssetIdentityVolatility: true, + }) + const { authority } = await captureConsolidationAuthority(fixture.input) + const currentAssetIds = fixture.remainingReleases[0].assets.map(({ id }) => String(id)) + const downloadedAssetIds = fixture.networkOperations + .filter((entry) => entry.startsWith(`download:${DUPLICATE_DRAFT_SURVIVOR_ID}:`)) + .map((entry) => entry.split(":").at(-1)) + + assert.equal(authority.releases[0].assets.length, 45) + assert.deepEqual(downloadedAssetIds, currentAssetIds) + assert.notDeepEqual( + authority.releases[0].assets.map(({ id }) => id), + fixture.proposal.releases[0].assets.map(({ id }) => id), + ) +}) + +test("production final authority rejects current asset semantic drift", async (t) => { + for (const drift of [ + "duplicate-name", + "missing", + "extra", + "size", + "digest", + "state", + "content-type", + "label", + "uploader", + "listed-download-identity", + ]) { + await t.test(drift, async () => { + const fixture = await authorityFixture({ stage: "final", finalAssetSemanticDrift: drift }) + await assert.rejects( + captureConsolidationAuthority(fixture.input), + /authority|asset|payload/iu, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) + }) + } +}) + +test("final authority rejects an extra asset found only by complete asset enumeration", async () => { + const fixture = await authorityFixture({ + stage: "final", + paginatedAssetSemanticDrift: "extra", + }) + + await assert.rejects(captureConsolidationAuthority(fixture.input), /authority|asset|payload/iu) + assert.equal( + fixture.networkOperations.includes(`list-assets:${DUPLICATE_DRAFT_SURVIVOR_ID}`), + true, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +}) + +test("writer freshness accepts exactly 120000ms and rejects 120001ms, future evidence, and noncanonical clocks", async () => { + const fixture = await authorityFixture() + const { authority } = await captureConsolidationAuthority(fixture.input) + const completed = Date.parse(authority.npmInventory.completedAt) + + assert.doesNotThrow(() => + assertFreshWriterAuthority( + authority, + fixture.proposal, + new Date(completed + 120_000).toISOString(), + ), + ) + assert.throws( + () => + assertFreshWriterAuthority( + authority, + fixture.proposal, + new Date(completed + 120_001).toISOString(), + ), + /stale|fresh|120/iu, + ) + assert.throws( + () => + assertFreshWriterAuthority( + authority, + fixture.proposal, + new Date(Date.parse(authority.observedAt) - 1).toISOString(), + ), + /future|clock|timestamp/iu, + ) + assert.throws( + () => assertFreshWriterAuthority(authority, fixture.proposal, "2026-09-01T12:00:00Z"), + /canonical|timestamp|clock/iu, + ) + + const reorderedPackages = structuredClone(authority) + reorderedPackages.npmInventory.packages[1].observedAt = reorderedPackages.npmInventory.startedAt + assert.throws( + () => assertFreshWriterAuthority(reorderedPackages, fixture.proposal, authority.observedAt), + /order|monotone|timestamp/iu, + ) + + const reorderedPhases = structuredClone(authority) + reorderedPhases.workflowAuthority.observedAt = reorderedPhases.npmInventory.packages[0].observedAt + assert.throws( + () => assertFreshWriterAuthority(reorderedPhases, fixture.proposal, authority.observedAt), + /order|monotone|timestamp/iu, + ) +}) + +test("writer validation owns every adjacent target-read chronology boundary", async (t) => { + const fixture = await authorityFixture() + const { authority } = await captureConsolidationAuthority(fixture.input) + const ordered = structuredClone(authority) + let timestamp = Date.parse(ordered.npmInventory.completedAt) + for (const [object, key] of [ + [ordered.targetRead, "releaseGetStartedAt"], + [ordered.targetRead, "releaseGetCompletedAt"], + [ordered.targetRead, "assetsListStartedAt"], + [ordered.targetRead, "assetsListCompletedAt"], + [ordered, "observedAt"], + ]) { + timestamp += 1 + object[key] = new Date(timestamp).toISOString() + } + const boundaries = [ + [ + "npm completion to release GET start", + (authorityValue) => authorityValue.npmInventory.completedAt, + (authorityValue) => [authorityValue.targetRead, "releaseGetStartedAt"], + ], + [ + "release GET start to completion", + (authorityValue) => authorityValue.targetRead.releaseGetStartedAt, + (authorityValue) => [authorityValue.targetRead, "releaseGetCompletedAt"], + ], + [ + "release GET completion to asset-list start", + (authorityValue) => authorityValue.targetRead.releaseGetCompletedAt, + (authorityValue) => [authorityValue.targetRead, "assetsListStartedAt"], + ], + [ + "asset-list start to completion", + (authorityValue) => authorityValue.targetRead.assetsListStartedAt, + (authorityValue) => [authorityValue.targetRead, "assetsListCompletedAt"], + ], + [ + "asset-list completion to authority observation", + (authorityValue) => authorityValue.targetRead.assetsListCompletedAt, + (authorityValue) => [authorityValue, "observedAt"], + ], + ] + for (const [name, earlierValue, laterLocation] of boundaries) { + await t.test(`${name} accepts equality`, () => { + const equal = structuredClone(ordered) + const [object, key] = laterLocation(equal) + object[key] = earlierValue(equal) + assert.doesNotThrow(() => + assertFreshWriterAuthority(equal, fixture.proposal, new Date(timestamp + 1).toISOString()), + ) + }) + await t.test(`${name} rejects reversal`, () => { + const reversed = structuredClone(ordered) + const [object, key] = laterLocation(reversed) + object[key] = new Date(Date.parse(earlierValue(reversed)) - 1).toISOString() + assert.throws( + () => + assertFreshWriterAuthority( + reversed, + fixture.proposal, + new Date(timestamp + 1).toISOString(), + ), + /chronology|monotone|target|timestamp/iu, + ) + }) + } +}) + +test("descriptor-hostile inputs fail without invoking getters", async () => { + const fixture = await authorityFixture() + let invoked = false + const hostile = {} + Object.defineProperty(hostile, "stage", { + enumerable: true, + get() { + invoked = true + throw new Error("secret getter payload") + }, + }) + + await assert.rejects(captureConsolidationAuthority(hostile), /input|descriptor|data/iu) + assert.equal(invoked, false) + await assert.rejects(captureConsolidationAuthority(new Proxy(fixture.input, {})), /proxy|input/iu) + assert.equal(invoked, false) +}) + +test("rejects symbol, hidden, sparse, nonplain, and mutable dependency inputs", async (t) => { + const malformedRoots = [] + const withSymbol = { stage: "pre-delete-1" } + withSymbol[Symbol("hidden")] = true + malformedRoots.push(["symbol", withSymbol]) + const withHidden = { stage: "pre-delete-1" } + Object.defineProperty(withHidden, "hidden", { value: true }) + malformedRoots.push(["hidden", withHidden]) + malformedRoots.push(["nonplain", Object.create({ stage: "pre-delete-1" })]) + for (const [name, value] of malformedRoots) { + await t.test(name, async () => { + await assert.rejects( + captureConsolidationAuthority(value), + /input|field|plain|symbol|descriptor/iu, + ) + }) + } + + await t.test("sparse", async () => { + const fixture = await authorityFixture() + const proposal = structuredClone(fixture.proposal) + proposal.roles.duplicates = Array(2) + fixture.input.proposal = proposal + await assert.rejects(captureConsolidationAuthority(fixture.input), /dense|array|proposal/iu) + }) + await t.test("mutable dependency", async () => { + const fixture = await authorityFixture() + fixture.input.adapters = { + ...fixture.adapters, + local: { + async readState() { + return fixture.localState + }, + }, + } + await assert.rejects(captureConsolidationAuthority(fixture.input), /adapter|facade|immutable/iu) + }) +}) + +test("rejects future service observations and authority that ages out during broad reads", async () => { + const future = await authorityFixture() + future.remainingReleases[0].updated_at = "2026-09-02T00:00:00Z" + await assert.rejects( + captureConsolidationAuthority(future.input), + /future|observation|timestamp|monotone/iu, + ) + + const stale = await authorityFixture() + let calls = 0 + stale.setClock(() => { + calls += 1 + return new Date(BASE_TIME + (calls > 24 ? 120_001 : 0)).toISOString() + }) + await assert.rejects( + captureConsolidationAuthority(stale.input), + /stale|fresh|120000|duration bound/iu, + ) +}) + +test("redacts dependency failures instead of exposing untrusted controls", async () => { + const fixture = await authorityFixture() + fixture.localState.failure = new Error("github_test_token_123\u0000payload") + await assert.rejects(captureConsolidationAuthority(fixture.input), (error) => { + assert.equal(error.message.includes("github_test_token_123"), false) + assert.equal(error.message.includes("payload"), false) + return true + }) +}) + +async function assertRawReleaseTraceRejected(fixture) { + await assert.rejects( + captureConsolidationAuthority(fixture.input), + /release|managed|candidate|duplicate|published|trace|authority/iu, + ) + assert.equal( + fixture.networkOperations.some((entry) => entry.startsWith("delete:")), + false, + ) +} + +async function authorityFixture({ + stage = "pre-delete-1", + terminalGate = null, + deleteHook = null, + attestationVerify = null, + finalAssetIdentityVolatility = false, + finalAssetSemanticDrift = null, + paginatedAssetSemanticDrift = null, +} = {}) { + const evidenceFixture = createDuplicateDraftConsolidationFixture() + const inspected = await inspectEquivalentDrafts({ + candidate: evidenceFixture.candidate, + survivorId: evidenceFixture.survivorId, + duplicateIds: evidenceFixture.duplicateIds, + releases: evidenceFixture.releases, + github: evidenceFixture.github, + attestations: evidenceFixture.attestations, + }) + evidenceFixture.clearOperations() + let nowMs = BASE_TIME + let clockImplementation = () => new Date(nowMs).toISOString() + const networkOperations = [] + const workflowRunUrls = [] + const expectedIds = + stage === "pre-delete-1" + ? [DUPLICATE_DRAFT_SURVIVOR_ID, ...DUPLICATE_DRAFT_IDS] + : stage === "pre-delete-2" + ? [DUPLICATE_DRAFT_SURVIVOR_ID, DUPLICATE_DRAFT_IDS[1]] + : [DUPLICATE_DRAFT_SURVIVOR_ID] + const remainingReleases = evidenceFixture.releases + .filter(({ id }) => expectedIds.includes(String(id))) + .map((release) => structuredClone(release)) + const originalAssetsByCurrentId = new Map() + if (stage === "final") { + const survivor = remainingReleases[0] + for (const [index, asset] of survivor.assets.entries()) { + const originalAssetId = asset.id + if (finalAssetIdentityVolatility) { + asset.id = 8_000_000 + index + asset.node_id = `RA_current_${index}` + asset.created_at = `2026-09-01T10:${String(index).padStart(2, "0")}:00Z` + asset.updated_at = `2026-09-01T11:${String(index).padStart(2, "0")}:00Z` + asset.download_count += 100 + } + originalAssetsByCurrentId.set(String(asset.id), originalAssetId) + } + const asset = survivor.assets[0] + if (finalAssetSemanticDrift === "duplicate-name") asset.name = survivor.assets[1].name + if (finalAssetSemanticDrift === "missing") survivor.assets.pop() + if (finalAssetSemanticDrift === "extra") { + survivor.assets.push({ + ...structuredClone(survivor.assets.at(-1)), + id: 9_999_999, + node_id: "RA_extra_current", + name: "unexpected-current-asset.tgz", + }) + } + if (finalAssetSemanticDrift === "size") asset.size += 1 + if (finalAssetSemanticDrift === "digest") asset.digest = `sha256:${"f".repeat(64)}` + if (finalAssetSemanticDrift === "state") asset.state = "open" + if (finalAssetSemanticDrift === "content-type") asset.content_type = "text/plain" + if (finalAssetSemanticDrift === "label") asset.label = "changed label" + if (finalAssetSemanticDrift === "uploader") asset.uploader.login = "changed-uploader" + if (finalAssetSemanticDrift === "listed-download-identity") { + const otherId = survivor.assets[1].id + survivor.assets[1].id = asset.id + asset.id = otherId + } + } + const paginatedAssetsByReleaseId = new Map( + remainingReleases.map((release) => [String(release.id), structuredClone(release.assets)]), + ) + if (paginatedAssetSemanticDrift === "extra") { + const survivorAssets = paginatedAssetsByReleaseId.get(DUPLICATE_DRAFT_SURVIVOR_ID) + survivorAssets.push({ + ...structuredClone(survivorAssets.at(-1)), + id: 9_999_998, + node_id: "RA_paginated_extra_current", + name: "unexpected-paginated-asset.tgz", + }) + } + const directRelease = structuredClone( + remainingReleases.find( + ({ id }) => + String(id) === (stage === "pre-delete-2" ? DUPLICATE_DRAFT_IDS[1] : DUPLICATE_DRAFT_IDS[0]), + ), + ) + const repository = { + name: "cacheplane/dawnai", + id: REPOSITORY_ID, + defaultBranch: "main", + } + const actor = { ...ACTOR } + const githubMainSha = { value: DUPLICATE_DRAFT_CANDIDATE.commitSha } + const workflow = { + workflowId: WORKFLOW_ID, + path: ".github/workflows/release.yml", + state: "disabled_manually", + } + const nonterminalRuns = [] + const annotatedTag = { + name: "v0.8.22", + objectSha: TAG_OBJECT_SHA, + targetSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + objectType: "tag", + observedAt: new Date(BASE_TIME).toISOString(), + } + const localState = { + headSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + branch: "main", + porcelainStatus: "", + originMainSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + } + const log = (operation) => { + networkOperations.push(operation) + } + const proposal = deepFreeze( + createConsolidationEnvelope("proposed", { + schemaVersion: 1, + repository: { + name: "cacheplane/dawnai", + id: REPOSITORY_ID, + defaultBranch: "main", + actor: { ...ACTOR }, + }, + controller: { + headSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + originMainSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + githubMainSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + }, + candidate: DUPLICATE_DRAFT_CANDIDATE, + roles: { + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + duplicates: [...DUPLICATE_DRAFT_IDS], + }, + confirmation: { + version: "0.8.22", + commitSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + duplicates: [...DUPLICATE_DRAFT_IDS], + template: "Consolidate <64-lowercase-hex-digest>", + }, + annotatedTag: { ...annotatedTag }, + workflowAuthority: { + ...workflow, + query: { + statuses: ["in_progress", "pending", "queued", "requested", "waiting"], + perPage: 100, + maximumPages: 100, + }, + nonterminalRuns: [], + observedAt: new Date(BASE_TIME).toISOString(), + }, + npmInventories: [npmInventory("inspect-initial"), npmInventory("inspect-ready")], + releases: inspected.releases, + payloadProof: inspected.payloadProof, + inspectedAt: new Date(BASE_TIME).toISOString(), + }).record, + ) + const root = await mkdtemp(path.join(await realpath(os.tmpdir()), "dawn-authority-")) + TEMPORARY_ROOTS.push(root) + await mkdir(path.join(root, ".dawn", "release"), { recursive: true }) + const adapters = await createDuplicateDraftConsolidationAdapters({ + cwd: root, + token: "github_test_token_123456789", + environment: { HOME: root, PATH: "/tools" }, + dependencies: { + fetchImpl: async (url, init) => { + const parsed = new URL(url) + if (init.method === "DELETE") { + if (deleteHook !== null) await deleteHook() + log(`delete:${parsed.pathname.split("/").at(-1)}`) + return new Response(null, { status: 204 }) + } + if (parsed.pathname === "/repos/cacheplane/dawnai") { + log("repository") + return jsonResponse({ + id: Number(repository.id), + full_name: repository.name, + default_branch: repository.defaultBranch, + }) + } + if (parsed.pathname === "/user") { + log("user") + return jsonResponse({ login: actor.login, id: Number(actor.id) }) + } + if (parsed.pathname.endsWith("/runs")) { + log("workflow-runs") + workflowRunUrls.push(parsed.href) + const workflowRuns = nonterminalRuns.map((run, index) => ({ + id: Number(run.id ?? index + 1), + run_attempt: run.runAttempt ?? 1, + status: run.status ?? "queued", + event: run.event ?? "workflow_dispatch", + head_sha: run.headSha ?? DUPLICATE_DRAFT_CANDIDATE.commitSha, + head_branch: run.headBranch ?? "main", + })) + return jsonResponse({ + total_count: workflowRuns.length, + workflow_runs: workflowRuns, + }) + } + throw new Error("unexpected fixture network request") + }, + now: () => clockImplementation(), + run: async (command, args) => { + if (command !== "git") throw new Error("unexpected fixture command") + if (args[0] === "symbolic-ref") { + if (localState.branch === null) throw new Error("detached") + return commandResult(`${localState.branch}\n`) + } + if (args[0] === "status") return commandResult(localState.porcelainStatus) + if (args[0] === "rev-parse") { + return commandResult(`${localState.originMainSha}\n`) + } + throw new Error("unexpected fixture git command") + }, + createOwnerPreflightAdapters: () => ({ + git: { + async headSha() { + if (localState.failure !== undefined) throw localState.failure + return localState.headSha + }, + }, + }), + createGitHubReader: () => ({ + async getRef({ ref }) { + if (ref === "heads/main") { + log("default-branch") + return present("ref", { + ref: "refs/heads/main", + object: { type: "commit", sha: githubMainSha.value }, + }) + } + log("tag-ref") + return present("ref", { + ref: `refs/tags/${annotatedTag.name}`, + object: { + type: annotatedTag.objectType, + sha: annotatedTag.objectSha, + }, + }) + }, + async getGitTag() { + log("tag-object") + return present("git-tag", { + sha: annotatedTag.objectSha, + tag: annotatedTag.name, + object: { type: "commit", sha: annotatedTag.targetSha }, + }) + }, + async getWorkflow() { + log("workflow") + return present("workflow", { + id: workflow.workflowId, + path: workflow.path, + state: workflow.state, + }) + }, + async listReleases() { + log("releases") + return present("releases", structuredClone(remainingReleases)) + }, + async downloadReleaseAsset(input) { + log(`download:${input.releaseId}:${input.assetId}`) + const originalAssetId = originalAssetsByCurrentId.get(String(input.assetId)) + return evidenceFixture.github.downloadReleaseAsset( + originalAssetId === undefined ? input : { ...input, assetId: originalAssetId }, + ) + }, + async getRelease({ releaseId }) { + log(`get-release:${releaseId}`) + if (terminalGate !== null) { + terminalGate.entered() + await terminalGate.wait + } + const selected = + directRelease !== undefined && String(directRelease.id) === String(releaseId) + ? directRelease + : remainingReleases.find(({ id }) => String(id) === String(releaseId)) + if (selected === undefined) { + throw new Error("fixture direct release mismatch") + } + return present("release", structuredClone(selected)) + }, + async listReleaseAssets({ releaseId }) { + log(`list-assets:${releaseId}`) + const selected = paginatedAssetsByReleaseId.get(String(releaseId)) + if (selected === undefined) { + throw new Error("fixture direct asset mismatch") + } + return present("release-assets", structuredClone(selected)) + }, + }), + createNpmReader: () => ({ + async observePackageVersion({ name }) { + log(`npm:${name}`) + nowMs += 1 + return absent() + }, + }), + createCliAttestationVerifier: () => ({ + verify: (input) => + attestationVerify === null + ? evidenceFixture.attestations.verify(input) + : attestationVerify(input), + }), + }, + }) + const input = { + stage, + proposal, + targetReleaseId: + stage === "final" + ? null + : stage === "pre-delete-1" + ? DUPLICATE_DRAFT_IDS[0] + : DUPLICATE_DRAFT_IDS[1], + adapters, + } + return { + input, + adapters, + root, + journalPath: path.join(root, ".dawn", "release", "duplicate-draft-consolidation.journal.json"), + journalHeadPath: path.join( + root, + ".dawn", + "release", + "duplicate-draft-consolidation.journal.head.json", + ), + proposal, + localState, + repository, + actor, + githubMainSha, + workflow, + nonterminalRuns, + annotatedTag, + remainingReleases, + get directRelease() { + return directRelease + }, + get networkOperations() { + return [...networkOperations] + }, + get workflowRunUrls() { + return [...workflowRunUrls] + }, + get nowMs() { + return nowMs + }, + setClock(implementation) { + clockImplementation = implementation + }, + } +} + +function npmInventory(stage) { + return { + stage, + startedAt: new Date(BASE_TIME).toISOString(), + completedAt: new Date(BASE_TIME).toISOString(), + packages: CANONICAL_RELEASE_PACKAGE_ORDER.map((name) => ({ + name, + version: "0.8.22", + status: "ABSENT", + httpStatus: 404, + code: "E404", + observedAt: new Date(BASE_TIME).toISOString(), + })), + } +} + +function absent() { + return { + status: "ABSENT", + operation: "package-version", + httpStatus: 404, + code: "E404", + } +} + +function present(operation, value) { + return { status: "PRESENT", operation, httpStatus: 200, code: null, value } +} + +function jsonResponse(value) { + return new Response(JSON.stringify(value), { + status: 200, + headers: { "content-type": "application/json" }, + }) +} + +function commandResult(stdout) { + return { exitCode: 0, stdout, stderr: "" } +} + +async function journalIntentConsumption(captured, fixture, overrides = {}) { + const targetReleaseId = captured.authority.targetRead.evidence.id + const recordedAt = captured.authority.observedAt + const proposedEnvelope = createConsolidationEnvelope("proposed", fixture.proposal) + const confirmation = exactConfirmation(proposedEnvelope) + const confirmationSha256 = createHash("sha256").update(confirmation, "utf8").digest("hex") + let currentJournal = createConsolidationJournal({ + proposedEnvelope, + confirmationSha256, + recordedAt, + }) + await writePrivateEnvelope( + fixture.journalPath, + canonicalConsolidationEnvelopeBytes("journal", currentJournal), + ) + await writePrivateEnvelope(fixture.journalHeadPath, journalHeadBytes(fixture, currentJournal)) + currentJournal = appendJournalEvent( + currentJournal, + "delete-authority-observed", + { + targetReleaseId, + attemptNumber: 1, + authority: captured.authority, + }, + recordedAt, + ) + await writePrivateEnvelope( + fixture.journalPath, + canonicalConsolidationEnvelopeBytes("journal", currentJournal), + ) + return { + authority: captured.authority, + proposal: fixture.proposal, + confirmation, + targetReleaseId, + intentPath: fixture.journalPath, + currentJournal, + ...overrides, + } +} + +function exactConfirmation(proposedEnvelope) { + const { candidate, roles } = proposedEnvelope.record + return `CONSOLIDATE v${candidate.version} ${candidate.commitSha} SURVIVOR ${roles.survivor} DELETE ${roles.duplicates.join(",")} PROPOSAL ${proposedEnvelope.recordSha256}` +} + +function journalHeadBytes(fixture, journal) { + return Buffer.from( + `${JSON.stringify({ + schemaVersion: 1, + journalPath: fixture.journalPath, + repository: journal.record.repository, + proposedRecordSha256: journal.record.proposedRecordSha256, + journalRecordSha256: journal.recordSha256, + lastEventSha256: journal.record.events.at(-1).eventSha256, + sequence: journal.record.events.length, + updatedAt: journal.record.updatedAt, + })}\n`, + "utf8", + ) +} + +function deepFreeze(value) { + if (value !== null && typeof value === "object" && !Object.isFrozen(value)) { + for (const child of Object.values(value)) deepFreeze(child) + Object.freeze(value) + } + return value +} + +function rebuildEventChain(events) { + let previousEventSha256 = null + return events.map(({ event }, index) => { + const envelope = canonicalEventEnvelope( + { + ...event, + sequence: index + 1, + previousEventSha256, + }, + previousEventSha256, + ) + previousEventSha256 = envelope.eventSha256 + return envelope + }) +} diff --git a/scripts/release/test/duplicate-draft-consolidation-cli.test.mjs b/scripts/release/test/duplicate-draft-consolidation-cli.test.mjs new file mode 100644 index 000000000..ca342c91a --- /dev/null +++ b/scripts/release/test/duplicate-draft-consolidation-cli.test.mjs @@ -0,0 +1,593 @@ +import assert from "node:assert/strict" +import { mkdir, mkdtemp, realpath, rm, symlink } from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { Writable } from "node:stream" +import test from "node:test" + +import { runDuplicateDraftConsolidationCli } from "../duplicate-draft-consolidation-cli.mjs" + +const COMMAND = [ + "inspect", + "--version", + "0.8.22", + "--commit-sha", + "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + "--survivor", + "379991871", + "--duplicates", + "379982100,379986168", + "--output", + ".dawn/release/duplicate-draft-consolidation.proposed.json", +] +const PROPOSAL_SHA256 = "a".repeat(64) +const CONFIRMATION = `CONSOLIDATE v0.8.22 2a80deece2ff958fe7fde8fddeb4f99bed70a1c8 SURVIVOR 379991871 DELETE 379982100,379986168 PROPOSAL ${PROPOSAL_SHA256}` +const PERFORM_COMMAND = [ + "perform", + "--proposal", + ".dawn/release/duplicate-draft-consolidation.proposed.json", + "--journal", + ".dawn/release/duplicate-draft-consolidation.journal.json", + "--receipt", + "scripts/release/duplicate-draft-consolidation.json", + "--confirmation", + CONFIRMATION, +] +const VERIFY_COMMAND = ["verify", "--receipt", "scripts/release/duplicate-draft-consolidation.json"] + +test("CLI accepts only the exact read-only verify shape and prints its bounded historical-parity report", async () => { + const stdout = sink() + const stderr = sink() + let received + const code = await runDuplicateDraftConsolidationCli({ + argv: VERIFY_COMMAND, + cwd: process.cwd(), + environment: {}, + stdout, + stderr, + dependencies: { + async createAdapters() { + return Object.freeze({}) + }, + async verify(input, dependencies) { + received = { input, dependencies } + return Object.freeze({ + status: "verified", + survivor: "379991871", + deleted: Object.freeze(["379982100", "379986168"]), + receipt: "scripts/release/duplicate-draft-consolidation.json", + receiptSha256: "c".repeat(64), + historicalParity: + "Historical duplicate payload parity is supported by embedded pre-delete evidence plus the currently reverified survivor; deleted bytes were not independently re-downloaded.", + }) + }, + now: () => "2026-09-01T12:00:00.000Z", + async wait() {}, + }, + }) + assert.equal(code, 0) + assert.equal(stderr.value, "") + assert.deepEqual(JSON.parse(stdout.value), { + status: "verified", + survivor: "379991871", + deleted: ["379982100", "379986168"], + receipt: "scripts/release/duplicate-draft-consolidation.json", + receiptSha256: "c".repeat(64), + historicalParity: + "Historical duplicate payload parity is supported by embedded pre-delete evidence plus the currently reverified survivor; deleted bytes were not independently re-downloaded.", + }) + assert.deepEqual(received.input, { + receipt: "scripts/release/duplicate-draft-consolidation.json", + }) + assert.equal(received.dependencies.repositoryRoot, process.cwd()) +}) + +test("CLI verify rejects every override, mutation, alternate path, and malformed shape before composition", async () => { + for (const argv of [ + [], + VERIFY_COMMAND.slice(0, -1), + [...VERIFY_COMMAND, "--force"], + [...VERIFY_COMMAND, "--override", "main"], + [...VERIFY_COMMAND, "--delete", "379982100"], + VERIFY_COMMAND.with(2, ".dawn/release/duplicate-draft-consolidation.json"), + VERIFY_COMMAND.with(2, "/tmp/receipt.json"), + ]) { + let composeCalls = 0 + const stderr = sink() + assert.equal( + await runDuplicateDraftConsolidationCli({ + argv, + cwd: process.cwd(), + environment: {}, + stdout: sink(), + stderr, + dependencies: { + async createAdapters() { + composeCalls += 1 + return Object.freeze({}) + }, + }, + }), + 2, + ) + assert.equal(composeCalls, 0) + assert.equal(stderr.value, "Invalid duplicate-draft consolidation invocation.\n") + } +}) + +test("CLI accepts only the exact perform shape and prints a bounded completion summary", async () => { + const stdout = sink() + const stderr = sink() + let received + const code = await runDuplicateDraftConsolidationCli({ + argv: PERFORM_COMMAND, + cwd: process.cwd(), + environment: {}, + stdout, + stderr, + dependencies: { + async createAdapters() { + return Object.freeze({}) + }, + async perform(input, dependencies) { + received = { input, dependencies } + return Object.freeze({ + status: "complete", + survivor: "379991871", + deleted: Object.freeze(["379982100", "379986168"]), + receipt: "scripts/release/duplicate-draft-consolidation.json", + receiptSha256: "b".repeat(64), + }) + }, + now: () => "2026-09-01T12:00:00.000Z", + async wait() {}, + }, + }) + assert.equal(code, 0) + assert.equal(stderr.value, "") + assert.deepEqual(JSON.parse(stdout.value), { + status: "complete", + survivor: "379991871", + deleted: ["379982100", "379986168"], + receipt: "scripts/release/duplicate-draft-consolidation.json", + receiptSha256: "b".repeat(64), + }) + assert.deepEqual(received.input, { + proposal: ".dawn/release/duplicate-draft-consolidation.proposed.json", + proposalSha256: PROPOSAL_SHA256, + journal: ".dawn/release/duplicate-draft-consolidation.journal.json", + receipt: "scripts/release/duplicate-draft-consolidation.json", + confirmation: CONFIRMATION, + }) + assert.equal(received.dependencies.repositoryRoot, process.cwd()) +}) + +test("CLI threads each exact convergence request budget into production adapter composition", async () => { + const controller = new AbortController() + const requestBudget = Object.freeze({ + operation: "release", + timeoutMs: 12_345, + signal: controller.signal, + }) + let adapterOptions + const code = await runDuplicateDraftConsolidationCli({ + argv: PERFORM_COMMAND, + cwd: process.cwd(), + environment: {}, + stdout: sink(), + stderr: sink(), + dependencies: { + async createAdapters(options) { + adapterOptions = options + return Object.freeze({}) + }, + async perform(_input, dependencies) { + await dependencies.createAdapters(requestBudget) + return Object.freeze({ + status: "complete", + survivor: "379991871", + deleted: Object.freeze(["379982100", "379986168"]), + receipt: "scripts/release/duplicate-draft-consolidation.json", + receiptSha256: "b".repeat(64), + }) + }, + now: () => "2026-09-01T12:00:00.000Z", + async wait() {}, + }, + }) + + assert.equal(code, 0) + assert.equal(adapterOptions.requestBudget, requestBudget) +}) + +test("CLI perform rejects digest, confirmation, path, force, survivor, and reordered-ID variants", async () => { + for (const argv of [ + PERFORM_COMMAND.with(8, CONFIRMATION.replace(PROPOSAL_SHA256, "A".repeat(64))), + PERFORM_COMMAND.with(8, `${CONFIRMATION} `), + PERFORM_COMMAND.with(2, "/tmp/proposal.json"), + [...PERFORM_COMMAND, "--force"], + [...PERFORM_COMMAND, "--survivor", "379982100"], + PERFORM_COMMAND.with(8, CONFIRMATION.replace("379982100,379986168", "379986168,379982100")), + ]) { + const stderr = sink() + assert.equal( + await runDuplicateDraftConsolidationCli({ + argv, + cwd: process.cwd(), + environment: {}, + stdout: sink(), + stderr, + }), + 2, + ) + assert.equal(stderr.value, "Invalid duplicate-draft consolidation invocation.\n") + } +}) + +test("CLI accepts only the exact ordered invocation and prints a bounded safe summary", async () => { + const stdout = sink() + const stderr = sink() + let received + const code = await runDuplicateDraftConsolidationCli({ + argv: COMMAND, + cwd: process.cwd(), + environment: {}, + stdout, + stderr, + dependencies: { + async createAdapters() { + return Object.freeze({}) + }, + async inspect(input, dependencies) { + received = { input, dependencies } + return Object.freeze({ + proposalSha256: "a".repeat(64), + version: input.version, + commitSha: input.commitSha, + survivor: input.survivor, + duplicates: Object.freeze([...input.duplicates]), + output: input.output, + }) + }, + now: () => "2026-09-01T12:00:00.000Z", + async wait() {}, + }, + }) + + assert.equal(code, 0) + assert.equal(stderr.value, "") + assert.deepEqual(JSON.parse(stdout.value), { + proposalSha256: "a".repeat(64), + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + survivor: "379991871", + duplicates: ["379982100", "379986168"], + output: ".dawn/release/duplicate-draft-consolidation.proposed.json", + }) + assert.deepEqual(received.input, { + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + survivor: "379991871", + duplicates: ["379982100", "379986168"], + output: ".dawn/release/duplicate-draft-consolidation.proposed.json", + }) + assert.equal(received.dependencies.repositoryRoot, process.cwd()) + assert.equal(typeof received.dependencies.repositoryRootIdentity, "object") +}) + +test("CLI rejects unknown, duplicate, missing, reordered, positional, equals, control, and numeric-coercion arguments", async () => { + const variants = [ + [], + ["perform", ...COMMAND.slice(1)], + COMMAND.slice(0, -2), + [...COMMAND, "extra"], + [...COMMAND.slice(0, 3), "--version", "0.8.22", ...COMMAND.slice(3)], + [ + COMMAND[0], + COMMAND[1], + COMMAND[2], + COMMAND[5], + COMMAND[6], + COMMAND[3], + COMMAND[4], + ...COMMAND.slice(7), + ], + ["inspect", "--version=0.8.22", ...COMMAND.slice(3)], + COMMAND.with(2, "0.8.22\n"), + COMMAND.with(6, "379991871.0"), + ] + for (const argv of variants) { + const stdout = sink() + const stderr = sink() + assert.equal( + await runDuplicateDraftConsolidationCli({ + argv, + cwd: "/repo", + environment: {}, + stdout, + stderr, + }), + 2, + ) + assert.equal(stdout.value, "") + assert.equal(stderr.value, "Invalid duplicate-draft consolidation invocation.\n") + } +}) + +test("CLI rejects an explicit unknown flag before composing production dependencies", async () => { + const stdout = sink() + const stderr = sink() + let composeCalls = 0 + const code = await runDuplicateDraftConsolidationCli({ + argv: [...COMMAND, "--unknown"], + cwd: "/repo", + environment: {}, + stdout, + stderr, + dependencies: { + async createAdapters() { + composeCalls += 1 + throw new Error("must not compose") + }, + }, + }) + assert.equal(code, 2) + assert.equal(stdout.value, "") + assert.equal(stderr.value, "Invalid duplicate-draft consolidation invocation.\n") + assert.equal(stderr.value.split("\n").filter(Boolean).length, 1) + assert.equal(composeCalls, 0) +}) + +test("CLI maps evidence failures to one redacted line and exit code 1", async () => { + const stdout = sink() + const stderr = sink() + const code = await runDuplicateDraftConsolidationCli({ + argv: COMMAND, + cwd: process.cwd(), + environment: { GH_TOKEN: "fixture_secret" }, + stdout, + stderr, + dependencies: { + async createAdapters() { + throw new Error("fixture_secret remote response body bytes") + }, + }, + }) + assert.equal(code, 1) + assert.equal(stdout.value, "") + assert.equal(stderr.value, "Duplicate-draft inspection failed.\n") + assert.doesNotMatch(stderr.value, /secret|body|bytes|stack/iu) +}) + +test("CLI contains synchronous and asynchronous stdout failures without leaking diagnostics", async () => { + for (const stdout of [ + throwingSink("stdout fixture_sync_secret body"), + rejectingSink("stdout fixture_async_secret body"), + ]) { + const stderr = sink() + const code = await runDuplicateDraftConsolidationCli({ + argv: COMMAND, + cwd: process.cwd(), + environment: {}, + stdout, + stderr, + dependencies: successfulDependencies(), + }) + assert.equal(code, 1) + assert.equal(stderr.value, "Duplicate-draft inspection failed.\n") + assert.doesNotMatch(stderr.value, /secret|body|stack/iu) + } +}) + +test("CLI preserves invocation and evidence classifications when stderr rejects", async () => { + for (const stderr of [ + throwingSink("stderr fixture_sync_secret body"), + rejectingSink("stderr fixture_async_secret body"), + ]) { + assert.equal( + await runDuplicateDraftConsolidationCli({ + argv: [], + cwd: process.cwd(), + environment: {}, + stdout: sink(), + stderr, + }), + 2, + ) + assert.equal( + await runDuplicateDraftConsolidationCli({ + argv: COMMAND, + cwd: process.cwd(), + environment: {}, + stdout: sink(), + stderr, + dependencies: { + async createAdapters() { + throw new Error("remote fixture_secret response body") + }, + }, + }), + 1, + ) + } +}) + +test("CLI contains a real Writable asynchronous stdout error without process-level leakage", async () => { + const stderr = sink() + const code = await runDuplicateDraftConsolidationCli({ + argv: COMMAND, + cwd: process.cwd(), + environment: {}, + stdout: failingWritable("stdout fixture_writable_secret response body"), + stderr, + dependencies: successfulDependencies(), + }) + await immediate() + assert.equal(code, 1) + assert.equal(stderr.value, "Duplicate-draft inspection failed.\n") + assert.doesNotMatch(stderr.value, /secret|body|stack/iu) +}) + +test("CLI preserves invocation classification when a real stderr Writable fails asynchronously", async () => { + const code = await runDuplicateDraftConsolidationCli({ + argv: [], + cwd: process.cwd(), + environment: {}, + stdout: sink(), + stderr: failingWritable("stderr fixture_writable_secret response body"), + }) + await immediate() + assert.equal(code, 2) +}) + +test("CLI rejects a symlinked root before production adapter composition", async (t) => { + const parent = await realpath(await mkdtemp(path.join(os.tmpdir(), "dawn-cli-root-"))) + t.after(() => rm(parent, { recursive: true, force: true })) + const physical = path.join(parent, "physical") + const linked = path.join(parent, "linked") + await mkdir(physical) + await symlink(physical, linked, "dir") + let composeCalls = 0 + const stderr = sink() + assert.equal( + await runDuplicateDraftConsolidationCli({ + argv: COMMAND, + cwd: linked, + environment: {}, + stdout: sink(), + stderr, + dependencies: { + async createAdapters() { + composeCalls += 1 + return Object.freeze({}) + }, + }, + }), + 1, + ) + assert.equal(composeCalls, 0) + assert.equal(stderr.value, "Duplicate-draft inspection failed.\n") +}) + +test("CLI rejects unsafe injected dependency descriptors as invocation errors", async () => { + const dependencies = {} + Object.defineProperty(dependencies, "inspect", { + enumerable: true, + get() { + throw new Error("token from accessor") + }, + }) + const stderr = sink() + assert.equal( + await runDuplicateDraftConsolidationCli({ + argv: COMMAND, + cwd: "/repo", + environment: {}, + stdout: sink(), + stderr, + dependencies, + }), + 2, + ) + assert.equal(stderr.value, "Invalid duplicate-draft consolidation invocation.\n") + assert.doesNotMatch(stderr.value, /token|accessor/iu) +}) + +test("CLI never invokes an accessor-backed stderr method while reporting invocation failure", async () => { + const unsafeStderr = {} + let accessorCalls = 0 + Object.defineProperty(unsafeStderr, "write", { + get() { + accessorCalls += 1 + throw new Error("fixture_secret accessor body") + }, + }) + const processStderr = sink() + const originalWrite = process.stderr.write + process.stderr.write = processStderr.write + try { + assert.equal( + await runDuplicateDraftConsolidationCli({ + argv: [], + cwd: "/repo", + environment: {}, + stdout: sink(), + stderr: unsafeStderr, + }), + 2, + ) + } finally { + process.stderr.write = originalWrite + } + assert.equal(accessorCalls, 0) + assert.equal(processStderr.value, "Invalid duplicate-draft consolidation invocation.\n") +}) + +test("importing the CLI has no executable side effects", async () => { + const stdout = sink() + const original = process.stdout.write + process.stdout.write = stdout.write + try { + await import(`../duplicate-draft-consolidation-cli.mjs?side-effect=${Date.now()}`) + } finally { + process.stdout.write = original + } + assert.equal(stdout.value, "") +}) + +function sink() { + const output = { + value: "", + write(chunk) { + output.value += String(chunk) + return true + }, + } + return output +} + +function throwingSink(message) { + return { + write() { + throw new Error(message) + }, + } +} + +function rejectingSink(message) { + return { + write() { + return Promise.reject(new Error(message)) + }, + } +} + +function successfulDependencies() { + return { + async createAdapters() { + return Object.freeze({}) + }, + async inspect(input) { + return Object.freeze({ + proposalSha256: "a".repeat(64), + version: input.version, + commitSha: input.commitSha, + survivor: input.survivor, + duplicates: Object.freeze([...input.duplicates]), + output: input.output, + }) + }, + } +} + +function failingWritable(message) { + return new Writable({ + write(_chunk, _encoding, callback) { + setImmediate(() => callback(new Error(message))) + }, + }) +} + +function immediate() { + return new Promise((resolve) => setImmediate(resolve)) +} diff --git a/scripts/release/test/duplicate-draft-consolidation-evidence.test.mjs b/scripts/release/test/duplicate-draft-consolidation-evidence.test.mjs new file mode 100644 index 000000000..13e2ec9ce --- /dev/null +++ b/scripts/release/test/duplicate-draft-consolidation-evidence.test.mjs @@ -0,0 +1,806 @@ +import assert from "node:assert/strict" +import test from "node:test" + +import { + assertEvidenceEqualsProposal, + captureDirectTargetRead, + inspectEquivalentDrafts, + inspectFinalSurvivor, + parseReleaseEvidence, + semanticAssetProjection, + semanticReleaseProjection, +} from "../duplicate-draft-consolidation-evidence.mjs" +import { RELEASE_PAYLOAD_LIMITS } from "../limits.mjs" +import { + createDuplicateDraftConsolidationFixture, + DUPLICATE_DRAFT_CANDIDATE, + DUPLICATE_DRAFT_IDS, + DUPLICATE_DRAFT_SURVIVOR_ID, +} from "./support/duplicate-draft-consolidation-fixture.mjs" + +const INPUT = (fixture) => ({ + candidate: fixture.candidate, + survivorId: fixture.survivorId, + duplicateIds: fixture.duplicateIds, + releases: fixture.releases, + github: fixture.github, + attestations: fixture.attestations, +}) + +const FINAL_INPUT = (fixture, attestations = fixture.attestations) => ({ + candidate: fixture.candidate, + survivorId: fixture.survivorId, + duplicateIds: fixture.duplicateIds, + releases: [fixture.releases[0]], + github: fixture.github, + attestations, +}) + +test("final survivor hydration verifies exactly one survivor, 45 assets, and production attestations", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + let attestationCalls = 0 + const attestations = Object.freeze({ + async verify(input) { + attestationCalls += 1 + return fixture.attestations.verify(input) + }, + }) + const result = await inspectFinalSurvivor(FINAL_INPUT(fixture, attestations)) + assert.deepEqual( + result.releases.map(({ role, id }) => ({ role, id })), + [{ role: "survivor", id: DUPLICATE_DRAFT_SURVIVOR_ID }], + ) + assert.equal(result.releases[0].assets.length, 45) + assert.deepEqual(result.payloadProof.baseAssetSet, fixture.expectedBaseAssetSet) + assert.equal(result.payloadProof.attestationVerification.status, "VERIFIED") + assert.equal(result.payloadProof.attestationVerification.subjects.length, 22) + assert.equal(fixture.downloadCount, 45) + assert.equal(attestationCalls, 1) +}) + +test("final survivor hydration rejects attestation, subject/digest, release-set, and asset-set drift", async (t) => { + await t.test("failed production attestation", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + fixture.failVerification() + await assert.rejects(inspectFinalSurvivor(FINAL_INPUT(fixture)), /attestation|verified/iu) + assert.equal(fixture.downloadCount, 45) + }) + await t.test("wrong verified subjects", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const attestations = Object.freeze({ + async verify(input) { + const result = await fixture.attestations.verify(input) + return { + status: result.status, + subjects: result.subjects.map((subject, index) => + index === 0 ? { ...subject, sha256: "f".repeat(64) } : { ...subject }, + ), + } + }, + }) + await assert.rejects( + inspectFinalSurvivor(FINAL_INPUT(fixture, attestations)), + /attestation|subject|verified/iu, + ) + }) + await t.test("wrong asset digest", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + fixture.releases[0].assets[0].digest = `sha256:${"f".repeat(64)}` + await assert.rejects(inspectFinalSurvivor(FINAL_INPUT(fixture)), /digest|bytes/iu) + }) + for (const [name, releases] of [ + ["missing survivor", []], + ["extra managed Release", null], + ]) { + await t.test(name, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const input = FINAL_INPUT(fixture) + input.releases = releases ?? [fixture.releases[0], structuredClone(fixture.releases[1])] + await assert.rejects(inspectFinalSurvivor(input), /exactly|Release|survivor|managed/iu) + assert.equal(fixture.downloadCount, 0) + }) + } + for (const [name, mutate] of [ + ["missing asset", (assets) => assets.pop()], + [ + "extra asset", + (assets) => { + const extra = structuredClone(assets[0]) + extra.id = 999_999_999 + extra.node_id = "RA_extra" + extra.name = "extra.bin" + assets.push(extra) + }, + ], + ]) { + await t.test(name, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + mutate(fixture.releases[0].assets) + await assert.rejects( + inspectFinalSurvivor(FINAL_INPUT(fixture)), + /asset|namespace|exact|missing/iu, + ) + assert.equal(fixture.downloadCount, 0) + }) + } +}) + +test("proves three ordered mutable drafts have the exact production 45-asset escrow", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const result = await inspectEquivalentDrafts(INPUT(fixture)) + + assert.notDeepEqual( + fixture.releases[0].assets.map(({ name }) => name), + fixture.expectedBaseAssetSet.map(({ name }) => name), + "the fixture must preserve realistic GitHub list order rather than canonical escrow order", + ) + assert.deepEqual( + result.releases.map(({ role, id }) => ({ role, id })), + [ + { role: "survivor", id: DUPLICATE_DRAFT_SURVIVOR_ID }, + { role: "duplicate", id: DUPLICATE_DRAFT_IDS[0] }, + { role: "duplicate", id: DUPLICATE_DRAFT_IDS[1] }, + ], + ) + assert.deepEqual(result.payloadProof.baseAssetSet, fixture.expectedBaseAssetSet) + assert.equal(result.payloadProof.baseAssetSet.length, 45) + assert.equal(result.payloadProof.attestationVerification.status, "VERIFIED") + assert.equal(result.payloadProof.attestationVerification.subjects.length, 22) + assert.equal(result.releases[0].semantic.targetCommitish, "main") + assert.equal(result.releases[0].createdAt, "2026-08-31T00:00:00.000Z") + assert.equal(result.releases[0].assets[0].createdAt.endsWith(".000Z"), true) + assert.equal(fixture.downloadCount, 135) + assert.equal(Object.isFrozen(result), true) + assert.equal(Object.isFrozen(result.releases[0].assets), true) + assert.deepEqual( + result.releases.map(semanticReleaseProjection), + Array.from({ length: 3 }, () => semanticReleaseProjection(result.releases[0])), + ) + assert.deepEqual( + result.releases.map((release) => release.assets.map(semanticAssetProjection)), + Array.from({ length: 3 }, () => result.releases[0].assets.map(semanticAssetProjection)), + ) +}) + +test("normalizes exact GitHub timestamps and rejects invalid raw calendar or precision forms", async (t) => { + const cases = [ + ["Release creation", (release, value) => (release.created_at = value)], + ["Release update", (release, value) => (release.updated_at = value)], + ["asset creation", (release, value) => (release.assets[0].created_at = value)], + ["asset update", (release, value) => (release.assets[0].updated_at = value)], + ] + for (const [name, mutate] of cases) { + await t.test(`${name} second precision`, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + mutate(fixture.releases[0], "2026-08-31T19:15:59Z") + const result = await inspectEquivalentDrafts(INPUT(fixture)) + const evidence = result.releases[0] + const normalized = name.startsWith("Release") + ? name.endsWith("creation") + ? evidence.createdAt + : evidence.updatedAt + : name.endsWith("creation") + ? evidence.assets.find(({ name }) => name === fixture.releases[0].assets[0].name) + .createdAt + : evidence.assets.find(({ name }) => name === fixture.releases[0].assets[0].name) + .updatedAt + assert.equal(normalized, "2026-08-31T19:15:59.000Z") + }) + for (const invalid of [ + "2026-02-30T19:15:59Z", + "2026-08-31T19:15:59.00Z", + "2026-08-31T19:15:59+00:00", + "2026-08-31T19:15:60Z", + ]) { + await t.test(`${name} rejects ${invalid}`, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + mutate(fixture.releases[0], invalid) + await assert.rejects( + inspectEquivalentDrafts(INPUT(fixture)), + /timestamp|calendar|GitHub|canonical/iu, + ) + }) + } + } +}) + +test("enforces namespace-specific declared-size caps before any asset download", async (t) => { + const categories = [ + ["release record", "release-record.json", RELEASE_PAYLOAD_LIMITS.releaseRecordBytes], + ["manifest", "manifest.json", RELEASE_PAYLOAD_LIMITS.manifestBytes], + [ + "bundle", + (name) => name.endsWith(".intoto.jsonl"), + RELEASE_PAYLOAD_LIMITS.attestationBundleBytes, + ], + ["package", (name) => name.endsWith(".tgz"), RELEASE_PAYLOAD_LIMITS.tarballBytes], + ] + for (const [label, selector, maximum] of categories) { + await t.test(`${label} one over`, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const asset = fixture.releases[0].assets.find(({ name }) => + typeof selector === "string" ? name === selector : selector(name), + ) + asset.size = maximum + 1 + await assert.rejects( + inspectEquivalentDrafts(INPUT(fixture)), + /bundle|manifest|package|payload|record|size|tarball/iu, + ) + assert.equal(fixture.downloadCount, 0) + }) + } + + const acceptedBoundaries = [ + ["release record", "release-record.json", RELEASE_PAYLOAD_LIMITS.releaseRecordBytes], + ["manifest", "manifest.json", RELEASE_PAYLOAD_LIMITS.manifestBytes], + [ + "bundle", + (name) => name.endsWith(".intoto.jsonl"), + RELEASE_PAYLOAD_LIMITS.attestationBundleBytes, + ], + [ + "package prepared", + (name) => name.endsWith(".tgz"), + RELEASE_PAYLOAD_LIMITS.preparedTarballsBytes, + ], + ] + for (const [label, selector, maximum] of acceptedBoundaries) { + await t.test(`${label} applicable boundary reaches its download`, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const asset = fixture.releases[0].assets.find(({ name }) => + typeof selector === "string" ? name === selector : selector(name), + ) + if (label === "package prepared") { + for (const other of fixture.releases[0].assets.filter( + ({ name }) => name.endsWith(".tgz") && name !== asset.name, + )) { + other.size = 0 + } + } + asset.size = maximum + await assert.rejects( + inspectEquivalentDrafts(INPUT(fixture)), + /bytes conflict|declared size/iu, + ) + assert.equal(fixture.operations.includes(`download:${fixture.survivorId}:${asset.id}`), true) + }) + } +}) + +test("rejects prepared-package and bundle aggregate overflow before the offending download", async (t) => { + await t.test("prepared packages", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const asset = fixture.releases[0].assets.find(({ name }) => name.endsWith(".tgz")) + asset.size = RELEASE_PAYLOAD_LIMITS.preparedTarballsBytes + 1 + await assert.rejects( + inspectEquivalentDrafts(INPUT(fixture)), + /prepared|package|tarball|payload/iu, + ) + assert.equal(fixture.downloadCount, 0) + }) + await t.test("attestation bundles", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + for (const asset of fixture.releases[0].assets + .filter(({ name }) => name.endsWith(".intoto.jsonl")) + .slice(0, 16)) { + asset.size = RELEASE_PAYLOAD_LIMITS.attestationBundleBytes + } + await assert.rejects(inspectEquivalentDrafts(INPUT(fixture)), /attestation|bundle|payload/iu) + assert.equal(fixture.downloadCount, 0) + }) +}) + +test("excludes only recorded Release and asset service volatility from equality", async (t) => { + const cases = [ + ["Release node id", (release) => (release.node_id = "RE_changed")], + ["opaque tag", (release) => (release.tag_name = "untagged-changed")], + ["Release creation", (release) => (release.created_at = "2026-08-30T00:00:00.000Z")], + ["Release update", (release) => (release.updated_at = "2026-08-30T00:01:00.000Z")], + ["derived Release URL", (release) => (release.html_url = "https://example.invalid/changed")], + ["asset id", (release) => (release.assets[0].id = 777_777_777)], + ["asset node id", (release) => (release.assets[0].node_id = "RA_changed")], + ["asset creation", (release) => (release.assets[0].created_at = "2026-08-30T00:00:00.000Z")], + ["asset update", (release) => (release.assets[0].updated_at = "2026-08-30T00:01:00.000Z")], + ["download count", (release) => (release.assets[0].download_count = 999)], + [ + "derived asset URL", + (release) => (release.assets[0].browser_download_url = "https://example.invalid/asset"), + ], + ] + for (const [name, mutate] of cases) { + await t.test(name, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + mutate(fixture.releases[1]) + const result = await inspectEquivalentDrafts(INPUT(fixture)) + assert.equal(result.releases.length, 3) + }) + } +}) + +test("every included Release semantic field blocks parity drift", async (t) => { + const cases = [ + ["name", (release) => (release.name = "Different release")], + ["target commitish", (release) => (release.target_commitish = "f".repeat(40))], + ["draft", (release) => (release.draft = false)], + ["immutable", (release) => (release.immutable = true)], + ["prerelease", (release) => (release.prerelease = true)], + ["published at", (release) => (release.published_at = "2026-09-01T00:00:00.000Z")], + ["canonical body", (release) => (release.body = `${release.body} `)], + ["author login", (release) => (release.author.login = "somebody-else")], + ["author id", (release) => (release.author.id = 2048)], + ["author node id", (release) => (release.author.node_id = "MDQ6VXNlcjIwNDg=")], + ] + for (const [name, mutate] of cases) { + await t.test(name, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + mutate(fixture.releases[1]) + await assert.rejects( + inspectEquivalentDrafts(INPUT(fixture)), + /candidate|canonical|equal|mutable|parity|published|author/iu, + ) + }) + } +}) + +test("every included asset semantic field blocks parity drift", async (t) => { + const cases = [ + ["name", (asset) => (asset.name = "unknown.bin")], + ["label", (asset) => (asset.label = "changed")], + ["state", (asset) => (asset.state = "new")], + ["content type", (asset) => (asset.content_type = "application/octet-stream")], + ["size", (asset) => (asset.size += 1)], + ["digest", (asset) => (asset.digest = `sha256:${"f".repeat(64)}`)], + ["uploader login", (asset) => (asset.uploader.login = "somebody-else")], + ["uploader id", (asset) => (asset.uploader.id = 2048)], + ["uploader node id", (asset) => (asset.uploader.node_id = "uploader-changed")], + ] + for (const [name, mutate] of cases) { + await t.test(name, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + mutate(fixture.releases[1].assets[0]) + await assert.rejects( + inspectEquivalentDrafts(INPUT(fixture)), + /asset|digest|download|equal|parity|uploaded|unknown/iu, + ) + }) + } +}) + +test("rejects noncanonical core evidence and incorrect downloaded bytes", async (t) => { + const cases = [ + ["malformed marker", (fixture) => (fixture.releases[0].body = "not a release marker")], + ["noncanonical marker", (fixture) => (fixture.releases[0].body += "\n")], + [ + "release record bytes", + (fixture) => + fixture.replaceAssetBytes(fixture.survivorId, "release-record.json", Buffer.from("{}\n"), { + updateMetadata: true, + }), + ], + [ + "manifest bytes", + (fixture) => + fixture.replaceAssetBytes(fixture.survivorId, "manifest.json", Buffer.from("{}\n"), { + updateMetadata: true, + }), + ], + [ + "manifest package order drift", + (fixture) => mutateManifest(fixture, (manifest) => manifest.packageOrder.reverse()), + ], + [ + "manifest package name drift", + (fixture) => + mutateManifest(fixture, (manifest) => (manifest.packages[0].name = "@dawn-ai/not-real")), + ], + [ + "manifest package hash drift", + (fixture) => + mutateManifest(fixture, (manifest) => (manifest.packages[0].sha256 = "f".repeat(64))), + ], + [ + "download mismatch", + (fixture) => + fixture.replaceAssetBytes(fixture.survivorId, "manifest.json", Buffer.from("changed")), + ], + [ + "bundle set mismatch", + (fixture) => + fixture.replaceAssetBytes( + fixture.survivorId, + fixture.releases[0].assets.at(-1).name, + Buffer.from("different bundle"), + { updateMetadata: true }, + ), + ], + ] + for (const [name, mutate] of cases) { + await t.test(name, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + mutate(fixture) + await assert.rejects( + inspectEquivalentDrafts(INPUT(fixture)), + /asset|attestation|bundle|canonical|digest|JSON|manifest|marker|package|record/iu, + ) + }) + } +}) + +test("rejects a canonical shared marker whose release-record digest is wrong", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + fixture.replaceMarker((marker) => { + marker.releaseRecordSha256 = "f".repeat(64) + }) + await assert.rejects(inspectEquivalentDrafts(INPUT(fixture)), /marker|record|digest/iu) +}) + +test("rejects malformed or non-exact Release asset inventories before destructive evidence exists", async (t) => { + const cases = [ + ["missing asset", (fixture) => fixture.releases[0].assets.pop()], + [ + "extra asset", + (fixture) => + fixture.releases[0].assets.push({ + ...fixture.releases[0].assets[0], + id: 888_888, + name: "extra.bin", + }), + ], + [ + "duplicate asset name", + (fixture) => (fixture.releases[0].assets[1].name = fixture.releases[0].assets[0].name), + ], + [ + "duplicate asset id", + (fixture) => (fixture.releases[0].assets[1].id = fixture.releases[0].assets[0].id), + ], + ["non-uploaded asset", (fixture) => (fixture.releases[0].assets[0].state = "new")], + ["missing GitHub digest", (fixture) => (fixture.releases[0].assets[0].digest = null)], + [ + "malformed GitHub digest", + (fixture) => (fixture.releases[0].assets[0].digest = `sha256:${"A".repeat(64)}`), + ], + [ + "per-Release payload over 64 MiB", + (fixture) => { + fixture.releases[0].assets[0].size = 32 * 1024 * 1024 + fixture.releases[0].assets[1].size = 32 * 1024 * 1024 + fixture.releases[0].assets[2].size = 1 + }, + ], + ] + for (const [name, mutate] of cases) { + await t.test(name, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + mutate(fixture) + await assert.rejects( + inspectEquivalentDrafts(INPUT(fixture)), + /45|asset|digest|duplicate|escrow|limit|payload|uploaded/iu, + ) + }) + } +}) + +test("rejects a fourth matching draft, a published candidate Release, wrong roles, or wrong author", async (t) => { + await t.test("fourth matching draft", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + fixture.releases.push({ + ...structuredClone(fixture.releases[0]), + id: 444_444_444, + node_id: "RE_fourth", + tag_name: "untagged-fourth", + }) + await assert.rejects(inspectEquivalentDrafts(INPUT(fixture)), /exactly three|fourth|managed/iu) + assert.equal(fixture.downloadCount, 0) + }) + await t.test("published candidate", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + fixture.releases.push({ + ...structuredClone(fixture.releases[0]), + id: 444_444_444, + tag_name: DUPLICATE_DRAFT_CANDIDATE.tag, + draft: false, + immutable: true, + published_at: "2026-09-01T00:00:00.000Z", + }) + await assert.rejects(inspectEquivalentDrafts(INPUT(fixture)), /published/iu) + }) + await t.test("reordered roles", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + fixture.duplicateIds.reverse() + await assert.rejects(inspectEquivalentDrafts(INPUT(fixture)), /approved|order|role/iu) + }) + await t.test("wrong author", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + for (const release of fixture.releases) release.author.login = "wrong-owner" + await assert.rejects(inspectEquivalentDrafts(INPUT(fixture)), /author/iu) + }) + await t.test("wrong author stable id", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + for (const release of fixture.releases) release.author.id = 2048 + await assert.rejects(inspectEquivalentDrafts(INPUT(fixture)), /author/iu) + }) + await t.test("wrong author node id", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + for (const release of fixture.releases) release.author.node_id = "MDQ6VXNlcjIwNDg=" + await assert.rejects(inspectEquivalentDrafts(INPUT(fixture)), /author/iu) + }) + await t.test("failed attestation verification", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + fixture.failVerification() + await assert.rejects(inspectEquivalentDrafts(INPUT(fixture)), /attestation|verification/iu) + }) +}) + +test("rejects an oversized download envelope before base64 decoding", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const github = { + ...fixture.github, + async downloadReleaseAsset() { + return { + status: "PRESENT", + operation: "release-asset-download", + httpStatus: 200, + code: null, + contentBase64: "A".repeat(1024 * 1024), + } + }, + } + await assert.rejects( + inspectEquivalentDrafts({ ...INPUT(fixture), github }), + /base64|declared|download|size/iu, + ) +}) + +test("enforces aggregate accounting before a 136th download or a 192 MiB crossing", async (t) => { + await t.test("136th download", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + await assert.rejects( + inspectEquivalentDrafts({ + ...INPUT(fixture), + accounting: { downloadedAssets: 1, downloadedBytes: 0 }, + }), + /135|download/iu, + ) + assert.equal(fixture.downloadCount, 0) + }) + await t.test("aggregate byte crossing", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + await assert.rejects( + inspectEquivalentDrafts({ + ...INPUT(fixture), + accounting: { + downloadedAssets: 0, + downloadedBytes: 192 * 1024 * 1024, + }, + }), + /192|aggregate|payload/iu, + ) + assert.equal(fixture.downloadCount, 0) + }) +}) + +test("captures a bounded monotone direct Release-by-ID and complete asset read", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const inspected = await inspectEquivalentDrafts(INPUT(fixture)) + fixture.clearOperations() + const timestamps = [ + "2026-09-01T12:00:00.000Z", + "2026-09-01T12:00:01.000Z", + "2026-09-01T12:00:02.000Z", + "2026-09-01T12:00:03.000Z", + ] + const direct = await captureDirectTargetRead({ + candidate: fixture.candidate, + releaseId: DUPLICATE_DRAFT_IDS[0], + role: "duplicate", + expectedEvidence: inspected.releases[1], + github: fixture.github, + now: () => timestamps.shift(), + }) + + assert.deepEqual(Object.fromEntries(Object.entries(direct).slice(0, 4)), { + releaseGetStartedAt: "2026-09-01T12:00:00.000Z", + releaseGetCompletedAt: "2026-09-01T12:00:01.000Z", + assetsListStartedAt: "2026-09-01T12:00:02.000Z", + assetsListCompletedAt: "2026-09-01T12:00:03.000Z", + }) + assert.equal(direct.evidence.id, DUPLICATE_DRAFT_IDS[0]) + assert.match(direct.evidenceSha256, /^[0-9a-f]{64}$/u) + assert.equal(Object.isFrozen(direct), true) + assert.deepEqual(fixture.operations, [ + `get:${DUPLICATE_DRAFT_IDS[0]}`, + `list-assets:${DUPLICATE_DRAFT_IDS[0]}`, + ]) +}) + +test("direct target reads allow approved service volatility and return its latest metadata", async (t) => { + const cases = [ + ["Release node id", (release) => (release.node_id = "RE_latest")], + ["opaque tag", (release) => (release.tag_name = "untagged-latest")], + ["Release creation", (release) => (release.created_at = "2026-08-29T00:00:00.000Z")], + ["Release update", (release) => (release.updated_at = "2026-09-01T00:00:00.000Z")], + ["asset id", (release) => (release.assets[0].id = 777_777_777)], + ["asset node id", (release) => (release.assets[0].node_id = "RA_latest")], + ["asset creation", (release) => (release.assets[0].created_at = "2026-08-29T00:00:00.000Z")], + ["asset update", (release) => (release.assets[0].updated_at = "2026-09-01T00:00:00.000Z")], + ["download count", (release) => (release.assets[0].download_count = 999)], + ] + for (const [name, mutate] of cases) { + await t.test(name, async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const inspected = await inspectEquivalentDrafts(INPUT(fixture)) + mutate(fixture.releases[1]) + fixture.clearOperations() + const direct = await captureDirectTargetRead({ + candidate: fixture.candidate, + releaseId: DUPLICATE_DRAFT_IDS[0], + role: "duplicate", + expectedEvidence: inspected.releases[1], + github: fixture.github, + }) + assert.deepEqual(fixture.operations, [ + `get:${DUPLICATE_DRAFT_IDS[0]}`, + `list-assets:${DUPLICATE_DRAFT_IDS[0]}`, + ]) + assert.deepEqual( + semanticReleaseProjection(direct.evidence), + semanticReleaseProjection(inspected.releases[1]), + ) + }) + } +}) + +test("direct target reads reject nonmonotone clocks and every included parity drift", async (t) => { + const setup = async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const inspected = await inspectEquivalentDrafts(INPUT(fixture)) + return { fixture, inspected } + } + await t.test("nonmonotone", async () => { + const { fixture, inspected } = await setup() + const timestamps = ["2026-09-01T12:00:01.000Z", "2026-09-01T12:00:00.000Z"] + await assert.rejects( + captureDirectTargetRead({ + candidate: fixture.candidate, + releaseId: DUPLICATE_DRAFT_IDS[0], + role: "duplicate", + expectedEvidence: inspected.releases[1], + github: fixture.github, + now: () => timestamps.shift() ?? "2026-09-01T12:00:02.000Z", + }), + /monotone|timestamp/iu, + ) + }) + const cases = [ + ["name", (release) => (release.name = "changed")], + ["target", (release) => (release.target_commitish = "f".repeat(40))], + ["draft", (release) => (release.draft = false)], + ["immutable", (release) => (release.immutable = true)], + ["prerelease", (release) => (release.prerelease = true)], + ["published", (release) => (release.published_at = "2026-09-01T00:00:00.000Z")], + ["body", (release) => (release.body += " ")], + ["author", (release) => (release.author.id = 2048)], + ["asset name", (release) => (release.assets[0].name = "wrong.bin")], + ["asset label", (release) => (release.assets[0].label = "changed")], + ["asset state", (release) => (release.assets[0].state = "new")], + ["asset content type", (release) => (release.assets[0].content_type = "text/plain")], + ["asset size", (release) => (release.assets[0].size += 1)], + ["asset digest", (release) => (release.assets[0].digest = `sha256:${"f".repeat(64)}`)], + ["asset uploader", (release) => (release.assets[0].uploader.id = 2048)], + ] + for (const [name, mutate] of cases) { + await t.test(name, async () => { + const { fixture, inspected } = await setup() + mutate(fixture.releases[1]) + fixture.clearOperations() + await assert.rejects( + captureDirectTargetRead({ + candidate: fixture.candidate, + releaseId: DUPLICATE_DRAFT_IDS[0], + role: "duplicate", + expectedEvidence: inspected.releases[1], + github: fixture.github, + }), + /asset|author|body|candidate|digest|evidence|equal|expected|marker|mutable|parity|proposal|uploaded/iu, + ) + assert.deepEqual(fixture.operations, [ + `get:${DUPLICATE_DRAFT_IDS[0]}`, + `list-assets:${DUPLICATE_DRAFT_IDS[0]}`, + ]) + }) + } +}) + +test("public evidence parsers reject hostile shapes without invoking accessors and return owned frozen data", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const result = await inspectEquivalentDrafts(INPUT(fixture)) + const source = structuredClone(result.releases[0]) + const parsed = parseReleaseEvidence(source) + source.semantic.name = "mutated later" + assert.notEqual(parsed.semantic.name, source.semantic.name) + assert.equal(Object.isFrozen(parsed), true) + assert.equal(Object.isFrozen(parsed.semantic), true) + assert.deepEqual(assertEvidenceEqualsProposal(parsed, result.releases[0]), parsed) + const volatile = structuredClone(result.releases[0]) + volatile.nodeId = "RE_latest" + volatile.tagName = "untagged-latest" + volatile.createdAt = "2026-08-29T00:00:00.000Z" + volatile.updatedAt = "2026-09-01T00:00:00.000Z" + volatile.assets[0].id = "777777777" + volatile.assets[0].nodeId = "RA_latest" + volatile.assets[0].downloadCount = 999 + assert.deepEqual(assertEvidenceEqualsProposal(volatile, parsed), parseReleaseEvidence(volatile)) + const oversized = structuredClone(result.releases[0]) + oversized.assets[0].size = 32 * 1024 * 1024 + oversized.assets[1].size = 32 * 1024 * 1024 + oversized.assets[2].size = 1 + assert.throws(() => parseReleaseEvidence(oversized), /escrow|payload|limit/iu) + + let invoked = false + const accessor = Object.defineProperty({}, "role", { + enumerable: true, + get() { + invoked = true + throw new Error("must not run") + }, + }) + for (const value of [ + accessor, + new Proxy({}, {}), + { ...structuredClone(result.releases[0]), [Symbol("hidden")]: true }, + Object.defineProperty(structuredClone(result.releases[0]), "hidden", { + value: true, + }), + { ...structuredClone(result.releases[0]), assets: new Array(45) }, + ]) { + assert.throws( + () => parseReleaseEvidence(value), + /accessor|array|data|field|plain|proxy|snapshot|symbol/iu, + ) + } + assert.equal(invoked, false) + assert.throws(() => semanticReleaseProjection(accessor), /accessor|data|field|plain|snapshot/iu) + assert.throws(() => semanticAssetProjection(accessor), /accessor|data|field|plain|snapshot/iu) +}) + +test("strict evidence IDs accept only canonical positive decimal strings", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + const result = await inspectEquivalentDrafts(INPUT(fixture)) + const invalid = [61436, 0, "0", "061436", "-1", "+1", " 1", "1 ", "1e3"] + for (const value of invalid) { + for (const mutate of [ + (evidence) => (evidence.id = value), + (evidence) => (evidence.semantic.author.id = value), + (evidence) => (evidence.assets[0].id = value), + (evidence) => (evidence.assets[0].uploader.id = value), + ]) { + const evidence = structuredClone(result.releases[0]) + mutate(evidence) + assert.throws(() => parseReleaseEvidence(evidence), /decimal|id|identity/iu) + } + } +}) + +test("candidate identity is exact", async () => { + const fixture = createDuplicateDraftConsolidationFixture() + await assert.rejects( + inspectEquivalentDrafts({ + ...INPUT(fixture), + candidate: { ...DUPLICATE_DRAFT_CANDIDATE, commitSha: "f".repeat(40) }, + }), + /new Error|candidate|approved/iu, + ) +}) + +function mutateManifest(fixture, mutate) { + const bytes = fixture.assetBytes(fixture.survivorId, "manifest.json") + const manifest = JSON.parse(bytes.toString("utf8")) + mutate(manifest) + fixture.replaceAssetBytes( + fixture.survivorId, + "manifest.json", + Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"), + { updateMetadata: true }, + ) +} diff --git a/scripts/release/test/duplicate-draft-consolidation-files.test.mjs b/scripts/release/test/duplicate-draft-consolidation-files.test.mjs new file mode 100644 index 000000000..eff1bbe2a --- /dev/null +++ b/scripts/release/test/duplicate-draft-consolidation-files.test.mjs @@ -0,0 +1,1067 @@ +import assert from "node:assert/strict" +import { + chmod, + link, + lstat, + mkdir, + mkdtemp, + open, + readdir, + readFile, + realpath, + rename, + rm, + stat, + symlink, + unlink, + writeFile, +} from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import test from "node:test" + +import { + readPrivateEnvelope, + readTrackedReceipt, + writePrivateEnvelope, + writeTrackedReceipt, +} from "../duplicate-draft-consolidation-files.mjs" + +const MAXIMUM_BYTES = 1024 * 1024 +const MAXIMUM_WRITE_BYTES = 96 * 1024 * 1024 +const PRIVATE_MODE = 0o600 +const TRACKED_MODE = 0o644 + +test("private envelopes round trip through exact mode 0600 without aliasing caller bytes", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, ".dawn", "proposal.json") + await mkdir(path.dirname(target), { mode: 0o700 }) + const bytes = Buffer.from("private evidence\n") + + const writing = writePrivateEnvelope(target, bytes) + bytes.fill(0x78) + const written = await writing + + assert.equal((await stat(target)).mode & 0o777, PRIVATE_MODE) + assert.deepEqual(written, Buffer.from("private evidence\n")) + const read = await readPrivateEnvelope(target, MAXIMUM_BYTES) + assert.deepEqual(read, Buffer.from("private evidence\n")) + read.fill(0x79) + assert.deepEqual(await readFile(target), Buffer.from("private evidence\n")) +}) + +test("private-read provenance is non-forgeable and bound to its exact path", async (t) => { + const repository = await temporaryRepository(t) + const first = path.join(repository, ".dawn", "first.json") + const second = path.join(repository, ".dawn", "second.json") + await mkdir(path.dirname(first), { mode: 0o700 }) + const bytes = Buffer.from("same private bytes\n") + await writePrivateEnvelope(first, bytes) + await writePrivateEnvelope(second, bytes) + const authenticated = await readPrivateEnvelope(first, MAXIMUM_BYTES) + + assert.equal(Reflect.ownKeys(authenticated).includes("consolidationPrivateFileIdentity"), false) + await assert.rejects( + writePrivateEnvelope(second, Buffer.from("replacement\n"), undefined, authenticated), + /path|provenance|authenticated|current/iu, + ) +}) + +test("an existing exact journal lock fails closed before replacement", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join( + repository, + ".dawn", + "release", + "duplicate-draft-consolidation.journal.json", + ) + await mkdir(path.dirname(target), { recursive: true, mode: 0o700 }) + await writePrivateEnvelope(target, Buffer.from("original\n")) + const current = await readPrivateEnvelope(target, MAXIMUM_BYTES) + const lockPath = path.join(path.dirname(target), `.${path.basename(target)}.lock`) + await writeFile(lockPath, '{"schemaVersion":1,"owner":"other"}\n', { + mode: 0o600, + }) + + await assert.rejects( + writePrivateEnvelope(target, Buffer.from("replacement\n"), undefined, current), + /lock|exclusive|exist|transaction/iu, + ) + assert.deepEqual(await readFile(target), Buffer.from("original\n")) +}) + +test("a provably dead canonical lock is quarantined before a new journal lease", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join( + repository, + ".dawn", + "release", + "duplicate-draft-consolidation.journal.json", + ) + await mkdir(path.dirname(target), { recursive: true, mode: 0o700 }) + const lockPath = path.join(path.dirname(target), `.${path.basename(target)}.lock`) + await writeFile(lockPath, canonicalLockRecord({ lockPath, pid: 2_147_483_647 }), { mode: 0o600 }) + + await writePrivateEnvelope(target, Buffer.from("recovered\n")) + + assert.deepEqual(await readFile(target), Buffer.from("recovered\n")) + const names = await readdir(path.dirname(target)) + assert.equal(names.includes(path.basename(lockPath)), false) + assert.equal( + names.some( + (name) => name.startsWith(`${path.basename(lockPath)}.`) && name.endsWith(".quarantine"), + ), + true, + ) +}) + +test("live and PID-reused lock owners are never stolen", async (t) => { + for (const processStartIdentity of [null, "different-process-start"]) { + await t.test(String(processStartIdentity), async (t) => { + const repository = await temporaryRepository(t) + const { target, lockPath } = await journalLockPaths(repository) + const record = JSON.parse( + canonicalLockRecord({ lockPath, pid: process.pid }).toString("utf8"), + ) + record.processStartIdentity = processStartIdentity + await writeFile(lockPath, `${JSON.stringify(record)}\n`, { mode: 0o600 }) + + await assert.rejects( + writePrivateEnvelope(target, Buffer.from("blocked\n")), + /live|PID|reused|owner|lock/iu, + ) + assert.equal((await lstat(lockPath)).isFile(), true) + await assert.rejects(readFile(target), /ENOENT/iu) + }) + } +}) + +test("malformed, symlinked, hardlinked, wrong-mode, and wrong-owner locks fail closed", async (t) => { + for (const kind of ["malformed", "symlink", "hardlink", "mode", "owner"]) { + await t.test(kind, async (t) => { + const repository = await temporaryRepository(t) + const { target, lockPath } = await journalLockPaths(repository) + const recordPath = path.join(path.dirname(lockPath), `${kind}.record`) + const bytes = canonicalLockRecord({ + lockPath, + pid: 2_147_483_647, + }) + let dependencies + if (kind === "malformed") { + await writeFile(lockPath, "not-json\n", { mode: 0o600 }) + } else if (kind === "symlink") { + await writeFile(recordPath, bytes, { mode: 0o600 }) + await symlink(recordPath, lockPath) + } else if (kind === "hardlink") { + await writeFile(recordPath, bytes, { mode: 0o600 }) + await link(recordPath, lockPath) + } else { + await writeFile(lockPath, bytes, { + mode: kind === "mode" ? 0o644 : 0o600, + }) + if (kind === "owner") { + dependencies = { effectiveUserId: () => effectiveUserId() + 1 } + } + } + + await assert.rejects( + writePrivateEnvelope(target, Buffer.from("blocked\n"), dependencies), + /lock|regular|follow|link|mode|owner|canonical|record|invalid/iu, + ) + await assert.rejects(readFile(target), /ENOENT/iu) + }) + } +}) + +test("stale-lock replacement during quarantine is detected without publishing", async (t) => { + const repository = await temporaryRepository(t) + const { target, lockPath } = await journalLockPaths(repository) + await writeFile(lockPath, canonicalLockRecord({ lockPath, pid: 2_147_483_647 }), { mode: 0o600 }) + const displacedPath = `${lockPath}.displaced` + const fileSystem = fileSystemWith({ + async rename(source, destination) { + if (source === lockPath && destination.endsWith(".quarantine")) { + await rename(source, displacedPath) + await writeFile( + source, + canonicalLockRecord({ + lockPath, + pid: 2_147_483_646, + nonce: "abcdefab-cdef-4abc-8def-abcdefabcdef", + }), + { mode: 0o600 }, + ) + } + return rename(source, destination) + }, + }) + + await assert.rejects( + writePrivateEnvelope(target, Buffer.from("blocked\n"), { fileSystem }), + /identity|changed|quarantine|lock/iu, + ) + assert.equal((await lstat(displacedPath)).isFile(), true) + await assert.rejects(readFile(target), /ENOENT/iu) +}) + +test("lock acquisition failure before identity never removes an unproven path", async (t) => { + const repository = await temporaryRepository(t) + const { target, lockPath } = await journalLockPaths(repository) + let lockUnlinks = 0 + const fileSystem = fileSystemWith({ + async open(filePath, flags, mode) { + const handle = await open(filePath, flags, mode) + if (filePath !== lockPath) return handle + return wrapHandle(handle, { + async chmod() { + throw new Error("injected crash before lock identity") + }, + }) + }, + async unlink(filePath) { + if (filePath === lockPath) lockUnlinks += 1 + return unlink(filePath) + }, + }) + + await assert.rejects( + writePrivateEnvelope(target, Buffer.from("blocked\n"), { fileSystem }), + /crash|identity|lock|transaction/iu, + ) + assert.equal(lockUnlinks, 0) + assert.equal((await lstat(lockPath)).isFile(), true) + await assert.rejects(readFile(target), /ENOENT/iu) +}) + +test("journal serialization blocks a final-check to rename overwrite race", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join( + repository, + ".dawn", + "release", + "duplicate-draft-consolidation.journal.json", + ) + await mkdir(path.dirname(target), { recursive: true, mode: 0o700 }) + await writePrivateEnvelope(target, Buffer.from("original\n")) + const current = await readPrivateEnvelope(target, MAXIMUM_BYTES) + let releaseFirstRename + let signalFirstRename + const firstRenameReached = new Promise((resolve) => { + signalFirstRename = resolve + }) + const firstRenameGate = new Promise((resolve) => { + releaseFirstRename = resolve + }) + let journalRenames = 0 + const fileSystem = fileSystemWith({ + async rename(source, destination) { + if (destination === target) { + journalRenames += 1 + if (journalRenames === 1) { + signalFirstRename() + await firstRenameGate + } + } + return rename(source, destination) + }, + }) + const first = writePrivateEnvelope.withExclusiveTransaction( + target, + () => writePrivateEnvelope(target, Buffer.from("first\n"), { fileSystem }, current), + { fileSystem }, + ) + await firstRenameReached + const second = writePrivateEnvelope.withExclusiveTransaction( + target, + () => writePrivateEnvelope(target, Buffer.from("second\n"), { fileSystem }, current), + { fileSystem }, + ) + await assert.rejects(second, /lock|exclusive|exist|transaction/iu) + releaseFirstRename() + await first + + assert.equal(journalRenames, 1) + assert.deepEqual(await readFile(target), Buffer.from("first\n")) +}) + +test("authenticated journal CAS requires an active exact transaction lease", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join( + repository, + ".dawn", + "release", + "duplicate-draft-consolidation.journal.json", + ) + await mkdir(path.dirname(target), { recursive: true, mode: 0o700 }) + await writePrivateEnvelope(target, Buffer.from("original\n")) + const current = await readPrivateEnvelope(target, MAXIMUM_BYTES) + + await assert.rejects( + writePrivateEnvelope(target, Buffer.from("outside lease\n"), undefined, current), + /active|lease|transaction|lock/iu, + ) + await writePrivateEnvelope.withExclusiveTransaction(target, async () => { + const leasedCurrent = await readPrivateEnvelope(target, MAXIMUM_BYTES) + await writePrivateEnvelope(target, Buffer.from("inside lease\n"), undefined, leasedCurrent) + }) + assert.deepEqual(await readFile(target), Buffer.from("inside lease\n")) +}) + +test("every in-scope journal and head publication holds the cooperative writer lock", async (t) => { + const repository = await temporaryRepository(t) + const releaseDirectory = path.join(repository, ".dawn", "release") + await mkdir(releaseDirectory, { recursive: true, mode: 0o700 }) + const lockPath = path.join(releaseDirectory, ".duplicate-draft-consolidation.journal.json.lock") + for (const basename of [ + "duplicate-draft-consolidation.journal.json", + "duplicate-draft-consolidation.journal.head.json", + ]) { + const target = path.join(releaseDirectory, basename) + let observedLock = false + const fileSystem = fileSystemWith({ + async rename(source, destination) { + if (destination === target) { + const lock = await lstat(lockPath) + observedLock = lock.isFile() && (lock.mode & 0o777) === 0o600 + } + return rename(source, destination) + }, + }) + await writePrivateEnvelope(target, Buffer.from(`${basename}\n`), { + fileSystem, + }) + assert.equal(observedLock, true) + } +}) + +test("deferred work cannot inherit a revoked lease while another writer holds the lock", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join( + repository, + ".dawn", + "release", + "duplicate-draft-consolidation.journal.json", + ) + await mkdir(path.dirname(target), { recursive: true, mode: 0o700 }) + await writePrivateEnvelope(target, Buffer.from("original\n")) + let releaseDeferred + const deferredGate = new Promise((resolve) => { + releaseDeferred = resolve + }) + let deferred + await writePrivateEnvelope.withExclusiveTransaction(target, async () => { + deferred = (async () => { + await deferredGate + return writePrivateEnvelope.withExclusiveTransaction(target, async () => + readPrivateEnvelope(target, MAXIMUM_BYTES), + ) + })() + }) + + let signalSecondLease + const secondLeaseEntered = new Promise((resolve) => { + signalSecondLease = resolve + }) + let releaseSecondLease + const secondLeaseGate = new Promise((resolve) => { + releaseSecondLease = resolve + }) + const second = writePrivateEnvelope.withExclusiveTransaction(target, async () => { + signalSecondLease() + await secondLeaseGate + }) + await secondLeaseEntered + releaseDeferred() + await assert.rejects(deferred, /active|lease|lock|exclusive|exist/iu) + releaseSecondLease() + await second +}) + +test("tracked receipts accept ordinary nonexecutable Git mode 0644", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "receipt.json") + const bytes = Buffer.from("tracked receipt\n") + + await writeTrackedReceipt(target, bytes) + + assert.equal((await stat(target)).mode & 0o777, TRACKED_MODE) + assert.deepEqual(await readTrackedReceipt(target, MAXIMUM_BYTES), bytes) +}) + +test("reads and replacements reject symlinked files and parent path components", async (t) => { + const repository = await temporaryRepository(t) + const outside = path.join(repository, "outside") + const linkedParent = path.join(repository, "linked") + await mkdir(outside) + const outsideFile = path.join(outside, "evidence.json") + await writeFile(outsideFile, "secret\n", { mode: PRIVATE_MODE }) + await symlink(outside, linkedParent, "dir") + const linkedFile = path.join(linkedParent, "evidence.json") + + await assert.rejects(readPrivateEnvelope(linkedFile, MAXIMUM_BYTES), /symlink|unsafe/iu) + await assert.rejects( + writePrivateEnvelope(linkedFile, Buffer.from("replacement\n")), + /symlink|unsafe/iu, + ) + + const directLink = path.join(repository, "direct-link.json") + await symlink(outsideFile, directLink) + await assert.rejects(readPrivateEnvelope(directLink, MAXIMUM_BYTES), /regular|symlink/iu) + await assert.rejects( + writePrivateEnvelope(directLink, Buffer.from("replacement\n")), + /regular|symlink/iu, + ) + assert.deepEqual(await readFile(outsideFile), Buffer.from("secret\n")) +}) + +test("reads reject non-regular files, hard links, and injected wrong ownership", async (t) => { + const repository = await temporaryRepository(t) + const directory = path.join(repository, "directory") + await mkdir(directory) + await assert.rejects(readPrivateEnvelope(directory, MAXIMUM_BYTES), /regular/iu) + + const target = path.join(repository, "private.json") + const alias = path.join(repository, "alias.json") + await writeFile(target, "evidence\n", { mode: PRIVATE_MODE }) + await link(target, alias) + await assert.rejects(readPrivateEnvelope(target, MAXIMUM_BYTES), /link/iu) + await unlink(alias) + + await assert.rejects( + readPrivateEnvelope(target, MAXIMUM_BYTES, { + effectiveUserId: () => effectiveUserId() + 1, + }), + /owner/iu, + ) +}) + +test("private sources require exactly 0600 while tracked sources reject executable or writable modes", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "source.json") + await writeFile(target, "evidence\n", { mode: 0o640 }) + await assert.rejects(readPrivateEnvelope(target, MAXIMUM_BYTES), /0600|mode/iu) + + for (const mode of [0o744, 0o664, 0o646]) { + await chmod(target, mode) + await assert.rejects( + readTrackedReceipt(target, MAXIMUM_BYTES), + /executable|writable|mode/iu, + mode.toString(8), + ) + } + + await chmod(target, TRACKED_MODE) + assert.deepEqual(await readTrackedReceipt(target, MAXIMUM_BYTES), Buffer.from("evidence\n")) +}) + +test("private and tracked reads reject every special permission bit", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "special-mode.json") + await writeFile(target, "evidence\n", { mode: PRIVATE_MODE }) + + for (const specialBit of [0o4000, 0o2000, 0o1000]) { + await chmod(target, PRIVATE_MODE | specialBit) + assert.equal((await stat(target)).mode & 0o7777, PRIVATE_MODE | specialBit) + await assert.rejects( + readPrivateEnvelope(target, MAXIMUM_BYTES), + /0600|special|mode/iu, + `private ${specialBit.toString(8)}`, + ) + + await chmod(target, TRACKED_MODE | specialBit) + assert.equal((await stat(target)).mode & 0o7777, TRACKED_MODE | specialBit) + await assert.rejects( + readTrackedReceipt(target, MAXIMUM_BYTES), + /special|mode/iu, + `tracked ${specialBit.toString(8)}`, + ) + } +}) + +test("replacement refuses unsafe existing destinations before creating a temporary file", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "destination.json") + const alias = path.join(repository, "destination-alias.json") + const replacement = Buffer.from("replacement\n") + + await mkdir(target) + await assert.rejects(writePrivateEnvelope(target, replacement), /regular/iu) + await rm(target, { recursive: true }) + + await writeFile(target, "previous\n", { mode: PRIVATE_MODE }) + await link(target, alias) + await assert.rejects(writePrivateEnvelope(target, replacement), /link/iu) + await unlink(alias) + + await chmod(target, 0o640) + await assert.rejects(writePrivateEnvelope(target, replacement), /0600|mode/iu) + await chmod(target, PRIVATE_MODE) + await assert.rejects( + writePrivateEnvelope(target, replacement, { + effectiveUserId: () => effectiveUserId() + 1, + }), + /owner/iu, + ) + + await chmod(target, 0o664) + await assert.rejects(writeTrackedReceipt(target, replacement), /writable|mode/iu) + assert.deepEqual(await readFile(target), Buffer.from("previous\n")) + assert.deepEqual( + (await entries(repository)).filter((name) => name.endsWith(".tmp")), + [], + ) +}) + +test("replacement refuses existing private and tracked destinations with special permission bits", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "destination.json") + const replacement = Buffer.from("replacement\n") + await writeFile(target, "previous\n", { mode: PRIVATE_MODE }) + + for (const specialBit of [0o4000, 0o2000, 0o1000]) { + await chmod(target, PRIVATE_MODE | specialBit) + await assert.rejects( + writePrivateEnvelope(target, replacement), + /0600|special|mode/iu, + `private ${specialBit.toString(8)}`, + ) + + await chmod(target, TRACKED_MODE | specialBit) + await assert.rejects( + writeTrackedReceipt(target, replacement), + /special|mode/iu, + `tracked ${specialBit.toString(8)}`, + ) + } + assert.deepEqual(await readFile(target), Buffer.from("previous\n")) +}) + +test("temporary-file validation rejects injected special permission bits before publication", async (t) => { + const repository = await temporaryRepository(t) + for (const [label, writeEnvelope, injectedMode] of [ + ["private", writePrivateEnvelope, 0o4600], + ["tracked", writeTrackedReceipt, 0o4644], + ]) { + const target = path.join(repository, `${label}.json`) + const temporaryBefore = new Set( + (await entries(repository)).filter((name) => name.endsWith(".tmp")), + ) + let renames = 0 + const fileSystem = fileSystemWith({ + async open(filePath, flags, mode) { + const handle = await open(filePath, flags, mode) + if (!filePath.endsWith(".tmp")) return handle + return wrapHandle(handle, { + async stat(options) { + return statusWithMode(await handle.stat(options), injectedMode) + }, + }) + }, + async rename(from, to) { + renames += 1 + return rename(from, to) + }, + }) + + await assert.rejects( + writeEnvelope(target, Buffer.from("replacement\n"), { fileSystem }), + /retained/iu, + label, + ) + assert.equal(renames, 0, label) + await assert.rejects(lstat(target), { code: "ENOENT" }) + const retained = (await entries(repository)).filter( + (name) => name.endsWith(".tmp") && !temporaryBefore.has(name), + ) + assert.equal(retained.length, 1, label) + await rm(path.join(repository, retained[0])) + } + assert.deepEqual( + (await entries(repository)).filter((name) => name.endsWith(".tmp")), + [], + ) +}) + +test("read and write byte bounds fail before reading or publishing oversized input", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "oversized.json") + await writeFile(target, Buffer.alloc(32, 0x61), { mode: PRIVATE_MODE }) + let descriptorReads = 0 + const fileSystem = fileSystemWith({ + async open(filePath, flags, mode) { + const handle = await open(filePath, flags, mode) + if (filePath !== target) return handle + return wrapHandle(handle, { + async read(...arguments_) { + descriptorReads += 1 + return handle.read(...arguments_) + }, + }) + }, + }) + await assert.rejects(readPrivateEnvelope(target, 16, { fileSystem }), /bound|limit|large/iu) + assert.equal(descriptorReads, 0) + + const oversized = new Uint8Array(MAXIMUM_WRITE_BYTES + 1) + await assert.rejects(writePrivateEnvelope(target, oversized), /bound|limit|large/iu) + assert.deepEqual(await readFile(target), Buffer.alloc(32, 0x61)) +}) + +test("reads reject pathname replacement and same-size mutation while the descriptor is open", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "private.json") + const displaced = path.join(repository, "displaced.json") + const original = Buffer.alloc(128 * 1024, 0x61) + await writeFile(target, original, { mode: PRIVATE_MODE }) + + let replaced = false + const replacementFileSystem = fileSystemWith({ + async open(filePath, flags, mode) { + const handle = await open(filePath, flags, mode) + if (filePath !== target) return handle + return wrapHandle(handle, { + async read(...arguments_) { + const result = await handle.read(...arguments_) + if (!replaced) { + replaced = true + await rename(target, displaced) + await writeFile(target, Buffer.alloc(original.length, 0x62), { + mode: PRIVATE_MODE, + }) + } + return result + }, + }) + }, + }) + await assert.rejects( + readPrivateEnvelope(target, original.length, { + fileSystem: replacementFileSystem, + }), + /changed/iu, + ) + + await rm(target) + await rename(displaced, target) + let mutated = false + const mutationFileSystem = fileSystemWith({ + async open(filePath, flags, mode) { + const handle = await open(filePath, flags, mode) + if (filePath !== target) return handle + return wrapHandle(handle, { + async read(...arguments_) { + const result = await handle.read(...arguments_) + if (!mutated) { + mutated = true + await writeFile(target, Buffer.alloc(original.length, 0x63), { + mode: PRIVATE_MODE, + }) + } + return result + }, + }) + }, + }) + await assert.rejects( + readPrivateEnvelope(target, original.length, { + fileSystem: mutationFileSystem, + }), + /changed/iu, + ) +}) + +test("reads revalidate the final pathname after consuming the descriptor", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "private.json") + await writeFile(target, "evidence\n", { mode: PRIVATE_MODE }) + let targetLstats = 0 + const fileSystem = fileSystemWith({ + async lstat(filePath, options) { + if (filePath === target) { + targetLstats += 1 + throw new Error("final-path-revalidation") + } + return lstat(filePath, options) + }, + }) + + await assert.rejects( + readPrivateEnvelope(target, MAXIMUM_BYTES, { fileSystem }), + /final-path-revalidation/, + ) + assert.equal(targetLstats, 1) +}) + +test("partial writes and every pre-rename failure preserve the destination and retain the temporary pathname", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "private.json") + const previous = Buffer.from("previous complete evidence\n") + await writeFile(target, previous, { mode: PRIVATE_MODE }) + + for (const fault of ["partial-write", "file-sync", "rename"]) { + const temporaryBefore = new Set( + (await entries(repository)).filter((name) => name.endsWith(".tmp")), + ) + const fileSystem = fileSystemWith({ + async open(filePath, flags, mode) { + const handle = await open(filePath, flags, mode) + if (!filePath.endsWith(".tmp")) return handle + if (fault === "partial-write") { + let writes = 0 + return wrapHandle(handle, { + async write(...arguments_) { + writes += 1 + if (writes > 1) throw new Error("injected partial write failure") + const [buffer, offset, length, position] = arguments_ + return handle.write(buffer, offset, Math.min(3, length), position) + }, + }) + } + if (fault === "file-sync") { + return wrapHandle(handle, { + async sync() { + throw new Error("injected file sync failure") + }, + }) + } + return handle + }, + async rename(from, to) { + if (fault === "rename" && to === target) throw new Error("injected rename failure") + return rename(from, to) + }, + }) + + await assert.rejects( + writePrivateEnvelope(target, Buffer.from(`replacement ${fault}\n`), { + fileSystem, + }), + /retained/iu, + fault, + ) + assert.deepEqual(await readFile(target), previous, fault) + const retained = (await entries(repository)).filter( + (name) => name.endsWith(".tmp") && !temporaryBefore.has(name), + ) + assert.equal(retained.length, 1, fault) + await rm(path.join(repository, retained[0])) + } +}) + +test("failure retention reports a replacement swapped after identity observation and never unlinks it", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "private.json") + const identifier = "11111111-1111-4111-8111-111111111111" + const temporary = path.join(repository, `.private.json.${process.pid}.${identifier}.tmp`) + const sibling = path.join(repository, "operation-owned-sibling.tmp") + await writeFile(target, "previous\n", { mode: PRIVATE_MODE }) + let attacked = false + let writeFailed = false + let unlinkCalls = 0 + const fileSystem = fileSystemWith({ + async lstat(filePath, options) { + const status = await lstat(filePath, options) + if (filePath === temporary && writeFailed && !attacked) { + attacked = true + await rename(temporary, sibling) + await writeFile(temporary, "attacker replacement\n", { + mode: PRIVATE_MODE, + }) + } + return status + }, + async open(filePath, flags, mode) { + const handle = await open(filePath, flags, mode) + if (filePath !== temporary) return handle + return wrapHandle(handle, { + async write(...arguments_) { + await handle.write(...arguments_) + writeFailed = true + throw new Error("injected post-attack write failure") + }, + }) + }, + async unlink(filePath) { + unlinkCalls += 1 + return unlink(filePath) + }, + }) + + await assert.rejects( + writePrivateEnvelope(target, Buffer.from("replacement\n"), { + fileSystem, + randomUUID: () => identifier, + }), + /no longer identifies|replacement.*untouched/iu, + ) + assert.deepEqual(await readFile(target), Buffer.from("previous\n")) + assert.deepEqual(await readFile(temporary), Buffer.from("attacker replacement\n")) + assert.equal((await lstat(sibling)).isFile(), true) + assert.equal(unlinkCalls, 0) +}) + +test("failure retention reports when the operation-owned temporary inode is missing", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "private.json") + const identifier = "22222222-2222-4222-8222-222222222222" + const temporary = path.join(repository, `.private.json.${process.pid}.${identifier}.tmp`) + const displaced = path.join(repository, "displaced-operation-temp.tmp") + let unlinkCalls = 0 + const fileSystem = fileSystemWith({ + async open(filePath, flags, mode) { + const handle = await open(filePath, flags, mode) + if (filePath !== temporary) return handle + return wrapHandle(handle, { + async write(...arguments_) { + await handle.write(...arguments_) + await rename(temporary, displaced) + throw new Error("injected missing retained temp") + }, + }) + }, + async unlink(filePath) { + unlinkCalls += 1 + return unlink(filePath) + }, + }) + + await assert.rejects( + writePrivateEnvelope(target, Buffer.from("replacement\n"), { + fileSystem, + randomUUID: () => identifier, + }), + /no longer present/iu, + ) + await assert.rejects(lstat(temporary), { code: "ENOENT" }) + assert.equal((await lstat(displaced)).isFile(), true) + assert.equal(unlinkCalls, 0) +}) + +test("same-inode same-size mutation immediately after rename is reported as ambiguous publication", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "private.json") + const intended = Buffer.from("AAAA\n") + const changed = Buffer.from("BBBB\n") + let inodeBeforeMutation + let inodeAfterMutation + const fileSystem = fileSystemWith({ + async rename(from, to) { + await rename(from, to) + inodeBeforeMutation = (await lstat(to)).ino + await writeFile(to, changed, { mode: PRIVATE_MODE }) + inodeAfterMutation = (await lstat(to)).ino + }, + }) + + await assert.rejects( + writePrivateEnvelope(target, intended, { fileSystem }), + /ambiguous|publication|durability/iu, + ) + assert.equal(inodeAfterMutation, inodeBeforeMutation) + assert.deepEqual(await readFile(target), changed) +}) + +test("mutate-then-restore after rename is rejected or the restored intended bytes are re-fsynced before the parent", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "private.json") + const intended = Buffer.from("AAAA\n") + const events = [] + const fileSystem = fileSystemWith({ + async open(filePath, flags, mode) { + const handle = await open(filePath, flags, mode) + if (filePath.endsWith(".tmp")) { + return wrapHandle(handle, { + async sync() { + events.push("file-sync") + return handle.sync() + }, + }) + } + if (filePath === repository) { + return wrapHandle(handle, { + async sync() { + events.push("directory-sync") + return handle.sync() + }, + }) + } + return handle + }, + async rename(from, to) { + await rename(from, to) + await writeFile(to, "BBBB\n", { mode: PRIVATE_MODE }) + await writeFile(to, intended, { mode: PRIVATE_MODE }) + events.push("restored") + }, + }) + + let rejected = false + try { + await writePrivateEnvelope(target, intended, { fileSystem }) + } catch (error) { + rejected = true + assert.match(String(error), /ambiguous|publication|durability/iu) + } + assert.deepEqual(await readFile(target), intended) + if (!rejected) { + assert.deepEqual(events, ["file-sync", "restored", "file-sync", "directory-sync"]) + } +}) + +test("writes fsync the file before rename and the parent directory after rename", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "private.json") + const events = [] + const fileSystem = fileSystemWith({ + async open(filePath, flags, mode) { + const handle = await open(filePath, flags, mode) + if (filePath.endsWith(".tmp")) { + return wrapHandle(handle, { + async sync() { + events.push("file-sync") + return handle.sync() + }, + }) + } + if (filePath === repository) { + return wrapHandle(handle, { + async sync() { + events.push("directory-sync") + return handle.sync() + }, + }) + } + return handle + }, + async rename(from, to) { + events.push("rename") + return rename(from, to) + }, + }) + + await writePrivateEnvelope(target, Buffer.from("replacement\n"), { + fileSystem, + }) + + assert.deepEqual(events, ["file-sync", "rename", "file-sync", "directory-sync"]) +}) + +test("a post-rename directory fsync failure reports ambiguous durability without rolling back", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "private.json") + await writeFile(target, "previous\n", { mode: PRIVATE_MODE }) + const fileSystem = fileSystemWith({ + async open(filePath, flags, mode) { + const handle = await open(filePath, flags, mode) + if (filePath !== repository) return handle + return wrapHandle(handle, { + async sync() { + throw new Error("injected directory sync failure") + }, + }) + }, + }) + + await assert.rejects( + writePrivateEnvelope(target, Buffer.from("replacement\n"), { fileSystem }), + /ambiguous|durability/iu, + ) + assert.deepEqual(await readFile(target), Buffer.from("replacement\n")) +}) + +test("dependency accessors are rejected without invocation", async (t) => { + const repository = await temporaryRepository(t) + const target = path.join(repository, "private.json") + await writeFile(target, "evidence\n", { mode: PRIVATE_MODE }) + let invoked = false + const dependencies = {} + Object.defineProperty(dependencies, "fileSystem", { + enumerable: true, + get() { + invoked = true + return fileSystemWith() + }, + }) + + await assert.rejects( + readPrivateEnvelope(target, MAXIMUM_BYTES, dependencies), + /dependencies|unsafe/iu, + ) + assert.equal(invoked, false) +}) + +function fileSystemWith(overrides = {}) { + return { + lstat, + open, + rename, + unlink, + ...overrides, + } +} + +function wrapHandle(handle, overrides) { + return { + chmod: handle.chmod.bind(handle), + close: handle.close.bind(handle), + read: handle.read.bind(handle), + stat: handle.stat.bind(handle), + sync: handle.sync.bind(handle), + write: handle.write.bind(handle), + ...overrides, + } +} + +function statusWithMode(status, mode) { + const result = Object.create(Object.getPrototypeOf(status)) + Object.assign(result, status) + result.mode = (status.mode & ~0o7777n) | BigInt(mode) + return result +} + +async function temporaryRepository(t) { + const temporary = await realpath(await mkdtemp(path.join(os.tmpdir(), "dawn-file-evidence-"))) + t.after(() => rm(temporary, { recursive: true, force: true })) + return temporary +} + +async function entries(directory) { + return readdir(directory) +} + +function effectiveUserId() { + assert.equal(typeof process.geteuid, "function") + return process.geteuid() +} + +async function journalLockPaths(repository) { + const target = path.join( + repository, + ".dawn", + "release", + "duplicate-draft-consolidation.journal.json", + ) + await mkdir(path.dirname(target), { recursive: true, mode: 0o700 }) + return { + target, + lockPath: path.join(path.dirname(target), `.${path.basename(target)}.lock`), + } +} + +function canonicalLockRecord({ + lockPath, + pid = process.pid, + nonce = "12345678-1234-4123-8123-123456789abc", +}) { + return Buffer.from( + `${JSON.stringify({ + schemaVersion: 1, + pid, + processStartIdentity: null, + nonce, + path: lockPath, + createdAt: "2026-09-01T12:00:00.000Z", + })}\n`, + "utf8", + ) +} diff --git a/scripts/release/test/duplicate-draft-consolidation-journal.test.mjs b/scripts/release/test/duplicate-draft-consolidation-journal.test.mjs new file mode 100644 index 000000000..36c493c55 --- /dev/null +++ b/scripts/release/test/duplicate-draft-consolidation-journal.test.mjs @@ -0,0 +1,851 @@ +import assert from "node:assert/strict" +import { createHash } from "node:crypto" +import test from "node:test" + +import { inspectEquivalentDrafts } from "../duplicate-draft-consolidation-evidence.mjs" +import { + appendJournalEvent, + createConsolidationJournal, + createFinalConsolidationReceipt, + deriveConsolidationState, + nextResumeAction, + parseConsolidationJournal, +} from "../duplicate-draft-consolidation-journal.mjs" +import { + canonicalConsolidationEnvelopeBytes, + canonicalEventEnvelope, + canonicalRecordSha256, + createConsolidationEnvelope, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS, +} from "../duplicate-draft-consolidation-schema.mjs" +import { CANONICAL_RELEASE_PACKAGE_ORDER } from "../manifest.mjs" +import { + createDuplicateDraftConsolidationFixture, + DUPLICATE_DRAFT_CANDIDATE, + DUPLICATE_DRAFT_IDS, + DUPLICATE_DRAFT_SURVIVOR_ID, +} from "./support/duplicate-draft-consolidation-fixture.mjs" + +const CONTROLLER_SHA = DUPLICATE_DRAFT_CANDIDATE.commitSha +const REPOSITORY_ID = "1210070282" +const ACTOR = Object.freeze({ login: "blove", id: "61436" }) +const TAG_OBJECT_SHA = "a".repeat(40) +const WORKFLOW_ID = "202458345" +const BASE_TIME = Date.parse("2026-09-01T12:00:00.000Z") +let confirmationSha256 + +let fixture + +test.before(async () => { + fixture = await journalFixture() + confirmationSha256 = createHash("sha256") + .update(exactConfirmation(fixture.proposedEnvelope), "utf8") + .digest("hex") +}) + +test("journal fixture hashes the exact v-prefixed incident confirmation", () => { + const { candidate, roles } = fixture.proposedEnvelope.record + assert.equal( + exactConfirmation(fixture.proposedEnvelope), + `CONSOLIDATE v${candidate.version} ${candidate.commitSha} SURVIVOR ${roles.survivor} DELETE ${roles.duplicates.join(",")} PROPOSAL ${fixture.proposedEnvelope.recordSha256}`, + ) +}) + +test("creates and strictly parses an immutable canonical operation journal", () => { + const journal = newJournal() + const parsed = parseConsolidationJournal(journal) + + assert.notEqual(parsed, journal) + assert.deepEqual(parsed, journal) + assert.equal(parsed.record.events.length, 1) + assert.equal(parsed.record.events[0].event.type, "operation-started") + assert.equal(parsed.record.events[0].event.sequence, 1) + assert.equal(parsed.record.events[0].event.previousEventSha256, null) + assert.equal(Object.isFrozen(parsed), true) + assert.equal(Object.isFrozen(parsed.record.events), true) + assert.equal(deriveConsolidationState(parsed).phase, "operation-started") +}) + +test("rejects event hash mutation, sequence gaps, reordering, and raw truncation", () => { + const authority = preDeleteAuthority(0) + const started = newJournal() + const withAuthority = appendAuthority(started, 0, 1, authority, 1) + const withIntent = appendIntent(withAuthority, 0, 1, 2) + + for (const mutate of [ + (value) => { + value.record.events[1].event.payload.attemptNumber = 2 + }, + (value) => { + value.record.events[1].event.sequence = 7 + }, + (value) => { + ;[value.record.events[1], value.record.events[2]] = [ + value.record.events[2], + value.record.events[1], + ] + }, + (value) => { + value.record.events.pop() + }, + ]) { + const changed = structuredClone(withIntent) + mutate(changed) + assert.throws( + () => parseConsolidationJournal(changed), + /digest|sequence|previous|canonical|bind/iu, + ) + } +}) + +test("replays every event type through the fixed two-target confirmed-204 sequence", () => { + let journal = newJournal() + journal = appendNpm(journal, 0, 1, "perform-initial", 1) + journal = appendAuthority(journal, 0, 1, preDeleteAuthority(0, 61), 61) + journal = appendIntent(journal, 0, 1, 62) + journal = appendOutcome(journal, 0, 1, "confirmed-204", 204, 63) + journal = appendAbsence(journal, 0, 1, "confirmed-204", 64) + journal = appendAuthority(journal, 1, 1, preDeleteAuthority(1, 65), 65) + journal = appendIntent(journal, 1, 1, 66) + journal = appendOutcome(journal, 1, 1, "confirmed-204", 204, 67) + journal = appendAbsence(journal, 1, 1, "confirmed-204", 68) + journal = appendJournalEvent( + journal, + "final-authority-observed", + { authority: finalAuthority() }, + at(69), + ) + + const state = deriveConsolidationState(journal) + assert.deepEqual(state.completedTargets, [...DUPLICATE_DRAFT_IDS]) + assert.equal(state.currentTargetReleaseId, null) + assert.equal(state.phase, "final-authority-observed") + assert.equal(nextResumeAction(state, { classification: "absent" }), "complete") +}) + +test("rejects second-target events before first-target absence convergence", () => { + assert.throws( + () => appendAuthority(newJournal(), 1, 1, preDeleteAuthority(1), 1), + /order|target|preceding|converge/iu, + ) +}) + +test("requires an authority event and its exact digest immediately before intent", () => { + assert.throws( + () => + appendJournalEvent( + newJournal(), + "delete-intent", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + authorityEventSha256: "d".repeat(64), + }, + at(1), + ), + /authority|intent|preced/iu, + ) + const authorityJournal = appendAuthority(newJournal(), 0, 1, preDeleteAuthority(0), 1) + assert.throws( + () => + appendJournalEvent( + authorityJournal, + "delete-intent", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + authorityEventSha256: "d".repeat(64), + }, + at(2), + ), + /digest|authority|intent|bind/iu, + ) +}) + +test("admits exactly one orphan authority recovery globally and rejects a second before append", () => { + let journal = newJournal() + const authority = preDeleteAuthority(0) + journal = appendAuthority(journal, 0, 1, authority, 1) + const firstDigest = journal.record.events.at(-1).eventSha256 + journal = appendAuthority(journal, 0, 1, authority, 2) + const newestDigest = journal.record.events.at(-1).eventSha256 + assert.notEqual(newestDigest, firstDigest) + assert.equal(deriveConsolidationState(journal).phase, "delete-authority-observed") + const driftedOrphan = preDeleteAuthority(0) + driftedOrphan.targetRead.evidence.assets[0].label = "included drift" + driftedOrphan.targetRead.evidenceSha256 = canonicalRecordSha256(driftedOrphan.targetRead.evidence) + driftedOrphan.releases[1] = structuredClone(driftedOrphan.targetRead.evidence) + assert.throws( + () => + appendAuthority(appendAuthority(newJournal(), 0, 1, authority, 1), 0, 1, driftedOrphan, 2), + /proposal|authority|evidence|asset|drift/iu, + ) + + assert.throws(() => appendAuthority(journal, 0, 1, authority, 3), /orphan|authority|bound/iu) + assert.equal(journal.record.events.length, 3) + assert.equal(journal.record.events.at(-1).eventSha256, newestDigest) + journal = appendIntent(journal, 0, 1, 3) + assert.equal(journal.record.events.at(-1).event.payload.authorityEventSha256, newestDigest) + assert.throws( + () => appendAuthority(journal, 0, 1, authority, 4), + /authority|intent|legal|state/iu, + ) +}) + +test("the maximum eight-stage authority history serializes within the 72 MiB journal admission bound", () => { + let journal = newJournal() + let second = 1 + for (let targetIndex = 0; targetIndex < 2; targetIndex += 1) { + for (let attemptNumber = 1; attemptNumber <= 3; attemptNumber += 1) { + const authority = maximumSizedAuthority(preDeleteAuthority(targetIndex)) + journal = appendAuthority(journal, targetIndex, attemptNumber, authority, second++) + if (targetIndex === 0 && attemptNumber === 1) { + journal = appendAuthority( + journal, + targetIndex, + attemptNumber, + maximumSizedAuthority(preDeleteAuthority(targetIndex)), + second++, + ) + } + if (targetIndex === 1 && attemptNumber === 1) { + const beforeRejectedRecovery = journal + assert.throws( + () => + appendAuthority( + journal, + targetIndex, + attemptNumber, + maximumSizedAuthority(preDeleteAuthority(targetIndex)), + second, + ), + /orphan|authority|bound/iu, + ) + assert.equal(journal, beforeRejectedRecovery) + } + journal = appendIntent(journal, targetIndex, attemptNumber, second++) + if (attemptNumber < 3) { + journal = appendOutcome( + journal, + targetIndex, + attemptNumber, + "transport-ambiguous", + null, + second++, + ) + journal = appendReconciliation( + journal, + targetIndex, + attemptNumber, + "present-unchanged-retryable", + targetEvidence(authority), + second++, + ) + } else { + journal = appendOutcome(journal, targetIndex, attemptNumber, "confirmed-204", 204, second++) + journal = appendAbsence(journal, targetIndex, attemptNumber, "confirmed-204", second++) + } + } + } + journal = appendJournalEvent( + journal, + "final-authority-observed", + { authority: maximumSizedAuthority(finalAuthority()) }, + at(second), + ) + const bytes = canonicalConsolidationEnvelopeBytes("journal", journal) + assert.ok(bytes.byteLength <= DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes) + assert.equal( + journal.record.events.filter(({ event }) => + ["delete-authority-observed", "final-authority-observed"].includes(event.type), + ).length, + 8, + ) +}) + +for (const [classification, httpStatus] of [ + ["transport-ambiguous", null], + ["response-404-ambiguous", 404], +]) { + test(`${classification} may converge absent without erasing its ambiguity`, () => { + let journal = appendIntent( + appendAuthority(newJournal(), 0, 1, preDeleteAuthority(0), 1), + 0, + 1, + 2, + ) + journal = appendOutcome(journal, 0, 1, classification, httpStatus, 3) + assert.equal( + nextResumeAction(deriveConsolidationState(journal), { + classification: "absent", + directGet404At: at(4), + listAbsentAt: at(4), + attempts: 2, + }), + "reconcile-absence", + ) + journal = appendAbsence(journal, 0, 1, "ambiguous", 4) + assert.equal(deriveConsolidationState(journal).phase, "target-converged") + }) +} + +test("response-hard-failure is terminal even when the target remains unchanged", () => { + let journal = appendIntent(appendAuthority(newJournal(), 0, 1, preDeleteAuthority(0), 1), 0, 1, 2) + journal = appendOutcome(journal, 0, 1, "response-hard-failure", 500, 3) + assert.equal( + nextResumeAction(deriveConsolidationState(journal), { + classification: "present-unchanged", + releaseEvidence: targetEvidence(preDeleteAuthority(0)), + observations: 6, + }), + "stop", + ) +}) + +test("an intent with no outcome and unchanged target requires reconciliation then a fresh attempt", () => { + const authority = preDeleteAuthority(0) + let journal = appendIntent(appendAuthority(newJournal(), 0, 1, authority, 1), 0, 1, 2) + let state = deriveConsolidationState(journal) + assert.equal( + nextResumeAction(state, { + classification: "present-unchanged", + releaseEvidence: targetEvidence(authority), + observations: 1, + }), + "refresh-and-retry", + ) + const freshEvidence = volatileEvidence(targetEvidence(authority), "retry") + journal = appendReconciliation(journal, 0, 1, "present-unchanged-retryable", freshEvidence, 3) + journal = appendAuthority(journal, 0, 2, preDeleteAuthority(0), 4) + journal = appendIntent(journal, 0, 2, 5) + state = deriveConsolidationState(journal) + assert.equal(state.attemptNumber, 2) + assert.equal(state.phase, "delete-intent") +}) + +test("retry evidence accepts service volatility but the next authority rejects included asset drift", () => { + const authority = preDeleteAuthority(0) + let journal = appendIntent(appendAuthority(newJournal(), 0, 1, authority, 1), 0, 1, 2) + journal = appendReconciliation( + journal, + 0, + 1, + "present-unchanged-retryable", + volatileEvidence(targetEvidence(authority), "observed"), + 3, + ) + const drifted = preDeleteAuthority(0) + drifted.targetRead.evidence.assets[0].label = "changed included label" + drifted.targetRead.evidenceSha256 = canonicalRecordSha256(drifted.targetRead.evidence) + drifted.releases[1] = structuredClone(drifted.targetRead.evidence) + assert.throws(() => appendAuthority(journal, 0, 2, drifted, 4), /asset|evidence|proposal|equal/iu) +}) + +test("a retry perform-initial observation advances and binds the next attempt", () => { + const authority = preDeleteAuthority(0) + let journal = appendAuthority(newJournal(), 0, 1, authority, 1) + journal = appendIntent(journal, 0, 1, 2) + journal = appendReconciliation( + journal, + 0, + 1, + "present-unchanged-retryable", + targetEvidence(authority), + 3, + ) + journal = appendNpm(journal, 0, 2, "perform-initial", 4) + assert.equal(deriveConsolidationState(journal).attemptNumber, 2) + assert.throws( + () => appendAuthority(journal, 0, 2, preDeleteAuthority(0, 63), 63), + /sixty-second|observation gap/iu, + ) + journal = appendAuthority(journal, 0, 2, preDeleteAuthority(0, 64), 64) + journal = appendIntent(journal, 0, 2, 65) + assert.equal(deriveConsolidationState(journal).attemptNumber, 2) + const pendingRetry = appendNpm(journal, 0, 3, "perform-initial", 66) + const pendingState = deriveConsolidationState(pendingRetry) + assert.equal(pendingState.phase, "npm-observed") + assert.equal(pendingState.attemptNumber, 2) + assert.equal(pendingState.pendingRetryFromAttempt, 2) + assert.throws( + () => appendAuthority(pendingRetry, 0, 3, preDeleteAuthority(0, 126), 126), + /reconciliation|authority|state/iu, + ) +}) + +test("recorded ambiguity requires six unchanged reads before retry, or reconciles absence", () => { + const authority = preDeleteAuthority(0) + let journal = appendOutcome( + appendIntent(appendAuthority(newJournal(), 0, 1, authority, 1), 0, 1, 2), + 0, + 1, + "transport-ambiguous", + null, + 3, + ) + const state = deriveConsolidationState(journal) + assert.equal( + nextResumeAction(state, { + classification: "present-unchanged", + releaseEvidence: targetEvidence(authority), + observations: 5, + }), + "stop", + ) + assert.equal( + nextResumeAction(state, { + classification: "present-unchanged", + releaseEvidence: targetEvidence(authority), + observations: 6, + }), + "refresh-and-retry", + ) + journal = appendReconciliation(journal, 0, 1, "absent-ambiguous", null, 4) + journal = appendAbsence(journal, 0, 1, "ambiguous", 5) + assert.equal(deriveConsolidationState(journal).phase, "target-converged") +}) + +test("changed, published, and malformed targets always stop", () => { + const authority = preDeleteAuthority(0) + const journal = appendIntent(appendAuthority(newJournal(), 0, 1, authority, 1), 0, 1, 2) + const state = deriveConsolidationState(journal) + for (const classification of ["changed", "published", "malformed"]) { + assert.equal(nextResumeAction(state, { classification }), "stop") + } +}) + +test("a target present after confirmed 204 stops instead of retrying", () => { + const authority = preDeleteAuthority(0) + const journal = appendOutcome( + appendIntent(appendAuthority(newJournal(), 0, 1, authority, 1), 0, 1, 2), + 0, + 1, + "confirmed-204", + 204, + 3, + ) + assert.equal( + nextResumeAction(deriveConsolidationState(journal), { + classification: "present-unchanged", + releaseEvidence: targetEvidence(authority), + observations: 6, + }), + "stop", + ) +}) + +test("caps one target at three intents", () => { + let journal = newJournal() + for (let attempt = 1; attempt <= 3; attempt += 1) { + const authority = preDeleteAuthority(0) + journal = appendAuthority(journal, 0, attempt, authority, attempt * 4 - 3) + journal = appendIntent(journal, 0, attempt, attempt * 4 - 2) + journal = appendOutcome(journal, 0, attempt, "transport-ambiguous", null, attempt * 4 - 1) + journal = appendReconciliation( + journal, + 0, + attempt, + "present-unchanged-retryable", + targetEvidence(authority), + attempt * 4, + ) + } + assert.equal( + nextResumeAction(deriveConsolidationState(journal), { + classification: "present-unchanged", + releaseEvidence: targetEvidence(preDeleteAuthority(0)), + observations: 6, + }), + "stop", + ) + assert.throws( + () => appendAuthority(journal, 0, 4, preDeleteAuthority(0), 13), + /attempt|maximum|three|exhaust/iu, + ) +}) + +test("rejects main drift from the operation-started controller SHA", () => { + const authority = structuredClone(preDeleteAuthority(0)) + const drifted = "f".repeat(40) + authority.controller = { + headSha: drifted, + originMainSha: drifted, + githubMainSha: drifted, + } + assert.throws( + () => appendAuthority(newJournal(), 0, 1, authority, 1), + /controller|main|drift|operation/iu, + ) +}) + +test("allows final authority only after both targets converge absent", () => { + assert.throws( + () => + appendJournalEvent( + newJournal(), + "final-authority-observed", + { authority: finalAuthority() }, + at(1), + ), + /both|target|converge|final/iu, + ) +}) + +test("creates a final receipt only from a completed two-target journal", () => { + let journal = newJournal() + for (let index = 0; index < 2; index += 1) { + journal = appendAuthority(journal, index, 1, preDeleteAuthority(index), index * 4 + 1) + journal = appendIntent(journal, index, 1, index * 4 + 2) + journal = appendOutcome(journal, index, 1, "confirmed-204", 204, index * 4 + 3) + journal = appendAbsence(journal, index, 1, "confirmed-204", index * 4 + 4) + } + const final = finalAuthority() + journal = appendJournalEvent(journal, "final-authority-observed", { authority: final }, at(9)) + const receipt = createFinalConsolidationReceipt({ + proposedEnvelope: fixture.proposedEnvelope, + journalEnvelope: journal, + finalAuthority: final, + completedAt: at(10), + }) + assert.equal(receipt.record.journalEnvelope.recordSha256, journal.recordSha256) + assert.deepEqual(receipt.record.finalSurvivor, final.releases[0]) + for (const mutate of [ + (authority) => { + authority.annotatedTag.objectSha = "b".repeat(40) + }, + (authority) => { + authority.workflowAuthority.state = "active" + }, + (authority) => { + authority.releases[0].semantic.name = "changed survivor" + }, + (authority) => { + authority.payloadProof.consolidationPayloadSha256 = "f".repeat(64) + }, + ]) { + const changed = structuredClone(final) + mutate(changed) + assert.throws(() => { + const changedJournal = replaceFinalAuthority(journal, changed) + createFinalConsolidationReceipt({ + proposedEnvelope: fixture.proposedEnvelope, + journalEnvelope: changedJournal, + finalAuthority: changed, + completedAt: at(10), + }) + }, /tag|workflow|survivor|payload|proposal|authority|state/iu) + } + + const incomplete = appendAuthority(newJournal(), 0, 1, preDeleteAuthority(0), 1) + assert.throws( + () => + createFinalConsolidationReceipt({ + proposedEnvelope: fixture.proposedEnvelope, + journalEnvelope: incomplete, + finalAuthority: final, + completedAt: at(10), + }), + /both|complete|final|converge/iu, + ) +}) + +function newJournal() { + return createConsolidationJournal({ + proposedEnvelope: fixture.proposedEnvelope, + confirmationSha256, + recordedAt: at(0), + }) +} + +function maximumSizedAuthority(value) { + const authority = structuredClone(value) + const currentBytes = Buffer.byteLength(`${JSON.stringify(authority)}\n`, "utf8") + const currentNameBytes = Buffer.byteLength(JSON.stringify(authority.annotatedTag.name), "utf8") + const replacementBytes = + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes - currentBytes + currentNameBytes + authority.annotatedTag.name = "x".repeat(replacementBytes - 2) + assert.equal( + Buffer.byteLength(`${JSON.stringify(authority)}\n`, "utf8"), + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes, + ) + return authority +} + +function exactConfirmation(proposedEnvelope) { + const { candidate, roles } = proposedEnvelope.record + return `CONSOLIDATE v${candidate.version} ${candidate.commitSha} SURVIVOR ${roles.survivor} DELETE ${roles.duplicates.join(",")} PROPOSAL ${proposedEnvelope.recordSha256}` +} + +function replaceFinalAuthority(journal, authority) { + const changed = structuredClone(journal) + changed.record.events.at(-1).event.payload.authority = authority + changed.record.events = rebuildEventChain(changed.record.events) + changed.record.updatedAt = changed.record.events.at(-1).event.recordedAt + return createConsolidationEnvelope("journal", changed.record) +} + +function rebuildEventChain(events) { + let previousEventSha256 = null + return events.map(({ event }, index) => { + const envelope = canonicalEventEnvelope( + { + ...event, + sequence: index + 1, + previousEventSha256, + }, + previousEventSha256, + ) + previousEventSha256 = envelope.eventSha256 + return envelope + }) +} + +function appendNpm(journal, targetIndex, attemptNumber, stage, second) { + return appendJournalEvent( + journal, + "npm-observed", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[targetIndex], + attemptNumber, + inventory: npmInventory(stage, second), + }, + at(second), + ) +} + +function appendAuthority(journal, targetIndex, attemptNumber, authority, second) { + return appendJournalEvent( + journal, + "delete-authority-observed", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[targetIndex], + attemptNumber, + authority, + }, + at(second), + ) +} + +function appendIntent(journal, targetIndex, attemptNumber, second) { + const authorityEvent = journal.record.events.at(-1) + return appendJournalEvent( + journal, + "delete-intent", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[targetIndex], + attemptNumber, + authorityEventSha256: authorityEvent.eventSha256, + }, + at(second), + ) +} + +function appendOutcome(journal, targetIndex, attemptNumber, classification, httpStatus, second) { + return appendJournalEvent( + journal, + "delete-outcome", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[targetIndex], + attemptNumber, + classification, + httpStatus, + observedAt: at(second), + }, + at(second), + ) +} + +function appendReconciliation( + journal, + targetIndex, + attemptNumber, + classification, + releaseEvidence, + second, +) { + return appendJournalEvent( + journal, + "resume-reconciliation", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[targetIndex], + attemptNumber, + classification, + releaseEvidence, + observedAt: at(second), + }, + at(second), + ) +} + +function appendAbsence(journal, targetIndex, attemptNumber, basis, second) { + return appendJournalEvent( + journal, + "absence-converged", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[targetIndex], + attemptNumber, + basis, + directGet404At: at(second), + listAbsentAt: at(second), + attempts: 1, + completedAt: at(second), + }, + at(second), + ) +} + +function preDeleteAuthority(targetIndex, second = 0) { + const stage = targetIndex === 0 ? "pre-delete-1" : "pre-delete-2" + const releases = + targetIndex === 0 + ? fixture.proposedEnvelope.record.releases + : [fixture.proposedEnvelope.record.releases[0], fixture.proposedEnvelope.record.releases[2]] + const target = releases.find(({ id }) => id === DUPLICATE_DRAFT_IDS[targetIndex]) + return { + stage, + controller: { ...fixture.proposedEnvelope.record.controller }, + annotatedTag: { + ...fixture.proposedEnvelope.record.annotatedTag, + observedAt: at(second), + }, + workflowAuthority: { + ...fixture.proposedEnvelope.record.workflowAuthority, + observedAt: at(second), + }, + npmInventory: npmInventory(stage, second), + releases: structuredClone(releases), + payloadProof: structuredClone(fixture.proposedEnvelope.record.payloadProof), + targetRead: { + releaseGetStartedAt: at(second), + releaseGetCompletedAt: at(second), + assetsListStartedAt: at(second), + assetsListCompletedAt: at(second), + evidence: structuredClone(target), + evidenceSha256: canonicalRecordSha256(target), + }, + observedAt: at(second), + } +} + +function finalAuthority() { + return { + stage: "final", + controller: { ...fixture.proposedEnvelope.record.controller }, + annotatedTag: { + ...fixture.proposedEnvelope.record.annotatedTag, + observedAt: at(0), + }, + workflowAuthority: { + ...fixture.proposedEnvelope.record.workflowAuthority, + observedAt: at(0), + }, + npmInventory: npmInventory("final", 0), + releases: [structuredClone(fixture.proposedEnvelope.record.releases[0])], + payloadProof: structuredClone(fixture.proposedEnvelope.record.payloadProof), + targetRead: null, + observedAt: at(0), + } +} + +function targetEvidence(authority) { + return structuredClone(authority.targetRead.evidence) +} + +function volatileEvidence(value, suffix) { + const evidence = structuredClone(value) + evidence.nodeId = `RE_${suffix}` + evidence.createdAt = at(1) + evidence.updatedAt = at(2) + evidence.assets[0].id = `99000${suffix.length}` + evidence.assets[0].nodeId = `RA_${suffix}` + evidence.assets[0].createdAt = at(1) + evidence.assets[0].updatedAt = at(2) + evidence.assets[0].downloadCount += 1 + return evidence +} + +async function journalFixture() { + const source = createDuplicateDraftConsolidationFixture() + const inspected = await inspectEquivalentDrafts({ + candidate: source.candidate, + survivorId: source.survivorId, + duplicateIds: source.duplicateIds, + releases: source.releases, + github: source.github, + attestations: source.attestations, + }) + const repository = { + name: "cacheplane/dawnai", + id: REPOSITORY_ID, + defaultBranch: "main", + actor: { ...ACTOR }, + } + const controller = { + headSha: CONTROLLER_SHA, + originMainSha: CONTROLLER_SHA, + githubMainSha: CONTROLLER_SHA, + } + const annotatedTag = { + name: DUPLICATE_DRAFT_CANDIDATE.tag, + objectSha: TAG_OBJECT_SHA, + targetSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + objectType: "tag", + observedAt: at(0), + } + const workflowAuthority = { + workflowId: WORKFLOW_ID, + path: ".github/workflows/release.yml", + state: "disabled_manually", + query: { + statuses: ["in_progress", "pending", "queued", "requested", "waiting"], + perPage: 100, + maximumPages: 100, + }, + nonterminalRuns: [], + observedAt: at(0), + } + const proposedEnvelope = createConsolidationEnvelope("proposed", { + schemaVersion: 1, + repository, + controller, + candidate: DUPLICATE_DRAFT_CANDIDATE, + roles: { + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + duplicates: [...DUPLICATE_DRAFT_IDS], + }, + confirmation: { + version: DUPLICATE_DRAFT_CANDIDATE.version, + commitSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + duplicates: [...DUPLICATE_DRAFT_IDS], + template: "CONSOLIDATE <64-lowercase-hex-digest>", + }, + annotatedTag, + workflowAuthority, + npmInventories: [npmInventory("inspect-initial", 0), npmInventory("inspect-ready", 0)], + releases: inspected.releases, + payloadProof: inspected.payloadProof, + inspectedAt: at(0), + }) + return { proposedEnvelope } +} + +function npmInventory(stage, second) { + return { + stage, + startedAt: at(second), + completedAt: at(second), + packages: CANONICAL_RELEASE_PACKAGE_ORDER.map((name) => ({ + name, + version: DUPLICATE_DRAFT_CANDIDATE.version, + status: "ABSENT", + httpStatus: 404, + code: "E404", + observedAt: at(second), + })), + } +} + +function at(second) { + return new Date(BASE_TIME + second * 1000).toISOString() +} diff --git a/scripts/release/test/duplicate-draft-consolidation-rehearsal.test.mjs b/scripts/release/test/duplicate-draft-consolidation-rehearsal.test.mjs new file mode 100644 index 000000000..a74dcc08c --- /dev/null +++ b/scripts/release/test/duplicate-draft-consolidation-rehearsal.test.mjs @@ -0,0 +1,829 @@ +import assert from "node:assert/strict" +import { spawn } from "node:child_process" +import { createHash } from "node:crypto" +import { mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import test from "node:test" +import { fileURLToPath } from "node:url" +import { + performDuplicateDraftConsolidation, + performOneDuplicateDeletion, +} from "../duplicate-draft-consolidation.mjs" +import { createDuplicateDraftConsolidationAdapters } from "../duplicate-draft-consolidation-adapters.mjs" +import { runDuplicateDraftConsolidationCli } from "../duplicate-draft-consolidation-cli.mjs" +import { assertEvidenceEqualsProposal } from "../duplicate-draft-consolidation-evidence.mjs" +import { readPrivateEnvelope, readTrackedReceipt } from "../duplicate-draft-consolidation-files.mjs" +import { + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS, + parseConsolidationEnvelope, +} from "../duplicate-draft-consolidation-schema.mjs" +import { + createDuplicateDraftConsolidationFixture, + DUPLICATE_DRAFT_CANDIDATE, + DUPLICATE_DRAFT_IDS, + DUPLICATE_DRAFT_SURVIVOR_ID, +} from "./support/duplicate-draft-consolidation-fixture.mjs" + +const CONTROLLER_SHA = "b".repeat(40) +const PROPOSAL = ".dawn/release/duplicate-draft-consolidation.proposed.json" +const JOURNAL = ".dawn/release/duplicate-draft-consolidation.journal.json" +const RECEIPT = "scripts/release/duplicate-draft-consolidation.json" +const INSPECT_COMMAND = Object.freeze([ + "inspect", + "--version", + DUPLICATE_DRAFT_CANDIDATE.version, + "--commit-sha", + DUPLICATE_DRAFT_CANDIDATE.commitSha, + "--survivor", + DUPLICATE_DRAFT_SURVIVOR_ID, + "--duplicates", + DUPLICATE_DRAFT_IDS.join(","), + "--output", + PROPOSAL, +]) +const VERIFY_COMMAND = Object.freeze(["verify", "--receipt", RECEIPT]) +const PROCESS_LOSS_CHILD = fileURLToPath( + new URL("./support/duplicate-draft-consolidation-process-loss-child.mjs", import.meta.url), +) +const PROCESS_LOSS_CASES = Object.freeze([ + Object.freeze({ name: "clean completion", fault: null, expectedIntents: 2 }), + Object.freeze({ + name: "before first intent", + target: DUPLICATE_DRAFT_IDS[0], + boundary: "after-authority-head", + expectedIntents: 2, + }), + Object.freeze({ + name: "after first intent before DELETE", + target: DUPLICATE_DRAFT_IDS[0], + boundary: "before-delete", + expectedIntents: 3, + }), + Object.freeze({ + name: "after server deletion before response", + target: DUPLICATE_DRAFT_IDS[0], + boundary: "after-delete", + loseDeleteResponse: true, + expectedIntents: 2, + }), + Object.freeze({ + name: "after first convergence", + target: DUPLICATE_DRAFT_IDS[0], + afterConvergence: true, + expectedIntents: 2, + }), + Object.freeze({ + name: "after second intent", + target: DUPLICATE_DRAFT_IDS[1], + boundary: "before-delete", + expectedIntents: 3, + }), + Object.freeze({ + name: "after second deletion before receipt", + target: DUPLICATE_DRAFT_IDS[1], + boundary: "after-delete", + expectedIntents: 2, + }), + Object.freeze({ + name: "after receipt write before CLI success output", + failSuccessOutput: true, + expectedIntents: 2, + }), +]) + +test("full inspect, perform, and verify rehearsal survives every approved process-loss point", async (t) => { + for (const scenario of PROCESS_LOSS_CASES) { + await t.test(scenario.name, async (t) => { + const harness = await createRehearsal(t) + assertThreeDistinctEquivalentDrafts(harness) + + const inspectStdout = memorySink() + const inspectStderr = memorySink() + assert.equal( + await runDuplicateDraftConsolidationCli( + harness.cli(INSPECT_COMMAND, inspectStdout, inspectStderr), + ), + 0, + inspectStderr.value, + ) + assert.equal(inspectStderr.value, "") + const inspectReport = JSON.parse(inspectStdout.value) + const proposalPath = path.join(harness.root, PROPOSAL) + const originalProposalBytes = await readPrivateEnvelope( + proposalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes, + ) + const proposal = parseConsolidationEnvelope("proposed", originalProposalBytes) + assert.deepEqual(inspectReport, { + proposalSha256: proposal.recordSha256, + version: DUPLICATE_DRAFT_CANDIDATE.version, + commitSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + duplicates: [...DUPLICATE_DRAFT_IDS], + output: PROPOSAL, + }) + + const confirmation = exactConfirmation(proposal) + + const performCommand = [ + "perform", + "--proposal", + PROPOSAL, + "--journal", + JOURNAL, + "--receipt", + RECEIPT, + "--confirmation", + confirmation, + ] + const firstStdout = scenario.failSuccessOutput ? throwingSink() : memorySink() + const firstStderr = memorySink() + if (scenario.loseDeleteResponse === true) { + harness.loseNextDeleteResponse(scenario.target) + } + const firstCode = await runDuplicateDraftConsolidationCli( + harness.cli( + performCommand, + firstStdout, + firstStderr, + performFaultInjection(scenario, harness), + ), + ) + + let performReport + if (scenario.fault === null) { + if (firstCode !== 0) { + const journal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + path.join(harness.root, JOURNAL), + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.fail( + `${firstStderr.value} journal events: ${journal.record.events + .map(({ event }) => event.type) + .join(",")}`, + ) + } + assert.equal(firstCode, 0, firstStderr.value) + assert.equal(firstStderr.value, "") + performReport = JSON.parse(firstStdout.value) + } else { + assert.equal(firstCode, 1) + assert.equal(firstStderr.value, "Duplicate-draft perform failed.\n") + const resumedStdout = memorySink() + const resumedStderr = memorySink() + const resumedCode = await runDuplicateDraftConsolidationCli( + harness.cli( + performCommand, + resumedStdout, + resumedStderr, + performFaultInjection({ fault: null }, harness), + ), + ) + if (resumedCode !== 0) { + const journal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + path.join(harness.root, JOURNAL), + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.fail( + `${resumedStderr.value} resumed journal events: ${journal.record.events + .map( + ({ event }) => + `${event.type}:${event.payload.targetReleaseId ?? "final"}:${event.payload.attemptNumber ?? "-"}`, + ) + .join(",")}`, + ) + } + assert.equal(resumedCode, 0, resumedStderr.value) + assert.equal(resumedStderr.value, "") + performReport = JSON.parse(resumedStdout.value) + } + + assert.deepEqual(harness.deleteEffects, [...DUPLICATE_DRAFT_IDS]) + assert.deepEqual(harness.remainingReleaseIds(), [DUPLICATE_DRAFT_SURVIVOR_ID]) + assert.deepEqual( + await readPrivateEnvelope(proposalPath, DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes), + originalProposalBytes, + ) + + const receiptPath = path.join(harness.root, RECEIPT) + const receipt = parseConsolidationEnvelope( + "final", + await readTrackedReceipt( + receiptPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes, + ), + ) + assert.deepEqual(performReport, { + status: "complete", + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + deleted: [...DUPLICATE_DRAFT_IDS], + receipt: RECEIPT, + receiptSha256: receipt.recordSha256, + }) + assert.equal((await stat(receiptPath)).mode & 0o777, 0o644) + assert.equal(receipt.record.proposedEnvelope.recordSha256, proposal.recordSha256) + assertEvidenceEqualsProposal(receipt.record.finalSurvivor, proposal.record.releases[0]) + await assertJournalRecovery(harness.root, receipt, scenario.expectedIntents) + assertStableAuthority(receipt, proposal) + + const verifyStdout = memorySink() + const verifyStderr = memorySink() + const deletesBeforeVerify = [...harness.deleteEffects] + assert.equal( + await runDuplicateDraftConsolidationCli( + harness.cli(VERIFY_COMMAND, verifyStdout, verifyStderr), + ), + 0, + verifyStderr.value, + ) + assert.equal(verifyStderr.value, "") + assert.deepEqual(harness.deleteEffects, deletesBeforeVerify) + assert.deepEqual(JSON.parse(verifyStdout.value), { + status: "verified", + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + deleted: [...DUPLICATE_DRAFT_IDS], + receipt: RECEIPT, + receiptSha256: receipt.recordSha256, + historicalParity: + "Historical duplicate payload parity is supported by embedded pre-delete evidence plus the currently reverified survivor; deleted bytes were not independently re-downloaded.", + }) + }) + } +}) + +test("a fresh process recovers the durable lock and intent after an actual SIGKILL", async (t) => { + if (process.platform === "win32") { + t.skip("SIGKILL is not supported on Windows") + return + } + const root = await realpath(await mkdtemp(path.join(os.tmpdir(), "dawn-consolidation-kill-"))) + t.after(() => rm(root, { recursive: true, force: true })) + const statePath = path.join(root, "fake-service.json") + const readyPath = path.join(root, "delete-entered") + + assert.equal((await runFreshChild("init", root, statePath, readyPath)).code, 0) + const inspect = await runFreshChild("inspect", root, statePath, readyPath) + assert.equal(inspect.code, 0, inspect.stderr) + const proposal = parseConsolidationEnvelope( + "proposed", + await readPrivateEnvelope( + path.join(root, PROPOSAL), + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes, + ), + ) + + const state = JSON.parse(await readFile(statePath, "utf8")) + state.armBeforeDelete = true + await writeFile(statePath, `${JSON.stringify(state)}\n`, { mode: 0o600 }) + const killed = startFreshChild("perform", root, statePath, readyPath) + try { + await Promise.race([ + waitForPath(readyPath, 10_000), + killed.result.then((result) => { + throw new Error(`Child exited before the durable boundary: ${result.stderr}`) + }), + ]) + killed.kill() + const killedResult = await killed.result + assert.equal(killedResult.code, null) + assert.equal(killedResult.signal, "SIGKILL") + } finally { + await killed.cleanup() + } + + const journalBeforeResume = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + path.join(root, JOURNAL), + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal( + journalBeforeResume.record.events.at(-1).event.type, + "delete-intent", + "the killed process must have durably recorded intent while holding the lock", + ) + const lockName = ".duplicate-draft-consolidation.journal.json.lock" + assert.ok((await readdir(path.join(root, ".dawn", "release"))).includes(lockName)) + + const resumable = JSON.parse(await readFile(statePath, "utf8")) + resumable.armBeforeDelete = false + await writeFile(statePath, `${JSON.stringify(resumable)}\n`, { mode: 0o600 }) + const resumed = await runFreshChild("resume", root, statePath, readyPath) + const resumeJournal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + path.join(root, JOURNAL), + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal( + resumed.code, + 0, + `${resumed.stderr}\nfiles=${(await readdir(path.join(root, ".dawn", "release"))).join(",")} events=${resumeJournal.record.events.map(({ event }) => `${event.type}:${event.payload.targetReleaseId ?? "-"}`).join(",")}`, + ) + const service = JSON.parse(await readFile(statePath, "utf8")) + assert.deepEqual(service.deleteEffects, [...DUPLICATE_DRAFT_IDS]) + assert.deepEqual(service.deleted, [...DUPLICATE_DRAFT_IDS]) + + const receipt = parseConsolidationEnvelope( + "final", + await readTrackedReceipt( + path.join(root, RECEIPT), + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes, + ), + ) + assertEvidenceEqualsProposal(receipt.record.finalSurvivor, proposal.record.releases[0]) + assert.equal(receipt.record.proposedEnvelope.recordSha256, proposal.recordSha256) + await assertJournalRecovery(root, receipt, 3) + assertStableAuthority(receipt, proposal) + const releaseDirectory = await readdir(path.join(root, ".dawn", "release")) + assert.equal(releaseDirectory.includes(lockName), false) + assert.ok( + releaseDirectory.some( + (name) => name.startsWith(`${lockName}.`) && name.endsWith(".quarantine"), + ), + ) + + const verified = await runFreshChild("verify", root, statePath, readyPath) + assert.equal(verified.code, 0, verified.stderr) + assert.deepEqual(JSON.parse(verified.stdout), { + status: "verified", + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + deleted: [...DUPLICATE_DRAFT_IDS], + receipt: RECEIPT, + receiptSha256: receipt.recordSha256, + historicalParity: + "Historical duplicate payload parity is supported by embedded pre-delete evidence plus the currently reverified survivor; deleted bytes were not independently re-downloaded.", + }) + assert.deepEqual(JSON.parse(await readFile(statePath, "utf8")).deleteEffects, [ + ...DUPLICATE_DRAFT_IDS, + ]) +}) + +test("fresh child cleanup reaps a process when sentinel polling fails", async (t) => { + if (process.platform === "win32") { + t.skip("SIGKILL is not supported on Windows") + return + } + const root = await realpath(await mkdtemp(path.join(os.tmpdir(), "dawn-child-cleanup-"))) + t.after(() => rm(root, { recursive: true, force: true })) + const statePath = path.join(root, "state.json") + const readyPath = path.join(root, "never-ready") + assert.equal((await runFreshChild("init", root, statePath, readyPath)).code, 0) + const running = startFreshChild("hang", root, statePath, readyPath) + const pid = running.child.pid + try { + await assert.rejects( + Promise.race([ + waitForPath(readyPath, 100), + running.result.then(() => { + throw new Error("child exited early") + }), + ]), + /Timed out waiting/u, + ) + } finally { + await running.cleanup() + } + assert.throws(() => process.kill(pid, 0), { code: "ESRCH" }) +}) + +test("fresh child output is bounded and overflow kills the process", async (t) => { + if (process.platform === "win32") { + t.skip("SIGKILL is not supported on Windows") + return + } + const root = await realpath(await mkdtemp(path.join(os.tmpdir(), "dawn-child-output-"))) + t.after(() => rm(root, { recursive: true, force: true })) + const running = startFreshChild("flood", root, path.join(root, "state"), path.join(root, "ready")) + try { + const result = await running.result + assert.equal(result.signal, "SIGKILL") + assert.match(result.stdout, /\[output truncated\]/u) + assert.ok(Buffer.byteLength(result.stdout) < 66 * 1024) + } finally { + await running.cleanup() + } +}) + +function startFreshChild(mode, root, statePath, readyPath) { + const child = spawn(process.execPath, [PROCESS_LOSS_CHILD, mode, root, statePath, readyPath], { + cwd: root, + stdio: ["ignore", "pipe", "pipe"], + }) + let stdout = "" + let stderr = "" + let settled = false + const append = (target, chunk) => { + const next = target + chunk + if (Buffer.byteLength(next) > 64 * 1024) { + child.kill("SIGKILL") + return `${next.slice(0, 64 * 1024)}\n[output truncated]` + } + return next + } + child.stdout.setEncoding("utf8").on("data", (chunk) => { + stdout = append(stdout, chunk) + }) + child.stderr.setEncoding("utf8").on("data", (chunk) => { + stderr = append(stderr, chunk) + }) + const result = new Promise((resolve, reject) => { + child.once("error", reject) + child.once("close", (code, signal) => { + settled = true + resolve({ code, signal, stderr, stdout }) + }) + }) + return { + child, + result, + kill() { + if (!settled) child.kill("SIGKILL") + }, + async cleanup() { + if (!settled) child.kill("SIGKILL") + let cleanupTimer + try { + await Promise.race([ + result, + new Promise((_, reject) => { + cleanupTimer = setTimeout(() => reject(new Error("Child cleanup timed out")), 2_000) + }), + ]) + } finally { + clearTimeout(cleanupTimer) + } + child.stdout.destroy() + child.stderr.destroy() + child.removeAllListeners() + }, + } +} + +async function runFreshChild(mode, root, statePath, readyPath) { + const running = startFreshChild(mode, root, statePath, readyPath) + const timeout = setTimeout(() => running.child.kill("SIGKILL"), 20_000) + try { + return await running.result + } finally { + clearTimeout(timeout) + await running.cleanup() + } +} + +async function waitForPath(target, timeoutMs) { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + try { + await stat(target) + return + } catch (error) { + if (error?.code !== "ENOENT") throw error + } + await new Promise((resolve) => setTimeout(resolve, 20)) + } + throw new Error("Timed out waiting for the child process to reach the durable boundary") +} + +async function createRehearsal(t) { + const root = await realpath( + await mkdtemp(path.join(os.tmpdir(), "dawn-consolidation-rehearsal-")), + ) + t.after(() => rm(root, { recursive: true, force: true })) + await mkdir(path.join(root, ".dawn", "release"), { recursive: true }) + await mkdir(path.join(root, "scripts", "release"), { recursive: true }) + + const fixture = createDuplicateDraftConsolidationFixture() + const deleted = new Set() + const deleteEffects = [] + let droppedResponseTarget = null + let nowMs = Date.now() + 60 * 60_000 + const now = () => new Date(nowMs++).toISOString() + const present = (operation, value) => ({ + status: "PRESENT", + operation, + httpStatus: 200, + code: null, + value, + }) + const currentReleases = () => + fixture.releases + .filter(({ id }) => !deleted.has(String(id))) + .map((release) => structuredClone(release)) + + const githubReader = { + async getRef({ ref }) { + if (ref === "heads/main") { + return present("ref", { + ref: "refs/heads/main", + object: { type: "commit", sha: CONTROLLER_SHA }, + }) + } + assert.equal(ref, `tags/${DUPLICATE_DRAFT_CANDIDATE.tag}`) + return present("ref", { + ref: `refs/tags/${DUPLICATE_DRAFT_CANDIDATE.tag}`, + object: { type: "tag", sha: "a".repeat(40) }, + }) + }, + async getGitTag({ tagSha }) { + assert.equal(tagSha, "a".repeat(40)) + return present("git-tag", { + sha: tagSha, + tag: DUPLICATE_DRAFT_CANDIDATE.tag, + object: { type: "commit", sha: DUPLICATE_DRAFT_CANDIDATE.commitSha }, + }) + }, + async getWorkflow({ workflow }) { + assert.equal(workflow, "release.yml") + return present("workflow", { + id: 202_458_345, + path: ".github/workflows/release.yml", + state: "disabled_manually", + }) + }, + async listReleases() { + return present("releases", currentReleases()) + }, + async getRelease({ releaseId }) { + const release = currentReleases().find(({ id }) => String(id) === String(releaseId)) + return release === undefined + ? { + status: "AMBIGUOUS", + operation: "release", + httpStatus: 404, + code: "NOT_FOUND", + } + : present("release", release) + }, + async listReleaseAssets({ releaseId }) { + const release = currentReleases().find(({ id }) => String(id) === String(releaseId)) + if (release === undefined) throw new Error("deleted fixture Release has no assets") + return present("release-assets", release.assets) + }, + async downloadReleaseAsset(input) { + return fixture.github.downloadReleaseAsset(input) + }, + } + + const fetchImpl = async (url, init = {}) => { + const target = String(url) + if (init.method === "DELETE") { + const releaseId = target.split("/").at(-1) + assert.equal(DUPLICATE_DRAFT_IDS.includes(releaseId), true) + assert.equal(deleted.has(releaseId), false, "a bounded resume must not repeat DELETE") + deleted.add(releaseId) + deleteEffects.push(releaseId) + if (droppedResponseTarget === releaseId) { + droppedResponseTarget = null + throw new Error("fixture process lost the response after the server deleted the Release") + } + return new Response(null, { status: 204 }) + } + if (target === "https://api.github.com/repos/cacheplane/dawnai") { + return jsonResponse({ + id: 1_210_070_282, + full_name: "cacheplane/dawnai", + default_branch: "main", + }) + } + if (target === "https://api.github.com/user") { + return jsonResponse({ id: 61_436, login: "blove" }) + } + if (target.includes("/actions/workflows/") && target.includes("/runs?")) { + return jsonResponse({ total_count: 0, workflow_runs: [] }) + } + throw new Error(`unexpected rehearsal request ${target}`) + } + const run = async (_command, args) => { + if (args[0] === "symbolic-ref") return { exitCode: 0, stdout: "main\n", stderr: "" } + if (args[0] === "status") return { exitCode: 0, stdout: "", stderr: "" } + if (args[0] === "rev-parse" && args.at(-1).startsWith("refs/remotes/origin/main")) { + return { exitCode: 0, stdout: `${CONTROLLER_SHA}\n`, stderr: "" } + } + throw new Error(`unexpected rehearsal command ${args.join(" ")}`) + } + const createAdapters = ({ cwd }) => + createDuplicateDraftConsolidationAdapters({ + cwd, + token: "fixture_token_value", + environment: { HOME: root, PATH: "/tools" }, + dependencies: { + fetchImpl, + run, + now, + createGitHubReader() { + return githubReader + }, + createOwnerPreflightAdapters() { + return { + git: { + async headSha() { + return CONTROLLER_SHA + }, + }, + } + }, + createNpmReader() { + return { + async observePackageVersion() { + return { + status: "ABSENT", + operation: "package-version", + httpStatus: 404, + code: "E404", + } + }, + } + }, + createCliAttestationVerifier() { + return { + async verify(input) { + return fixture.attestations.verify(input) + }, + } + }, + }, + }) + const wait = async (milliseconds, { signal }) => { + assert.equal(signal instanceof AbortSignal, true) + assert.equal(signal.aborted, false) + nowMs += milliseconds + } + + return { + root, + fixture, + deleteEffects, + remainingReleaseIds() { + return currentReleases().map(({ id }) => String(id)) + }, + loseNextDeleteResponse(releaseId) { + assert.equal(droppedResponseTarget, null) + droppedResponseTarget = releaseId + }, + wallClockTimeline() { + const value = new Date(nowMs + 90_000).toISOString() + return Object.freeze(Array.from({ length: 256 }, () => value)) + }, + cli(argv, stdout, stderr, perform = undefined) { + return { + argv: [...argv], + cwd: root, + environment: {}, + stdout, + stderr, + dependencies: { + createAdapters, + now, + wait, + ...(perform === undefined ? {} : { perform }), + }, + } + }, + } +} + +function performFaultInjection(scenario, harness) { + let injected = false + return async (input, dependencies) => + performDuplicateDraftConsolidation(input, { + ...dependencies, + async performOneDeletion(deletionInput, deletionDependencies) { + const selected = !injected && deletionInput.targetReleaseId === scenario.target + if (!selected) { + return performOneDuplicateDeletion(deletionInput, { + ...deletionDependencies, + wallClockTimeline: harness.wallClockTimeline(), + }) + } + injected = true + const result = await performOneDuplicateDeletion(deletionInput, { + ...deletionDependencies, + ...(scenario.boundary === undefined ? {} : { faultAt: scenario.boundary }), + wallClockTimeline: harness.wallClockTimeline(), + }) + if (scenario.afterConvergence === true) { + throw new Error("fixture process loss after durable convergence") + } + return result + }, + }) +} + +function assertThreeDistinctEquivalentDrafts(harness) { + const releases = harness.fixture.releases + assert.deepEqual( + releases.map(({ id }) => String(id)), + [DUPLICATE_DRAFT_SURVIVOR_ID, ...DUPLICATE_DRAFT_IDS], + ) + assert.equal(new Set(releases.map(({ id }) => String(id))).size, 3) + assert.deepEqual( + releases.map(({ assets }) => assets.length), + [45, 45, 45], + ) + for (let index = 0; index < releases[0].assets.length; index += 1) { + const assets = releases.map(({ assets: entries }) => entries[index]) + assert.equal(new Set(assets.map(({ id }) => String(id))).size, 3) + assert.equal(new Set(assets.map(({ name }) => name)).size, 1) + const bytes = releases.map(({ id }) => harness.fixture.assetBytes(String(id), assets[0].name)) + assert.deepEqual(bytes[1], bytes[0]) + assert.deepEqual(bytes[2], bytes[0]) + } +} + +async function assertJournalRecovery(root, receipt, expectedIntents) { + const journal = receipt.record.journalEnvelope + const confirmation = exactConfirmation(receipt.record.proposedEnvelope) + assert.equal( + journal.record.confirmationSha256, + createHash("sha256").update(confirmation, "utf8").digest("hex"), + ) + assert.equal( + journal.record.events.filter(({ event }) => event.type === "delete-intent").length, + expectedIntents, + ) + assert.deepEqual( + journal.record.events + .filter(({ event }) => event.type === "absence-converged") + .map(({ event }) => event.payload.targetReleaseId), + [...DUPLICATE_DRAFT_IDS], + ) + assert.equal(journal.record.events.at(-1).event.type, "final-authority-observed") + const journalPath = path.join(root, JOURNAL) + const durableJournal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope(journalPath, DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes), + ) + assert.deepEqual(durableJournal, journal) + const headPath = journalPath.replace(/journal\.json$/u, "journal.head.json") + const head = JSON.parse(await readPrivateEnvelope(headPath, 16 * 1024)) + assert.deepEqual(head, { + schemaVersion: 1, + journalPath, + repository: journal.record.repository, + proposedRecordSha256: journal.record.proposedRecordSha256, + journalRecordSha256: journal.recordSha256, + lastEventSha256: journal.record.events.at(-1).eventSha256, + sequence: journal.record.events.length, + updatedAt: journal.record.updatedAt, + }) +} + +function assertStableAuthority(receipt, proposal) { + assert.deepEqual(proposal.record.controller, { + headSha: CONTROLLER_SHA, + originMainSha: CONTROLLER_SHA, + githubMainSha: CONTROLLER_SHA, + }) + const authorities = receipt.record.journalEnvelope.record.events.flatMap(({ event }) => + event.type === "delete-authority-observed" || event.type === "final-authority-observed" + ? [event.payload.authority] + : [], + ) + assert.ok(authorities.length >= 3) + for (const authority of authorities) { + assert.deepEqual(authority.controller, proposal.record.controller) + assert.equal(authority.workflowAuthority.state, "disabled_manually") + assert.deepEqual(authority.workflowAuthority.nonterminalRuns, []) + assert.equal(authority.annotatedTag.targetSha, DUPLICATE_DRAFT_CANDIDATE.commitSha) + } +} + +function exactConfirmation(proposal) { + return `CONSOLIDATE v${proposal.record.candidate.version} ${proposal.record.candidate.commitSha} SURVIVOR ${proposal.record.roles.survivor} DELETE ${proposal.record.roles.duplicates.join(",")} PROPOSAL ${proposal.recordSha256}` +} + +function jsonResponse(value) { + return new Response(JSON.stringify(value), { + status: 200, + headers: { "content-type": "application/json" }, + }) +} + +function memorySink() { + const sink = { + value: "", + write(chunk) { + sink.value += String(chunk) + return true + }, + } + return sink +} + +function throwingSink() { + return { + write() { + throw new Error("fixture process loss before CLI success output") + }, + } +} diff --git a/scripts/release/test/duplicate-draft-consolidation-schema.test.mjs b/scripts/release/test/duplicate-draft-consolidation-schema.test.mjs new file mode 100644 index 000000000..16c164e52 --- /dev/null +++ b/scripts/release/test/duplicate-draft-consolidation-schema.test.mjs @@ -0,0 +1,1268 @@ +import assert from "node:assert/strict" +import test from "node:test" +import { + canonicalConsolidationEnvelopeBytes, + canonicalEventEnvelope, + canonicalRecordSha256, + createConsolidationEnvelope, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS, + parseConsolidationEnvelope, + parseJournalEventEnvelope, +} from "../duplicate-draft-consolidation-schema.mjs" +import { RELEASE_PAYLOAD_LIMITS } from "../limits.mjs" +import { CANONICAL_RELEASE_PACKAGE_ORDER } from "../manifest.mjs" + +const MEBIBYTE = 1024 * 1024 +const SHA = "0123456789abcdef0123456789abcdef01234567" +const DIGEST = `${SHA}0123456789abcdef01234567` +const NOW = "2026-09-01T12:00:00.000Z" +const SURVIVOR_ID = "379991871" +const DUPLICATE_IDS = Object.freeze(["379982100", "379986168"]) + +test("dedicated consolidation limits preserve journal and receipt headroom", () => { + assert.deepEqual(DUPLICATE_DRAFT_CONSOLIDATION_LIMITS, { + proposedBytes: 4 * MEBIBYTE, + journalBytes: 72 * MEBIBYTE, + finalReceiptBytes: 96 * MEBIBYTE, + authorityStageBytes: 8 * MEBIBYTE, + survivorEvidenceBytes: 2 * MEBIBYTE, + journalEventReserveBytes: 8 * MEBIBYTE, + envelopeReserveBytes: MEBIBYTE, + maximumDeleteAttempts: 3, + maximumTargets: 2, + maximumOrphanAuthorityRecoveries: 1, + maximumAssetDownloads: 135, + }) + assert.ok( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes >= + (2 * 3 + 1 + 1) * DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalEventReserveBytes, + ) + assert.ok( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes >= + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.survivorEvidenceBytes + + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.envelopeReserveBytes, + ) + assert.equal(Object.isFrozen(DUPLICATE_DRAFT_CONSOLIDATION_LIMITS), true) +}) + +test("proposed envelopes round trip as canonical newline-terminated bytes", () => { + const envelope = createConsolidationEnvelope("proposed", proposedRecord()) + const bytes = canonicalConsolidationEnvelopeBytes("proposed", envelope) + + assert.deepEqual(parseConsolidationEnvelope("proposed", bytes), envelope) + assert.match(envelope.recordSha256, /^[0-9a-f]{64}$/u) + assert.equal(envelope.recordSha256, canonicalRecordSha256(envelope.record)) + assert.equal(bytes.at(-1), 0x0a) + assert.equal(bytes.at(-2) === 0x0a, false) +}) + +test("all three top-level record schemas and the journal event envelope are exact", () => { + const proposedEnvelope = createConsolidationEnvelope("proposed", proposedRecord()) + const eventEnvelope = canonicalEventEnvelope( + operationStartedEvent(proposedEnvelope.recordSha256), + null, + ) + const journalEnvelope = createConsolidationEnvelope( + "journal", + journalRecord(proposedEnvelope, [eventEnvelope]), + ) + const finalEnvelope = createConsolidationEnvelope( + "final", + finalRecord(proposedEnvelope, journalEnvelope), + ) + + for (const [kind, envelope] of [ + ["proposed", proposedEnvelope], + ["journal", journalEnvelope], + ["final", finalEnvelope], + ]) { + const bytes = canonicalConsolidationEnvelopeBytes(kind, envelope) + assert.deepEqual(parseConsolidationEnvelope(kind, bytes), envelope) + + for (const path of requiredObjectFieldPaths(envelope.record)) { + const missing = structuredClone(envelope.record) + delete valueAtPath(missing, path.slice(0, -1))[path.at(-1)] + assert.throws( + () => createConsolidationEnvelope(kind, missing), + undefined, + `${kind} accepted missing ${path.join(".")}`, + ) + } + + const unknown = structuredClone(envelope.record) + unknown.unexpected = true + assert.throws(() => createConsolidationEnvelope(kind, unknown)) + } + + assert.deepEqual(parseJournalEventEnvelope(eventEnvelope, 1, null), eventEnvelope) + const unknownEvent = structuredClone(eventEnvelope.event) + unknownEvent.unexpected = true + assert.throws(() => canonicalEventEnvelope(unknownEvent, null)) +}) + +test("fixed array order, identities, workflow authority, and npm absence are enforced", () => { + const reorderedStatuses = proposedRecord() + reorderedStatuses.workflowAuthority.query.statuses.reverse() + assert.throws(() => createConsolidationEnvelope("proposed", reorderedStatuses)) + + const reorderedRoles = proposedRecord() + reorderedRoles.roles.duplicates.reverse() + assert.throws(() => createConsolidationEnvelope("proposed", reorderedRoles)) + + const consistentlyReorderedRoles = proposedRecord() + consistentlyReorderedRoles.roles.duplicates.reverse() + consistentlyReorderedRoles.confirmation.duplicates.reverse() + const [survivor, firstDuplicate, secondDuplicate] = consistentlyReorderedRoles.releases + consistentlyReorderedRoles.releases = [survivor, secondDuplicate, firstDuplicate] + assert.throws(() => createConsolidationEnvelope("proposed", consistentlyReorderedRoles)) + + const reorderedReleases = proposedRecord() + reorderedReleases.releases.reverse() + assert.throws(() => createConsolidationEnvelope("proposed", reorderedReleases)) + + const wrongWorkflow = proposedRecord() + wrongWorkflow.workflowAuthority.state = "active" + assert.throws(() => createConsolidationEnvelope("proposed", wrongWorkflow)) + + const publishedPackage = proposedRecord() + publishedPackage.npmInventories[0].packages[0].status = "PRESENT" + assert.throws(() => createConsolidationEnvelope("proposed", publishedPackage)) +}) + +test("canonical byte parsing rejects duplicate keys, drift, invalid UTF-8, and every size bound", () => { + const envelope = createConsolidationEnvelope("proposed", proposedRecord()) + const canonical = canonicalConsolidationEnvelopeBytes("proposed", envelope) + const source = canonical.toString("utf8") + + assert.throws(() => + parseConsolidationEnvelope( + "proposed", + Buffer.from(source.replace('{"record":', '{"recordSha256":"0","record":')), + ), + ) + + const changedDigest = structuredClone(envelope) + changedDigest.recordSha256 = "f".repeat(64) + assert.throws(() => + parseConsolidationEnvelope( + "proposed", + Buffer.from(`${JSON.stringify(changedDigest)}\n`, "utf8"), + ), + ) + assert.throws(() => parseConsolidationEnvelope("proposed", canonical.subarray(0, -1))) + assert.throws(() => parseConsolidationEnvelope("proposed", Buffer.from([0xc3, 0x28, 0x0a]))) + assert.throws(() => + parseConsolidationEnvelope( + "proposed", + Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), canonical]), + ), + ) + + for (const [kind, maximum] of [ + ["proposed", DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes], + ["journal", DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes], + ["final", DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes], + ]) { + assert.throws(() => parseConsolidationEnvelope(kind, Buffer.alloc(maximum + 1, 0x20))) + } + + const oversizedRecord = proposedRecord() + oversizedRecord.releases[0].semantic.body = "x".repeat( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes, + ) + assert.throws(() => createConsolidationEnvelope("proposed", oversizedRecord)) +}) + +test("nested evidence limits reject oversized authority, survivor, download, and Release payloads", () => { + const oversizedAuthority = authorityStage("pre-delete-1") + oversizedAuthority.annotatedTag.name = "x".repeat( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes, + ) + assert.throws(() => + canonicalEventEnvelope( + journalEvent("delete-authority-observed", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + authority: oversizedAuthority, + }), + null, + ), + ) + + const oversizedFinalAuthority = authorityStage("final", null) + oversizedFinalAuthority.annotatedTag.name = "x".repeat( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes, + ) + assert.throws(() => + canonicalEventEnvelope( + journalEvent("final-authority-observed", { + authority: oversizedFinalAuthority, + }), + null, + ), + ) + + const tooManyDownloads = authorityStage("pre-delete-1") + tooManyDownloads.releases.push(releaseEvidence("duplicate", "999999999", 4000)) + assert.throws(() => + canonicalEventEnvelope( + journalEvent("delete-authority-observed", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + authority: tooManyDownloads, + }), + null, + ), + ) + + const { proposedEnvelope, journalEnvelope } = envelopeFixtures() + const oversizedSurvivorRecord = finalRecord(proposedEnvelope, journalEnvelope) + const oversizedNodeId = "x".repeat(DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.survivorEvidenceBytes) + oversizedSurvivorRecord.finalAuthority.releases[0].nodeId = oversizedNodeId + oversizedSurvivorRecord.finalSurvivor.nodeId = oversizedNodeId + assert.throws(() => createConsolidationEnvelope("final", oversizedSurvivorRecord)) + + const oversizedAsset = releaseEvidence("duplicate", DUPLICATE_IDS[0], 2000) + oversizedAsset.assets[0].size = RELEASE_PAYLOAD_LIMITS.tarballBytes + 1 + assert.throws(() => + canonicalEventEnvelope( + journalEvent("resume-reconciliation", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification: "present-unchanged-retryable", + releaseEvidence: oversizedAsset, + observedAt: NOW, + }), + null, + ), + ) + + const aggregateOverflow = releaseEvidence("duplicate", DUPLICATE_IDS[0], 2000) + for (const asset of aggregateOverflow.assets) asset.size = 2 * MEBIBYTE + assert.throws(() => + canonicalEventEnvelope( + journalEvent("resume-reconciliation", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification: "present-unchanged-retryable", + releaseEvidence: aggregateOverflow, + observedAt: NOW, + }), + null, + ), + ) + + const oversizedAssetName = releaseEvidence("duplicate", DUPLICATE_IDS[0], 2000) + oversizedAssetName.assets[0].name = "x".repeat(RELEASE_PAYLOAD_LIMITS.archiveFilenameBytes + 1) + assert.throws(() => + canonicalEventEnvelope( + journalEvent("resume-reconciliation", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification: "present-unchanged-retryable", + releaseEvidence: oversizedAssetName, + observedAt: NOW, + }), + null, + ), + ) +}) + +test("embedded envelopes retain their own proposed and journal byte ceilings", () => { + const { proposedEnvelope, journalEnvelope } = envelopeFixtures() + const oversizedProposedRecord = structuredClone(proposedEnvelope.record) + oversizedProposedRecord.repository.name = "x".repeat( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes, + ) + const oversizedProposedEnvelope = { + record: oversizedProposedRecord, + recordSha256: canonicalRecordSha256(oversizedProposedRecord), + } + assert.throws(() => + createConsolidationEnvelope("final", finalRecord(oversizedProposedEnvelope, journalEnvelope)), + ) + + const oversizedJournalRecord = journalRecord(proposedEnvelope, []) + let previousEventSha256 = null + const operation = operationStartedEvent(proposedEnvelope.recordSha256) + oversizedJournalRecord.events.push(canonicalEventEnvelope(operation, previousEventSha256)) + previousEventSha256 = oversizedJournalRecord.events.at(-1).eventSha256 + for (let index = 0; index < 11; index += 1) { + const authority = authorityStage("final", null) + authority.annotatedTag.name = "x".repeat(7 * MEBIBYTE) + const event = journalEvent("final-authority-observed", { authority }) + event.sequence = index + 2 + event.previousEventSha256 = previousEventSha256 + const envelope = canonicalEventEnvelope(event, previousEventSha256) + oversizedJournalRecord.events.push(envelope) + previousEventSha256 = envelope.eventSha256 + } + const oversizedJournalEnvelope = { + record: oversizedJournalRecord, + recordSha256: canonicalRecordSha256(oversizedJournalRecord), + } + assert.throws(() => + createConsolidationEnvelope("final", finalRecord(proposedEnvelope, oversizedJournalEnvelope)), + ) +}) + +test("every fixed array rejects holes and unexpected own properties", () => { + for (const path of [ + ["roles", "duplicates"], + ["confirmation", "duplicates"], + ["workflowAuthority", "query", "statuses"], + ["workflowAuthority", "nonterminalRuns"], + ["npmInventories"], + ["npmInventories", 0, "packages"], + ["releases"], + ["releases", 0, "assets"], + ["payloadProof", "baseAssetSet"], + ["payloadProof", "attestationVerification", "subjects"], + ]) { + const withExtra = proposedRecord() + Object.defineProperty(valueAtPath(withExtra, path), "extra", { + value: "unexpected", + enumerable: true, + configurable: true, + }) + assert.throws( + () => createConsolidationEnvelope("proposed", withExtra), + undefined, + `accepted extra array property at ${path.join(".")}`, + ) + + const withHole = proposedRecord() + const array = valueAtPath(withHole, path) + if (array.length > 0) { + const last = array.length - 1 + const displaced = array[last] + delete array[last] + Object.defineProperty(array, "replacement", { + value: displaced, + enumerable: true, + configurable: true, + }) + assert.throws( + () => createConsolidationEnvelope("proposed", withHole), + undefined, + `accepted sparse array at ${path.join(".")}`, + ) + } + } + + const symbolArray = proposedRecord() + symbolArray.workflowAuthority.query.statuses[Symbol("hidden")] = true + assert.throws(() => createConsolidationEnvelope("proposed", symbolArray)) + + const hiddenArray = proposedRecord() + Object.defineProperty(hiddenArray.workflowAuthority.query.statuses, "hidden", { + value: true, + enumerable: false, + }) + assert.throws(() => createConsolidationEnvelope("proposed", hiddenArray)) + + let arrayGetterCalls = 0 + const accessorArray = proposedRecord() + Object.defineProperty(accessorArray.workflowAuthority.query.statuses, 0, { + get() { + arrayGetterCalls += 1 + return "in_progress" + }, + enumerable: true, + configurable: true, + }) + assert.throws(() => createConsolidationEnvelope("proposed", accessorArray)) + assert.equal(arrayGetterCalls, 0) + + const { proposedEnvelope } = envelopeFixtures() + for (const path of [["deletionOrder"], ["events"]]) { + const record = journalRecord(proposedEnvelope, [ + canonicalEventEnvelope(operationStartedEvent(proposedEnvelope.recordSha256), null), + ]) + Object.defineProperty(valueAtPath(record, path), "extra", { + value: true, + enumerable: true, + }) + assert.throws(() => createConsolidationEnvelope("journal", record)) + } + + const event = operationStartedEvent() + Object.defineProperty(event.payload.deletionOrder, "extra", { + value: true, + enumerable: true, + }) + assert.throws(() => canonicalEventEnvelope(event, null)) +}) + +test("exact objects reject accessors, hidden and symbol fields, unsafe keys, and prototypes", () => { + let getterCalls = 0 + const accessor = proposedRecord() + Object.defineProperty(accessor.repository, "name", { + get() { + getterCalls += 1 + return "cacheplane/dawnai" + }, + enumerable: true, + configurable: true, + }) + assert.throws(() => createConsolidationEnvelope("proposed", accessor)) + assert.equal(getterCalls, 0) + + const hidden = proposedRecord() + Object.defineProperty(hidden.repository, "hidden", { + value: true, + enumerable: false, + }) + assert.throws(() => createConsolidationEnvelope("proposed", hidden)) + + const symbol = proposedRecord() + symbol.repository[Symbol("hidden")] = true + assert.throws(() => createConsolidationEnvelope("proposed", symbol)) + + const unsafe = proposedRecord() + Object.defineProperty(unsafe.repository, "__proto__", { + value: {}, + enumerable: true, + }) + assert.throws(() => createConsolidationEnvelope("proposed", unsafe)) + + const prototype = proposedRecord() + Object.setPrototypeOf(prototype.repository.actor, { inherited: true }) + assert.throws(() => createConsolidationEnvelope("proposed", prototype)) + + let proxyReads = 0 + const proxied = proposedRecord() + proxied.repository.actor = new Proxy(proxied.repository.actor, { + get(target, property, receiver) { + proxyReads += 1 + return Reflect.get(target, property, receiver) + }, + }) + assert.throws(() => createConsolidationEnvelope("proposed", proxied)) + assert.equal(proxyReads, 0) + + let eventGetterCalls = 0 + const eventAccessor = journalEvent("delete-intent", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + authorityEventSha256: DIGEST, + }) + Object.defineProperty(eventAccessor.payload, "targetReleaseId", { + get() { + eventGetterCalls += 1 + return DUPLICATE_IDS[0] + }, + enumerable: true, + configurable: true, + }) + assert.throws(() => canonicalEventEnvelope(eventAccessor, null)) + assert.equal(eventGetterCalls, 0) +}) + +test("timestamps reject impossible dates and require canonical milliseconds", () => { + const omittedMilliseconds = proposedRecord() + omittedMilliseconds.inspectedAt = "2026-09-01T12:00:00Z" + assert.throws(() => createConsolidationEnvelope("proposed", omittedMilliseconds)) + + for (const invalid of [ + "2026-02-31T12:00:00.000Z", + "2025-02-29T12:00:00.000Z", + "2026-13-01T12:00:00.000Z", + ]) { + const record = proposedRecord() + record.inspectedAt = invalid + assert.throws(() => createConsolidationEnvelope("proposed", record)) + } +}) + +test("fixed cardinality and journal ceilings reject hostile tails before traversal", () => { + let fixedTailCalls = 0 + const oversizedFixedArray = proposedRecord() + const npmInventories = new Array(3) + npmInventories[0] = npmInventory("inspect-initial") + npmInventories[1] = npmInventory("inspect-ready") + Object.defineProperty(npmInventories, 2, { + get() { + fixedTailCalls += 1 + return npmInventory("inspect-ready") + }, + enumerable: true, + }) + oversizedFixedArray.npmInventories = npmInventories + assert.throws(() => createConsolidationEnvelope("proposed", oversizedFixedArray)) + assert.equal(fixedTailCalls, 0) + + let journalTailCalls = 0 + const { proposedEnvelope } = envelopeFixtures() + const record = journalRecord(proposedEnvelope, []) + record.events = new Array(DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes + 1) + Object.defineProperty(record.events, record.events.length - 1, { + get() { + journalTailCalls += 1 + return {} + }, + enumerable: true, + }) + assert.throws(() => createConsolidationEnvelope("journal", record)) + assert.equal(journalTailCalls, 0) +}) + +test("cumulative budgets stop repeated shared strings before hostile trailing evidence", () => { + const record = proposedRecord() + const shared = "x".repeat(512 * 1024) + for (const asset of record.releases[1].assets) asset.label = shared + let sentinelTraps = 0 + record.payloadProof = new Proxy(record.payloadProof, { + getPrototypeOf(target) { + sentinelTraps += 1 + return Reflect.getPrototypeOf(target) + }, + ownKeys(target) { + sentinelTraps += 1 + return Reflect.ownKeys(target) + }, + }) + assert.throws( + () => createConsolidationEnvelope("proposed", record), + /cumulative proposed envelope budget/iu, + ) + assert.equal(sentinelTraps, 0) + + const authority = authorityStage("pre-delete-1") + for (const asset of authority.releases[1].assets) asset.label = shared + let authoritySentinelTraps = 0 + authority.payloadProof = new Proxy(authority.payloadProof, { + getPrototypeOf(target) { + authoritySentinelTraps += 1 + return Reflect.getPrototypeOf(target) + }, + }) + assert.throws( + () => + canonicalEventEnvelope( + journalEvent("delete-authority-observed", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + authority, + }), + null, + ), + /cumulative (?:journal event envelope|authority stage) budget/iu, + ) + assert.equal(authoritySentinelTraps, 0) + + const resumeEvidence = releaseEvidence("duplicate", DUPLICATE_IDS[0], 2000) + for (const asset of resumeEvidence.assets.slice(0, -1)) asset.label = shared + let resumeSentinelTraps = 0 + resumeEvidence.assets[resumeEvidence.assets.length - 1] = new Proxy( + resumeEvidence.assets.at(-1), + { + getPrototypeOf(target) { + resumeSentinelTraps += 1 + return Reflect.getPrototypeOf(target) + }, + }, + ) + assert.throws( + () => + canonicalEventEnvelope( + journalEvent("resume-reconciliation", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification: "present-unchanged-retryable", + releaseEvidence: resumeEvidence, + observedAt: NOW, + }), + null, + ), + /cumulative journal event envelope budget/iu, + ) + assert.equal(resumeSentinelTraps, 0) +}) + +test("incremental accounting accepts canonical evidence close to its proposed cap", () => { + const record = proposedRecord() + const shared = "x".repeat(1_700_000) + record.releases[1].semantic.body = shared + record.releases[2].semantic.body = shared + const envelope = createConsolidationEnvelope("proposed", record) + const bytes = canonicalConsolidationEnvelopeBytes("proposed", envelope) + assert.ok(bytes.byteLength > DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes - MEBIBYTE) + assert.ok(bytes.byteLength < DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes) +}) + +test("authority-bearing events compose exact authority and wrapper budgets", () => { + for (const [type, authority, payload] of [ + [ + "delete-authority-observed", + authorityStage("pre-delete-1"), + (authorityValue) => ({ + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + authority: authorityValue, + }), + ], + [ + "final-authority-observed", + authorityStage("final", null), + (authorityValue) => ({ authority: authorityValue }), + ], + ]) { + resizeAuthorityStage(authority, DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes) + const event = journalEvent(type, payload(authority)) + const envelope = canonicalEventEnvelope(event, null) + assert.deepEqual(parseJournalEventEnvelope(envelope, 1, null), envelope) + + const oversized = structuredClone(authority) + resizeAuthorityStage(oversized, DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.authorityStageBytes + 1) + const oversizedEvent = journalEvent(type, payload(oversized)) + assert.throws(() => canonicalEventEnvelope(oversizedEvent, null)) + assert.throws(() => + parseJournalEventEnvelope( + { + event: oversizedEvent, + eventSha256: canonicalRecordSha256(oversizedEvent), + }, + 1, + null, + ), + ) + } + + const resumeEvidence = releaseEvidence("duplicate", DUPLICATE_IDS[0], 2000) + resumeEvidence.semantic.body = "x".repeat( + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalEventReserveBytes, + ) + const oversizedResumeEvent = journalEvent("resume-reconciliation", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification: "present-unchanged-retryable", + releaseEvidence: resumeEvidence, + observedAt: NOW, + }) + assert.throws(() => canonicalEventEnvelope(oversizedResumeEvent, null)) + assert.throws(() => + parseJournalEventEnvelope( + { + event: oversizedResumeEvent, + eventSha256: canonicalRecordSha256(oversizedResumeEvent), + }, + 1, + null, + ), + ) +}) + +test("Git object SHAs accept exactly 40 or 64 lowercase hex characters", () => { + assert.doesNotThrow(() => createConsolidationEnvelope("proposed", proposedRecord())) + + const sha64 = "a".repeat(64) + const withSha256Objects = proposedRecord() + withSha256Objects.controller = { + headSha: sha64, + originMainSha: sha64, + githubMainSha: sha64, + } + withSha256Objects.candidate.commitSha = sha64 + withSha256Objects.confirmation.commitSha = sha64 + withSha256Objects.annotatedTag.objectSha = sha64 + withSha256Objects.annotatedTag.targetSha = sha64 + assert.doesNotThrow(() => createConsolidationEnvelope("proposed", withSha256Objects)) + + for (const length of [39, 41, 52, 63, 65]) { + const impossible = proposedRecord() + impossible.controller = { + headSha: "a".repeat(length), + originMainSha: "a".repeat(length), + githubMainSha: "a".repeat(length), + } + assert.throws( + () => createConsolidationEnvelope("proposed", impossible), + undefined, + `accepted impossible Git object SHA length ${length}`, + ) + } +}) + +test("Release evidence requires the exact main target commitish", () => { + assert.doesNotThrow(() => createConsolidationEnvelope("proposed", proposedRecord())) + + for (const targetCommitish of [SHA, "develop", "refs/heads/main", "main\n", "ma\u0456n"]) { + const proposed = proposedRecord() + proposed.releases[0].semantic.targetCommitish = targetCommitish + assert.throws(() => createConsolidationEnvelope("proposed", proposed)) + + const resume = journalEvent("resume-reconciliation", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification: "present-unchanged-retryable", + releaseEvidence: releaseEvidence("duplicate", DUPLICATE_IDS[0], 2000), + observedAt: NOW, + }) + resume.payload.releaseEvidence.semantic.targetCommitish = targetCommitish + assert.throws(() => canonicalEventEnvelope(resume, null)) + + const { proposedEnvelope, journalEnvelope } = envelopeFixtures() + const final = finalRecord(proposedEnvelope, journalEnvelope) + final.finalSurvivor.semantic.targetCommitish = targetCommitish + assert.throws(() => createConsolidationEnvelope("final", final)) + } +}) + +test("journal events enforce the hash chain and every exact typed payload", () => { + const events = eventFixtures() + let previous = null + for (let index = 0; index < events.length; index += 1) { + const event = { + ...events[index], + sequence: index + 1, + previousEventSha256: previous, + } + const envelope = canonicalEventEnvelope(event, previous) + assert.deepEqual(parseJournalEventEnvelope(envelope, index + 1, previous), envelope) + + for (const path of requiredObjectFieldPaths(event)) { + const missingField = structuredClone(event) + delete valueAtPath(missingField, path.slice(0, -1))[path.at(-1)] + assert.throws( + () => canonicalEventEnvelope(missingField, previous), + undefined, + `${event.type} accepted missing ${path.join(".")}`, + ) + } + + previous = envelope.eventSha256 + } + + const first = canonicalEventEnvelope(operationStartedEvent(), null) + assert.throws(() => parseJournalEventEnvelope(first, 2, null)) + assert.throws(() => parseJournalEventEnvelope(first, 1, DIGEST)) + assert.throws(() => parseJournalEventEnvelope({ ...first, eventSha256: DIGEST }, 1, null)) +}) + +test("event classifications, nullable evidence, convergence bases, and attempts are exact", () => { + const outcomeTriplets = [ + ["confirmed-204", 204], + ["transport-ambiguous", null], + ["response-404-ambiguous", 404], + ] + for (const [classification, httpStatus] of outcomeTriplets) { + assert.doesNotThrow(() => + canonicalEventEnvelope( + journalEvent("delete-outcome", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification, + httpStatus, + observedAt: NOW, + }), + null, + ), + ) + for (const wrongStatus of [204, null, 404].filter((value) => value !== httpStatus)) { + assert.throws(() => + canonicalEventEnvelope( + journalEvent("delete-outcome", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification, + httpStatus: wrongStatus, + observedAt: NOW, + }), + null, + ), + ) + } + } + for (const httpStatus of [null, 302, 403, 429, 500]) { + assert.doesNotThrow(() => + canonicalEventEnvelope( + journalEvent("delete-outcome", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification: "response-hard-failure", + httpStatus, + observedAt: NOW, + }), + null, + ), + ) + } + for (const httpStatus of [204, 404]) { + assert.throws(() => + canonicalEventEnvelope( + journalEvent("delete-outcome", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification: "response-hard-failure", + httpStatus, + observedAt: NOW, + }), + null, + ), + ) + } + assert.throws(() => + canonicalEventEnvelope( + journalEvent("delete-outcome", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification: "unknown", + httpStatus: null, + observedAt: NOW, + }), + null, + ), + ) + + const presentEvidence = releaseEvidence("duplicate", DUPLICATE_IDS[0], 2000) + for (const [classification, releaseEvidenceValue] of [ + ["present-unchanged-retryable", presentEvidence], + ["absent-ambiguous", null], + ]) { + assert.doesNotThrow(() => + canonicalEventEnvelope( + journalEvent("resume-reconciliation", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification, + releaseEvidence: releaseEvidenceValue, + observedAt: NOW, + }), + null, + ), + ) + } + for (const [classification, releaseEvidenceValue] of [ + ["present-unchanged-retryable", null], + ["absent-ambiguous", presentEvidence], + ]) { + assert.throws(() => + canonicalEventEnvelope( + journalEvent("resume-reconciliation", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification, + releaseEvidence: releaseEvidenceValue, + observedAt: NOW, + }), + null, + ), + ) + } + + for (const basis of ["confirmed-204", "ambiguous"]) { + assert.doesNotThrow(() => + canonicalEventEnvelope( + journalEvent("absence-converged", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + basis, + directGet404At: NOW, + listAbsentAt: NOW, + attempts: 1, + completedAt: NOW, + }), + null, + ), + ) + } + assert.throws(() => + canonicalEventEnvelope( + journalEvent("absence-converged", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + basis: "unknown", + directGet404At: NOW, + listAbsentAt: NOW, + attempts: 1, + completedAt: NOW, + }), + null, + ), + ) + assert.throws(() => + canonicalEventEnvelope( + journalEvent("delete-intent", { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 4, + authorityEventSha256: DIGEST, + }), + null, + ), + ) +}) + +function proposedRecord() { + const releases = [ + releaseEvidence("survivor", SURVIVOR_ID, 1000), + releaseEvidence("duplicate", DUPLICATE_IDS[0], 2000), + releaseEvidence("duplicate", DUPLICATE_IDS[1], 3000), + ] + return { + schemaVersion: 1, + repository: repository(), + controller: controller(), + candidate: candidate(), + roles: { survivor: SURVIVOR_ID, duplicates: [...DUPLICATE_IDS] }, + confirmation: { + version: "0.8.22", + commitSha: SHA, + survivor: SURVIVOR_ID, + duplicates: [...DUPLICATE_IDS], + template: "Consolidate <64-lowercase-hex-digest>", + }, + annotatedTag: annotatedTag(), + workflowAuthority: workflowAuthority(), + npmInventories: [npmInventory("inspect-initial"), npmInventory("inspect-ready")], + releases, + payloadProof: payloadProof(), + inspectedAt: NOW, + } +} + +function journalRecord(proposedEnvelope, events) { + return { + schemaVersion: 1, + repository: repository(), + candidate: candidate(), + proposedRecordSha256: proposedEnvelope.recordSha256, + confirmationSha256: DIGEST, + deletionOrder: [...DUPLICATE_IDS], + events, + updatedAt: NOW, + } +} + +function finalRecord(proposedEnvelope, journalEnvelope) { + const finalAuthority = authorityStage("final", null) + return { + schemaVersion: 1, + proposedEnvelope, + journalEnvelope, + finalAuthority, + finalSurvivor: finalAuthority.releases[0], + completedAt: NOW, + } +} + +function envelopeFixtures() { + const proposedEnvelope = createConsolidationEnvelope("proposed", proposedRecord()) + const operation = canonicalEventEnvelope( + operationStartedEvent(proposedEnvelope.recordSha256), + null, + ) + const journalEnvelope = createConsolidationEnvelope( + "journal", + journalRecord(proposedEnvelope, [operation]), + ) + return { proposedEnvelope, journalEnvelope } +} + +function repository() { + return { + name: "cacheplane/dawnai", + id: "123456789", + defaultBranch: "main", + actor: { login: "blove", id: "1234" }, + } +} + +function controller() { + return { headSha: SHA, originMainSha: SHA, githubMainSha: SHA } +} + +function candidate() { + return { version: "0.8.22", commitSha: SHA, tag: "v0.8.22" } +} + +function annotatedTag() { + return { + name: "v0.8.22", + objectSha: SHA, + targetSha: SHA, + objectType: "tag", + observedAt: NOW, + } +} + +function workflowAuthority() { + return { + workflowId: "12345", + path: ".github/workflows/release.yml", + state: "disabled_manually", + query: { + statuses: ["in_progress", "pending", "queued", "requested", "waiting"], + perPage: 100, + maximumPages: 100, + }, + nonterminalRuns: [], + observedAt: NOW, + } +} + +function npmInventory(stage) { + return { + stage, + startedAt: NOW, + completedAt: NOW, + packages: CANONICAL_RELEASE_PACKAGE_ORDER.map((name) => ({ + name, + version: "0.8.22", + status: "ABSENT", + httpStatus: 404, + code: "E404", + observedAt: NOW, + })), + } +} + +function releaseEvidence(role, id, assetIdStart) { + return { + role, + id, + nodeId: `RE_${id}`, + tagName: `untagged-${id}`, + createdAt: NOW, + updatedAt: NOW, + semantic: { + name: "v0.8.22 escrow", + targetCommitish: "main", + draft: true, + immutable: false, + prerelease: false, + publishedAt: null, + body: "ESCROWED", + bodySha256: DIGEST, + author: { + login: "github-actions[bot]", + id: "41898282", + nodeId: "MDQ6VXNlcjQxODk4Mjgy", + }, + }, + assets: Array.from({ length: 45 }, (_, index) => ({ + id: String(assetIdStart + index), + nodeId: `RA_${assetIdStart + index}`, + name: `asset-${String(index).padStart(2, "0")}`, + label: null, + state: "uploaded", + contentType: "application/octet-stream", + size: 1, + digest: `sha256:${DIGEST}`, + uploader: { + login: "github-actions[bot]", + id: "41898282", + nodeId: "MDQ6VXNlcjQxODk4Mjgy", + }, + createdAt: NOW, + updatedAt: NOW, + downloadCount: 0, + downloadSha256: DIGEST, + })), + } +} + +function payloadProof() { + const baseAssetSet = Array.from({ length: 45 }, (_, index) => ({ + name: `asset-${String(index).padStart(2, "0")}`, + sha256: DIGEST, + })) + return { + baseAssetSet, + baseAssetSetSha256: DIGEST, + consolidationPayloadSha256: DIGEST, + attestationVerification: { + status: "VERIFIED", + subjects: Array.from({ length: 22 }, (_, index) => ({ + name: `subject-${String(index).padStart(2, "0")}`, + sha256: DIGEST, + })), + }, + } +} + +function targetRead() { + const evidence = releaseEvidence("duplicate", DUPLICATE_IDS[0], 2000) + return { + releaseGetStartedAt: NOW, + releaseGetCompletedAt: NOW, + assetsListStartedAt: NOW, + assetsListCompletedAt: NOW, + evidence, + evidenceSha256: canonicalRecordSha256(evidence), + } +} + +function authorityStage(stage, read = targetRead()) { + return { + stage, + controller: controller(), + annotatedTag: annotatedTag(), + workflowAuthority: workflowAuthority(), + npmInventory: npmInventory(stage), + releases: + stage === "final" + ? [releaseEvidence("survivor", SURVIVOR_ID, 1000)] + : [ + releaseEvidence("survivor", SURVIVOR_ID, 1000), + releaseEvidence("duplicate", DUPLICATE_IDS[0], 2000), + releaseEvidence("duplicate", DUPLICATE_IDS[1], 3000), + ], + payloadProof: payloadProof(), + targetRead: read, + observedAt: NOW, + } +} + +function resizeAuthorityStage(authority, canonicalBytes) { + const currentBytes = Buffer.byteLength(`${JSON.stringify(authority)}\n`, "utf8") + const currentNameBytes = Buffer.byteLength(JSON.stringify(authority.annotatedTag.name), "utf8") + const replacementBytes = canonicalBytes - currentBytes + currentNameBytes + assert.ok(replacementBytes >= 2) + authority.annotatedTag.name = "x".repeat(replacementBytes - 2) + assert.equal(Buffer.byteLength(`${JSON.stringify(authority)}\n`, "utf8"), canonicalBytes) +} + +function operationStartedEvent(proposedRecordSha256 = DIGEST) { + return { + schemaVersion: 1, + sequence: 1, + previousEventSha256: null, + type: "operation-started", + recordedAt: NOW, + payload: { + proposedRecordSha256, + confirmationSha256: DIGEST, + controllerSha: SHA, + deletionOrder: [...DUPLICATE_IDS], + }, + } +} + +function journalEvent(type, payload) { + return { + schemaVersion: 1, + sequence: 1, + previousEventSha256: null, + type, + recordedAt: NOW, + payload, + } +} + +function eventFixtures() { + return [ + operationStartedEvent(), + { + schemaVersion: 1, + sequence: 2, + previousEventSha256: null, + type: "npm-observed", + recordedAt: NOW, + payload: { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + inventory: npmInventory("perform-initial"), + }, + }, + { + schemaVersion: 1, + sequence: 3, + previousEventSha256: null, + type: "delete-authority-observed", + recordedAt: NOW, + payload: { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + authority: authorityStage("pre-delete-1"), + }, + }, + { + schemaVersion: 1, + sequence: 4, + previousEventSha256: null, + type: "delete-intent", + recordedAt: NOW, + payload: { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + authorityEventSha256: DIGEST, + }, + }, + { + schemaVersion: 1, + sequence: 5, + previousEventSha256: null, + type: "delete-outcome", + recordedAt: NOW, + payload: { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification: "confirmed-204", + httpStatus: 204, + observedAt: NOW, + }, + }, + { + schemaVersion: 1, + sequence: 6, + previousEventSha256: null, + type: "resume-reconciliation", + recordedAt: NOW, + payload: { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + classification: "absent-ambiguous", + releaseEvidence: null, + observedAt: NOW, + }, + }, + { + schemaVersion: 1, + sequence: 7, + previousEventSha256: null, + type: "absence-converged", + recordedAt: NOW, + payload: { + targetReleaseId: DUPLICATE_IDS[0], + attemptNumber: 1, + basis: "confirmed-204", + directGet404At: NOW, + listAbsentAt: NOW, + attempts: 1, + completedAt: NOW, + }, + }, + { + schemaVersion: 1, + sequence: 8, + previousEventSha256: null, + type: "final-authority-observed", + recordedAt: NOW, + payload: { authority: authorityStage("final", null) }, + }, + ] +} + +function requiredObjectFieldPaths(value, prefix = []) { + const paths = [] + if (value === null || typeof value !== "object") return paths + if (Array.isArray(value)) { + if (value.length > 0) paths.push(...requiredObjectFieldPaths(value[0], [...prefix, 0])) + return paths + } + for (const [key, child] of Object.entries(value)) { + const path = [...prefix, key] + paths.push(path) + paths.push(...requiredObjectFieldPaths(child, path)) + } + return paths +} + +function valueAtPath(value, path) { + return path.reduce((current, key) => current[key], value) +} diff --git a/scripts/release/test/duplicate-draft-consolidation.test.mjs b/scripts/release/test/duplicate-draft-consolidation.test.mjs new file mode 100644 index 000000000..e4b621f26 --- /dev/null +++ b/scripts/release/test/duplicate-draft-consolidation.test.mjs @@ -0,0 +1,4073 @@ +import assert from "node:assert/strict" +import { createHash } from "node:crypto" +import { mkdirSync, renameSync } from "node:fs" +import { + chmod, + lstat, + mkdir, + mkdtemp, + readFile, + realpath, + rm, + stat, + symlink, + writeFile, +} from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import test from "node:test" +import { + inspectDuplicateDrafts, + performDuplicateDraftConsolidation, + performOneDuplicateDeletion, + verifyDuplicateDraftConsolidation, +} from "../duplicate-draft-consolidation.mjs" +import { createDuplicateDraftConsolidationAdapters } from "../duplicate-draft-consolidation-adapters.mjs" +import { runDuplicateDraftConsolidationCli } from "../duplicate-draft-consolidation-cli.mjs" +import { + captureDirectTargetRead, + semanticAssetProjection, + semanticReleaseProjection, +} from "../duplicate-draft-consolidation-evidence.mjs" +import { + readPrivateEnvelope, + readTrackedReceipt, + writePrivateEnvelope, + writeTrackedReceipt, +} from "../duplicate-draft-consolidation-files.mjs" +import { + appendJournalEvent, + createConsolidationJournal, + createFinalConsolidationReceipt, + deriveConsolidationState, +} from "../duplicate-draft-consolidation-journal.mjs" +import { + canonicalConsolidationEnvelopeBytes, + canonicalEventEnvelope, + canonicalRecordSha256, + createConsolidationEnvelope, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS, + parseConsolidationEnvelope, +} from "../duplicate-draft-consolidation-schema.mjs" +import { CANONICAL_RELEASE_PACKAGE_ORDER } from "../manifest.mjs" +import { + createDuplicateDraftConsolidationFixture, + DUPLICATE_DRAFT_CANDIDATE, + DUPLICATE_DRAFT_IDS, + DUPLICATE_DRAFT_SURVIVOR_ID, +} from "./support/duplicate-draft-consolidation-fixture.mjs" + +const OUTPUT = ".dawn/release/duplicate-draft-consolidation.proposed.json" +const BASE_TIME = Date.parse("2026-09-01T12:00:00.000Z") +const CONTROLLER_SHA = "b".repeat(40) +const CONVERGENCE_BACKOFFS = Object.freeze([1_000, 5_000, 15_000, 30_000, 30_000]) + +function rehashTestEnvelope(envelope) { + envelope.recordSha256 = canonicalRecordSha256(envelope.record) +} + +function rechainTestJournal(journalEnvelope) { + let previous = null + journalEnvelope.record.events = journalEnvelope.record.events.map(({ event }, index) => { + const next = canonicalEventEnvelope( + { + ...event, + sequence: index + 1, + previousEventSha256: previous, + }, + previous, + ) + previous = next.eventSha256 + return next + }) + journalEnvelope.record.updatedAt = journalEnvelope.record.events.at(-1).event.recordedAt + rehashTestEnvelope(journalEnvelope) +} + +function rebuildSelfConsistentTestReceipt(receipt) { + const proposal = receipt.record.proposedEnvelope + const journal = receipt.record.journalEnvelope + rehashTestEnvelope(proposal) + const confirmation = `CONSOLIDATE v${proposal.record.candidate.version} ${proposal.record.candidate.commitSha} SURVIVOR ${proposal.record.roles.survivor} DELETE ${proposal.record.roles.duplicates.join(",")} PROPOSAL ${proposal.recordSha256}` + const confirmationSha256 = createHash("sha256").update(confirmation, "utf8").digest("hex") + journal.record.repository = structuredClone(proposal.record.repository) + journal.record.candidate = structuredClone(proposal.record.candidate) + journal.record.proposedRecordSha256 = proposal.recordSha256 + journal.record.confirmationSha256 = confirmationSha256 + journal.record.deletionOrder = [...proposal.record.roles.duplicates] + let previous = null + journal.record.events = journal.record.events.map(({ event }, index) => { + const nextEvent = structuredClone(event) + nextEvent.sequence = index + 1 + nextEvent.previousEventSha256 = previous + if (nextEvent.type === "operation-started") { + nextEvent.payload.proposedRecordSha256 = proposal.recordSha256 + nextEvent.payload.confirmationSha256 = confirmationSha256 + nextEvent.payload.controllerSha = proposal.record.controller.headSha + nextEvent.payload.deletionOrder = [...proposal.record.roles.duplicates] + } + if (nextEvent.type === "delete-intent") { + nextEvent.payload.authorityEventSha256 = previous + } + const next = canonicalEventEnvelope(nextEvent, previous) + previous = next.eventSha256 + return next + }) + journal.record.updatedAt = journal.record.events.at(-1).event.recordedAt + rehashTestEnvelope(journal) + const finalEvent = journal.record.events.at(-1).event + assert.equal(finalEvent.type, "final-authority-observed") + receipt.record.finalAuthority = structuredClone(finalEvent.payload.authority) + receipt.record.finalSurvivor = structuredClone(finalEvent.payload.authority.releases[0]) + rehashTestEnvelope(receipt) +} + +function rebuildUncheckedChronologyReceipt(receipt) { + const proposal = receipt.record.proposedEnvelope + const journal = receipt.record.journalEnvelope + rehashTestEnvelope(proposal) + const confirmation = `CONSOLIDATE v${proposal.record.candidate.version} ${proposal.record.candidate.commitSha} SURVIVOR ${proposal.record.roles.survivor} DELETE ${proposal.record.roles.duplicates.join(",")} PROPOSAL ${proposal.recordSha256}` + const confirmationSha256 = createHash("sha256").update(confirmation, "utf8").digest("hex") + journal.record.proposedRecordSha256 = proposal.recordSha256 + journal.record.confirmationSha256 = confirmationSha256 + let previous = null + journal.record.events = journal.record.events.map(({ event }, index) => { + const nextEvent = structuredClone(event) + nextEvent.sequence = index + 1 + nextEvent.previousEventSha256 = previous + if (nextEvent.type === "operation-started") { + nextEvent.payload.proposedRecordSha256 = proposal.recordSha256 + nextEvent.payload.confirmationSha256 = confirmationSha256 + } + if (nextEvent.type === "delete-intent") nextEvent.payload.authorityEventSha256 = previous + const eventSha256 = canonicalRecordSha256(nextEvent) + previous = eventSha256 + return { event: nextEvent, eventSha256 } + }) + journal.record.updatedAt = journal.record.events.at(-1).event.recordedAt + rehashTestEnvelope(journal) + const finalAuthority = journal.record.events.at(-1).event.payload.authority + receipt.record.finalAuthority = structuredClone(finalAuthority) + receipt.record.finalSurvivor = structuredClone(finalAuthority.releases[0]) + rehashTestEnvelope(receipt) +} + +function after(timestamp, milliseconds = 1_000) { + return new Date(Date.parse(timestamp) + milliseconds).toISOString() +} + +function before(timestamp, milliseconds = 1_000) { + return new Date(Date.parse(timestamp) - milliseconds).toISOString() +} + +function mutateHistoricalEvidence(receipt, releaseId, mutate) { + const proposalEvidence = receipt.record.proposedEnvelope.record.releases.find( + ({ id }) => id === releaseId, + ) + if (proposalEvidence !== undefined) mutate(proposalEvidence) + for (const { event } of receipt.record.journalEnvelope.record.events) { + if (event.type === "delete-authority-observed" || event.type === "final-authority-observed") { + const authorityEvidence = event.payload.authority.releases.find(({ id }) => id === releaseId) + if (authorityEvidence !== undefined) mutate(authorityEvidence) + if (event.payload.authority.targetRead?.evidence.id === releaseId) { + mutate(event.payload.authority.targetRead.evidence) + event.payload.authority.targetRead.evidenceSha256 = canonicalRecordSha256( + event.payload.authority.targetRead.evidence, + ) + } + } + if (event.type === "resume-reconciliation" && event.payload.releaseEvidence?.id === releaseId) { + mutate(event.payload.releaseEvidence) + } + } +} + +function currentConsolidationPayloadSha256(releases) { + return canonicalRecordSha256( + releases.map((release) => ({ + release: semanticReleaseProjection(release), + assets: release.assets.map(semanticAssetProjection), + })), + ) +} + +function rebindTestProposal(receipt) { + const proposal = receipt.record.proposedEnvelope + const journal = receipt.record.journalEnvelope + rehashTestEnvelope(proposal) + journal.record.proposedRecordSha256 = proposal.recordSha256 + journal.record.events[0].event.payload.proposedRecordSha256 = proposal.recordSha256 + rechainTestJournal(journal) + rehashTestEnvelope(receipt) +} + +test("verify independently replays the receipt and revalidates the exact live survivor read-only", async (t) => { + const harness = await verificationFixture(t) + const result = await verifyDuplicateDraftConsolidation( + { receipt: "scripts/release/duplicate-draft-consolidation.json" }, + harness.dependencies, + ) + + assert.deepEqual(result, { + status: "verified", + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + deleted: [...DUPLICATE_DRAFT_IDS], + receipt: "scripts/release/duplicate-draft-consolidation.json", + receiptSha256: harness.receipt.recordSha256, + historicalParity: + "Historical duplicate payload parity is supported by embedded pre-delete evidence plus the currently reverified survivor; deleted bytes were not independently re-downloaded.", + }) + assert.deepEqual(harness.calls, [ + `direct:${DUPLICATE_DRAFT_IDS[0]}`, + `direct:${DUPLICATE_DRAFT_IDS[1]}`, + "releases", + "final-authority", + ]) + assert.equal(harness.writerCalls, 0) + assert.equal((await stat(harness.receiptPath)).mode & 0o777, 0o644) +}) + +async function legalRetryVerificationFixture(t) { + const deletion = await oneDeletionFixture(t, { + deleteClassifications: ["transport-ambiguous", "confirmed-204"], + freshAuthorityMutation: "volatile", + }) + const proposal = deletion.input.proposedEnvelope + let initialJournal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + deletion.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + const npmAt = new Date(Date.parse(initialJournal.record.updatedAt) + 1_000).toISOString() + initialJournal = appendJournalEvent( + initialJournal, + "npm-observed", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + inventory: { + ...structuredClone(proposal.record.npmInventories[1]), + stage: "perform-initial", + startedAt: npmAt, + completedAt: npmAt, + packages: proposal.record.npmInventories[1].packages.map((entry) => ({ + ...structuredClone(entry), + observedAt: npmAt, + })), + }, + }, + npmAt, + ) + await writePrivateEnvelope( + deletion.input.journalPath, + canonicalConsolidationEnvelopeBytes("journal", initialJournal), + ) + await writePrivateEnvelope( + deletion.input.journalPath.replace(/journal\.json$/u, "journal.head.json"), + testJournalHeadBytes(deletion.input.journalPath, initialJournal), + ) + await performOneDuplicateDeletion(deletion.input, deletion.dependencies) + let journal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + deletion.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + let tick = Date.parse(journal.record.updatedAt) + 1_000 + const nextTime = () => { + const value = new Date(tick).toISOString() + tick += 1_000 + return value + } + const targetReleaseId = DUPLICATE_DRAFT_IDS[1] + const authority = deletionAuthorityFixture({ + proposal, + stage: "pre-delete-2", + targetEvidence: proposal.record.releases[2], + observedAt: nextTime(), + releases: [proposal.record.releases[0], proposal.record.releases[2]], + }) + journal = appendJournalEvent( + journal, + "delete-authority-observed", + { targetReleaseId, attemptNumber: 1, authority }, + authority.observedAt, + ) + journal = appendJournalEvent( + journal, + "delete-intent", + { + targetReleaseId, + attemptNumber: 1, + authorityEventSha256: journal.record.events.at(-1).eventSha256, + }, + nextTime(), + ) + const outcomeAt = nextTime() + journal = appendJournalEvent( + journal, + "delete-outcome", + { + targetReleaseId, + attemptNumber: 1, + classification: "confirmed-204", + httpStatus: 204, + observedAt: outcomeAt, + }, + outcomeAt, + ) + const convergedAt = nextTime() + journal = appendJournalEvent( + journal, + "absence-converged", + { + targetReleaseId, + attemptNumber: 1, + basis: "confirmed-204", + directGet404At: convergedAt, + listAbsentAt: convergedAt, + attempts: 1, + completedAt: convergedAt, + }, + convergedAt, + ) + const finalAt = nextTime() + const finalAuthority = { + stage: "final", + controller: structuredClone(proposal.record.controller), + annotatedTag: { + ...structuredClone(proposal.record.annotatedTag), + observedAt: finalAt, + }, + workflowAuthority: { + ...structuredClone(proposal.record.workflowAuthority), + observedAt: finalAt, + }, + npmInventory: { + ...structuredClone(proposal.record.npmInventories[1]), + stage: "final", + startedAt: finalAt, + completedAt: finalAt, + packages: proposal.record.npmInventories[1].packages.map((entry) => ({ + ...structuredClone(entry), + observedAt: finalAt, + })), + }, + releases: [structuredClone(proposal.record.releases[0])], + payloadProof: structuredClone(proposal.record.payloadProof), + targetRead: null, + observedAt: finalAt, + } + journal = appendJournalEvent( + journal, + "final-authority-observed", + { authority: finalAuthority }, + finalAt, + ) + const receipt = createFinalConsolidationReceipt({ + proposedEnvelope: proposal, + journalEnvelope: journal, + finalAuthority, + completedAt: finalAt, + }) + const repositoryRoot = path.dirname(path.dirname(path.dirname(deletion.input.journalPath))) + const receiptPath = path.join( + repositoryRoot, + "scripts/release/duplicate-draft-consolidation.json", + ) + await mkdir(path.dirname(receiptPath), { recursive: true }) + await writeTrackedReceipt(receiptPath, canonicalConsolidationEnvelopeBytes("final", receipt)) + const harness = await verificationFixture(t, { + performed: { receiptPath, dependencies: { repositoryRoot } }, + }) + + return { harness, receipt } +} + +test("verify accepts a legal retry and stage-two history with volatile service evidence", async (t) => { + const { harness, receipt } = await legalRetryVerificationFixture(t) + + const result = await verifyDuplicateDraftConsolidation( + { receipt: "scripts/release/duplicate-draft-consolidation.json" }, + harness.dependencies, + ) + assert.equal(result.status, "verified") + assert.deepEqual( + receipt.record.journalEnvelope.record.events + .filter(({ event }) => event.type === "delete-intent") + .map(({ event }) => event.payload.attemptNumber), + [1, 2, 1], + ) +}) + +test("verify rejects every receipt, embedded-envelope, event-chain, evidence, and identity tamper before claims", async (t) => { + const cases = [ + [ + "outer digest", + (receipt) => { + receipt.recordSha256 = "f".repeat(64) + }, + ], + [ + "embedded proposal digest", + (receipt) => { + receipt.record.proposedEnvelope.recordSha256 = "f".repeat(64) + rehashTestEnvelope(receipt) + }, + ], + [ + "embedded journal digest", + (receipt) => { + receipt.record.journalEnvelope.recordSha256 = "f".repeat(64) + rehashTestEnvelope(receipt) + }, + ], + [ + "event digest", + (receipt) => { + receipt.record.journalEnvelope.record.events[1].eventSha256 = "f".repeat(64) + rehashTestEnvelope(receipt.record.journalEnvelope) + rehashTestEnvelope(receipt) + }, + ], + [ + "event previous link", + (receipt) => { + const events = receipt.record.journalEnvelope.record.events + events[1] = canonicalEventEnvelope( + { ...events[1].event, previousEventSha256: "f".repeat(64) }, + "f".repeat(64), + ) + rehashTestEnvelope(receipt.record.journalEnvelope) + rehashTestEnvelope(receipt) + }, + ], + [ + "journal truncation", + (receipt) => { + receipt.record.journalEnvelope.record.events.pop() + receipt.record.journalEnvelope.record.updatedAt = + receipt.record.journalEnvelope.record.events.at(-1).event.recordedAt + rehashTestEnvelope(receipt.record.journalEnvelope) + rehashTestEnvelope(receipt) + }, + ], + [ + "proposal evidence", + (receipt) => { + receipt.record.proposedEnvelope.record.releases[0].semantic.name = "tampered survivor" + rebindTestProposal(receipt) + }, + ], + [ + "final authority evidence", + (receipt) => { + const authority = receipt.record.finalAuthority + authority.releases[0].assets[0].label = "tampered" + receipt.record.finalSurvivor = structuredClone(authority.releases[0]) + receipt.record.journalEnvelope.record.events.at(-1).event.payload.authority = + structuredClone(authority) + rechainTestJournal(receipt.record.journalEnvelope) + rehashTestEnvelope(receipt) + }, + ], + [ + "intermediate authority evidence", + (receipt) => { + const event = receipt.record.journalEnvelope.record.events.find( + ({ event: candidate }) => candidate.type === "delete-authority-observed", + ).event + event.payload.authority.releases[0].semantic.name = "tampered intermediate survivor" + rechainTestJournal(receipt.record.journalEnvelope) + rehashTestEnvelope(receipt) + }, + ], + [ + "intermediate asset identity", + (receipt) => { + const event = receipt.record.journalEnvelope.record.events.find( + ({ event: candidate }) => candidate.type === "delete-authority-observed", + ).event + event.payload.authority.releases[0].assets[0].id = "999999999" + rechainTestJournal(receipt.record.journalEnvelope) + rehashTestEnvelope(receipt) + }, + ], + [ + "controller identity", + (receipt) => { + receipt.record.proposedEnvelope.record.controller = { + headSha: "c".repeat(40), + originMainSha: "c".repeat(40), + githubMainSha: "c".repeat(40), + } + receipt.record.journalEnvelope.record.events[0].event.payload.controllerSha = "c".repeat(40) + rebindTestProposal(receipt) + }, + ], + [ + "repository identity", + (receipt) => { + receipt.record.proposedEnvelope.record.repository.name = "other/repo" + receipt.record.journalEnvelope.record.repository.name = "other/repo" + rebindTestProposal(receipt) + }, + ], + [ + "confirmation binding", + (receipt) => { + receipt.record.journalEnvelope.record.confirmationSha256 = "f".repeat(64) + receipt.record.journalEnvelope.record.events[0].event.payload.confirmationSha256 = + "f".repeat(64) + rechainTestJournal(receipt.record.journalEnvelope) + rehashTestEnvelope(receipt) + }, + ], + ] + + for (const [name, mutate] of cases) { + await t.test(name, async (t) => { + const harness = await verificationFixture(t) + const tampered = structuredClone(harness.receipt) + mutate(tampered) + await writeFile(harness.receiptPath, `${JSON.stringify(tampered)}\n`) + await assert.rejects( + verifyDuplicateDraftConsolidation( + { receipt: "scripts/release/duplicate-draft-consolidation.json" }, + harness.dependencies, + ), + /failed/iu, + ) + assert.deepEqual(harness.calls, []) + assert.equal(harness.writerCalls, 0) + }) + } +}) + +test("verify rejects fully rehashed self-consistent historical semantic tampering before live composition", async (t) => { + const cases = [ + [ + "one duplicate semantic projection", + (receipt) => { + mutateHistoricalEvidence(receipt, DUPLICATE_DRAFT_IDS[0], (evidence) => { + evidence.semantic.name = "self-consistent changed duplicate" + }) + }, + ], + [ + "one duplicate asset projection", + (receipt) => { + mutateHistoricalEvidence(receipt, DUPLICATE_DRAFT_IDS[0], (evidence) => { + evidence.assets[0].label = "self-consistent changed asset" + }) + }, + ], + [ + "payload proof digest", + (receipt) => { + receipt.record.proposedEnvelope.record.payloadProof.consolidationPayloadSha256 = "f".repeat( + 64, + ) + for (const { event } of receipt.record.journalEnvelope.record.events) { + if ( + event.type === "delete-authority-observed" || + event.type === "final-authority-observed" + ) { + event.payload.authority.payloadProof.consolidationPayloadSha256 = "f".repeat(64) + } + } + }, + ], + [ + "targetRead semantic evidence and digest", + (receipt) => { + const event = receipt.record.journalEnvelope.record.events.find( + ({ event: candidate }) => candidate.type === "delete-authority-observed", + ).event + const targetId = event.payload.targetReleaseId + const target = event.payload.authority.releases.find(({ id }) => id === targetId) + target.semantic.name = "self-consistent changed target" + event.payload.authority.targetRead.evidence.semantic.name = target.semantic.name + event.payload.authority.targetRead.evidenceSha256 = canonicalRecordSha256( + event.payload.authority.targetRead.evidence, + ) + }, + ], + [ + "authority stage Release set", + (receipt) => { + const event = receipt.record.journalEnvelope.record.events.find( + ({ event: candidate }) => + candidate.type === "delete-authority-observed" && + candidate.payload.authority.stage === "pre-delete-2", + ).event + event.payload.authority.releases.push( + structuredClone(receipt.record.proposedEnvelope.record.releases[1]), + ) + }, + ], + [ + "proposal and full history pair", + (receipt) => { + for (const releaseId of [DUPLICATE_DRAFT_SURVIVOR_ID, ...DUPLICATE_DRAFT_IDS]) { + mutateHistoricalEvidence(receipt, releaseId, (evidence) => { + evidence.semantic.name = "self-consistent alternate release title" + }) + } + const proof = receipt.record.proposedEnvelope.record.payloadProof + proof.consolidationPayloadSha256 = currentConsolidationPayloadSha256( + receipt.record.proposedEnvelope.record.releases, + ) + for (const { event } of receipt.record.journalEnvelope.record.events) { + if ( + event.type === "delete-authority-observed" || + event.type === "final-authority-observed" + ) { + event.payload.authority.payloadProof = structuredClone(proof) + } + } + }, + ], + [ + "base asset proof", + (receipt) => { + const proof = receipt.record.proposedEnvelope.record.payloadProof + const assetName = proof.baseAssetSet[0].name + const changedSha256 = "f".repeat(64) + for (const releaseId of [DUPLICATE_DRAFT_SURVIVOR_ID, ...DUPLICATE_DRAFT_IDS]) { + mutateHistoricalEvidence(receipt, releaseId, (evidence) => { + const asset = evidence.assets.find(({ name }) => name === assetName) + asset.digest = `sha256:${changedSha256}` + asset.downloadSha256 = changedSha256 + }) + } + proof.baseAssetSet[0].sha256 = changedSha256 + proof.baseAssetSetSha256 = canonicalRecordSha256(proof.baseAssetSet) + proof.consolidationPayloadSha256 = currentConsolidationPayloadSha256( + receipt.record.proposedEnvelope.record.releases, + ) + for (const { event } of receipt.record.journalEnvelope.record.events) { + if ( + event.type === "delete-authority-observed" || + event.type === "final-authority-observed" + ) { + event.payload.authority.payloadProof = structuredClone(proof) + } + } + }, + ], + [ + "attestation proof", + (receipt) => { + const proof = receipt.record.proposedEnvelope.record.payloadProof + proof.attestationVerification.subjects[0].sha256 = "f".repeat(64) + for (const { event } of receipt.record.journalEnvelope.record.events) { + if ( + event.type === "delete-authority-observed" || + event.type === "final-authority-observed" + ) { + event.payload.authority.payloadProof = structuredClone(proof) + } + } + }, + ], + ] + + for (const [name, mutate] of cases) { + await t.test(name, async (t) => { + const harness = await verificationFixture(t) + const tampered = structuredClone(harness.receipt) + mutate(tampered) + rebuildSelfConsistentTestReceipt(tampered) + await writeFile(harness.receiptPath, `${JSON.stringify(tampered)}\n`) + await assert.rejects( + verifyDuplicateDraftConsolidation( + { receipt: "scripts/release/duplicate-draft-consolidation.json" }, + harness.dependencies, + ), + /failed/iu, + ) + assert.deepEqual(harness.calls, []) + assert.equal(harness.writerCalls, 0) + }) + } +}) + +test("verify rejects fully rehashed historical chronology contradictions before live composition", async (t) => { + const cases = [ + [ + "reversed npm interval", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "npm-observed").event + event.payload.inventory.startedAt = after(event.payload.inventory.completedAt) + }, + ], + [ + "npm package outside interval", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "npm-observed").event + event.payload.inventory.packages[0].observedAt = after(event.payload.inventory.completedAt) + }, + ], + [ + "authority observation after event", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "delete-authority-observed").event + event.payload.authority.observedAt = after(event.recordedAt) + }, + ], + [ + "authority component after authority", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "delete-authority-observed").event + event.payload.authority.annotatedTag.observedAt = after(event.payload.authority.observedAt) + }, + ], + [ + "authority npm after authority", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "delete-authority-observed").event + const timestamp = after(event.payload.authority.observedAt) + event.payload.authority.npmInventory.completedAt = timestamp + event.payload.authority.npmInventory.packages.at(-1).observedAt = timestamp + }, + ], + [ + "release evidence after authority", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "delete-authority-observed").event + event.payload.authority.releases[0].updatedAt = after(event.payload.authority.observedAt) + }, + ], + [ + "target read reversal", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "delete-authority-observed").event + event.payload.authority.targetRead.releaseGetStartedAt = after( + event.payload.authority.targetRead.releaseGetCompletedAt, + ) + }, + ], + [ + "target read after authority", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "delete-authority-observed").event + event.payload.authority.targetRead.assetsListCompletedAt = after( + event.payload.authority.observedAt, + ) + }, + ], + [ + "delete outcome after event", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "delete-outcome").event + event.payload.observedAt = after(event.recordedAt) + }, + ], + [ + "resume reconciliation after event", + "retry", + (events) => { + const event = events.find(({ event }) => event.type === "resume-reconciliation").event + event.payload.observedAt = after(event.recordedAt) + }, + ], + [ + "absence direct after list", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "absence-converged").event + event.payload.directGet404At = after(event.payload.listAbsentAt) + }, + ], + [ + "absence list after completion", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "absence-converged").event + event.payload.listAbsentAt = after(event.payload.completedAt) + }, + ], + [ + "absence completion after event", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "absence-converged").event + event.payload.completedAt = after(event.recordedAt) + }, + ], + [ + "event recordedAt reversal", + "normal", + (events) => { + const index = events.findIndex(({ event }) => event.type === "delete-intent") + events[index].event.recordedAt = before(events[index - 1].event.recordedAt) + }, + ], + [ + "final authority observation after event", + "normal", + (events) => { + const event = events.find(({ event }) => event.type === "final-authority-observed").event + event.payload.authority.observedAt = after(event.recordedAt) + }, + ], + ] + + for (const [name, fixtureKind, mutate] of cases) { + await t.test(name, async (t) => { + const harness = + fixtureKind === "retry" + ? (await legalRetryVerificationFixture(t)).harness + : await verificationFixture(t) + const tampered = structuredClone(harness.receipt) + mutate(tampered.record.journalEnvelope.record.events) + rebuildUncheckedChronologyReceipt(tampered) + await writeFile(harness.receiptPath, `${JSON.stringify(tampered)}\n`) + await assert.rejects( + verifyDuplicateDraftConsolidation( + { receipt: "scripts/release/duplicate-draft-consolidation.json" }, + harness.dependencies, + ), + /failed/iu, + ) + assert.deepEqual(harness.calls, []) + assert.equal(harness.writerCalls, 0) + }) + } +}) + +test("verify stops on deleted-ID presence, list disagreement, survivor drift, or final authority drift without mutation", async (t) => { + for (const drift of [ + "deleted-present", + "deleted-listed", + "extra-managed", + "survivor", + "asset", + "main", + "workflow", + "run", + "tag", + "npm", + ]) { + await t.test(drift, async (t) => { + const harness = await verificationFixture(t, { drift }) + await assert.rejects( + verifyDuplicateDraftConsolidation( + { receipt: "scripts/release/duplicate-draft-consolidation.json" }, + harness.dependencies, + ), + /failed/iu, + ) + assert.equal(harness.writerCalls, 0) + }) + } +}) + +test("verify applies the tracked-receipt nofollow and non-writable source-file policy before adapters", async (t) => { + for (const kind of ["symlink", "group-writable"]) { + await t.test(kind, async (t) => { + const harness = await verificationFixture(t) + if (kind === "symlink") { + await rm(harness.receiptPath) + await symlink(path.join(harness.dependencies.repositoryRoot, OUTPUT), harness.receiptPath) + } else { + await chmod(harness.receiptPath, 0o664) + } + await assert.rejects( + verifyDuplicateDraftConsolidation( + { receipt: "scripts/release/duplicate-draft-consolidation.json" }, + harness.dependencies, + ), + /failed/iu, + ) + assert.deepEqual(harness.calls, []) + assert.equal(harness.writerCalls, 0) + }) + } +}) + +test("perform durably completes both targets in order and writes the canonical final receipt", async (t) => { + const harness = await performFixture(t) + const result = await performDuplicateDraftConsolidation(harness.input, harness.dependencies) + + assert.deepEqual(harness.calls, [ + "perform-initial", + "delete:379982100", + "delete:379986168", + "final", + "receipt", + ]) + assert.deepEqual(result, { + status: "complete", + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + deleted: [...DUPLICATE_DRAFT_IDS], + receipt: "scripts/release/duplicate-draft-consolidation.json", + receiptSha256: result.receiptSha256, + }) + assert.match(result.receiptSha256, /^[0-9a-f]{64}$/u) + const receipt = parseConsolidationEnvelope( + "final", + await readTrackedReceipt( + harness.receiptPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes, + ), + ) + assert.equal(receipt.record.journalEnvelope.record.events[0].event.type, "operation-started") + assert.deepEqual( + receipt.record.journalEnvelope.record.events + .filter(({ event }) => event.type === "absence-converged") + .map(({ event }) => event.payload.targetReleaseId), + [...DUPLICATE_DRAFT_IDS], + ) + assert.equal( + receipt.record.journalEnvelope.record.events.at(-1).event.type, + "final-authority-observed", + ) + assert.equal(receipt.record.finalSurvivor.id, DUPLICATE_DRAFT_SURVIVOR_ID) + assert.deepEqual( + [ + ...receipt.record.proposedEnvelope.record.npmInventories.map(({ stage }) => stage), + ...receipt.record.journalEnvelope.record.events.flatMap(({ event }) => { + if (event.type === "npm-observed") return [event.payload.inventory.stage] + if (event.type === "delete-authority-observed") { + return [event.payload.authority.npmInventory.stage] + } + if (event.type === "final-authority-observed") { + return [event.payload.authority.npmInventory.stage] + } + return [] + }), + ], + [ + "inspect-initial", + "inspect-ready", + "perform-initial", + "pre-delete-1", + "pre-delete-2", + "final", + ], + ) +}) + +test("perform binds the exact proposal digest and confirmation before any operation", async (t) => { + const harness = await performFixture(t) + const wrongDigest = "f".repeat(64) + for (const input of [ + { + ...harness.input, + proposalSha256: wrongDigest, + confirmation: harness.input.confirmation.replace(harness.proposal.recordSha256, wrongDigest), + }, + { ...harness.input, confirmation: `${harness.input.confirmation} altered` }, + ]) { + await assert.rejects( + performDuplicateDraftConsolidation(input, harness.dependencies), + /failed/iu, + ) + } + const changedProposal = createConsolidationEnvelope("proposed", { + ...harness.proposal.record, + inspectedAt: new Date(Date.parse(harness.proposal.record.inspectedAt) + 1_000).toISOString(), + }) + await writePrivateEnvelope( + path.join(harness.dependencies.repositoryRoot, OUTPUT), + canonicalConsolidationEnvelopeBytes("proposed", changedProposal), + ) + await assert.rejects( + performDuplicateDraftConsolidation(harness.input, harness.dependencies), + /failed/iu, + ) + assert.deepEqual(harness.calls, []) +}) + +test("perform refuses journal genesis when a prior durable head survives", async (t) => { + const harness = await performFixture(t) + await writePrivateEnvelope( + harness.journalPath.replace(/journal\.json$/u, "journal.head.json"), + Buffer.from("orphan durable head\n", "utf8"), + ) + await assert.rejects( + performDuplicateDraftConsolidation(harness.input, harness.dependencies), + /failed/iu, + ) + assert.deepEqual(harness.calls, []) +}) + +test("perform resumes a failed receipt publication without another DELETE and rematerializes identical bytes", async (t) => { + const harness = await performFixture(t, { failReceiptOnce: true }) + await assert.rejects( + performDuplicateDraftConsolidation(harness.input, harness.dependencies), + /failed/iu, + ) + const finalJournal = await readPrivateEnvelope( + harness.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ) + const expected = createFinalConsolidationReceipt({ + proposedEnvelope: harness.proposal, + journalEnvelope: parseConsolidationEnvelope("journal", finalJournal), + finalAuthority: deriveConsolidationState(finalJournal).lastAuthority, + completedAt: deriveConsolidationState(finalJournal).lastAuthority.observedAt, + }) + const result = await performDuplicateDraftConsolidation(harness.input, harness.dependencies) + assert.equal(result.receiptSha256, expected.recordSha256) + assert.equal(harness.calls.filter((entry) => entry.startsWith("delete:")).length, 2) + assert.equal(harness.calls.filter((entry) => entry === "final").length, 2) +}) + +test("perform receipt resume freshly rechecks final authority and stops on every live drift", async (t) => { + for (const drift of [ + "main", + "npm-publication", + "survivor", + "asset", + "duplicate-reappeared", + "extra-release", + "workflow", + "run", + "tag", + ]) { + await t.test(drift, async (t) => { + const harness = await performFixture(t, { + failReceiptOnce: true, + resumeFinalDrift: drift, + }) + await assert.rejects( + performDuplicateDraftConsolidation(harness.input, harness.dependencies), + /failed/iu, + ) + await assert.rejects( + performDuplicateDraftConsolidation(harness.input, harness.dependencies), + /failed/iu, + ) + assert.equal(harness.calls.filter((entry) => entry.startsWith("delete:")).length, 2) + assert.equal(harness.calls.filter((entry) => entry === "receipt").length, 1) + }) + } +}) + +test("perform accepts an existing byte-identical canonical receipt without overwrite after fresh validation", async (t) => { + const harness = await performFixture(t) + const first = await performDuplicateDraftConsolidation(harness.input, harness.dependencies) + const durable = await readFile(harness.receiptPath) + const second = await performDuplicateDraftConsolidation(harness.input, harness.dependencies) + assert.deepEqual(second, first) + assert.equal(harness.calls.filter((entry) => entry === "final").length, 2) + assert.equal(harness.calls.filter((entry) => entry === "receipt").length, 1) + assert.deepEqual(await readFile(harness.receiptPath), durable) +}) + +test("CLI performs the complete production-composed consolidation using only external service fakes", async (t) => { + const harness = await productionPerformRehearsalFixture(t) + const code = await runDuplicateDraftConsolidationCli(harness.options) + assert.equal(code, 0, harness.stderr.value) + assert.equal(harness.stderr.value, "") + assert.deepEqual(harness.deleteIds, [...DUPLICATE_DRAFT_IDS]) + assert.equal(harness.deleteIds.includes(DUPLICATE_DRAFT_SURVIVOR_ID), false) + + const firstDelete = harness.events.indexOf(`delete:${DUPLICATE_DRAFT_IDS[0]}`) + const secondDelete = harness.events.indexOf(`delete:${DUPLICATE_DRAFT_IDS[1]}`) + assert.ok(firstDelete > 0) + assert.ok(secondDelete > firstDelete) + const downloads = (from, to) => + harness.events.slice(from, to).filter((entry) => entry.startsWith("download:")).length + assert.equal(downloads(0, firstDelete), 270) + assert.equal(downloads(firstDelete + 1, secondDelete), 90) + assert.equal(downloads(secondDelete + 1), 45) + for (const [from, to, expectedIds] of [ + [0, firstDelete, [DUPLICATE_DRAFT_SURVIVOR_ID, ...DUPLICATE_DRAFT_IDS]], + [firstDelete + 1, secondDelete, [DUPLICATE_DRAFT_SURVIVOR_ID, DUPLICATE_DRAFT_IDS[1]]], + [secondDelete + 1, harness.events.length, [DUPLICATE_DRAFT_SURVIVOR_ID]], + ]) { + const sets = harness.events + .slice(from, to) + .filter((entry) => entry.startsWith("releases:")) + .map((entry) => entry.slice("releases:".length)) + assert.ok(sets.length > 0) + assert.equal( + sets.every((entry) => entry === expectedIds.join(",")), + true, + ) + } + assert.equal(harness.events.filter((entry) => entry === "local-main").length, 3) + assert.equal(harness.events.filter((entry) => entry === "github-main").length, 3) + assert.equal(harness.events.filter((entry) => entry === "npm").length, 84) + + const receiptBytes = await readTrackedReceipt( + harness.receiptPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes, + ) + const receipt = parseConsolidationEnvelope("final", receiptBytes) + assert.deepEqual( + [ + ...receipt.record.proposedEnvelope.record.npmInventories.map(({ stage }) => stage), + ...receipt.record.journalEnvelope.record.events.flatMap(({ event }) => { + if (event.type === "npm-observed") return [event.payload.inventory.stage] + if (event.type === "delete-authority-observed") { + return [event.payload.authority.npmInventory.stage] + } + if (event.type === "final-authority-observed") { + return [event.payload.authority.npmInventory.stage] + } + return [] + }), + ], + [ + "inspect-initial", + "inspect-ready", + "perform-initial", + "pre-delete-1", + "pre-delete-2", + "final", + ], + ) + assert.equal((await stat(harness.receiptPath)).mode & 0o777, 0o644) + assert.deepEqual(JSON.parse(harness.stdout.value), { + status: "complete", + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + deleted: [...DUPLICATE_DRAFT_IDS], + receipt: "scripts/release/duplicate-draft-consolidation.json", + receiptSha256: receipt.recordSha256, + }) + + const beforeVerify = harness.events.length + const deletesBeforeVerify = [...harness.deleteIds] + const currentAssetIds = harness.volatilizeSurvivorAssets() + harness.stdout.value = "" + harness.options.argv = [ + "verify", + "--receipt", + "scripts/release/duplicate-draft-consolidation.json", + ] + assert.equal(await runDuplicateDraftConsolidationCli(harness.options), 0, harness.stderr.value) + assert.deepEqual(harness.deleteIds, deletesBeforeVerify) + const verifyEvents = harness.events.slice(beforeVerify) + assert.equal( + verifyEvents.filter((entry) => entry === `get-release:${DUPLICATE_DRAFT_IDS[0]}`).length, + 1, + ) + assert.equal( + verifyEvents.filter((entry) => entry === `get-release:${DUPLICATE_DRAFT_IDS[1]}`).length, + 1, + ) + assert.deepEqual( + verifyEvents + .filter((entry) => entry.startsWith(`download:${DUPLICATE_DRAFT_SURVIVOR_ID}:`)) + .map((entry) => entry.split(":").at(-1)), + currentAssetIds, + ) + assert.equal(verifyEvents.filter((entry) => entry === "npm").length, 21) + assert.equal(verifyEvents.filter((entry) => entry === "attestations").length, 1) + assert.deepEqual(JSON.parse(harness.stdout.value), { + status: "verified", + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + deleted: [...DUPLICATE_DRAFT_IDS], + receipt: "scripts/release/duplicate-draft-consolidation.json", + receiptSha256: receipt.recordSha256, + historicalParity: + "Historical duplicate payload parity is supported by embedded pre-delete evidence plus the currently reverified survivor; deleted bytes were not independently re-downloaded.", + }) +}) + +test("production-composed CLI stops after target one when main advances before target two", async (t) => { + const harness = await productionPerformRehearsalFixture(t, { + driftMainAfterFirstDelete: true, + }) + assert.equal(await runDuplicateDraftConsolidationCli(harness.options), 1) + assert.deepEqual(harness.deleteIds, [DUPLICATE_DRAFT_IDS[0]]) + assert.equal(harness.stderr.value, "Duplicate-draft perform failed.\n") + await assert.rejects(readFile(harness.receiptPath), /ENOENT/iu) +}) + +test("perform recovers post-rename receipt ambiguity only after fresh final validation", async (t) => { + const harness = await performFixture(t, { + postRenameReceiptFailureOnce: true, + }) + await assert.rejects( + performDuplicateDraftConsolidation(harness.input, harness.dependencies), + /failed/iu, + ) + const durable = await readFile(harness.receiptPath) + const result = await performDuplicateDraftConsolidation(harness.input, harness.dependencies) + assert.match(result.receiptSha256, /^[0-9a-f]{64}$/u) + assert.equal(harness.calls.filter((entry) => entry.startsWith("delete:")).length, 2) + assert.equal(harness.calls.filter((entry) => entry === "final").length, 2) + assert.equal(harness.calls.filter((entry) => entry === "receipt").length, 1) + assert.deepEqual(await readFile(harness.receiptPath), durable) +}) + +test("perform rejects an exact durable receipt when its mandatory fresh final recheck drifts", async (t) => { + const harness = await performFixture(t, { resumeFinalDrift: "main" }) + await performDuplicateDraftConsolidation(harness.input, harness.dependencies) + const durable = await readFile(harness.receiptPath) + await assert.rejects( + performDuplicateDraftConsolidation(harness.input, harness.dependencies), + /failed/iu, + ) + assert.equal(harness.calls.filter((entry) => entry.startsWith("delete:")).length, 2) + assert.equal(harness.calls.filter((entry) => entry === "receipt").length, 1) + assert.deepEqual(await readFile(harness.receiptPath), durable) +}) + +test("perform never overwrites malformed, different canonical, or unsafe existing receipts", async (t) => { + for (const kind of ["malformed", "different", "unsafe"]) { + await t.test(kind, async (t) => { + const harness = await performFixture(t) + await performDuplicateDraftConsolidation(harness.input, harness.dependencies) + const beforeCalls = [...harness.calls] + if (kind === "malformed") { + await writeTrackedReceipt(harness.receiptPath, Buffer.from("malformed\n", "utf8")) + } else if (kind === "different") { + const receipt = parseConsolidationEnvelope( + "final", + await readTrackedReceipt( + harness.receiptPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes, + ), + ) + const different = createConsolidationEnvelope("final", { + ...receipt.record, + completedAt: new Date(Date.parse(receipt.record.completedAt) + 1_000).toISOString(), + }) + await writeTrackedReceipt( + harness.receiptPath, + canonicalConsolidationEnvelopeBytes("final", different), + ) + } else { + await rm(harness.receiptPath) + await symlink(harness.proposalPath, harness.receiptPath) + } + await assert.rejects( + performDuplicateDraftConsolidation(harness.input, harness.dependencies), + /failed/iu, + ) + assert.deepEqual(harness.calls, beforeCalls) + }) + } +}) + +test("perform rejects a canonical completed history that omitted perform-initial proof", async (t) => { + const harness = await performFixture(t) + const confirmationSha256 = createHash("sha256") + .update(harness.input.confirmation, "utf8") + .digest("hex") + const genesis = createConsolidationJournal({ + proposedEnvelope: harness.proposal, + confirmationSha256, + recordedAt: harness.proposal.record.inspectedAt, + }) + await harness.persistJournal(genesis) + await harness.dependencies.performOneDeletion({ + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + }) + await harness.dependencies.performOneDeletion({ + targetReleaseId: DUPLICATE_DRAFT_IDS[1], + }) + harness.calls.length = 0 + await assert.rejects( + performDuplicateDraftConsolidation(harness.input, harness.dependencies), + /failed/iu, + ) + assert.deepEqual(harness.calls, []) +}) + +test("journal replay rejects reordered, duplicate, and wrong-target perform-initial histories", async (t) => { + const harness = await performFixture(t) + const confirmationSha256 = createHash("sha256") + .update(harness.input.confirmation, "utf8") + .digest("hex") + const genesis = createConsolidationJournal({ + proposedEnvelope: harness.proposal, + confirmationSha256, + recordedAt: harness.proposal.record.inspectedAt, + }) + const inventory = harness.performInventory() + const correct = appendJournalEvent( + genesis, + "npm-observed", + { targetReleaseId: DUPLICATE_DRAFT_IDS[0], attemptNumber: 1, inventory }, + inventory.completedAt, + ) + assert.throws( + () => + appendJournalEvent( + correct, + "npm-observed", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + inventory, + }, + inventory.completedAt, + ), + /npm|state|legal/iu, + ) + assert.throws( + () => + appendJournalEvent( + genesis, + "npm-observed", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[1], + attemptNumber: 1, + inventory, + }, + inventory.completedAt, + ), + /target|current/iu, + ) + const authority = harness.deletionAuthority(0) + assert.throws( + () => + appendJournalEvent( + appendJournalEvent( + genesis, + "delete-authority-observed", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + authority, + }, + authority.observedAt, + ), + "npm-observed", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + inventory, + }, + new Date(Date.parse(authority.observedAt) + 1_000).toISOString(), + ), + /npm|state|legal/iu, + ) +}) + +test("perform resumes the durable initial npm stage by repeating payload proof before any DELETE", async (t) => { + const harness = await performFixture(t, { + failInitialVerificationOnce: true, + }) + await assert.rejects( + performDuplicateDraftConsolidation(harness.input, harness.dependencies), + /failed/iu, + ) + assert.deepEqual(harness.calls, ["perform-initial", "verify-initial"]) + await performDuplicateDraftConsolidation(harness.input, harness.dependencies) + assert.equal(harness.calls.filter((entry) => entry === "perform-initial").length, 1) + assert.equal(harness.calls.filter((entry) => entry === "verify-initial").length, 2) + assert.equal(harness.calls.filter((entry) => entry.startsWith("delete:")).length, 2) +}) + +test("perform stops between targets on main, publication, survivor, or managed-set drift", async (t) => { + for (const drift of ["main", "publication", "survivor", "fourth-draft"]) { + await t.test(drift, async (t) => { + const harness = await performFixture(t, { failSecondTarget: drift }) + await assert.rejects( + performDuplicateDraftConsolidation(harness.input, harness.dependencies), + /failed/iu, + ) + assert.deepEqual( + harness.calls.filter((entry) => entry.startsWith("delete:")), + ["delete:379982100"], + ) + assert.equal(harness.calls.includes("receipt"), false) + }) + } +}) + +test("one-target deletion rejects a numeric or survivor target before creating network adapters", async () => { + let adapterCreations = 0 + const dependencies = Object.freeze({ + async createAdapters() { + adapterCreations += 1 + throw new Error("network adapter creation must not be reached") + }, + async wait() { + assert.fail("wait must not be reached") + }, + }) + for (const targetReleaseId of [379982100, DUPLICATE_DRAFT_SURVIVOR_ID]) { + await assert.rejects( + performOneDuplicateDeletion( + { + proposedEnvelope: {}, + confirmation: "invalid", + targetReleaseId, + journalPath: "/tmp/duplicate-draft-consolidation.journal.json", + }, + dependencies, + ), + /failed/iu, + ) + } + assert.equal(adapterCreations, 0) +}) + +test("one-target deletion durably orders authority, intent, confirmed outcome, and two-source convergence", async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["confirmed-204"], + }) + const result = await performOneDuplicateDeletion(harness.input, harness.dependencies) + + assert.deepEqual(result, { + status: "converged", + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + basis: "confirmed-204", + }) + assert.deepEqual(harness.events, [ + "authority:pre-delete-1:379982100", + "durable:authority", + "durable:intent", + "delete:379982100", + "durable:outcome:confirmed-204", + "direct:379982100", + "list", + ]) + const journal = await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ) + const state = deriveConsolidationState(journal) + assert.equal(state.phase, "target-converged") + assert.deepEqual(state.completedTargets, [DUPLICATE_DRAFT_IDS[0]]) + assert.equal(state.currentTargetReleaseId, DUPLICATE_DRAFT_IDS[1]) + assert.deepEqual( + parseConsolidationEnvelope("journal", journal).record.events.map(({ event }) => event.type), + [ + "operation-started", + "delete-authority-observed", + "delete-intent", + "delete-outcome", + "absence-converged", + ], + ) +}) + +test("one-target deletion resumes a lost post-DELETE outcome as absent ambiguity without another writer", async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["confirmed-204"], + }) + await assert.rejects( + performOneDuplicateDeletion(harness.input, harness.dependenciesWithFault("after-delete")), + /failed/iu, + ) + const result = await performOneDuplicateDeletion(harness.input, harness.dependencies) + assert.equal(result.basis, "ambiguous") + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + const state = deriveConsolidationState( + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal(state.phase, "target-converged") +}) + +test("one-target deletion gives an unchanged ambiguous target six complete reads before fresh attempt two", async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["transport-ambiguous", "confirmed-204"], + }) + const result = await performOneDuplicateDeletion(harness.input, harness.dependencies) + assert.equal(result.basis, "confirmed-204") + assert.equal(harness.events.filter((entry) => entry === "list").length, 7) + assert.deepEqual( + harness.events.filter((entry) => entry.startsWith("authority:")), + ["authority:pre-delete-1:379982100", "authority:pre-delete-1:379982100"], + ) + const journal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.deepEqual( + journal.record.events + .filter(({ event }) => event.type === "delete-intent") + .map(({ event }) => event.payload.attemptNumber), + [1, 2], + ) +}) + +test("one-target retry persists the actual fresh 45-asset target evidence before binding that same authority", async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["transport-ambiguous", "confirmed-204"], + freshAuthorityMutation: "volatile", + }) + await performOneDuplicateDeletion(harness.input, harness.dependencies) + const journal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + const authorities = journal.record.events.filter( + ({ event }) => event.type === "delete-authority-observed", + ) + const retry = journal.record.events.find( + ({ event }) => + event.type === "resume-reconciliation" && + event.payload.classification === "present-unchanged-retryable", + ) + assert.equal(authorities.length, 2) + assert.equal(retry.event.payload.releaseEvidence.assets.length, 45) + assert.notDeepEqual( + retry.event.payload.releaseEvidence, + authorities[0].event.payload.authority.targetRead.evidence, + ) + assert.deepEqual( + retry.event.payload.releaseEvidence, + authorities[1].event.payload.authority.targetRead.evidence, + ) + assert.ok(journal.record.events.indexOf(retry) < journal.record.events.indexOf(authorities[1])) + const secondCapture = harness.events.lastIndexOf("authority:pre-delete-1:379982100") + assert.deepEqual(harness.events.slice(secondCapture), [ + "authority:pre-delete-1:379982100", + "durable:authority", + "durable:intent", + "delete:379982100", + "durable:outcome:confirmed-204", + "direct:379982100", + "list", + ]) +}) + +test("one-target retry refreshes npm only beyond the exact two-minute boundary", async (t) => { + for (const { ageMs, stale } of [ + { ageMs: 120_000, stale: false }, + { ageMs: 120_001, stale: true }, + ]) { + await t.test(`${ageMs}ms`, async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["transport-ambiguous", "confirmed-204"], + freshAuthorityAdvanceMs: stale ? 61_000 : 1_000, + }) + const timeline = retryWallClockTimeline(harness.authorityTime, ageMs, { + heavyVerificationMs: 20_000, + }) + await performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithWallClockTimeline(timeline), + ) + const journal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + const npmEvents = journal.record.events.filter(({ event }) => event.type === "npm-observed") + assert.equal(npmEvents.length, stale ? 1 : 0) + if (!stale) { + assert.equal( + harness.events.some((entry) => entry.startsWith("npm:")), + false, + ) + return + } + assert.equal(npmEvents[0].event.payload.attemptNumber, 2) + assert.equal(npmEvents[0].event.payload.inventory.stage, "perform-initial") + assert.equal( + harness.events.filter((entry) => entry.startsWith("npm:")).length, + CANONICAL_RELEASE_PACKAGE_ORDER.length, + ) + assert.equal( + harness.events.filter((entry) => entry.startsWith("payload-download:")).length, + 135, + ) + assert.equal(harness.waits.at(-1), 40_000) + const retryAuthorityIndex = harness.events.lastIndexOf("authority:pre-delete-1:379982100") + assert.deepEqual(harness.events.slice(retryAuthorityIndex), [ + "authority:pre-delete-1:379982100", + "durable:authority", + "durable:intent", + "delete:379982100", + "durable:outcome:confirmed-204", + "direct:379982100", + "list", + ]) + }) + } +}) + +test("second-target retry verifies the exact remaining set across the npm freshness boundary", async (t) => { + for (const { ageMs, stale } of [ + { ageMs: 120_000, stale: false }, + { ageMs: 120_001, stale: true }, + ]) { + await t.test(`${ageMs}ms`, async (t) => { + const harness = await oneDeletionFixture(t, { + targetIndex: 1, + deleteClassifications: ["transport-ambiguous", "confirmed-204"], + freshAuthorityAdvanceMs: stale ? 61_000 : 1_000, + }) + await performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithWallClockTimeline( + retryWallClockTimeline(harness.authorityTime, ageMs, { + heavyVerificationMs: 20_000, + }), + ), + ) + + const journalBytes = await readFile(harness.input.journalPath) + assert.ok(journalBytes.byteLength <= DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes) + const journal = parseConsolidationEnvelope("journal", journalBytes) + const npmEvents = journal.record.events.filter(({ event }) => event.type === "npm-observed") + assert.equal(npmEvents.length, stale ? 1 : 0) + assert.equal( + harness.events.filter((entry) => entry.startsWith("payload-download:")).length, + stale ? 90 : 0, + ) + assert.equal( + harness.events.filter((entry) => entry === "payload-attestations").length, + stale ? 2 : 0, + ) + if (stale) { + assert.equal(npmEvents[0].event.payload.inventory.stage, "perform-initial") + assert.equal(harness.waits.at(-1), 40_000) + } + const retryAuthorityIndex = harness.events.lastIndexOf("authority:pre-delete-2:379986168") + assert.deepEqual(harness.events.slice(retryAuthorityIndex), [ + "authority:pre-delete-2:379986168", + "durable:authority", + "durable:intent", + "delete:379986168", + "durable:outcome:confirmed-204", + "direct:379986168", + "list", + ]) + }) + } +}) + +test("second-target stale retry rejects changed, missing, or extra remaining Release evidence", async (t) => { + for (const mutation of ["metadata-included", "asset-included", "missing", "extra"]) { + await t.test(mutation, async (t) => { + const harness = await oneDeletionFixture(t, { + targetIndex: 1, + deleteClassifications: ["transport-ambiguous"], + retryRemainingMutation: mutation, + }) + await assert.rejects( + performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithWallClockTimeline( + retryWallClockTimeline(harness.authorityTime, 120_001), + ), + ), + /failed/iu, + ) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + assert.equal( + harness.events.filter((entry) => entry.startsWith("payload-download:")).length, + mutation === "metadata-included" ? 45 : mutation === "asset-included" ? 90 : 0, + ) + const state = deriveConsolidationState( + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal(state.phase, "npm-observed") + }) + } +}) + +test("second-target stale retry resumes npm durability crashes without replaying the uncertain DELETE", async (t) => { + for (const faultAt of ["after-npm-journal", "after-npm-head"]) { + await t.test(faultAt, async (t) => { + const harness = await oneDeletionFixture(t, { + targetIndex: 1, + deleteClassifications: ["transport-ambiguous", "confirmed-204"], + freshAuthorityAdvanceMs: 61_000, + }) + await assert.rejects( + performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithWallClockTimeline( + retryWallClockTimeline(harness.authorityTime, 120_001), + faultAt, + ), + ), + /failed/iu, + ) + const interrupted = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + const npmEvent = interrupted.record.events.find(({ event }) => event.type === "npm-observed") + assert.ok(npmEvent) + assert.equal(deriveConsolidationState(interrupted).phase, "npm-observed") + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + + const completedAt = Date.parse(npmEvent.event.payload.inventory.completedAt) + await performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithWallClockTimeline( + Object.freeze([ + new Date(completedAt + 30_000).toISOString(), + new Date(completedAt + 60_000).toISOString(), + ]), + ), + ) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 2) + const resumed = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal( + resumed.record.events.filter(({ event }) => event.type === "npm-observed").length, + 1, + ) + }) + } +}) + +test("second-target stale retry rejects a future or reversing wall clock", async (t) => { + for (const scenario of ["future", "reversal"]) { + await t.test(scenario, async (t) => { + const harness = await oneDeletionFixture(t, { + targetIndex: 1, + deleteClassifications: ["transport-ambiguous"], + }) + const authorityMs = Date.parse(harness.authorityTime) + const timeline = + scenario === "future" + ? Object.freeze([new Date(authorityMs - 1).toISOString()]) + : Object.freeze([ + new Date(authorityMs + 120_001).toISOString(), + new Date(authorityMs + 120_000).toISOString(), + ]) + await assert.rejects( + performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithWallClockTimeline(timeline), + ), + /failed/iu, + ) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + }) + } +}) + +test("one-target stale npm retry resumes either npm durability crash without duplicating evidence", async (t) => { + for (const faultAt of ["after-npm-journal", "after-npm-head"]) { + await t.test(faultAt, async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["transport-ambiguous", "confirmed-204"], + freshAuthorityAdvanceMs: 61_000, + }) + await assert.rejects( + performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithWallClockTimeline( + retryWallClockTimeline(harness.authorityTime, 120_001), + faultAt, + ), + ), + /failed/iu, + ) + const interrupted = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + const npmEvent = interrupted.record.events.find(({ event }) => event.type === "npm-observed") + assert.ok(npmEvent, JSON.stringify(harness.events)) + const npmCompletedAt = npmEvent.event.payload.inventory.completedAt + assert.equal(deriveConsolidationState(interrupted).phase, "npm-observed") + + await performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithWallClockTimeline( + Object.freeze([ + new Date(Date.parse(npmCompletedAt) + 30_000).toISOString(), + new Date(Date.parse(npmCompletedAt) + 60_000).toISOString(), + ]), + ), + ) + const resumed = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal( + resumed.record.events.filter(({ event }) => event.type === "npm-observed").length, + 1, + ) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 2) + }) + } +}) + +test("one-target stale npm retry fails closed on wall-clock future, reversal, or payload verification failure", async (t) => { + for (const scenario of ["future", "reversal", "payload"]) { + await t.test(scenario, async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["transport-ambiguous"], + ...(scenario === "payload" ? { retryPayloadFailure: true } : {}), + }) + const authorityMs = Date.parse(harness.authorityTime) + const timeline = + scenario === "future" + ? Object.freeze([new Date(authorityMs - 1).toISOString()]) + : scenario === "reversal" + ? Object.freeze([ + new Date(authorityMs + 120_001).toISOString(), + new Date(authorityMs + 120_000).toISOString(), + ]) + : retryWallClockTimeline(harness.authorityTime, 120_001) + await assert.rejects( + performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithWallClockTimeline(timeline), + ), + /failed/iu, + ) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + const state = deriveConsolidationState( + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal(state.phase, scenario === "payload" ? "npm-observed" : "delete-outcome") + }) + } +}) + +test("one-target retry stops when the fresh full authority capture finds included asset drift", async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["transport-ambiguous"], + freshAuthorityMutation: "asset-included", + }) + await assert.rejects(performOneDuplicateDeletion(harness.input, harness.dependencies), /failed/iu) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + const state = deriveConsolidationState( + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal(state.phase, "delete-outcome") +}) + +test("one-target deletion preserves received 404 ambiguity while converging absence", async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["response-404-ambiguous"], + }) + const result = await performOneDuplicateDeletion(harness.input, harness.dependencies) + assert.equal(result.basis, "ambiguous") + const journal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal( + journal.record.events.find(({ event }) => event.type === "delete-outcome").event.payload + .classification, + "response-404-ambiguous", + ) +}) + +test("one-target deletion persists a hard HTTP outcome and never retries it on resume", async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["response-hard-failure"], + deleteLeavesPresent: true, + }) + + await assert.rejects(performOneDuplicateDeletion(harness.input, harness.dependencies), /failed/iu) + const firstState = deriveConsolidationState( + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal(firstState.phase, "delete-outcome") + assert.equal(firstState.lastOutcomeClassification, "response-hard-failure") + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + const networkReadsBeforeResume = harness.events.filter( + (entry) => entry === "list" || entry.startsWith("direct:"), + ).length + + await assert.rejects(performOneDuplicateDeletion(harness.input, harness.dependencies), /failed/iu) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + assert.equal( + harness.events.filter((entry) => entry === "list" || entry.startsWith("direct:")).length, + networkReadsBeforeResume, + ) +}) + +test("one-target deletion resumes each exact intent, request, outcome, resume, and convergence crash boundary", async (t) => { + const scenarios = [ + ["after-intent-journal", ["confirmed-204"], 1, "confirmed-204", "delete-intent"], + ["after-intent-head", ["confirmed-204"], 1, "confirmed-204", "delete-intent"], + ["before-delete", ["confirmed-204"], 1, "confirmed-204", "delete-intent"], + ["after-delete", ["confirmed-204"], 1, "ambiguous", "delete-intent"], + ["after-outcome-journal", ["confirmed-204"], 1, "confirmed-204", "delete-outcome"], + ["after-outcome-head", ["confirmed-204"], 1, "confirmed-204", "delete-outcome"], + [ + "after-resume-journal", + ["transport-ambiguous", "confirmed-204"], + 2, + "confirmed-204", + "resume-present", + ], + [ + "after-resume-head", + ["transport-ambiguous", "confirmed-204"], + 2, + "confirmed-204", + "resume-present", + ], + ["after-convergence-journal", ["confirmed-204"], 1, "confirmed-204", "target-converged"], + ["after-convergence-head", ["confirmed-204"], 1, "confirmed-204", "target-converged"], + ] + for (const [faultAt, deleteClassifications, deleteCalls, basis, crashPhase] of scenarios) { + await t.test(faultAt, async (t) => { + const harness = await oneDeletionFixture(t, { deleteClassifications }) + await assert.rejects( + performOneDuplicateDeletion(harness.input, harness.dependenciesWithFault(faultAt)), + /failed/iu, + ) + assert.equal( + deriveConsolidationState( + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ).phase, + crashPhase, + ) + const result = await performOneDuplicateDeletion(harness.input, harness.dependencies) + assert.equal(result.status, "converged") + assert.equal(result.basis, basis) + assert.equal( + harness.events.filter((entry) => entry.startsWith("delete:")).length, + deleteCalls, + ) + const journal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal(deriveConsolidationState(journal).phase, "target-converged") + assert.deepEqual( + await readPrivateEnvelope( + harness.input.journalPath.replace(/journal\.json$/u, "journal.head.json"), + 16 * 1024, + ), + testJournalHeadBytes(harness.input.journalPath, journal), + ) + }) + } +}) + +test("one-target deletion supersedes orphan authority after either authority durability crash window", async (t) => { + for (const faultAt of ["after-authority-journal", "after-authority-head"]) { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["confirmed-204"], + }) + await assert.rejects( + performOneDuplicateDeletion(harness.input, harness.dependenciesWithFault(faultAt)), + /failed/iu, + ) + const result = await performOneDuplicateDeletion(harness.input, harness.dependencies) + assert.equal(result.status, "converged") + assert.equal(harness.events.filter((entry) => entry.startsWith("authority:")).length, 2) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + } +}) + +test("one-target deletion stops honestly before a second global orphan-authority recovery", async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["confirmed-204"], + }) + for (let crash = 0; crash < 2; crash += 1) { + await assert.rejects( + performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithFault("after-authority-journal"), + ), + /failed/iu, + ) + } + await assert.rejects(performOneDuplicateDeletion(harness.input, harness.dependencies), /failed/iu) + const journal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + assert.equal( + journal.record.events.filter(({ event }) => event.type === "delete-authority-observed").length, + 2, + ) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 0) +}) + +test("one-target deletion refreshes volatile retry evidence again after either resume durability crash", async (t) => { + for (const faultAt of ["after-resume-journal", "after-resume-head"]) { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["transport-ambiguous", "confirmed-204"], + freshAuthorityMutation: "volatile", + }) + await assert.rejects( + performOneDuplicateDeletion(harness.input, harness.dependenciesWithFault(faultAt)), + /failed/iu, + ) + const result = await performOneDuplicateDeletion(harness.input, harness.dependencies) + assert.equal(result.status, "converged") + const journal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope( + harness.input.journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ), + ) + const retry = journal.record.events.find( + ({ event }) => + event.type === "resume-reconciliation" && + event.payload.classification === "present-unchanged-retryable", + ) + const retryAuthority = journal.record.events.find( + ({ event }) => + event.type === "delete-authority-observed" && event.payload.attemptNumber === 2, + ) + assert.notDeepEqual( + retry.event.payload.releaseEvidence, + retryAuthority.event.payload.authority.targetRead.evidence, + ) + } +}) + +test("one-target deletion resumes either convergence durability crash window from its completed target", async (t) => { + for (const faultAt of ["after-convergence-journal", "after-convergence-head"]) { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["confirmed-204"], + }) + await assert.rejects( + performOneDuplicateDeletion(harness.input, harness.dependenciesWithFault(faultAt)), + /failed/iu, + ) + const networkEventsBeforeResume = harness.events.length + const result = await performOneDuplicateDeletion(harness.input, harness.dependencies) + assert.deepEqual(result, { + status: "converged", + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + basis: "confirmed-204", + }) + assert.equal(harness.events.length, networkEventsBeforeResume) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + } +}) + +test("one-target deletion stops on third ambiguity and never mints a fourth writer permit", async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["transport-ambiguous", "transport-ambiguous", "transport-ambiguous"], + }) + await assert.rejects(performOneDuplicateDeletion(harness.input, harness.dependencies), /failed/iu) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 3) +}) + +test("one-target deletion stops on reader disagreement, changed evidence, confirmed-204 presence, and bounded read errors", async (t) => { + for (const scenario of [ + { deleteLeavesPresent: true }, + { retainDeletedInList: true }, + { + deleteClassifications: ["transport-ambiguous"], + currentMutation: "changed", + }, + { + deleteClassifications: ["transport-ambiguous"], + currentMutation: "published", + }, + { + deleteClassifications: ["transport-ambiguous"], + currentMutation: "malformed", + }, + { convergenceDirectFailure: 403 }, + { convergenceDirectFailure: 429 }, + { convergenceDirectFailure: 500 }, + { convergenceDirectFailure: "timeout" }, + { convergenceListFailure: 429 }, + ]) { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["confirmed-204"], + ...scenario, + }) + await assert.rejects( + performOneDuplicateDeletion(harness.input, harness.dependencies), + /failed/iu, + ) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + } +}) + +test("one-target convergence shares one exact 90-second budget across six complete request pairs and waits", async (t) => { + const timing = exactConvergenceTimeline([ + 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 3_500, + ]) + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["transport-ambiguous"], + absenceOnConvergenceAttempt: 6, + }) + const result = await performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithTimeline(timing.values), + ) + assert.equal(result.basis, "ambiguous") + assert.deepEqual(harness.waits, [1_000, 5_000, 15_000, 30_000, 30_000]) + assert.deepEqual( + harness.requestBudgets.map(({ operation }) => operation), + timing.requests.map(({ operation }) => operation), + ) + for (const [index, { timeoutMs }] of harness.requestBudgets.entries()) { + assert.ok(timeoutMs > 0) + assert.ok(timeoutMs <= timing.requests[index].timeoutMs) + } + assert.ok(harness.requestBudgets.at(-1).timeoutMs <= 3_500) + assert.equal(harness.events.filter((entry) => entry.startsWith("direct:")).length, 6) + assert.equal(harness.events.filter((entry) => entry === "list").length, 6) + assert.equal(timing.completedAt, 90_000) +}) + +test("one-target convergence stops on a reversed or future monotonic test trace before a seventh request", async (t) => { + for (const timeline of [Object.freeze([0, 1, 0]), Object.freeze([0, 90_001])]) { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["transport-ambiguous"], + }) + await assert.rejects( + performOneDuplicateDeletion(harness.input, harness.dependenciesWithTimeline(timeline)), + /failed/iu, + ) + assert.equal(harness.events.filter((entry) => entry.startsWith("delete:")).length, 1) + assert.ok(harness.events.filter((entry) => entry.startsWith("direct:")).length < 7) + } +}) + +test("one-target deletion rejects an accessor-backed monotonic test trace without invocation or network", async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["confirmed-204"], + }) + let accessorCalls = 0 + const monotonicTimeline = new Array(2) + for (let index = 0; index < monotonicTimeline.length; index += 1) { + Object.defineProperty(monotonicTimeline, index, { + enumerable: true, + get() { + accessorCalls += 1 + return index + }, + }) + } + Object.freeze(monotonicTimeline) + const eventCount = harness.events.length + await assert.rejects( + performOneDuplicateDeletion(harness.input, harness.dependenciesWithTimeline(monotonicTimeline)), + /failed/iu, + ) + assert.equal(accessorCalls, 0) + assert.equal(harness.events.length, eventCount) +}) + +test("one-target deletion rejects an accessor-backed wall-clock test trace without invocation or network", async (t) => { + const harness = await oneDeletionFixture(t, { + deleteClassifications: ["confirmed-204"], + }) + let accessorCalls = 0 + const wallClockTimeline = new Array(1) + Object.defineProperty(wallClockTimeline, 0, { + enumerable: true, + get() { + accessorCalls += 1 + return harness.authorityTime + }, + }) + Object.freeze(wallClockTimeline) + const eventCount = harness.events.length + await assert.rejects( + performOneDuplicateDeletion( + harness.input, + harness.dependenciesWithWallClockTimeline(wallClockTimeline), + ), + /failed/iu, + ) + assert.equal(accessorCalls, 0) + assert.equal(harness.events.length, eventCount) +}) + +test("inspects the exact incident, observes the gap, and writes one canonical private proposal", async (t) => { + const fixture = await inspectionFixture(t) + const result = await inspectDuplicateDrafts(exactInput(), fixture.dependencies) + + assert.deepEqual(result, { + proposalSha256: result.proposalSha256, + version: DUPLICATE_DRAFT_CANDIDATE.version, + commitSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + duplicates: [...DUPLICATE_DRAFT_IDS], + output: OUTPUT, + }) + assert.match(result.proposalSha256, /^[0-9a-f]{64}$/u) + assert.equal(Object.isFrozen(result), true) + assert.equal(Object.isFrozen(result.duplicates), true) + assert.deepEqual(fixture.waits, [60_000]) + assert.equal(fixture.releaseFixture.downloadCount, 135) + assert.deepEqual(fixture.npmCalls, [ + ...CANONICAL_RELEASE_PACKAGE_ORDER, + ...CANONICAL_RELEASE_PACKAGE_ORDER, + ]) + + const target = path.join(fixture.root, OUTPUT) + const bytes = await readFile(target) + const envelope = parseConsolidationEnvelope("proposed", bytes) + assert.equal(envelope.recordSha256, result.proposalSha256) + assert.deepEqual( + envelope.record.npmInventories.map(({ stage }) => stage), + ["inspect-initial", "inspect-ready"], + ) + assert.equal( + Date.parse(envelope.record.npmInventories[1].startedAt) - + Date.parse(envelope.record.npmInventories[0].completedAt), + 60_000, + ) + assert.equal(envelope.record.confirmation.template, "<64-lowercase-hex-digest>") + assert.deepEqual(envelope.record.controller, { + headSha: CONTROLLER_SHA, + originMainSha: CONTROLLER_SHA, + githubMainSha: CONTROLLER_SHA, + }) + assert.deepEqual( + envelope.record.releases.map(({ role, id }) => ({ role, id })), + [ + { role: "survivor", id: DUPLICATE_DRAFT_SURVIVOR_ID }, + { role: "duplicate", id: DUPLICATE_DRAFT_IDS[0] }, + { role: "duplicate", id: DUPLICATE_DRAFT_IDS[1] }, + ], + ) + assert.equal((await stat(target)).mode & 0o777, 0o600) + assert.equal((await lstat(target)).isSymbolicLink(), false) + assert.deepEqual(fixture.releaseFixture.operations.slice(-6), [ + `get:${DUPLICATE_DRAFT_SURVIVOR_ID}`, + `list-assets:${DUPLICATE_DRAFT_SURVIVOR_ID}`, + `get:${DUPLICATE_DRAFT_IDS[0]}`, + `list-assets:${DUPLICATE_DRAFT_IDS[0]}`, + `get:${DUPLICATE_DRAFT_IDS[1]}`, + `list-assets:${DUPLICATE_DRAFT_IDS[1]}`, + ]) + assert.deepEqual(fixture.events, expectedInspectionEvents(fixture.releaseFixture)) + assert.equal(fixture.events.filter((event) => event.startsWith("download:")).length, 135) + assert.equal(fixture.events.filter((event) => event.startsWith("attest:")).length, 3) + assert.equal(fixture.events.filter((event) => event.startsWith("npm:initial:")).length, 21) + assert.equal(fixture.events.filter((event) => event.startsWith("npm:ready:")).length, 21) + assert.equal(fixture.events.filter((event) => event.startsWith("metadata:initial:")).length, 7) + assert.equal(fixture.events.filter((event) => event.startsWith("metadata:final:")).length, 7) + assert.equal(fixture.clockCallsAfterTerminal, 0) + assert.throws(() => fixture.dependencies.now(), /terminal/iu) + await assert.rejects(fixture.dependencies.adapters.github.getRepository(), /sealed|terminal/iu) +}) + +test("uses verification work inside the gap and waits only the exact nonnegative remainder", async (t) => { + const fixture = await inspectionFixture(t, { verificationMs: 61_000 }) + await inspectDuplicateDrafts(exactInput(), fixture.dependencies) + assert.deepEqual(fixture.waits, []) + + const second = await inspectionFixture(t, { verificationMs: 17_250 }) + await inspectDuplicateDrafts(exactInput(), second.dependencies) + assert.deepEqual(second.waits, [42_750]) +}) + +test("creates only the exact private proposal parent in a clean canonical repository", async (t) => { + const fixture = await inspectionFixture(t, { makeReleaseDirectory: false }) + await inspectDuplicateDrafts(exactInput(), fixture.dependencies) + assert.equal((await stat(path.join(fixture.root, ".dawn"))).isDirectory(), true) + assert.equal((await stat(path.join(fixture.root, ".dawn", "release"))).isDirectory(), true) + assert.equal((await stat(path.join(fixture.root, OUTPUT))).mode & 0o777, 0o600) +}) + +test("rejects a symlinked repository root before any adapter, download, or write", async (t) => { + const parent = await realpath(await mkdtemp(path.join(os.tmpdir(), "dawn-inspect-link-"))) + t.after(() => rm(parent, { recursive: true, force: true })) + const physical = path.join(parent, "physical") + const linked = path.join(parent, "linked") + await mkdir(physical) + await symlink(physical, linked, "dir") + const fixture = await inspectionFixture(t, { + root: linked, + makeReleaseDirectory: false, + }) + await assert.rejects(inspectDuplicateDrafts(exactInput(), fixture.dependencies)) + assert.deepEqual(fixture.events, []) + assert.equal(fixture.releaseFixture.downloadCount, 0) + await assert.rejects(() => lstat(path.join(physical, ".dawn")), { + code: "ENOENT", + }) +}) + +test("seal validation precedes root revalidation and rejects a root replacement from the seal boundary", async (t) => { + const parent = await realpath(await mkdtemp(path.join(os.tmpdir(), "dawn-inspect-race-"))) + t.after(() => rm(parent, { recursive: true, force: true })) + const root = path.join(parent, "checkout") + const displaced = path.join(parent, "checkout-displaced") + await mkdir(path.join(root, ".dawn", "release"), { recursive: true }) + const fixture = await inspectionFixture(t, { + root, + onSeal() { + renameSync(root, displaced) + mkdirSync(path.join(root, ".dawn", "release"), { recursive: true }) + }, + }) + await assert.rejects(inspectDuplicateDrafts(exactInput(), fixture.dependencies)) + assert.equal(fixture.events.includes("write"), false) + await assert.rejects(() => readFile(path.join(root, OUTPUT)), { + code: "ENOENT", + }) + await assert.rejects(() => readFile(path.join(displaced, OUTPUT)), { + code: "ENOENT", + }) +}) + +test("rejects any injected post-root-validation hook before adapter calls", async (t) => { + const fixture = await inspectionFixture(t) + await assert.rejects( + inspectDuplicateDrafts(exactInput(), { + ...fixture.dependencies, + afterRootValidation() { + assert.fail("post-root-validation hooks must be unreachable") + }, + }), + ) + assert.deepEqual(fixture.events, []) +}) + +test("rejects a retreating trusted clock without writing", async (t) => { + const fixture = await inspectionFixture(t) + let calls = 0 + const dependencies = Object.freeze({ + ...fixture.dependencies, + now() { + calls += 1 + return new Date(BASE_TIME - calls).toISOString() + }, + }) + await assert.rejects(inspectDuplicateDrafts(exactInput(), dependencies)) + await assert.rejects(() => readFile(path.join(fixture.root, OUTPUT)), { + code: "ENOENT", + }) +}) + +test("rejects malformed incident input before any adapter or filesystem call", async (t) => { + const variants = [ + {}, + { ...exactInput(), version: "0.8.23" }, + { ...exactInput(), survivor: 379991871 }, + { ...exactInput(), duplicates: [...DUPLICATE_DRAFT_IDS].reverse() }, + { + ...exactInput(), + duplicates: [DUPLICATE_DRAFT_IDS[0], DUPLICATE_DRAFT_IDS[0]], + }, + { ...exactInput(), output: `../${OUTPUT}` }, + { ...exactInput(), output: path.resolve("/tmp/proposed.json") }, + { ...exactInput(), extra: true }, + new Proxy(exactInput(), {}), + ] + const accessor = exactInput() + Object.defineProperty(accessor, "version", { + enumerable: true, + get() { + throw new Error("secret accessor") + }, + }) + variants.push(accessor) + const hidden = exactInput() + Object.defineProperty(hidden, "hidden", { value: true }) + variants.push(hidden) + const symbol = exactInput() + symbol[Symbol("hidden")] = true + variants.push(symbol) + + for (const input of variants) { + let calls = 0 + const fixture = await inspectionFixture(t) + const adapters = Object.freeze({ + ...fixture.dependencies.adapters, + local: Object.freeze({ + async readState() { + calls += 1 + throw new Error("called") + }, + }), + }) + await assert.rejects( + inspectDuplicateDrafts(input, Object.freeze({ ...fixture.dependencies, adapters })), + ) + assert.equal(calls, 0) + await assert.rejects(() => readFile(path.join(fixture.root, OUTPUT)), { + code: "ENOENT", + }) + } +}) + +test("rejects unsafe dependencies and adapter descriptors before calls", async (t) => { + const fixture = await inspectionFixture(t) + const badAdapters = { ...fixture.dependencies.adapters } + Object.defineProperty(badAdapters, "github", { + enumerable: true, + get() { + throw new Error("credential body") + }, + }) + const hiddenAdapters = { ...fixture.dependencies.adapters } + Object.defineProperty(hiddenAdapters, "hidden", { value: true }) + Object.freeze(hiddenAdapters) + const hiddenLocal = { ...fixture.dependencies.adapters.local } + Object.defineProperty(hiddenLocal, "hidden", { value: true }) + Object.freeze(hiddenLocal) + const hiddenLocalAdapters = replaceFacade(fixture.dependencies.adapters, "local", hiddenLocal) + for (const dependencies of [ + { ...fixture.dependencies, extra: true }, + { + ...fixture.dependencies, + repositoryRootIdentity: Object.freeze({}), + }, + { ...fixture.dependencies, adapters: badAdapters }, + { ...fixture.dependencies, adapters: hiddenAdapters }, + { ...fixture.dependencies, adapters: hiddenLocalAdapters }, + new Proxy(fixture.dependencies, {}), + ]) { + await assert.rejects(inspectDuplicateDrafts(exactInput(), dependencies), (error) => { + assert.doesNotMatch(String(error), /credential body/iu) + return true + }) + } +}) + +test("fails closed on changed authority or non-E404 npm evidence without writing", async (t) => { + for (const mutation of ["dirty", "active-workflow", "published-package"]) { + const fixture = await inspectionFixture(t, { mutation }) + await assert.rejects(inspectDuplicateDrafts(exactInput(), fixture.dependencies)) + await assert.rejects(() => readFile(path.join(fixture.root, OUTPUT)), { + code: "ENOENT", + }) + } +}) + +test("rejects the historical candidate in every current-controller position before output effects", async (t) => { + for (const candidateControllerField of ["local", "origin", "github"]) { + const fixture = await inspectionFixture(t, { + candidateControllerField, + makeReleaseDirectory: false, + }) + await assert.rejects(inspectDuplicateDrafts(exactInput(), fixture.dependencies)) + assert.equal(fixture.events.includes("write"), false) + assert.deepEqual( + fixture.events, + candidateControllerField === "github" + ? [ + "metadata:initial:local", + "metadata:initial:repository", + "metadata:initial:actor", + "metadata:initial:main", + ] + : ["metadata:initial:local"], + ) + await assert.rejects(() => lstat(path.join(fixture.root, ".dawn")), { + code: "ENOENT", + }) + } +}) + +test("rejects authority and release drift between complete capture phases without writing", async (t) => { + for (const lateMutation of ["controller", "repository", "workflow", "tag", "release"]) { + const fixture = await inspectionFixture(t, { lateMutation }) + await assert.rejects(inspectDuplicateDrafts(exactInput(), fixture.dependencies)) + assert.equal(fixture.events.includes("write"), false) + await assert.rejects(() => readFile(path.join(fixture.root, OUTPUT)), { + code: "ENOENT", + }) + } +}) + +test("rejects a managed Release added or published during the observation gap", async (t) => { + for (const lateRelease of ["extra-draft", "published"]) { + const fixture = await inspectionFixture(t, { lateRelease }) + await assert.rejects(inspectDuplicateDrafts(exactInput(), fixture.dependencies)) + await assert.rejects(() => readFile(path.join(fixture.root, OUTPUT)), { + code: "ENOENT", + }) + assert.equal(fixture.releaseListCalls, 2) + } +}) + +test("rejects an unsafe existing output and a symlinked release directory", async (t) => { + const fixture = await inspectionFixture(t) + const target = path.join(fixture.root, OUTPUT) + await writeFile(target, "unsafe existing output\n", { mode: 0o644 }) + const original = await readFile(target) + await assert.rejects(inspectDuplicateDrafts(exactInput(), fixture.dependencies)) + assert.deepEqual(await readFile(target), original) + + const linked = await inspectionFixture(t, { makeReleaseDirectory: false }) + const outside = await mkdtemp(path.join(os.tmpdir(), "dawn-inspect-outside-")) + t.after(() => rm(outside, { recursive: true, force: true })) + await mkdir(path.join(linked.root, ".dawn"), { recursive: true }) + await symlink(outside, path.join(linked.root, ".dawn", "release"), "dir") + await assert.rejects(inspectDuplicateDrafts(exactInput(), linked.dependencies)) + assert.deepEqual(linked.events, []) + assert.equal(linked.releaseFixture.downloadCount, 0) + await assert.rejects(() => readFile(path.join(outside, path.basename(OUTPUT))), { + code: "ENOENT", + }) +}) + +test("redacts remote diagnostics and never returns raw evidence", async (t) => { + const fixture = await inspectionFixture(t, { + remoteError: "token fixture_secret response body", + }) + await assert.rejects(inspectDuplicateDrafts(exactInput(), fixture.dependencies), (error) => { + assert.equal(error.message, "Duplicate-draft inspection failed.") + assert.doesNotMatch(String(error), /fixture_secret|response body/iu) + return true + }) +}) + +function replaceFacade(adapters, name, facade) { + const replacement = { ...adapters, [name]: facade } + Object.defineProperty( + replacement, + "captureConsolidationAuthority", + Object.getOwnPropertyDescriptor(adapters, "captureConsolidationAuthority"), + ) + return Object.freeze(replacement) +} + +function exactInput() { + return { + version: DUPLICATE_DRAFT_CANDIDATE.version, + commitSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + duplicates: [...DUPLICATE_DRAFT_IDS], + output: OUTPUT, + } +} + +function expectedInspectionEvents(releaseFixture) { + const metadata = (phase) => + ["local", "repository", "actor", "main", "workflow", "runs", "tag"].map( + (operation) => `metadata:${phase}:${operation}`, + ) + const hydration = releaseFixture.releases.flatMap((release) => [ + ...release.assets.map((asset) => `download:${release.id}:${asset.id}`), + `attest:${release.id}`, + ]) + const terminalReads = [DUPLICATE_DRAFT_SURVIVOR_ID, ...DUPLICATE_DRAFT_IDS].flatMap( + (releaseId) => [`get:${releaseId}`, `list-assets:${releaseId}`], + ) + return [ + ...metadata("initial"), + ...CANONICAL_RELEASE_PACKAGE_ORDER.map((name) => `npm:initial:${name}`), + "releases:initial", + ...hydration, + "wait:60000", + ...CANONICAL_RELEASE_PACKAGE_ORDER.map((name) => `npm:ready:${name}`), + "releases:final", + ...metadata("final"), + ...terminalReads, + ] +} + +async function inspectionFixture(t, options = {}) { + const root = + options.root ?? (await realpath(await mkdtemp(path.join(os.tmpdir(), "dawn-inspect-")))) + if (options.root === undefined) t.after(() => rm(root, { recursive: true, force: true })) + if (options.makeReleaseDirectory !== false) + await mkdir(path.join(root, ".dawn", "release"), { recursive: true }) + const releaseFixture = createDuplicateDraftConsolidationFixture() + const events = [] + const npmCalls = [] + const waits = [] + let releaseListCalls = 0 + let metadataPhase = "initial" + let attestationCalls = 0 + let terminalComplete = false + let clockCallsAfterTerminal = 0 + let nowMs = BASE_TIME + const now = () => { + if (terminalComplete) { + clockCallsAfterTerminal += 1 + throw new Error("injected clock rejected after terminal completion") + } + return new Date(nowMs).toISOString() + } + const assertNetworkOpen = () => { + if (terminalComplete) throw new Error("adapter rejected by terminal seal") + } + const localState = { + headSha: + options.candidateControllerField === "local" + ? DUPLICATE_DRAFT_CANDIDATE.commitSha + : CONTROLLER_SHA, + branch: "main", + porcelainStatus: options.mutation === "dirty" ? " M package.json" : "", + originMainSha: + options.candidateControllerField === "origin" + ? DUPLICATE_DRAFT_CANDIDATE.commitSha + : CONTROLLER_SHA, + } + const adapters = { + local: Object.freeze({ + async readState() { + assertNetworkOpen() + events.push(`metadata:${metadataPhase}:local`) + if (metadataPhase === "final" && options.lateMutation === "controller") { + return { + ...structuredClone(localState), + headSha: "c".repeat(40), + originMainSha: "c".repeat(40), + } + } + return structuredClone(localState) + }, + }), + github: Object.freeze({ + async getRepository() { + assertNetworkOpen() + events.push(`metadata:${metadataPhase}:repository`) + if (options.remoteError) throw new Error(options.remoteError) + if (metadataPhase === "final" && options.lateMutation === "repository") { + return { + name: "cacheplane/dawnai", + id: "1210070283", + defaultBranch: "main", + } + } + return { + name: "cacheplane/dawnai", + id: "1210070282", + defaultBranch: "main", + } + }, + async getAuthenticatedUser() { + assertNetworkOpen() + events.push(`metadata:${metadataPhase}:actor`) + return { login: "blove", id: "61436" } + }, + async getDefaultBranchSha() { + assertNetworkOpen() + events.push(`metadata:${metadataPhase}:main`) + return metadataPhase === "final" && options.lateMutation === "controller" + ? "c".repeat(40) + : options.candidateControllerField === "github" + ? DUPLICATE_DRAFT_CANDIDATE.commitSha + : CONTROLLER_SHA + }, + async getWorkflowState() { + assertNetworkOpen() + events.push(`metadata:${metadataPhase}:workflow`) + return { + workflowId: "202458345", + path: ".github/workflows/release.yml", + state: + options.mutation === "active-workflow" || + (metadataPhase === "final" && options.lateMutation === "workflow") + ? "active" + : "disabled_manually", + } + }, + async listNonterminalWorkflowRuns(query) { + assertNetworkOpen() + events.push(`metadata:${metadataPhase}:runs`) + return { query: structuredClone(query), runs: [] } + }, + async getAnnotatedTag() { + assertNetworkOpen() + events.push(`metadata:${metadataPhase}:tag`) + return { + name: "v0.8.22", + objectSha: + metadataPhase === "final" && options.lateMutation === "tag" + ? "c".repeat(40) + : "a".repeat(40), + targetSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + objectType: "tag", + observedAt: now(), + } + }, + async listReleases() { + assertNetworkOpen() + releaseListCalls += 1 + const phase = releaseListCalls === 1 ? "initial" : "final" + events.push(`releases:${phase}`) + nowMs += options.verificationMs ?? 0 + const releases = structuredClone(releaseFixture.releases) + if (releaseListCalls === 2 && options.lateRelease === "extra-draft") { + const extra = structuredClone(releases[1]) + extra.id = 379999999 + extra.node_id = "RE_late_extra" + extra.tag_name = "untagged-late-extra" + for (const [index, asset] of extra.assets.entries()) { + asset.id = 990_000 + index + asset.node_id = `RA_late_${index}` + } + releases.push(extra) + } + if (releaseListCalls === 2 && options.lateRelease === "published") { + releases[0].draft = false + releases[0].published_at = "2026-09-01T12:01:00Z" + } + if (phase === "final") metadataPhase = "final" + return { + status: "PRESENT", + operation: "releases", + httpStatus: 200, + code: null, + value: releases, + } + }, + async downloadReleaseAsset(request) { + assertNetworkOpen() + events.push(`download:${request.releaseId}:${request.assetId}`) + return releaseFixture.github.downloadReleaseAsset(request) + }, + async getRelease(request) { + assertNetworkOpen() + events.push(`get:${request.releaseId}`) + const result = await releaseFixture.github.getRelease(request) + if (options.lateMutation === "release") result.value.name = "changed after observation" + return result + }, + async listReleaseAssets(request) { + assertNetworkOpen() + events.push(`list-assets:${request.releaseId}`) + return releaseFixture.github.listReleaseAssets(request) + }, + }), + npm: Object.freeze({ + async observePackageVersion({ name }) { + assertNetworkOpen() + events.push( + `npm:${npmCalls.length < CANONICAL_RELEASE_PACKAGE_ORDER.length ? "initial" : "ready"}:${name}`, + ) + npmCalls.push(name) + return options.mutation === "published-package" + ? { + status: "PRESENT", + operation: "package-version", + httpStatus: 200, + code: null, + } + : { + status: "ABSENT", + operation: "package-version", + httpStatus: 404, + code: "E404", + } + }, + }), + attestations: Object.freeze({ + async verify(request) { + assertNetworkOpen() + const releaseId = [DUPLICATE_DRAFT_SURVIVOR_ID, ...DUPLICATE_DRAFT_IDS][attestationCalls] + attestationCalls += 1 + events.push(`attest:${releaseId}`) + return releaseFixture.attestations.verify(request) + }, + }), + writer: Object.freeze({ + async deleteDuplicate() { + assert.fail("inspection must not delete") + }, + }), + } + Object.defineProperty(adapters, "captureConsolidationAuthority", { + value: Object.freeze(async function captureConsolidationAuthority() { + assert.fail("inspection must not capture delete authority") + }), + enumerable: false, + writable: false, + configurable: false, + }) + Object.defineProperty(adapters, "captureInspectionTerminal", { + value: Object.freeze(async function captureInspectionTerminal(input) { + assertNetworkOpen() + const releases = [] + for (const expectedEvidence of input.releases) { + const read = await captureDirectTargetRead({ + candidate: input.candidate, + releaseId: expectedEvidence.id, + role: expectedEvidence.role, + expectedEvidence, + github: adapters.github, + now, + }) + releases.push(read.evidence) + } + await options.afterTerminal?.() + const completedAt = new Date(nowMs).toISOString() + terminalComplete = true + return Object.freeze({ + releases: Object.freeze(releases), + completedAt, + }) + }), + enumerable: false, + writable: false, + configurable: false, + }) + Object.defineProperty(adapters, "assertInspectionTerminalSealed", { + value: Object.freeze(function assertInspectionTerminalSealed() { + if (!terminalComplete) throw new Error("inspection terminal is not sealed") + options.onSeal?.() + }), + enumerable: false, + writable: false, + configurable: false, + }) + Object.freeze(adapters) + const dependencies = Object.freeze({ + repositoryRoot: root, + adapters, + now, + async wait(milliseconds, { signal }) { + assert.equal(signal instanceof AbortSignal, true) + assert.equal(signal.aborted, false) + waits.push(milliseconds) + events.push(`wait:${milliseconds}`) + nowMs += milliseconds + }, + }) + return { + root, + releaseFixture, + events, + npmCalls, + waits, + get clockCallsAfterTerminal() { + return clockCallsAfterTerminal + }, + get releaseListCalls() { + return releaseListCalls + }, + dependencies, + } +} + +async function productionPerformRehearsalFixture(t, options = {}) { + const inspection = await inspectionFixture(t) + await inspectDuplicateDrafts(exactInput(), inspection.dependencies) + await mkdir(path.join(inspection.root, "scripts", "release"), { + recursive: true, + }) + const proposal = parseConsolidationEnvelope( + "proposed", + await readPrivateEnvelope( + path.join(inspection.root, OUTPUT), + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.proposedBytes, + ), + ) + const releaseFixture = createDuplicateDraftConsolidationFixture() + const deleted = new Set() + const deleteIds = [] + const events = [] + let nowMs = Date.now() + 60 * 60_000 + const now = () => new Date(nowMs++).toISOString() + const currentReleases = () => + releaseFixture.releases + .filter(({ id }) => !deleted.has(String(id))) + .map((release) => structuredClone(release)) + const present = (operation, value) => ({ + status: "PRESENT", + operation, + httpStatus: 200, + code: null, + value, + }) + const externalGithub = { + async getRef({ ref }) { + if (ref === "heads/main") { + events.push("github-main") + const sha = + options.driftMainAfterFirstDelete && deleted.has(DUPLICATE_DRAFT_IDS[0]) + ? "c".repeat(40) + : CONTROLLER_SHA + return present("ref", { + ref: "refs/heads/main", + object: { type: "commit", sha }, + }) + } + assert.equal(ref, `tags/${DUPLICATE_DRAFT_CANDIDATE.tag}`) + return present("ref", { + ref: `refs/tags/${DUPLICATE_DRAFT_CANDIDATE.tag}`, + object: { type: "tag", sha: "a".repeat(40) }, + }) + }, + async getGitTag({ tagSha }) { + assert.equal(tagSha, "a".repeat(40)) + return present("git-tag", { + sha: tagSha, + tag: DUPLICATE_DRAFT_CANDIDATE.tag, + object: { type: "commit", sha: DUPLICATE_DRAFT_CANDIDATE.commitSha }, + }) + }, + async getWorkflow({ workflow }) { + events.push(`workflow:${workflow}`) + assert.equal(workflow, "release.yml") + return present("workflow", { + id: 202_458_345, + path: ".github/workflows/release.yml", + state: "disabled_manually", + }) + }, + async listReleases() { + const releases = currentReleases() + events.push(`releases:${releases.map(({ id }) => id).join(",")}`) + return present("releases", releases) + }, + async getRelease({ releaseId }) { + events.push(`get-release:${releaseId}`) + const release = currentReleases().find(({ id }) => String(id) === String(releaseId)) + if (release === undefined) { + return { + status: "AMBIGUOUS", + operation: "release", + httpStatus: 404, + code: "NOT_FOUND", + } + } + return present("release", release) + }, + async listReleaseAssets({ releaseId }) { + const release = currentReleases().find(({ id }) => String(id) === String(releaseId)) + if (release === undefined) throw new Error("deleted Release has no assets") + return present("release-assets", release.assets) + }, + async downloadReleaseAsset(input) { + events.push(`download:${input.releaseId}:${input.assetId}`) + return releaseFixture.github.downloadReleaseAsset(input) + }, + } + const fetchImpl = async (url, init = {}) => { + const target = String(url) + if (init.method === "DELETE") { + const releaseId = target.split("/").at(-1) + assert.equal(DUPLICATE_DRAFT_IDS.includes(releaseId), true) + assert.equal(deleted.has(releaseId), false) + deleted.add(releaseId) + deleteIds.push(releaseId) + events.push(`delete:${releaseId}`) + return new Response(null, { status: 204 }) + } + if (target === "https://api.github.com/repos/cacheplane/dawnai") { + return jsonTestResponse({ + id: 1_210_070_282, + full_name: "cacheplane/dawnai", + default_branch: "main", + }) + } + if (target === "https://api.github.com/user") { + return jsonTestResponse({ id: 61_436, login: "blove" }) + } + if (target.includes("/actions/workflows/") && target.includes("/runs?")) { + return jsonTestResponse({ total_count: 0, workflow_runs: [] }) + } + throw new Error(`unexpected rehearsal request ${target}`) + } + const run = async (_command, args) => { + if (args[0] === "symbolic-ref") { + return { exitCode: 0, stdout: "main\n", stderr: "" } + } + if (args[0] === "status") return { exitCode: 0, stdout: "", stderr: "" } + if (args[0] === "rev-parse" && args.at(-1).startsWith("refs/remotes/origin/main")) { + return { exitCode: 0, stdout: `${CONTROLLER_SHA}\n`, stderr: "" } + } + throw new Error(`unexpected rehearsal command ${args.join(" ")}`) + } + const createAdapters = () => + createDuplicateDraftConsolidationAdapters({ + cwd: inspection.root, + token: "fixture_token_value", + environment: { HOME: inspection.root, PATH: "/tools" }, + dependencies: { + fetchImpl, + run, + now, + createGitHubReader() { + return externalGithub + }, + createOwnerPreflightAdapters() { + return { + git: { + async headSha() { + events.push("local-main") + return CONTROLLER_SHA + }, + }, + } + }, + createNpmReader() { + return { + async observePackageVersion() { + events.push("npm") + return { + status: "ABSENT", + operation: "package-version", + httpStatus: 404, + code: "E404", + } + }, + } + }, + createCliAttestationVerifier() { + return { + async verify(input) { + events.push("attestations") + return releaseFixture.attestations.verify(input) + }, + } + }, + }, + }) + const stdout = memorySink() + const stderr = memorySink() + const confirmation = `CONSOLIDATE v${proposal.record.candidate.version} ${proposal.record.candidate.commitSha} SURVIVOR ${proposal.record.roles.survivor} DELETE ${proposal.record.roles.duplicates.join(",")} PROPOSAL ${proposal.recordSha256}` + return { + deleteIds, + events, + stdout, + stderr, + receiptPath: path.join(inspection.root, "scripts/release/duplicate-draft-consolidation.json"), + volatilizeSurvivorAssets() { + const ids = [] + for (const [index, asset] of releaseFixture.releases[0].assets.entries()) { + asset.id = 8_500_000 + index + asset.node_id = `RA_verify_current_${index}` + asset.created_at = `2026-09-01T10:${String(index).padStart(2, "0")}:00Z` + asset.updated_at = `2026-09-01T11:${String(index).padStart(2, "0")}:00Z` + asset.download_count += 1_000 + ids.push(String(asset.id)) + } + return ids + }, + options: { + argv: [ + "perform", + "--proposal", + ".dawn/release/duplicate-draft-consolidation.proposed.json", + "--journal", + ".dawn/release/duplicate-draft-consolidation.journal.json", + "--receipt", + "scripts/release/duplicate-draft-consolidation.json", + "--confirmation", + confirmation, + ], + cwd: inspection.root, + environment: {}, + stdout, + stderr, + dependencies: { + createAdapters, + now, + async wait(milliseconds, { signal }) { + assert.equal(signal instanceof AbortSignal, true) + nowMs += milliseconds + }, + }, + }, + } +} + +function jsonTestResponse(value) { + return new Response(JSON.stringify(value), { + status: 200, + headers: { "content-type": "application/json" }, + }) +} + +function memorySink() { + const sink = { + value: "", + write(chunk) { + sink.value += String(chunk) + return true + }, + } + return sink +} + +async function performFixture(t, options = {}) { + const inspection = await inspectionFixture(t) + await inspectDuplicateDrafts(exactInput(), inspection.dependencies) + const proposalPath = path.join(inspection.root, OUTPUT) + const proposal = parseConsolidationEnvelope("proposed", await readFile(proposalPath)) + const confirmation = `CONSOLIDATE v${proposal.record.candidate.version} ${proposal.record.candidate.commitSha} SURVIVOR ${proposal.record.roles.survivor} DELETE ${proposal.record.roles.duplicates.join(",")} PROPOSAL ${proposal.recordSha256}` + const journalPath = path.join( + inspection.root, + ".dawn/release/duplicate-draft-consolidation.journal.json", + ) + const receiptPath = path.join( + inspection.root, + "scripts/release/duplicate-draft-consolidation.json", + ) + await mkdir(path.dirname(receiptPath), { recursive: true }) + const calls = [] + let failReceipt = options.failReceiptOnce === true + let failAfterReceiptRename = options.postRenameReceiptFailureOnce === true + let failInitialVerification = options.failInitialVerificationOnce === true + let finalCaptures = 0 + let tick = Date.parse(proposal.record.inspectedAt) + 1_000 + const nextTime = () => { + const value = new Date(tick).toISOString() + tick += 1_000 + return value + } + const inventory = (stage) => { + const observedAt = nextTime() + return { + stage, + startedAt: observedAt, + completedAt: observedAt, + packages: proposal.record.npmInventories[0].packages.map((entry) => ({ + ...entry, + observedAt, + })), + } + } + const persistJournal = async (journal) => { + await writePrivateEnvelope(journalPath, canonicalConsolidationEnvelopeBytes("journal", journal)) + await writePrivateEnvelope( + journalPath.replace(/journal\.json$/u, "journal.head.json"), + testJournalHeadBytes(journalPath, journal), + ) + } + const completeTarget = async ({ targetReleaseId }) => { + if (targetReleaseId === DUPLICATE_DRAFT_IDS[1] && options.failSecondTarget !== undefined) { + throw new Error(`simulated ${options.failSecondTarget} drift`) + } + calls.push(`delete:${targetReleaseId}`) + let journal = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope(journalPath, DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes), + ) + const targetIndex = DUPLICATE_DRAFT_IDS.indexOf(targetReleaseId) + const targetEvidence = proposal.record.releases[targetIndex + 1] + const authority = deletionAuthorityFixture({ + proposal, + stage: targetIndex === 0 ? "pre-delete-1" : "pre-delete-2", + targetEvidence, + observedAt: nextTime(), + releases: + targetIndex === 0 + ? proposal.record.releases + : [proposal.record.releases[0], proposal.record.releases[2]], + }) + journal = appendJournalEvent( + journal, + "delete-authority-observed", + { targetReleaseId, attemptNumber: 1, authority }, + authority.observedAt, + ) + journal = appendJournalEvent( + journal, + "delete-intent", + { + targetReleaseId, + attemptNumber: 1, + authorityEventSha256: journal.record.events.at(-1).eventSha256, + }, + nextTime(), + ) + journal = appendJournalEvent( + journal, + "delete-outcome", + { + targetReleaseId, + attemptNumber: 1, + classification: "confirmed-204", + httpStatus: 204, + observedAt: nextTime(), + }, + new Date(tick - 1_000).toISOString(), + ) + const completedAt = nextTime() + journal = appendJournalEvent( + journal, + "absence-converged", + { + targetReleaseId, + attemptNumber: 1, + basis: "confirmed-204", + directGet404At: completedAt, + listAbsentAt: completedAt, + attempts: 1, + completedAt, + }, + completedAt, + ) + await persistJournal(journal) + return { + status: "converged", + targetReleaseId, + attemptNumber: 1, + basis: "confirmed-204", + } + } + const captureFinal = async () => { + calls.push("final") + finalCaptures += 1 + const observedAt = nextTime() + const authority = { + stage: "final", + controller: structuredClone(proposal.record.controller), + annotatedTag: { + ...structuredClone(proposal.record.annotatedTag), + observedAt, + }, + workflowAuthority: { + ...structuredClone(proposal.record.workflowAuthority), + observedAt, + }, + npmInventory: inventory("final"), + releases: [structuredClone(proposal.record.releases[0])], + payloadProof: structuredClone(proposal.record.payloadProof), + targetRead: null, + observedAt: nextTime(), + } + if (finalCaptures > 1) { + if (options.resumeFinalDrift === "main") authority.controller.headSha = "c".repeat(40) + if (options.resumeFinalDrift === "npm-publication") { + authority.npmInventory.packages[0].status = "PRESENT" + authority.npmInventory.packages[0].httpStatus = 200 + authority.npmInventory.packages[0].code = null + } + if (options.resumeFinalDrift === "survivor") { + authority.releases[0].semantic.name = "changed survivor" + } + if (options.resumeFinalDrift === "asset") authority.releases[0].assets[0].label = "changed" + if (options.resumeFinalDrift === "duplicate-reappeared") { + authority.releases.push(structuredClone(proposal.record.releases[1])) + } + if (options.resumeFinalDrift === "extra-release") { + const extra = structuredClone(proposal.record.releases[1]) + extra.id = "400000001" + authority.releases.push(extra) + } + if (options.resumeFinalDrift === "workflow") authority.workflowAuthority.state = "active" + if (options.resumeFinalDrift === "run") { + authority.workflowAuthority.nonterminalRuns.push({ + id: "1", + runAttempt: 1, + status: "queued", + event: "workflow_dispatch", + headSha: proposal.record.controller.headSha, + headBranch: "main", + }) + } + if (options.resumeFinalDrift === "tag") authority.annotatedTag.targetSha = "d".repeat(40) + } + return authority + } + return { + proposal, + proposalPath, + journalPath, + receiptPath, + calls, + persistJournal, + performInventory: () => inventory("perform-initial"), + deletionAuthority(targetIndex) { + const targetEvidence = proposal.record.releases[targetIndex + 1] + return deletionAuthorityFixture({ + proposal, + stage: targetIndex === 0 ? "pre-delete-1" : "pre-delete-2", + targetEvidence, + observedAt: nextTime(), + releases: + targetIndex === 0 + ? proposal.record.releases + : [proposal.record.releases[0], proposal.record.releases[2]], + }) + }, + input: { + proposal: ".dawn/release/duplicate-draft-consolidation.proposed.json", + proposalSha256: proposal.recordSha256, + journal: ".dawn/release/duplicate-draft-consolidation.journal.json", + receipt: "scripts/release/duplicate-draft-consolidation.json", + confirmation, + }, + dependencies: Object.freeze({ + repositoryRoot: inspection.root, + async createAdapters() { + throw new Error("high-level test facade should prevent network composition") + }, + now: nextTime, + async wait() {}, + async capturePerformInitial() { + calls.push("perform-initial") + return inventory("perform-initial") + }, + async verifyPerformInitial() { + if (options.failInitialVerificationOnce !== undefined) calls.push("verify-initial") + if (failInitialVerification) { + failInitialVerification = false + throw new Error("simulated initial verification failure") + } + tick += 60_000 + }, + performOneDeletion: completeTarget, + captureFinalAuthority: captureFinal, + async publishReceipt(target, bytes) { + calls.push("receipt") + assert.equal(target, receiptPath) + if (failReceipt) { + failReceipt = false + throw new Error("simulated receipt write failure") + } + const { writeTrackedReceipt } = await import("../duplicate-draft-consolidation-files.mjs") + await writeTrackedReceipt(target, bytes) + if (failAfterReceiptRename) { + failAfterReceiptRename = false + throw new Error("simulated post-rename receipt durability ambiguity") + } + }, + }), + } +} + +async function verificationFixture(t, options = {}) { + const performed = options.performed ?? (await performFixture(t)) + if (options.performed === undefined) { + await performDuplicateDraftConsolidation(performed.input, performed.dependencies) + } + const receipt = parseConsolidationEnvelope( + "final", + await readTrackedReceipt( + performed.receiptPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.finalReceiptBytes, + ), + ) + const releaseFixture = createDuplicateDraftConsolidationFixture() + const calls = [] + let writerCalls = 0 + const drift = options.drift + const survivorRaw = structuredClone(releaseFixture.releases[0]) + const listed = [survivorRaw] + if (drift === "deleted-listed") listed.push(structuredClone(releaseFixture.releases[1])) + if (drift === "extra-managed") { + const extra = structuredClone(releaseFixture.releases[1]) + extra.id = 400_000_001 + extra.node_id = "RE_extra_managed" + extra.tag_name = "untagged-extra-managed" + listed.push(extra) + } + const currentAuthority = structuredClone(receipt.record.finalAuthority) + if (drift === "survivor") currentAuthority.releases[0].semantic.name = "changed survivor" + if (drift === "asset") currentAuthority.releases[0].assets[0].label = "changed asset" + if (drift === "main") currentAuthority.controller.headSha = "c".repeat(40) + if (drift === "workflow") currentAuthority.workflowAuthority.state = "active" + if (drift === "run") { + currentAuthority.workflowAuthority.nonterminalRuns.push({ + id: "1", + runAttempt: 1, + status: "queued", + event: "workflow_dispatch", + headSha: currentAuthority.controller.headSha, + headBranch: "main", + }) + } + if (drift === "tag") currentAuthority.annotatedTag.targetSha = "d".repeat(40) + if (drift === "npm") { + currentAuthority.npmInventory.packages[0].status = "PRESENT" + currentAuthority.npmInventory.packages[0].httpStatus = 200 + currentAuthority.npmInventory.packages[0].code = null + } + + const uncalled = async () => { + throw new Error("unexpected verification adapter call") + } + const adapters = { + local: Object.freeze({ readState: uncalled }), + github: Object.freeze({ + getRepository: uncalled, + getAuthenticatedUser: uncalled, + getDefaultBranchSha: uncalled, + getWorkflowState: uncalled, + listNonterminalWorkflowRuns: uncalled, + getAnnotatedTag: uncalled, + async listReleases() { + calls.push("releases") + return { + status: "PRESENT", + operation: "releases", + httpStatus: 200, + code: null, + value: structuredClone(listed), + } + }, + async getRelease({ releaseId }) { + calls.push(`direct:${releaseId}`) + if (drift === "deleted-present" && releaseId === DUPLICATE_DRAFT_IDS[0]) { + return { + status: "PRESENT", + operation: "release", + httpStatus: 200, + code: null, + value: structuredClone(releaseFixture.releases[1]), + } + } + return { + status: "AMBIGUOUS", + operation: "release", + httpStatus: 404, + code: "NOT_FOUND", + } + }, + listReleaseAssets: uncalled, + downloadReleaseAsset: uncalled, + }), + npm: Object.freeze({ observePackageVersion: uncalled }), + attestations: Object.freeze({ verify: uncalled }), + writer: Object.freeze({ + async deleteDuplicate() { + writerCalls += 1 + throw new Error("verify must never mutate") + }, + }), + } + Object.defineProperties(adapters, { + captureConsolidationAuthority: { + value: Object.freeze(async function captureConsolidationAuthority(input) { + calls.push("final-authority") + assert.equal(input.stage, "final") + assert.equal(input.targetReleaseId, null) + return Object.freeze({ authority: structuredClone(currentAuthority) }) + }), + enumerable: false, + writable: false, + configurable: false, + }, + captureInspectionTerminal: { + value: Object.freeze(uncalled), + enumerable: false, + writable: false, + configurable: false, + }, + assertInspectionTerminalSealed: { + value: Object.freeze(function assertInspectionTerminalSealed() { + throw new Error("verify must not use inspection terminal state") + }), + enumerable: false, + writable: false, + configurable: false, + }, + }) + Object.freeze(adapters) + return { + receipt, + receiptPath: performed.receiptPath, + calls, + get writerCalls() { + return writerCalls + }, + dependencies: Object.freeze({ + repositoryRoot: performed.dependencies.repositoryRoot, + async createAdapters() { + return adapters + }, + }), + } +} + +async function oneDeletionFixture(t, options) { + const inspection = await inspectionFixture(t) + await inspectDuplicateDrafts(exactInput(), inspection.dependencies) + const proposalPath = path.join(inspection.root, OUTPUT) + const proposal = parseConsolidationEnvelope("proposed", await readFile(proposalPath)) + const confirmation = `CONSOLIDATE v${proposal.record.candidate.version} ${proposal.record.candidate.commitSha} SURVIVOR ${proposal.record.roles.survivor} DELETE ${proposal.record.roles.duplicates.join(",")} PROPOSAL ${proposal.recordSha256}` + const confirmationSha256 = createHash("sha256").update(confirmation, "utf8").digest("hex") + const journalPath = path.join( + inspection.root, + ".dawn", + "release", + "duplicate-draft-consolidation.journal.json", + ) + const headPath = journalPath.replace(/journal\.json$/u, "journal.head.json") + let journal = createConsolidationJournal({ + proposedEnvelope: proposal, + confirmationSha256, + recordedAt: proposal.record.inspectedAt, + }) + + const events = [] + const waits = [] + const requestBudgets = [] + const targetIndex = options.targetIndex ?? 0 + const targetReleaseId = DUPLICATE_DRAFT_IDS[targetIndex] + if (targetIndex !== 0 && targetIndex !== 1) throw new Error("invalid fixture target index") + if (targetIndex === 1) { + const seedAuthorityTime = new Date( + Date.parse(proposal.record.inspectedAt) + 1_000, + ).toISOString() + const seedTargetEvidence = proposal.record.releases.find( + ({ id }) => id === DUPLICATE_DRAFT_IDS[0], + ) + const seedAuthority = deletionAuthorityFixture({ + proposal, + stage: "pre-delete-1", + targetEvidence: seedTargetEvidence, + observedAt: seedAuthorityTime, + releases: proposal.record.releases, + }) + journal = appendJournalEvent( + journal, + "delete-authority-observed", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + authority: seedAuthority, + }, + seedAuthorityTime, + ) + const seedIntentTime = new Date(Date.parse(seedAuthorityTime) + 1_000).toISOString() + journal = appendJournalEvent( + journal, + "delete-intent", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + authorityEventSha256: journal.record.events.at(-1).eventSha256, + }, + seedIntentTime, + ) + const seedOutcomeTime = new Date(Date.parse(seedIntentTime) + 1_000).toISOString() + journal = appendJournalEvent( + journal, + "delete-outcome", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + classification: "confirmed-204", + httpStatus: 204, + observedAt: seedOutcomeTime, + }, + seedOutcomeTime, + ) + const seedConvergenceTime = new Date(Date.parse(seedOutcomeTime) + 1_000).toISOString() + journal = appendJournalEvent( + journal, + "absence-converged", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + basis: "confirmed-204", + directGet404At: seedConvergenceTime, + listAbsentAt: seedConvergenceTime, + attempts: 1, + completedAt: seedConvergenceTime, + }, + seedConvergenceTime, + ) + } + await writePrivateEnvelope(journalPath, canonicalConsolidationEnvelopeBytes("journal", journal)) + await writePrivateEnvelope(headPath, testJournalHeadBytes(journalPath, journal)) + const authorityTime = new Date( + Math.max(Date.now(), Date.parse(journal.record.updatedAt) + 1_000), + ).toISOString() + const targetEvidence = proposal.record.releases.find(({ id }) => id === targetReleaseId) + const authority = deletionAuthorityFixture({ + proposal, + stage: targetIndex === 0 ? "pre-delete-1" : "pre-delete-2", + targetEvidence, + observedAt: authorityTime, + releases: + targetIndex === 0 + ? proposal.record.releases + : [proposal.record.releases[0], proposal.record.releases[2]], + }) + let deleted = false + let deleteCalls = 0 + let authorityCaptures = 0 + let adapterFault = null + let convergenceDirectReads = 0 + let permit + let currentAuthorityTime = authorityTime + let npmDurabilityObserved = false + const unsupported = async () => { + throw new Error("unexpected fake adapter operation") + } + const currentRawReleases = ({ heavyVerification = false } = {}) => { + const releases = inspection.releaseFixture.releases + .filter( + (release) => + !( + targetIndex === 1 && + String(release.id) === DUPLICATE_DRAFT_IDS[0] && + !(heavyVerification && options.retryRemainingMutation === "extra") + ) && + !( + heavyVerification && + options.retryRemainingMutation === "missing" && + String(release.id) === DUPLICATE_DRAFT_SURVIVOR_ID + ) && + (!deleted || + options.retainDeletedInList === true || + String(release.id) !== targetReleaseId), + ) + .map((release) => structuredClone(release)) + const target = releases.find(({ id }) => String(id) === targetReleaseId) + if (target !== undefined) { + if (options.currentMutation === "changed") target.name = "changed" + if (options.currentMutation === "published") { + target.draft = false + target.published_at = "2026-09-01T12:35:00Z" + } + if (options.currentMutation === "malformed") target.body = "{" + if (heavyVerification && options.retryRemainingMutation === "metadata-included") { + target.name = "changed" + } + if (heavyVerification && options.retryRemainingMutation === "asset-included") { + target.assets[0].label = "changed" + } + } + return releases + } + const github = Object.freeze({ + getRepository: unsupported, + getAuthenticatedUser: unsupported, + getDefaultBranchSha: unsupported, + getWorkflowState: unsupported, + listNonterminalWorkflowRuns: unsupported, + getAnnotatedTag: unsupported, + async listReleases() { + const current = deriveConsolidationState( + await readPrivateEnvelope(journalPath, DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes), + ) + if (current.phase === "npm-observed") { + if (!npmDurabilityObserved) { + events.push("durable:npm") + npmDurabilityObserved = true + } + events.push("payload:list") + } else { + events.push("list") + } + if (options.convergenceListFailure !== undefined) { + return { + status: "ERROR", + operation: "releases", + httpStatus: options.convergenceListFailure, + code: "HTTP_ERROR", + } + } + return { + status: "PRESENT", + operation: "releases", + httpStatus: 200, + code: null, + value: currentRawReleases({ + heavyVerification: current.phase === "npm-observed", + }), + } + }, + async getRelease({ releaseId }) { + events.push(`direct:${releaseId}`) + convergenceDirectReads += 1 + if (options.absenceOnConvergenceAttempt === convergenceDirectReads) { + deleted = true + } + if (options.convergenceDirectFailure === "timeout") { + throw new Error("simulated timeout") + } + if (Number.isInteger(options.convergenceDirectFailure)) { + return { + status: "ERROR", + operation: "release", + httpStatus: options.convergenceDirectFailure, + code: "HTTP_ERROR", + } + } + const current = await readPrivateEnvelope( + journalPath, + DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes, + ) + const outcome = deriveConsolidationState(current).lastOutcomeClassification + if (outcome !== null) events.splice(-1, 0, `durable:outcome:${outcome}`) + if (deleted) { + return { + status: "AMBIGUOUS", + operation: "release", + httpStatus: 404, + code: "NOT_FOUND", + } + } + const release = currentRawReleases().find(({ id }) => String(id) === releaseId) + return { + status: "PRESENT", + operation: "release", + httpStatus: 200, + code: null, + value: structuredClone(release), + } + }, + async listReleaseAssets(input) { + events.push(`payload-assets:${input.releaseId}`) + return inspection.releaseFixture.github.listReleaseAssets(input) + }, + async downloadReleaseAsset(input) { + events.push(`payload-download:${input.releaseId}:${input.assetId}`) + return inspection.releaseFixture.github.downloadReleaseAsset(input) + }, + }) + const npm = Object.freeze({ + async observePackageVersion({ name, version }) { + assert.equal(version, DUPLICATE_DRAFT_CANDIDATE.version) + assert.equal(CANONICAL_RELEASE_PACKAGE_ORDER.includes(name), true) + events.push(`npm:${name}`) + return { + status: "ABSENT", + operation: "package-version", + httpStatus: 404, + code: "E404", + } + }, + }) + const attestations = Object.freeze({ + async verify(input) { + events.push("payload-attestations") + return inspection.releaseFixture.attestations.verify(input) + }, + }) + const writer = Object.freeze({ + async deleteDuplicate({ releaseId, permit: candidate }) { + assert.equal(releaseId, targetReleaseId) + assert.equal(candidate, permit) + events.push(`delete:${releaseId}`) + const classification = options.deleteClassifications[deleteCalls] + deleteCalls += 1 + if (classification === undefined) throw new Error("unexpected additional delete attempt") + deleted = + ["confirmed-204", "response-404-ambiguous"].includes(classification) && + options.deleteLeavesPresent !== true + return { + classification, + httpStatus: + classification === "confirmed-204" + ? 204 + : classification === "response-404-ambiguous" + ? 404 + : classification === "response-hard-failure" + ? 500 + : null, + observedAt: currentAuthorityTime, + } + }, + }) + const adapters = { + local: Object.freeze({ readState: unsupported }), + github, + npm, + attestations, + writer, + } + Object.defineProperty(adapters, "captureConsolidationAuthority", { + value: Object.freeze(async function capture(input) { + assert.equal(input.adapters, adapters) + authorityCaptures += 1 + events.push(`authority:${input.stage}:${input.targetReleaseId}`) + const beforeAuthority = parseConsolidationEnvelope( + "journal", + await readPrivateEnvelope(journalPath, DUPLICATE_DRAFT_CONSOLIDATION_LIMITS.journalBytes), + ) + currentAuthorityTime = + authorityCaptures === 1 + ? authorityTime + : new Date( + Date.parse(beforeAuthority.record.updatedAt) + + (options.freshAuthorityAdvanceMs ?? 1_000), + ).toISOString() + const networkEpoch = {} + Object.defineProperty(networkEpoch, "consume", { + value: async (consumption) => { + assert.equal(consumption.targetReleaseId, targetReleaseId) + const consumptionState = deriveConsolidationState(consumption.currentJournal) + assert.equal(consumptionState.phase, "delete-authority-observed") + const attemptNumber = consumptionState.attemptNumber + events.push("durable:authority") + journal = appendJournalEvent( + consumption.currentJournal, + "delete-intent", + { + targetReleaseId, + attemptNumber, + authorityEventSha256: consumption.currentJournal.record.events.at(-1).eventSha256, + }, + currentAuthorityTime, + ) + await writePrivateEnvelope( + journalPath, + canonicalConsolidationEnvelopeBytes("journal", journal), + ) + if (adapterFault === "after-intent-journal") { + throw new Error("injected intent journal process loss") + } + await writePrivateEnvelope(headPath, testJournalHeadBytes(journalPath, journal)) + if (adapterFault === "after-intent-head") { + throw new Error("injected intent head process loss") + } + events.push("durable:intent") + permit = Object.freeze({}) + return permit + }, + enumerable: false, + writable: false, + configurable: false, + }) + Object.freeze(networkEpoch) + const freshAuthority = structuredClone(authority) + if (authorityCaptures > 1 && options.freshAuthorityMutation === "asset-included") { + throw new Error("fresh authority rejected included asset drift") + } + if (authorityCaptures > 1 && options.freshAuthorityMutation === "volatile") { + const evidence = freshAuthority.targetRead.evidence + evidence.nodeId = `RE_volatile_retry_${authorityCaptures}` + evidence.createdAt = new Date( + Date.parse(evidence.createdAt) + authorityCaptures * 1_000, + ).toISOString() + evidence.updatedAt = evidence.createdAt + evidence.assets[0].id = String(999_999_000 + authorityCaptures) + evidence.assets[0].nodeId = `RA_volatile_retry_${authorityCaptures}` + evidence.assets[0].createdAt = evidence.createdAt + evidence.assets[0].updatedAt = evidence.updatedAt + evidence.assets[0].downloadCount += 1 + freshAuthority.targetRead.evidenceSha256 = canonicalRecordSha256(evidence) + const releaseIndex = freshAuthority.releases.findIndex(({ id }) => id === targetReleaseId) + freshAuthority.releases[releaseIndex] = structuredClone(evidence) + } + freshAuthority.annotatedTag.observedAt = currentAuthorityTime + freshAuthority.workflowAuthority.observedAt = currentAuthorityTime + freshAuthority.npmInventory.startedAt = currentAuthorityTime + freshAuthority.npmInventory.completedAt = currentAuthorityTime + for (const entry of freshAuthority.npmInventory.packages) + entry.observedAt = currentAuthorityTime + freshAuthority.targetRead.releaseGetStartedAt = currentAuthorityTime + freshAuthority.targetRead.releaseGetCompletedAt = currentAuthorityTime + freshAuthority.targetRead.assetsListStartedAt = currentAuthorityTime + freshAuthority.targetRead.assetsListCompletedAt = currentAuthorityTime + freshAuthority.observedAt = currentAuthorityTime + const captured = { authority: freshAuthority } + Object.defineProperty(captured, "networkEpoch", { + value: networkEpoch, + enumerable: false, + writable: false, + configurable: false, + }) + return Object.freeze(captured) + }), + enumerable: false, + writable: false, + configurable: false, + }) + for (const name of ["captureInspectionTerminal", "assertInspectionTerminalSealed"]) { + Object.defineProperty(adapters, name, { + value: Object.freeze(unsupported), + enumerable: false, + writable: false, + configurable: false, + }) + } + Object.freeze(adapters) + if (options.retryPayloadFailure === true) { + inspection.releaseFixture.failVerification() + } + + return { + events, + waits, + requestBudgets, + authorityTime, + input: { + proposedEnvelope: proposal, + confirmation, + targetReleaseId, + journalPath, + }, + dependencies: Object.freeze({ + createAdapters: Object.freeze(async (requestBudget) => { + adapterFault = null + if (requestBudget !== undefined) requestBudgets.push(requestBudget) + return adapters + }), + wait: Object.freeze(async (milliseconds, { signal }) => { + assert.equal(signal instanceof AbortSignal, true) + waits.push(milliseconds) + }), + }), + dependenciesWithFault(faultAt) { + return Object.freeze({ + createAdapters: Object.freeze(async (requestBudget) => { + adapterFault = faultAt + if (requestBudget !== undefined) requestBudgets.push(requestBudget) + return adapters + }), + wait: Object.freeze(async (milliseconds, { signal }) => { + assert.equal(signal instanceof AbortSignal, true) + waits.push(milliseconds) + }), + faultAt, + }) + }, + dependenciesWithTimeline(monotonicTimeline) { + return Object.freeze({ + createAdapters: Object.freeze(async (requestBudget) => { + adapterFault = null + if (requestBudget !== undefined) requestBudgets.push(requestBudget) + return adapters + }), + wait: Object.freeze(async (milliseconds, { signal }) => { + assert.equal(signal instanceof AbortSignal, true) + waits.push(milliseconds) + }), + monotonicTimeline, + }) + }, + dependenciesWithWallClockTimeline(wallClockTimeline, faultAt) { + return Object.freeze({ + createAdapters: Object.freeze(async (requestBudget) => { + adapterFault = faultAt ?? null + if (requestBudget !== undefined) requestBudgets.push(requestBudget) + return adapters + }), + wait: Object.freeze(async (milliseconds, { signal }) => { + assert.equal(signal instanceof AbortSignal, true) + waits.push(milliseconds) + }), + ...(faultAt === undefined ? {} : { faultAt }), + wallClockTimeline, + }) + }, + } +} + +function deletionAuthorityFixture({ proposal, stage, targetEvidence, observedAt, releases }) { + return { + stage, + controller: structuredClone(proposal.record.controller), + annotatedTag: { + ...structuredClone(proposal.record.annotatedTag), + observedAt, + }, + workflowAuthority: { + ...structuredClone(proposal.record.workflowAuthority), + observedAt, + }, + npmInventory: { + ...structuredClone(proposal.record.npmInventories[1]), + stage, + startedAt: observedAt, + completedAt: observedAt, + packages: proposal.record.npmInventories[1].packages.map((entry) => ({ + ...structuredClone(entry), + observedAt, + })), + }, + releases: structuredClone(releases), + payloadProof: structuredClone(proposal.record.payloadProof), + targetRead: { + releaseGetStartedAt: observedAt, + releaseGetCompletedAt: observedAt, + assetsListStartedAt: observedAt, + assetsListCompletedAt: observedAt, + evidence: structuredClone(targetEvidence), + evidenceSha256: canonicalRecordSha256(targetEvidence), + }, + observedAt, + } +} + +function retryWallClockTimeline(authorityTime, ageMs, { heavyVerificationMs = 0 } = {}) { + const decisionMs = Date.parse(authorityTime) + ageMs + const values = [decisionMs] + if (ageMs > 120_000) { + for (let index = 0; index < CANONICAL_RELEASE_PACKAGE_ORDER.length + 2; index += 1) { + values.push(decisionMs) + } + values.push(decisionMs + heavyVerificationMs) + values.push(decisionMs + 60_000) + } + return Object.freeze(values.map((value) => new Date(value).toISOString())) +} + +function exactConvergenceTimeline(requestDurations) { + assert.equal(requestDurations.length, 12) + let now = 0 + const values = [now] + const requests = [] + let requestIndex = 0 + for (let attempt = 0; attempt < 6; attempt += 1) { + values.push(now) + for (const operation of ["release", "releases"]) { + values.push(now) + requests.push({ operation, timeoutMs: 90_000 - now }) + now += requestDurations[requestIndex++] + values.push(now) + } + if (attempt < 5) { + values.push(now) + const delay = Math.min(CONVERGENCE_BACKOFFS[attempt], 30_000, 90_000 - now) + now += delay + values.push(now) + } + } + return Object.freeze({ + values: Object.freeze(values), + requests: Object.freeze(requests), + completedAt: now, + }) +} + +function testJournalHeadBytes(journalPath, journal) { + return Buffer.from( + `${JSON.stringify({ + schemaVersion: 1, + journalPath, + repository: journal.record.repository, + proposedRecordSha256: journal.record.proposedRecordSha256, + journalRecordSha256: journal.recordSha256, + lastEventSha256: journal.record.events.at(-1).eventSha256, + sequence: journal.record.events.length, + updatedAt: journal.record.updatedAt, + })}\n`, + "utf8", + ) +} diff --git a/scripts/release/test/support/duplicate-draft-consolidation-authorized-delete.mjs b/scripts/release/test/support/duplicate-draft-consolidation-authorized-delete.mjs new file mode 100644 index 000000000..fb35092ed --- /dev/null +++ b/scripts/release/test/support/duplicate-draft-consolidation-authorized-delete.mjs @@ -0,0 +1,312 @@ +import { createHash } from "node:crypto" +import { mkdir, mkdtemp, realpath } from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +import { createDuplicateDraftConsolidationAdapters } from "../../duplicate-draft-consolidation-adapters.mjs" +import { captureConsolidationAuthority } from "../../duplicate-draft-consolidation-authority.mjs" +import { inspectEquivalentDrafts } from "../../duplicate-draft-consolidation-evidence.mjs" +import { writePrivateEnvelope } from "../../duplicate-draft-consolidation-files.mjs" +import { + appendJournalEvent, + createConsolidationJournal, +} from "../../duplicate-draft-consolidation-journal.mjs" +import { + canonicalConsolidationEnvelopeBytes, + createConsolidationEnvelope, +} from "../../duplicate-draft-consolidation-schema.mjs" +import { CANONICAL_RELEASE_PACKAGE_ORDER } from "../../manifest.mjs" +import { + createDuplicateDraftConsolidationFixture, + DUPLICATE_DRAFT_CANDIDATE, + DUPLICATE_DRAFT_IDS, + DUPLICATE_DRAFT_SURVIVOR_ID, +} from "./duplicate-draft-consolidation-fixture.mjs" + +const BASE_TIME = Date.parse("2026-09-01T12:34:55.000Z") +const REPOSITORY_ID = "1210070282" +const ACTOR = Object.freeze({ login: "blove", id: "61436" }) +const TAG_OBJECT_SHA = "123456789abcdef0123456789abcdef012345678" +const WORKFLOW_ID = "202458345" + +export async function createAuthorizedDeleteHarness({ fetchImpl, deleteNow }) { + const evidenceFixture = createDuplicateDraftConsolidationFixture() + const inspected = await inspectEquivalentDrafts({ + candidate: evidenceFixture.candidate, + survivorId: evidenceFixture.survivorId, + duplicateIds: evidenceFixture.duplicateIds, + releases: evidenceFixture.releases, + github: evidenceFixture.github, + attestations: evidenceFixture.attestations, + }) + evidenceFixture.clearOperations() + let nowMs = BASE_TIME + let clock = () => new Date(nowMs).toISOString() + const remainingReleases = evidenceFixture.releases.map((release) => structuredClone(release)) + const directRelease = structuredClone( + remainingReleases.find(({ id }) => String(id) === DUPLICATE_DRAFT_IDS[0]), + ) + const annotatedTag = { + name: DUPLICATE_DRAFT_CANDIDATE.tag, + objectSha: TAG_OBJECT_SHA, + targetSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + objectType: "tag", + observedAt: new Date(BASE_TIME).toISOString(), + } + const proposal = createConsolidationEnvelope("proposed", { + schemaVersion: 1, + repository: { + name: "cacheplane/dawnai", + id: REPOSITORY_ID, + defaultBranch: "main", + actor: { ...ACTOR }, + }, + controller: { + headSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + originMainSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + githubMainSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + }, + candidate: DUPLICATE_DRAFT_CANDIDATE, + roles: { + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + duplicates: [...DUPLICATE_DRAFT_IDS], + }, + confirmation: { + version: DUPLICATE_DRAFT_CANDIDATE.version, + commitSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + survivor: DUPLICATE_DRAFT_SURVIVOR_ID, + duplicates: [...DUPLICATE_DRAFT_IDS], + template: "Consolidate <64-lowercase-hex-digest>", + }, + annotatedTag, + workflowAuthority: { + workflowId: WORKFLOW_ID, + path: ".github/workflows/release.yml", + state: "disabled_manually", + query: workflowQuery(), + nonterminalRuns: [], + observedAt: new Date(BASE_TIME).toISOString(), + }, + npmInventories: [npmInventory("inspect-initial"), npmInventory("inspect-ready")], + releases: inspected.releases, + payloadProof: inspected.payloadProof, + inspectedAt: new Date(BASE_TIME).toISOString(), + }) + const root = await mkdtemp(path.join(await realpath(os.tmpdir()), "dawn-authorized-delete-")) + await mkdir(path.join(root, ".dawn", "release"), { recursive: true }) + const adapters = await createDuplicateDraftConsolidationAdapters({ + cwd: root, + token: "github_test_token_123456789", + environment: { HOME: root, PATH: "/tools" }, + dependencies: { + fetchImpl: async (url, init) => { + const parsed = new URL(url) + if (init.method === "DELETE") return fetchImpl(url, init) + if (parsed.pathname === "/repos/cacheplane/dawnai") { + return jsonResponse({ + id: Number(REPOSITORY_ID), + full_name: "cacheplane/dawnai", + default_branch: "main", + }) + } + if (parsed.pathname === "/user") { + return jsonResponse({ login: ACTOR.login, id: Number(ACTOR.id) }) + } + if (parsed.pathname.endsWith("/runs")) { + return jsonResponse({ total_count: 0, workflow_runs: [] }) + } + throw new Error("unexpected authorized-delete network request") + }, + now: () => clock(), + run: async (command, args) => { + if (command !== "git") throw new Error("unexpected command") + if (args[0] === "symbolic-ref") return commandResult("main\n") + if (args[0] === "status") return commandResult("") + if (args[0] === "rev-parse") { + return commandResult(`${DUPLICATE_DRAFT_CANDIDATE.commitSha}\n`) + } + throw new Error("unexpected git command") + }, + createOwnerPreflightAdapters: () => ({ + git: { headSha: async () => DUPLICATE_DRAFT_CANDIDATE.commitSha }, + }), + createGitHubReader: () => ({ + async getRef({ ref }) { + if (ref === "heads/main") { + return present("ref", { + ref: "refs/heads/main", + object: { + type: "commit", + sha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + }, + }) + } + return present("ref", { + ref: `refs/tags/${annotatedTag.name}`, + object: { type: "tag", sha: annotatedTag.objectSha }, + }) + }, + async getGitTag() { + return present("git-tag", { + sha: annotatedTag.objectSha, + tag: annotatedTag.name, + object: { + type: "commit", + sha: annotatedTag.targetSha, + }, + }) + }, + async getWorkflow() { + return present("workflow", { + id: WORKFLOW_ID, + path: ".github/workflows/release.yml", + state: "disabled_manually", + }) + }, + async listReleases() { + return present("releases", structuredClone(remainingReleases)) + }, + async downloadReleaseAsset(input) { + return evidenceFixture.github.downloadReleaseAsset(input) + }, + async getRelease({ releaseId }) { + if (String(releaseId) !== DUPLICATE_DRAFT_IDS[0]) { + throw new Error("unexpected direct target") + } + return present("release", structuredClone(directRelease)) + }, + async listReleaseAssets({ releaseId }) { + const selected = remainingReleases.find(({ id }) => String(id) === String(releaseId)) + if (selected === undefined) throw new Error("unexpected Release assets") + return present("release-assets", structuredClone(selected.assets)) + }, + }), + createNpmReader: () => ({ + async observePackageVersion() { + nowMs += 1 + return absent() + }, + }), + createCliAttestationVerifier: () => ({ + verify: (input) => evidenceFixture.attestations.verify(input), + }), + }, + }) + const captured = await captureConsolidationAuthority({ + stage: "pre-delete-1", + proposal: proposal.record, + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + adapters, + }) + const confirmation = exactConfirmation(proposal) + const confirmationSha256 = createHash("sha256").update(confirmation, "utf8").digest("hex") + let journal = createConsolidationJournal({ + proposedEnvelope: proposal, + confirmationSha256, + recordedAt: captured.authority.observedAt, + }) + const journalPath = path.join( + root, + ".dawn", + "release", + "duplicate-draft-consolidation.journal.json", + ) + const journalHeadPath = path.join( + root, + ".dawn", + "release", + "duplicate-draft-consolidation.journal.head.json", + ) + await writePrivateEnvelope(journalPath, canonicalConsolidationEnvelopeBytes("journal", journal)) + await writePrivateEnvelope(journalHeadPath, journalHeadBytes(journalPath, journal)) + journal = appendJournalEvent( + journal, + "delete-authority-observed", + { + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + attemptNumber: 1, + authority: captured.authority, + }, + captured.authority.observedAt, + ) + await writePrivateEnvelope(journalPath, canonicalConsolidationEnvelopeBytes("journal", journal)) + const permit = await captured.networkEpoch.consume({ + authority: captured.authority, + proposal: proposal.record, + confirmation, + targetReleaseId: DUPLICATE_DRAFT_IDS[0], + intentPath: journalPath, + currentJournal: journal, + }) + clock = deleteNow + return { adapters, permit, root } +} + +function exactConfirmation(proposal) { + const { candidate, roles } = proposal.record + return `CONSOLIDATE v${candidate.version} ${candidate.commitSha} SURVIVOR ${roles.survivor} DELETE ${roles.duplicates.join(",")} PROPOSAL ${proposal.recordSha256}` +} + +function journalHeadBytes(journalPath, journal) { + return Buffer.from( + `${JSON.stringify({ + schemaVersion: 1, + journalPath, + repository: journal.record.repository, + proposedRecordSha256: journal.record.proposedRecordSha256, + journalRecordSha256: journal.recordSha256, + lastEventSha256: journal.record.events.at(-1).eventSha256, + sequence: journal.record.events.length, + updatedAt: journal.record.updatedAt, + })}\n`, + "utf8", + ) +} + +function workflowQuery() { + return { + statuses: ["in_progress", "pending", "queued", "requested", "waiting"], + perPage: 100, + maximumPages: 100, + } +} + +function npmInventory(stage) { + return { + stage, + startedAt: new Date(BASE_TIME).toISOString(), + completedAt: new Date(BASE_TIME).toISOString(), + packages: CANONICAL_RELEASE_PACKAGE_ORDER.map((name) => ({ + name, + version: DUPLICATE_DRAFT_CANDIDATE.version, + status: "ABSENT", + httpStatus: 404, + code: "E404", + observedAt: new Date(BASE_TIME).toISOString(), + })), + } +} + +function absent() { + return { + status: "ABSENT", + operation: "package-version", + httpStatus: 404, + code: "E404", + } +} + +function present(operation, value) { + return { status: "PRESENT", operation, httpStatus: 200, code: null, value } +} + +function jsonResponse(value) { + return new Response(JSON.stringify(value), { + status: 200, + headers: { "content-type": "application/json" }, + }) +} + +function commandResult(stdout) { + return { exitCode: 0, stdout, stderr: "" } +} diff --git a/scripts/release/test/support/duplicate-draft-consolidation-fixture.mjs b/scripts/release/test/support/duplicate-draft-consolidation-fixture.mjs new file mode 100644 index 000000000..24f71d1cf --- /dev/null +++ b/scripts/release/test/support/duplicate-draft-consolidation-fixture.mjs @@ -0,0 +1,330 @@ +import { createHash } from "node:crypto" + +import { CANONICAL_RELEASE_PACKAGE_ORDER, canonicalManifestBytes } from "../../manifest.mjs" +import { canonicalBaseAssetSet, canonicalReleaseBody } from "../../metadata.mjs" +import { createReleaseRecord, releaseRecordSha256 } from "../../release-record.mjs" + +export const DUPLICATE_DRAFT_CANDIDATE = Object.freeze({ + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + tag: "v0.8.22", +}) +export const DUPLICATE_DRAFT_SURVIVOR_ID = "379991871" +export const DUPLICATE_DRAFT_IDS = Object.freeze(["379982100", "379986168"]) + +const REPOSITORY = "cacheplane/dawnai" +const AUTHOR = Object.freeze({ + login: "blove", + id: 61436, + node_id: "MDQ6VXNlcjYxNDM2", +}) + +export function createDuplicateDraftConsolidationFixture() { + const artifact = createArtifact() + const attestation = createAttestation(artifact) + const base = canonicalBaseAssetSet({ + record: artifact.record, + artifact: artifact.artifact, + attestationSet: attestation.set, + bundles: attestation.bundles, + }) + const marker = { + schemaVersion: 1, + epoch: "fixed-group-v1", + revision: 2, + phase: "ESCROWED", + version: DUPLICATE_DRAFT_CANDIDATE.version, + commitSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + tag: DUPLICATE_DRAFT_CANDIDATE.tag, + manifestSha256: sha256(canonicalManifestBytes(artifact.manifest)), + releaseRecordSha256: releaseRecordSha256(artifact.record), + baseAssetSetSha256: base.sha256, + attestationSet: attestation.set, + npmEvidenceSha256: null, + smoke: null, + audit: null, + abandonmentSha256: null, + } + const body = canonicalReleaseBody({ marker, manifest: artifact.manifest }) + const bytesByName = new Map( + base.assets.map((entry) => [entry.name, Buffer.from(entry.contentBase64, "base64")]), + ) + const identities = [ + { + id: DUPLICATE_DRAFT_SURVIVOR_ID, + nodeId: "RE_survivor", + tagName: "untagged-be0ff4bee4ba43b521a9", + }, + { + id: DUPLICATE_DRAFT_IDS[0], + nodeId: "RE_duplicate_one", + tagName: "untagged-a13939767dd2419ade01", + }, + { + id: DUPLICATE_DRAFT_IDS[1], + nodeId: "RE_duplicate_two", + tagName: "untagged-20706099efa3c38335a8", + }, + ] + const payloads = new Map() + const releases = identities.map((identity, releaseIndex) => { + const assets = base.assets.map((entry, assetIndex) => { + const bytes = bytesByName.get(entry.name) + const id = String(900_000 + releaseIndex * 100 + assetIndex + 1) + payloads.set(`${identity.id}:${entry.name}`, Buffer.from(bytes)) + return { + id: Number(id), + node_id: `RA_${releaseIndex}_${assetIndex}`, + name: entry.name, + label: entry.name === "manifest.json" ? "sealed manifest" : null, + state: "uploaded", + content_type: contentType(entry.name), + size: bytes.byteLength, + digest: `sha256:${entry.sha256}`, + uploader: { ...AUTHOR }, + created_at: `2026-08-31T0${releaseIndex}:01:${String(assetIndex).padStart(2, "0")}Z`, + updated_at: `2026-08-31T0${releaseIndex}:02:${String(assetIndex).padStart(2, "0")}Z`, + download_count: releaseIndex + assetIndex, + browser_download_url: `https://github.invalid/releases/assets/${id}`, + } + }) + assets.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) + return { + id: Number(identity.id), + node_id: identity.nodeId, + tag_name: identity.tagName, + name: `Dawn v${DUPLICATE_DRAFT_CANDIDATE.version}`, + target_commitish: "main", + draft: true, + immutable: false, + prerelease: false, + published_at: null, + body, + author: { ...AUTHOR }, + created_at: `2026-08-31T0${releaseIndex}:00:00Z`, + updated_at: `2026-08-31T0${releaseIndex}:30:00Z`, + assets, + html_url: `https://github.invalid/releases/${identity.id}`, + } + }) + + let downloadCount = 0 + let failVerification = false + const operations = [] + const github = Object.freeze({ + async downloadReleaseAsset({ assetId, maximumBytes, releaseId }) { + operations.push(`download:${releaseId}:${assetId}`) + downloadCount += 1 + const release = releases.find(({ id }) => String(id) === String(releaseId)) + const asset = release?.assets.find(({ id }) => String(id) === String(assetId)) + const bytes = + asset === undefined ? undefined : payloads.get(`${String(releaseId)}:${asset.name}`) + if (bytes === undefined || bytes.byteLength > maximumBytes) { + throw new Error("fixture asset download request is invalid") + } + return present("release-asset-download", { + contentBase64: bytes.toString("base64"), + }) + }, + async getRelease({ releaseId }) { + operations.push(`get:${releaseId}`) + const release = releases.find(({ id }) => String(id) === String(releaseId)) + if (release === undefined) throw new Error("fixture Release does not exist") + return present("release", { value: structuredClone(release) }) + }, + async listReleaseAssets({ releaseId }) { + operations.push(`list-assets:${releaseId}`) + const release = releases.find(({ id }) => String(id) === String(releaseId)) + if (release === undefined) throw new Error("fixture Release does not exist") + return present("release-assets", { + value: structuredClone(release.assets), + }) + }, + }) + const attestations = Object.freeze({ + async verify({ subjects }) { + if (failVerification) throw new Error("fixture verification failure") + return { status: "VERIFIED", subjects } + }, + }) + + return { + candidate: DUPLICATE_DRAFT_CANDIDATE, + survivorId: DUPLICATE_DRAFT_SURVIVOR_ID, + duplicateIds: [...DUPLICATE_DRAFT_IDS], + releases, + github, + attestations, + expectedBaseAssetSet: base.assets.map(({ name, sha256: digest }) => ({ + name, + sha256: digest, + })), + get downloadCount() { + return downloadCount + }, + get operations() { + return [...operations] + }, + clearOperations() { + operations.length = 0 + }, + failVerification() { + failVerification = true + }, + replaceMarker(mutator) { + const next = structuredClone(marker) + mutator(next) + const nextBody = canonicalReleaseBody({ + marker: next, + manifest: artifact.manifest, + }) + for (const release of releases) release.body = nextBody + }, + assetBytes(releaseId, name) { + const bytes = payloads.get(`${String(releaseId)}:${name}`) + if (bytes === undefined) throw new Error("fixture asset is missing") + return Buffer.from(bytes) + }, + replaceAssetBytes(releaseId, name, bytes, { updateMetadata = false } = {}) { + const release = releases.find(({ id }) => String(id) === String(releaseId)) + const asset = release?.assets.find((entry) => entry.name === name) + if (asset === undefined) throw new Error("fixture asset is missing") + const replacement = Buffer.from(bytes) + payloads.set(`${String(releaseId)}:${asset.name}`, replacement) + if (updateMetadata) { + asset.size = replacement.byteLength + asset.digest = `sha256:${sha256(replacement)}` + } + }, + } +} + +function createArtifact() { + const packageFiles = [] + const packages = CANONICAL_RELEASE_PACKAGE_ORDER.map((name) => { + const filename = `${name.replace(/^@/u, "").replace("/", "-")}-${DUPLICATE_DRAFT_CANDIDATE.version}.tgz` + const bytes = Buffer.from(`package:${name}:${DUPLICATE_DRAFT_CANDIDATE.commitSha}\n`, "utf8") + packageFiles.push({ name: filename, bytes }) + const sha512 = createHash("sha512").update(bytes).digest("hex") + return { + name, + version: DUPLICATE_DRAFT_CANDIDATE.version, + filename, + size: bytes.byteLength, + sha256: sha256(bytes), + sha512, + npmIntegrity: `sha512-${Buffer.from(sha512, "hex").toString("base64")}`, + access: "public", + } + }) + const manifest = { + schemaVersion: 1, + version: DUPLICATE_DRAFT_CANDIDATE.version, + commitSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + ci: { workflow: "CI", runId: 8001, runAttempt: 1 }, + artifact: { + name: `release-v${DUPLICATE_DRAFT_CANDIDATE.version}-${DUPLICATE_DRAFT_CANDIDATE.commitSha.slice(0, 12)}`, + prepareRunId: 8002, + prepareRunAttempt: 1, + }, + packageOrder: [...CANONICAL_RELEASE_PACKAGE_ORDER], + packages, + } + const manifestBytes = canonicalManifestBytes(manifest) + const record = createReleaseRecord({ + candidate: { + version: DUPLICATE_DRAFT_CANDIDATE.version, + commitSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + ciWorkflow: "CI", + ciCheck: "validate", + publisherWorkflow: ".github/workflows/release.yml", + }, + manifestSha256: sha256(manifestBytes), + artifact: { name: manifest.artifact.name }, + artifactUpload: { id: "8003", digest: `sha256:${"a".repeat(64)}` }, + prepareRun: { id: 8002, attempt: 1 }, + }) + return { + manifest, + record, + artifact: { + manifest, + files: [{ name: "manifest.json", bytes: manifestBytes }, ...packageFiles], + }, + } +} + +function createAttestation(artifact) { + const subjects = artifact.artifact.files.map(({ name, bytes }) => ({ + name, + sha256: sha256(bytes), + })) + const statement = { + _type: "https://in-toto.io/Statement/v1", + subject: subjects.map(({ name, sha256: digest }) => ({ + name, + digest: { sha256: digest }, + })), + predicateType: "https://slsa.dev/provenance/v1", + predicate: { + runDetails: { + metadata: { + invocationId: "https://github.com/cacheplane/dawnai/actions/runs/8004/attempts/1", + }, + }, + }, + } + const bundleBytes = Buffer.from( + `${JSON.stringify({ + mediaType: "application/vnd.dev.sigstore.bundle.v0.3+json", + verificationMaterial: {}, + dsseEnvelope: { + payloadType: "application/vnd.in-toto+json", + payload: Buffer.from(JSON.stringify(statement), "utf8").toString("base64"), + signatures: [{ sig: "fixture-signature" }], + }, + })}\n`, + "utf8", + ) + const bundleSha256 = sha256(bundleBytes) + return { + set: { + repository: REPOSITORY, + workflow: ".github/workflows/release.yml", + sourceRef: `refs/tags/${DUPLICATE_DRAFT_CANDIDATE.tag}`, + commitSha: DUPLICATE_DRAFT_CANDIDATE.commitSha, + workflowRunId: 8004, + runAttempt: 1, + subjects: subjects.map(({ name, sha256: digest }) => ({ + subjectName: name, + subjectSha256: digest, + bundleName: `${name}.intoto.jsonl`, + bundleSha256, + })), + }, + bundles: subjects.map(({ name }) => ({ + name: `${name}.intoto.jsonl`, + bytes: bundleBytes, + })), + } +} + +function present(operation, fields) { + return { + status: "PRESENT", + operation, + httpStatus: 200, + code: null, + ...fields, + } +} + +function contentType(name) { + if (name.endsWith(".tgz")) return "application/gzip" + if (name.endsWith(".jsonl")) return "application/jsonl" + return "application/json" +} + +function sha256(bytes) { + return createHash("sha256").update(bytes).digest("hex") +} diff --git a/scripts/release/test/support/duplicate-draft-consolidation-process-loss-child.mjs b/scripts/release/test/support/duplicate-draft-consolidation-process-loss-child.mjs new file mode 100644 index 000000000..f50cf8583 --- /dev/null +++ b/scripts/release/test/support/duplicate-draft-consolidation-process-loss-child.mjs @@ -0,0 +1,257 @@ +import assert from "node:assert/strict" +import { readFileSync, writeFileSync } from "node:fs" +import { mkdir, readFile } from "node:fs/promises" +import path from "node:path" +import { + performDuplicateDraftConsolidation, + performOneDuplicateDeletion, +} from "../../duplicate-draft-consolidation.mjs" +import { createDuplicateDraftConsolidationAdapters } from "../../duplicate-draft-consolidation-adapters.mjs" +import { runDuplicateDraftConsolidationCli } from "../../duplicate-draft-consolidation-cli.mjs" +import { parseConsolidationEnvelope } from "../../duplicate-draft-consolidation-schema.mjs" +import { + createDuplicateDraftConsolidationFixture, + DUPLICATE_DRAFT_CANDIDATE, + DUPLICATE_DRAFT_IDS, + DUPLICATE_DRAFT_SURVIVOR_ID, +} from "./duplicate-draft-consolidation-fixture.mjs" + +const CONTROLLER_SHA = "b".repeat(40) +const PROPOSAL = ".dawn/release/duplicate-draft-consolidation.proposed.json" +const JOURNAL = ".dawn/release/duplicate-draft-consolidation.journal.json" +const RECEIPT = "scripts/release/duplicate-draft-consolidation.json" +const [mode, root, statePath, readyPath] = process.argv.slice(2) + +if (mode === "init") { + await mkdir(path.join(root, ".dawn", "release"), { recursive: true }) + await mkdir(path.join(root, "scripts", "release"), { recursive: true }) + saveState({ + armBeforeDelete: false, + deleteEffects: [], + deleted: [], + nowMs: Date.now() + 60 * 60_000, + }) + process.exit(0) +} +if (mode === "hang") await new Promise(() => setInterval(() => {}, 1_000)) +if (mode === "flood") { + process.stdout.write("x".repeat(128 * 1024)) + await new Promise(() => setInterval(() => {}, 1_000)) +} + +const fixture = createDuplicateDraftConsolidationFixture() +const state = () => JSON.parse(readFileSync(statePath, "utf8")) +function saveState(value) { + writeFileSync(statePath, `${JSON.stringify(value)}\n`, { mode: 0o600 }) +} +const updateState = (operation) => { + const current = state() + operation(current) + saveState(current) + return current +} +const present = (operation, value) => ({ + status: "PRESENT", + operation, + httpStatus: 200, + code: null, + value, +}) +const currentReleases = () => { + const deleted = new Set(state().deleted) + return fixture.releases + .filter(({ id }) => !deleted.has(String(id))) + .map((release) => structuredClone(release)) +} +const githubReader = { + async getRef({ ref }) { + if (ref === "heads/main") { + return present("ref", { + ref: "refs/heads/main", + object: { type: "commit", sha: CONTROLLER_SHA }, + }) + } + return present("ref", { + ref: `refs/tags/${DUPLICATE_DRAFT_CANDIDATE.tag}`, + object: { type: "tag", sha: "a".repeat(40) }, + }) + }, + async getGitTag({ tagSha }) { + return present("git-tag", { + sha: tagSha, + tag: DUPLICATE_DRAFT_CANDIDATE.tag, + object: { type: "commit", sha: DUPLICATE_DRAFT_CANDIDATE.commitSha }, + }) + }, + async getWorkflow() { + return present("workflow", { + id: 202_458_345, + path: ".github/workflows/release.yml", + state: "disabled_manually", + }) + }, + async listReleases() { + return present("releases", currentReleases()) + }, + async getRelease({ releaseId }) { + const release = currentReleases().find(({ id }) => String(id) === String(releaseId)) + return release === undefined + ? { + status: "AMBIGUOUS", + operation: "release", + httpStatus: 404, + code: "NOT_FOUND", + } + : present("release", release) + }, + async listReleaseAssets({ releaseId }) { + const release = currentReleases().find(({ id }) => String(id) === String(releaseId)) + assert.ok(release) + return present("release-assets", release.assets) + }, + async downloadReleaseAsset(input) { + return fixture.github.downloadReleaseAsset(input) + }, +} +const fetchImpl = async (url, init = {}) => { + const target = String(url) + if (init.method === "DELETE") { + const releaseId = target.split("/").at(-1) + const current = state() + if (current.armBeforeDelete === true) { + writeFileSync(readyPath, `${process.pid}\n`, { mode: 0o600 }) + await new Promise(() => {}) + } + assert.equal(current.deleted.includes(releaseId), false) + updateState((next) => { + next.deleted.push(releaseId) + next.deleteEffects.push(releaseId) + }) + return new Response(null, { status: 204 }) + } + if (target.endsWith("/repos/cacheplane/dawnai")) { + return jsonResponse({ + id: 1_210_070_282, + full_name: "cacheplane/dawnai", + default_branch: "main", + }) + } + if (target.endsWith("/user")) return jsonResponse({ id: 61_436, login: "blove" }) + if (target.includes("/actions/workflows/") && target.includes("/runs?")) { + return jsonResponse({ total_count: 0, workflow_runs: [] }) + } + throw new Error(`unexpected process-loss request ${target}`) +} +const now = () => { + const current = updateState((next) => { + next.nowMs += 1 + }) + return new Date(current.nowMs).toISOString() +} +const run = async (_command, args) => { + if (args[0] === "symbolic-ref") return { exitCode: 0, stdout: "main\n", stderr: "" } + if (args[0] === "status") return { exitCode: 0, stdout: "", stderr: "" } + if (args[0] === "rev-parse") return { exitCode: 0, stdout: `${CONTROLLER_SHA}\n`, stderr: "" } + throw new Error(`unexpected process-loss command ${args.join(" ")}`) +} +const createAdapters = ({ cwd }) => + createDuplicateDraftConsolidationAdapters({ + cwd, + token: "fixture_token_value", + environment: { HOME: root, PATH: "/tools" }, + dependencies: { + fetchImpl, + run, + now, + createGitHubReader: () => githubReader, + createOwnerPreflightAdapters: () => ({ + git: { headSha: async () => CONTROLLER_SHA }, + }), + createNpmReader: () => ({ + observePackageVersion: async () => ({ + status: "ABSENT", + operation: "package-version", + httpStatus: 404, + code: "E404", + }), + }), + createCliAttestationVerifier: () => ({ + verify: (input) => fixture.attestations.verify(input), + }), + }, + }) +const wait = async (milliseconds, { signal }) => { + assert.equal(signal.aborted, false) + updateState((next) => { + next.nowMs += milliseconds + }) +} + +const argv = + mode === "inspect" + ? [ + "inspect", + "--version", + DUPLICATE_DRAFT_CANDIDATE.version, + "--commit-sha", + DUPLICATE_DRAFT_CANDIDATE.commitSha, + "--survivor", + DUPLICATE_DRAFT_SURVIVOR_ID, + "--duplicates", + DUPLICATE_DRAFT_IDS.join(","), + "--output", + PROPOSAL, + ] + : mode === "verify" + ? ["verify", "--receipt", RECEIPT] + : [ + "perform", + "--proposal", + PROPOSAL, + "--journal", + JOURNAL, + "--receipt", + RECEIPT, + "--confirmation", + await confirmation(), + ] + +process.exitCode = await runDuplicateDraftConsolidationCli({ + argv, + cwd: root, + environment: {}, + stdout: process.stdout, + stderr: process.stderr, + dependencies: { + createAdapters, + now, + wait, + async perform(input, dependencies) { + return performDuplicateDraftConsolidation(input, { + ...dependencies, + performOneDeletion(deletionInput, deletionDependencies) { + if (mode !== "resume") { + return performOneDuplicateDeletion(deletionInput, deletionDependencies) + } + const future = new Date(state().nowMs + 90_000).toISOString() + return performOneDuplicateDeletion(deletionInput, { + ...deletionDependencies, + wallClockTimeline: Object.freeze(Array.from({ length: 256 }, () => future)), + }) + }, + }) + }, + }, +}) + +async function confirmation() { + const proposal = parseConsolidationEnvelope("proposed", await readFile(path.join(root, PROPOSAL))) + return `CONSOLIDATE v${proposal.record.candidate.version} ${proposal.record.candidate.commitSha} SURVIVOR ${proposal.record.roles.survivor} DELETE ${proposal.record.roles.duplicates.join(",")} PROPOSAL ${proposal.recordSha256}` +} + +function jsonResponse(value) { + return new Response(JSON.stringify(value), { + status: 200, + headers: { "content-type": "application/json" }, + }) +} diff --git a/scripts/release/test/workflow-contracts.test.mjs b/scripts/release/test/workflow-contracts.test.mjs index baa125fde..af3bca7e1 100644 --- a/scripts/release/test/workflow-contracts.test.mjs +++ b/scripts/release/test/workflow-contracts.test.mjs @@ -11,13 +11,13 @@ import { symlink, writeFile, } from "node:fs/promises" +import { createRequire } from "node:module" import os from "node:os" import path from "node:path" import test from "node:test" import { fileURLToPath } from "node:url" -import { parse } from "yaml" - +import { parse, stringify } from "yaml" import { classifyReleaseWorkflowAbandonment } from "../abandonment-reachability.mjs" import { ARTIFACT_STORE_SPARSE_FILES } from "../artifact-store.mjs" import { readBoundedFixture } from "../fixture-io.mjs" @@ -25,6 +25,11 @@ import { PUBLISHER_SPARSE_FILES } from "../publisher.mjs" import { REQUIRED_RELEASE_SMOKE_LANES } from "../smoke-result.mjs" const ROOT = fileURLToPath(new URL("../../..", import.meta.url)) +const requireFromCore = createRequire(path.join(ROOT, "packages", "core", "package.json")) +const typescript = requireFromCore("typescript") +if (typescript.version !== "6.0.2" || typeof typescript.createSourceFile !== "function") { + throw new Error("The packages/core TypeScript compiler parser is unavailable") +} const WORKFLOWS = path.join(ROOT, ".github/workflows") const CONTROLLER_SCHEMA_PATH = path.join(ROOT, "scripts/release/controller-schema.json") const ENTRYPOINT_ALLOWLIST_PATH = path.join( @@ -40,6 +45,9 @@ const EXECUTABLE_ALLOWLIST = JSON.parse( ) const SCRIPT_PIN_FIXTURE = "scripts/release/test/fixtures/release-script-hashes.json" const SCRIPT_PIN_PATH = path.join(ROOT, SCRIPT_PIN_FIXTURE) +// Exact fixture bytes at Task 11's starting HEAD e5cf1986c0f2cb2f55b891a7c92fa7291289dfdb. +const STARTING_SCRIPT_PIN_SHA256 = + "b6d939f6ad17ffa011f600fda31792716c73baf5a7cd4e3540dfbe30c75d727c" const SHA256_HEX = /^[0-9a-f]{64}$/u const workflowExpression = (value) => `\${{ ${value} }}` const SCRIPT_REFERENCE = /(?:^|[\s;&|"'(])(scripts\/[\w.-]+(?:\/[\w.-]+)*)/gu @@ -120,6 +128,617 @@ test("final release ownership is switched atomically and legacy owners are absen assert.equal(typeof sources["publish-chart.yml"], "string", "chart publication remains owned") }) +test("duplicate-draft consolidation stays isolated from every workflow and preserves release pins", async () => { + const sources = await readWorkflowSourcesFromRoot(ROOT) + assert.ok(Object.keys(sources).length > FINAL_WORKFLOW_FILES.length) + await assertNoDuplicateDraftWorkflowMutationFromRoot(ROOT) + + const pinBytes = await readFile(SCRIPT_PIN_PATH) + assert.equal(createHash("sha256").update(pinBytes).digest("hex"), STARTING_SCRIPT_PIN_SHA256) +}) + +test("workflow isolation rejects Release DELETE bypasses in each execution context", async (t) => { + const unsafe = [ + [ + "gh interpolated repository", + runWorkflow(`gh api --method DELETE repos/\${{ github.repository }}/releases/379982100`), + ], + [ + "curl compact method", + runWorkflow( + "curl -XDELETE https://api.github.com/repos/cacheplane/dawnai/releases/379982100", + ), + ], + [ + "curl spaced method", + runWorkflow( + "curl -x DeLeTe https://api.github.com/repos/cacheplane/dawnai/releases/379982100", + ), + ], + [ + "curl request method", + runWorkflow( + "curl --request DELETE https://api.github.com/repos/cacheplane/dawnai/releases/379982100", + ), + ], + [ + "curl request equals method", + runWorkflow( + "curl --request=delete https://api.github.com/repos/cacheplane/dawnai/releases/379982100", + ), + ], + [ + "interpolated owner and repository name", + runWorkflow( + `gh api --method DELETE repos/\${{ github.repository_owner }}/\${{ vars.repository_name }}/releases/\${{ inputs.release_id }}`, + ), + ], + [ + "literal multiline api url", + runWorkflow(`gh api \\ + --method + DELETE \\ + "\${{ github.api_url }}/repos/\${{ github.repository }}/releases/379982100"`), + ], + [ + "folded multiline api url", + runWorkflow( + `gh api --method +DELETE +repos/\${{ github.repository }}/releases/379982100`, + ">", + ), + ], + [ + "action inputs", + actionWorkflow({ + method: "DELETE", + endpoint: `/repos/\${{ github.repository }}/releases/\${{ inputs.release_id }}`, + }), + ], + [ + "step environment", + runWorkflow('curl --request "$HTTP_METHOD" "$RELEASE_ENDPOINT"', "|", { + HTTP_METHOD: "DELETE", + RELEASE_ENDPOINT: `\${{ github.api_url }}/repos/\${{ github.repository }}/releases/379982100`, + }), + ], + [ + "job environment", + runWorkflow('curl -X "$HTTP_METHOD" "$RELEASE_ENDPOINT"', "|", undefined, { + HTTP_METHOD: "DELETE", + RELEASE_ENDPOINT: "/repos/cacheplane/dawnai/releases/379982100", + }), + ], + [ + "reusable workflow inputs", + reusableWorkflow({ + "http-method": "delete", + url: `\${{ github.api_url }}/repos/\${{ github.repository }}/releases/379982100`, + }), + ], + [ + "Octokit deleteRelease call", + runWorkflow("await github.rest.repos.deleteRelease({ owner, repo, release_id })"), + ], + [ + "optional and spaced Octokit deleteRelease call", + runWorkflow( + "await github ?. rest ?. repos ?. deleteRelease ?. ({ owner, repo, release_id })", + ), + ], + [ + "GitHub request route template", + runWorkflow( + "await github.request('DELETE /repos/{owner}/{repo}/releases/{release_id}', options)", + ), + ], + [ + "optional GitHub request route template", + runWorkflow( + 'await github?.request?.("delete /repos/{owner}/{repo}/releases/{release_id}", options)', + ), + ], + [ + "GitHub request object", + runWorkflow( + "await github.request({ method: 'DELETE', url: '/repos/{owner}/{repo}/releases/{release_id}' })", + ), + ], + [ + "shell braced path variables", + runWorkflow(`gh api --method DELETE "repos/\${GITHUB_REPOSITORY}/releases/\${RELEASE_ID}"`), + ], + [ + "shell unbraced path variables", + runWorkflow('gh api --method DELETE "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID"'), + ], + [ + "matrix axes", + matrixWorkflow( + { + method: ["DELETE"], + endpoint: ["/repos/{owner}/{repo}/releases/{release_id}"], + }, + `gh api --method "\${{ matrix.method }}" "\${{ matrix.endpoint }}"`, + ), + ], + [ + "matrix include", + matrixWorkflow( + { + include: [ + { + method: "DELETE", + endpoint: "/repos/cacheplane/dawnai/releases/379982100", + }, + ], + }, + `curl --request "\${{ matrix.method }}" "\${{ matrix.endpoint }}"`, + ), + ], + [ + "generic matrix axes", + matrixWorkflow( + { + a: ["GET", "DELETE"], + b: ["/repos/cacheplane/dawnai/releases/379982100"], + }, + `gh api --method "\${{ matrix.a }}" "\${{ matrix.b }}"`, + ), + ], + [ + "generic matrix include object", + matrixWorkflow( + { + include: [ + { + x: "DELETE", + y: "/repos/{owner}/{repo}/releases/{release_id}", + }, + ], + }, + `github.request("\${{ matrix.x }} \${{ matrix.y }}")`, + ), + ], + [ + "generic matrix bracket notation", + matrixWorkflow( + { + v: ["DELETE"], + r: ["/repos/cacheplane/dawnai/releases/379982100"], + }, + `curl --request "\${{ matrix['v'] }}" "\${{ matrix["r"] }}"`, + ), + ], + [ + "mixed matrix interpolation", + matrixWorkflow( + { + prefix: ["DEL"], + suffix: ["ETE"], + owner: ["cacheplane"], + repository: ["dawnai"], + release: ["379982100"], + }, + `gh api --method "\${{ matrix.prefix }}\${{ matrix.suffix }}" "repos/\${{ matrix.owner }}/\${{ matrix.repository }}/releases/\${{ matrix.release }}"`, + ), + ], + [ + "dynamic matrix references in method and endpoint positions", + dynamicMatrixWorkflow(`gh api --method "\${{ matrix.a }}" "\${{ matrix.b }}"`), + ], + [ + "matrix keys with unresolved generated values", + matrixWorkflow( + { + a: [`\${{ fromJSON(needs.scope.outputs.methods) }}`], + b: [`\${{ fromJSON(needs.scope.outputs.endpoints) }}`], + }, + `gh api --method "\${{ matrix.a }}" "\${{ matrix.b }}"`, + ), + ], + ["gh release delete", runWorkflow("gh release delete opaque-tag --yes")], + [ + "curl GitHub API shell variables", + runWorkflow( + `curl -X DELETE "\${GITHUB_API_URL}/repos/\${GITHUB_REPOSITORY}/releases/\${RELEASE_ID}"`, + ), + ], + [ + "Octokit bracket deleteRelease call", + runWorkflow(`await github?.rest?.repos?.["deleteRelease"]?.({ release_id })`), + ], + [ + "workflow dispatch input defaults", + inputDefaultWorkflow( + { + method: "DELETE", + endpoint: "/repos/{owner}/{repo}/releases/{release_id}", + }, + `curl --request "\${{ inputs.method }}" "\${{ inputs.endpoint }}"`, + ), + ], + [ + "generic environment indirection", + runWorkflow('curl --request "$A" "$B"', "|", { + A: "DELETE", + B: "/repos/cacheplane/dawnai/releases/379982100", + }), + ], + [ + "generated method and endpoint expressions", + runWorkflow( + `gh api --method "\${{ fromJSON(inputs.config).verb }}" "\${{ fromJSON(inputs.config).route }}"`, + ), + ], + ] + + for (const [name, source] of unsafe) { + await t.test(name, () => { + assert.throws( + () => assertNoDuplicateDraftWorkflowMutation({ "fixture.yml": source }), + /Release DELETE/u, + ) + }) + } +}) + +test("workflow isolation permits comments, documentation, GETs, and separate invocations", () => { + const safe = `name: "Documentation: DELETE /repos/cacheplane/dawnai/releases/379982100" +on: + workflow_dispatch: {} +jobs: + safe: + runs-on: ubuntu-latest + steps: + # curl -XDELETE https://api.github.com/repos/cacheplane/dawnai/releases/379982100 + - name: "DELETE /repos/cacheplane/dawnai/releases/379982100 is forbidden" + run: gh api --method GET repos/\${{ github.repository }}/releases/379982100 + - run: echo --method DELETE + - run: echo /repos/cacheplane/dawnai/releases/379982100 +` + assert.doesNotThrow(() => assertNoDuplicateDraftWorkflowMutation({ "safe.yml": safe })) + + const unrelatedMatrix = matrixWorkflow( + { + os: ["ubuntu-latest", "windows-latest"], + method: ["GET"], + endpoint: ["/repos/{owner}/{repo}/releases/{release_id}"], + }, + `gh api --method "\${{ matrix.method }}" "\${{ matrix.endpoint }}"`, + ) + assert.doesNotThrow(() => + assertNoDuplicateDraftWorkflowMutation({ + "unrelated-matrix.yml": unrelatedMatrix, + }), + ) + + const unusedDangerousMatrix = matrixWorkflow( + { + a: ["DELETE"], + b: ["/repos/cacheplane/dawnai/releases/379982100"], + safeMethod: ["GET"], + safeEndpoint: ["/repos/cacheplane/dawnai/releases/379982100"], + }, + `gh api --method "\${{ matrix.safeMethod }}" "\${{ matrix.safeEndpoint }}"`, + ) + assert.doesNotThrow(() => + assertNoDuplicateDraftWorkflowMutation({ + "unused-matrix.yml": unusedDangerousMatrix, + }), + ) + + const separateJobs = `name: separate jobs +on: + workflow_dispatch: {} +jobs: + method: + strategy: + matrix: + method: [DELETE] + runs-on: ubuntu-latest + steps: + - run: echo "\${{ matrix.method }}" + endpoint: + runs-on: ubuntu-latest + steps: + - run: echo /repos/cacheplane/dawnai/releases/379982100 +` + assert.doesNotThrow(() => + assertNoDuplicateDraftWorkflowMutation({ + "separate-jobs.yml": separateJobs, + }), + ) + + const separateMatrixSteps = matrixWorkflow( + { + a: ["DELETE"], + b: ["/repos/cacheplane/dawnai/releases/379982100"], + }, + [`echo "\${{ matrix.a }}"`, `echo "\${{ matrix.b }}"`], + ) + assert.doesNotThrow(() => + assertNoDuplicateDraftWorkflowMutation({ + "separate-matrix-steps.yml": separateMatrixSteps, + }), + ) + + const coherentUnknownMatrix = dynamicMatrixWorkflow([ + `echo "\${{ matrix.a }}"`, + `gh api --method "\${{ matrix.b }}" "\${{ matrix.b }}"`, + ]) + assert.doesNotThrow(() => + assertNoDuplicateDraftWorkflowMutation({ + "coherent-unknown-matrix.yml": coherentUnknownMatrix, + }), + ) + + const excludedDeleteRow = matrixWorkflow( + { + a: ["GET", "DELETE"], + b: ["/repos/cacheplane/dawnai/releases/379982100"], + exclude: [{ a: "DELETE" }], + }, + `curl --request "\${{ matrix.a }}" "\${{ matrix.b }}"`, + ) + assert.doesNotThrow(() => + assertNoDuplicateDraftWorkflowMutation({ + "excluded-delete-row.yml": excludedDeleteRow, + }), + ) + + const nonComposingInclude = matrixWorkflow( + { + a: ["GET"], + b: ["/repos/cacheplane/dawnai/releases/379982100"], + include: [{ a: "DELETE", c: "https://example.invalid/not-a-release" }], + }, + `curl --request "\${{ matrix.a }}" "\${{ matrix.b }}\${{ matrix.c }}"`, + ) + assert.doesNotThrow(() => + assertNoDuplicateDraftWorkflowMutation({ + "non-composing-include.yml": nonComposingInclude, + }), + ) + + const standaloneIncludesDoNotCompose = matrixWorkflow( + { + method: ["GET"], + include: [{ method: "DELETE" }, { endpoint: "/repos/cacheplane/dawnai/releases/1" }], + }, + `curl --request "\${{ matrix.method }}" "\${{ matrix.endpoint }}"`, + ) + assert.doesNotThrow(() => + assertNoDuplicateDraftWorkflowMutation({ + "standalone-includes.yml": standaloneIncludesDoNotCompose, + }), + ) +}) + +test("workflow isolation follows every repository-local executable transitively", async (t) => { + const cases = [ + { + name: "package script wrapper", + workflow: "pnpm run hidden", + packageScripts: { hidden: "bash scripts/hidden.sh" }, + files: { "scripts/hidden.sh": "gh release delete opaque-tag --yes\n" }, + }, + { + name: "local composite action", + uses: "./.github/actions/hidden", + files: { + ".github/actions/hidden/action.yml": + 'runs:\n using: composite\n steps:\n - shell: bash\n run: curl -X DELETE "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID"\n', + }, + }, + { + name: "local JavaScript action entrypoint", + uses: "./.github/actions/javascript", + files: { + ".github/actions/javascript/action.yml": "runs:\n using: node24\n main: index.mjs\n", + ".github/actions/javascript/index.mjs": + 'await github.rest.repos["deleteRelease"]({ release_id: 1 })\n', + }, + }, + { + name: "local reusable workflow", + jobUses: "./.github/workflows/reusable.yml", + files: { + ".github/workflows/reusable.yml": + "on:\n workflow_call: {}\njobs:\n hidden:\n runs-on: ubuntu-latest\n steps:\n - run: gh release delete opaque-tag --yes\n", + }, + }, + { + name: "shell wrapper chain", + workflow: "bash scripts/first.sh", + files: { + "scripts/first.sh": "bash scripts/second.sh\n", + "scripts/second.sh": + "curl --request DELETE https://api.github.com/repos/cacheplane/dawnai/releases/379982100\n", + }, + }, + { + name: "reachable banned identifier", + workflow: "node --enable-source-maps scripts/hidden.mjs", + files: { "scripts/hidden.mjs": "export const lane = 'duplicate-draft-consolidation'\n" }, + }, + { + name: "static JavaScript import and spawn", + workflow: "node scripts/first.mjs", + files: { + "scripts/first.mjs": "import './second.js'\n", + "scripts/second.js": "spawn('bash', ['-eu', 'scripts/hidden.sh'])\n", + "scripts/hidden.sh": "gh release delete opaque-tag --yes\n", + }, + }, + { + name: "comment-separated static import", + workflow: "node scripts/first.mjs", + files: { + "scripts/first.mjs": "import/*comment*/'./hidden.mjs'\n", + "scripts/hidden.mjs": "gh release delete opaque-tag --yes\n", + }, + }, + { + name: "comment-separated CommonJS require", + workflow: "node scripts/first.cjs", + files: { + "scripts/first.cjs": "require/*comment*/('./hidden.cjs')\n", + "scripts/hidden.cjs": "gh release delete opaque-tag --yes\n", + }, + }, + { + name: "comment-separated export and dynamic import", + workflow: "node scripts/first.mjs", + files: { + "scripts/first.mjs": "export/*one*/{ value }/*two*/from/*three*/'./middle.mjs'\n", + "scripts/middle.mjs": "import/*four*/('./hidden.mjs')\n", + "scripts/hidden.mjs": "gh release delete opaque-tag --yes\n", + }, + }, + { + name: "TypeScript import equals", + workflow: "pnpm exec tsx scripts/first.ts", + files: { + "scripts/first.ts": "import hidden = require('./hidden.cjs')\nvoid hidden\n", + "scripts/hidden.cjs": "gh release delete opaque-tag --yes\n", + }, + }, + { + name: "dynamic import with options", + workflow: "node scripts/first.mjs", + files: { + "scripts/first.mjs": "import('./hidden.mjs', {})\n", + "scripts/hidden.mjs": "gh release delete opaque-tag --yes\n", + }, + }, + { + name: "CommonJS require with extra argument", + workflow: "node scripts/first.cjs", + files: { + "scripts/first.cjs": "require('./hidden.cjs', undefined)\n", + "scripts/hidden.cjs": "gh release delete opaque-tag --yes\n", + }, + }, + { + name: "pnpm exec tsx runner", + workflow: "pnpm exec tsx scripts/hidden.ts", + files: { "scripts/hidden.ts": "github.rest.repos.deleteRelease({ release_id: 1 })\n" }, + }, + { + name: "bash option runner", + workflow: "bash -eu scripts/hidden.sh", + files: { "scripts/hidden.sh": "gh release delete opaque-tag --yes\n" }, + }, + { + name: "filtered workspace package script", + workflow: "pnpm --filter @fixture/worker run hidden", + files: { + "packages/worker/package.json": JSON.stringify({ + name: "@fixture/worker", + scripts: { hidden: "node scripts/hidden.mjs" }, + }), + "packages/worker/scripts/hidden.mjs": "gh release delete opaque-tag --yes\n", + }, + }, + ] + for (const fixture of cases) { + await t.test(fixture.name, async () => { + const root = await createWorkflowReachabilityFixture(t, fixture) + await assert.rejects( + () => assertNoDuplicateDraftWorkflowMutationFromRoot(root), + /Release DELETE|consolidation identifier/u, + ) + }) + } + + await t.test("unreachable mutation and reachable GET remain safe", async () => { + const root = await createWorkflowReachabilityFixture(t, { + workflow: "bash scripts/safe.sh", + files: { + "scripts/safe.sh": "gh api --method GET repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID\n", + "scripts/unreachable.sh": "gh release delete opaque-tag --yes\n", + }, + }) + await assert.doesNotReject(() => assertNoDuplicateDraftWorkflowMutationFromRoot(root)) + }) + + await t.test("safe wrapper cycles terminate", async () => { + const root = await createWorkflowReachabilityFixture(t, { + workflow: "bash scripts/a.sh", + files: { + "scripts/a.sh": "bash scripts/b.sh\n", + "scripts/b.sh": "bash scripts/a.sh\n", + }, + }) + await assert.doesNotReject(() => assertNoDuplicateDraftWorkflowMutationFromRoot(root)) + }) + + await t.test("reachable symlink escape fails closed", async () => { + const root = await createWorkflowReachabilityFixture(t, { + workflow: "bash scripts/escape.sh", + }) + const outside = await mkdtemp(path.join(os.tmpdir(), "dawn-workflow-outside-")) + t.after(() => rm(outside, { recursive: true, force: true })) + await writeFile(path.join(outside, "escape.sh"), "echo safe\n") + await mkdir(path.join(root, "scripts"), { recursive: true }) + await symlink(path.join(outside, "escape.sh"), path.join(root, "scripts", "escape.sh")) + await assert.rejects(() => assertNoDuplicateDraftWorkflowMutationFromRoot(root)) + }) + + await t.test("unsupported repository-local runner fails closed", async () => { + const root = await createWorkflowReachabilityFixture(t, { + workflow: "custom-runner scripts/hidden.mjs", + files: { "scripts/hidden.mjs": "echo safe\n" }, + }) + await assert.rejects(() => assertNoDuplicateDraftWorkflowMutationFromRoot(root)) + }) + + await t.test("comments and generated source do not create module edges", async () => { + const root = await createWorkflowReachabilityFixture(t, { + workflow: "node scripts/safe.mjs", + files: { + "scripts/safe.mjs": [ + "// import './missing-one.mjs'", + "const text = \"require('./missing-two.cjs')\"", + "const template = `export * from './missing-three.mjs'`", + "export const safe = text + template", + ].join("\n"), + }, + }) + await assert.doesNotReject(() => assertNoDuplicateDraftWorkflowMutationFromRoot(root)) + }) + + await t.test("nonliteral first arguments and harmless extra arguments stay safe", async () => { + const root = await createWorkflowReachabilityFixture(t, { + workflow: "node scripts/safe.mjs", + files: { + "scripts/safe.mjs": [ + "const target = './unreachable.mjs'", + "import(target, {})", + "require(target, undefined)", + "import('node:path', {})", + "require('node:fs', undefined)", + ].join("\n"), + "scripts/unreachable.mjs": "gh release delete opaque-tag --yes\n", + }, + }) + await assert.doesNotReject(() => assertNoDuplicateDraftWorkflowMutationFromRoot(root)) + }) + + await t.test("reachable JavaScript syntax errors fail closed", async () => { + const root = await createWorkflowReachabilityFixture(t, { + workflow: "node scripts/broken.mjs", + files: { "scripts/broken.mjs": "import { from './broken.mjs'\n" }, + }) + await assert.rejects( + () => assertNoDuplicateDraftWorkflowMutationFromRoot(root), + /cannot be parsed/u, + ) + }) +}) + test("version-pr.yml is version-only and uses only RELEASE_GITHUB_TOKEN", async () => { const { source, workflow } = await readRequiredWorkflow("version-pr.yml") const packageJson = JSON.parse( @@ -1780,6 +2399,809 @@ async function readFinalWorkflowSources() { return sources } +function assertNoDuplicateDraftWorkflowMutation(sources) { + for (const [file, source] of Object.entries(sources)) { + assert.doesNotMatch(source, /duplicate-draft-consolidation|release:consolidate-drafts/u, file) + const workflow = parseWorkflowSource(source, file) + for (const context of workflowExecutionContexts(workflow)) { + for (const normalized of normalizeExecutionContexts(context)) { + if ( + containsGhReleaseDelete(normalized) || + containsDeleteReleaseOperation(normalized) || + (containsDeleteMethod(normalized) && containsPossibleReleaseEndpoint(normalized)) + ) { + throw new Error(`${file} contains a Release DELETE endpoint in ${context.label}`) + } + } + } + } +} + +async function assertNoDuplicateDraftWorkflowMutationFromRoot(root) { + const sources = await readWorkflowSourcesFromRoot(root) + assertNoDuplicateDraftWorkflowMutation(sources) + const packageJson = JSON.parse( + await readBoundedFixture(path.join(root, "package.json"), { + root, + maxBytes: 1024 * 1024, + }), + ) + const workspacePackages = await discoverWorkspacePackages(root) + const visited = new Set() + let visits = 0 + const claimVisit = (identity) => { + if (visited.has(identity)) return false + if (visits >= 256) throw new Error("Workflow executable traversal exceeds the isolation bound") + visited.add(identity) + visits += 1 + return true + } + const visitFile = async (relative, kind = "script") => { + const normalized = normalizeReachablePath(relative) + const identity = `${kind}:${normalized}` + if (!claimVisit(identity)) return + const source = await readBoundedFixture(path.join(root, normalized), { + root, + maxBytes: 1024 * 1024, + }) + if (kind === "workflow") { + await visitWorkflow(parseWorkflowSource(source, normalized), normalized) + return + } + if (kind === "action") { + const action = parse(source, { maxAliasCount: 0, uniqueKeys: true }) + if (!isRecord(action?.runs)) throw new TypeError(`${normalized} is not a local action`) + if (action.runs.using === "composite") { + for (const step of action.runs.steps ?? []) await visitStep(step, normalized) + } else { + for (const key of ["pre", "main", "post"]) { + if (typeof action.runs[key] === "string") { + await visitFile(path.posix.join(path.posix.dirname(normalized), action.runs[key])) + } + } + } + return + } + if (/duplicate-draft-consolidation|release:consolidate-drafts/u.test(source)) { + throw new Error(`${normalized} contains a banned consolidation identifier`) + } + assertNoReleaseDeleteExecution(source, normalized) + if (/\.(?:ba|z)?sh$/u.test(normalized) || !/\.[a-z0-9]+$/iu.test(normalized)) { + await visitCommand(source, normalized) + } + if (/\.[cm]?[jt]sx?$/u.test(normalized)) { + for (const reference of localModuleReferences(source, normalized)) { + await visitResolvedFile(path.posix.dirname(normalized), reference) + } + for (const reference of localSpawnReferences(source)) { + await visitCommand(reference, normalized) + } + } + } + const visitResolvedFile = async (directory, reference) => { + const base = normalizeReachablePath(path.posix.join(directory, reference)) + if (base.split("/").includes("node_modules")) return + const candidates = /\.[a-z0-9]+$/iu.test(base) + ? [base, ...(/\.js$/u.test(base) ? [base.replace(/\.js$/u, ".ts")] : [])] + : [ + base, + ...[".mjs", ".js", ".cjs", ".ts", ".tsx"].map((suffix) => `${base}${suffix}`), + ...["index.mjs", "index.js", "index.ts"].map((name) => path.posix.join(base, name)), + ] + let lastError + for (const candidate of candidates) { + try { + const status = await lstat(path.join(root, candidate)) + if (!status.isFile() || status.isSymbolicLink()) { + throw new TypeError("Invalid repository-local executable file") + } + await visitFile(candidate) + return + } catch (error) { + if (error?.code !== "ENOENT") throw error + lastError = error + } + } + if (lastError?.code === "ENOENT") return + throw lastError + } + const visitCommand = async (command, label, baseDirectory = ".") => { + assertNoReleaseDeleteExecution(command, label) + for (const name of packageScriptReferences(command)) { + const script = packageJson.scripts?.[name] + if (typeof script !== "string") continue + const identity = `package:${name}` + if (!claimVisit(identity)) continue + await visitCommand(script, `package script ${name}`) + } + for (const { packageName, scriptName } of filteredPackageScriptReferences(command)) { + const normalizedPackageName = packageName.replace(/^\.\.\./u, "").replace(/\.\.\.$/u, "") + const workspace = workspacePackages.get(normalizedPackageName) + const script = workspace?.manifest.scripts?.[scriptName] + if (typeof script !== "string") { + if (["exec", "install", "list"].includes(scriptName)) continue + throw new Error( + `${label} contains an unresolved filtered workspace script ${packageName}:${scriptName}`, + ) + } + const identity = `package:${normalizedPackageName}:${scriptName}` + if (!claimVisit(identity)) continue + await visitCommand(script, `package script ${packageName}:${scriptName}`, workspace.directory) + } + const files = localCommandFileReferences(command) + for (const file of files) await visitResolvedFile(baseDirectory, file) + const unknownRunner = /(?:^|[\n;&|])\s*([\w.-]+)\s+(?:\.\/)?(?:scripts|\.github)\//gu.exec( + command, + )?.[1] + if (unknownRunner !== undefined && !["node", "bash", "sh", "tsx"].includes(unknownRunner)) { + throw new Error(`${label} contains unsupported repository-local runner ${unknownRunner}`) + } + if ( + /(?:^|[\s;&|"'(])(?:scripts|\.\/scripts|\.github)\/[\w./-]+/u.test(command) && + files.size === 0 && + !/\bnode\s+--test\s+[^\n]*\*/u.test(command) + ) { + throw new Error(`${label} contains unsupported repository-local execution syntax: ${command}`) + } + } + const visitStep = async (step, label) => { + if (!isRecord(step)) throw new TypeError(`${label} contains an invalid executable step`) + if (typeof step.run === "string") await visitCommand(step.run, label) + if (typeof step.uses === "string" && step.uses.startsWith("./")) { + await visitLocalUses(step.uses) + } + } + const visitLocalUses = async (uses) => { + const relative = normalizeReachablePath(uses) + if (/\.ya?ml$/u.test(relative)) { + await visitFile(relative, "workflow") + return + } + let lastError + for (const name of ["action.yml", "action.yaml"]) { + try { + await visitFile(path.posix.join(relative, name), "action") + return + } catch (error) { + if (error?.code !== "ENOENT") throw error + lastError = error + } + } + throw lastError + } + const visitWorkflow = async (workflow, label) => { + for (const job of Object.values(workflow.jobs)) { + if (!isRecord(job)) continue + if (typeof job.uses === "string" && job.uses.startsWith("./")) { + await visitLocalUses(job.uses) + } + for (const step of job.steps ?? []) await visitStep(step, label) + } + } + for (const [file, source] of Object.entries(sources)) { + if (!claimVisit(`workflow:${file}`)) continue + await visitWorkflow(parseWorkflowSource(source, file), file) + } +} + +function assertNoReleaseDeleteExecution(value, label) { + const normalized = String(value) + .replace(/\\\r?\n/gu, "") + .toLowerCase() + if ( + containsGhReleaseDelete(normalized) || + containsDeleteReleaseOperation(normalized) || + (containsDeleteMethod(normalized) && containsPossibleReleaseEndpoint(normalized)) + ) { + throw new Error(`${label} contains a Release DELETE endpoint`) + } +} + +function normalizeReachablePath(value) { + const normalized = path.posix.normalize(String(value).replace(/^\.\//u, "")) + if ( + normalized.length === 0 || + normalized === "." || + normalized.startsWith("../") || + path.posix.isAbsolute(normalized) || + /[\0\r\n]|\$\{\{/u.test(normalized) + ) { + throw new TypeError("Invalid repository-local executable path") + } + return normalized +} + +function packageScriptReferences(command) { + const names = [] + const pattern = + /(?:^|[\s;&|"'(])(?:pnpm\s+(?:run\s+)?|npm\s+run\s+|yarn\s+(?:run\s+)?)(?!-)([\w:.-]+)/gu + for (const match of String(command).matchAll(pattern)) names.push(match[1]) + return names +} + +function filteredPackageScriptReferences(command) { + const references = [] + const pattern = /\bpnpm\s+(?:--filter|-F)\s+([^\s]+)\s+(?:run\s+)?([\w:.-]+)/gu + for (const match of String(command).matchAll(pattern)) { + references.push({ packageName: match[1], scriptName: match[2] }) + } + return references +} + +async function discoverWorkspacePackages(root) { + const packages = new Map() + const visitDirectory = async (relative, depth) => { + let entries + try { + entries = await readdir(path.join(root, relative), { withFileTypes: true }) + } catch (error) { + if (error?.code === "ENOENT") return + throw error + } + if (entries.length > 256) throw new Error("Workspace package discovery exceeds the bound") + const manifestPath = path.join(root, relative, "package.json") + try { + const status = await lstat(manifestPath) + if (!status.isFile() || status.isSymbolicLink()) + throw new TypeError("Invalid workspace manifest") + const manifest = JSON.parse( + await readBoundedFixture(manifestPath, { + root, + maxBytes: 1024 * 1024, + }), + ) + if (typeof manifest.name === "string") + packages.set(manifest.name, { directory: relative, manifest }) + } catch (error) { + if (error?.code !== "ENOENT") throw error + } + if (depth === 0) return + for (const entry of entries) { + if (entry.isDirectory() && !entry.isSymbolicLink() && !entry.name.startsWith(".")) { + await visitDirectory(path.posix.join(relative, entry.name), depth - 1) + } + } + } + for (const [directory, depth] of [ + ["packages", 1], + ["apps", 1], + ["examples", 2], + ]) { + await visitDirectory(directory, depth) + } + return packages +} + +function localCommandFileReferences(command) { + const files = new Set() + const pattern = + /(?:^|[\s;&|"'(])(?:node(?:\s+--?[\w=-]+)*|(?:ba)?sh(?:\s+-[a-z]+)*|pnpm\s+exec\s+(?:tsx|node)(?:\s+--?[\w=-]+)*|tsx)\s+((?:\.\/)?(?:scripts|\.github)\/[\w./-]*[\w.-])(?![\w./*-])/giu + for (const match of String(command).matchAll(pattern)) files.add(match[1]) + const direct = /(?:^|[\s;&|"'(])((?:\.\/)?(?:scripts|\.github)\/[\w./-]*[\w.-])(?![\w./*-])/gu + for (const match of String(command).matchAll(direct)) files.add(match[1]) + return files +} + +function localModuleReferences(source, file) { + const references = new Set() + const scriptKind = typescriptScriptKind(file) + const sourceFile = typescript.createSourceFile( + file, + source, + typescript.ScriptTarget.Latest, + true, + scriptKind, + ) + if (sourceFile.parseDiagnostics.length > 0) { + const diagnostic = sourceFile.parseDiagnostics[0] + const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, " ") + throw new Error(`${file} cannot be parsed as executable JavaScript/TypeScript: ${message}`) + } + const addLiteral = (node) => { + if (typescript.isStringLiteralLike(node) && /^\.\.?\//u.test(node.text)) { + references.add(node.text) + } + } + const visit = (node) => { + if ( + (typescript.isImportDeclaration(node) || typescript.isExportDeclaration(node)) && + node.moduleSpecifier !== undefined + ) { + addLiteral(node.moduleSpecifier) + } else if ( + typescript.isImportEqualsDeclaration(node) && + typescript.isExternalModuleReference(node.moduleReference) && + node.moduleReference.expression !== undefined + ) { + addLiteral(node.moduleReference.expression) + } else if (typescript.isCallExpression(node) && node.arguments.length >= 1) { + if ( + node.expression.kind === typescript.SyntaxKind.ImportKeyword || + (typescript.isIdentifier(node.expression) && node.expression.text === "require") + ) { + addLiteral(node.arguments[0]) + } + } + typescript.forEachChild(node, visit) + } + visit(sourceFile) + return references +} + +function typescriptScriptKind(file) { + if (/\.tsx$/iu.test(file)) return typescript.ScriptKind.TSX + if (/\.jsx$/iu.test(file)) return typescript.ScriptKind.JSX + if (/\.(?:ts|mts|cts)$/iu.test(file)) return typescript.ScriptKind.TS + if (/\.json$/iu.test(file)) return typescript.ScriptKind.JSON + return typescript.ScriptKind.JS +} + +function localSpawnReferences(source) { + const commands = new Set() + const executableSource = String(source).replace(/`(?:\\[\s\S]|[^`])*`/gu, "") + const pattern = + /\b(?:spawn|spawnSync|execFile|execFileSync)\s*\(\s*(["'])([^"']+)\1\s*,\s*\[([^\]]*)\]/gu + for (const match of executableSource.matchAll(pattern)) { + const args = [...match[3].matchAll(/(["'])([^"']+)\1/gu)].map((entry) => entry[2]) + commands.add([match[2], ...args].join(" ")) + } + const execPattern = /\b(?:exec|execSync)\s*\(\s*(["'])([^"']+)\1/gu + for (const match of executableSource.matchAll(execPattern)) commands.add(match[2]) + return commands +} + +function workflowExecutionContexts(workflow) { + const contexts = [] + const inputDefaults = collectWorkflowInputDefaults(workflow) + const workflowEnv = collectScalarMap(workflow.env) + for (const [jobId, job] of Object.entries(workflow.jobs)) { + if (!isRecord(job)) continue + const jobEnv = mergeScalarMaps( + workflowEnv, + collectScalarMap(job.container?.env), + collectScalarMap(job.env), + ) + const matrixRows = collectStaticMatrixRows(job.strategy?.matrix) + if (typeof job.uses === "string") { + contexts.push({ + env: jobEnv, + inputDefaults, + label: `job ${jobId}`, + matrixRows, + values: [ + `jobs.${jobId}.uses=${job.uses}`, + ...executionObjectScalars(job.with, `jobs.${jobId}.with`), + ...executionObjectScalars(job.secrets, `jobs.${jobId}.secrets`), + ], + }) + } + if (!Array.isArray(job.steps)) continue + for (const [stepIndex, step] of job.steps.entries()) { + if (!isRecord(step)) continue + const env = mergeScalarMaps(jobEnv, collectScalarMap(step.env)) + const values = [...executionObjectScalars(step.with, `jobs.${jobId}.steps.${stepIndex}.with`)] + if (typeof step.run === "string") { + values.push(`jobs.${jobId}.steps.${stepIndex}.run=${step.run}`) + } + if (typeof step.uses === "string") { + values.push(`jobs.${jobId}.steps.${stepIndex}.uses=${step.uses}`) + } + contexts.push({ + env, + inputDefaults, + label: `job ${jobId} step ${stepIndex}`, + matrixRows, + values, + }) + } + } + return contexts +} + +function collectStaticMatrixRows(matrix) { + if (!isRecord(matrix)) return { dynamic: matrix !== undefined, rows: [{}] } + const axes = Object.entries(matrix).filter(([key]) => key !== "include" && key !== "exclude") + if (axes.some(([, values]) => !Array.isArray(values) || !values.every(isStaticScalar))) { + return { dynamic: true, rows: [{}] } + } + let states = [{ base: {}, row: {} }] + for (const [key, values] of axes) { + if (values.length === 0) return { dynamic: false, rows: [] } + if (states.length > Math.floor(1024 / values.length)) { + throw new Error("Workflow matrix expansion exceeds the isolation bound") + } + const next = [] + for (const state of states) { + for (const value of values) { + const scalar = String(value) + next.push({ + base: { ...state.base, [key.toLowerCase()]: scalar }, + row: { ...state.row, [key.toLowerCase()]: scalar }, + }) + } + } + states = next + } + const exclusions = staticMatrixObjects(matrix.exclude) + if (exclusions === null) return { dynamic: true, rows: [{}] } + states = states.filter(({ base }) => !exclusions.some((entry) => rowMatches(base, entry))) + const includes = staticMatrixObjects(matrix.include) + if (includes === null) return { dynamic: true, rows: [{}] } + if (axes.length === 0 && includes.length > 0) { + if (includes.length > 1024) { + throw new Error("Workflow matrix expansion exceeds the isolation bound") + } + states = includes.map((entry) => ({ + base: { ...entry }, + row: { ...entry }, + })) + } else { + const standalone = [] + for (const include of includes) { + let applied = false + for (const state of states) { + if (!rowCompatible(state.base, include)) continue + state.row = { ...state.row, ...include } + applied = true + } + if (!applied) { + if (states.length + standalone.length >= 1024) { + throw new Error("Workflow matrix expansion exceeds the isolation bound") + } + standalone.push({ base: { ...include }, row: { ...include } }) + } + } + states.push(...standalone) + } + if (states.length > 1024) throw new Error("Workflow matrix expansion exceeds the isolation bound") + return { dynamic: false, rows: states.map(({ row }) => row) } +} + +function staticMatrixObjects(value) { + if (value === undefined) return [] + if (!Array.isArray(value)) return null + const objects = [] + for (const entry of value) { + if (!isRecord(entry) || Object.values(entry).some((item) => !isStaticScalar(item))) return null + objects.push( + Object.fromEntries( + Object.entries(entry).map(([key, item]) => [key.toLowerCase(), String(item)]), + ), + ) + } + return objects +} + +function isStaticScalar(value) { + return ( + (typeof value === "string" || typeof value === "number" || typeof value === "boolean") && + !(typeof value === "string" && /\$\{\{/u.test(value)) + ) +} + +function rowMatches(row, expected) { + return Object.entries(expected).every(([key, value]) => row[key] === value) +} + +function rowCompatible(row, included) { + return Object.entries(included).every( + ([key, value]) => !Object.hasOwn(row, key) || row[key] === value, + ) +} + +function collectWorkflowInputDefaults(workflow) { + const defaults = Object.create(null) + for (const event of [workflow.on?.workflow_dispatch, workflow.on?.workflow_call]) { + if (!isRecord(event?.inputs)) continue + for (const [key, descriptor] of Object.entries(event.inputs)) { + if (isRecord(descriptor) && isStaticScalar(descriptor.default)) { + defaults[key.toLowerCase()] = String(descriptor.default) + } + } + } + return defaults +} + +function collectScalarMap(value) { + const result = Object.create(null) + if (!isRecord(value)) return result + for (const [key, entry] of Object.entries(value)) { + if (typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean") { + result[key.toLowerCase()] = String(entry) + } + } + return result +} + +function mergeScalarMaps(...maps) { + return Object.assign(Object.create(null), ...maps) +} + +function executionObjectScalars(value, prefix) { + if (!isRecord(value) && !Array.isArray(value)) return [] + const found = [] + const visit = (current, pathParts) => { + if ( + typeof current === "string" || + typeof current === "number" || + typeof current === "boolean" + ) { + found.push(`${prefix}.${pathParts.join(".")}=${String(current)}`) + return + } + if (Array.isArray(current)) { + for (const [index, entry] of current.entries()) visit(entry, [...pathParts, String(index)]) + return + } + if (!isRecord(current)) return + for (const [key, entry] of Object.entries(current)) visit(entry, [...pathParts, key]) + } + visit(value, []) + return found +} + +function normalizeExecutionContexts(context) { + const resolved = resolveKnownExecutionReferences(context.values.join("\n"), context) + const rows = context.matrixRows.dynamic ? [null] : context.matrixRows.rows + return rows.flatMap((row) => + expandMatrixReferences(resolved, row, context.matrixRows.dynamic).map((value) => + value + .replace(/\$\{\{[\s\S]*?\}\}/gu, "__expression__") + .replace(/\\\r?\n/gu, "") + .toLowerCase(), + ), + ) +} + +function resolveKnownExecutionReferences(value, context) { + let resolved = value + for (let attempt = 0; attempt < 8; attempt += 1) { + const next = resolved + .replace(/\$\{\{\s*inputs\.([a-z_][a-z0-9_-]*)\s*\}\}/giu, (match, key) => + Object.hasOwn(context.inputDefaults, key.toLowerCase()) + ? context.inputDefaults[key.toLowerCase()] + : match, + ) + .replace(/\$\{\{\s*env\.([a-z_][a-z0-9_]*)\s*\}\}/giu, (match, key) => + Object.hasOwn(context.env, key.toLowerCase()) ? context.env[key.toLowerCase()] : match, + ) + .replace(/\$\{([a-z_][a-z0-9_]*)\}/giu, (match, key) => + Object.hasOwn(context.env, key.toLowerCase()) ? context.env[key.toLowerCase()] : match, + ) + .replace(/\$([a-z_][a-z0-9_]*)/giu, (match, key) => + Object.hasOwn(context.env, key.toLowerCase()) ? context.env[key.toLowerCase()] : match, + ) + if (next === resolved) return resolved + resolved = next + } + throw new Error("Workflow expression indirection exceeds the isolation bound") +} + +function expandMatrixReferences(value, row, dynamic) { + const expansions = [] + const visit = (current, assignments) => { + if (expansions.length >= 1024) { + throw new Error("Workflow matrix expansion exceeds the isolation bound") + } + const reference = findMatrixReference(current) + if (reference === null) { + expansions.push(current) + return + } + const assignmentKey = reference.key ?? `dynamic:${reference.expression.toLowerCase()}` + const assigned = assignments.get(assignmentKey) + const configured = + reference.key !== null && row !== null && Object.hasOwn(row, reference.key) + ? row[reference.key] + : undefined + const choices = + assigned === undefined + ? configured !== undefined + ? [configured] + : dynamic || reference.key === null + ? ["DELETE", "/repos/{owner}/{repo}/releases/{release_id}"] + : [""] + : [assigned] + for (const choice of choices) { + const nextAssignments = + assigned === undefined ? new Map(assignments).set(assignmentKey, choice) : assignments + visit( + `${current.slice(0, reference.index)}${choice}${current.slice(reference.index + reference.expression.length)}`, + nextAssignments, + ) + } + } + visit(value, new Map()) + return expansions +} + +function findMatrixReference(value) { + const match = /\$\{\{\s*matrix\b[\s\S]*?\}\}/iu.exec(value) + if (match === null) return null + const parsed = + /^\$\{\{\s*matrix\s*(?:\.\s*([a-z_][a-z0-9_-]*)|\[\s*(["'])([^"']+)\2\s*\])\s*\}\}$/iu.exec( + match[0], + ) + return { + expression: match[0], + index: match.index, + key: parsed === null ? null : (parsed[1] ?? parsed[3]).toLowerCase(), + } +} + +function containsDeleteMethod(value) { + return /(?:^|[\s"'`(])delete\s+(?=\/?repos\/)|(?:^|\s)(?:-x\s*["'`]?\s*(?:delete\b|__expression__)|--(?:method|request)(?:\s+|\s*=\s*)["'`]?\s*(?:delete\b|__expression__)|[^\s=:]*(?:method|request|verb)[^\s=:]*\s*[:=]\s*["'`]?(?:delete\b|__expression__))/iu.test( + value, + ) +} + +function containsGhReleaseDelete(value) { + return /\bgh\s+release\s+delete(?:\s|$)/iu.test(value) +} + +function containsDeleteReleaseOperation(value) { + return /(?:\bdeleterelease\s*(?:\?\s*\.\s*)?\(|["']deleterelease["']\s*\]\s*(?:\?\s*\.\s*)?\()/iu.test( + value, + ) +} + +function containsReleaseEndpoint(value) { + const segment = String.raw`(?:__expression__|\$[a-z_][a-z0-9_]*|[^/\s"'=:]+)` + const repository = `(?:${segment}|${segment}/${segment})` + const releaseId = String.raw`(?:[1-9][0-9]*|__expression__|\$[a-z_][a-z0-9_]*|\$\{[a-z_][a-z0-9_]*\}|\{[a-z_][a-z0-9_]*\})` + const host = String.raw`(?:https?://[^/\s"']+|__expression__|\$[a-z_][a-z0-9_]*|\$\{[a-z_][a-z0-9_]*\})` + return new RegExp( + String.raw`(?:^|[\s"'=])${host}?/?repos/${repository}/releases/${releaseId}(?:$|[?&#/\s"'])`, + "iu", + ).test(value) +} + +function containsPossibleReleaseEndpoint(value) { + return ( + containsReleaseEndpoint(value) || + /\b(?:gh\s+api|curl\b)[^\n]*["']?__expression__["']?/iu.test(value) + ) +} + +function runWorkflow(run, block = "|", stepEnv, jobEnv) { + const indent = (value, spaces) => + value + .split("\n") + .map((line) => `${" ".repeat(spaces)}${line}`) + .join("\n") + const yamlMap = (value, spaces) => + Object.entries(value ?? {}) + .map(([key, entry]) => `${" ".repeat(spaces)}${key}: ${JSON.stringify(entry)}`) + .join("\n") + return `name: fixture +on: + workflow_dispatch: {} +jobs: + mutation: + runs-on: ubuntu-latest +${jobEnv === undefined ? "" : ` env:\n${yamlMap(jobEnv, 6)}\n`} steps: + - run: ${block} +${indent(run, 10)} +${stepEnv === undefined ? "" : ` env:\n${yamlMap(stepEnv, 10)}\n`}` +} + +function actionWorkflow(withValues) { + const inputs = Object.entries(withValues) + .map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`) + .join("\n") + return `name: fixture +on: + workflow_dispatch: {} +jobs: + mutation: + runs-on: ubuntu-latest + steps: + - uses: example/action@0123456789012345678901234567890123456789 + with: +${inputs} +` +} + +function reusableWorkflow(withValues) { + const inputs = Object.entries(withValues) + .map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`) + .join("\n") + return `name: fixture +on: + workflow_dispatch: {} +jobs: + mutation: + uses: example/workflows/.github/workflows/delete.yml@0123456789012345678901234567890123456789 + with: +${inputs} +` +} + +function matrixWorkflow(matrix, run) { + const matrixYaml = stringify(matrix, { lineWidth: 0 }) + .trimEnd() + .split("\n") + .map((line) => ` ${line}`) + .join("\n") + const steps = (Array.isArray(run) ? run : [run]) + .map((value) => ` - run: ${JSON.stringify(value)}`) + .join("\n") + return `name: fixture +on: + workflow_dispatch: {} +jobs: + mutation: + strategy: + matrix: +${matrixYaml} + runs-on: ubuntu-latest + steps: +${steps} +` +} + +function dynamicMatrixWorkflow(run) { + const steps = (Array.isArray(run) ? run : [run]) + .map((value) => ` - run: ${JSON.stringify(value)}`) + .join("\n") + return `name: fixture +on: + workflow_dispatch: {} +jobs: + mutation: + strategy: + matrix: \${{ fromJSON(needs.scope.outputs.matrix) }} + runs-on: ubuntu-latest + steps: +${steps} +` +} + +function inputDefaultWorkflow(defaults, run) { + const inputs = Object.entries(defaults) + .map( + ([key, value]) => + ` ${key}:\n type: string\n default: ${JSON.stringify(value)}`, + ) + .join("\n") + return `name: fixture +on: + workflow_dispatch: + inputs: +${inputs} +jobs: + mutation: + runs-on: ubuntu-latest + steps: + - run: ${JSON.stringify(run)} +` +} + +async function createWorkflowReachabilityFixture(t, fixture) { + const root = await mkdtemp(path.join(os.tmpdir(), "dawn-workflow-reachability-")) + t.after(() => rm(root, { recursive: true, force: true })) + await mkdir(path.join(root, ".github", "workflows"), { recursive: true }) + const step = + fixture.uses === undefined + ? ` - run: ${JSON.stringify(fixture.workflow ?? "echo safe")}` + : ` - uses: ${fixture.uses}` + const workflow = + fixture.jobUses === undefined + ? `on:\n workflow_dispatch: {}\njobs:\n fixture:\n runs-on: ubuntu-latest\n steps:\n${step}\n` + : `on:\n workflow_dispatch: {}\njobs:\n fixture:\n uses: ${fixture.jobUses}\n` + await writeFile(path.join(root, ".github", "workflows", "fixture.yml"), workflow) + await writeFile( + path.join(root, "package.json"), + `${JSON.stringify({ private: true, scripts: fixture.packageScripts ?? {} }, null, 2)}\n`, + ) + for (const [file, source] of Object.entries(fixture.files ?? {})) { + const target = path.join(root, file) + await mkdir(path.dirname(target), { recursive: true }) + await writeFile(target, source) + } + return root +} + function parseWorkflowSource(source, file) { let workflow try {