diff --git a/docs/superpowers/plans/2026-09-01-v0.8.22-duplicate-draft-recovery.md b/docs/superpowers/plans/2026-09-01-v0.8.22-duplicate-draft-recovery.md new file mode 100644 index 000000000..7a1fe8c34 --- /dev/null +++ b/docs/superpowers/plans/2026-09-01-v0.8.22-duplicate-draft-recovery.md @@ -0,0 +1,696 @@ +# v0.8.22 Duplicate Draft Recovery 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:** Safely quarantine the two duplicate v0.8.22 escrow drafts, resume the canonical exact-tag release, publish the README-bearing v0.8.23 fixed group, and remove the one-time recovery surface. + +**Architecture:** Build a temporary candidate-pinned operator command with a pure evidence/state core, a bounded read-only production adapter, and a narrow recovery-only writer. The command captures short-lived canonical evidence, resumes four exact duplicate states idempotently, archives each original body before removing its live marker, and leaves normal controller duplicate detection unchanged. Because GitHub does not support conditional Release PATCH, body quarantine uses an explicit operator edit freeze plus fail-closed pre/post write fences and never claims atomic compare-and-swap. After production convergence, the existing `release.yml` owns npm publication and the temporary command is removed. + +**Tech Stack:** Node.js 24 ESM, `node:test`, existing Dawn release readers and controller primitives, GitHub REST API, npm registry metadata, canonical JSON/SHA-256 receipts, pnpm 10.33.0. + +--- + +## File Map + +| File | Responsibility | +| --- | --- | +| `scripts/release/duplicate-draft-recovery.mjs` | Candidate constants, canonical evidence schema, duplicate-state classification, archive/receipt/notice bytes, capture/apply orchestration, and final authorization checks. | +| `scripts/release/duplicate-draft-recovery-adapters.mjs` | Bounded operator reads, reviewed-merge authority, exact production snapshots, candidate-specific evidence upload, and body-only compare-before-write fencing. | +| `scripts/release/recover-v0.8.22-duplicate-drafts.mjs` | Strict `capture`/`apply` CLI, contained write-once files, stable secret-safe errors. | +| `scripts/release/test/duplicate-draft-recovery.test.mjs` | Pure state/evidence/orchestration tests with an in-memory production model. | +| `scripts/release/test/duplicate-draft-recovery-adapters.test.mjs` | Exact GitHub/npm/git command and HTTP boundary tests. | +| `scripts/release/test/duplicate-draft-recovery-cli.test.mjs` | CLI arguments, file safety, capture/apply wiring, and error-surface tests. | +| `docs/superpowers/runbooks/2026-08-09-release-integrity-cutover.md` | Temporary operator sequence and production receipt fields. | + +Do not modify normal candidate discovery, `isManagedReleaseForTag`, planner behavior, `.github/workflows/release.yml`, or the normal `createGitHubWriter` capability surface. The recovery command must remain unreachable from final release-owner workflows and package scripts. + +### Task 1: Define the candidate policy and four-state classifier + +**Files:** +- Create: `scripts/release/duplicate-draft-recovery.mjs` +- Create: `scripts/release/test/duplicate-draft-recovery.test.mjs` + +- [ ] **Step 1: Write failing policy and state-classification tests** + +Add tests that import the planned exports and assert the exact frozen policy: + +```js +const POLICY = { + repository: "cacheplane/dawnai", + version: "0.8.22", + candidateSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + canonicalReleaseId: 379991871, + duplicates: [ + { releaseId: 379982100, tagName: "untagged-a13939767dd2419ade01" }, + { releaseId: 379986168, tagName: "untagged-20706099efa3c38335a8" }, + ], +} +``` + +Build fixtures for these exact states: + +```js +assert.equal(classifyDuplicate(snapshot({ evidenceAssets: [] })), "untouched") +assert.equal(classifyDuplicate(snapshot({ evidenceAssets: ["body"] })), "body-archived") +assert.equal(classifyDuplicate(snapshot({ evidenceAssets: ["body", "receipt"] })), "receipt-archived") +assert.equal(classifyDuplicate(snapshot({ quarantined: true })), "quarantined") +``` + +Reject a wrong ID, exact `v0.8.22` tag name, changed original asset, extra asset, noncanonical marker, malformed notice, receipt without body archive, or unknown combination. + +- [ ] **Step 2: Run the focused test and verify RED** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH \ +node --test scripts/release/test/duplicate-draft-recovery.test.mjs +``` + +Expected: FAIL because `duplicate-draft-recovery.mjs` does not exist. + +- [ ] **Step 3: Implement the frozen policy and classifier** + +Export only the small planning surface needed by later tasks: + +```js +export const DUPLICATE_DRAFT_RECOVERY_POLICY = deepFreeze({ ... }) +export function classifyDuplicateDraft(snapshot, expected) { ... } +export function originalBodyAssetName(releaseId, bodySha256) { ... } +export function recoveryReceiptAssetName(releaseId) { ... } +export function canonicalRecoveryReceipt(input) { ... } +export function canonicalRecoveryNotice(input) { ... } +``` + +Use `snapshotJson`, exact-field checks, safe integer validation, lowercase SHA-256 validation, and canonical newline-terminated JSON. Keep asset names candidate-specific, ASCII-only, and bounded. The notice must not contain the Dawn marker delimiter accepted by `parseReleaseMarker`. + +- [ ] **Step 4: Run the focused test and verify GREEN** + +Run the Step 2 command. + +Expected: all policy and four-state tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/release/duplicate-draft-recovery.mjs \ + scripts/release/test/duplicate-draft-recovery.test.mjs +git commit -m "feat(release): define duplicate draft recovery states" +``` + +### Task 2: Define canonical short-lived recovery evidence + +**Files:** +- Modify: `scripts/release/duplicate-draft-recovery.mjs` +- Modify: `scripts/release/test/duplicate-draft-recovery.test.mjs` + +- [ ] **Step 1: Write failing evidence tests** + +Test an exact schema containing: + +```js +{ + schemaVersion: 1, + capturedAt: "2026-09-01T00:00:00.000Z", + reviewedAuthority: { + mergeCommitSha, + mergeTreeSha, + pullRequestNumber, + reviewedHeadSha, + reviewedTreeSha, + validateRunId, + }, + repository: { id, nameWithOwner, mainSha }, + workflow: { id: 260503756, state: "disabled_manually" }, + immutableReleases: { enabled: true }, + candidate: { version, commitSha, tagObjectSha }, + npm: { packages: [...] }, + releaseRuns: [], + releases: { canonical, duplicates: [...] }, +} +``` + +Require canonical byte stability, deep freezing, a maximum age of 15 minutes, exact current time bounds, exact package order, exact Release IDs, exact state-derived next steps, and rejection of unknown fields/accessors/sparse arrays. Prove an expired partial-state evidence file remains immutable and can be superseded by newly captured canonical evidence at a distinct write-once path. + +- [ ] **Step 2: Run focused tests and verify RED** + +Run the Task 1 test command. + +Expected: FAIL because evidence parsing and verification are absent. + +- [ ] **Step 3: Implement evidence parsing and verification** + +Add: + +```js +export function canonicalDuplicateDraftEvidence(value) { ... } +export function parseDuplicateDraftEvidence(bytes) { ... } +export function verifyDuplicateDraftEvidence({ evidence, current, now }) { ... } +``` + +Derive—not trust—body digests, 45-asset inventory digests, archive names, receipt bytes, notice bytes, duplicate state, and remaining transitions. Never store tokens, URLs containing signed query parameters, response headers, or raw npm/GitHub errors. + +- [ ] **Step 4: Run focused tests and verify GREEN** + +Expected: all evidence tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/release/duplicate-draft-recovery.mjs \ + scripts/release/test/duplicate-draft-recovery.test.mjs +git commit -m "feat(release): seal duplicate draft evidence" +``` + +### Task 3: Build the read-only production capture boundary + +**Files:** +- Create: `scripts/release/duplicate-draft-recovery-adapters.mjs` +- Create: `scripts/release/test/duplicate-draft-recovery-adapters.test.mjs` +- Modify: `scripts/release/duplicate-draft-recovery.mjs` +- Modify: `scripts/release/test/duplicate-draft-recovery.test.mjs` + +- [ ] **Step 1: Write failing adapter contract tests** + +Require an exact frozen reader surface. It may compose `createGitHubReader`, `createNpmReader`, `createGitReader`, and narrowly scoped owner-preflight operations, but it must expose only named recovery reads such as: + +```js +{ + readReviewedMergeAuthority, + readRepositoryState, + readCandidateTag, + readWorkflowState, + readImmutableReleases, + readReleaseRuns, + readCandidatePublishJobs, + readNpmAbsence, + readReleaseSnapshot, + listCandidateReleases, +} +``` + +Test exact GitHub API routes for the reviewed commit, associated pull request, pull request head tree, merge tree, required `CI / validate` run, repository identity, immutable setting, workflow state, tag, Releases, assets, and jobs. Reject pagination drift, another repository, multiple associated PRs, a non-merged PR, wrong base, unequal trees, missing validate success, later `main`, auth ambiguity, malformed schemas, and unsafe URLs. + +- [ ] **Step 2: Run the adapter test and verify RED** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH \ +node --test scripts/release/test/duplicate-draft-recovery-adapters.test.mjs +``` + +Expected: FAIL because the adapter module does not exist. + +- [ ] **Step 3: Implement the bounded read adapter** + +Use argument arrays, the trusted `https://api.github.com` origin, the repository ID already verified by the release readers, existing timeout/byte/page limits, and safe normalized envelopes. Do not shell-interpolate tokens. The reviewed authority must prove: + +```text +supplied SHA == local HEAD == remote refs/heads/main +one associated merged PR targets main +PR merge_commit_sha == supplied SHA +merge tree == reviewed head tree +CI / validate succeeded at reviewed head +``` + +For npm absence, reuse the package metadata plus exact-version E404 confirmation used by the production observer; do not trust an error string alone. + +- [ ] **Step 4: Write failing capture-orchestration tests** + +Inject the reader into a new `captureDuplicateDraftRecoveryEvidence` function. Assert that it reads all 21 packages in canonical order, discovers exactly the configured three Releases, recognizes all four partial states, and refuses a fourth marker-backed or exact-tag Release. Feed it candidate workflow runs and their jobs, and assert it rejects if any observed run has started a job named `publish-npm`, regardless of that job's terminal state. Assert the dependency object contains no writer. + +- [ ] **Step 5: Implement capture orchestration** + +Add: + +```js +export async function captureDuplicateDraftRecoveryEvidence({ reviewedCommit, reader, now }) { + // collect, normalize, classify, derive, and return canonical evidence +} +``` + +Every absence or conflict must produce a stable error code without remote body text. +The capture must consume `readCandidatePublishJobs` for every observed candidate +run and fail closed unless no `publish-npm` job has started. + +- [ ] **Step 6: Run both focused suites** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH \ +node --test \ + scripts/release/test/duplicate-draft-recovery.test.mjs \ + scripts/release/test/duplicate-draft-recovery-adapters.test.mjs +``` + +Expected: PASS. + +- [ ] **Step 7: Commit** + +```bash +git add scripts/release/duplicate-draft-recovery.mjs \ + scripts/release/duplicate-draft-recovery-adapters.mjs \ + scripts/release/test/duplicate-draft-recovery.test.mjs \ + scripts/release/test/duplicate-draft-recovery-adapters.test.mjs +git commit -m "feat(release): capture duplicate draft recovery evidence" +``` + +### Task 4: Add the candidate-specific recovery writer + +**Files:** +- Modify: `scripts/release/duplicate-draft-recovery-adapters.mjs` +- Modify: `scripts/release/test/duplicate-draft-recovery-adapters.test.mjs` + +- [ ] **Step 1: Write failing writer boundary tests** + +Define exactly two mutation methods: + +```js +uploadEvidenceAssetIfAbsentAndEqual(input) +quarantineDuplicateBodyIfCurrent(input) +``` + +Test that the writer: + +- accepts only the two duplicate Release IDs and their exact opaque tag names; +- accepts only derived candidate-specific archive/receipt names; +- verifies annotated `v0.8.22` before and after mutation; +- requires a final pre-write fence immediately before PATCH that concurrently + re-reads the annotated tag and the complete normalized Release/body/asset + projection; +- uploads only absent bytes and accepts existing assets only after download equality; +- PATCHes `{ body: expectedNotice }` and never sends `name`, `tag_name`, `draft`, or other metadata; +- compares the exact normalized projection: Release ID, opaque tag, title, + target, draft/prerelease/immutable flags, and ordered asset + IDs/names/digests/sizes; permits only the canonical-body-to-recovery-notice + delta and excludes GitHub-managed URLs, author data, headers, and + `updated_at` from equality; +- rejects stale body digests, unexpected titles, targets, prerelease state, + assets, or any observable pre/post drift; +- performs no automatic retry after network failure, timeout, retryable HTTP + status, malformed response, or ambiguous outcome; +- always performs the first post-write tag read and then a complete post-write + projection read after any issued PATCH, even when the response is ambiguous; +- never issues DELETE, npm, tag-write, workflow-enable, or workflow-dispatch requests; and +- bounds request time, response bytes, asset bytes, redirects, and content types. + +- [ ] **Step 2: Run adapter tests and verify RED** + +Run the Task 3 adapter test command. + +Expected: FAIL because the recovery writer is absent. + +- [ ] **Step 3: Implement the narrow writer** + +Keep it separate from `createGitHubWriter`. Construct it lazily only after evidence verification and the orchestration layer's exact freeze acknowledgement. Use the same trusted API/upload origins and token validation as the production writer, but hard-code the candidate policy and exact allowed asset classes. Snapshot inputs before reading any field. Do not add `If-Match` or `If-Unmodified-Since`: GitHub does not document conditional Release PATCH. Implement the approved compare-before-write fence honestly and expose `atomic: false` in the body-mutation receipt. + +- [ ] **Step 4: Run adapter tests and verify GREEN** + +Expected: all read and write boundary tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/release/duplicate-draft-recovery-adapters.mjs \ + scripts/release/test/duplicate-draft-recovery-adapters.test.mjs +git commit -m "feat(release): add duplicate draft quarantine writer" +``` + +### Task 5: Implement idempotent apply orchestration + +**Files:** +- Modify: `scripts/release/duplicate-draft-recovery.mjs` +- Modify: `scripts/release/test/duplicate-draft-recovery.test.mjs` + +- [ ] **Step 1: Write failing transition tests** + +Build an in-memory boundary that records each read and write. Cover: + +```text +untouched -> body-archived -> receipt-archived -> quarantined +body-archived -> receipt-archived -> quarantined +receipt-archived -> quarantined +quarantined -> no-op +``` + +For every transition assert a fresh full authorization capture occurs immediately before the one mutation. Inject failures after each mutation, expire the evidence, recapture from the resulting partial state, and prove the next run resumes without restoring a marker. Assert the second duplicate never starts before the first is exactly quarantined. + +Require an exact frozen concurrency acknowledgement: + +```js +{ + acknowledged: true, + atomic: false, + mode: "operator-freeze-compare-before-write-v1", + releaseIds: [379982100, 379986168], +} +``` + +Missing, false, accessor-bearing, or expanded acknowledgements must +fail before writer construction or any mutation. + +- [ ] **Step 2: Run core tests and verify RED** + +Run the Task 1 test command. + +Expected: FAIL because apply orchestration is absent. + +- [ ] **Step 3: Implement apply orchestration** + +Add: + +```js +export async function applyDuplicateDraftRecovery({ + evidence, + concurrencyAcknowledgement, + reader, + createWriter, + observer, + now, +}) { + // verify evidence, converge duplicate IDs in ascending order, + // recapture before each mutation, and return final credential-free receipt +} +``` + +Writer construction must occur only after the exact concurrency acknowledgement and first complete live evidence comparison. Track whether each duplicate's body PATCH was performed by this invocation. The final authorization receipt must be canonical, frozen, credential-free, and contain `atomic: false`, the acknowledgement/freeze scope, apply/evidence times, and one exact discriminated record per duplicate: + +```js +{ + releaseId: 379982100, + outcome: "performed", + preWriteFence: { observedAt, projectionSha256, tagObjectSha }, + postWriteFence: { observedAt, projectionSha256, tagObjectSha }, +} + +{ + releaseId: 379986168, + outcome: "preexisting-quarantined", + priorFenceObservations: null, + verifiedAt, + projectionSha256, +} +``` + +Never convert a no-op into a claim about an earlier invocation. A failed or ambiguous writer result stops without the next mutation and without an automatic retry. After both duplicates converge, invoke the injected normal read-only production observer for the exact candidate and require: + +```js +{ + state: "CANDIDATE_ESCROWED", + disposition: "would-transition", + nextTransition: "publish-npm-packages", + conflicts: [], + diagnostics: [], + releaseId: 379991871, +} +``` + +Do not emulate that result from recovery-local state. + +- [ ] **Step 4: Test adversarial and concurrent cases** + +Add cases for an expired receipt, changed main, enabled workflow, new run, npm appearance, moved tag, fourth draft, canonical-body drift, service-digest drift, body-only PATCH followed by title drift, and a writer that returns a malformed receipt. Add partial recovery cases where a prior invocation quarantined the first duplicate and the resumed successful receipt records `preexisting-quarantined` with `priorFenceObservations: null`, while the second is `performed`. Prove no retry after timeout, transport rejection, retryable response, or ambiguous mutation. Every case must stop without the next mutation. + +- [ ] **Step 5: Run focused suites and verify GREEN** + +Run the Task 3 combined test command. + +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add scripts/release/duplicate-draft-recovery.mjs \ + scripts/release/test/duplicate-draft-recovery.test.mjs +git commit -m "feat(release): converge duplicate escrow drafts" +``` + +### Task 6: Add the strict operator CLI and write-once receipts + +**Files:** +- Create: `scripts/release/recover-v0.8.22-duplicate-drafts.mjs` +- Create: `scripts/release/test/duplicate-draft-recovery-cli.test.mjs` + +- [ ] **Step 1: Write failing CLI tests** + +Test exact accepted invocations: + +```text +capture --reviewed-commit <40-lowercase-hex> --output +apply --evidence --acknowledge-non-atomic-release-edit-freeze --output +``` + +The acknowledgement is an exact literal boolean flag with no value, alias, environment/config fallback, or joined form. It constructs the exact core acknowledgement only after canonical evidence parsing and before writer construction. The explicit final authorization receipt output avoids stdout being treated as durable evidence. Require every `--output` and `--evidence` path to be a distinct relative descendant of the repository's ignored `.dawn/release-recovery/` directory; reject the directory itself, absolute paths, traversal, alternate `.dawn` subdirectories, and paths that are not ignored by the repository. Also reject missing, duplicate, unknown, joined, reordered ambiguities, NUL/control characters, symlinks, hard links where applicable, existing conflicting files, and files above the evidence byte bound. Assert errors are stable and never contain tokens, remote bodies, signed URLs, or stack traces. + +- [ ] **Step 2: Run CLI tests and verify RED** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH \ +node --test scripts/release/test/duplicate-draft-recovery-cli.test.mjs +``` + +Expected: FAIL because the executable does not exist. + +- [ ] **Step 3: Implement CLI parsing and safe file writes** + +Follow `preflight-owner-cli.mjs` and `workflow-handoff.mjs` patterns: resolve the repository root, enforce the exact `.dawn/release-recovery/` containment boundary before every read or write, require that boundary to remain gitignored, use bounded no-follow reads, write to a mode-`0600` temporary file, fsync, no-clobber link/rename, cleanup on failure, and emit one concise success line. Serialize only the canonical final authorization receipt returned by core, including honest `performed`/`preexisting-quarantined` records; never synthesize prior fences. Construct `GITHUB_TOKEN` dependencies only for production capture/apply; never serialize the token. + +- [ ] **Step 4: Run CLI tests and verify GREEN** + +Expected: PASS. + +- [ ] **Step 5: Run all three focused suites** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH \ +node --test \ + scripts/release/test/duplicate-draft-recovery.test.mjs \ + scripts/release/test/duplicate-draft-recovery-adapters.test.mjs \ + scripts/release/test/duplicate-draft-recovery-cli.test.mjs +``` + +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add scripts/release/recover-v0.8.22-duplicate-drafts.mjs \ + scripts/release/test/duplicate-draft-recovery-cli.test.mjs +git commit -m "feat(release): add v0.8.22 recovery command" +``` + +### Task 7: Document the temporary operator sequence + +**Files:** +- Modify: `docs/superpowers/runbooks/2026-08-09-release-integrity-cutover.md` + +- [ ] **Step 1: Add a candidate-specific recovery section** + +Document exact prerequisites, capture/apply syntax including the non-atomic freeze acknowledgement, the two duplicate IDs, expected four-state recovery behavior, evidence inspection, independent post-apply reads, Release enablement, exact-tag dispatch, and stop conditions. State plainly that the command does not delete drafts or publish npm and that GitHub provides no atomic conditional Release PATCH. + +- [ ] **Step 2: Add live receipt fields** + +Use the spec's exact artifact names. Add pending operator freeze record fields for authenticated operator, establishment/release times, scope Release IDs, evidence capture time, each pre/post fence time/outcome, and any exact partial/ambiguous state. Add recovery PR/head/merge SHA, evidence SHA-256, duplicate recovery receipt asset IDs/digests, post-quarantine body digests, final authorization receipt path/SHA-256 and per-duplicate `performed`/`preexisting-quarantined` outcome, final observer result, release run, npm/provenance conclusions, immutable Release ID, v0.8.23 run, and cleanup PR. + +- [ ] **Step 3: Verify docs and workflow reachability** + +```bash +node scripts/check-docs.mjs +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH \ +pnpm test:release-controller +``` + +Expected: docs pass; release-controller passes except no new failure may be attributed to the pre-existing 100 ms process-start timing test. If that test fails, rerun it on the exact unmodified `origin/main` SHA and retain both outputs before proceeding. + +- [ ] **Step 4: Commit** + +```bash +git add docs/superpowers/runbooks/2026-08-09-release-integrity-cutover.md +git commit -m "docs(release): add duplicate draft recovery runbook" +``` + +### Task 8: Verify, review, and merge the recovery implementation + +**Files:** +- Verify all files changed in Tasks 1–7. + +- [ ] **Step 1: Run focused verification** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH \ +node --test \ + scripts/release/test/duplicate-draft-recovery.test.mjs \ + scripts/release/test/duplicate-draft-recovery-adapters.test.mjs \ + scripts/release/test/duplicate-draft-recovery-cli.test.mjs +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH pnpm lint +git diff --check origin/main...HEAD +``` + +Expected: all pass. + +- [ ] **Step 2: 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. Diagnose any failure from first principles. A reproduced unchanged host-timing failure may be documented, but modified release code receives no waiver. + +- [ ] **Step 3: Request code review** + +Invoke `superpowers:requesting-code-review`. Require a reviewer to verify that normal duplicate detection and release workflow bytes are unchanged, every mutation is candidate-pinned, partial states are resumable, and no destructive HTTP method exists. + +- [ ] **Step 4: Push and open the PR** + +```bash +git push -u origin blove/recover-0.8.22-duplicate-drafts +gh pr create --repo cacheplane/dawnai --base main \ + --head blove/recover-0.8.22-duplicate-drafts \ + --title "fix(release): recover duplicate v0.8.22 drafts" \ + --body-file +``` + +- [ ] **Step 5: Require exact-head checks and review** + +Record the PR head SHA. Approve any bot-authored `action_required` workflow runs when GitHub requires owner approval. Require `CI / validate`, CodeQL, release-boundary jobs, Vercel, CopilotKit, and substantive infrastructure lanes for that exact head. Treat only the known no-credit reviewer lane as non-blocking. + +- [ ] **Step 6: Merge only the reviewed head while Release remains disabled** + +Use the repository's merge-commit convention and an exact-head guard. Immediately before merge, re-read `.github/workflows/release.yml` and require `disabled_manually`, re-read the PR head and require the reviewed SHA, fetch current `main`, and compute the prospective merge tree. Merge only if the prospective merge tree equals the reviewed head tree; if `main` advanced, update and re-review the branch instead of accepting a different tree. After merge, fetch `main`, require the actual merge tree equals the reviewed head tree, and record the resulting merge SHA/tree for `--reviewed-commit`. Stop before production recovery if any equality or workflow-state check changes. + +### Task 9: Quarantine the duplicates and release v0.8.22 + +**Files:** +- Production evidence only under ignored `.dawn/release-recovery/`. + +- [ ] **Step 1: Create an exact merged checkout** + +Use `superpowers:using-git-worktrees`. Create a detached worktree at the exact recovery merge SHA, install with the repository-pinned Node/pnpm versions, and require remote `main` to remain that SHA. + +- [ ] **Step 2: Reconfirm non-mutating prerequisites** + +Independently verify Release is disabled, Immutable Releases is enabled, no release run is active, all 21 npm versions remain absent, the annotated tag is exact, and the three draft IDs/tag names/45-asset inventories still match the policy. + +- [ ] **Step 3: Establish and record the operator edit freeze** + +Resolve the authenticated GitHub operator identity read-only. Record the operator, UTC establishment time, exact scope `[379982100, 379986168]`, workflow-disabled observation, and the explicit non-atomic TOCTOU limitation in the operator freeze record. From this point until Step 7 completes, no human, bot, or process may edit either duplicate Release. + +- [ ] **Step 4: Capture fresh evidence under the freeze** + +```bash +install -d -m 0700 .dawn/release-recovery +node scripts/release/recover-v0.8.22-duplicate-drafts.mjs capture \ + --reviewed-commit "$RECOVERY_SHA" \ + --output .dawn/release-recovery/v0.8.22-capture-01.json +``` + +Hash the file, inspect only credential-free facts, and require both duplicates to report an exact recognized state. Every recapture uses the next unused sequence number (`capture-02.json`, `capture-03.json`, and so on); evidence files are never replaced. + +- [ ] **Step 5: Apply once** + +```bash +node scripts/release/recover-v0.8.22-duplicate-drafts.mjs apply \ + --evidence .dawn/release-recovery/v0.8.22-capture-01.json \ + --acknowledge-non-atomic-release-edit-freeze \ + --output .dawn/release-recovery/v0.8.22-apply-01.json +``` + +Do not retry blindly. On failure, preserve all created files and keep the edit freeze active. Use read-only capture at the next unused `capture-NN.json` to establish an exact recognized partial state and record it in the operator freeze record. Invoke `apply` again only after deliberate review, using that exact new evidence path, the acknowledgement flag, and a matching unused `apply-NN.json` output path. Never infer or synthesize prior fence observations. + +- [ ] **Step 6: Verify quarantine and final authorization receipt independently** + +Read all three Releases and assets directly. Require the canonical body/assets unchanged, both duplicate bodies non-marker recovery notices, both original bodies downloadable byte-for-byte, both duplicate recovery receipt assets canonical, no exact `v0.8.22` duplicate tag names, and final observation `CANDIDATE_ESCROWED -> publish-npm-packages`. Verify the local final authorization receipt is canonical, credential-free, `atomic: false`, scope-exact, and records each duplicate honestly as `performed` or `preexisting-quarantined`; the latter must contain `priorFenceObservations: null`. + +- [ ] **Step 7: Release the edit freeze** + +Record the exact successful or recognized partial outcome and UTC release time in the operator freeze record. Release the freeze only after Step 6 succeeds, or after a failed/ambiguous apply has been read-only recaptured into one of the exact recognized states and recorded. Do not enable Release while the state is partial or ambiguous. + +- [ ] **Step 8: Enable and dispatch Release** + +Enable only `.github/workflows/release.yml`, re-read its state as `active`, then dispatch: + +```bash +gh workflow run release.yml --repo cacheplane/dawnai \ + --ref v0.8.22 \ + -f version=0.8.22 \ + -f commitSha=2a80deece2ff958fe7fde8fddeb4f99bed70a1c8 \ + -f operation=reconcile +``` + +Record the exact direct dispatch/run identity. Do not cancel or generically rerun a failed transition. + +- [ ] **Step 9: Observe every irreversible boundary** + +Require serial trusted publication of all 21 packages, exact npm provenance, npm reconciliation, five smoke lanes, independent audit, and immutable GitHub publication. Stop at the first failed transition and preserve all evidence. + +- [ ] **Step 10: Verify v0.8.22 independently** + +Confirm each exact npm version/integrity/provenance/latest tag, final Release `379991871` at `v0.8.22`, exact candidate SHA, immutable state, audited marker, exact asset set, and production smoke receipts. + +### Task 10: Publish the README-bearing v0.8.23 release + +**Files:** +- Existing Version Packages PR #525 and production evidence only. + +- [ ] **Step 1: Refresh PR #525 state** + +Require its fixed-group version is `0.8.23`, it consumes the README changeset, and it contains no unexpected source changes. If the Changesets bot updates the head, review the new exact diff and SHA. + +- [ ] **Step 2: Approve and run exact-head CI** + +Approve bot-authored Actions runs when required. Require `CI / validate`, changesets, CodeQL, Vercel, CopilotKit, release-boundary, and substantive infrastructure checks on the exact current head. + +- [ ] **Step 3: Merge on green** + +Merge PR #525 using the repository's merge convention only after exact-head checks are green. Record the merge SHA. + +- [ ] **Step 4: Observe v0.8.23 release** + +Watch the controller through tag creation, immutable payload, npm trusted publication, smoke/audit, and final Release. Do not leapfrog or manually publish. + +- [ ] **Step 5: Verify npm README rendering** + +For representative packages in each README tier and `create-dawn-ai-app`, verify npm latest is `0.8.23`, tarball README bytes match the sealed manifest, and npm displays the updated awareness content and video-linked poster assets. + +### Task 11: Remove the one-time recovery surface + +**Files:** +- Delete: `scripts/release/duplicate-draft-recovery.mjs` +- Delete: `scripts/release/duplicate-draft-recovery-adapters.mjs` +- Delete: `scripts/release/recover-v0.8.22-duplicate-drafts.mjs` +- Delete: `scripts/release/test/duplicate-draft-recovery.test.mjs` +- Delete: `scripts/release/test/duplicate-draft-recovery-adapters.test.mjs` +- Delete: `scripts/release/test/duplicate-draft-recovery-cli.test.mjs` +- Modify: `docs/superpowers/runbooks/2026-08-09-release-integrity-cutover.md` + +- [ ] **Step 1: Create a cleanup branch from terminal v0.8.23 main** + +Remove the temporary executable, support modules, tests, and candidate-specific operational instructions. Preserve the design, implementation plan, operator freeze record, local final authorization receipt, and duplicate GitHub evidence drafts/assets. + +- [ ] **Step 2: Verify no reachability or stale references remain** + +```bash +rg -n "recover-v0\.8\.22-duplicate-drafts|duplicate-draft-recovery" \ + .github package.json scripts docs/superpowers/runbooks +pnpm test:release-controller +node scripts/check-docs.mjs +pnpm lint +git diff --check +``` + +Expected: only historical design/plan/live-receipt references remain where intentionally retained; all checks pass. + +- [ ] **Step 3: Open and merge the cleanup PR** + +Require exact-head CI and review, then merge on green. Confirm Release remains active, npm latest remains `0.8.23`, and no scheduled reconciliation mutates the terminal immutable releases. + +## Completion Receipt + +Record in the final handoff: + +- recovery spec and plan commits; +- recovery PR number, reviewed head, merge SHA, and equal-tree proof; +- capture evidence and final authorization receipt digests; +- exact duplicate archive/recovery-receipt asset IDs and body digests; +- v0.8.22 release run, npm/provenance conclusions, and immutable Release URL; +- PR #525 exact head and merge SHA; +- v0.8.23 release run and representative npm README verification; +- cleanup PR and merge SHA; and +- any separately tracked pre-existing test timing failure with exact untouched-main reproduction. diff --git a/docs/superpowers/runbooks/2026-08-09-release-integrity-cutover.md b/docs/superpowers/runbooks/2026-08-09-release-integrity-cutover.md index 71a9d67a6..332e3d321 100644 --- a/docs/superpowers/runbooks/2026-08-09-release-integrity-cutover.md +++ b/docs/superpowers/runbooks/2026-08-09-release-integrity-cutover.md @@ -213,6 +213,1002 @@ remain empty, and the aggregate abandonment mode must remain disabled. If `main` moves, evidence expires, a workflow state differs, or a candidate draft exists before Immutable Releases was enabled, stop the cutover. +## v0.8.22 duplicate-draft recovery (one time) + +Use this candidate-specific procedure only for the reviewed v0.8.22 recovery. +It preserves the canonical draft at Release `379991871` and quarantines the two +duplicate controller identities without deleting either draft or any asset: + +| Role | Release ID | Required temporary `tag_name` | +| --- | ---: | --- | +| Canonical candidate | `379991871` | `untagged-be0ff4bee4ba43b521a9` | +| Duplicate | `379982100` | `untagged-a13939767dd2419ade01` | +| Duplicate | `379986168` | `untagged-20706099efa3c38335a8` | + +The exact candidate is version `0.8.22` at commit +`2a80deece2ff958fe7fde8fddeb4f99bed70a1c8`. The recovery command cannot +delete drafts, enable or dispatch Release, or publish npm. GitHub does not +provide an atomic conditional `PATCH` for the Release update endpoint. The +body-only update therefore depends on an explicit operator edit freeze plus a +final compare-before-write fence; do not describe it as compare-and-swap or +atomic. + +### Prerequisites and reviewed authority + +Keep `.github/workflows/release.yml` in `disabled_manually` throughout this +procedure. Before establishing the freeze, independently require Immutable +Releases to be enabled, no Release workflow run to be nonterminal, annotated +tag `v0.8.22` to peel to the exact candidate commit, no published Release to +use `v0.8.22`, and exact npm version `0.8.22` to remain absent for all 21 +packages. Every terminal and nonterminal Release workflow run at the candidate +SHA must have complete attempt/job coverage, and its one `publish-npm` job per +attempt must never have started: only `queued` with no conclusion or `completed` +with conclusion `skipped` is acceptable. Require the canonical and duplicate +numeric IDs, temporary tag names, draft metadata, bodies, and complete +45-base-asset inventories to match the reviewed recovery policy. Stop on a +fourth marker-backed or exact-tag candidate, an unavailable read, or any +mismatch. + +Pre-merge production reference (read-only, `2026-09-02`, recorded so the freeze +window is not the first time these are checked; re-verify them live at capture +time rather than trusting this table): + +| Fact | Observed | +| --- | --- | +| Annotated tag `v0.8.22` peels to | `2a80deece2ff958fe7fde8fddeb4f99bed70a1c8` | +| Draft Releases identifying the candidate | exactly three; no fourth | +| Release IDs / temporary tag names | exactly the policy values | +| Body SHA-256 of all three drafts | `54924b7e963e593d3988d9ce1708bfbb2dfec46606cd4a47125987d24ad789f0` (both duplicates `untouched`) | +| Base assets per draft | 45, mutable, not prerelease | +| `.github/workflows/release.yml` | `disabled_manually`, no nonterminal runs | +| Candidate runs | 5, all `completed`; each `publish-npm` job `completed`/`skipped`, one attempt each | +| Published Release at `v0.8.22` | none | +| npm | `latest` = `0.8.21`; `0.8.22` returns `E404` for all 21 packages | + +Trailing-newline round trip: the recovery notice ends with exactly one `\n`, and +its byte-exact survival through `PATCH` is not an assumption. `canonicalReleaseBody` +composes controller bodies that also end with exactly one `\n`, and the shipped +`updateDraftReleaseIfCurrent` PATCHes such a body and then re-reads it under exact +body equality. The escrow drafts' stored bodies carry that trailing newline today. + +Run only from a newly cloned, isolated checkout of the merged recovery commit. +Do not reuse a worktree or clone that has ever contained `node_modules`; if any +preexisting dependency tree is found in the checkout or its module-resolution +ancestors, fail and create another isolated checkout rather than deleting or +overwriting an unknown tree. Let +`RECOVERY_SHA` be the 40-character lowercase merge SHA, not the pull-request +branch head, a later `main`, or the candidate commit. Fetch `main`, then require +local `HEAD`, `origin/main`, and `RECOVERY_SHA` to be identical. The `capture` +command additionally verifies that this SHA is the merge commit of exactly one +merged pull request targeting `main`, that `CI / validate` succeeded at its +reviewed head, and that the merge commit tree equals the reviewed head tree. +Do not continue if any identity or tree check differs. + +Before loading credentials, start one fresh zsh session, create the fresh clone +and a fresh private pnpm store, and establish clean local execution authority. +Replace the placeholder with the reviewed merged recovery commit. The install +is lockfile-frozen, uses the exact `pnpm@10.33.0` declared by the reviewed root +`package.json`, enables content-store integrity verification, and disables all +dependency lifecycle scripts. Initial absence plus the uninterrupted install +and post-install checks prove that the only ignored dependency trees used by +this procedure were materialized from the reviewed lockfile during this +session; they do not claim that a registry is trustworthy beyond the +lockfile's integrity bindings. Never load the GitHub token before this block +finishes. + +```zsh +set -euo pipefail +umask 077 +unset NODE_OPTIONS NODE_PATH +RECOVERY_PARENT="$(mktemp -d "${TMPDIR%/}/dawn-v0.8.22-recovery.XXXXXXXX")" +RECOVERY_PARENT="${RECOVERY_PARENT:A}" +chmod 0700 "$RECOVERY_PARENT" +RECOVERY_CHECKOUT="$RECOVERY_PARENT/dawn" +PNPM_STORE_DIR="$RECOVERY_PARENT/pnpm-store" +test ! -e "$RECOVERY_CHECKOUT" +test ! -e "$PNPM_STORE_DIR" +git -c core.hooksPath=/dev/null clone https://github.com/cacheplane/dawnai.git "$RECOVERY_CHECKOUT" +cd "$RECOVERY_CHECKOUT" +test "$(pwd -P)" = "$RECOVERY_CHECKOUT" +test -z "$(find . -name node_modules -prune -print -quit)" +MODULE_ANCESTOR="${RECOVERY_CHECKOUT:h}" +while [ "$MODULE_ANCESTOR" != / ]; do + test ! -e "$MODULE_ANCESTOR/node_modules" + test ! -L "$MODULE_ANCESTOR/node_modules" + MODULE_ANCESTOR="${MODULE_ANCESTOR:h}" +done +test ! -e /node_modules +test ! -L /node_modules +git fetch origin main --tags +RECOVERY_SHA='' +printf '%s\n' "$RECOVERY_SHA" | grep -Eq '^[0-9a-f]{40}$' +test "$(git rev-parse HEAD)" = "$RECOVERY_SHA" +test "$(git rev-parse origin/main)" = "$RECOVERY_SHA" +git diff --quiet --exit-code +git diff --cached --quiet --exit-code +test -z "$(git ls-files --others --exclude-standard)" +test -z "$(git status --porcelain=v1 --untracked-files=all)" +git diff --quiet --exit-code "$RECOVERY_SHA" -- +test -z "${NODE_OPTIONS-}" +test -z "${NODE_PATH-}" +PATH="/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH" +export PATH RECOVERY_SHA RECOVERY_PARENT RECOVERY_CHECKOUT PNPM_STORE_DIR +test "$(node --version)" = 'v24.19.0' +test "$(pnpm --version)" = '10.33.0' +test "$(node -p 'require("./package.json").packageManager')" = 'pnpm@10.33.0' +test ! -e "$PNPM_STORE_DIR" +install -d -m 0700 "$PNPM_STORE_DIR" +pnpm install --frozen-lockfile --ignore-scripts --verify-store-integrity \ + --store-dir "$PNPM_STORE_DIR" +pnpm store status --store-dir "$PNPM_STORE_DIR" +chmod 0700 "$PNPM_STORE_DIR" +test -d node_modules +MODULE_ANCESTOR="${RECOVERY_CHECKOUT:h}" +while [ "$MODULE_ANCESTOR" != / ]; do + test ! -e "$MODULE_ANCESTOR/node_modules" + test ! -L "$MODULE_ANCESTOR/node_modules" + MODULE_ANCESTOR="${MODULE_ANCESTOR:h}" +done +test ! -e /node_modules +test ! -L /node_modules +test -z "$(git status --porcelain=v1 --untracked-files=all)" +git status --ignored --porcelain=v1 --untracked-files=all | node -e ' + let input = "" + process.stdin.setEncoding("utf8") + process.stdin.on("data", (chunk) => { input += chunk }) + process.stdin.on("end", () => { + const lines = input.split("\n").filter(Boolean) + if (lines.length === 0 || lines.some((line) => !/^!! (?:.+\/)?node_modules\/$/u.test(line))) { + throw new Error("Post-install ignored paths are not exclusively node_modules trees") + } + }) +' +git diff --quiet --exit-code +git diff --cached --quiet --exit-code +git diff --quiet --exit-code "$RECOVERY_SHA" -- +test -z "$(git ls-files --others --exclude-standard)" +test -z "${NODE_OPTIONS-}" +test -z "${NODE_PATH-}" +``` + +### Establish the operator edit freeze and private directory + +Require one nonempty `GITHUB_TOKEN` for both the recovery CLI and every `gh` or +direct API call. `GH_TOKEN` must be overwritten with that exact value. Resolve +and record the login without printing the token, then re-run the final equality +before capture, apply, verification, enablement, and dispatch. Any login change +or mismatch stops the procedure. + +```zsh +: "${GITHUB_TOKEN:?GITHUB_TOKEN must be set for the authenticated operator}" +test -n "$GITHUB_TOKEN" +export GH_TOKEN="$GITHUB_TOKEN" +OPERATOR_LOGIN="$(GH_TOKEN="$GITHUB_TOKEN" gh api user -H 'X-GitHub-Api-Version: 2022-11-28' --jq '.login')" +test -n "$OPERATOR_LOGIN" +test "$(GH_TOKEN="$GITHUB_TOKEN" gh api user -H 'X-GitHub-Api-Version: 2022-11-28' --jq '.login')" = "$OPERATOR_LOGIN" +``` + +After that same-principal check succeeds, enter `OPERATOR_LOGIN`, the UTC +establishment time, exact scope `[379982100, 379986168]`, the +workflow-disabled observation, and the explicit non-atomic +time-of-check/time-of-use limitation in the operator freeze record. From that +point until the freeze is deliberately released, no human, bot, workflow, or +other process may edit either duplicate Release. + +Create both private directories as the authenticated local operator. Every +directory from the repository root through `.dawn` must be owned by that +operator and not group- or world-writable; the final recovery directory must be +exactly mode `0700`. The CLI fails closed before credential or writer +construction if these conditions do not hold. + +```bash +umask 077 +install -d -m 0700 .dawn +install -d -m 0700 .dawn/release-recovery +chmod go-w .dawn +chmod 0700 .dawn/release-recovery +``` + +The directory must remain ignored by the reviewed Git policy. Capture and apply +use distinct, unused, regular-file paths below exactly +`.dawn/release-recovery/`. They refuse symlinks, hard links, clobbering, +traversal, absolute paths, alternate `.dawn` directories, and unsafe file +modes. A resumed attempt always uses the next unused matching sequence number; +never replace or reuse a path. + +### Capture and inspect fresh evidence + +With the edit freeze active and all prerequisites still exact, run: + +```zsh +set -euo pipefail +test "$(GH_TOKEN="$GITHUB_TOKEN" gh api user -H 'X-GitHub-Api-Version: 2022-11-28' --jq '.login')" = "$OPERATOR_LOGIN" +: "${ATTEMPT:=01}" +printf '%s\n' "$ATTEMPT" | grep -Eq '^(0[1-9]|[1-9][0-9])$' +typeset -gA CAPTURE_PATHS CAPTURE_STARTED_AT CAPTURE_FINISHED_AT CAPTURE_EXIT_CODES CAPTURE_SHA256S +CAPTURE_PATHS[$ATTEMPT]=".dawn/release-recovery/v0.8.22-capture-$ATTEMPT.json" +test ! -e "${CAPTURE_PATHS[$ATTEMPT]}" +CAPTURE_STARTED_AT[$ATTEMPT]="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" +if env -u NODE_OPTIONS -u NODE_PATH GITHUB_TOKEN="$GITHUB_TOKEN" \ + node scripts/release/recover-v0.8.22-duplicate-drafts.mjs capture \ + --reviewed-commit "$RECOVERY_SHA" \ + --output "${CAPTURE_PATHS[$ATTEMPT]}"; then + CAPTURE_EXIT_CODES[$ATTEMPT]=0 +else + CAPTURE_EXIT_CODES[$ATTEMPT]=$? +fi +CAPTURE_FINISHED_AT[$ATTEMPT]="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" +``` + +Capture is read-only and writes one credential-free, canonical evidence file +with mode `0600`. It is valid for at most 15 minutes. Hash it and inspect its +JSON before apply: + +```zsh +set -euo pipefail +test "${CAPTURE_EXIT_CODES[$ATTEMPT]}" -eq 0 +CAPTURE_SHA256S[$ATTEMPT]="$(shasum -a 256 "${CAPTURE_PATHS[$ATTEMPT]}" | awk '{print $1}')" +printf '%s %s\n' "${CAPTURE_SHA256S[$ATTEMPT]}" "${CAPTURE_PATHS[$ATTEMPT]}" +env -u NODE_OPTIONS -u NODE_PATH node -e ' + const fs = require("node:fs") + const path = process.argv[1] + const value = JSON.parse(fs.readFileSync(path, "utf8")) + process.stdout.write(`${JSON.stringify(value, null, 2)}\n`) +' "${CAPTURE_PATHS[$ATTEMPT]}" +``` + +Require the repository/recovery/candidate identities, tag object and peel, +workflow and Immutable Releases states, empty nonterminal-run inventory, +package-level npm absence, all three Release snapshots, body and asset +digests, and the expected notice/asset bytes to be exact. Capture emits evidence +only after it has also enumerated every terminal and nonterminal candidate run, +read every attempt's jobs, and rejected any `publish-npm` job that ever started; +require that acceptance gate during inspection even though raw job records are +not serialized into the credential-free evidence file. Each duplicate must +classify into exactly one of these states: + +| State | Exact recognized contents | Next resumable transition | +| --- | --- | --- | +| `untouched` | Original marker body and exactly 45 original base assets | Upload the original-body archive asset. | +| `body-archived` | Original body, 45 base assets, and the exact original-body archive asset | Upload the duplicate recovery receipt asset. | +| `receipt-archived` | Original body, 45 base assets, and both exact recovery evidence assets | Replace only the live body with the exact non-marker notice. | +| `quarantined` | Exact recovery notice, 45 base assets, and both exact recovery evidence assets | No mutation; verify the state. | + +The command processes the duplicates only in ascending ID order. It cannot +start Release `379986168` until Release `379982100` is exactly quarantined. +Anything outside these four states is a conflict, not a repair opportunity. + +### Apply once and handle partial outcomes + +Apply the inspected evidence exactly once with the literal acknowledgement +flag and a distinct unused output path: + +```zsh +set -euo pipefail +test "$(GH_TOKEN="$GITHUB_TOKEN" gh api user -H 'X-GitHub-Api-Version: 2022-11-28' --jq '.login')" = "$OPERATOR_LOGIN" +printf '%s\n' "$ATTEMPT" | grep -Eq '^(0[1-9]|[1-9][0-9])$' +typeset -gA APPLY_PATHS APPLY_STARTED_AT APPLY_FINISHED_AT APPLY_EXIT_CODES APPLY_SHA256S +APPLY_PATHS[$ATTEMPT]=".dawn/release-recovery/v0.8.22-apply-$ATTEMPT.json" +test ! -e "${APPLY_PATHS[$ATTEMPT]}" +APPLY_STARTED_AT[$ATTEMPT]="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" +if env -u NODE_OPTIONS -u NODE_PATH GITHUB_TOKEN="$GITHUB_TOKEN" \ + node scripts/release/recover-v0.8.22-duplicate-drafts.mjs apply \ + --evidence "${CAPTURE_PATHS[$ATTEMPT]}" \ + --acknowledge-non-atomic-release-edit-freeze \ + --output "${APPLY_PATHS[$ATTEMPT]}"; then + APPLY_EXIT_CODES[$ATTEMPT]=0 +else + APPLY_EXIT_CODES[$ATTEMPT]=$? +fi +APPLY_FINISHED_AT[$ATTEMPT]="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" +if [ "${APPLY_EXIT_CODES[$ATTEMPT]}" -eq 0 ]; then + APPLY_SHA256S[$ATTEMPT]="$(shasum -a 256 "${APPLY_PATHS[$ATTEMPT]}" | awk '{print $1}')" + SUCCESS_ATTEMPT="$ATTEMPT" +fi +``` + +The acknowledgement accepts no value, alias, reordered form, environment +fallback, or configuration substitute. `apply` reauthorizes every mutation +against fresh production reads, uploads only absent byte-identical recovery +assets, performs at most one body-only `PATCH` per duplicate, never retries an +ambiguous write, and emits the local write-once final authorization receipt +only after both duplicates and the final normal-controller observation pass. + +On any nonzero exit, preserve every remaining file, GitHub asset, and the +operator freeze. Record the exit, UTC time, and the exact known or ambiguous +state. Exit code `3` specifically means output cleanup is +uncertain: do not delete, rename, reuse, or infer the contents of that apply +path. Do not rerun `apply` blindly after a timeout, transport error, retryable +HTTP response, malformed response, or otherwise ambiguous outcome. + +Instead, use read-only `capture` at the next unused path, such as +`v0.8.22-capture-02.json`, while the freeze remains active. If it proves one of +the four exact states, inspect and hash it, record that state, and deliberately +resume only the missing transition with the matching unused +`v0.8.22-apply-02.json` path and the exact acknowledgement. A resumed final +authorization receipt may report a freshly verified +`preexisting-quarantined` duplicate with `priorFenceObservations: null`; never +invent an earlier invocation's pre/post fence observations. If fresh capture +cannot classify the live state exactly, keep the workflow disabled and the +edit freeze active, preserve all evidence, and escalate for review. + +For a deliberate second attempt, set the next number and paste the capture, +inspection, and apply blocks above unchanged. This is the copy/paste-safe resume +selector; it derives new paths and ledger values without reading or overwriting +attempt `01`: + +```zsh +ATTEMPT='02' +printf '%s\n' "$ATTEMPT" | grep -Eq '^(0[1-9]|[1-9][0-9])$' +test ! -e ".dawn/release-recovery/v0.8.22-capture-$ATTEMPT.json" +test ! -e ".dawn/release-recovery/v0.8.22-apply-$ATTEMPT.json" +# Paste the three unchanged capture/inspect/apply blocks; a successful apply +# assigns SUCCESS_ATTEMPT="$ATTEMPT" for independent verification. +``` + +Maintain this append-only attempt ledger in the operator freeze record. Add a +row immediately after every capture/apply pair, including failed or ambiguous +attempts. Preserve every numbered file; a missing digest must say why it could +not be safely read rather than being left implicit. + +| Attempt | Capture path / SHA-256 / UTC / exit | Apply path / SHA-256 / UTC / exit | Exact states after read-only observation | Output cleanup | Per-duplicate outcome | +| ---: | --- | --- | --- | --- | --- | +| `01` | pending | pending | pending for `379982100`; pending for `379986168` | pending (`clean` or `uncertain`) | pending (`performed`, `preexisting-quarantined`, or no final receipt) | + +For a `performed` duplicate, record its outcome and both exact fence objects: +`preWriteFence` and `postWriteFence` each contain only `observedAt`, +`projectionSha256`, and `tagObjectSha`. For a +`preexisting-quarantined` duplicate, record only `verifiedAt`, +`projectionSha256`, and `priorFenceObservations: null`. Do not assign an +outcome to a fence or invent a fence for a preexisting quarantine. + +### Independent verification and freeze release + +After successful apply, independently enumerate Releases with pagination and +read all three objects and their assets by numeric ID; do not rely on a +published-only tag lookup or solely on the recovery command's success line. +Download both evidence assets on each duplicate through the authenticated +numeric asset endpoint, not a response URL. Run this in the same zsh session; +it creates one unused mode-`0700` verification directory and keeps every API +response and download private. It prints no headers, token, or signed URL. + +```zsh +set -euo pipefail +test "$(GH_TOKEN="$GITHUB_TOKEN" gh api user -H 'X-GitHub-Api-Version: 2022-11-28' --jq '.login')" = "$OPERATOR_LOGIN" +: "${SUCCESS_ATTEMPT:?set SUCCESS_ATTEMPT to the successful two-digit ledger attempt}" +printf '%s\n' "$SUCCESS_ATTEMPT" | grep -Eq '^(0[1-9]|[1-9][0-9])$' +test "${CAPTURE_EXIT_CODES[$SUCCESS_ATTEMPT]}" -eq 0 +test "${APPLY_EXIT_CODES[$SUCCESS_ATTEMPT]}" -eq 0 +CAPTURE_PATH="${CAPTURE_PATHS[$SUCCESS_ATTEMPT]}" +APPLY_PATH="${APPLY_PATHS[$SUCCESS_ATTEMPT]}" +VERIFY_DIR=".dawn/release-recovery/v0.8.22-verify-$SUCCESS_ATTEMPT" +SUCCESS_LEDGER_ATTEMPT="$SUCCESS_ATTEMPT" +test "$CAPTURE_PATH" = ".dawn/release-recovery/v0.8.22-capture-$SUCCESS_ATTEMPT.json" +test "$APPLY_PATH" = ".dawn/release-recovery/v0.8.22-apply-$SUCCESS_ATTEMPT.json" +test -n "${CAPTURE_STARTED_AT[$SUCCESS_LEDGER_ATTEMPT]}" +test -n "${CAPTURE_FINISHED_AT[$SUCCESS_LEDGER_ATTEMPT]}" +test -n "${APPLY_STARTED_AT[$SUCCESS_LEDGER_ATTEMPT]}" +test -n "${APPLY_FINISHED_AT[$SUCCESS_LEDGER_ATTEMPT]}" +test "$(shasum -a 256 "$CAPTURE_PATH" | awk '{print $1}')" = "${CAPTURE_SHA256S[$SUCCESS_LEDGER_ATTEMPT]}" +test "$(shasum -a 256 "$APPLY_PATH" | awk '{print $1}')" = "${APPLY_SHA256S[$SUCCESS_LEDGER_ATTEMPT]}" +test ! -e "$VERIFY_DIR" +install -d -m 0700 "$VERIFY_DIR" +chmod 0700 "$VERIFY_DIR" + +GH_TOKEN="$GITHUB_TOKEN" gh api --paginate --slurp \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + 'repos/cacheplane/dawnai/releases?per_page=100' \ + > "$VERIFY_DIR/releases-pages.json" +GH_TOKEN="$GITHUB_TOKEN" gh api --paginate --slurp \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + 'repos/cacheplane/dawnai/actions/workflows/260503756/runs?per_page=100' \ + > "$VERIFY_DIR/release-run-pages.json" + +for release_id in 379991871 379982100 379986168; do + GH_TOKEN="$GITHUB_TOKEN" gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "repos/cacheplane/dawnai/releases/$release_id" \ + > "$VERIFY_DIR/release-$release_id.json" + GH_TOKEN="$GITHUB_TOKEN" gh api --paginate --slurp \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "repos/cacheplane/dawnai/releases/$release_id/assets?per_page=100" \ + > "$VERIFY_DIR/release-$release_id-assets-pages.json" +done + +VERIFY_DIR="$VERIFY_DIR" env -u NODE_OPTIONS -u NODE_PATH node --input-type=module \ + > "$VERIFY_DIR/candidate-run-ids.txt" <<'NODE' +import { readFileSync } from "node:fs" +const pages = JSON.parse(readFileSync(`${process.env.VERIFY_DIR}/release-run-pages.json`, "utf8")) +if (!Array.isArray(pages)) throw new Error("Release run pagination is malformed") +const runs = pages.flatMap((page) => { + if (!page || !Array.isArray(page.workflow_runs)) throw new Error("Release run page is malformed") + return page.workflow_runs +}) +const ids = new Set() +for (const run of runs) { + if (!Number.isSafeInteger(run.id) || ids.has(run.id)) throw new Error("Release run ID is invalid") + ids.add(run.id) + if (run.status !== "completed") throw new Error("A Release workflow run is nonterminal") + if (run.head_sha === "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8") { + process.stdout.write(`${run.id}\n`) + } +} +NODE + +while IFS= read -r run_id; do + printf '%s\n' "$run_id" | grep -Eq '^[1-9][0-9]*$' + GH_TOKEN="$GITHUB_TOKEN" gh api --paginate --slurp \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "repos/cacheplane/dawnai/actions/runs/$run_id/jobs?filter=all&per_page=100" \ + > "$VERIFY_DIR/run-$run_id-jobs-pages.json" +done < "$VERIFY_DIR/candidate-run-ids.txt" + +VERIFY_DIR="$VERIFY_DIR" env -u NODE_OPTIONS -u NODE_PATH node --input-type=module \ + > "$VERIFY_DIR/evidence-asset-ids.tsv" <<'NODE' +import { readFileSync } from "node:fs" +for (const releaseId of [379982100, 379986168]) { + const file = `${process.env.VERIFY_DIR}/release-${releaseId}-assets-pages.json` + const pages = JSON.parse(readFileSync(file, "utf8")) + if (!Array.isArray(pages) || pages.some((page) => !Array.isArray(page))) { + throw new Error("Release asset pagination is malformed") + } + const prefix = `dawn-v0.8.22-duplicate-${releaseId}-` + const assets = pages.flat().filter((asset) => + typeof asset?.name === "string" && asset.name.startsWith(prefix), + ) + if (assets.length !== 2 || assets.some((asset) => !Number.isSafeInteger(asset.id))) { + throw new Error("Recovery evidence asset identity is not exact") + } + for (const asset of assets) process.stdout.write(`${releaseId}\t${asset.id}\n`) +} +NODE + +while IFS=$'\t' read -r release_id asset_id; do + printf '%s\n' "$release_id" | grep -Eq '^(379982100|379986168)$' + printf '%s\n' "$asset_id" | grep -Eq '^[1-9][0-9]*$' + GH_TOKEN="$GITHUB_TOKEN" gh api \ + -H 'Accept: application/octet-stream' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "repos/cacheplane/dawnai/releases/assets/$asset_id" \ + > "$VERIFY_DIR/release-$release_id-asset-$asset_id.bin" +done < "$VERIFY_DIR/evidence-asset-ids.tsv" + +find "$VERIFY_DIR" -type f -exec chmod 0600 {} + +``` + +Run this fail-closed verifier over the preserved files. It reuses the recovery +module's canonical evidence parser; independently validates canonical final +authorization and duplicate recovery receipt JSON; checks list/numeric-ID +correlation, body/tag/draft state, exact 45/47 asset counts and digests; hashes +the downloaded evidence assets; and rechecks every candidate run attempt's +`publish-npm` job. Its output is a credential-free mode-`0600` verification +receipt. + +```zsh +set -euo pipefail +: "${GITHUB_TOKEN:?GITHUB_TOKEN must be nonempty for credential-leak verification}" +VERIFY_DIR="$VERIFY_DIR" CAPTURE_PATH="$CAPTURE_PATH" APPLY_PATH="$APPLY_PATH" \ + GITHUB_TOKEN="$GITHUB_TOKEN" \ + env -u NODE_OPTIONS -u NODE_PATH node --input-type=module \ + > "$VERIFY_DIR/independent-verification.json" <<'NODE' +import { createHash } from "node:crypto" +import { readFileSync, statSync } from "node:fs" +import { + canonicalDuplicateDraftEvidence, + parseDuplicateDraftEvidence, +} from "./scripts/release/duplicate-draft-recovery.mjs" +import { parseReleaseMarker } from "./scripts/release/metadata.mjs" + +const verifyDir = process.env.VERIFY_DIR +const evidenceBytes = readFileSync(process.env.CAPTURE_PATH) +const receiptBytes = readFileSync(process.env.APPLY_PATH) +const githubToken = process.env.GITHUB_TOKEN +if (typeof githubToken !== "string" || githubToken.length === 0) { + throw new Error("GitHub token is unavailable for credential-leak verification") +} +const githubTokenBytes = Buffer.from(githubToken, "utf8") +const sha256 = (value) => createHash("sha256").update(value).digest("hex") +const canonicalize = (value) => Array.isArray(value) + ? value.map(canonicalize) + : value && typeof value === "object" + ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])) + : value +const canonicalBytes = (value) => Buffer.from(`${JSON.stringify(canonicalize(value))}\n`, "utf8") +const exactKeys = (value, keys, label) => { + if (!value || Array.isArray(value) || typeof value !== "object") throw new Error(`${label} is invalid`) + const actual = Object.keys(value).sort().join(",") + if (actual !== [...keys].sort().join(",")) throw new Error(`${label} fields are not exact`) +} +const parseJson = (path) => JSON.parse(readFileSync(path, "utf8")) +const arrayPages = (path) => { + const pages = parseJson(path) + if (!Array.isArray(pages) || pages.some((page) => !Array.isArray(page))) { + throw new Error(`${path} pagination is malformed`) + } + return pages.flat() +} +const objectPages = (path, field) => { + const pages = parseJson(path) + if (!Array.isArray(pages) || pages.some((page) => !page || !Array.isArray(page[field]))) { + throw new Error(`${path} pagination is malformed`) + } + return pages.flatMap((page) => page[field]) +} +const requirePrivate = (path) => { + const stat = statSync(path) + if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o077) !== 0) { + throw new Error(`${path} is not one private regular file`) + } +} +const same = (left, right) => JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right)) +const shaPattern = /^[0-9a-f]{64}$/u +const timestamp = (value) => { + if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) return false + const canonical = new Date(Date.parse(value)).toISOString() + return value === canonical || (canonical.endsWith(".000Z") && value === canonical.replace(".000Z", "Z")) +} + +requirePrivate(process.env.CAPTURE_PATH) +requirePrivate(process.env.APPLY_PATH) +const evidence = parseDuplicateDraftEvidence(evidenceBytes) +if (!canonicalDuplicateDraftEvidence(evidence).equals(evidenceBytes)) { + throw new Error("Capture evidence is not canonical") +} +const receipt = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(receiptBytes)) +if (!canonicalBytes(receipt).equals(receiptBytes)) throw new Error("Final authorization receipt is not canonical") +exactKeys(receipt, ["schemaVersion", "atomic", "concurrencyAcknowledgement", "freezeScope", "evidenceCapturedAt", "appliedAt", "candidate", "duplicates", "finalAuthorization"], "final authorization receipt") +if (receipt.schemaVersion !== 1 || receipt.atomic !== false || receipt.evidenceCapturedAt !== evidence.capturedAt || !timestamp(receipt.appliedAt) || Date.parse(receipt.appliedAt) < Date.parse(receipt.evidenceCapturedAt)) { + throw new Error("Final authorization receipt identity is not exact") +} +if (receiptBytes.includes(githubTokenBytes)) throw new Error("Final authorization receipt contains the configured credential") +const expectedScope = [379982100, 379986168] +exactKeys(receipt.concurrencyAcknowledgement, ["acknowledged", "atomic", "mode", "releaseIds"], "acknowledgement") +exactKeys(receipt.freezeScope, ["mode", "releaseIds"], "freeze scope") +if (receipt.concurrencyAcknowledgement.acknowledged !== true || receipt.concurrencyAcknowledgement.atomic !== false || receipt.concurrencyAcknowledgement.mode !== "operator-freeze-compare-before-write-v1" || !same(receipt.concurrencyAcknowledgement.releaseIds, expectedScope) || receipt.freezeScope.mode !== "operator-freeze-compare-before-write-v1" || !same(receipt.freezeScope.releaseIds, expectedScope)) { + throw new Error("Final authorization freeze acknowledgement is not exact") +} +if (!same(receipt.candidate, { version: "0.8.22", commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", releaseId: 379991871 })) { + throw new Error("Final authorization candidate is not exact") +} +if (!Array.isArray(receipt.duplicates) || receipt.duplicates.length !== 2) throw new Error("Duplicate outcomes are not exact") +for (const [index, result] of receipt.duplicates.entries()) { + if (result.releaseId !== expectedScope[index]) throw new Error("Duplicate outcome order is not exact") + if (result.outcome === "performed") { + exactKeys(result, ["releaseId", "outcome", "preWriteFence", "postWriteFence"], "performed outcome") + for (const fence of [result.preWriteFence, result.postWriteFence]) { + exactKeys(fence, ["observedAt", "projectionSha256", "tagObjectSha"], "write fence") + if (!timestamp(fence.observedAt) || !shaPattern.test(fence.projectionSha256) || fence.tagObjectSha !== evidence.candidate.tagObjectSha) throw new Error("Write fence is not exact") + } + if (Date.parse(result.preWriteFence.observedAt) < Date.parse(receipt.evidenceCapturedAt) || Date.parse(result.preWriteFence.observedAt) > Date.parse(result.postWriteFence.observedAt) || Date.parse(result.postWriteFence.observedAt) > Date.parse(receipt.appliedAt)) throw new Error("Write fence times are not exact") + } else { + exactKeys(result, ["releaseId", "outcome", "priorFenceObservations", "verifiedAt", "projectionSha256"], "preexisting outcome") + if (result.outcome !== "preexisting-quarantined" || result.priorFenceObservations !== null || !timestamp(result.verifiedAt) || Date.parse(result.verifiedAt) < Date.parse(receipt.evidenceCapturedAt) || Date.parse(result.verifiedAt) > Date.parse(receipt.appliedAt) || !shaPattern.test(result.projectionSha256)) throw new Error("Preexisting quarantine outcome is not exact") + } +} +const expectedObserver = { state: "CANDIDATE_ESCROWED", disposition: "would-transition", nextTransition: "publish-npm-packages", conflicts: [], diagnostics: [], releaseId: 379991871 } +if (!same(receipt.finalAuthorization, expectedObserver)) throw new Error("Final observer result is not exact") + +const listed = arrayPages(`${verifyDir}/releases-pages.json`) +const listedIds = new Set() +for (const release of listed) { + if (!Number.isSafeInteger(release?.id) || listedIds.has(release.id)) throw new Error("Complete Release pagination has an invalid or duplicate ID") + listedIds.add(release.id) +} +const expectedIds = [379991871, ...expectedScope] +for (const releaseId of expectedIds) { + if (listed.filter((release) => release?.id === releaseId).length !== 1) throw new Error(`Release ${releaseId} list identity is not unique`) +} +const controllerVisible = listed.filter((release) => { + if (release?.tag_name === "v0.8.22") return true + if (typeof release?.body !== "string" || !release.body.includes("DAWN_RELEASE_CONTROLLER_MARKER")) return false + try { + const marker = parseReleaseMarker(release.body) + return marker.version === "0.8.22" || marker.commitSha === "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8" || marker.tag === "v0.8.22" + } catch { + throw new Error(`Release ${release?.id ?? "unknown"} has a malformed candidate marker`) + } +}) +if (controllerVisible.length !== 1 || controllerVisible[0].id !== 379991871) { + throw new Error("Complete Release inventory does not have exactly one canonical controller-visible candidate") +} +if (!same(parseReleaseMarker(controllerVisible[0].body), evidence.releases.canonical.marker)) { + throw new Error("The remaining controller-visible candidate marker is not canonical") +} +const findings = [] +for (const [index, releaseId] of expectedIds.entries()) { + const release = parseJson(`${verifyDir}/release-${releaseId}.json`) + const assets = arrayPages(`${verifyDir}/release-${releaseId}-assets-pages.json`) + const expected = index === 0 ? evidence.releases.canonical : evidence.releases.duplicates[index - 1] + const listedRelease = listed.find((item) => item.id === releaseId) + for (const field of ["id", "tag_name", "name", "body", "target_commitish", "draft", "prerelease", "immutable"]) { + if (listedRelease[field] !== release[field]) throw new Error(`Release ${releaseId} list/numeric ${field} differs`) + } + if (release.id !== releaseId || release.tag_name !== expected.tagName || release.name !== "Dawn v0.8.22" || release.draft !== true || release.prerelease !== false || release.immutable !== false || release.target_commitish !== "main") throw new Error(`Release ${releaseId} mutable draft metadata is not exact`) + if (index === 0) { + if (release.body !== expected.body || assets.length !== 45) throw new Error("Canonical Release body or asset count changed") + } else if (release.body !== expected.noticeBytes || release.body.includes("DAWN_RELEASE_CONTROLLER_MARKER") || assets.length !== 47) { + throw new Error(`Duplicate Release ${releaseId} is not quarantined`) + } + const ids = new Set() + const names = new Set() + for (const asset of assets) { + if (!Number.isSafeInteger(asset.id) || ids.has(asset.id) || typeof asset.name !== "string" || names.has(asset.name) || !Number.isSafeInteger(asset.size) || asset.size < 1 || !/^sha256:[0-9a-f]{64}$/u.test(asset.digest)) throw new Error(`Release ${releaseId} asset inventory is malformed`) + ids.add(asset.id) + names.add(asset.name) + } + const original = index === 0 ? expected.assets : expected.assets.slice(0, 45) + for (const asset of original) { + const live = assets.find((item) => item.id === asset.id) + if (!live || live.name !== asset.name || live.digest !== `sha256:${asset.sha256}`) throw new Error(`Release ${releaseId} original asset changed`) + } + const recoveryAssets = [] + if (index > 0) { + for (const item of [ + { name: expected.archiveAssetName, sha256: expected.originalBodySha256, bytes: Buffer.from(evidence.releases.canonical.body, "utf8") }, + { name: expected.receiptAssetName, sha256: expected.receiptSha256, bytes: Buffer.from(expected.receiptBytes, "utf8") }, + ]) { + const live = assets.find((asset) => asset.name === item.name) + if (!live || live.digest !== `sha256:${item.sha256}`) throw new Error(`Release ${releaseId} recovery asset metadata changed`) + const downloaded = readFileSync(`${verifyDir}/release-${releaseId}-asset-${live.id}.bin`) + requirePrivate(`${verifyDir}/release-${releaseId}-asset-${live.id}.bin`) + if (downloaded.length !== live.size || sha256(downloaded) !== item.sha256 || !downloaded.equals(item.bytes)) throw new Error(`Release ${releaseId} recovery asset bytes changed`) + if (downloaded.includes(githubTokenBytes)) throw new Error(`Release ${releaseId} recovery asset contains the configured credential`) + if (item.name === expected.receiptAssetName) { + const json = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(downloaded)) + if (!canonicalBytes(json).equals(downloaded)) throw new Error(`Release ${releaseId} duplicate recovery receipt asset is not canonical`) + } + recoveryAssets.push({ id: live.id, name: live.name, size: live.size, sha256: item.sha256 }) + } + } + findings.push({ releaseId, tagName: release.tag_name, name: release.name, targetCommitish: release.target_commitish, draft: release.draft, prerelease: release.prerelease, immutable: release.immutable, bodySha256: sha256(Buffer.from(release.body, "utf8")), assetCount: assets.length, recoveryAssets }) +} + +const runs = objectPages(`${verifyDir}/release-run-pages.json`, "workflow_runs") +const runIds = new Set() +for (const run of runs) { + if (!Number.isSafeInteger(run.id) || runIds.has(run.id) || run.status !== "completed") throw new Error("Release workflow run inventory is not terminal and unique") + runIds.add(run.id) + if (run.head_sha !== "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8") continue + if (!Number.isSafeInteger(run.run_attempt) || run.run_attempt < 1) throw new Error("Candidate run attempt is invalid") + const jobs = objectPages(`${verifyDir}/run-${run.id}-jobs-pages.json`, "jobs") + for (let attempt = 1; attempt <= run.run_attempt; attempt += 1) { + const publishers = jobs.filter((job) => job.run_id === run.id && job.run_attempt === attempt && job.name === "publish-npm") + if (publishers.length !== 1) throw new Error("Candidate publish-npm job coverage is not exact") + const job = publishers[0] + const neverStarted = (job.status === "queued" && job.conclusion === null && job.started_at === null) || (job.status === "completed" && job.conclusion === "skipped") + if (!neverStarted) throw new Error("A candidate publish-npm job started") + } +} + +const report = { + schemaVersion: 1, + verifiedAt: new Date().toISOString(), + evidenceSha256: sha256(evidenceBytes), + finalAuthorizationReceiptSha256: sha256(receiptBytes), + candidatePublishJobsStarted: false, + releases: findings, + finalAuthorization: receipt.finalAuthorization, +} +process.stdout.write(canonicalBytes(report)) +NODE +chmod 0600 "$VERIFY_DIR/independent-verification.json" +shasum -a 256 "$CAPTURE_PATH" "$APPLY_PATH" "$VERIFY_DIR/independent-verification.json" +``` + +Require all of the following before releasing the edit freeze: + +- canonical Release `379991871` still has its exact original body, temporary + tag, title `Dawn v0.8.22`, `target_commitish: main`, draft/prerelease/ + immutable flags, and original 45-member asset namespace; +- both duplicate Releases retain their exact opaque temporary tags and all 45 + original assets, exact title `Dawn v0.8.22`, target and draft flags, have the + exact non-marker recovery notice, and have no `v0.8.22` tag name; +- the complete paginated Release inventory contains exactly one Release with + either the exact v0.8.22 controller marker identity or exact tag `v0.8.22`, + and it is canonical Release `379991871`; there is no fourth marker-backed or + exact-tag candidate; +- each original-body archive downloads byte-for-byte to the canonical original + body, and each duplicate recovery receipt asset is canonical with the + recorded Release ID, asset ID, size, and SHA-256; +- the local final authorization receipt is canonical, credential-free, + `atomic: false`, and scope-exact; each duplicate is honestly either + `performed`, with this invocation's exact `preWriteFence` and + `postWriteFence` (`observedAt`, `projectionSha256`, and `tagObjectSha`), or + `preexisting-quarantined`, with a fresh `verifiedAt`, projection SHA-256, and + `priorFenceObservations: null`; and +- its normal-controller observer is exactly `state: CANDIDATE_ESCROWED`, + `disposition: would-transition`, `nextTransition: publish-npm-packages`, + `releaseId: 379991871`, `conflicts: []`, and `diagnostics: []`. + +Hash the final authorization receipt and enter the direct-read results in the +live receipt. Only after every successful verification above may the operator +freeze record receive its successful outcome and UTC release time and the edit +freeze be released. + +If apply failed or was ambiguous, release the edit freeze only after a fresh +read-only capture proves and records one exact recognized state. A partial state +may be preserved for a later reviewed resume, but Release must remain disabled. +If the state remains ambiguous, do not release the freeze. Under no failure +condition may an operator restore a marker body, delete a draft or asset, +enable Release, dispatch the workflow, or publish npm manually. + +### Resume the exact-tag release + +Only after both duplicates are independently verified as quarantined, the final +observer is exact, and the edit freeze is released may an owner enable +`.github/workflows/release.yml`. Its schedule is `17 7 * * *`; its +`dawn-release-controller` concurrency group queues and never cancels. Activate +only from `00:00` through `05:59` UTC, away from the 07:17 scheduled edge, and +still prove no queued or in-progress controller exists immediately before and +after enablement and again immediately before dispatch. Activation, all +post-enable reads, dispatch, and durable receipt validation are one trapped +operation below. Once enablement succeeds, every local error, normal exit, or +`INT`, `TERM`, or `HUP` before a fully validated durable HTTP-200 receipt first +attempts to disable Release and read back exact `disabled_manually`. A failed +disable or failed read-back is a hard escalation: preserve every artifact and +do not dispatch or retry. + +```zsh +run_v0822_activation_dispatch() ( + set -euo pipefail + umask 077 + : "${GITHUB_TOKEN:?GITHUB_TOKEN must be nonempty}" + case "$GITHUB_TOKEN" in + *$'\r'*|*$'\n'*) print -u2 'GITHUB_TOKEN contains a forbidden newline'; return 1 ;; + esac + test "$(GH_TOKEN="$GITHUB_TOKEN" gh api user -H 'X-GitHub-Api-Version: 2022-11-28' --jq '.login')" = "$OPERATOR_LOGIN" + UTC_HHMM="$(date -u '+%H%M')" + case "$UTC_HHMM" in + 0[0-5][0-9][0-9]) ;; + *) print -u2 'Release activation is outside the approved 00:00-05:59 UTC window'; return 1 ;; + esac + + ACTIVATION_DIR='.dawn/release-recovery/v0.8.22-activation-01' + test ! -e "$ACTIVATION_DIR" + install -d -m 0700 "$ACTIVATION_DIR" + chmod 0700 "$ACTIVATION_DIR" + ENABLE_MAY_BE_ACTIVE=false + DISPATCH_DURABLE=false + + assert_workflow_state() { + WORKFLOW_PATH="$1" EXPECTED_STATE="$2" env -u NODE_OPTIONS -u NODE_PATH node -e ' + const fs = require("node:fs") + const workflow = JSON.parse(fs.readFileSync(process.env.WORKFLOW_PATH, "utf8")) + if (workflow.id !== 260503756 || workflow.path !== ".github/workflows/release.yml" || workflow.state !== process.env.EXPECTED_STATE) throw new Error("Release workflow state is not exact") + ' + } + + assert_terminal_release_runs() { + RUNS_PATH="$1" env -u NODE_OPTIONS -u NODE_PATH node -e ' + const fs = require("node:fs") + const pages = JSON.parse(fs.readFileSync(process.env.RUNS_PATH, "utf8")) + if (!Array.isArray(pages)) throw new Error("Release run pagination is malformed") + const runs = pages.flatMap((page) => { + if (!page || !Array.isArray(page.workflow_runs)) throw new Error("Release run page is malformed") + return page.workflow_runs + }) + const ids = new Set() + for (const run of runs) { + if (!Number.isSafeInteger(run.id) || ids.has(run.id) || run.status !== "completed") throw new Error("Release run inventory is nonterminal or duplicated") + ids.add(run.id) + } + ' + } + + assert_same_release_run_ids() { + BASELINE_RUNS="$1" CURRENT_RUNS="$2" env -u NODE_OPTIONS -u NODE_PATH node -e ' + const fs = require("node:fs") + const ids = (path) => new Set(JSON.parse(fs.readFileSync(path, "utf8")).flatMap((page) => page.workflow_runs).map((run) => run.id)) + const baseline = ids(process.env.BASELINE_RUNS) + const current = ids(process.env.CURRENT_RUNS) + if (baseline.size !== current.size || [...current].some((id) => !baseline.has(id))) throw new Error("Release run inventory changed after enablement") + ' + } + + disable_release_after_abort() { + local disable_exit read_exit chmod_exit validate_exit + set +e + GH_TOKEN="$GITHUB_TOKEN" gh api --silent --method PUT \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + 'repos/cacheplane/dawnai/actions/workflows/260503756/disable' + disable_exit=$? + GH_TOKEN="$GITHUB_TOKEN" gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + 'repos/cacheplane/dawnai/actions/workflows/260503756' \ + > "$ACTIVATION_DIR/workflow-after-abort-disable.json" + read_exit=$? + chmod 0600 "$ACTIVATION_DIR/workflow-after-abort-disable.json" 2>/dev/null + chmod_exit=$? + assert_workflow_state "$ACTIVATION_DIR/workflow-after-abort-disable.json" disabled_manually + validate_exit=$? + set -e + if [ "$disable_exit" -ne 0 ] || [ "$read_exit" -ne 0 ] || [ "$chmod_exit" -ne 0 ] || [ "$validate_exit" -ne 0 ]; then + print -u2 'HARD ESCALATION: Release may still be active; preserve activation evidence and obtain an independent workflow-state read' + return 1 + fi + print -u2 'Release disabled and independently read back as disabled_manually after aborted dispatch' + } + + fail_safe_exit() { + local original_exit_code=$? + trap - EXIT INT TERM HUP + if [ "$ENABLE_MAY_BE_ACTIVE" = true ] && [ "$DISPATCH_DURABLE" != true ]; then + if ! disable_release_after_abort; then + original_exit_code=125 + elif [ "$original_exit_code" -eq 0 ]; then + original_exit_code=1 + fi + fi + exit "$original_exit_code" + } + trap 'fail_safe_exit' EXIT + trap 'exit 129' HUP + trap 'exit 130' INT + trap 'exit 143' TERM + + PRE_RUNS="$ACTIVATION_DIR/pre-enable-runs.json" + POST_RUNS="$ACTIVATION_DIR/post-enable-runs.json" + PRE_DISPATCH_RUNS="$ACTIVATION_DIR/pre-dispatch-runs.json" + GH_TOKEN="$GITHUB_TOKEN" gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + 'repos/cacheplane/dawnai/actions/workflows/260503756' \ + > "$ACTIVATION_DIR/workflow-before-enable.json" + chmod 0600 "$ACTIVATION_DIR/workflow-before-enable.json" + assert_workflow_state "$ACTIVATION_DIR/workflow-before-enable.json" disabled_manually + GH_TOKEN="$GITHUB_TOKEN" gh api --paginate --slurp \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + 'repos/cacheplane/dawnai/actions/workflows/260503756/runs?per_page=100' \ + > "$PRE_RUNS" + chmod 0600 "$PRE_RUNS" + assert_terminal_release_runs "$PRE_RUNS" + + ENABLED_AT="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" + ENABLE_MAY_BE_ACTIVE=true + GH_TOKEN="$GITHUB_TOKEN" gh api --silent --method PUT \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + 'repos/cacheplane/dawnai/actions/workflows/260503756/enable' + GH_TOKEN="$GITHUB_TOKEN" gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + 'repos/cacheplane/dawnai/actions/workflows/260503756' \ + > "$ACTIVATION_DIR/workflow-after-enable.json" + chmod 0600 "$ACTIVATION_DIR/workflow-after-enable.json" + assert_workflow_state "$ACTIVATION_DIR/workflow-after-enable.json" active + GH_TOKEN="$GITHUB_TOKEN" gh api --paginate --slurp \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + 'repos/cacheplane/dawnai/actions/workflows/260503756/runs?per_page=100' \ + > "$POST_RUNS" + chmod 0600 "$POST_RUNS" + assert_terminal_release_runs "$POST_RUNS" + assert_same_release_run_ids "$PRE_RUNS" "$POST_RUNS" + printf '%s\n' "$ENABLED_AT" > "$ACTIVATION_DIR/enabled-at.txt" + chmod 0600 "$ACTIVATION_DIR/enabled-at.txt" + + DISPATCH_BODY="$ACTIVATION_DIR/v0.8.22-dispatch-body.json" + DISPATCH_RECEIPT="$ACTIVATION_DIR/v0.8.22-dispatch-receipt.json" + DISPATCH_STATUS="$ACTIVATION_DIR/v0.8.22-dispatch-http-status.txt" + DISPATCH_HASHES="$ACTIVATION_DIR/v0.8.22-dispatch-sha256.txt" + env -u NODE_OPTIONS -u NODE_PATH node -e ' + const body = { ref: "v0.8.22", inputs: { version: "0.8.22", commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", operation: "reconcile" } } + process.stdout.write(JSON.stringify(body)) + ' > "$DISPATCH_BODY" + chmod 0600 "$DISPATCH_BODY" + + test "$(GH_TOKEN="$GITHUB_TOKEN" gh api user -H 'X-GitHub-Api-Version: 2022-11-28' --jq '.login')" = "$OPERATOR_LOGIN" + UTC_HHMM="$(date -u '+%H%M')" + case "$UTC_HHMM" in + 0[0-5][0-9][0-9]) ;; + *) print -u2 'Release dispatch is outside the approved 00:00-05:59 UTC window'; return 1 ;; + esac + GH_TOKEN="$GITHUB_TOKEN" gh api --paginate --slurp \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + 'repos/cacheplane/dawnai/actions/workflows/260503756/runs?per_page=100' \ + > "$PRE_DISPATCH_RUNS" + chmod 0600 "$PRE_DISPATCH_RUNS" + assert_terminal_release_runs "$PRE_DISPATCH_RUNS" + assert_same_release_run_ids "$PRE_RUNS" "$PRE_DISPATCH_RUNS" + + set +e + HTTP_STATUS="$(curl --silent --show-error \ + --output "$DISPATCH_RECEIPT" \ + --write-out '%{http_code}' \ + --request POST \ + --header @- \ + 'https://api.github.com/repos/cacheplane/dawnai/actions/workflows/260503756/dispatches' \ + --data-binary "@$DISPATCH_BODY" < "$DISPATCH_STATUS" + chmod 0600 "$DISPATCH_STATUS" + test "$CURL_EXIT_CODE" -eq 0 + test "$HTTP_STATUS" = 200 + test -f "$DISPATCH_RECEIPT" + chmod 0600 "$DISPATCH_RECEIPT" + shasum -a 256 "$DISPATCH_BODY" "$DISPATCH_RECEIPT" "$DISPATCH_STATUS" > "$DISPATCH_HASHES" + chmod 0600 "$DISPATCH_HASHES" + + DISPATCH_BODY="$DISPATCH_BODY" DISPATCH_RECEIPT="$DISPATCH_RECEIPT" \ + DISPATCH_STATUS="$DISPATCH_STATUS" DISPATCH_HASHES="$DISPATCH_HASHES" \ + GITHUB_TOKEN="$GITHUB_TOKEN" env -u NODE_OPTIONS -u NODE_PATH node -e ' + const { createHash } = require("node:crypto") + const { closeSync, fsyncSync, openSync, readFileSync, statSync } = require("node:fs") + const { basename } = require("node:path") + const paths = [process.env.DISPATCH_BODY, process.env.DISPATCH_RECEIPT, process.env.DISPATCH_STATUS, process.env.DISPATCH_HASHES] + const token = process.env.GITHUB_TOKEN + if (typeof token !== "string" || token.length === 0) throw new Error("Dispatch token is unavailable for leak verification") + const tokenBytes = Buffer.from(token, "utf8") + const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex") + for (const path of paths) { + const stat = statSync(path) + if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o777) !== 0o600) throw new Error(`${path} is not one mode-0600 regular file`) + if (readFileSync(path).includes(tokenBytes)) throw new Error(`${path} contains the GitHub token`) + } + const expectedBody = { ref: "v0.8.22", inputs: { version: "0.8.22", commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", operation: "reconcile" } } + if (JSON.stringify(JSON.parse(readFileSync(process.env.DISPATCH_BODY, "utf8"))) !== JSON.stringify(expectedBody)) throw new Error("Workflow dispatch body is not exact") + if (readFileSync(process.env.DISPATCH_STATUS, "utf8") !== "200\n") throw new Error("Workflow dispatch status is not exact") + const receipt = JSON.parse(readFileSync(process.env.DISPATCH_RECEIPT, "utf8")) + const keys = Object.keys(receipt).sort().join(",") + if (keys !== ["workflow_run_id", "run_url", "html_url"].sort().join(",") || !Number.isSafeInteger(receipt.workflow_run_id) || receipt.workflow_run_id < 1) throw new Error("Workflow dispatch receipt is malformed") + const id = receipt.workflow_run_id + if (receipt.run_url !== `https://api.github.com/repos/cacheplane/dawnai/actions/runs/${id}` || receipt.html_url !== `https://github.com/cacheplane/dawnai/actions/runs/${id}`) throw new Error("Workflow dispatch receipt URLs are not exact") + const hashes = new Map(readFileSync(process.env.DISPATCH_HASHES, "utf8").trim().split("\n").map((line) => { + const match = /^([0-9a-f]{64}) (.+)$/u.exec(line) + if (!match) throw new Error("Dispatch hash record is malformed") + return [basename(match[2]), match[1]] + })) + for (const path of paths.slice(0, 3)) { + if (hashes.get(basename(path)) !== sha256(readFileSync(path))) throw new Error(`${path} hash is not exact`) + } + for (const path of paths) { + const fd = openSync(path, "r") + try { fsyncSync(fd) } finally { closeSync(fd) } + } + ' + sync + DISPATCH_DURABLE=true + trap - EXIT INT TERM HUP + shasum -a 256 "$DISPATCH_BODY" "$DISPATCH_RECEIPT" "$DISPATCH_STATUS" "$DISPATCH_HASHES" +) + +run_v0822_activation_dispatch +``` + +The repository's pinned GitHub writer requires this 2026 API direct HTTP-200 +receipt. Do not use `gh workflow run`, because it does not durably bind the +returned run. The Authorization header above is read by `curl` from standard +input and never appears in its argument vector or a dispatch artifact. Any +transport failure, non-200 status, malformed receipt, chmod/hash/fsync failure, +identity drift, time-window failure, or new run is ambiguous after enablement; +the armed trap disables and verifies before returning failure. Never retry or +list recent runs to infer a dispatch. + +Record the direct dispatch receipt and run identity. Do not cancel, generically +rerun, list recent runs to infer a dispatch, or substitute `main`. Require +serial trusted publication of all 21 packages, +package-level npm integrity and provenance, all five smoke lanes, the +independent audit, and immutable publication of Release `379991871`. Stop at +the first failed transition and preserve its evidence. Only after v0.8.22 is +terminal may Version Packages PR #525 advance the fixed group to the +README-bearing v0.8.23 release; after that release is terminal, remove the +one-time recovery surface in its separately reviewed cleanup pull request. + ## First live patch release The first controller-owned release is a patch release. For this cutover the @@ -435,10 +1431,44 @@ stop, preserve the candidate, and escalate; the live workflow cannot abandon it. ## Live receipt Append only credential-free facts after the live release. Do not mark the -cutover complete while any field is missing. +cutover complete while any field is missing. The recovery-specific rows are +also the operator freeze record: preserve exact observations and use `none` +only when a field was independently proved inapplicable. Do not backfill prior +fence observations for a `preexisting-quarantined` outcome. | Receipt | Value | | --- | --- | +| Recovery PR number / reviewed head SHA / merge SHA | pending | +| Fresh checkout path / reviewed `packageManager` / pnpm version / lockfile-frozen no-lifecycle install / fresh store integrity result | pending | +| Operator freeze record: authenticated operator | pending | +| Operator freeze record: established UTC | pending | +| Operator freeze record: released UTC | pending | +| Operator freeze record: scope Release IDs | pending (`379982100`, `379986168`) | +| Operator freeze record: workflow-disabled observation and non-atomic limitation | pending | +| Append-only recovery attempt ledger: numbered capture/apply paths, SHA-256, UTC, exits, states, cleanup, outcomes | pending | +| Release `379982100` performed `preWriteFence`: `observedAt` / `projectionSha256` / `tagObjectSha` | pending or inapplicable | +| Release `379982100` performed `postWriteFence`: `observedAt` / `projectionSha256` / `tagObjectSha` | pending or inapplicable | +| Release `379982100` preexisting verification: `verifiedAt` / `projectionSha256` / `priorFenceObservations: null` | pending or inapplicable | +| Release `379986168` performed `preWriteFence`: `observedAt` / `projectionSha256` / `tagObjectSha` | pending or inapplicable | +| Release `379986168` performed `postWriteFence`: `observedAt` / `projectionSha256` / `tagObjectSha` | pending or inapplicable | +| Release `379986168` preexisting verification: `verifiedAt` / `projectionSha256` / `priorFenceObservations: null` | pending or inapplicable | +| Operator freeze record: exact per-attempt partial or ambiguous state | pending | +| Release `379982100` original-body archive asset ID/SHA-256 | pending | +| Release `379982100` duplicate recovery receipt asset ID/SHA-256 | pending | +| Release `379986168` original-body archive asset ID/SHA-256 | pending | +| Release `379986168` duplicate recovery receipt asset ID/SHA-256 | pending | +| Post-quarantine body SHA-256 for both duplicates | pending | +| Evidence `capturedAt` used by the successful apply (must equal `receipt.evidenceCapturedAt`) | pending | +| Final authorization receipt path / SHA-256 | pending | +| Final authorization receipt per-duplicate `performed` or `preexisting-quarantined` outcomes | pending | +| Final observer `CANDIDATE_ESCROWED` / `would-transition` / `publish-npm-packages` / Release `379991871` / `conflicts: []` / `diagnostics: []` | pending | +| Release activation UTC / pre-enable, post-enable, and immediately-pre-dispatch run snapshot SHA-256 / exact no-new-run proofs | pending | +| Direct v0.8.22 dispatch HTTP 200 receipt SHA-256 / durable hash record / `workflow_run_id` / `run_url` / `html_url` | pending | +| v0.8.22 Release run/attempt | pending | +| v0.8.22 npm integrity/provenance conclusions | pending | +| v0.8.22 immutable Release ID and re-read | pending (`379991871`) | +| v0.8.23 Version Packages PR / Release run/attempt | pending | +| Recovery cleanup PR / merge SHA | pending | | Atomic switch SHA | pending | | Pre-enable evidence digest/time | pending | | Post-enable evidence digest/time | pending | diff --git a/docs/superpowers/specs/2026-09-01-v0.8.22-duplicate-draft-recovery-design.md b/docs/superpowers/specs/2026-09-01-v0.8.22-duplicate-draft-recovery-design.md new file mode 100644 index 000000000..8a63d5212 --- /dev/null +++ b/docs/superpowers/specs/2026-09-01-v0.8.22-duplicate-draft-recovery-design.md @@ -0,0 +1,400 @@ +# v0.8.22 Duplicate Draft Recovery Design + +## Problem + +The v0.8.22 release candidate is fully escrowed but cannot advance to npm +publication. GitHub currently contains three mutable draft Releases with the +same canonical Dawn `ESCROWED` marker, candidate commit, and 45-asset base: + +| Role | Release ID | Current temporary `tag_name` | +| --- | ---: | --- | +| Canonical candidate | `379991871` | `untagged-be0ff4bee4ba43b521a9` | +| Duplicate evidence draft | `379982100` | `untagged-a13939767dd2419ade01` | +| Duplicate evidence draft | `379986168` | `untagged-20706099efa3c38335a8` | + +All three identify version `0.8.22` and candidate commit +`2a80deece2ff958fe7fde8fddeb4f99bed70a1c8`. Every one of Dawn's 21 +publishable packages remains at `0.8.21` on npm, and exact version `0.8.22` is +absent for all 21 packages. + +The controller deliberately rejects more than one marker-backed draft before +an exact Release read or any mutation. That invariant is correct for the +normal release path, but it leaves this already-created candidate unable to +resume. The candidate tag is immutable and predates the later draft-identity +fixes on `main`, so a general controller change on `main` cannot replace the +workflow definition stored at `v0.8.22`. + +This recovery must remove only the duplicate live controller identities while +preserving the duplicate draft objects, their original bodies, and every +existing asset as auditable incident evidence. + +## Goals + +- Preserve Release `379991871` byte-for-byte as the canonical v0.8.22 draft. +- Preserve both duplicate draft objects and all existing assets. +- Archive each duplicate's original body byte-for-byte before changing it. +- Make the two duplicates no longer parse as live Dawn release candidates. +- Keep normal duplicate detection unchanged and fail-closed. +- Resume the existing exact-tag release workflow only after a fresh observer + proves one canonical `CANDIDATE_ESCROWED` Release. +- Remove the recovery command after v0.8.22 and the queued v0.8.23 release are + complete. + +## Non-goals + +- A permanent Release-ID override or generic duplicate-selection mechanism. +- Deleting a Release, Release asset, tag, Actions artifact, or npm version. +- Moving or replacing `v0.8.22`. +- Publishing packages, modifying npm configuration, or enabling workflows from + the recovery command. +- Weakening marker, asset, provenance, audit, or immutable-release validation. +- Repairing unrelated release-controller or process-runner behavior. + +## Recovery Command + +Add one temporary executable: + +```text +scripts/release/recover-v0.8.22-duplicate-drafts.mjs +``` + +It exposes only two modes: + +```bash +node scripts/release/recover-v0.8.22-duplicate-drafts.mjs capture \ + --reviewed-commit "$RECOVERY_SHA" \ + --output .dawn/release-recovery/v0.8.22-capture-01.json + +node scripts/release/recover-v0.8.22-duplicate-drafts.mjs apply \ + --evidence .dawn/release-recovery/v0.8.22-capture-01.json \ + --acknowledge-non-atomic-release-edit-freeze \ + --output .dawn/release-recovery/v0.8.22-apply-01.json +``` + +`capture` is read-only. `apply` is the only mutating mode. Unknown commands, +arguments, duplicate flags, symlinks, paths outside the repository, malformed +UTF-8, or noncanonical evidence fail before constructing a writer. + +The acknowledgement flag is an exact literal boolean gate; it accepts no +value, alias, environment fallback, or configuration-file substitute. `apply` +records it in the write-once file named by `--output`. Capture and apply paths +must be distinct, unused descendants of the ignored private +`.dawn/release-recovery/` directory. A resumed run uses the next unused +sequence number for both files. + +Terminology is exact throughout this design: a **duplicate recovery receipt +asset** is the canonical JSON asset uploaded to one duplicate GitHub Release; +the **operator freeze record** is the manually maintained live runbook entry; +and the **final authorization receipt** is the local credential-free write-once +file produced by successful `apply`. + +The candidate version, commit, canonical Release ID, and two duplicate Release +IDs are constants in the executable. Callers cannot provide or override them. +`capture` requires one caller-supplied `--reviewed-commit` because a program +cannot contain its own Git commit hash. The command verifies that SHA through +GitHub: it must be the merge commit of exactly one merged pull request targeting +`main`, the pull request's required `CI / validate` check must have succeeded at +the reviewed head, and the merge commit's tree must equal the reviewed head's +tree. Local `HEAD`, remote `refs/heads/main`, and the supplied merge commit must +then be identical. A later `main` commit, an unmerged branch head, or an +operator-invented SHA is not an execution authority. + +The command is not referenced by `.github/workflows/release.yml`, a package +script, or any other release-owner workflow. It is an explicit operator tool, +not a new publication owner. + +## Capture Preconditions + +Capture reuses the existing bounded owner-preflight readers for repository, +remote-main, workflow-state, and Immutable Releases evidence, plus the existing +GitHub and npm production readers for candidate state. It records only +credential-free facts. It requires all of the following: + +1. Repository identity is exactly `cacheplane/dawnai`. +2. Local `HEAD`, remote `refs/heads/main`, and the reviewed recovery commit are + identical. +3. GitHub Immutable Releases remains enabled. +4. `.github/workflows/release.yml` is `disabled_manually`. +5. There are no nonterminal Release workflow runs. +6. Annotated tag `v0.8.22` peels to the exact candidate commit. +7. No GitHub Release is published at tag `v0.8.22`. +8. Exact version `0.8.22` is absent for all 21 canonical npm packages, with + package-level metadata independently confirming each absence. +9. None of the observed candidate workflow runs has started a `publish-npm` + job. +10. Release `379991871` is present, mutable, not a prerelease, and identifies + the exact candidate through its canonical `ESCROWED` marker and original + 45-member base namespace. +11. Each duplicate has its exact observed opaque temporary `tag_name`, never + `v0.8.22`. The exact expected temporary values are candidate-specific + constants. This matters because the normal controller treats an exact tag + match as managed even when the body has no marker. +12. Each duplicate is in exactly one recognized state: untouched, + body-archived, receipt-archived, or quarantined, as defined below. Its + original 45-member base namespace must remain exact in every state. +13. For an untouched duplicate, its `ESCROWED` marker and original body must + equal the canonical draft. For a body-archived duplicate, those facts plus + the exact original-body asset are required. For a receipt-archived + duplicate, those facts plus both exact evidence assets are required. For a + quarantined duplicate, the exact recovery notice and both evidence assets + are required, and the archived original body must equal the canonical + draft. +14. No fourth marker-backed or exact-tag Release identifies the candidate. + +Any unavailable, ambiguous, malformed, or conflicting observation blocks +capture. The evidence file is created write-once with mode `0600` inside an +ignored private directory. + +## Evidence Model + +The canonical JSON evidence binds: + +- schema version and capture timestamp; +- repository ID, recovery commit, candidate version, and candidate commit; +- the unique merged recovery pull request, reviewed head, successful required + check, merge commit, and equal reviewed/merged tree identity; +- annotated tag object and peeled commit; +- Immutable Releases and Release workflow states; +- complete nonterminal-run and npm-absence observations; +- all three exact Release snapshots; +- SHA-256 of each original body; +- the ordered 45-asset inventory and its canonical SHA-256; +- canonical and duplicate Release roles; and +- the expected archive asset names and post-recovery notice bytes. + +Evidence is valid for at most 15 minutes. `apply` reparses canonical bytes and +recomputes every derived field. Before every individual mutation, it re-reads +the target Release, its complete assets, the canonical Release, workflow/run +state, tag identity, and npm absence, then requires the exact state recorded in +the evidence. Production drift never triggers inference inside `apply`; it +stops and requires a new explicit `capture` invocation. + +`capture` remains available after a partial run. It recognizes body-archived, +receipt-archived, and quarantined duplicates from their exact +candidate-specific evidence, validates the archived original body against the +canonical draft, and issues fresh evidence for only the remaining transitions. +Evidence expiry can therefore never require restoration of a live marker or +strand an exact partial recovery. + +## Duplicate Quarantine + +For each duplicate, in ascending Release-ID order, `apply` performs one +idempotent sequence: + +1. Immediately before the archive upload, re-read all live authorization facts + and require the exact untouched state from the evidence. +2. Upload an asset containing the original body bytes. The asset name includes + the duplicate Release ID and original-body SHA-256. +3. Re-read all authorization facts and the exact archived-body asset before the + receipt upload. +4. Upload a canonical JSON recovery receipt containing the candidate identity, + canonical and duplicate Release IDs, recovery commit, original-body digest, + base-asset inventory digest, and archive asset identity. +5. Re-read all authorization facts and both evidence assets before the body + update. +6. Replace only the duplicate's live body through a narrow recovery writer. + That writer requires the full expected Release snapshot, exact asset + namespace, and expected body digest in a final pre-write fence immediately + before PATCH, and sends only the new `body` field. It does not reuse the + general writer's title-and-body update or rewrite other metadata. +7. Re-read the Release and require the exact recovery notice, opaque temporary + tag name, mutable draft + state, unchanged metadata, and unchanged original 45 assets. + +The recovery notice is intentionally not a valid Dawn release marker. It names +Release `379991871` as canonical and links its own archive and receipt asset +names and digests. It does not claim that the duplicate was published, +abandoned, or deleted. + +No existing asset is overwritten. An already-present archive or receipt asset +is accepted only when its downloaded bytes are identical. The allowed recovery +asset names and byte limits are exact and candidate-specific. + +### Body-update concurrency guarantee + +GitHub's documented REST API does not provide conditional `PATCH` for the +Release update endpoint. GitHub states that conditional requests for unsafe +methods are unsupported unless an endpoint explicitly documents them, and the +Release endpoint does not. Its live draft Release responses also use weak +ETags, which cannot provide a standards-compliant `If-Match` compare-and-swap. +The recovery command therefore must not claim atomic body compare-and-swap. + +The accepted guarantee is an explicit, fail-closed compare-before-write fence: + +1. The Release workflow remains disabled and no Release workflow run may be + nonterminal. +2. The operator establishes a temporary edit freeze: no human, bot, or other + process may edit Releases `379982100` or `379986168` from fresh evidence + capture until `apply` finishes or stops. +3. Immediately before PATCH, the writer completes a final concurrent read of + the annotated candidate tag and the duplicate's complete Release/body/asset + state. Both must still equal the fresh evidence and expected partial state. +4. The writer sends exactly one body-only PATCH. +5. Its first post-write reads revalidate the annotated tag and then the complete + Release/body/asset state. The exact recovery notice and unchanged metadata + and assets are required. +6. Any unavailable, ambiguous, malformed, or drifting pre/post observation + stops the command. An ambiguous write is never retried blindly; the operator + captures fresh evidence and resumes only from one of the four recognized + states. + +There is an unavoidable time-of-check/time-of-use interval between the final +pre-write read and GitHub applying the PATCH. The temporary edit freeze is the +operational control for that interval. This limitation is recorded in the live +receipt and runbook rather than hidden behind an unsupported conditional +header. See GitHub's +[REST API best practices](https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api#use-conditional-requests-if-appropriate). + +The comparison uses an explicit normalized projection rather than literal +equality over GitHub's entire response. Before and after PATCH it requires the +same Release ID, opaque `tag_name`, title, `target_commitish`, draft/prerelease/ +immutable flags, and ordered asset IDs/names/digests/sizes. The only permitted +application-owned delta is `body`: the exact canonical marker body becomes the +exact recovery notice. GitHub-managed representation fields such as +`updated_at`, API URLs, author data, and transport headers are validated for +safe shape where consumed but are excluded from equality because GitHub may +change them as a consequence of the body update. No other Release metadata or +asset delta is permitted. + +The edit freeze is explicit rather than implied. The runbook records the +authenticated operator, freeze establishment time, the two Release IDs, the +evidence capture time, each pre/post fence time and outcome, and the freeze +release time. `apply` requires an exact acknowledgement of the non-atomic +Release edit freeze before constructing the writer. The final authorization +receipt records that acknowledgement, `atomic: false`, the freeze scope, and a +discriminated result for each duplicate Release: + +- `performed` means this successful `apply` invocation sent the body PATCH and + records that invocation's exact pre-write and post-write fence observations; +- `preexisting-quarantined` means fresh capture found the exact quarantined + state and this invocation performed no mutation. It records the current exact + verification, sets prior fence observations to `null`, and makes no claim + about an earlier invocation's unavailable machine history. + +On failure, the operator freeze record records the exact observed partial or +ambiguous state before the freeze is released. A later successful `apply` may +therefore issue an honest final authorization receipt without accepting or +inventing a prior failure receipt. + +## Idempotence and Partial Failure + +Both `capture` and `apply` recognize four states for each duplicate: + +- **untouched**: exact original body and 45 original assets; +- **body-archived**: exact original body plus the exact original-body archive + asset and no recovery-receipt asset; +- **receipt-archived**: exact original body plus exact archive and receipt + assets; +- **quarantined**: exact recovery notice plus the original 45 assets and exact + archive and receipt assets. + +Anything else is a conflict. A rerun resumes only the missing transition for an +exact recognized state, using newly captured evidence when the prior receipt +has expired. If the first duplicate is quarantined and the second fails, the +normal release controller still sees two live candidate markers and remains +blocked. No partial recovery can authorize npm publication. + +The command has no automatic rollback. Reintroducing an archived controller +body could recreate ambiguity. The archived original bytes make a separately +reviewed restoration possible, but restoration is outside this design. + +## Final Authorization Check + +After both duplicates are quarantined, `apply` constructs a new read-only +production observation through the normal controller boundary. Success +requires: + +- Release `379991871` is the only managed Release for v0.8.22; +- it remains an exact mutable `ESCROWED` draft with the original 45 assets; +- npm `0.8.22` remains absent for every package; +- the classified state is `CANDIDATE_ESCROWED`; +- the disposition is `would-transition`; +- the next transition is `publish-npm-packages`; and +- diagnostics and conflicts are empty. + +The final report is the final authorization receipt. It includes the non-atomic +freeze acknowledgement and the per-duplicate `performed` or +`preexisting-quarantined` records described above. The recovery command does +not enable or dispatch the Release workflow. + +## Testing + +Tests must cover: + +- exact CLI argument and contained-file handling; +- authenticated reviewed-commit and merged-PR anchoring, including rejection + of later `main`, mismatched trees, and unsuccessful required checks; +- canonical evidence encoding, expiry, no-clobber writes, and symlink refusal; +- `capture` exposing no writer and performing zero mutations; +- every identity, marker, body, asset, workflow, run, tag, and npm mismatch + blocking before mutation; +- rejection when any candidate `publish-npm` job has started; +- exact archive and receipt bytes; +- exact opaque temporary tag names and rejection of `v0.8.22` on either + duplicate; +- no deletion or replacement of an existing asset; +- a fresh full authorization snapshot before every upload and body update; +- the recovery-only body writer never sending or rewriting title or other + metadata; +- final pre-write body/tag/asset comparison, body-only PATCH, immediate + post-write verification, and rejection of any observable concurrent drift; +- explicit operator edit-freeze acknowledgement in the live runbook and no + unsupported claim of atomic compare-and-swap; +- exact normalized pre/post Release projections, including the sole permitted + body delta and excluded GitHub-managed representation fields; +- no automatic PATCH retry after a timeout, connection failure, retryable HTTP + status, malformed response, or ambiguous outcome; +- final authorization receipt discrimination between a mutation performed by + the successful invocation and a freshly verified preexisting quarantine, + including `null` rather than invented prior fence observations; +- successful quarantine of both configured duplicates only; +- safe resume from each partial state; +- fresh evidence capture from body-archived, receipt-archived, and quarantined + partial states after the previous evidence expires; +- byte-identical replay becoming a no-op; +- rejection of a fourth candidate draft; +- final observation requiring exactly `CANDIDATE_ESCROWED` and + `publish-npm-packages`; and +- unchanged normal-controller duplicate rejection. + +Focused tests run before the complete release-controller suite. The repository +Definition of Done remains required before merge. The existing host-timing +failure in `process-runner.test.mjs` is tracked as a pre-existing baseline +failure only if it reproduces unchanged on the exact `main` SHA; it is not +silently waived for modified release code. + +## Production Sequence + +1. Merge the reviewed recovery PR while Release remains disabled. +2. Synchronize an isolated checkout to the exact merged recovery commit. +3. Establish the temporary edit freeze for Releases `379982100` and + `379986168`; record the authenticated operator, time, and scope in the live + runbook receipt. +4. Capture fresh evidence under the freeze and inspect the credential-free + report. +5. Run `apply` once against that evidence with the exact non-atomic freeze + acknowledgement. +6. Independently verify the two duplicate drafts, their evidence assets, and + the one remaining controller-visible candidate. +7. Record the successful or exact recognized partial outcome and release the + edit freeze. An ambiguous outcome requires fresh read-only capture and a + recorded recognized state before release; it is never retried blindly. +8. Enable Release and dispatch `.github/workflows/release.yml` at `v0.8.22` + with the exact version, candidate commit, and `operation=reconcile`. +9. Require all 21 npm publications, provenance checks, five smoke lanes, + independent audit, and final immutable GitHub Release to pass. +10. Approve and merge Version Packages PR #525 only after v0.8.22 is terminal. +11. Verify the fixed-group v0.8.23 publication and updated npm READMEs. +12. Remove the recovery executable, its tests, and candidate-specific asset + allowlist in a cleanup PR. Preserve the production receipts and duplicate + evidence drafts. + +## Success Criteria + +- No Release, tag, existing asset, npm version, or Actions evidence is deleted. +- Only Releases `379982100` and `379986168` receive mutations. +- Release `379991871` remains the sole live v0.8.22 controller candidate. +- The normal exact-tag workflow publishes v0.8.22 without a generic identity + override. +- PR #525 subsequently publishes the README-bearing v0.8.23 fixed group. +- The one-time recovery surface is removed after use. diff --git a/scripts/release/duplicate-draft-recovery-adapters.mjs b/scripts/release/duplicate-draft-recovery-adapters.mjs new file mode 100644 index 000000000..31bd2736e --- /dev/null +++ b/scripts/release/duplicate-draft-recovery-adapters.mjs @@ -0,0 +1,2569 @@ +import { createHash } from "node:crypto" + +import { snapshotJson } from "./adapter-normalize.mjs" +import { createGitReader } from "./adapters/git.mjs" +import { createGitHubReader } from "./adapters/github.mjs" +import { + createHttpGet, + DEFAULT_HTTP_MAX_RESPONSE_BYTES, + DEFAULT_HTTP_TIMEOUT_MS, +} from "./adapters/http.mjs" +import { createNpmReader } from "./adapters/npm.mjs" +import { + canonicalRecoveryNotice, + canonicalRecoveryReceipt, + DUPLICATE_DRAFT_RECOVERY_POLICY, + duplicateDraftReleaseProjectionSha256, + MAX_ARCHIVE_ASSET_BYTES, + normalizeDuplicateDraftReleaseProjection, + originalBodyAssetName, + recoveryReceiptAssetName, +} from "./duplicate-draft-recovery.mjs" +import { parseReleaseMarker } from "./metadata.mjs" + +const OWNER = "cacheplane" +const REPOSITORY = "dawnai" +const REPOSITORY_ID = "1210070282" +const API_ORIGIN = "https://api.github.com" +const UPLOAD_ORIGIN = "https://uploads.github.com" +const API_VERSION = "2022-11-28" +const RELEASE_WORKFLOW_ID = 260503756 +const RELEASE_WORKFLOW_PATH = ".github/workflows/release.yml" +const CANDIDATE_TAG = `v${DUPLICATE_DRAFT_RECOVERY_POLICY.version}` +const CANDIDATE_RELEASE_IDS = new Set([ + DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId, + ...DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates.map(({ releaseId }) => releaseId), +]) +const SHA_PATTERN = /^[0-9a-f]{40}$/u +const MAX_PAGES = 100 +const MAX_RECORDS = 10_000 +const RECOVERY_ASSET_BYTES = MAX_ARCHIVE_ASSET_BYTES +const WRITER_MAX_TIMEOUT_MS = 300_000 +const WRITER_MAX_RESPONSE_BYTES = 4 * 1024 * 1024 +const WRITER_MAX_RESPONSE_CHUNKS = 1_024 +const WRITER_MAX_RESPONSE_HEADERS = 128 +const WRITER_MAX_RESPONSE_HEADER_BYTES = 64 * 1024 +const WRITER_MAX_LOCATION_DECODE_PASSES = 6 +const WRITER_SIGNED_DOWNLOAD_HOSTS = new Set([ + "objects.githubusercontent.com", + "release-assets.githubusercontent.com", + "github-releases.githubusercontent.com", + "pipelines.actions.githubusercontent.com", +]) +const WRITER_SIGNED_AZURE_HOST_PATTERN = /^productionresultssa[0-9]+\.blob\.core\.windows\.net$/u +const WRITER_RELEASE_ASSET_PATH_PATTERN = + /^\/repos\/cacheplane\/dawnai\/releases\/assets\/[1-9][0-9]*$/u +const WRITER_TITLE = `Dawn v${DUPLICATE_DRAFT_RECOVERY_POLICY.version}` +const DUPLICATE_TAG_BY_ID = new Map( + DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates.map(({ releaseId, tagName }) => [releaseId, tagName]), +) +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&") +} +const DUPLICATE_RELEASE_ID_ALTERNATION = DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates + .map(({ releaseId }) => String(releaseId)) + .join("|") +const WRITER_PATCH_URL_PATTERN = new RegExp( + `^${escapeRegExp(`${API_ORIGIN}/repos/${OWNER}/${REPOSITORY}/releases/`)}(?:${DUPLICATE_RELEASE_ID_ALTERNATION})$`, + "u", +) +const WRITER_UPLOAD_URL_PATTERN = new RegExp( + `^${escapeRegExp(`${UPLOAD_ORIGIN}/repos/${OWNER}/${REPOSITORY}/releases/`)}(?:${DUPLICATE_RELEASE_ID_ALTERNATION})/assets\\?name=[A-Za-z0-9._~%@+-]{1,512}$`, + "u", +) +const SHA256_PATTERN = /^[0-9a-f]{64}$/u +const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(Uint8Array.prototype) +const TYPED_ARRAY_BYTE_LENGTH = Object.getOwnPropertyDescriptor( + TYPED_ARRAY_PROTOTYPE, + "byteLength", +).get +const NONTERMINAL_STATUSES = new Set(["requested", "waiting", "pending", "queued", "in_progress"]) +const RUN_STATUSES = new Set([...NONTERMINAL_STATUSES, "completed"]) +const JOB_STATUSES = new Set(["waiting", "pending", "queued", "in_progress", "completed"]) +const TERMINAL_CONCLUSIONS = new Set([ + "success", + "failure", + "neutral", + "cancelled", + "skipped", + "timed_out", + "action_required", + "stale", + "startup_failure", +]) +const TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/u +const ASSET_NAME_PATTERN = /^(?!\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._+-]{0,254}$/u +const UNSAFE_REMOTE_KEYS = new Set(["__proto__", "constructor", "prototype"]) + +export class DuplicateDraftRecoveryReadError extends Error { + constructor(code, message) { + super(message) + this.name = "DuplicateDraftRecoveryReadError" + this.code = code + } +} + +/** Build the immutable, read-only production boundary used by recovery capture. */ +export function createDuplicateDraftRecoveryReader({ + root, + token, + fetchImpl = fetch, + run, + timeoutMs, + maxResponseBytes = DEFAULT_HTTP_MAX_RESPONSE_BYTES, + now = Date.now, +} = {}) { + const git = createGitReader({ + root, + ...(run === undefined ? {} : { run }), + ...(timeoutMs === undefined ? {} : { timeoutMs }), + }) + const github = createGitHubReader({ + owner: OWNER, + repo: REPOSITORY, + repositoryId: REPOSITORY_ID, + ...(token === undefined ? {} : { token }), + fetchImpl, + ...(timeoutMs === undefined ? {} : { timeoutMs }), + maxResponseBytes, + maxPages: MAX_PAGES, + maxRecords: MAX_RECORDS, + now, + }) + const npm = createNpmReader({ + fetchImpl, + ...(timeoutMs === undefined ? {} : { timeoutMs }), + maxResponseBytes, + }) + const http = createHttpGet({ + fetchImpl, + ...(timeoutMs === undefined ? {} : { timeoutMs }), + maxResponseBytes, + }) + const context = { + git, + github, + npm, + http, + token: token ?? null, + timeoutMs: timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS, + maxResponseBytes, + now, + } + + return Object.freeze({ + async readReviewedMergeAuthority(reviewedCommit) { + assertSha(reviewedCommit, "reviewed commit") + const [localHistory, repository, pullRequests, mergeCommit] = await Promise.all([ + readBoundary("LOCAL_HEAD_UNAVAILABLE", () => + git.listFirstParentHistory({ ref: "HEAD", maxCount: 1 }), + ), + readRepositoryState(context), + readExactJson(context, { + path: `/repos/${OWNER}/${REPOSITORY}/commits/${reviewedCommit}/pulls?per_page=2`, + operation: "reviewed-associated-pull-request", + accept: "application/vnd.github+json", + }), + readExactJson(context, { + path: `/repos/${OWNER}/${REPOSITORY}/git/commits/${reviewedCommit}`, + operation: "reviewed-merge-commit", + }), + ]) + if (localHistory.length !== 1 || localHistory[0] !== reviewedCommit) { + fail("REVIEWED_COMMIT_NOT_LOCAL_HEAD", "Reviewed recovery commit is not local HEAD") + } + if (repository.mainSha !== reviewedCommit) { + fail("REVIEWED_COMMIT_NOT_REMOTE_MAIN", "Reviewed recovery commit is not remote main") + } + if (!Array.isArray(pullRequests) || pullRequests.length !== 1) { + fail( + "REVIEWED_PULL_REQUEST_AMBIGUOUS", + "Reviewed recovery commit must have exactly one associated pull request", + ) + } + const pullRequest = normalizeReviewedPullRequest(pullRequests[0], reviewedCommit) + const [headCommit, ci] = await Promise.all([ + readExactJson(context, { + path: `/repos/${OWNER}/${REPOSITORY}/git/commits/${pullRequest.reviewedHeadSha}`, + operation: "reviewed-head-commit", + }), + readRequiredCi(context, pullRequest.reviewedHeadSha), + ]) + const mergeTreeSha = commitTreeSha(mergeCommit, reviewedCommit, "merge") + const reviewedTreeSha = commitTreeSha( + headCommit, + pullRequest.reviewedHeadSha, + "pull request head", + ) + if (mergeTreeSha !== reviewedTreeSha) { + fail("REVIEWED_TREE_MISMATCH", "Reviewed and merged recovery trees are not identical") + } + return deepFreeze({ + mergeCommitSha: reviewedCommit, + mergeTreeSha, + pullRequestNumber: pullRequest.pullRequestNumber, + reviewedHeadSha: pullRequest.reviewedHeadSha, + reviewedTreeSha, + validateRunId: ci.validateRunId, + }) + }, + + readRepositoryState() { + return readRepositoryState(context) + }, + + async readCandidateTag() { + const ref = requirePresent( + await github.getRef({ ref: `tags/${CANDIDATE_TAG}` }), + "CANDIDATE_TAG_UNAVAILABLE", + "Candidate tag could not be verified", + ) + if ( + !isObject(ref) || + ref.ref !== `refs/tags/${CANDIDATE_TAG}` || + !isObject(ref.object) || + ref.object.type !== "tag" || + !isSha(ref.object.sha) + ) { + fail("CANDIDATE_TAG_MALFORMED", "Candidate tag evidence is malformed") + } + const tag = requirePresent( + await github.getGitTag({ tagSha: ref.object.sha }), + "CANDIDATE_TAG_UNAVAILABLE", + "Candidate tag could not be verified", + ) + if ( + !isObject(tag) || + tag.sha !== ref.object.sha || + tag.tag !== CANDIDATE_TAG || + !isObject(tag.object) || + tag.object.type !== "commit" || + tag.object.sha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha + ) { + fail("CANDIDATE_TAG_CONFLICT", "Candidate tag identity is not exact") + } + return deepFreeze({ + version: DUPLICATE_DRAFT_RECOVERY_POLICY.version, + commitSha: DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha, + tagObjectSha: ref.object.sha, + }) + }, + + async readWorkflowState() { + const value = await readExactJson(context, { + path: `/repos/${OWNER}/${REPOSITORY}/actions/workflows/${RELEASE_WORKFLOW_ID}`, + operation: "release-workflow", + }) + if ( + !isObject(value) || + value.id !== RELEASE_WORKFLOW_ID || + value.path !== RELEASE_WORKFLOW_PATH || + value.state !== "disabled_manually" + ) { + fail("RELEASE_WORKFLOW_CONFLICT", "Release workflow state is not exact") + } + return deepFreeze({ id: RELEASE_WORKFLOW_ID, state: "disabled_manually" }) + }, + + async readImmutableReleases() { + const value = await readExactJson(context, { + path: `/repos/${OWNER}/${REPOSITORY}/immutable-releases`, + operation: "immutable-releases", + }) + if (!isObject(value) || value.enabled !== true) { + fail("IMMUTABLE_RELEASES_DISABLED", "Immutable Releases is not enabled") + } + return deepFreeze({ enabled: true }) + }, + + async readReleaseRuns() { + const rawRuns = await readStrictPages(context, { + path: `/repos/${OWNER}/${REPOSITORY}/actions/workflows/${RELEASE_WORKFLOW_ID}/runs?per_page=100`, + operation: "RELEASE_RUNS", + field: "workflow_runs", + requireTotalCount: true, + }) + const runs = normalizeReleaseRuns(rawRuns) + const candidateRuns = runs.filter( + (run) => run.headSha === DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha, + ) + return deepFreeze({ runs, candidateRuns }) + }, + + async readCandidatePublishJobs(runId, runAttempt) { + assertPositiveInteger(runId, "candidate workflow run ID") + assertPositiveInteger(runAttempt, "candidate workflow run attempt") + const jobs = await readStrictPages(context, { + path: `/repos/${OWNER}/${REPOSITORY}/actions/runs/${runId}/jobs?filter=all&per_page=100`, + operation: "CANDIDATE_JOBS", + field: "jobs", + requireTotalCount: true, + }) + return deepFreeze(normalizeCandidateJobs(jobs, runId, runAttempt)) + }, + + async readNpmAbsence(name) { + const result = await npm.observePackageVersion({ + name, + version: DUPLICATE_DRAFT_RECOVERY_POLICY.version, + }) + if ( + result?.status !== "ABSENT" || + result.operation !== "package-version" || + result.httpStatus !== 404 || + result.code !== "E404" + ) { + fail("NPM_VERSION_NOT_ABSENT", "Exact npm package version absence could not be verified") + } + return deepFreeze({ + name, + version: DUPLICATE_DRAFT_RECOVERY_POLICY.version, + status: "absent", + }) + }, + + async readReleaseSnapshot(releaseId, { expectedOriginalBody } = {}) { + assertPositiveInteger(releaseId, "Release ID") + if (expectedOriginalBody !== undefined && typeof expectedOriginalBody !== "string") { + throw new TypeError("Expected original Release body is invalid") + } + const release = requirePresent( + await github.getRelease({ releaseId }), + "RELEASE_UNAVAILABLE", + "Release snapshot could not be verified", + ) + const rawAssets = await readStrictPages(context, { + path: `/repos/${OWNER}/${REPOSITORY}/releases/${releaseId}/assets?per_page=100`, + operation: "RELEASE_ASSETS", + }) + return normalizeReleaseSnapshot({ + release, + rawAssets, + releaseId, + expectedOriginalBody, + github, + token: context.token, + }) + }, + + async listCandidateReleases() { + const releases = await readStrictPages(context, { + path: `/repos/${OWNER}/${REPOSITORY}/releases?per_page=100`, + operation: "RELEASE_LIST", + }) + const candidates = [] + const releaseIds = new Set() + for (const raw of releases) { + const release = normalizeReleaseRow(raw) + if (releaseIds.has(release.id)) { + fail("PAGINATION_DRIFT", "Release inventory contains a repeated ID") + } + releaseIds.add(release.id) + const marker = releaseMarker(release.body) + if ( + marker === null && + typeof release.body === "string" && + release.body.includes("DAWN_RELEASE_CONTROLLER_MARKER") + ) { + fail("RELEASE_LIST_MALFORMED", "Release inventory contains a malformed Dawn marker") + } + const identifiesCandidate = + CANDIDATE_RELEASE_IDS.has(release.id) || + release.tagName === CANDIDATE_TAG || + (marker !== null && marker.tag === CANDIDATE_TAG) + if (identifiesCandidate) { + if (release.title !== WRITER_TITLE) { + fail("RELEASE_LIST_MALFORMED", "Candidate Release title is not exact") + } + candidates.push({ + releaseId: release.id, + tagName: release.tagName, + title: release.title, + draft: release.draft, + prerelease: release.prerelease, + immutable: release.immutable, + targetCommitish: release.targetCommitish, + marker, + }) + } + } + return deepFreeze(candidates.sort((left, right) => left.releaseId - right.releaseId)) + }, + }) +} + +/** Build the immutable, candidate-specific production mutation boundary. */ +export function createDuplicateDraftRecoveryWriter(options = {}) { + const config = snapshotWriterOptions(options) + const token = config.token + const fetchImpl = config.fetchImpl ?? fetch + const timeoutMs = config.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS + const maxResponseBytes = config.maxResponseBytes ?? WRITER_MAX_RESPONSE_BYTES + const observedNow = config.now ?? Date.now + if ( + typeof token !== "string" || + token.length === 0 || + token.length > 4096 || + hasUnsafeTokenCharacters(token) + ) { + throw new TypeError("Invalid GitHub token") + } + if (typeof fetchImpl !== "function") throw new TypeError("Recovery writer fetch is invalid") + if (typeof observedNow !== "function") throw new TypeError("Recovery writer clock is invalid") + assertBoundedInteger(timeoutMs, 1, WRITER_MAX_TIMEOUT_MS, "Recovery writer timeout") + assertBoundedInteger( + maxResponseBytes, + 1, + WRITER_MAX_RESPONSE_BYTES, + "Recovery writer response limit", + ) + const strictFetchImpl = createStrictCredentialFetch(fetchImpl, token, maxResponseBytes, timeoutMs) + const github = createGitHubReader({ + owner: OWNER, + repo: REPOSITORY, + repositoryId: REPOSITORY_ID, + fetchImpl: strictFetchImpl, + timeoutMs, + maxResponseBytes, + now: Date.now, + maxPages: MAX_PAGES, + maxRecords: MAX_RECORDS, + }) + const http = createHttpGet({ fetchImpl: strictFetchImpl, timeoutMs, maxResponseBytes }) + const context = Object.freeze({ + token, + github, + http, + fetchImpl: strictFetchImpl, + timeoutMs, + maxResponseBytes, + now: Date.now, + observedNow, + strictCredentials: true, + }) + + return Object.freeze({ + async uploadEvidenceAssetIfAbsentAndEqual(input) { + const args = snapshotRecoveryAssetInput(input, token) + const expected = normalizeExpectedWriterSnapshot(args.expectedSnapshot) + const releaseId = expected.releaseId + const kind = validateEvidenceUpload(expected, args) + const current = await readExpectedWriterSnapshot(context, expected, expected.body) + const existing = current.assets.find((asset) => asset.name === args.name) ?? null + if (existing !== null) { + assertExistingEvidenceAsset(existing, args, kind) + await verifyRecoveryCandidateTag(context, args.expectedTagObjectSha) + return deepFreeze({ + releaseId, + assetId: existing.id, + name: args.name, + status: "existing", + sha256: args.sha256, + }) + } + + await verifyRecoveryCandidateTag(context, args.expectedTagObjectSha) + const observation = await observeIssuedRecoveryMutation( + context, + () => + requestRecoveryJson(context, { + url: `${UPLOAD_ORIGIN}/repos/${OWNER}/${REPOSITORY}/releases/${releaseId}/assets?name=${encodeURIComponent(args.name)}`, + method: "POST", + bytes: args.bytes, + contentType: "application/octet-stream", + maximumRequestBytes: RECOVERY_ASSET_BYTES, + }), + { + releaseId, + originalBody: current.body, + expectedTagObjectSha: args.expectedTagObjectSha, + }, + ) + const { response, snapshot: postSnapshot } = requireExactMutationObservation(observation) + let created + try { + if (response.httpStatus !== 201) { + throw new TypeError("Unexpected upload status") + } + created = normalizeUploadResponse(response.body, args) + } catch { + writeFail("MUTATION_OUTCOME_AMBIGUOUS", "GitHub recovery mutation outcome is ambiguous") + } + const appendedAsset = { + id: created.id, + name: args.name, + sha256: args.sha256, + size: args.bytes.byteLength, + ...(kind === "receipt" ? { bytes: args.bytes.toString("utf8") } : {}), + } + const expectedAfter = { + ...current, + assets: [...current.assets, appendedAsset], + evidenceAssets: [...current.evidenceAssets, kind], + } + assertExactObservedMutationState(postSnapshot, expectedAfter) + return deepFreeze({ + releaseId, + assetId: created.id, + name: args.name, + status: "uploaded", + sha256: args.sha256, + }) + }, + + async quarantineDuplicateBodyIfCurrent(input) { + const args = snapshotExactWriterInput( + input, + ["expectedSnapshot", "expectedTagObjectSha", "expectedBodySha256", "expectedNotice"], + "quarantine", + ) + const expected = normalizeExpectedWriterSnapshot(args.expectedSnapshot) + assertExpectedTagObjectSha(args.expectedTagObjectSha) + if (Buffer.from(args.expectedNotice, "utf8").includes(Buffer.from(token, "utf8"))) { + throw new TypeError("Recovery notice contains configured credentials") + } + validateQuarantineInput(expected, args) + const baseline = await readExpectedWriterObservation(context, expected, expected.body) + const preWrite = await readQuarantinePreWriteFence( + context, + expected, + expected.body, + baseline.projection, + args.expectedTagObjectSha, + ) + const current = preWrite.snapshot + const observation = await observeIssuedRecoveryMutation( + context, + () => + requestRecoveryJson(context, { + url: `${API_ORIGIN}/repos/${OWNER}/${REPOSITORY}/releases/${current.releaseId}`, + method: "PATCH", + body: { body: args.expectedNotice }, + contentType: "application/json", + maximumRequestBytes: 16 * 1024, + }), + { + releaseId: current.releaseId, + originalBody: current.body, + expectedTagObjectSha: args.expectedTagObjectSha, + recordFence: true, + }, + ) + const { + response, + snapshot: postSnapshot, + projection: postProjection, + tagObjectSha: postTagObjectSha, + observedAt: postObservedAt, + } = requireExactMutationObservation(observation) + try { + if (response.httpStatus !== 200) throw new TypeError("Unexpected quarantine status") + normalizePatchResponse(response.body, current, args.expectedNotice) + } catch { + writeFail("MUTATION_OUTCOME_AMBIGUOUS", "GitHub recovery mutation outcome is ambiguous") + } + const expectedAfter = { + ...current, + body: args.expectedNotice, + marker: null, + } + assertExactObservedMutationState(postSnapshot, expectedAfter) + assertExactObservedMutationState(postProjection, { + ...preWrite.projection, + body: args.expectedNotice, + }) + return deepFreeze({ + atomic: false, + releaseId: current.releaseId, + outcome: "performed", + preWriteFence: preWrite.fence, + postWriteFence: canonicalWriterFence(postObservedAt, postProjection, postTagObjectSha), + }) + }, + }) +} + +function snapshotWriterOptions(value) { + if (!isPlainObject(value)) throw new TypeError("Recovery writer options schema is invalid") + const allowed = new Set(["token", "fetchImpl", "timeoutMs", "maxResponseBytes", "now"]) + const result = {} + for (const key of Reflect.ownKeys(value)) { + if (typeof key !== "string" || !allowed.has(key)) { + throw new TypeError("Recovery writer options schema is invalid") + } + const descriptor = Object.getOwnPropertyDescriptor(value, key) + if (!isEnumerableData(descriptor)) { + throw new TypeError("Recovery writer options contain an accessor") + } + result[key] = descriptor.value + } + return result +} + +function snapshotRecoveryAssetInput(value, token) { + if (!isPlainObject(value)) throw new TypeError("Recovery asset input schema is invalid") + const expectedFields = ["expectedSnapshot", "expectedTagObjectSha", "name", "bytes", "sha256"] + assertExactDataFields(value, expectedFields, "Recovery asset input") + const expectedTagObjectSha = Object.getOwnPropertyDescriptor(value, "expectedTagObjectSha").value + assertExpectedTagObjectSha(expectedTagObjectSha) + const copied = snapshotExactEvidenceBytes(Object.getOwnPropertyDescriptor(value, "bytes").value) + if (copied.includes(Buffer.from(token, "utf8"))) { + throw new TypeError("Recovery evidence bytes contain configured credentials") + } + return { + expectedSnapshot: snapshotJson( + Object.getOwnPropertyDescriptor(value, "expectedSnapshot").value, + ), + expectedTagObjectSha, + name: Object.getOwnPropertyDescriptor(value, "name").value, + bytes: copied, + sha256: Object.getOwnPropertyDescriptor(value, "sha256").value, + } +} + +function snapshotExactEvidenceBytes(value) { + let prototype + let byteLength + try { + byteLength = TYPED_ARRAY_BYTE_LENGTH.call(value) + prototype = Object.getPrototypeOf(value) + if (prototype !== Buffer.prototype && prototype !== Uint8Array.prototype) { + throw new TypeError("Unexpected byte container prototype") + } + } catch { + throw new TypeError("Recovery evidence bytes are invalid") + } + if (!Number.isSafeInteger(byteLength) || byteLength < 1 || byteLength > RECOVERY_ASSET_BYTES) { + throw new TypeError("Recovery evidence bytes are invalid") + } + let keys + try { + keys = Reflect.ownKeys(value) + } catch { + throw new TypeError("Recovery evidence bytes are invalid") + } + if (keys.length !== byteLength) throw new TypeError("Recovery evidence bytes are invalid") + for (let index = 0; index < byteLength; index += 1) { + const key = keys[index] + const descriptor = typeof key === "string" ? Object.getOwnPropertyDescriptor(value, key) : null + if (key !== String(index) || !isEnumerableData(descriptor)) { + throw new TypeError("Recovery evidence bytes are invalid") + } + } + try { + return Buffer.from(Uint8Array.prototype.slice.call(value)) + } catch { + throw new TypeError("Recovery evidence bytes are invalid") + } +} + +function snapshotExactWriterInput(value, fields, label) { + if (!isPlainObject(value)) throw new TypeError(`${label} input schema is invalid`) + assertExactDataFields(value, fields, `${label} input`) + const source = {} + for (const field of fields) source[field] = Object.getOwnPropertyDescriptor(value, field).value + try { + return deepFreeze(snapshotJson(source)) + } catch { + throw new TypeError(`${label} input schema is invalid`) + } +} + +function assertExactDataFields(value, fields, label) { + const keys = Reflect.ownKeys(value) + if ( + keys.length !== fields.length || + keys.some((key) => typeof key !== "string" || !fields.includes(key)) || + fields.some((field) => !isEnumerableData(Object.getOwnPropertyDescriptor(value, field))) + ) { + throw new TypeError(`${label} schema is invalid`) + } +} + +function isEnumerableData(descriptor) { + return ( + descriptor?.enumerable === true && + "value" in descriptor && + descriptor.get === undefined && + descriptor.set === undefined + ) +} + +function assertExpectedTagObjectSha(value) { + if (!isSha(value)) throw new TypeError("Expected candidate tag object SHA is invalid") +} + +function normalizeExpectedWriterSnapshot(value) { + let snapshot + try { + snapshot = snapshotJson(value) + } catch { + throw new TypeError("Expected recovery snapshot is invalid") + } + const fields = [ + "releaseId", + "tagName", + "title", + "targetCommitish", + "draft", + "prerelease", + "immutable", + "body", + "marker", + "assets", + "evidenceAssets", + ] + if (!hasExactFields(snapshot, fields)) { + throw new TypeError("Expected recovery snapshot schema is invalid") + } + normalizeDuplicateDraftReleaseProjection(snapshot) + const expectedTag = DUPLICATE_TAG_BY_ID.get(snapshot.releaseId) + if (expectedTag === undefined || snapshot.tagName !== expectedTag) { + throw new TypeError("Recovery mutation target is not an approved duplicate Release") + } + if (!isBoundedText(snapshot.body, 512 * 1024, true)) { + throw new TypeError("Expected recovery body is invalid") + } + if (!Array.isArray(snapshot.assets) || !Array.isArray(snapshot.evidenceAssets)) { + throw new TypeError("Expected recovery asset inventory is invalid") + } + const expectedKinds = snapshot.evidenceAssets + if ( + expectedKinds.length > 2 || + expectedKinds.some((kind) => kind !== "body" && kind !== "receipt") || + new Set(expectedKinds).size !== expectedKinds.length || + (expectedKinds.includes("receipt") && !expectedKinds.includes("body")) + ) { + throw new TypeError("Expected recovery evidence state is invalid") + } + const names = new Set() + const ids = new Set() + for (const asset of snapshot.assets) { + if ( + !isObject(asset) || + ![4, 5].includes(Object.keys(asset).length) || + !hasExactFields( + asset, + Object.hasOwn(asset, "bytes") + ? ["id", "name", "sha256", "size", "bytes"] + : ["id", "name", "sha256", "size"], + ) || + !Number.isSafeInteger(asset.id) || + asset.id < 1 || + typeof asset.name !== "string" || + !ASSET_NAME_PATTERN.test(asset.name) || + !SHA256_PATTERN.test(asset.sha256) || + !Number.isSafeInteger(asset.size) || + asset.size < 1 || + names.has(asset.name) || + ids.has(asset.id) || + (Object.hasOwn(asset, "bytes") && typeof asset.bytes !== "string") + ) { + throw new TypeError("Expected recovery asset inventory is invalid") + } + names.add(asset.name) + ids.add(asset.id) + } + if (snapshot.assets.length !== 45 + expectedKinds.length) { + throw new TypeError("Expected recovery asset inventory is incomplete") + } + return deepFreeze(snapshot) +} + +function validateEvidenceUpload(snapshot, args) { + validateEscrowedSnapshot(snapshot) + if (typeof args.sha256 !== "string" || !SHA256_PATTERN.test(args.sha256)) { + throw new TypeError("Recovery evidence digest is invalid") + } + if (sha256(args.bytes) !== args.sha256) { + throw new TypeError("Recovery evidence input digest is not exact") + } + const bodySha256 = sha256(snapshot.body) + const archiveName = originalBodyAssetName(snapshot.releaseId, bodySha256) + const receiptName = recoveryReceiptAssetName(snapshot.releaseId) + if (args.name === archiveName) { + if (!args.bytes.equals(Buffer.from(snapshot.body, "utf8"))) { + throw new TypeError("Original-body archive bytes are not exact") + } + if (!["", "body"].includes(snapshot.evidenceAssets.join(","))) { + throw new TypeError("Original-body archive state is not recognized") + } + return "body" + } + if (args.name === receiptName) { + if (!["body", "body,receipt"].includes(snapshot.evidenceAssets.join(","))) { + throw new TypeError("Recovery receipt state is not recognized") + } + const receipt = parseCanonicalRecoveryReceipt(args.bytes) + if ( + receipt.duplicateReleaseId !== snapshot.releaseId || + receipt.originalBodySha256 !== bodySha256 || + receipt.baseAssetSetSha256 !== snapshot.marker.baseAssetSetSha256 || + receipt.archiveAsset.name !== archiveName || + receipt.archiveAsset.sha256 !== bodySha256 + ) { + throw new TypeError("Recovery receipt is not derived from the candidate snapshot") + } + return "receipt" + } + throw new TypeError("Recovery evidence asset name is not candidate-derived") +} + +function validateEscrowedSnapshot(snapshot) { + let parsed + try { + parsed = parseReleaseMarker(snapshot.body) + } catch { + throw new TypeError("Expected duplicate Release body is not canonical") + } + if ( + !sameJson(parsed, snapshot.marker) || + parsed.phase !== "ESCROWED" || + parsed.version !== DUPLICATE_DRAFT_RECOVERY_POLICY.version || + parsed.commitSha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha || + parsed.tag !== CANDIDATE_TAG || + typeof parsed.baseAssetSetSha256 !== "string" || + !SHA256_PATTERN.test(parsed.baseAssetSetSha256) + ) { + throw new TypeError("Expected duplicate Release marker is not the approved candidate") + } + const originalAssets = snapshot.assets.slice(0, 45) + const baseAssetSetSha256 = sha256( + `${JSON.stringify(originalAssets.map(({ name, sha256: digest }) => ({ name, sha256: digest })))}\n`, + ) + if (baseAssetSetSha256 !== parsed.baseAssetSetSha256) { + throw new TypeError("Expected duplicate Release asset inventory is not exact") + } + const bodySha256 = sha256(snapshot.body) + const archiveName = originalBodyAssetName(snapshot.releaseId, bodySha256) + const receiptName = recoveryReceiptAssetName(snapshot.releaseId) + for (const [index, kind] of snapshot.evidenceAssets.entries()) { + const asset = snapshot.assets[45 + index] + if (kind === "body") { + if ( + asset.name !== archiveName || + asset.sha256 !== bodySha256 || + Object.hasOwn(asset, "bytes") + ) { + throw new TypeError("Expected original-body archive asset is not exact") + } + continue + } + if ( + asset.name !== receiptName || + typeof asset.bytes !== "string" || + sha256(asset.bytes) !== asset.sha256 + ) { + throw new TypeError("Expected recovery receipt asset is not exact") + } + const receipt = parseCanonicalRecoveryReceipt(Buffer.from(asset.bytes, "utf8")) + if ( + receipt.duplicateReleaseId !== snapshot.releaseId || + receipt.originalBodySha256 !== bodySha256 || + receipt.baseAssetSetSha256 !== parsed.baseAssetSetSha256 || + receipt.archiveAsset.name !== archiveName || + receipt.archiveAsset.sha256 !== bodySha256 + ) { + throw new TypeError("Expected recovery receipt asset is not candidate-derived") + } + } +} + +function parseCanonicalRecoveryReceipt(bytes) { + let parsed + try { + parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) + } catch { + throw new TypeError("Recovery receipt bytes are malformed") + } + if ( + !hasExactFields(parsed, [ + "schemaVersion", + "repository", + "version", + "candidateSha", + "recoveryCommit", + "canonicalReleaseId", + "duplicateReleaseId", + "originalBodySha256", + "baseAssetSetSha256", + "archiveAsset", + ]) || + parsed.schemaVersion !== 1 + ) { + throw new TypeError("Recovery receipt schema is invalid") + } + const input = { ...parsed } + delete input.schemaVersion + let canonical + try { + canonical = canonicalRecoveryReceipt(input) + } catch { + throw new TypeError("Recovery receipt is not candidate-derived") + } + if (!canonical.equals(bytes)) throw new TypeError("Recovery receipt bytes are not canonical") + return parsed +} + +function validateQuarantineInput(snapshot, args) { + validateEscrowedSnapshot(snapshot) + if (snapshot.evidenceAssets.join(",") !== "body,receipt") { + throw new TypeError("Duplicate Release is not ready for quarantine") + } + if ( + typeof args.expectedBodySha256 !== "string" || + !SHA256_PATTERN.test(args.expectedBodySha256) || + sha256(snapshot.body) !== args.expectedBodySha256 + ) { + throw new TypeError("Expected duplicate Release body digest is stale") + } + if (typeof args.expectedNotice !== "string") { + throw new TypeError("Expected recovery notice is invalid") + } + let notice + try { + notice = JSON.parse(args.expectedNotice) + } catch { + throw new TypeError("Expected recovery notice is malformed") + } + if ( + !hasExactFields(notice, [ + "schemaVersion", + "type", + "repository", + "version", + "candidateSha", + "canonicalReleaseId", + "duplicateReleaseId", + "originalBodySha256", + "archiveAssetName", + "receiptAssetName", + "receiptSha256", + ]) + ) { + throw new TypeError("Expected recovery notice schema is invalid") + } + const noticeInput = { ...notice } + delete noticeInput.schemaVersion + delete noticeInput.type + delete noticeInput.candidateSha + let canonical + try { + canonical = canonicalRecoveryNotice(noticeInput) + } catch { + throw new TypeError("Expected recovery notice is not candidate-derived") + } + const archive = snapshot.assets.at(-2) + const receipt = snapshot.assets.at(-1) + if ( + canonical !== args.expectedNotice || + notice.originalBodySha256 !== args.expectedBodySha256 || + archive.name !== notice.archiveAssetName || + archive.sha256 !== notice.originalBodySha256 || + receipt.name !== notice.receiptAssetName || + receipt.sha256 !== notice.receiptSha256 || + receipt.bytes === undefined || + sha256(receipt.bytes) !== receipt.sha256 + ) { + throw new TypeError("Expected recovery notice does not match the complete snapshot") + } +} + +async function verifyRecoveryCandidateTag(context, expectedTagObjectSha) { + try { + const ref = requirePresent( + await context.github.getRef({ ref: `tags/${CANDIDATE_TAG}` }), + "CANDIDATE_TAG_UNAVAILABLE", + "Candidate tag could not be verified", + ) + if ( + !isObject(ref) || + ref.ref !== `refs/tags/${CANDIDATE_TAG}` || + !isObject(ref.object) || + ref.object.type !== "tag" || + ref.object.sha !== expectedTagObjectSha + ) { + writeFail("CANDIDATE_TAG_CONFLICT", "Candidate tag identity is not exact") + } + const tag = requirePresent( + await context.github.getGitTag({ tagSha: ref.object.sha }), + "CANDIDATE_TAG_UNAVAILABLE", + "Candidate tag could not be verified", + ) + if ( + !isObject(tag) || + tag.sha !== ref.object.sha || + tag.tag !== CANDIDATE_TAG || + !isObject(tag.object) || + tag.object.type !== "commit" || + tag.object.sha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha + ) { + writeFail("CANDIDATE_TAG_CONFLICT", "Candidate tag identity is not exact") + } + return ref.object.sha + } catch (error) { + if (error instanceof DuplicateDraftRecoveryWriteError) throw error + writeFail("CANDIDATE_TAG_UNAVAILABLE", "Candidate tag could not be verified") + } +} + +async function observeIssuedRecoveryMutation( + context, + request, + { releaseId, originalBody, expectedTagObjectSha, recordFence = false }, +) { + let response + let requestError = null + try { + response = await request() + } catch (error) { + requestError = error + } + let tagError = null + let tagObjectSha = null + try { + tagObjectSha = await verifyRecoveryCandidateTag(context, expectedTagObjectSha) + } catch (error) { + tagError = error + } + let snapshot = null + let projection = null + let observedAt = null + let snapshotError = null + try { + const current = await readCurrentWriterObservation(context, releaseId, originalBody) + snapshot = current.snapshot + projection = current.projection + observedAt = recordFence ? writerObservedAt(context) : null + } catch (error) { + snapshotError = error + } + return { + response, + requestError, + tagError, + tagObjectSha, + snapshot, + projection, + observedAt, + snapshotError, + } +} + +function requireExactMutationObservation(observation) { + if (observation.tagError !== null) { + writeFail( + "POST_WRITE_TAG_FENCE_CONFLICT", + "Candidate tag post-write fence could not be verified", + ) + } + if (observation.requestError !== null || observation.snapshotError !== null) { + writeFail("MUTATION_OUTCOME_AMBIGUOUS", "GitHub recovery mutation outcome is ambiguous") + } + return { + response: observation.response, + snapshot: observation.snapshot, + projection: observation.projection, + tagObjectSha: observation.tagObjectSha, + observedAt: observation.observedAt, + } +} + +function assertExactObservedMutationState(actual, expected) { + if (!sameJson(actual, expected)) { + writeFail("MUTATION_OUTCOME_AMBIGUOUS", "GitHub recovery mutation outcome is ambiguous") + } +} + +async function readExpectedWriterSnapshot(context, expected, originalBody) { + return (await readExpectedWriterObservation(context, expected, originalBody)).snapshot +} + +async function readExpectedWriterObservation(context, expected, originalBody) { + const current = await readCurrentWriterObservation(context, expected.releaseId, originalBody) + if (!sameJson(current.snapshot, expected)) { + writeFail("RELEASE_SNAPSHOT_CONFLICT", "Duplicate Release snapshot drifted") + } + return current +} + +async function readCurrentWriterObservation(context, releaseId, originalBody) { + try { + const release = requirePresent( + await context.github.getRelease({ releaseId }), + "RELEASE_UNAVAILABLE", + "Release snapshot could not be verified", + ) + const raw = safeWriterRemoteSnapshot(release, context.token, "RELEASE_MALFORMED") + const rawAssets = await readStrictPages(context, { + path: `/repos/${OWNER}/${REPOSITORY}/releases/${releaseId}/assets?per_page=100`, + operation: "RECOVERY_WRITE_ASSETS", + }) + const snapshot = await normalizeReleaseSnapshot({ + release: raw, + rawAssets, + releaseId, + expectedOriginalBody: originalBody, + github: context.github, + token: context.token, + }) + return deepFreeze({ + snapshot, + projection: normalizeDuplicateDraftReleaseProjection(snapshot), + }) + } catch (error) { + if (error instanceof DuplicateDraftRecoveryWriteError) throw error + writeFail("RELEASE_SNAPSHOT_UNAVAILABLE", "Duplicate Release snapshot could not be verified") + } +} + +async function readQuarantinePreWriteFence( + context, + expected, + originalBody, + baselineProjection, + expectedTagObjectSha, +) { + const [tagObjectSha, current] = await Promise.all([ + verifyRecoveryCandidateTag(context, expectedTagObjectSha), + readCurrentWriterObservation(context, expected.releaseId, originalBody), + ]) + if (!sameJson(current.snapshot, expected) || !sameJson(current.projection, baselineProjection)) { + writeFail("RELEASE_SNAPSHOT_CONFLICT", "Duplicate Release snapshot drifted") + } + const observedAt = writerObservedAt(context) + return deepFreeze({ + snapshot: current.snapshot, + projection: current.projection, + fence: canonicalWriterFence(observedAt, current.projection, tagObjectSha), + }) +} + +function writerObservedAt(context) { + let milliseconds + try { + milliseconds = context.observedNow() + } catch { + writeFail("WRITE_FENCE_CLOCK_INVALID", "Recovery write fence clock is invalid") + } + if (!Number.isSafeInteger(milliseconds)) { + writeFail("WRITE_FENCE_CLOCK_INVALID", "Recovery write fence clock is invalid") + } + let observedAt + try { + observedAt = new Date(milliseconds).toISOString() + } catch { + writeFail("WRITE_FENCE_CLOCK_INVALID", "Recovery write fence clock is invalid") + } + return observedAt +} + +function canonicalWriterFence(observedAt, projection, tagObjectSha) { + return deepFreeze({ + observedAt, + projectionSha256: duplicateDraftReleaseProjectionSha256(projection), + tagObjectSha, + }) +} + +function assertExistingEvidenceAsset(asset, args, kind) { + if (asset.sha256 !== args.sha256) { + writeFail("EVIDENCE_ASSET_CONFLICT", "Existing recovery evidence asset digest differs") + } + if (kind === "receipt" && asset.bytes !== args.bytes.toString("utf8")) { + writeFail("EVIDENCE_ASSET_CONFLICT", "Existing recovery evidence asset bytes differ") + } +} + +function normalizeUploadResponse(value, args) { + const response = safeWriteResponse(value, "EVIDENCE_UPLOAD_RESPONSE_MALFORMED") + if ( + !isObject(response) || + !Number.isSafeInteger(response.id) || + response.id < 1 || + response.name !== args.name || + response.digest !== `sha256:${args.sha256}` || + response.size !== args.bytes.byteLength || + response.state !== "uploaded" + ) { + writeFail("EVIDENCE_UPLOAD_RESPONSE_MALFORMED", "Evidence upload response is malformed") + } + return { id: response.id } +} + +function normalizePatchResponse(value, current, expectedNotice) { + const response = safeWriteResponse(value, "QUARANTINE_RESPONSE_MALFORMED") + if ( + !isObject(response) || + response.id !== current.releaseId || + response.tag_name !== current.tagName || + response.name !== WRITER_TITLE || + response.body !== expectedNotice || + response.draft !== true || + response.prerelease !== false || + response.immutable !== false || + response.target_commitish !== "main" + ) { + writeFail("QUARANTINE_RESPONSE_MALFORMED", "Quarantine response is malformed") + } +} + +function safeWriteResponse(value, code) { + try { + return snapshotJson(value) + } catch { + writeFail(code, "GitHub write response is malformed") + } +} + +function createStrictCredentialFetch(fetchImpl, token, maximumResponseBytes, timeoutMs) { + const credential = Buffer.from(token, "utf8") + const credentialName = Buffer.from(token.toLowerCase(), "utf8") + return async (url, init) => { + const deadline = createWriterResponseDeadline(timeoutMs, init?.signal) + try { + const parsedUrl = new URL(url) + const responseMaximum = strictWriterResponseMaximum(parsedUrl, maximumResponseBytes) + const existingHeaders = new Headers(init?.headers) + const authenticatedInit = { + ...init, + signal: deadline.signal, + ...(parsedUrl.origin === API_ORIGIN && !existingHeaders.has("authorization") + ? { + headers: { + ...Object.fromEntries(existingHeaders.entries()), + Authorization: `Bearer ${token}`, + }, + } + : {}), + } + const response = await deadline.race(() => fetchImpl(url, authenticatedInit)) + let status + let headers + let body + try { + body = response?.body + status = response?.status + headers = normalizeStrictWriterResponseHeaders( + response?.headers, + credential, + credentialName, + ) + } catch (error) { + cancelResponseBody(body) + throw error + } + if (body === null || body === undefined || typeof body.getReader !== "function") { + return { status, headers, body } + } + const bytes = await readStrictWriterResponse(body, responseMaximum, credential, deadline) + return { + status, + headers, + body: bufferedResponseBody(bytes), + } + } finally { + deadline.dispose() + } + } +} + +function strictWriterResponseMaximum(url, configuredMaximum) { + const isReleaseAsset = + url.origin === API_ORIGIN && + url.search === "" && + WRITER_RELEASE_ASSET_PATH_PATTERN.test(url.pathname) + const hostname = url.hostname.toLowerCase() + const isSignedDownload = + url.protocol === "https:" && + url.username === "" && + url.password === "" && + url.hash === "" && + (WRITER_SIGNED_DOWNLOAD_HOSTS.has(hostname) || WRITER_SIGNED_AZURE_HOST_PATTERN.test(hostname)) + return isReleaseAsset || isSignedDownload + ? Math.min(configuredMaximum, RECOVERY_ASSET_BYTES) + : configuredMaximum +} + +function normalizeStrictWriterResponseHeaders(headers, credential, credentialName) { + let iterator + try { + iterator = Headers.prototype.entries.call(headers) + } catch { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery response headers are malformed") + } + const normalized = new Headers() + let count = 0 + let totalBytes = 0 + while (true) { + let step + try { + step = iterator.next() + } catch { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery response headers are malformed") + } + if (step === null || typeof step !== "object" || typeof step.done !== "boolean") { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery response headers are malformed") + } + if (step.done) break + if ( + !Array.isArray(step.value) || + step.value.length !== 2 || + typeof step.value[0] !== "string" || + typeof step.value[1] !== "string" + ) { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery response headers are malformed") + } + const [name, value] = step.value + count += 1 + totalBytes += Buffer.byteLength(name) + Buffer.byteLength(value) + if (count > WRITER_MAX_RESPONSE_HEADERS || totalBytes > WRITER_MAX_RESPONSE_HEADER_BYTES) { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery response headers are malformed") + } + if ( + Buffer.from(name.toLowerCase(), "utf8").includes(credentialName) || + Buffer.from(value, "utf8").includes(credential) || + (name === "location" && decodedLocationContainsCredential(value, credential)) + ) { + writeFail( + "REMOTE_CREDENTIAL_CONFLICT", + "GitHub recovery response contains configured credentials", + ) + } + normalized.append(name, value) + } + return normalized +} + +function decodedLocationContainsCredential(value, credential) { + let decoded = value + for (let depth = 0; depth < WRITER_MAX_LOCATION_DECODE_PASSES; depth += 1) { + const next = decoded.replace(/(?:%[0-9a-f]{2})+/giu, (encoded) => { + const octets = encoded.slice(1).split("%").map(hexByte) + return new TextDecoder().decode(Uint8Array.from(octets)) + }) + if (next === decoded) return false + if (Buffer.from(next, "utf8").includes(credential)) return true + decoded = next + } + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery response Location encoding is unsafe") +} + +function hexByte(value) { + return Number.parseInt(value, 16) +} + +async function readStrictWriterResponse(body, maximum, credential, deadline) { + const reader = body.getReader() + if (reader === null || typeof reader !== "object" || typeof reader.read !== "function") { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery response body is malformed") + } + const chunks = [] + let total = 0 + let chunkCount = 0 + let tail = Buffer.alloc(0) + try { + while (true) { + const result = await deadline.race(() => reader.read()) + if (result === null || typeof result !== "object" || typeof result.done !== "boolean") { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery response body is malformed") + } + if (result.done) break + const bytes = snapshotStrictResponseChunk(result.value, maximum) + if (bytes.byteLength === 0) { + writeFail("WRITE_RESPONSE_NO_PROGRESS", "GitHub recovery response made no progress") + } + chunkCount += 1 + if (chunkCount > WRITER_MAX_RESPONSE_CHUNKS) { + writeFail( + "WRITE_RESPONSE_CHUNKS_OVER_LIMIT", + "GitHub recovery response has too many chunks", + ) + } + total += bytes.byteLength + if (total > maximum) { + writeFail("WRITE_RESPONSE_OVER_LIMIT", "GitHub recovery response exceeds byte limit") + } + const searchable = tail.length === 0 ? bytes : Buffer.concat([tail, bytes]) + if (searchable.includes(credential)) { + writeFail( + "REMOTE_CREDENTIAL_CONFLICT", + "GitHub recovery response contains configured credentials", + ) + } + const retained = Math.min(Math.max(credential.byteLength - 1, 0), searchable.byteLength) + tail = + retained === 0 ? Buffer.alloc(0) : searchable.subarray(searchable.byteLength - retained) + chunks.push(bytes) + } + } catch (error) { + safelyCancelReader(reader) + throw error + } finally { + safelyReleaseReader(reader) + } + return Buffer.concat(chunks, total) +} + +function bufferedResponseBody(bytes) { + let claimed = false + return { + getReader() { + if (claimed) throw new TypeError("Recovery response body was already consumed") + claimed = true + let delivered = false + return { + async read() { + if (delivered || bytes.byteLength === 0) return { done: true, value: undefined } + delivered = true + return { done: false, value: bytes } + }, + async cancel() { + delivered = true + }, + releaseLock() {}, + } + }, + async cancel() { + claimed = true + }, + } +} + +function createWriterResponseDeadline(timeoutMs, callerSignal) { + const controller = new AbortController() + const deadline = performance.now() + timeoutMs + const abort = () => controller.abort() + if (callerSignal?.aborted === true) abort() + else callerSignal?.addEventListener("abort", abort, { once: true }) + const timeout = setTimeout(abort, timeoutMs) + return { + signal: controller.signal, + async race(operation) { + assertWriterResponseDeadline(deadline, controller.signal) + let rejectAbort + const aborted = new Promise((_resolve, reject) => { + rejectAbort = () => + reject( + new DuplicateDraftRecoveryWriteError( + "WRITE_TIMEOUT", + "GitHub recovery response timed out", + ), + ) + controller.signal.addEventListener("abort", rejectAbort, { once: true }) + }) + try { + const result = await Promise.race([Promise.resolve().then(operation), aborted]) + assertWriterResponseDeadline(deadline, controller.signal) + return result + } finally { + controller.signal.removeEventListener("abort", rejectAbort) + } + }, + dispose() { + clearTimeout(timeout) + callerSignal?.removeEventListener("abort", abort) + }, + } +} + +function assertWriterResponseDeadline(deadline, signal) { + if (signal.aborted || performance.now() >= deadline) { + writeFail("WRITE_TIMEOUT", "GitHub recovery response timed out") + } +} + +function safelyCancelReader(reader) { + if (typeof reader.cancel !== "function") return + try { + Promise.resolve(reader.cancel()).catch(() => {}) + } catch { + // Preserve the primary fail-closed result. + } +} + +function safelyReleaseReader(reader) { + if (typeof reader.releaseLock !== "function") return + try { + reader.releaseLock() + } catch { + // The response has already been bounded or failed closed. + } +} + +function snapshotStrictResponseChunk(value, maximum) { + let prototype + let byteLength + try { + byteLength = TYPED_ARRAY_BYTE_LENGTH.call(value) + prototype = Object.getPrototypeOf(value) + if (prototype !== Buffer.prototype && prototype !== Uint8Array.prototype) { + throw new TypeError("Unexpected response chunk prototype") + } + } catch { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery response chunk is malformed") + } + if (byteLength > maximum) { + writeFail("WRITE_RESPONSE_OVER_LIMIT", "GitHub recovery write response exceeds byte limit") + } + try { + return Buffer.from(Uint8Array.prototype.slice.call(value)) + } catch { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery response chunk is malformed") + } +} + +async function requestRecoveryJson( + context, + { url, method, body, bytes: suppliedBytes, contentType, maximumRequestBytes }, +) { + // Innermost guard: as narrow as the caller-side pin, so neither the canonical + // Release nor an asset endpoint is reachable even if a call site regresses. + if ( + !["POST", "PATCH"].includes(method) || + !(method === "POST" ? WRITER_UPLOAD_URL_PATTERN : WRITER_PATCH_URL_PATTERN).test(url) + ) { + throw new TypeError("Recovery writer URL or method is not allowed") + } + const requestBytes = + suppliedBytes === undefined + ? Buffer.from(JSON.stringify(canonicalize(body)), "utf8") + : Buffer.from(suppliedBytes) + if (requestBytes.byteLength < 1 || requestBytes.byteLength > maximumRequestBytes) { + throw new TypeError("Recovery write request exceeds its byte limit") + } + const controller = new AbortController() + const deadline = performance.now() + context.timeoutMs + const timeout = setTimeout(() => controller.abort(), context.timeoutMs) + try { + let response + try { + response = await fetchRecoveryWrite( + context.fetchImpl, + url, + { + method, + redirect: "manual", + headers: { + Accept: "application/vnd.github+json", + "Content-Type": contentType, + "X-GitHub-Api-Version": API_VERSION, + Authorization: `Bearer ${context.token}`, + }, + body: requestBytes, + signal: controller.signal, + }, + controller.signal, + ) + assertRecoveryWriteDeadline(deadline, controller.signal) + } catch { + writeFail( + controller.signal.aborted ? "WRITE_TIMEOUT" : "WRITE_UNAVAILABLE", + controller.signal.aborted + ? "GitHub recovery write timed out" + : "GitHub recovery write failed", + ) + } + if (!Number.isInteger(response?.status) || response.status < 100 || response.status > 599) { + cancelResponseBody(response?.body) + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery write response is malformed") + } + if (response.status >= 300 && response.status < 400) { + cancelResponseBody(response.body) + writeFail("WRITE_REDIRECT_FORBIDDEN", "GitHub recovery write redirects are forbidden") + } + const responseBytes = await readBoundedWriteResponse( + response.body, + context.maxResponseBytes, + controller.signal, + deadline, + ) + if (responseBytes.byteLength === 0) { + return { httpStatus: response.status, body: null } + } + const responseContentType = response.headers?.get?.("content-type") + if ( + typeof responseContentType !== "string" || + !/^application\/(?:[A-Za-z0-9!#$&^_.+-]+\+)?json(?:\s*;|\s*$)/iu.test(responseContentType) + ) { + writeFail("WRITE_CONTENT_TYPE_CONFLICT", "GitHub recovery write response is not JSON") + } + let parsed + try { + parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(responseBytes)) + } catch { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery write response JSON is malformed") + } + return { + httpStatus: response.status, + body: safeWriterRemoteSnapshot(parsed, context.token, "WRITE_RESPONSE_MALFORMED"), + } + } catch (error) { + if (error instanceof DuplicateDraftRecoveryWriteError) throw error + writeFail( + controller.signal.aborted ? "WRITE_TIMEOUT" : "WRITE_RESPONSE_MALFORMED", + controller.signal.aborted + ? "GitHub recovery write timed out" + : "GitHub recovery write response is malformed", + ) + } finally { + clearTimeout(timeout) + } +} + +async function fetchRecoveryWrite(fetchImpl, url, init, signal) { + let rejectAbort + const aborted = new Promise((_resolve, reject) => { + rejectAbort = () => + reject( + new DuplicateDraftRecoveryWriteError("WRITE_TIMEOUT", "GitHub recovery write timed out"), + ) + signal.addEventListener("abort", rejectAbort, { once: true }) + }) + try { + return await Promise.race([fetchImpl(url, init), aborted]) + } finally { + signal.removeEventListener("abort", rejectAbort) + } +} + +async function readBoundedWriteResponse(stream, maximum, signal, deadline) { + if (stream === null) return Buffer.alloc(0) + if (stream === undefined || typeof stream.getReader !== "function") { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery write response body is malformed") + } + const reader = stream.getReader() + const chunks = [] + let total = 0 + let chunkCount = 0 + try { + while (true) { + assertRecoveryWriteDeadline(deadline, signal) + const { done, value } = await readRecoveryWriteChunk(reader, signal, deadline) + assertRecoveryWriteDeadline(deadline, signal) + if (done) break + if (!(value instanceof Uint8Array)) { + writeFail("WRITE_RESPONSE_MALFORMED", "GitHub recovery write response body is malformed") + } + if (value.byteLength === 0) { + writeFail("WRITE_RESPONSE_NO_PROGRESS", "GitHub recovery write response made no progress") + } + chunkCount += 1 + if (chunkCount > WRITER_MAX_RESPONSE_CHUNKS) { + writeFail( + "WRITE_RESPONSE_CHUNKS_OVER_LIMIT", + "GitHub recovery write response has too many chunks", + ) + } + total += value.byteLength + if (total > maximum) { + writeFail("WRITE_RESPONSE_OVER_LIMIT", "GitHub recovery write response exceeds byte limit") + } + chunks.push(Buffer.from(value)) + } + } catch (error) { + void reader.cancel().catch(() => {}) + throw error + } + return Buffer.concat(chunks, total) +} + +async function readRecoveryWriteChunk(reader, signal, deadline) { + assertRecoveryWriteDeadline(deadline, signal) + let rejectAbort + const aborted = new Promise((_resolve, reject) => { + rejectAbort = () => + reject( + new DuplicateDraftRecoveryWriteError("WRITE_TIMEOUT", "GitHub recovery write timed out"), + ) + signal.addEventListener("abort", rejectAbort, { once: true }) + }) + try { + return await Promise.race([reader.read(), aborted]) + } finally { + signal.removeEventListener("abort", rejectAbort) + } +} + +function assertRecoveryWriteDeadline(deadline, signal) { + if (signal.aborted || performance.now() >= deadline) { + writeFail("WRITE_TIMEOUT", "GitHub recovery write timed out") + } +} + +function cancelResponseBody(body) { + if (body !== null && body !== undefined && typeof body.cancel === "function") { + void body.cancel().catch(() => {}) + } +} + +function assertBoundedInteger(value, minimum, maximum, label) { + if (!Number.isSafeInteger(value) || value < minimum || value > maximum) { + throw new TypeError(`${label} is invalid`) + } +} + +function hasUnsafeTokenCharacters(value) { + for (const character of value) { + const code = character.codePointAt(0) + if (code <= 31 || code === 127) return true + } + return false +} + +function isPlainObject(value) { + if (!isObject(value)) return false + const prototype = Object.getPrototypeOf(value) + return prototype === Object.prototype || prototype === null +} + +function hasExactFields(value, fields) { + if (!isObject(value)) return false + const actual = Object.keys(value).sort() + const expected = [...fields].sort() + return actual.length === expected.length && actual.every((key, index) => key === expected[index]) +} + +function sameJson(left, right) { + return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right)) +} + +function canonicalize(value) { + if (Array.isArray(value)) return value.map(canonicalize) + if (!isObject(value)) return value + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, canonicalize(value[key])]), + ) +} + +export class DuplicateDraftRecoveryWriteError extends Error { + constructor(code, message) { + super(message) + this.name = "DuplicateDraftRecoveryWriteError" + this.code = code + } +} + +function writeFail(code, message) { + throw new DuplicateDraftRecoveryWriteError(code, message) +} + +async function readRepositoryState(context) { + const [repository, mainRef] = await Promise.all([ + readExactJson(context, { + path: `/repos/${OWNER}/${REPOSITORY}`, + operation: "repository", + }), + context.github.getRef({ ref: "heads/main" }), + ]) + if ( + !isObject(repository) || + repository.id !== Number(REPOSITORY_ID) || + repository.full_name !== DUPLICATE_DRAFT_RECOVERY_POLICY.repository || + repository.name !== REPOSITORY || + repository.default_branch !== "main" || + !isObject(repository.owner) || + repository.owner.login !== OWNER + ) { + fail("REPOSITORY_IDENTITY_CONFLICT", "Recovery repository identity is not exact") + } + const ref = requirePresent( + await mainRef, + "REMOTE_MAIN_UNAVAILABLE", + "Remote main could not be verified", + ) + if ( + !isObject(ref) || + ref.ref !== "refs/heads/main" || + !isObject(ref.object) || + ref.object.type !== "commit" || + !isSha(ref.object.sha) + ) { + fail("REMOTE_MAIN_MALFORMED", "Remote main evidence is malformed") + } + return deepFreeze({ + id: Number(REPOSITORY_ID), + nameWithOwner: DUPLICATE_DRAFT_RECOVERY_POLICY.repository, + mainSha: ref.object.sha, + }) +} + +async function readRequiredCi(context, reviewedHeadSha) { + const [checks, workflows] = await Promise.all([ + readStrictPages(context, { + path: `/repos/${OWNER}/${REPOSITORY}/commits/${reviewedHeadSha}/check-runs?per_page=100`, + operation: "REVIEWED_VALIDATE_CHECKS", + field: "check_runs", + requireTotalCount: true, + }), + readStrictPages(context, { + path: `/repos/${OWNER}/${REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${reviewedHeadSha}&per_page=100`, + operation: "REVIEWED_CI_RUNS", + field: "workflow_runs", + requireTotalCount: true, + }), + ]) + const normalizedChecks = checks.map((check) => normalizeCheckRun(check, reviewedHeadSha)) + const normalizedWorkflows = workflows.map((run) => normalizeReviewedCiRun(run, reviewedHeadSha)) + assertUniqueIds(normalizedChecks, "REVIEWED_VALIDATE_MALFORMED") + assertUniqueIds(normalizedWorkflows, "REVIEWED_VALIDATE_MALFORMED") + const matchingWorkflows = normalizedWorkflows.filter( + (run) => + run?.name === "CI" && + run?.path === ".github/workflows/ci.yml" && + run?.head_sha === reviewedHeadSha && + run?.event === "pull_request", + ) + if (matchingWorkflows.length !== 1) { + fail("REVIEWED_VALIDATE_NOT_SUCCESSFUL", "Reviewed CI validate check did not succeed") + } + const workflow = matchingWorkflows[0] + const matchingChecks = normalizedChecks.filter( + (check) => + check?.name === "validate" && + check?.head_sha === reviewedHeadSha && + String(check?.check_suite?.id) === String(workflow.check_suite_id), + ) + if ( + matchingChecks.length !== 1 || + !Number.isSafeInteger(workflow.id) || + workflow.id < 1 || + !Number.isSafeInteger(workflow.run_attempt) || + workflow.run_attempt < 1 || + !Number.isSafeInteger(workflow.check_suite_id) || + workflow.check_suite_id < 1 || + workflow.status !== "completed" || + workflow.conclusion !== "success" || + matchingChecks[0].status !== "completed" || + matchingChecks[0].conclusion !== "success" + ) { + fail("REVIEWED_VALIDATE_NOT_SUCCESSFUL", "Reviewed CI validate check did not succeed") + } + return { validateRunId: workflow.id } +} + +function normalizeReviewedPullRequest(value, reviewedCommit) { + const pull = safeSnapshot(value, "REVIEWED_PULL_REQUEST_MALFORMED") + if ( + !isObject(pull) || + !Number.isSafeInteger(pull.number) || + pull.number < 1 || + pull.state !== "closed" || + !isTimestamp(pull.merged_at) || + pull.merge_commit_sha !== reviewedCommit || + !isObject(pull.base) || + pull.base.ref !== "main" || + !isObject(pull.base.repo) || + pull.base.repo.id !== Number(REPOSITORY_ID) || + pull.base.repo.full_name !== DUPLICATE_DRAFT_RECOVERY_POLICY.repository || + !isObject(pull.head) || + !isSha(pull.head.sha) + ) { + fail("REVIEWED_PULL_REQUEST_CONFLICT", "Reviewed pull request identity is not exact") + } + return { pullRequestNumber: pull.number, reviewedHeadSha: pull.head.sha } +} + +function commitTreeSha(value, expectedCommitSha, label) { + const commit = safeSnapshot(value, "REVIEWED_COMMIT_MALFORMED") + if ( + !isObject(commit) || + commit.sha !== expectedCommitSha || + !isObject(commit.tree) || + !isSha(commit.tree.sha) + ) { + fail("REVIEWED_COMMIT_MALFORMED", `Reviewed ${label} evidence is malformed`) + } + return commit.tree.sha +} + +function normalizeReleaseRuns(value) { + if (!Array.isArray(value)) fail("RELEASE_RUNS_MALFORMED", "Release workflow runs are malformed") + const seen = new Set() + const runs = value.map((raw) => { + const run = safeSnapshot(raw, "RELEASE_RUNS_MALFORMED") + if ( + !isObject(run) || + !Number.isSafeInteger(run.id) || + run.id < 1 || + seen.has(run.id) || + !Number.isSafeInteger(run.run_attempt) || + run.run_attempt < 1 || + !RUN_STATUSES.has(run.status) || + !isSha(run.head_sha) || + run.path !== RELEASE_WORKFLOW_PATH || + !isTimestamp(run.created_at) || + !isNullableTimestamp(run.run_started_at) || + !isTimestamp(run.updated_at) || + !coherentRunState(run) || + !orderedTimestamps(run.created_at, run.run_started_at, run.updated_at) + ) { + fail("RELEASE_RUNS_MALFORMED", "Release workflow runs are malformed") + } + seen.add(run.id) + return { + id: run.id, + runAttempt: run.run_attempt, + status: run.status, + conclusion: run.conclusion, + headSha: run.head_sha, + createdAt: run.created_at, + startedAt: run.run_started_at, + updatedAt: run.updated_at, + } + }) + return runs.sort((left, right) => left.id - right.id || left.runAttempt - right.runAttempt) +} + +function normalizeCandidateJobs(value, expectedRunId, currentAttempt) { + if (!Array.isArray(value) || value.length === 0) { + fail("CANDIDATE_JOBS_MALFORMED", "Candidate workflow jobs are malformed") + } + const ids = new Set() + const identities = new Set() + const attempts = new Set() + const publisherJobsByAttempt = new Map() + const jobs = value.map((raw) => { + const job = safeSnapshot(raw, "CANDIDATE_JOBS_MALFORMED") + const identity = `${job?.run_attempt}:${job?.id}` + if ( + !isObject(job) || + !Number.isSafeInteger(job.id) || + job.id < 1 || + ids.has(job.id) || + identities.has(identity) || + !Number.isSafeInteger(job.run_id) || + job.run_id !== expectedRunId || + !Number.isSafeInteger(job.run_attempt) || + job.run_attempt < 1 || + job.run_attempt > currentAttempt || + !isBoundedText(job.name, 512) || + !JOB_STATUSES.has(job.status) || + !isNullableTimestamp(job.started_at) || + !isNullableTimestamp(job.completed_at) || + !coherentJobState(job) + ) { + fail("CANDIDATE_JOBS_MALFORMED", "Candidate workflow jobs are malformed") + } + ids.add(job.id) + identities.add(identity) + attempts.add(job.run_attempt) + if (job.name === "publish-npm") { + publisherJobsByAttempt.set( + job.run_attempt, + (publisherJobsByAttempt.get(job.run_attempt) ?? 0) + 1, + ) + } + return { + id: job.id, + runId: job.run_id, + runAttempt: job.run_attempt, + name: job.name, + status: job.status, + conclusion: job.conclusion, + startedAt: job.started_at, + completedAt: job.completed_at, + } + }) + if (attempts.size !== currentAttempt) { + fail("CANDIDATE_JOBS_MALFORMED", "Candidate workflow job attempt coverage is incomplete") + } + for (let attempt = 1; attempt <= currentAttempt; attempt += 1) { + if (!attempts.has(attempt) || publisherJobsByAttempt.get(attempt) !== 1) { + fail("CANDIDATE_JOBS_MALFORMED", "Candidate workflow publish job identity is not exact") + } + } + return jobs.sort((left, right) => left.runAttempt - right.runAttempt || left.id - right.id) +} + +function normalizeReleaseRow(value) { + const release = safeSnapshot(value, "RELEASE_LIST_MALFORMED") + if ( + !isObject(release) || + !Number.isSafeInteger(release.id) || + release.id < 1 || + !isBoundedText(release.tag_name, 1024) || + !(release.name === null || isBoundedText(release.name, 1024)) || + !(release.body === null || isBoundedText(release.body, 512 * 1024, true)) || + typeof release.draft !== "boolean" || + typeof release.prerelease !== "boolean" || + typeof release.immutable !== "boolean" || + !isBoundedText(release.target_commitish, 1024) + ) { + fail("RELEASE_LIST_MALFORMED", "Release inventory is malformed") + } + return { + id: release.id, + tagName: release.tag_name, + title: release.name, + body: release.body, + draft: release.draft, + prerelease: release.prerelease, + immutable: release.immutable, + targetCommitish: release.target_commitish, + } +} + +function normalizeCheckRun(value, reviewedHeadSha) { + const check = safeSnapshot(value, "REVIEWED_VALIDATE_MALFORMED") + if ( + !isObject(check) || + !Number.isSafeInteger(check.id) || + check.id < 1 || + !isBoundedText(check.name, 256) || + check.head_sha !== reviewedHeadSha || + !isObject(check.check_suite) || + !Number.isSafeInteger(check.check_suite.id) || + check.check_suite.id < 1 || + !["queued", "in_progress", "completed"].includes(check.status) || + !coherentTerminalState(check.status, check.conclusion) + ) { + fail("REVIEWED_VALIDATE_MALFORMED", "Reviewed validate check evidence is malformed") + } + return check +} + +function normalizeReviewedCiRun(value, reviewedHeadSha) { + const run = safeSnapshot(value, "REVIEWED_VALIDATE_MALFORMED") + if ( + !isObject(run) || + !Number.isSafeInteger(run.id) || + run.id < 1 || + !Number.isSafeInteger(run.run_attempt) || + run.run_attempt < 1 || + !Number.isSafeInteger(run.check_suite_id) || + run.check_suite_id < 1 || + run.head_sha !== reviewedHeadSha || + !isBoundedText(run.name, 256) || + !isBoundedText(run.path, 1024) || + !isBoundedText(run.event, 256) || + !RUN_STATUSES.has(run.status) || + !coherentTerminalState(run.status, run.conclusion) + ) { + fail("REVIEWED_VALIDATE_MALFORMED", "Reviewed CI workflow evidence is malformed") + } + return run +} + +function assertUniqueIds(values, code) { + if (new Set(values.map(({ id }) => id)).size !== values.length) { + fail(code, "Recovery read contains duplicate record IDs") + } +} + +async function normalizeReleaseSnapshot({ + release, + rawAssets, + releaseId, + expectedOriginalBody, + github, + token, +}) { + const raw = safeSnapshot(release, "RELEASE_MALFORMED") + if ( + !isObject(raw) || + raw.id !== releaseId || + !isBoundedText(raw.tag_name, 1024) || + raw.name !== WRITER_TITLE || + !isBoundedText(raw.body, 512 * 1024, true) || + raw.draft !== true || + raw.prerelease !== false || + raw.immutable !== false || + raw.target_commitish !== "main" || + !Array.isArray(rawAssets) + ) { + fail("RELEASE_MALFORMED", "Release snapshot is malformed") + } + const marker = releaseMarker(raw.body) + const assets = [] + const evidenceAssets = [] + const assetIds = new Set() + const assetNames = new Set() + for (const rawAsset of rawAssets) { + const asset = safeSnapshot(rawAsset, "RELEASE_ASSETS_MALFORMED") + if ( + !isObject(asset) || + !Number.isSafeInteger(asset.id) || + asset.id < 1 || + assetIds.has(asset.id) || + !ASSET_NAME_PATTERN.test(asset.name) || + assetNames.has(asset.name) || + !/^sha256:[0-9a-f]{64}$/u.test(asset.digest) || + !Number.isSafeInteger(asset.size) || + asset.size < 1 + ) { + fail("RELEASE_ASSETS_MALFORMED", "Release asset inventory is malformed") + } + assetIds.add(asset.id) + assetNames.add(asset.name) + const normalized = { + id: asset.id, + name: asset.name, + sha256: asset.digest.slice(7), + size: asset.size, + } + const kind = recoveryEvidenceKind(asset.name, releaseId) + if (kind !== null) { + if (expectedOriginalBody === undefined) { + fail("RECOVERY_ASSET_UNEXPECTED", "Recovery evidence asset is not expected here") + } + const downloaded = requirePresent( + await github.downloadReleaseAsset({ + assetId: asset.id, + maximumBytes: RECOVERY_ASSET_BYTES, + }), + "RECOVERY_ASSET_UNAVAILABLE", + "Recovery evidence asset bytes could not be verified", + ) + if ( + typeof downloaded !== "string" || + !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(downloaded) + ) { + fail("RECOVERY_ASSET_BYTES_CONFLICT", "Recovery evidence asset bytes are not exact") + } + const bytes = Buffer.from(downloaded, "base64") + if (token !== null && bytes.includes(Buffer.from(token, "utf8"))) { + fail( + "RECOVERY_ASSET_CREDENTIAL_CONFLICT", + "Recovery evidence asset contains configured credentials", + ) + } + if ( + bytes.toString("base64") !== downloaded || + bytes.byteLength !== asset.size || + sha256(bytes) !== normalized.sha256 + ) { + fail("RECOVERY_ASSET_BYTES_CONFLICT", "Recovery evidence asset bytes are not exact") + } + if (kind === "body") { + if (bytes.toString("utf8") !== expectedOriginalBody) { + fail("RECOVERY_BODY_ARCHIVE_CONFLICT", "Archived original Release body is not exact") + } + } else { + normalized.bytes = bytes.toString("utf8") + } + evidenceAssets.push(kind) + } + assets.push(normalized) + } + return deepFreeze({ + releaseId, + tagName: raw.tag_name, + title: raw.name, + targetCommitish: raw.target_commitish, + draft: raw.draft, + prerelease: raw.prerelease, + immutable: raw.immutable, + body: raw.body, + marker, + assets, + ...(releaseId === DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId ? {} : { evidenceAssets }), + }) +} + +function recoveryEvidenceKind(name, releaseId) { + const prefix = `dawn-v${DUPLICATE_DRAFT_RECOVERY_POLICY.version}-duplicate-${releaseId}-` + // The prefix embeds the version, so its dots must be escaped: an unescaped "." + // would classify a near-miss asset name as the original-body archive. + if (new RegExp(`^${escapeRegExp(prefix)}original-body-[0-9a-f]{64}\\.txt$`, "u").test(name)) { + return "body" + } + if (name === `${prefix}recovery-receipt.json`) return "receipt" + return null +} + +function releaseMarker(body) { + if (typeof body !== "string") return null + try { + return parseReleaseMarker(body) + } catch { + return null + } +} + +async function readExactJson(context, { path, operation, accept = "application/vnd.github+json" }) { + const result = await context.http.getJson({ + url: `${API_ORIGIN}${path}`, + headers: githubHeaders(context.token, accept), + }) + if ( + result.status !== "OK" || + result.httpStatus !== 200 || + result.code !== null || + result.headers?.link !== null + ) { + fail(`${operation.toUpperCase().replaceAll("-", "_")}_UNAVAILABLE`, `${operation} read failed`) + } + const malformedCode = `${operation.toUpperCase().replaceAll("-", "_")}_MALFORMED` + return safeContextRemoteSnapshot(context, result.body, malformedCode) +} + +async function readStrictPages(context, { path, operation, field, requireTotalCount = false }) { + const records = [] + const seenUrls = new Set() + let totalCount = null + let advertisedLastPage = null + let url = `${API_ORIGIN}${path}` + let remainingBytes = context.maxResponseBytes + const startedAt = context.now() + if (!Number.isSafeInteger(startedAt)) { + fail(`${operation}_UNAVAILABLE`, `${operation.toLowerCase()} read failed`) + } + const deadline = startedAt + context.timeoutMs + for (let page = 0; page < MAX_PAGES; page += 1) { + if (seenUrls.has(url)) fail("PAGINATION_DRIFT", "Recovery read pagination is unsafe") + seenUrls.add(url) + const currentTime = context.now() + const remainingTime = deadline - currentTime + if (!Number.isSafeInteger(currentTime) || remainingTime < 1) { + fail(`${operation}_UNAVAILABLE`, `${operation.toLowerCase()} read failed`) + } + if (remainingBytes < 1) { + fail(`${operation}_OVER_LIMIT`, `${operation.toLowerCase()} read exceeds the byte limit`) + } + const result = await readBoundary(`${operation}_UNAVAILABLE`, () => + context.http.getJson({ + url, + headers: githubHeaders(context.token, "application/vnd.github+json"), + timeoutMs: Math.min(context.timeoutMs, remainingTime), + maxResponseBytes: remainingBytes, + }), + ) + if (result.status !== "OK" || result.httpStatus !== 200 || result.code !== null) { + const code = + result.code === "RESPONSE_TOO_LARGE" + ? `${operation}_OVER_LIMIT` + : `${operation}_UNAVAILABLE` + fail(code, `${operation.toLowerCase()} read failed`) + } + if (!Number.isSafeInteger(result.bodyBytes) || result.bodyBytes < 0) { + fail(`${operation}_MALFORMED`, `${operation.toLowerCase()} response is malformed`) + } + remainingBytes -= result.bodyBytes + const body = safeContextRemoteSnapshot(context, result.body, `${operation}_MALFORMED`) + let pageRecords + if (field === undefined) { + if (!Array.isArray(body)) { + fail(`${operation}_MALFORMED`, `${operation.toLowerCase()} response is malformed`) + } + pageRecords = body + } else { + if (!isObject(body) || !Array.isArray(body[field])) { + fail(`${operation}_MALFORMED`, `${operation.toLowerCase()} response is malformed`) + } + pageRecords = body[field] + } + if (requireTotalCount) { + if ( + !isObject(body) || + !Number.isSafeInteger(body.total_count) || + body.total_count < 0 || + body.total_count > MAX_RECORDS + ) { + fail(`${operation}_MALFORMED`, `${operation.toLowerCase()} response is malformed`) + } + if (totalCount === null) totalCount = body.total_count + if (body.total_count !== totalCount) { + fail("PAGINATION_DRIFT", "Recovery read pagination total changed") + } + } + if (pageRecords.length > 100) { + fail(`${operation}_OVER_LIMIT`, `${operation.toLowerCase()} read exceeds the page size`) + } + if (records.length + pageRecords.length > MAX_RECORDS) { + fail(`${operation}_OVER_LIMIT`, `${operation.toLowerCase()} read exceeds the record limit`) + } + records.push(...pageRecords) + const links = linkRelations(result.headers?.link) + for (const link of links.values()) { + if (normalizePaginationPage(link, path) === null) { + fail("PAGINATION_DRIFT", "Recovery read pagination is unsafe") + } + } + const last = links.get("last") + const observedLastPage = + last === undefined ? null : (normalizePaginationPage(last, path)?.page ?? null) + if ( + advertisedLastPage !== null && + observedLastPage !== null && + observedLastPage !== advertisedLastPage + ) { + fail("PAGINATION_DRIFT", "Recovery read pagination last page changed") + } + if (advertisedLastPage === null && observedLastPage !== null) { + advertisedLastPage = observedLastPage + } + const next = links.get("next") ?? null + if (next === null) { + if (advertisedLastPage !== null && advertisedLastPage !== page + 1) { + fail("PAGINATION_DRIFT", "Recovery read pagination is incomplete") + } + if (requireTotalCount && records.length !== totalCount) { + fail("PAGINATION_DRIFT", "Recovery read pagination is incomplete") + } + return records + } + if (pageRecords.length !== 100 || (requireTotalCount && records.length >= totalCount)) { + fail("PAGINATION_DRIFT", "Recovery read pagination is inconsistent") + } + const normalized = normalizePaginationUrl(next, path, page + 2) + if (normalized === null) fail("PAGINATION_DRIFT", "Recovery read pagination is unsafe") + if (advertisedLastPage !== null && page + 2 > advertisedLastPage) { + fail("PAGINATION_DRIFT", "Recovery read pagination is inconsistent") + } + url = normalized + } + fail(`${operation}_OVER_LIMIT`, `${operation.toLowerCase()} read exceeds the page limit`) +} + +function normalizePaginationUrl(value, initialPath, expectedPage) { + const normalized = normalizePaginationPage(value, initialPath) + return normalized?.page === expectedPage ? normalized.href : null +} + +function normalizePaginationPage(value, initialPath) { + try { + const url = new URL(value) + const initial = new URL(`${API_ORIGIN}${initialPath}`) + if ( + url.origin !== API_ORIGIN || + url.username !== "" || + url.password !== "" || + url.hash !== "" || + ![initial.pathname, repositoryIdPath(initial.pathname)].includes(url.pathname) + ) { + return null + } + const expectedEntries = [...initial.searchParams.entries()] + const actualEntries = [...url.searchParams.entries()] + if (actualEntries.length !== expectedEntries.length + 1) { + return null + } + for (const [name, expected] of expectedEntries) { + const matches = actualEntries.filter(([actualName]) => actualName === name) + if (matches.length !== 1 || matches[0][1] !== expected) return null + } + const pageEntries = actualEntries.filter(([name]) => name === "page") + if (pageEntries.length !== 1 || !/^[1-9][0-9]*$/u.test(pageEntries[0][1])) return null + const page = Number(pageEntries[0][1]) + return Number.isSafeInteger(page) && page <= MAX_PAGES ? { href: url.href, page } : null + } catch { + return null + } +} + +function repositoryIdPath(pathname) { + const prefix = `/repos/${OWNER}/${REPOSITORY}` + return pathname.startsWith(prefix) + ? `/repositories/${REPOSITORY_ID}${pathname.slice(prefix.length)}` + : pathname +} + +function linkRelations(value) { + if (value === null || value === undefined) return new Map() + if (typeof value !== "string") fail("PAGINATION_DRIFT", "Recovery read pagination is unsafe") + const relations = new Map() + for (const part of value.split(",")) { + const match = /^\s*<([^>]+)>;\s*rel="([a-z]+)"\s*$/u.exec(part) + if (match === null || relations.has(match[2])) { + fail("PAGINATION_DRIFT", "Recovery read pagination is unsafe") + } + relations.set(match[2], match[1]) + } + return relations +} + +function githubHeaders(token, accept) { + return { + Accept: accept, + ...(token === null ? {} : { Authorization: `Bearer ${token}` }), + "X-GitHub-Api-Version": API_VERSION, + } +} + +function requirePresent(result, code, message) { + if (result?.status !== "PRESENT" || result.code !== null) fail(code, message) + return safeSnapshot(result.value ?? result.contentBase64, code) +} + +async function readBoundary(code, operation) { + try { + return await operation() + } catch (error) { + if (error instanceof DuplicateDraftRecoveryReadError) throw error + fail(code, "Recovery read boundary is unavailable") + } +} + +function safeSnapshot(value, code) { + try { + return snapshotJson(value) + } catch { + fail(code, "Recovery read response is malformed") + } +} + +function safeRemoteSnapshot(value, token, code) { + try { + return canonicalRemoteJson(snapshotJson(value), token) + } catch { + fail(code, "Recovery read response is malformed") + } +} + +function safeContextRemoteSnapshot(context, value, code) { + return context.strictCredentials === true + ? safeWriterRemoteSnapshot(value, context.token, code) + : safeRemoteSnapshot(value, context.token, code) +} + +function safeWriterRemoteSnapshot(value, token, code) { + let snapshot + try { + snapshot = snapshotJson(value) + } catch { + writeFail(code, "GitHub recovery response is malformed") + } + try { + return canonicalWriterRemoteJson(snapshot, token) + } catch { + writeFail("REMOTE_CREDENTIAL_CONFLICT", "GitHub recovery response is not credential-safe") + } +} + +function canonicalWriterRemoteJson(value, token) { + if (value === null || typeof value === "boolean" || typeof value === "number") return value + if (typeof value === "string") { + if (value.includes(token)) throw new TypeError("Credential occurrence") + return value + } + if (Array.isArray(value)) return value.map((item) => canonicalWriterRemoteJson(item, token)) + const normalized = {} + for (const key of Object.keys(value).sort()) { + if ( + UNSAFE_REMOTE_KEYS.has(key) || + /token|secret|authorization|cookie/iu.test(key) || + key.includes(token) + ) { + throw new TypeError("Unsafe remote response key") + } + Object.defineProperty(normalized, key, { + value: canonicalWriterRemoteJson(value[key], token), + enumerable: true, + configurable: false, + writable: false, + }) + } + return normalized +} + +function canonicalRemoteJson(value, token) { + if (value === null || typeof value === "boolean" || typeof value === "number") return value + if (typeof value === "string") { + return token === null ? value : value.split(token).join("[REDACTED]") + } + if (Array.isArray(value)) return value.map((item) => canonicalRemoteJson(item, token)) + const normalized = {} + for (const key of Object.keys(value).sort()) { + if ( + UNSAFE_REMOTE_KEYS.has(key) || + /token|secret|authorization|cookie/iu.test(key) || + (token !== null && key.includes(token)) + ) { + throw new TypeError("Unsafe remote response key") + } + Object.defineProperty(normalized, key, { + value: canonicalRemoteJson(value[key], token), + enumerable: true, + configurable: false, + writable: false, + }) + } + return normalized +} + +function assertSha(value, label) { + if (!isSha(value)) throw new TypeError(`${label} is invalid`) +} + +function assertPositiveInteger(value, label) { + if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${label} is invalid`) +} + +function isSha(value) { + return typeof value === "string" && SHA_PATTERN.test(value) +} + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value) +} + +function isTimestamp(value) { + if (typeof value !== "string" || !TIMESTAMP_PATTERN.test(value)) return false + const milliseconds = Date.parse(value) + if (!Number.isFinite(milliseconds)) return false + const canonical = new Date(milliseconds).toISOString() + return ( + value === canonical || + (canonical.endsWith(".000Z") && value === canonical.replace(".000Z", "Z")) + ) +} + +function isNullableTimestamp(value) { + return value === null || isTimestamp(value) +} + +function isBoundedText(value, maximumBytes, allowEmpty = false) { + return ( + typeof value === "string" && + (allowEmpty || value.length > 0) && + Buffer.byteLength(value) <= maximumBytes && + !/[\0\r]/u.test(value) + ) +} + +function coherentTerminalState(status, conclusion) { + return status === "completed" ? TERMINAL_CONCLUSIONS.has(conclusion) : conclusion === null +} + +function coherentRunState(run) { + return ( + coherentTerminalState(run.status, run.conclusion) && + (run.status === "completed" || run.status === "in_progress" + ? run.run_started_at !== null + : run.run_started_at === null) + ) +} + +function coherentJobState(job) { + return ( + coherentTerminalState(job.status, job.conclusion) && + (job.status === "completed" + ? job.started_at !== null && job.completed_at !== null + : job.completed_at === null && + (job.status === "in_progress" ? job.started_at !== null : job.started_at === null)) + ) +} + +function orderedTimestamps(...values) { + const timestamps = values.filter((value) => value !== null).map((value) => Date.parse(value)) + return timestamps.every((value, index) => index === 0 || timestamps[index - 1] <= value) +} + +function sha256(value) { + return createHash("sha256").update(value).digest("hex") +} + +function fail(code, message) { + throw new DuplicateDraftRecoveryReadError(code, message) +} + +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-recovery.mjs b/scripts/release/duplicate-draft-recovery.mjs new file mode 100644 index 000000000..122dfb11c --- /dev/null +++ b/scripts/release/duplicate-draft-recovery.mjs @@ -0,0 +1,2272 @@ +import { createHash } from "node:crypto" +import { isProxy } from "node:util/types" + +import { snapshotJson } from "./adapter-normalize.mjs" +import { CANONICAL_RELEASE_PACKAGE_ORDER } from "./manifest.mjs" +import { parseReleaseMarker } from "./metadata.mjs" + +const SHA256_PATTERN = /^[0-9a-f]{64}$/u +const GIT_SHA_PATTERN = /^[0-9a-f]{40}$/u +const ASSET_NAME_PATTERN = /^(?!\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._+-]{0,254}$/u +const MARKER_DELIMITER = "DAWN_RELEASE_CONTROLLER_MARKER" +// Single source of truth for the recovery archive/receipt asset bound. The writer +// enforces it on every upload; capture proves the canonical body fits before any +// mutation is possible. +export const MAX_ARCHIVE_ASSET_BYTES = 64 * 1024 +const MAX_NOTICE_BYTES = 16 * 1024 +const MAX_RECEIPT_BYTES = 64 * 1024 +const MAX_DUPLICATE_DRAFT_EVIDENCE_BYTES = 512 * 1024 +const RECOVERY_RELEASE_TITLE = "Dawn v0.8.22" +const RELEASE_PROJECTION_FIELDS = [ + "releaseId", + "tagName", + "title", + "targetCommitish", + "draft", + "prerelease", + "immutable", + "body", + "assets", +] +const DUPLICATE_EVIDENCE_FIELDS = [ + "schemaVersion", + "capturedAt", + "reviewedAuthority", + "repository", + "workflow", + "immutableReleases", + "candidate", + "npm", + "releaseRuns", + "releases", +] +const DUPLICATE_OBSERVATION_FIELDS = DUPLICATE_EVIDENCE_FIELDS.filter( + (field) => field !== "schemaVersion", +) +const DUPLICATE_SOURCE_FIELDS = [...RELEASE_PROJECTION_FIELDS, "marker", "evidenceAssets"] +const DUPLICATE_DERIVED_FIELDS = [ + "originalBodySha256", + "originalAssets", + "baseAssetSetSha256", + "archiveAssetName", + "receiptAssetName", + "receiptSha256", + "receiptBytes", + "noticeBytes", + "state", + "remainingTransitions", +] +const MAX_DUPLICATE_EVIDENCE_AGE_MS = 15 * 60 * 1000 +const CANONICAL_OPAQUE_TAG = "untagged-be0ff4bee4ba43b521a9" +const CAPTURE_RUN_STATUSES = new Set([ + "requested", + "waiting", + "pending", + "queued", + "in_progress", + "completed", +]) +const CAPTURE_JOB_STATUSES = new Set(["waiting", "pending", "queued", "in_progress", "completed"]) +const CAPTURE_TERMINAL_CONCLUSIONS = new Set([ + "success", + "failure", + "neutral", + "cancelled", + "skipped", + "timed_out", + "action_required", + "stale", + "startup_failure", +]) +const CAPTURE_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/u +const CAPTURE_READER_METHODS = [ + "readReviewedMergeAuthority", + "readRepositoryState", + "readCandidateTag", + "readWorkflowState", + "readImmutableReleases", + "readReleaseRuns", + "readCandidatePublishJobs", + "readNpmAbsence", + "readReleaseSnapshot", + "listCandidateReleases", +] +const RECOVERY_READ_ERROR_CODES = new Set([ + "CANDIDATE_JOBS_MALFORMED", + "CANDIDATE_JOBS_OVER_LIMIT", + "CANDIDATE_JOBS_UNAVAILABLE", + "CANDIDATE_TAG_CONFLICT", + "CANDIDATE_TAG_MALFORMED", + "CANDIDATE_TAG_UNAVAILABLE", + "IMMUTABLE_RELEASES_DISABLED", + "IMMUTABLE_RELEASES_MALFORMED", + "IMMUTABLE_RELEASES_UNAVAILABLE", + "LOCAL_HEAD_UNAVAILABLE", + "NPM_VERSION_NOT_ABSENT", + "PAGINATION_DRIFT", + "RECOVERY_ASSET_BYTES_CONFLICT", + "RECOVERY_ASSET_CREDENTIAL_CONFLICT", + "RECOVERY_ASSET_UNAVAILABLE", + "RECOVERY_ASSET_UNEXPECTED", + "RECOVERY_BODY_ARCHIVE_CONFLICT", + "RELEASE_ASSETS_MALFORMED", + "RELEASE_ASSETS_OVER_LIMIT", + "RELEASE_ASSETS_UNAVAILABLE", + "RELEASE_LIST_MALFORMED", + "RELEASE_LIST_OVER_LIMIT", + "RELEASE_LIST_UNAVAILABLE", + "RELEASE_MALFORMED", + "RELEASE_RUNS_MALFORMED", + "RELEASE_RUNS_OVER_LIMIT", + "RELEASE_RUNS_UNAVAILABLE", + "RELEASE_UNAVAILABLE", + "RELEASE_WORKFLOW_CONFLICT", + "RELEASE_WORKFLOW_MALFORMED", + "RELEASE_WORKFLOW_UNAVAILABLE", + "REMOTE_MAIN_MALFORMED", + "REMOTE_MAIN_UNAVAILABLE", + "REPOSITORY_IDENTITY_CONFLICT", + "REPOSITORY_MALFORMED", + "REPOSITORY_UNAVAILABLE", + "REVIEWED_ASSOCIATED_PULL_REQUEST_MALFORMED", + "REVIEWED_ASSOCIATED_PULL_REQUEST_UNAVAILABLE", + "REVIEWED_CI_RUNS_MALFORMED", + "REVIEWED_CI_RUNS_OVER_LIMIT", + "REVIEWED_CI_RUNS_UNAVAILABLE", + "REVIEWED_COMMIT_MALFORMED", + "REVIEWED_COMMIT_NOT_LOCAL_HEAD", + "REVIEWED_COMMIT_NOT_REMOTE_MAIN", + "REVIEWED_HEAD_COMMIT_MALFORMED", + "REVIEWED_HEAD_COMMIT_UNAVAILABLE", + "REVIEWED_MERGE_COMMIT_MALFORMED", + "REVIEWED_MERGE_COMMIT_UNAVAILABLE", + "REVIEWED_PULL_REQUEST_AMBIGUOUS", + "REVIEWED_PULL_REQUEST_CONFLICT", + "REVIEWED_TREE_MISMATCH", + "REVIEWED_VALIDATE_CHECKS_MALFORMED", + "REVIEWED_VALIDATE_CHECKS_OVER_LIMIT", + "REVIEWED_VALIDATE_CHECKS_UNAVAILABLE", + "REVIEWED_VALIDATE_MALFORMED", + "REVIEWED_VALIDATE_NOT_SUCCESSFUL", +]) + +export const DUPLICATE_DRAFT_RECOVERY_POLICY = deepFreeze({ + repository: "cacheplane/dawnai", + version: "0.8.22", + candidateSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + canonicalReleaseId: 379991871, + duplicates: [ + { releaseId: 379982100, tagName: "untagged-a13939767dd2419ade01" }, + { releaseId: 379986168, tagName: "untagged-20706099efa3c38335a8" }, + ], +}) + +export class DuplicateDraftRecoveryCaptureError extends Error { + constructor(code, message) { + super(message) + this.name = "DuplicateDraftRecoveryCaptureError" + this.code = code + } +} + +/** + * Project a normalized recovery Release snapshot onto the exact application- + * owned fields fenced by the writer. Recovery-only bytes and evidence kinds + * are intentionally excluded. + */ +export function normalizeDuplicateDraftReleaseProjection(value) { + const source = snapshotJson(value) + if (!isRecord(source)) throw new TypeError("Recovery Release projection is invalid") + assertReleaseId(source.releaseId, "Recovery Release ID") + if ( + typeof source.tagName !== "string" || + source.tagName.length === 0 || + source.title !== RECOVERY_RELEASE_TITLE || + source.targetCommitish !== "main" || + source.draft !== true || + source.prerelease !== false || + source.immutable !== false || + typeof source.body !== "string" || + !Array.isArray(source.assets) + ) { + throw new TypeError("Recovery Release projection metadata is not exact") + } + const assets = source.assets.map((asset, index) => { + const normalized = snapshotJson(asset) + if ( + !isRecord(normalized) || + !Number.isSafeInteger(normalized.id) || + normalized.id < 1 || + typeof normalized.name !== "string" || + !ASSET_NAME_PATTERN.test(normalized.name) || + typeof normalized.sha256 !== "string" || + !SHA256_PATTERN.test(normalized.sha256) || + !Number.isSafeInteger(normalized.size) || + normalized.size < 1 + ) { + throw new TypeError(`Recovery Release projection asset[${index}] is invalid`) + } + return { + id: normalized.id, + name: normalized.name, + sha256: normalized.sha256, + size: normalized.size, + } + }) + assertUniqueAssets(assets, "recovery Release projection assets") + return deepFreeze({ + releaseId: source.releaseId, + tagName: source.tagName, + title: source.title, + targetCommitish: source.targetCommitish, + draft: source.draft, + prerelease: source.prerelease, + immutable: source.immutable, + body: source.body, + assets, + }) +} + +/** Hash the exact canonical writer-fence projection without transport fields. */ +export function duplicateDraftReleaseProjectionSha256(value) { + const projection = normalizeDuplicateDraftReleaseProjection(value) + return sha256(JSON.stringify(canonicalize(projection))) +} + +/** Validate and preserve the exact frozen recovery-reader capability surface. */ +export function assertDuplicateDraftRecoveryReader(value) { + assertCaptureReader(value) + return value +} + +/** Collect and seal one complete, read-only production recovery observation. */ +export async function captureDuplicateDraftRecoveryEvidence({ + reviewedCommit, + reader, + now = Date.now, +}) { + assertGitSha(reviewedCommit, "Reviewed recovery commit") + assertDuplicateDraftRecoveryReader(reader) + if (typeof now !== "function") throw new TypeError("Duplicate draft capture clock is invalid") + const capturedAtMs = now() + if (!Number.isSafeInteger(capturedAtMs) || capturedAtMs < 0) { + throw new TypeError("Duplicate draft capture time is invalid") + } + if (capturedAtMs > 8_640_000_000_000_000) { + throw new TypeError("Duplicate draft capture time is invalid") + } + const capturedAt = new Date(capturedAtMs).toISOString() + + const reviewedAuthority = await captureRead( + reader, + "readReviewedMergeAuthority", + [reviewedCommit], + "REVIEWED_AUTHORITY_UNAVAILABLE", + ) + if (reviewedAuthority?.mergeCommitSha !== reviewedCommit) { + captureFail( + "REVIEWED_AUTHORITY_CONFLICT", + "Reviewed recovery authority does not match the supplied commit", + ) + } + const repository = await captureRead( + reader, + "readRepositoryState", + [], + "REPOSITORY_STATE_UNAVAILABLE", + ) + const workflow = await captureRead(reader, "readWorkflowState", [], "WORKFLOW_STATE_UNAVAILABLE") + const immutableReleases = await captureRead( + reader, + "readImmutableReleases", + [], + "IMMUTABLE_RELEASES_UNAVAILABLE", + ) + const candidate = await captureRead(reader, "readCandidateTag", [], "CANDIDATE_TAG_UNAVAILABLE") + const runObservation = await captureRead( + reader, + "readReleaseRuns", + [], + "RELEASE_RUNS_UNAVAILABLE", + ) + let normalizedRuns + try { + normalizedRuns = normalizeCaptureRuns(runObservation) + } catch (error) { + if (error instanceof DuplicateDraftRecoveryCaptureError) throw error + captureFail("RELEASE_RUNS_MALFORMED", "Recovery workflow run observation is malformed") + } + const { nonterminalRuns, candidateRuns } = normalizedRuns + if (nonterminalRuns.length !== 0) { + captureFail("RELEASE_RUN_NONTERMINAL", "A Release workflow run is nonterminal") + } + const candidateJobObservations = [] + for (const run of candidateRuns) { + candidateJobObservations.push({ + runId: run.id, + runAttempt: run.runAttempt, + jobs: await captureRead( + reader, + "readCandidatePublishJobs", + [run.id, run.runAttempt], + "CANDIDATE_JOBS_UNAVAILABLE", + ), + }) + } + for (const { jobs, runId, runAttempt } of candidateJobObservations) { + assertNoStartedPublishJob(jobs, runId, runAttempt) + } + + const npmPackages = [] + for (const packageName of CANONICAL_RELEASE_PACKAGE_ORDER) { + npmPackages.push( + await captureRead(reader, "readNpmAbsence", [packageName], "NPM_ABSENCE_UNAVAILABLE"), + ) + } + + const candidateReleases = await captureRead( + reader, + "listCandidateReleases", + [], + "CANDIDATE_RELEASES_UNAVAILABLE", + ) + try { + normalizeCandidateReleaseInventory(candidateReleases) + } catch (error) { + if (error instanceof DuplicateDraftRecoveryCaptureError) throw error + captureFail("CANDIDATE_RELEASE_INVENTORY_CONFLICT", "Candidate Release inventory is not exact") + } + const canonical = await captureRead( + reader, + "readReleaseSnapshot", + [DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId], + "CANONICAL_RELEASE_UNAVAILABLE", + ) + // The canonical body becomes each duplicate's original-body archive asset, which + // the writer bounds at MAX_ARCHIVE_ASSET_BYTES. Prove that here so an oversized + // body fails at capture rather than part-way through a frozen production window. + if ( + typeof canonical?.body === "string" && + Buffer.byteLength(canonical.body, "utf8") > MAX_ARCHIVE_ASSET_BYTES + ) { + captureFail( + "CANONICAL_BODY_OVER_ARCHIVE_LIMIT", + "Canonical Release body exceeds the recovery archive asset limit", + ) + } + const duplicates = [] + for (const duplicate of DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates) { + duplicates.push( + await captureRead( + reader, + "readReleaseSnapshot", + [duplicate.releaseId, { expectedOriginalBody: canonical?.body }], + "DUPLICATE_RELEASE_UNAVAILABLE", + ), + ) + } + + try { + return parseDuplicateDraftEvidence( + canonicalDuplicateDraftEvidence({ + capturedAt, + reviewedAuthority, + repository, + workflow, + immutableReleases, + candidate, + npm: { packages: npmPackages }, + releaseRuns: [], + releases: { canonical, duplicates }, + }), + ) + } catch (error) { + if (error instanceof DuplicateDraftRecoveryCaptureError) throw error + captureFail("CAPTURE_EVIDENCE_CONFLICT", "Captured duplicate draft evidence is not exact") + } +} + +/** + * Converge the two candidate-pinned duplicate drafts through their recognized + * partial states. Every individual mutation is preceded by a complete fresh + * capture; no failed or ambiguous mutation is retried by this orchestrator. + */ +export async function applyDuplicateDraftRecovery({ + evidence, + concurrencyAcknowledgement, + reader, + createWriter, + observer, + now = Date.now, +}) { + const acknowledgement = normalizeConcurrencyAcknowledgement(concurrencyAcknowledgement) + if (typeof createWriter !== "function") { + throw new TypeError("Duplicate draft recovery writer factory is invalid") + } + if (typeof observer !== "function") { + throw new TypeError("Duplicate draft recovery observer is invalid") + } + if (typeof now !== "function") throw new TypeError("Duplicate draft recovery clock is invalid") + + const startedAtMs = readApplyTime(now) + const sealed = parseDuplicateDraftEvidence(canonicalDuplicateDraftEvidence(evidence)) + assertApplyEvidenceFresh(sealed, startedAtMs) + const expectedSources = sealed.releases.duplicates.map(duplicateSource) + const performed = new Map() + const results = [] + let writer + let chronologyWatermarkMs = startedAtMs + + for (const [index, configured] of DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates.entries()) { + let lastAuthorization + while (true) { + const fresh = await captureDuplicateDraftRecoveryEvidence({ + reviewedCommit: sealed.reviewedAuthority.mergeCommitSha, + reader, + now, + }) + assertApplyAuthorization(sealed, fresh, expectedSources) + const authorizationMs = readApplyTime(now) + assertFreshAuthorizationTimeline({ + sealed, + fresh, + authorizationMs, + previousAuthorizationMs: chronologyWatermarkMs, + }) + chronologyWatermarkMs = authorizationMs + lastAuthorization = fresh + const duplicate = fresh.releases.duplicates[index] + if (duplicate.releaseId !== configured.releaseId) { + throw new Error("Duplicate draft recovery order changed") + } + if (index > 0 && fresh.releases.duplicates[index - 1].state !== "quarantined") { + throw new Error("The prior duplicate Release is not exactly quarantined") + } + if (duplicate.state === "quarantined") break + + writer ??= normalizeApplyWriter(createWriter()) + const mutationAuthorizationMs = readApplyTime(now) + assertMutationAuthorizationTimeline({ + sealed, + fresh, + authorizationMs: mutationAuthorizationMs, + previousAuthorizationMs: chronologyWatermarkMs, + }) + chronologyWatermarkMs = mutationAuthorizationMs + if (duplicate.state === "untouched") { + const receipt = await writer.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: duplicateSource(duplicate), + expectedTagObjectSha: sealed.candidate.tagObjectSha, + name: duplicate.archiveAssetName, + bytes: Buffer.from(sealed.releases.canonical.body, "utf8"), + sha256: duplicate.originalBodySha256, + }) + const normalized = normalizeUploadMutationReceipt(receipt, { + releaseId: duplicate.releaseId, + name: duplicate.archiveAssetName, + sha256: duplicate.originalBodySha256, + }) + expectedSources[index] = { + ...duplicateSource(duplicate), + assets: [ + ...duplicate.assets, + { + id: normalized.assetId, + name: normalized.name, + sha256: normalized.sha256, + size: Buffer.byteLength(sealed.releases.canonical.body, "utf8"), + }, + ], + evidenceAssets: ["body"], + } + continue + } + if (duplicate.state === "body-archived") { + const receipt = await writer.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: duplicateSource(duplicate), + expectedTagObjectSha: sealed.candidate.tagObjectSha, + name: duplicate.receiptAssetName, + bytes: Buffer.from(duplicate.receiptBytes, "utf8"), + sha256: duplicate.receiptSha256, + }) + const normalized = normalizeUploadMutationReceipt(receipt, { + releaseId: duplicate.releaseId, + name: duplicate.receiptAssetName, + sha256: duplicate.receiptSha256, + }) + expectedSources[index] = { + ...duplicateSource(duplicate), + assets: [ + ...duplicate.assets, + { + id: normalized.assetId, + name: normalized.name, + sha256: normalized.sha256, + size: Buffer.byteLength(duplicate.receiptBytes, "utf8"), + bytes: duplicate.receiptBytes, + }, + ], + evidenceAssets: ["body", "receipt"], + } + continue + } + if (duplicate.state === "receipt-archived") { + const mutation = normalizeQuarantineMutationReceipt( + await writer.quarantineDuplicateBodyIfCurrent({ + expectedSnapshot: duplicateSource(duplicate), + expectedTagObjectSha: sealed.candidate.tagObjectSha, + expectedBodySha256: duplicate.originalBodySha256, + expectedNotice: duplicate.noticeBytes, + }), + duplicate.releaseId, + sealed.candidate.tagObjectSha, + mutationAuthorizationMs, + ) + performed.set(duplicate.releaseId, mutation) + chronologyWatermarkMs = Date.parse(mutation.postWriteFence.observedAt) + expectedSources[index] = { + ...duplicateSource(duplicate), + body: duplicate.noticeBytes, + marker: null, + } + continue + } + throw new Error("Duplicate Release state is not resumable") + } + + const exactDuplicate = lastAuthorization.releases.duplicates[index] + const mutation = performed.get(configured.releaseId) + results.push( + mutation ?? + deepFreeze({ + releaseId: configured.releaseId, + outcome: "preexisting-quarantined", + priorFenceObservations: null, + verifiedAt: lastAuthorization.capturedAt, + projectionSha256: duplicateDraftReleaseProjectionSha256(exactDuplicate), + }), + ) + } + + const finalAuthorization = normalizeFinalRecoveryObservation( + await observer({ + candidate: { + version: DUPLICATE_DRAFT_RECOVERY_POLICY.version, + commitSha: DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha, + }, + }), + ) + const appliedAtMs = readApplyTime(now) + assertFinalReceiptTimeline(results, chronologyWatermarkMs, appliedAtMs) + const appliedAt = new Date(appliedAtMs).toISOString() + return deepFreeze({ + schemaVersion: 1, + atomic: false, + concurrencyAcknowledgement: acknowledgement, + freezeScope: { + mode: acknowledgement.mode, + releaseIds: [...acknowledgement.releaseIds], + }, + evidenceCapturedAt: sealed.capturedAt, + appliedAt, + candidate: { + version: DUPLICATE_DRAFT_RECOVERY_POLICY.version, + commitSha: DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha, + releaseId: DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId, + }, + duplicates: results, + finalAuthorization, + }) +} + +/** + * Serialize the authority-bound recovery observation into its sole canonical + * representation. It performs no I/O and accepts neither credentials nor + * transport data. + */ +export function canonicalDuplicateDraftEvidence(value) { + const evidence = normalizeDuplicateDraftEvidence(value) + const bytes = Buffer.from(`${JSON.stringify(canonicalize(evidence))}\n`, "utf8") + if (bytes.byteLength > MAX_DUPLICATE_DRAFT_EVIDENCE_BYTES) { + throw new TypeError("Duplicate draft evidence exceeds its byte bounds") + } + return bytes +} + +/** Parse only canonical, bounded evidence bytes and return an immutable value. */ +export function parseDuplicateDraftEvidence(bytes) { + const input = normalizeEvidenceBytes(bytes) + let parsed + try { + parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input)) + } catch (error) { + throw new TypeError("Duplicate draft evidence is not valid UTF-8 JSON", { cause: error }) + } + const evidence = normalizeDuplicateDraftEvidence(parsed) + const canonical = canonicalDuplicateDraftEvidence(evidence) + if (!canonical.equals(input)) throw new TypeError("Duplicate draft evidence is not canonical") + return deepFreeze(evidence) +} + +/** + * Reparse the sealed value, verify its short validity interval, and prove a + * fresh observation derives byte-for-byte identical facts. Drift is never + * repaired or inferred by the verifier. + */ +export function verifyDuplicateDraftEvidence({ evidence, current, now = Date.now }) { + if (typeof now !== "function") throw new TypeError("Duplicate draft evidence clock is invalid") + const sealed = parseDuplicateDraftEvidence(canonicalDuplicateDraftEvidence(evidence)) + const nowMs = now() + if (!Number.isSafeInteger(nowMs) || nowMs < 0) { + throw new TypeError("Duplicate draft evidence time is invalid") + } + const capturedAtMs = Date.parse(sealed.capturedAt) + if (capturedAtMs > nowMs) + throw new Error("Duplicate draft evidence capture time is in the future") + if (nowMs - capturedAtMs > MAX_DUPLICATE_EVIDENCE_AGE_MS) { + throw new Error("Duplicate draft evidence has expired and must be recaptured") + } + const currentObservation = validateCurrentObservationTimestamp(current, { + evidenceCapturedAtMs: capturedAtMs, + nowMs, + }) + const fresh = normalizeDuplicateDraftObservation(currentObservation, sealed.capturedAt) + if (!canonicalDuplicateDraftEvidence(fresh).equals(canonicalDuplicateDraftEvidence(sealed))) { + throw new Error("Duplicate draft evidence drifted from the fresh observation") + } + return deepFreeze({ schemaVersion: 1, status: "PASS" }) +} + +export function classifyDuplicateDraft(value, expected) { + const snapshot = exactObject(value, DUPLICATE_SOURCE_FIELDS, "duplicate Release snapshot") + normalizeDuplicateDraftReleaseProjection(snapshot) + const requirements = exactObject( + expected, + [ + "releaseId", + "tagName", + "canonicalBody", + "canonicalMarker", + "originalBodySha256", + "originalAssets", + "recoveryReceipt", + "recoveryNotice", + ], + "duplicate Release expectations", + ) + + assertDuplicateIdentity(snapshot.releaseId, snapshot.tagName, requirements) + assertBodyDigest(requirements.canonicalBody, requirements.originalBodySha256) + if (!Array.isArray(requirements.originalAssets) || requirements.originalAssets.length !== 45) { + throw new TypeError("Duplicate Release expectations require exactly 45 original assets") + } + const originalAssets = normalizeAssets( + requirements.originalAssets, + "original Release assets", + false, + ) + assertUniqueAssets(originalAssets, "original Release assets") + const assets = normalizeAssets(snapshot.assets, "duplicate Release assets", true) + assertUniqueAssets(assets, "duplicate Release assets") + const evidenceKinds = normalizeEvidenceKinds(snapshot.evidenceAssets) + let parsedCanonicalMarker + try { + parsedCanonicalMarker = parseReleaseMarker(requirements.canonicalBody) + } catch (error) { + throw new Error("Duplicate canonical body is not a valid Dawn release body", { cause: error }) + } + if ( + parsedCanonicalMarker.phase !== "ESCROWED" || + parsedCanonicalMarker.version !== DUPLICATE_DRAFT_RECOVERY_POLICY.version || + parsedCanonicalMarker.commitSha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha || + parsedCanonicalMarker.tag !== `v${DUPLICATE_DRAFT_RECOVERY_POLICY.version}` || + !sameJson(parsedCanonicalMarker, requirements.canonicalMarker) + ) { + throw new Error("Duplicate canonical body marker is not the approved ESCROWED marker") + } + const expectedBaseAssetSetSha256 = assetSetSha256(originalAssets) + if (parsedCanonicalMarker.baseAssetSetSha256 !== expectedBaseAssetSetSha256) { + throw new Error("Duplicate canonical marker base-asset digest is not exact") + } + const bodyAssetName = originalBodyAssetName( + requirements.releaseId, + requirements.originalBodySha256, + ) + const receiptAssetName = recoveryReceiptAssetName(requirements.releaseId) + const recoveryReceiptInput = exactObject( + requirements.recoveryReceipt, + [ + "repository", + "version", + "candidateSha", + "recoveryCommit", + "canonicalReleaseId", + "duplicateReleaseId", + "originalBodySha256", + "baseAssetSetSha256", + "archiveAsset", + ], + "duplicate recovery receipt", + ) + if ( + recoveryReceiptInput.repository !== DUPLICATE_DRAFT_RECOVERY_POLICY.repository || + recoveryReceiptInput.version !== DUPLICATE_DRAFT_RECOVERY_POLICY.version || + recoveryReceiptInput.candidateSha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha || + recoveryReceiptInput.canonicalReleaseId !== + DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId || + recoveryReceiptInput.duplicateReleaseId !== requirements.releaseId || + recoveryReceiptInput.originalBodySha256 !== requirements.originalBodySha256 || + recoveryReceiptInput.baseAssetSetSha256 !== expectedBaseAssetSetSha256 || + recoveryReceiptInput.archiveAsset.name !== bodyAssetName || + recoveryReceiptInput.archiveAsset.sha256 !== requirements.originalBodySha256 + ) { + throw new Error("Duplicate recovery receipt identity is not bound to the classified draft") + } + const recoveryReceiptBytes = canonicalRecoveryReceipt(recoveryReceiptInput) + const recoveryReceiptSha256 = sha256Bytes(recoveryReceiptBytes) + const expectedAssetNames = new Set(originalAssets.map((asset) => asset.name)) + for (const asset of assets) { + if ( + !expectedAssetNames.has(asset.name) && + asset.name !== bodyAssetName && + asset.name !== receiptAssetName + ) { + throw new Error("Duplicate Release has an unexpected asset") + } + } + if (assets.length !== originalAssets.length + evidenceKinds.length) { + throw new Error("Duplicate Release asset namespace is not exact") + } + const originalActual = assets.slice(0, originalAssets.length) + if (!sameJson(assetSizeNamespace(originalActual), assetSizeNamespace(originalAssets))) { + throw new Error("Duplicate Release original asset namespace changed") + } + const evidence = assets.slice(originalAssets.length) + const notice = + evidenceKinds.length === 2 && snapshot.marker === null + ? parseCanonicalNotice( + snapshot.body, + requirements.releaseId, + requirements.originalBodySha256, + bodyAssetName, + ) + : null + for (const [index, asset] of evidence.entries()) { + const kind = evidenceKinds[index] + if (kind === "body") { + if ( + asset.name !== bodyAssetName || + asset.sha256 !== requirements.originalBodySha256 || + asset.size !== Buffer.byteLength(requirements.canonicalBody, "utf8") || + (notice !== null && + (asset.name !== notice.archiveAssetName || asset.sha256 !== notice.originalBodySha256)) + ) { + throw new Error("Duplicate Release original-body archive is not exact") + } + } else if ( + asset.name !== receiptAssetName || + asset.sha256 !== recoveryReceiptSha256 || + asset.size !== recoveryReceiptBytes.byteLength || + typeof asset.bytes !== "string" || + asset.bytes !== recoveryReceiptBytes.toString("utf8") || + (notice !== null && asset.sha256 !== notice.receiptSha256) + ) { + throw new Error("Duplicate Release recovery receipt asset is not exact") + } + } + + if (!sameJson(snapshot.marker, parsedCanonicalMarker)) { + if (evidenceKinds.length === 2 && snapshot.marker === null) { + // The quarantine state intentionally has no live Dawn marker. + } else { + throw new Error("Duplicate Release marker is not canonical") + } + } + if (evidenceKinds.length === 2 && snapshot.marker === null) { + if (snapshot.body !== requirements.recoveryNotice || !isCanonicalNotice(snapshot.body)) { + throw new Error("Duplicate Release recovery notice is malformed") + } + return "quarantined" + } + if (snapshot.body !== requirements.canonicalBody) { + throw new Error("Duplicate Release original body changed") + } + if (evidenceKinds.length === 0) return "untouched" + if (evidenceKinds.length === 1 && evidenceKinds[0] === "body") return "body-archived" + if (evidenceKinds.length === 2 && evidenceKinds[0] === "body" && evidenceKinds[1] === "receipt") { + return "receipt-archived" + } + throw new Error("Duplicate Release state is unknown") +} + +export function originalBodyAssetName(releaseId, bodySha256) { + assertDuplicateReleaseId(releaseId) + assertSha256(bodySha256, "Original body SHA-256") + return `dawn-v${DUPLICATE_DRAFT_RECOVERY_POLICY.version}-duplicate-${releaseId}-original-body-${bodySha256}.txt` +} + +export function recoveryReceiptAssetName(releaseId) { + assertDuplicateReleaseId(releaseId) + return `dawn-v${DUPLICATE_DRAFT_RECOVERY_POLICY.version}-duplicate-${releaseId}-recovery-receipt.json` +} + +export function canonicalRecoveryReceipt(input) { + const source = exactObject( + input, + [ + "repository", + "version", + "candidateSha", + "recoveryCommit", + "canonicalReleaseId", + "duplicateReleaseId", + "originalBodySha256", + "baseAssetSetSha256", + "archiveAsset", + ], + "recovery receipt", + ) + assertPolicyIdentity(source) + assertGitSha(source.recoveryCommit, "Recovery commit") + assertReleaseId(source.canonicalReleaseId, "Canonical Release ID") + if (source.canonicalReleaseId !== DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId) { + throw new Error("Recovery receipt canonical Release ID is not approved") + } + assertDuplicateReleaseId(source.duplicateReleaseId) + assertSha256(source.originalBodySha256, "Original body SHA-256") + assertSha256(source.baseAssetSetSha256, "Base asset set SHA-256") + const archiveAsset = exactObject(source.archiveAsset, ["name", "sha256"], "archive asset") + if ( + archiveAsset.name !== + originalBodyAssetName(source.duplicateReleaseId, source.originalBodySha256) || + archiveAsset.sha256 !== source.originalBodySha256 + ) { + throw new Error("Recovery receipt archive asset is not derived from the candidate") + } + const record = { + schemaVersion: 1, + repository: source.repository, + version: source.version, + candidateSha: source.candidateSha, + recoveryCommit: source.recoveryCommit, + canonicalReleaseId: source.canonicalReleaseId, + duplicateReleaseId: source.duplicateReleaseId, + originalBodySha256: source.originalBodySha256, + baseAssetSetSha256: source.baseAssetSetSha256, + archiveAsset, + } + const bytes = Buffer.from(`${JSON.stringify(canonicalize(record))}\n`, "utf8") + if (bytes.byteLength > MAX_RECEIPT_BYTES) + throw new Error("Recovery receipt exceeds its byte limit") + return bytes +} + +export function canonicalRecoveryNotice(input) { + const source = exactObject( + input, + [ + "repository", + "version", + "canonicalReleaseId", + "duplicateReleaseId", + "originalBodySha256", + "archiveAssetName", + "receiptAssetName", + "receiptSha256", + ], + "recovery notice", + ) + if (source.repository !== DUPLICATE_DRAFT_RECOVERY_POLICY.repository) { + throw new Error("Recovery notice repository is not approved") + } + if (source.version !== DUPLICATE_DRAFT_RECOVERY_POLICY.version) { + throw new Error("Recovery notice version is not approved") + } + assertReleaseId(source.canonicalReleaseId, "Canonical Release ID") + if (source.canonicalReleaseId !== DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId) { + throw new Error("Recovery notice canonical Release ID is not approved") + } + assertDuplicateReleaseId(source.duplicateReleaseId) + assertSha256(source.originalBodySha256, "Original body SHA-256") + if ( + source.archiveAssetName !== + originalBodyAssetName(source.duplicateReleaseId, source.originalBodySha256) + ) { + throw new Error("Recovery notice archive asset is not derived from the candidate") + } + if (source.receiptAssetName !== recoveryReceiptAssetName(source.duplicateReleaseId)) { + throw new Error("Recovery notice receipt asset is not derived from the candidate") + } + assertSha256(source.receiptSha256, "Recovery receipt SHA-256") + const notice = { + schemaVersion: 1, + type: "DAWN_DUPLICATE_DRAFT_RECOVERY", + repository: source.repository, + version: source.version, + candidateSha: DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha, + canonicalReleaseId: source.canonicalReleaseId, + duplicateReleaseId: source.duplicateReleaseId, + originalBodySha256: source.originalBodySha256, + archiveAssetName: source.archiveAssetName, + receiptAssetName: source.receiptAssetName, + receiptSha256: source.receiptSha256, + } + const text = `${JSON.stringify(canonicalize(notice))}\n` + if (text.includes(MARKER_DELIMITER)) throw new Error("Recovery notice contains a Dawn marker") + if (Buffer.byteLength(text, "utf8") > MAX_NOTICE_BYTES) { + throw new Error("Recovery notice exceeds its byte limit") + } + return text +} + +function normalizeDuplicateDraftEvidence(value) { + const source = snapshotJson(value) + if (!isRecord(source)) throw new TypeError("Duplicate draft evidence must be an object") + if (!Object.hasOwn(source, "schemaVersion")) return normalizeDuplicateDraftObservation(source) + exactObject(source, DUPLICATE_EVIDENCE_FIELDS, "duplicate draft evidence") + if (source.schemaVersion !== 1) throw new TypeError("Duplicate draft evidence schema is invalid") + const rawDuplicates = normalizeDuplicateEvidenceDuplicates(source.releases) + const rebuilt = normalizeDuplicateDraftObservation({ + capturedAt: source.capturedAt, + reviewedAuthority: source.reviewedAuthority, + repository: source.repository, + workflow: source.workflow, + immutableReleases: source.immutableReleases, + candidate: source.candidate, + npm: source.npm, + releaseRuns: source.releaseRuns, + releases: { canonical: source.releases.canonical, duplicates: rawDuplicates }, + }) + if (!sameJson(source, rebuilt)) { + throw new TypeError("Duplicate draft evidence contains caller-trusted derived fields") + } + return rebuilt +} + +function normalizeDuplicateDraftObservation(value, capturedAtOverride) { + const source = exactObject(value, DUPLICATE_OBSERVATION_FIELDS, "duplicate draft observation") + const capturedAt = normalizeCanonicalTimestamp( + capturedAtOverride === undefined ? source.capturedAt : capturedAtOverride, + "Duplicate draft evidence capture time", + ) + if (capturedAtOverride !== undefined) { + normalizeCanonicalTimestamp( + source.capturedAt, + "Current duplicate draft observation capture time", + ) + } + const reviewedAuthority = normalizeReviewedAuthority(source.reviewedAuthority) + const repository = normalizeRecoveryRepository( + source.repository, + reviewedAuthority.mergeCommitSha, + ) + const workflow = normalizeRecoveryWorkflow(source.workflow) + const immutableReleases = normalizeImmutableReleases(source.immutableReleases) + const candidate = normalizeRecoveryCandidate(source.candidate) + const npm = normalizeNpmAbsence(source.npm, candidate.version) + const releaseRuns = normalizeReleaseRuns(source.releaseRuns) + const releases = normalizeRecoveryReleases(source.releases, { reviewedAuthority, candidate }) + return { + schemaVersion: 1, + capturedAt, + reviewedAuthority, + repository, + workflow, + immutableReleases, + candidate, + npm, + releaseRuns, + releases, + } +} + +function validateCurrentObservationTimestamp(value, { evidenceCapturedAtMs, nowMs }) { + const source = exactObject( + value, + DUPLICATE_OBSERVATION_FIELDS, + "current duplicate draft observation", + ) + const capturedAt = normalizeCanonicalTimestamp( + source.capturedAt, + "Current duplicate draft observation capture time", + ) + const capturedAtMs = Date.parse(capturedAt) + if (capturedAtMs > nowMs) { + throw new Error("Current duplicate draft observation capture time is in the future") + } + if (nowMs - capturedAtMs > MAX_DUPLICATE_EVIDENCE_AGE_MS) { + throw new Error("Current duplicate draft observation has expired and must be recaptured") + } + if (capturedAtMs < evidenceCapturedAtMs) { + throw new Error("Current duplicate draft observation predates the sealed evidence") + } + return source +} + +function normalizeReviewedAuthority(value) { + const source = exactObject( + value, + [ + "mergeCommitSha", + "mergeTreeSha", + "pullRequestNumber", + "reviewedHeadSha", + "reviewedTreeSha", + "validateRunId", + ], + "reviewed recovery authority", + ) + assertGitSha(source.mergeCommitSha, "Recovery merge commit") + assertGitSha(source.mergeTreeSha, "Recovery merge tree") + assertGitSha(source.reviewedHeadSha, "Reviewed pull request head") + assertGitSha(source.reviewedTreeSha, "Reviewed pull request tree") + assertPositiveInteger(source.pullRequestNumber, "Reviewed pull request number") + assertPositiveInteger(source.validateRunId, "Reviewed validate run ID") + if (source.mergeTreeSha !== source.reviewedTreeSha) { + throw new TypeError("Reviewed and merged recovery trees must be identical") + } + return { + mergeCommitSha: source.mergeCommitSha, + mergeTreeSha: source.mergeTreeSha, + pullRequestNumber: source.pullRequestNumber, + reviewedHeadSha: source.reviewedHeadSha, + reviewedTreeSha: source.reviewedTreeSha, + validateRunId: source.validateRunId, + } +} + +function normalizeRecoveryRepository(value, mergeCommitSha) { + const source = exactObject(value, ["id", "nameWithOwner", "mainSha"], "recovery repository") + assertGitSha(source.mainSha, "Recovery repository main SHA") + if ( + source.id !== 1210070282 || + source.nameWithOwner !== DUPLICATE_DRAFT_RECOVERY_POLICY.repository || + source.mainSha !== mergeCommitSha + ) { + throw new TypeError("Recovery repository identity is not exact") + } + return { id: source.id, nameWithOwner: source.nameWithOwner, mainSha: source.mainSha } +} + +function normalizeRecoveryWorkflow(value) { + const source = exactObject(value, ["id", "state"], "recovery workflow") + if (source.id !== 260503756 || source.state !== "disabled_manually") { + throw new TypeError("Recovery workflow is not the disabled Release workflow") + } + return { id: 260503756, state: "disabled_manually" } +} + +function normalizeImmutableReleases(value) { + const source = exactObject(value, ["enabled"], "immutable Releases evidence") + if (source.enabled !== true) throw new TypeError("Immutable Releases must be enabled") + return { enabled: true } +} + +function normalizeRecoveryCandidate(value) { + const source = exactObject(value, ["version", "commitSha", "tagObjectSha"], "recovery candidate") + assertGitSha(source.tagObjectSha, "Recovery candidate annotated tag object") + if ( + source.version !== DUPLICATE_DRAFT_RECOVERY_POLICY.version || + source.commitSha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha + ) { + throw new TypeError("Recovery candidate identity is not exact") + } + return { version: source.version, commitSha: source.commitSha, tagObjectSha: source.tagObjectSha } +} + +function normalizeNpmAbsence(value, version) { + const source = exactObject(value, ["packages"], "npm absence evidence") + if (!Array.isArray(source.packages) || source.packages.length !== 21) { + throw new TypeError("npm absence evidence package inventory is invalid") + } + const packageOrder = CANONICAL_RELEASE_PACKAGE_ORDER + return { + packages: source.packages.map((entry, index) => { + const item = exactObject(entry, ["name", "version", "status"], "npm absence package") + if ( + item.name !== packageOrder[index] || + item.version !== version || + item.status !== "absent" + ) { + throw new TypeError("npm absence evidence is not exact") + } + return { name: item.name, version: item.version, status: "absent" } + }), + } +} + +function normalizeReleaseRuns(value) { + if (!Array.isArray(value) || value.length !== 0) { + throw new TypeError("Recovery evidence requires no release workflow runs") + } + return [] +} + +function normalizeRecoveryReleases(value, { reviewedAuthority, candidate }) { + const source = exactObject(value, ["canonical", "duplicates"], "recovery Releases evidence") + const canonical = normalizeCanonicalRecoveryRelease(source.canonical, candidate) + if ( + !Array.isArray(source.duplicates) || + source.duplicates.length !== DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates.length + ) { + throw new TypeError("Recovery duplicate Release inventory is invalid") + } + const originalBodySha256 = sha256(canonical.body) + const originalAssets = canonical.assets.map(({ name, sha256: digest }) => ({ + name, + sha256: digest, + })) + const baseAssetSetSha256 = assetSetSha256(canonical.assets) + const duplicates = source.duplicates.map((value, index) => { + const raw = normalizeRecoveryDuplicateSource(value, canonical.assets) + const configured = DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates[index] + if (raw.releaseId !== configured.releaseId || raw.tagName !== configured.tagName) { + throw new TypeError("Recovery duplicate Release order or identity is not exact") + } + const recoveryReceipt = { + repository: DUPLICATE_DRAFT_RECOVERY_POLICY.repository, + version: candidate.version, + candidateSha: candidate.commitSha, + recoveryCommit: reviewedAuthority.mergeCommitSha, + canonicalReleaseId: DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId, + duplicateReleaseId: raw.releaseId, + originalBodySha256, + baseAssetSetSha256, + archiveAsset: { + name: originalBodyAssetName(raw.releaseId, originalBodySha256), + sha256: originalBodySha256, + }, + } + const receiptBytes = canonicalRecoveryReceipt(recoveryReceipt).toString("utf8") + const receiptSha256 = sha256Bytes(Buffer.from(receiptBytes, "utf8")) + const noticeBytes = canonicalRecoveryNotice({ + repository: DUPLICATE_DRAFT_RECOVERY_POLICY.repository, + version: candidate.version, + canonicalReleaseId: DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId, + duplicateReleaseId: raw.releaseId, + originalBodySha256, + archiveAssetName: recoveryReceipt.archiveAsset.name, + receiptAssetName: recoveryReceiptAssetName(raw.releaseId), + receiptSha256, + }) + const state = classifyDuplicateDraft(raw, { + releaseId: raw.releaseId, + tagName: raw.tagName, + canonicalBody: canonical.body, + canonicalMarker: canonical.marker, + originalBodySha256, + originalAssets: canonical.assets, + recoveryReceipt, + recoveryNotice: noticeBytes, + }) + return { + ...raw, + originalBodySha256, + originalAssets, + baseAssetSetSha256, + archiveAssetName: recoveryReceipt.archiveAsset.name, + receiptAssetName: recoveryReceiptAssetName(raw.releaseId), + receiptSha256, + receiptBytes, + noticeBytes, + state, + remainingTransitions: remainingTransitions(state), + } + }) + return { canonical, duplicates } +} + +function normalizeRecoveryDuplicateSource(value, originalAssets) { + const source = exactObject(value, DUPLICATE_SOURCE_FIELDS, "recovery duplicate Release") + const projection = normalizeDuplicateDraftReleaseProjection(source) + const evidenceKinds = normalizeEvidenceKinds(source.evidenceAssets) + if ( + !Array.isArray(source.assets) || + source.assets.length !== originalAssets.length + evidenceKinds.length + ) { + throw new TypeError("Recovery duplicate Release asset inventory is invalid") + } + const original = normalizeAssets( + source.assets.slice(0, originalAssets.length), + "recovery duplicate original assets", + false, + ) + const evidence = source.assets + .slice(originalAssets.length) + .map( + (asset, index) => + normalizeAssets( + [asset], + "recovery duplicate evidence asset", + evidenceKinds[index] === "receipt", + )[0], + ) + assertUniqueAssets([...original, ...evidence], "recovery duplicate Release assets") + return { + ...projection, + assets: [...original, ...evidence], + marker: source.marker, + evidenceAssets: evidenceKinds, + } +} + +function normalizeCanonicalRecoveryRelease(value, candidate) { + const source = exactObject(value, [...RELEASE_PROJECTION_FIELDS, "marker"], "canonical Release") + const projection = normalizeDuplicateDraftReleaseProjection(source) + assertReleaseId(source.releaseId, "Canonical Release ID") + if ( + source.releaseId !== DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId || + source.tagName !== CANONICAL_OPAQUE_TAG || + typeof source.body !== "string" || + !Array.isArray(source.assets) || + source.assets.length !== 45 + ) { + throw new TypeError("Canonical Release identity is not exact") + } + const assets = normalizeAssets(source.assets, "canonical Release assets", false) + assertUniqueAssets(assets, "canonical Release assets") + let marker + try { + marker = parseReleaseMarker(source.body) + } catch (error) { + throw new TypeError("Canonical Release body is not a valid Dawn release body", { cause: error }) + } + if ( + !sameJson(marker, source.marker) || + marker.phase !== "ESCROWED" || + marker.version !== candidate.version || + marker.commitSha !== candidate.commitSha || + marker.tag !== `v${candidate.version}` || + marker.baseAssetSetSha256 !== assetSetSha256(assets) + ) { + throw new TypeError("Canonical Release marker is not exact") + } + return { ...projection, marker, assets } +} + +function assertCaptureReader(value) { + let prototype + let keys + let frozen + try { + if (!isRecord(value)) throw new TypeError("invalid reader") + prototype = Object.getPrototypeOf(value) + keys = Reflect.ownKeys(value) + frozen = Object.isFrozen(value) + } catch { + captureFail("CAPTURE_READER_SURFACE_INVALID", "Recovery capture reader is invalid") + } + if ( + ![Object.prototype, null].includes(prototype) || + !frozen || + keys.some((key) => typeof key !== "string") + ) { + captureFail("CAPTURE_READER_SURFACE_INVALID", "Recovery capture reader is invalid") + } + const actual = [...keys].sort(compareText) + const expected = [...CAPTURE_READER_METHODS].sort(compareText) + if (actual.length !== expected.length || actual.some((name, index) => name !== expected[index])) { + captureFail( + "CAPTURE_READER_SURFACE_INVALID", + "Recovery capture reader must expose only recovery reads", + ) + } + for (const name of CAPTURE_READER_METHODS) { + let descriptor + try { + descriptor = Object.getOwnPropertyDescriptor(value, name) + } catch { + captureFail("CAPTURE_READER_SURFACE_INVALID", "Recovery capture reader is invalid") + } + if ( + descriptor?.enumerable !== true || + !("value" in descriptor) || + descriptor.get !== undefined || + descriptor.set !== undefined || + typeof descriptor.value !== "function" + ) { + captureFail( + "CAPTURE_READER_SURFACE_INVALID", + "Recovery capture reader must expose only recovery reads", + ) + } + } +} + +async function captureRead(reader, method, args, code) { + try { + return snapshotJson(await reader[method](...args)) + } catch (error) { + if (error instanceof DuplicateDraftRecoveryCaptureError) throw error + const readCode = knownRecoveryReadCode(error) + if (readCode !== null) { + captureFail(`READ_${readCode}`, `Recovery capture read ${method} failed`) + } + captureFail(code, `Recovery capture read ${method} failed`) + } +} + +function knownRecoveryReadCode(error) { + try { + if (!(error instanceof Error)) return null + const prototype = Object.getPrototypeOf(error) + const constructorDescriptor = Object.getOwnPropertyDescriptor(prototype, "constructor") + const name = Object.getOwnPropertyDescriptor(error, "name") + const code = Object.getOwnPropertyDescriptor(error, "code") + if ( + constructorDescriptor === undefined || + !("value" in constructorDescriptor) || + constructorDescriptor.value?.name !== "DuplicateDraftRecoveryReadError" || + name === undefined || + !("value" in name) || + name.value !== "DuplicateDraftRecoveryReadError" || + code === undefined || + !("value" in code) || + typeof code.value !== "string" || + !RECOVERY_READ_ERROR_CODES.has(code.value) + ) { + return null + } + return code.value + } catch { + return null + } +} + +function normalizeCaptureRuns(value) { + const source = exactObject(value, ["runs", "candidateRuns"], "recovery workflow run observation") + if (!Array.isArray(source.runs) || !Array.isArray(source.candidateRuns)) { + captureFail("RELEASE_RUNS_MALFORMED", "Recovery workflow run observation is malformed") + } + const runs = source.runs + .map((run) => normalizeCaptureRun(run, "Release workflow run", false)) + .sort(compareCaptureRun) + const candidateRuns = source.candidateRuns + .map((run) => normalizeCaptureRun(run, "candidate Release workflow run", true)) + .sort(compareCaptureRun) + assertUniqueRunIds(runs, "exhaustive") + assertUniqueRunIds(candidateRuns, "candidate") + const expectedCandidates = runs.filter( + (run) => run.headSha === DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha, + ) + if (!sameJson(expectedCandidates, candidateRuns)) { + captureFail("RELEASE_RUNS_MALFORMED", "Candidate workflow run set is not exhaustive") + } + return { + nonterminalRuns: runs.filter((run) => run.status !== "completed"), + candidateRuns, + } +} + +function compareCaptureRun(left, right) { + return left.id - right.id || left.runAttempt - right.runAttempt +} + +function normalizeCaptureRun(value, label, requireCandidateSha) { + const source = exactObject( + value, + ["id", "runAttempt", "status", "conclusion", "headSha", "createdAt", "startedAt", "updatedAt"], + label, + ) + assertPositiveInteger(source.id, `${label} ID`) + assertPositiveInteger(source.runAttempt, `${label} attempt`) + assertGitSha(source.headSha, `${label} head SHA`) + if ( + !CAPTURE_RUN_STATUSES.has(source.status) || + !isCaptureTimestamp(source.createdAt) || + !isNullableCaptureTimestamp(source.startedAt) || + !isCaptureTimestamp(source.updatedAt) || + !coherentCaptureTerminalState(source.status, source.conclusion) || + (source.status === "completed" || source.status === "in_progress") !== + (source.startedAt !== null) || + !orderedCaptureTimestamps(source.createdAt, source.startedAt, source.updatedAt) || + (requireCandidateSha && source.headSha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha) + ) { + captureFail("RELEASE_RUNS_MALFORMED", "Recovery workflow run observation is malformed") + } + return source +} + +function assertUniqueRunIds(runs, label) { + if (new Set(runs.map((run) => run.id)).size !== runs.length) { + captureFail("RELEASE_RUNS_MALFORMED", `Recovery ${label} workflow runs are duplicated`) + } +} + +function assertNoStartedPublishJob(value, expectedRunId, currentAttempt) { + if (!Array.isArray(value) || value.length === 0) { + captureFail("CANDIDATE_JOBS_MALFORMED", "Candidate workflow jobs are malformed") + } + const ids = new Set() + const identities = new Set() + const attempts = new Set() + const publishJobs = [] + for (const raw of value) { + let job + try { + job = exactObject( + raw, + ["id", "runId", "runAttempt", "name", "status", "conclusion", "startedAt", "completedAt"], + "candidate workflow job", + ) + } catch { + captureFail("CANDIDATE_JOBS_MALFORMED", "Candidate workflow jobs are malformed") + } + const identity = `${job.runAttempt}:${job.id}` + if ( + !Number.isSafeInteger(job.id) || + job.id < 1 || + ids.has(job.id) || + identities.has(identity) || + !Number.isSafeInteger(job.runId) || + job.runId !== expectedRunId || + !Number.isSafeInteger(job.runAttempt) || + job.runAttempt < 1 || + job.runAttempt > currentAttempt || + !isBoundedCaptureText(job.name, 512) || + !CAPTURE_JOB_STATUSES.has(job.status) || + !isNullableCaptureTimestamp(job.startedAt) || + !isNullableCaptureTimestamp(job.completedAt) || + !coherentCaptureTerminalState(job.status, job.conclusion) || + (job.status === "completed" + ? job.startedAt === null || job.completedAt === null + : job.completedAt !== null || (job.status === "in_progress") !== (job.startedAt !== null)) + ) { + captureFail("CANDIDATE_JOBS_MALFORMED", "Candidate workflow jobs are malformed") + } + ids.add(job.id) + identities.add(identity) + attempts.add(job.runAttempt) + if (job.name === "publish-npm") publishJobs.push(job) + } + if (attempts.size !== currentAttempt) { + captureFail("CANDIDATE_JOBS_MALFORMED", "Candidate workflow job attempt coverage is incomplete") + } + for (let attempt = 1; attempt <= currentAttempt; attempt += 1) { + if ( + !attempts.has(attempt) || + publishJobs.filter((job) => job.runAttempt === attempt).length !== 1 + ) { + captureFail( + "CANDIDATE_JOBS_MALFORMED", + "Candidate workflow publish job identity is not exact", + ) + } + } + if ( + publishJobs.some( + ({ status, conclusion }) => + !( + (status === "queued" && conclusion === null) || + (status === "completed" && conclusion === "skipped") + ), + ) + ) { + captureFail("CANDIDATE_PUBLISH_JOB_STARTED", "A candidate publish-npm job has already started") + } +} + +function normalizeCandidateReleaseInventory(value) { + if (!Array.isArray(value) || value.length !== 3) { + captureFail("CANDIDATE_RELEASE_INVENTORY_CONFLICT", "Candidate Release inventory is not exact") + } + const expected = [ + { + releaseId: DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId, + tagName: CANONICAL_OPAQUE_TAG, + }, + ...DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates, + ].sort((left, right) => left.releaseId - right.releaseId) + const normalized = value + .map((raw) => { + const release = exactObject( + raw, + [ + "releaseId", + "tagName", + "title", + "draft", + "prerelease", + "immutable", + "targetCommitish", + "marker", + ], + "candidate Release summary", + ) + assertReleaseId(release.releaseId, "Candidate Release ID") + if ( + typeof release.tagName !== "string" || + release.title !== RECOVERY_RELEASE_TITLE || + release.draft !== true || + release.prerelease !== false || + release.immutable !== false || + release.targetCommitish !== "main" + ) { + captureFail( + "CANDIDATE_RELEASE_INVENTORY_CONFLICT", + "Candidate Release metadata is not exact", + ) + } + return release + }) + .sort((left, right) => left.releaseId - right.releaseId) + if ( + normalized.some( + (release, index) => + release.releaseId !== expected[index].releaseId || + release.tagName !== expected[index].tagName || + release.tagName === `v${DUPLICATE_DRAFT_RECOVERY_POLICY.version}`, + ) + ) { + captureFail( + "CANDIDATE_RELEASE_INVENTORY_CONFLICT", + "Candidate Release identities are not exact", + ) + } + for (const release of normalized) { + if (release.marker === null) continue + const marker = snapshotJson(release.marker) + if ( + !isRecord(marker) || + marker.version !== DUPLICATE_DRAFT_RECOVERY_POLICY.version || + marker.commitSha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha || + marker.tag !== `v${DUPLICATE_DRAFT_RECOVERY_POLICY.version}` + ) { + captureFail( + "CANDIDATE_RELEASE_INVENTORY_CONFLICT", + "Candidate Release marker identity is not exact", + ) + } + } +} + +function captureFail(code, message) { + throw new DuplicateDraftRecoveryCaptureError(code, message) +} + +function normalizeDuplicateEvidenceDuplicates(value) { + const releases = exactObject(value, ["canonical", "duplicates"], "recovery Releases evidence") + if (!Array.isArray(releases.duplicates)) + throw new TypeError("Recovery duplicate Release inventory is invalid") + return releases.duplicates.map((duplicate) => { + const item = exactObject( + duplicate, + [...DUPLICATE_SOURCE_FIELDS, ...DUPLICATE_DERIVED_FIELDS], + "sealed recovery duplicate Release", + ) + return Object.fromEntries(DUPLICATE_SOURCE_FIELDS.map((field) => [field, item[field]])) + }) +} + +function remainingTransitions(state) { + if (state === "untouched") return ["archive-body", "archive-receipt", "quarantine"] + if (state === "body-archived") return ["archive-receipt", "quarantine"] + if (state === "receipt-archived") return ["quarantine"] + if (state === "quarantined") return [] + throw new TypeError("Duplicate Release state is invalid") +} + +function normalizeEvidenceBytes(value) { + if (!(value instanceof Uint8Array)) + throw new TypeError("Duplicate draft evidence bytes are invalid") + const bytes = Buffer.from(value) + if ( + bytes.byteLength < 1 || + bytes.byteLength > MAX_DUPLICATE_DRAFT_EVIDENCE_BYTES || + bytes.at(-1) !== 0x0a || + bytes.includes(0x0d) + ) { + throw new TypeError("Duplicate draft evidence bytes are outside canonical bounds") + } + return bytes +} + +function normalizeCanonicalTimestamp(value, label) { + if (typeof value !== "string") throw new TypeError(`${label} is invalid`) + const milliseconds = Date.parse(value) + if (!Number.isSafeInteger(milliseconds) || new Date(milliseconds).toISOString() !== value) { + throw new TypeError(`${label} is not canonical`) + } + return value +} + +function assertPositiveInteger(value, label) { + if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${label} is invalid`) +} + +function assertUniqueAssets(assets, label) { + const ids = new Set() + const names = new Set() + for (const asset of assets) { + if (ids.has(asset.id) || names.has(asset.name)) throw new TypeError(`${label} are not unique`) + ids.add(asset.id) + names.add(asset.name) + } +} + +function assertPolicyIdentity(source) { + if ( + source.repository !== DUPLICATE_DRAFT_RECOVERY_POLICY.repository || + source.version !== DUPLICATE_DRAFT_RECOVERY_POLICY.version || + source.candidateSha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha + ) { + throw new Error("Recovery receipt candidate identity is not approved") + } +} + +function assertDuplicateIdentity(releaseId, tagName, expected) { + assertDuplicateReleaseId(releaseId) + const configured = DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates.find( + (duplicate) => duplicate.releaseId === releaseId, + ) + if ( + configured === undefined || + releaseId !== expected.releaseId || + tagName !== expected.tagName || + tagName !== configured.tagName + ) { + throw new Error("Duplicate Release identity is not exact") + } + if (tagName === `v${DUPLICATE_DRAFT_RECOVERY_POLICY.version}`) { + throw new Error("Duplicate Release must retain its opaque temporary tag") + } +} + +function assertDuplicateReleaseId(value) { + assertReleaseId(value, "Duplicate Release ID") + if (!DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates.some((item) => item.releaseId === value)) { + throw new Error("Release ID is not an approved duplicate") + } +} + +function assertReleaseId(value, label) { + if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${label} is invalid`) +} + +function assertGitSha(value, label) { + if (typeof value !== "string" || !GIT_SHA_PATTERN.test(value)) { + throw new TypeError(`${label} must be a lowercase Git SHA-1`) + } +} + +function assertSha256(value, label) { + if (typeof value !== "string" || !SHA256_PATTERN.test(value)) { + throw new TypeError(`${label} must be a lowercase SHA-256 digest`) + } +} + +function assertBodyDigest(body, expectedDigest) { + if (typeof body !== "string") throw new TypeError("Canonical duplicate body is invalid") + assertSha256(expectedDigest, "Original body SHA-256") + if (sha256(body) !== expectedDigest) + throw new Error("Canonical duplicate body digest is not exact") +} + +function normalizeEvidenceKinds(value) { + if (!Array.isArray(value) || value.length > 2) + throw new TypeError("Duplicate evidence asset list is invalid") + const result = value.map((kind) => { + if (kind !== "body" && kind !== "receipt") throw new Error("Unknown duplicate evidence asset") + return kind + }) + if (new Set(result).size !== result.length) + throw new Error("Duplicate evidence asset list contains duplicates") + if (result.includes("receipt") && !result.includes("body")) { + throw new Error("Recovery receipt cannot exist without the original-body archive") + } + return result +} + +function normalizeAssets(value, label, allowBytes) { + if (!Array.isArray(value)) throw new TypeError(`${label} must be an array`) + return value.map((asset, index) => { + const source = snapshotJson(asset) + const normalized = exactObject( + source, + allowBytes && Object.hasOwn(source, "bytes") + ? ["id", "name", "sha256", "size", "bytes"] + : ["id", "name", "sha256", "size"], + `${label}[${index}]`, + ) + assertReleaseId(normalized.id, `${label}[${index}] id`) + if (typeof normalized.name !== "string" || !ASSET_NAME_PATTERN.test(normalized.name)) { + throw new TypeError(`${label}[${index}] name is invalid`) + } + assertSha256(normalized.sha256, `${label}[${index}] SHA-256`) + assertPositiveInteger(normalized.size, `${label}[${index}] size`) + if (Object.hasOwn(normalized, "bytes") && typeof normalized.bytes !== "string") { + throw new TypeError(`${label}[${index}] bytes are invalid`) + } + return normalized + }) +} + +function exactObject(value, fields, label) { + const source = snapshotJson(value) + if (!isRecord(source)) throw new TypeError(`${label} must be an object`) + const actual = Object.keys(source).sort() + const expected = [...fields].sort() + if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { + throw new TypeError(`${label} contains unexpected or missing fields`) + } + return source +} + +function normalizeConcurrencyAcknowledgement(value) { + const fields = ["acknowledged", "atomic", "mode", "releaseIds"] + if (value === null || typeof value !== "object" || isProxy(value)) { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is invalid") + } + let keys + let prototype + let frozen + try { + keys = Reflect.ownKeys(value) + prototype = Object.getPrototypeOf(value) + frozen = Object.isFrozen(value) + } catch { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is invalid") + } + if ( + ![Object.prototype, null].includes(prototype) || + !frozen || + keys.length !== fields.length || + keys.some((key) => typeof key !== "string" || !fields.includes(key)) + ) { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is invalid") + } + const values = {} + for (const field of fields) { + let descriptor + try { + descriptor = Object.getOwnPropertyDescriptor(value, field) + } catch { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is invalid") + } + if ( + descriptor?.enumerable !== true || + descriptor.configurable !== false || + descriptor.writable !== false || + !("value" in descriptor) || + descriptor.get !== undefined || + descriptor.set !== undefined + ) { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is invalid") + } + values[field] = descriptor.value + } + const releaseIds = normalizeAcknowledgedReleaseIds(values.releaseIds) + if ( + values.acknowledged !== true || + values.atomic !== false || + values.mode !== "operator-freeze-compare-before-write-v1" || + !sameJson( + releaseIds, + DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates.map(({ releaseId }) => releaseId), + ) + ) { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is not exact") + } + return deepFreeze({ + acknowledged: true, + atomic: false, + mode: values.mode, + releaseIds, + }) +} + +function normalizeAcknowledgedReleaseIds(value) { + if (value === null || typeof value !== "object" || isProxy(value)) { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is invalid") + } + let keys + let prototype + let frozen + try { + keys = Reflect.ownKeys(value) + prototype = Object.getPrototypeOf(value) + frozen = Object.isFrozen(value) + } catch { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is invalid") + } + if ( + !Array.isArray(value) || + prototype !== Array.prototype || + !frozen || + keys.length !== 3 || + !keys.includes("0") || + !keys.includes("1") || + !keys.includes("length") + ) { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is invalid") + } + const result = [] + for (const key of ["0", "1"]) { + let descriptor + try { + descriptor = Object.getOwnPropertyDescriptor(value, key) + } catch { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is invalid") + } + if ( + descriptor?.enumerable !== true || + descriptor.configurable !== false || + descriptor.writable !== false || + !("value" in descriptor) + ) { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is invalid") + } + result.push(descriptor.value) + } + const length = Object.getOwnPropertyDescriptor(value, "length") + if ( + length?.enumerable !== false || + length.configurable !== false || + length.writable !== false || + length.value !== 2 + ) { + throw new TypeError("Duplicate draft recovery concurrency acknowledgement is invalid") + } + return result +} + +function readApplyTime(now) { + let milliseconds + try { + milliseconds = now() + } catch { + throw new TypeError("Duplicate draft recovery time is invalid") + } + if ( + !Number.isSafeInteger(milliseconds) || + milliseconds < 0 || + milliseconds > 8_640_000_000_000_000 + ) { + throw new TypeError("Duplicate draft recovery time is invalid") + } + return milliseconds +} + +function assertApplyEvidenceFresh(evidence, nowMs) { + const capturedAtMs = Date.parse(evidence.capturedAt) + if (capturedAtMs > nowMs) { + throw new Error("Duplicate draft recovery evidence capture time is in the future") + } + if (nowMs - capturedAtMs > MAX_DUPLICATE_EVIDENCE_AGE_MS) { + throw new Error("Duplicate draft recovery evidence has expired and must be recaptured") + } +} + +function assertApplyAuthorization(sealed, fresh, expectedSources) { + assertApplyEvidenceFresh(sealed, Date.parse(fresh.capturedAt)) + for (const field of [ + "reviewedAuthority", + "repository", + "workflow", + "immutableReleases", + "candidate", + "npm", + "releaseRuns", + ]) { + if (!sameJson(fresh[field], sealed[field])) { + throw new Error(`Duplicate draft recovery authorization drifted at ${field}`) + } + } + if (!sameJson(fresh.releases.canonical, sealed.releases.canonical)) { + throw new Error("Duplicate draft recovery canonical Release drifted") + } + if (fresh.releases.duplicates.length !== expectedSources.length) { + throw new Error("Duplicate draft recovery duplicate inventory drifted") + } + for (const [index, duplicate] of fresh.releases.duplicates.entries()) { + if (!sameJson(duplicateSource(duplicate), expectedSources[index])) { + throw new Error("Duplicate draft recovery partial state drifted") + } + } +} + +function assertFreshAuthorizationTimeline({ + sealed, + fresh, + authorizationMs, + previousAuthorizationMs, +}) { + const evidenceMs = Date.parse(sealed.capturedAt) + const captureMs = Date.parse(fresh.capturedAt) + if ( + captureMs < evidenceMs || + captureMs < previousAuthorizationMs || + authorizationMs < captureMs || + authorizationMs < previousAuthorizationMs + ) { + throw new Error("Duplicate draft recovery authorization timeline regressed") + } + if ( + authorizationMs - evidenceMs > MAX_DUPLICATE_EVIDENCE_AGE_MS || + authorizationMs - captureMs > MAX_DUPLICATE_EVIDENCE_AGE_MS + ) { + throw new Error("Duplicate draft recovery authorization is no longer fresh") + } +} + +function assertMutationAuthorizationTimeline({ + sealed, + fresh, + authorizationMs, + previousAuthorizationMs, +}) { + const evidenceMs = Date.parse(sealed.capturedAt) + const captureMs = Date.parse(fresh.capturedAt) + if (authorizationMs < captureMs || authorizationMs < previousAuthorizationMs) { + throw new Error("Duplicate draft recovery mutation authorization timeline regressed") + } + if ( + authorizationMs - evidenceMs > MAX_DUPLICATE_EVIDENCE_AGE_MS || + authorizationMs - captureMs > MAX_DUPLICATE_EVIDENCE_AGE_MS + ) { + throw new Error("Duplicate draft recovery mutation authorization is no longer fresh") + } +} + +function duplicateSource(duplicate) { + return Object.fromEntries(DUPLICATE_SOURCE_FIELDS.map((field) => [field, duplicate[field]])) +} + +function normalizeApplyWriter(value) { + const fields = ["quarantineDuplicateBodyIfCurrent", "uploadEvidenceAssetIfAbsentAndEqual"] + if (value === null || typeof value !== "object" || isProxy(value)) { + throw new TypeError("Duplicate draft recovery writer is invalid") + } + let keys + let prototype + let frozen + try { + keys = Reflect.ownKeys(value) + prototype = Object.getPrototypeOf(value) + frozen = Object.isFrozen(value) + } catch { + throw new TypeError("Duplicate draft recovery writer is invalid") + } + if ( + ![Object.prototype, null].includes(prototype) || + !frozen || + keys.length !== fields.length || + keys.some((key) => typeof key !== "string" || !fields.includes(key)) + ) { + throw new TypeError("Duplicate draft recovery writer surface is invalid") + } + const result = {} + for (const field of fields) { + const descriptor = Object.getOwnPropertyDescriptor(value, field) + if ( + descriptor?.enumerable !== true || + !("value" in descriptor) || + descriptor.get !== undefined || + descriptor.set !== undefined || + typeof descriptor.value !== "function" || + isProxy(descriptor.value) + ) { + throw new TypeError("Duplicate draft recovery writer surface is invalid") + } + result[field] = descriptor.value + } + return Object.freeze(result) +} + +function normalizeUploadMutationReceipt(value, expected) { + assertFrozenNonProxyGraph(value, "duplicate draft recovery upload receipt") + const receipt = exactObject( + value, + ["releaseId", "assetId", "name", "status", "sha256"], + "duplicate draft recovery upload receipt", + ) + if ( + receipt.releaseId !== expected.releaseId || + !Number.isSafeInteger(receipt.assetId) || + receipt.assetId < 1 || + receipt.name !== expected.name || + !["uploaded", "existing"].includes(receipt.status) || + receipt.sha256 !== expected.sha256 + ) { + throw new Error("Duplicate draft recovery upload receipt is not exact") + } + return receipt +} + +function normalizeQuarantineMutationReceipt( + value, + releaseId, + expectedTagObjectSha, + authorizationMs, +) { + assertFrozenNonProxyGraph(value, "duplicate draft recovery quarantine receipt") + const receipt = exactObject( + value, + ["atomic", "releaseId", "outcome", "preWriteFence", "postWriteFence"], + "duplicate draft recovery quarantine receipt", + ) + if ( + receipt.atomic !== false || + receipt.releaseId !== releaseId || + receipt.outcome !== "performed" + ) { + throw new Error("Duplicate draft recovery quarantine receipt is not exact") + } + const preWriteFence = normalizeWriterFence( + receipt.preWriteFence, + expectedTagObjectSha, + "pre-write", + ) + const postWriteFence = normalizeWriterFence( + receipt.postWriteFence, + expectedTagObjectSha, + "post-write", + ) + if ( + Date.parse(preWriteFence.observedAt) < authorizationMs || + Date.parse(preWriteFence.observedAt) > Date.parse(postWriteFence.observedAt) + ) { + throw new Error("Duplicate draft recovery write fence times are not ordered") + } + return deepFreeze({ + releaseId, + outcome: "performed", + preWriteFence, + postWriteFence, + }) +} + +function normalizeWriterFence(value, expectedTagObjectSha, label) { + // Core capture and the writer share the complete Release projection. This + // boundary preserves its digest exactly while independently validating the + // tag binding and truthful timeline. + const fence = exactObject( + value, + ["observedAt", "projectionSha256", "tagObjectSha"], + `duplicate draft recovery ${label} fence`, + ) + normalizeCanonicalTimestamp(fence.observedAt, `Duplicate draft recovery ${label} fence time`) + assertSha256(fence.projectionSha256, `Duplicate draft recovery ${label} projection SHA-256`) + if (fence.tagObjectSha !== expectedTagObjectSha) { + throw new Error(`Duplicate draft recovery ${label} candidate tag drifted`) + } + return fence +} + +function normalizeFinalRecoveryObservation(value) { + assertNoProxyGraph(value, "duplicate draft recovery final authorization") + const observation = exactObject( + value, + ["state", "disposition", "nextTransition", "conflicts", "diagnostics", "releaseId"], + "duplicate draft recovery final authorization", + ) + if ( + observation.state !== "CANDIDATE_ESCROWED" || + observation.disposition !== "would-transition" || + observation.nextTransition !== "publish-npm-packages" || + !Array.isArray(observation.conflicts) || + observation.conflicts.length !== 0 || + !Array.isArray(observation.diagnostics) || + observation.diagnostics.length !== 0 || + observation.releaseId !== DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId + ) { + throw new Error("Duplicate draft recovery final authorization is not exact") + } + return deepFreeze({ + state: observation.state, + disposition: observation.disposition, + nextTransition: observation.nextTransition, + conflicts: [], + diagnostics: [], + releaseId: observation.releaseId, + }) +} + +function assertFinalReceiptTimeline(results, chronologyWatermarkMs, appliedAtMs) { + if (appliedAtMs < chronologyWatermarkMs) { + throw new Error("Duplicate draft recovery final receipt timeline regressed") + } + for (const result of results) { + if ( + result.outcome === "performed" && + Date.parse(result.postWriteFence.observedAt) > appliedAtMs + ) { + throw new Error("Duplicate draft recovery write fence is after the final receipt time") + } + } +} + +function assertFrozenNonProxyGraph(value, label, seen = new Set()) { + if (value === null || typeof value !== "object" || seen.has(value)) return + if (isProxy(value) || !Object.isFrozen(value)) { + throw new TypeError(`${label} must be deeply frozen and contain no proxy`) + } + seen.add(value) + const keys = Reflect.ownKeys(value) + for (const key of keys) { + const descriptor = Object.getOwnPropertyDescriptor(value, key) + if (descriptor !== undefined && "value" in descriptor) { + assertFrozenNonProxyGraph(descriptor.value, label, seen) + } + } +} + +function assertNoProxyGraph(value, label, seen = new Set()) { + if (value === null || typeof value !== "object" || seen.has(value)) return + if (isProxy(value)) throw new TypeError(`${label} contains a proxy`) + seen.add(value) + let keys + try { + keys = Reflect.ownKeys(value) + } catch { + throw new TypeError(`${label} is invalid`) + } + for (const key of keys) { + const descriptor = Object.getOwnPropertyDescriptor(value, key) + if (descriptor !== undefined && "value" in descriptor) { + assertNoProxyGraph(descriptor.value, label, seen) + } + } +} + +function isCanonicalNotice(value) { + if (typeof value !== "string" || !value.endsWith("\n") || value.includes(MARKER_DELIMITER)) + return false + try { + const parsed = JSON.parse(value) + return `${JSON.stringify(canonicalize(parsed))}\n` === value + } catch { + return false + } +} + +function parseCanonicalNotice( + value, + expectedDuplicateReleaseId, + expectedOriginalBodySha256, + expectedArchiveAssetName, +) { + if (!isCanonicalNotice(value)) throw new Error("Duplicate Release recovery notice is malformed") + const notice = snapshotJson(JSON.parse(value)) + if ( + !isRecord(notice) || + Object.keys(notice).sort().join(",") !== + [ + "archiveAssetName", + "candidateSha", + "canonicalReleaseId", + "duplicateReleaseId", + "originalBodySha256", + "receiptAssetName", + "receiptSha256", + "repository", + "schemaVersion", + "type", + "version", + ] + .sort() + .join(",") || + notice.repository !== DUPLICATE_DRAFT_RECOVERY_POLICY.repository || + notice.version !== DUPLICATE_DRAFT_RECOVERY_POLICY.version || + notice.candidateSha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha || + notice.canonicalReleaseId !== DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId || + notice.duplicateReleaseId !== expectedDuplicateReleaseId || + notice.originalBodySha256 !== expectedOriginalBodySha256 || + notice.archiveAssetName !== expectedArchiveAssetName || + notice.archiveAssetName !== + originalBodyAssetName(notice.duplicateReleaseId, notice.originalBodySha256) || + notice.receiptAssetName !== recoveryReceiptAssetName(notice.duplicateReleaseId) || + notice.schemaVersion !== 1 || + notice.type !== "DAWN_DUPLICATE_DRAFT_RECOVERY" + ) { + throw new Error("Duplicate Release recovery notice identity is not exact") + } + assertSha256(notice.originalBodySha256, "Recovery notice original body SHA-256") + assertSha256(notice.receiptSha256, "Recovery notice receipt SHA-256") + return notice +} + +function sameJson(left, right) { + return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right)) +} + +function canonicalize(value) { + if (Array.isArray(value)) return value.map(canonicalize) + if (isRecord(value)) { + return Object.fromEntries( + Object.keys(value) + .sort(compareText) + .map((key) => [key, canonicalize(value[key])]), + ) + } + return value +} + +function isRecord(value) { + return value !== null && typeof value === "object" && !Array.isArray(value) +} + +function isCaptureTimestamp(value) { + if (typeof value !== "string" || !CAPTURE_TIMESTAMP_PATTERN.test(value)) return false + const milliseconds = Date.parse(value) + if (!Number.isFinite(milliseconds)) return false + const canonical = new Date(milliseconds).toISOString() + return ( + value === canonical || + (canonical.endsWith(".000Z") && value === canonical.replace(".000Z", "Z")) + ) +} + +function isNullableCaptureTimestamp(value) { + return value === null || isCaptureTimestamp(value) +} + +function isBoundedCaptureText(value, maximumBytes) { + return ( + typeof value === "string" && + value.length > 0 && + Buffer.byteLength(value) <= maximumBytes && + !/[\0\r\n]/u.test(value) + ) +} + +function coherentCaptureTerminalState(status, conclusion) { + return status === "completed" ? CAPTURE_TERMINAL_CONCLUSIONS.has(conclusion) : conclusion === null +} + +function orderedCaptureTimestamps(...values) { + const timestamps = values.filter((value) => value !== null).map((value) => Date.parse(value)) + return timestamps.every((value, index) => index === 0 || timestamps[index - 1] <= value) +} + +function compareText(left, right) { + return left === right ? 0 : left < right ? -1 : 1 +} + +function sha256(value) { + return createHash("sha256").update(value, "utf8").digest("hex") +} + +function assetSetSha256(assets) { + return sha256(`${JSON.stringify(assetNamespace(assets))}\n`) +} + +function assetNamespace(assets) { + return assets.map(({ name, sha256: digest }) => ({ name, sha256: digest })) +} + +function assetSizeNamespace(assets) { + return assets.map(({ name, sha256: digest, size }) => ({ name, sha256: digest, size })) +} + +function sha256Bytes(value) { + return createHash("sha256").update(value).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/recover-v0.8.22-duplicate-drafts.mjs b/scripts/release/recover-v0.8.22-duplicate-drafts.mjs new file mode 100755 index 000000000..a879aad6c --- /dev/null +++ b/scripts/release/recover-v0.8.22-duplicate-drafts.mjs @@ -0,0 +1,1553 @@ +#!/usr/bin/env node + +import { execFile as execFileCallback } from "node:child_process" +import { 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 { pathToFileURL } from "node:url" +import { promisify } from "node:util" +import { isProxy } from "node:util/types" + +import { snapshotJson } from "./adapter-normalize.mjs" +import { createGitReader } from "./adapters/git.mjs" +import { createGitHubReader } from "./adapters/github.mjs" +import { createNpmReader } from "./adapters/npm.mjs" +import { createCliAttestationVerifier } from "./artifact-store.mjs" +import { + assertDuplicateDraftRecoveryReader, + DUPLICATE_DRAFT_RECOVERY_POLICY, + applyDuplicateDraftRecovery as defaultApplyDuplicateDraftRecovery, + canonicalDuplicateDraftEvidence as defaultCanonicalDuplicateDraftEvidence, + captureDuplicateDraftRecoveryEvidence as defaultCaptureDuplicateDraftRecoveryEvidence, + parseDuplicateDraftEvidence as defaultParseDuplicateDraftEvidence, + normalizeDuplicateDraftReleaseProjection, +} from "./duplicate-draft-recovery.mjs" +import { + createDuplicateDraftRecoveryReader as defaultCreateDuplicateDraftRecoveryReader, + createDuplicateDraftRecoveryWriter as defaultCreateDuplicateDraftRecoveryWriter, +} from "./duplicate-draft-recovery-adapters.mjs" +import { createProductionInventoryReader, observeProductionCandidate } from "./observe.mjs" +import { planRelease } from "./planner.mjs" + +const execFile = promisify(execFileCallback) +const RECOVERY_DIRECTORY = ".dawn/release-recovery" +const ACKNOWLEDGEMENT_FLAG = "--acknowledge-non-atomic-release-edit-freeze" +const SHA_PATTERN = /^[0-9a-f]{40}$/u +const SHA256_PATTERN = /^[0-9a-f]{64}$/u +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}$/iu +const MAX_PATH_BYTES = 4_096 +const MAX_EVIDENCE_BYTES = 512 * 1024 +const MAX_RECEIPT_BYTES = 512 * 1024 +const MAX_GIT_OUTPUT_BYTES = 8 * 1024 +const DEPENDENCY_FIELDS = Object.freeze([ + "applyDuplicateDraftRecovery", + "canonicalDuplicateDraftEvidence", + "captureDuplicateDraftRecoveryEvidence", + "createDuplicateDraftRecoveryReader", + "createDuplicateDraftRecoveryWriter", + "createNormalProductionRecoveryObserver", + "createProductionRecoveryObserver", + "fileSystem", + "parseDuplicateDraftEvidence", + "randomUUID", + "resolveRepositoryRoot", + "runGit", +]) + +export function parseDuplicateDraftRecoveryCliArguments(argv) { + const values = snapshotArgumentArray(argv) + if ( + values.length === 5 && + values[0] === "capture" && + values[1] === "--reviewed-commit" && + SHA_PATTERN.test(values[2]) && + values[3] === "--output" + ) { + return Object.freeze({ + command: "capture", + reviewedCommit: values[2], + output: normalizePrivatePath(values[4]), + }) + } + if ( + values.length === 6 && + values[0] === "apply" && + values[1] === "--evidence" && + values[3] === ACKNOWLEDGEMENT_FLAG && + values[4] === "--output" + ) { + const evidence = normalizePrivatePath(values[2]) + const output = normalizePrivatePath(values[5]) + if (evidence === output) throw new RecoveryInputError() + return Object.freeze({ command: "apply", evidence, output }) + } + throw new RecoveryInputError() +} + +export async function runDuplicateDraftRecoveryCli({ + argv = process.argv.slice(2), + cwd = process.cwd(), + environment = process.env, + stdout = process.stdout, + stderr = process.stderr, + dependencies = {}, +} = {}) { + let outputReservation = null + try { + const options = parseDuplicateDraftRecoveryCliArguments(argv) + const runtime = normalizeRuntime({ + cwd, + environment, + stdout, + stderr, + dependencies, + }) + const root = await runtime.resolveRepositoryRoot(runtime.cwd, runtime.runGit) + const paths = resolveInvocationPaths(root, options) + await assertPrivatePathBoundary(runtime.fileSystem, root, paths.output) + + if (options.command === "capture") { + await assertReviewedIgnorePolicy({ + fileSystem: runtime.fileSystem, + root, + reviewedCommit: options.reviewedCommit, + relativePaths: [paths.output.relative], + runGit: runtime.runGit, + }) + await assertUnusedOutput(runtime.fileSystem, paths.output.absolute) + outputReservation = await reserveExclusiveOutput(runtime, paths.output.absolute) + const token = environmentToken(runtime.environment) + const reader = runtime.createDuplicateDraftRecoveryReader({ + root, + token, + run: runtime.runGit, + }) + const evidence = await runtime.captureDuplicateDraftRecoveryEvidence({ + reviewedCommit: options.reviewedCommit, + reader, + }) + const bytes = runtime.canonicalDuplicateDraftEvidence(evidence) + assertCredentialFreeEvidence(bytes, token) + await assertPrivatePathBoundary(runtime.fileSystem, root, paths.output) + await assertReviewedIgnorePolicy({ + fileSystem: runtime.fileSystem, + root, + reviewedCommit: options.reviewedCommit, + relativePaths: [paths.output.relative], + runGit: runtime.runGit, + }) + await outputReservation.commit(bytes, MAX_EVIDENCE_BYTES) + outputReservation = null + writeSuccessBestEffort(runtime.stdout, "Duplicate draft recovery evidence captured.\n") + return 0 + } + + await assertPrivatePathBoundary(runtime.fileSystem, root, paths.evidence) + await assertReviewedIgnorePolicy({ + fileSystem: runtime.fileSystem, + root, + reviewedCommit: "HEAD", + relativePaths: [paths.evidence.relative, paths.output.relative], + runGit: runtime.runGit, + }) + const evidenceBytes = await readBoundedPrivateFile( + runtime.fileSystem, + paths.evidence.absolute, + MAX_EVIDENCE_BYTES, + ) + // Reject malformed UTF-8 at the filesystem boundary before invoking the + // canonical evidence parser. + new TextDecoder("utf-8", { fatal: true }).decode(evidenceBytes) + const evidence = runtime.parseDuplicateDraftEvidence(evidenceBytes) + const acknowledgement = concurrencyAcknowledgement() + const reviewedCommit = evidence?.reviewedAuthority?.mergeCommitSha + if (typeof reviewedCommit !== "string" || !SHA_PATTERN.test(reviewedCommit)) { + throw new Error("Recovery reviewed authority is invalid") + } + + await assertPrivatePathBoundary(runtime.fileSystem, root, paths.output) + await assertReviewedIgnorePolicy({ + fileSystem: runtime.fileSystem, + root, + reviewedCommit, + relativePaths: [paths.evidence.relative, paths.output.relative], + runGit: runtime.runGit, + }) + outputReservation = await reserveExclusiveOutput(runtime, paths.output.absolute) + + const token = environmentToken(runtime.environment) + const reader = runtime.createDuplicateDraftRecoveryReader({ + root, + token, + run: runtime.runGit, + }) + const observer = runtime.createProductionRecoveryObserver({ + root, + token, + reader, + environment: runtime.environment, + fileSystem: runtime.fileSystem, + runGit: runtime.runGit, + createNormalObserver: runtime.createNormalProductionRecoveryObserver, + }) + const receipt = await runtime.applyDuplicateDraftRecovery({ + evidence, + concurrencyAcknowledgement: acknowledgement, + reader, + createWriter: () => runtime.createDuplicateDraftRecoveryWriter({ token }), + observer, + }) + const receiptBytes = canonicalFinalAuthorizationReceiptBytes(receipt, token) + await assertPrivatePathBoundary(runtime.fileSystem, root, paths.output) + await assertReviewedIgnorePolicy({ + fileSystem: runtime.fileSystem, + root, + reviewedCommit, + relativePaths: [paths.evidence.relative, paths.output.relative], + runGit: runtime.runGit, + }) + await outputReservation.commit(receiptBytes, MAX_RECEIPT_BYTES) + outputReservation = null + writeSuccessBestEffort(runtime.stdout, "Duplicate draft recovery authorization recorded.\n") + return 0 + } catch (error) { + let cleanupUncertain = error instanceof RecoveryOutputCleanupUncertainError + if (outputReservation !== null) { + try { + await outputReservation.abort() + } catch (cleanupError) { + if (cleanupError instanceof RecoveryOutputCleanupUncertainError) { + cleanupUncertain = true + } + } + } + const input = error instanceof RecoveryInputError + try { + stderr.write( + cleanupUncertain + ? "Duplicate draft recovery output cleanup uncertain.\n" + : input + ? "Invalid duplicate draft recovery input.\n" + : "Duplicate draft recovery failed.\n", + ) + } catch {} + return cleanupUncertain ? 3 : input ? 2 : 1 + } +} + +function normalizeRuntime({ cwd, environment, stdout, stderr, dependencies }) { + if ( + typeof cwd !== "string" || + !path.isAbsolute(cwd) || + hasControlCharacters(cwd) || + environment === null || + typeof environment !== "object" || + stdout === null || + typeof stdout?.write !== "function" || + stderr === null || + typeof stderr?.write !== "function" + ) { + throw new RecoveryInputError() + } + validateDependencies(dependencies) + const dependency = (name, fallback) => dataProperty(dependencies, name) ?? fallback + const runGit = createScrubbedGitRunner(dependency("runGit", defaultGitExecutor)) + return Object.freeze({ + cwd, + environment, + stdout, + stderr, + fileSystem: dependency("fileSystem", defaultFileSystem), + randomUUID: dependency("randomUUID", defaultRandomUUID), + runGit, + resolveRepositoryRoot: dependency("resolveRepositoryRoot", resolveRepositoryRoot), + applyDuplicateDraftRecovery: dependency( + "applyDuplicateDraftRecovery", + defaultApplyDuplicateDraftRecovery, + ), + canonicalDuplicateDraftEvidence: dependency( + "canonicalDuplicateDraftEvidence", + defaultCanonicalDuplicateDraftEvidence, + ), + captureDuplicateDraftRecoveryEvidence: dependency( + "captureDuplicateDraftRecoveryEvidence", + defaultCaptureDuplicateDraftRecoveryEvidence, + ), + createDuplicateDraftRecoveryReader: dependency( + "createDuplicateDraftRecoveryReader", + defaultCreateDuplicateDraftRecoveryReader, + ), + createDuplicateDraftRecoveryWriter: dependency( + "createDuplicateDraftRecoveryWriter", + defaultCreateDuplicateDraftRecoveryWriter, + ), + createNormalProductionRecoveryObserver: dependency( + "createNormalProductionRecoveryObserver", + createNormalProductionRecoveryObserver, + ), + createProductionRecoveryObserver: dependency( + "createProductionRecoveryObserver", + createProductionRecoveryObserver, + ), + parseDuplicateDraftEvidence: dependency( + "parseDuplicateDraftEvidence", + defaultParseDuplicateDraftEvidence, + ), + }) +} + +function validateDependencies(value) { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + ![Object.prototype, null].includes(Object.getPrototypeOf(value)) + ) { + throw new RecoveryInputError() + } + for (const key of Reflect.ownKeys(value)) { + const descriptor = typeof key === "string" ? Object.getOwnPropertyDescriptor(value, key) : null + if ( + typeof key !== "string" || + !DEPENDENCY_FIELDS.includes(key) || + descriptor === null || + !descriptor.enumerable || + !("value" in descriptor) || + (key !== "fileSystem" && typeof descriptor.value !== "function") + ) { + throw new RecoveryInputError() + } + } +} + +function snapshotArgumentArray(value) { + if (!Array.isArray(value) || value.length > 8) throw new RecoveryInputError() + const output = [] + for (let index = 0; index < value.length; index += 1) { + const descriptor = Object.getOwnPropertyDescriptor(value, index) + if ( + descriptor === undefined || + !descriptor.enumerable || + !("value" in descriptor) || + typeof descriptor.value !== "string" || + descriptor.value.length === 0 || + Buffer.byteLength(descriptor.value, "utf8") > MAX_PATH_BYTES || + hasControlCharacters(descriptor.value) + ) { + throw new RecoveryInputError() + } + output.push(descriptor.value) + } + return output +} + +function normalizePrivatePath(value) { + if ( + typeof value !== "string" || + value.length === 0 || + Buffer.byteLength(value, "utf8") > MAX_PATH_BYTES || + hasControlCharacters(value) || + path.isAbsolute(value) || + value !== path.normalize(value) || + !value.startsWith(`${RECOVERY_DIRECTORY}/`) + ) { + throw new RecoveryInputError() + } + const relative = path.relative(RECOVERY_DIRECTORY, value) + if ( + relative === "" || + relative.startsWith("..") || + path.isAbsolute(relative) || + relative.split(path.sep).some((part) => part.length === 0 || part === "." || part === "..") + ) { + throw new RecoveryInputError() + } + return value +} + +function resolveInvocationPaths(root, options) { + const output = resolvePrivatePath(root, options.output) + if (options.command === "capture") return { output } + return { evidence: resolvePrivatePath(root, options.evidence), output } +} + +function resolvePrivatePath(root, relative) { + const absolute = path.resolve(root, relative) + const boundary = path.resolve(root, RECOVERY_DIRECTORY) + const fromBoundary = path.relative(boundary, absolute) + if (fromBoundary === "" || fromBoundary.startsWith("..") || path.isAbsolute(fromBoundary)) { + throw new RecoveryInputError() + } + return Object.freeze({ absolute, relative }) +} + +async function resolveRepositoryRoot(cwd, runGit) { + let output + try { + output = await runGit("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { + encoding: "utf8", + timeout: 10_000, + maxBuffer: MAX_GIT_OUTPUT_BYTES, + windowsHide: true, + }) + } catch { + throw new Error("Recovery repository root is unavailable") + } + const root = output.trim() + if ( + root.length === 0 || + Buffer.byteLength(root, "utf8") > MAX_PATH_BYTES || + hasControlCharacters(root) || + !path.isAbsolute(root) || + path.resolve(root) !== root + ) { + throw new Error("Recovery repository root is invalid") + } + return root +} + +async function assertReviewedIgnorePolicy({ + fileSystem, + root, + reviewedCommit, + relativePaths, + runGit, +}) { + if ( + !(reviewedCommit === "HEAD" || SHA_PATTERN.test(reviewedCommit)) || + !Array.isArray(relativePaths) || + relativePaths.length < 1 + ) { + throw new Error("Recovery ignore authority is invalid") + } + let reviewedText + try { + reviewedText = await runGit("git", ["-C", root, "show", `${reviewedCommit}:.gitignore`], { + encoding: "utf8", + timeout: 10_000, + maxBuffer: 64 * 1024, + windowsHide: true, + }) + } catch { + throw new Error("Recovery reviewed gitignore is unavailable") + } + const currentBytes = await readBoundedPrivateFile( + fileSystem, + path.join(root, ".gitignore"), + 64 * 1024, + { requirePrivateMode: false }, + ) + const currentText = new TextDecoder("utf-8", { fatal: true }).decode(currentBytes) + if (currentText !== reviewedText || !hasReviewedRecoveryIgnoreRule(reviewedText)) { + throw new Error("Recovery reviewed gitignore rule is absent or changed") + } + for (const relative of relativePaths) { + try { + await runGit("git", ["-C", root, "check-ignore", "--quiet", "--no-index", "--", relative], { + encoding: "utf8", + timeout: 10_000, + maxBuffer: MAX_GIT_OUTPUT_BYTES, + windowsHide: true, + }) + } catch { + throw new Error("Recovery private path is not ignored by reviewed policy") + } + } +} + +function hasReviewedRecoveryIgnoreRule(source) { + if (typeof source !== "string" || Buffer.byteLength(source, "utf8") > 64 * 1024) return false + const accepted = new Set([ + ".dawn/", + "/.dawn/", + ".dawn/release-recovery/", + "/.dawn/release-recovery/", + ]) + return source.split(/\r?\n/u).some((line) => accepted.has(line)) +} + +function createScrubbedGitRunner(executor) { + if (typeof executor !== "function") throw new RecoveryInputError() + const policy = recoveryGitExecutionPolicy() + const safeEnvironment = safeGitEnvironment() + return (command, args, options = {}) => { + if (command !== "git" || !Array.isArray(args)) { + throw new Error("Recovery Git invocation is invalid") + } + return executor( + policy.executable, + [ + "-c", + `core.excludesFile=${policy.nullDevice}`, + "-c", + "credential.helper=", + "--no-pager", + ...args, + ], + { + ...options, + shell: false, + env: { ...safeEnvironment }, + windowsHide: true, + }, + ) + } +} + +export function recoveryGitExecutionPolicy(platform = process.platform) { + if (platform !== "darwin" && platform !== "linux") { + throw new Error("Recovery trusted Git execution is unavailable on this platform") + } + return Object.freeze({ executable: "/usr/bin/git", nullDevice: "/dev/null" }) +} + +function safeGitEnvironment() { + const descriptor = Object.getOwnPropertyDescriptor(process.env, "PATH") + const executablePath = descriptor?.value + if ( + typeof executablePath !== "string" || + executablePath.length === 0 || + hasControlCharacters(executablePath) + ) { + throw new Error("Recovery executable path is invalid") + } + return Object.freeze({ + PATH: executablePath, + HOME: "/nonexistent", + XDG_CONFIG_HOME: "/nonexistent", + LANG: "C", + LC_ALL: "C", + GCM_INTERACTIVE: "never", + }) +} + +function defaultGitExecutor(command, args, options) { + return execFile(command, args, options).then(({ stdout }) => stdout) +} + +async function assertPrivatePathBoundary(fileSystem, root, target) { + const operations = fileSystemOperations(fileSystem, ["lstat"]) + const boundary = path.resolve(root, RECOVERY_DIRECTORY) + const fromBoundary = path.relative(boundary, target.absolute) + if ( + fromBoundary === "" || + fromBoundary.startsWith("..") || + path.isAbsolute(fromBoundary) || + path.resolve(root, target.relative) !== target.absolute + ) { + throw new Error("Recovery private path escaped containment") + } + const parent = path.dirname(target.absolute) + const relativeParent = path.relative(root, parent) + const currentUid = typeof process.getuid === "function" ? BigInt(process.getuid()) : null + let current = root + for (const part of ["", ...relativeParent.split(path.sep)]) { + if (part.length > 0) current = path.join(current, part) + const state = await operations.lstat(current, { bigint: true }) + const mode = state.mode & 0o777n + if ( + !state.isDirectory() || + state.isSymbolicLink() || + (currentUid !== null && state.uid !== currentUid) || + (mode & 0o022n) !== 0n || + (current === boundary && mode !== 0o700n) + ) { + throw new Error("Recovery private path parent is unsafe") + } + } +} + +async function assertUnusedOutput(fileSystem, target) { + const operations = fileSystemOperations(fileSystem, ["lstat"]) + try { + await operations.lstat(target) + } catch (error) { + if (error?.code === "ENOENT") return + throw error + } + throw new Error("Recovery output already exists") +} + +async function readBoundedPrivateFile( + fileSystem, + target, + maximumBytes, + { requirePrivateMode = true } = {}, +) { + const operations = fileSystemOperations(fileSystem, ["lstat", "open"]) + if (!Number.isInteger(fsConstants.O_NOFOLLOW)) { + throw new Error("Recovery no-follow reads are unavailable") + } + let handle + try { + handle = await operations.open(target, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW) + } catch { + throw new Error("Recovery evidence is unavailable") + } + try { + const before = await handle.stat({ bigint: true }) + if ( + !before.isFile() || + before.nlink !== 1n || + (requirePrivateMode && (before.mode & 0o077n) !== 0n) || + before.size < 1n || + before.size > BigInt(maximumBytes) || + before.size > BigInt(Number.MAX_SAFE_INTEGER) + ) { + throw new Error("Recovery evidence is not one bounded private file") + } + const bytes = Buffer.allocUnsafe(Number(before.size)) + let offset = 0 + while (offset < bytes.byteLength) { + const { bytesRead } = await handle.read(bytes, offset, bytes.byteLength - offset, offset) + if (bytesRead === 0) break + offset += bytesRead + } + const after = await handle.stat({ bigint: true }) + const linked = await operations.lstat(target, { bigint: true }) + if ( + offset !== bytes.byteLength || + !sameFileIdentity(before, after) || + linked.isSymbolicLink() || + !sameFileIdentity(after, linked) + ) { + throw new Error("Recovery evidence changed while it was read") + } + return bytes + } finally { + await handle.close() + } +} + +async function reserveExclusiveOutput(runtime, target) { + const operations = fileSystemOperations(runtime.fileSystem, ["link", "lstat", "open", "unlink"]) + if (!Number.isInteger(fsConstants.O_DIRECTORY) || !Number.isInteger(fsConstants.O_NOFOLLOW)) { + throw new Error("Recovery durable output primitives are unavailable") + } + await assertUnusedOutput(runtime.fileSystem, target) + const directory = path.dirname(target) + const directoryHandle = await operations.open( + directory, + fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW, + ) + let directoryIdentity = null + let temporaryHandle = null + let temporary = null + let temporaryIdentity = null + let temporaryCreationAttempted = false + let temporaryOpenCompleted = false + let temporaryPathCollision = false + try { + directoryIdentity = await directoryHandle.stat({ bigint: true }) + if (!directoryIdentity.isDirectory()) throw new Error("Recovery output directory is invalid") + await assertDirectoryIdentity(operations, directory, directoryIdentity) + // Prove directory fsync works before any production mutation is possible. + await directoryHandle.sync() + const identifier = runtime.randomUUID() + if (typeof identifier !== "string" || !UUID_PATTERN.test(identifier)) { + throw new Error("Recovery temporary output identity is invalid") + } + temporary = path.join(directory, `.${path.basename(target)}.${identifier}.tmp`) + temporaryCreationAttempted = true + temporaryHandle = await operations.open( + temporary, + fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW, + 0o600, + ) + temporaryOpenCompleted = true + temporaryIdentity = await temporaryHandle.stat({ bigint: true }) + assertReservedTemporaryIdentity(temporaryIdentity) + + let settled = false + let publicationAttempted = false + let targetPublished = false + let publishedIdentity = null + return Object.freeze({ + async commit(value, maximumBytes) { + if (settled) throw new Error("Recovery output reservation is already settled") + const bytes = normalizeOutputBytes(value, maximumBytes) + await assertDirectoryIdentity(operations, directory, directoryIdentity) + await temporaryHandle.writeFile(bytes) + await temporaryHandle.sync() + const identity = await temporaryHandle.stat({ bigint: true }) + if ( + !identity.isFile() || + identity.nlink !== 1n || + identity.size !== BigInt(bytes.byteLength) || + (identity.mode & 0o777n) !== 0o600n + ) { + throw new Error("Recovery temporary output was not durably written") + } + await temporaryHandle.close() + temporaryHandle = null + await assertDirectoryIdentity(operations, directory, directoryIdentity) + publicationAttempted = true + publishedIdentity = identity + await operations.link(temporary, target) + targetPublished = true + const linked = await operations.lstat(target, { bigint: true }) + if ( + linked.isSymbolicLink() || + linked.dev !== identity.dev || + linked.ino !== identity.ino || + linked.size !== identity.size || + linked.nlink < 2n + ) { + throw new Error("Recovery output link identity is invalid") + } + await directoryHandle.sync() + await operations.unlink(temporary) + await directoryHandle.sync() + temporary = null + temporaryCreationAttempted = false + temporaryIdentity = null + const final = await operations.lstat(target, { bigint: true }) + if ( + final.isSymbolicLink() || + final.dev !== identity.dev || + final.ino !== identity.ino || + final.size !== identity.size || + final.nlink !== 1n || + (final.mode & 0o777n) !== 0o600n + ) { + throw new Error("Recovery output final identity is invalid") + } + await directoryHandle.close() + settled = true + }, + async abort() { + if (settled) return + const cleanupFailures = [] + let handleCloseFailure = null + if (temporaryHandle !== null) { + handleCloseFailure = await closeHandleWithRetries(temporaryHandle) + if (handleCloseFailure === null) temporaryHandle = null + } + let cleanupComplete = false + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + await cleanupReservationFiles() + cleanupComplete = true + cleanupFailures.length = 0 + break + } catch (error) { + cleanupFailures.push(error) + } + } + const directoryCloseFailure = await closeHandleWithRetries(directoryHandle) + if (!cleanupComplete || publicationAttempted || targetPublished || temporary !== null) { + throw new RecoveryOutputCleanupUncertainError(cleanupFailures) + } + settled = true + const closeFailures = [handleCloseFailure, directoryCloseFailure].filter( + (failure) => failure !== null, + ) + if (closeFailures.length > 0) { + throw new AggregateError(closeFailures, "Recovery output handle cleanup failed") + } + }, + }) + + async function cleanupReservationFiles() { + if (settled) return + if (publicationAttempted) { + let targetState = null + try { + targetState = await operations.lstat(target, { bigint: true }) + } catch (error) { + if (error?.code !== "ENOENT") throw error + } + if (targetState !== null) { + if ( + publishedIdentity === null || + targetState.isSymbolicLink() || + targetState.dev !== publishedIdentity.dev || + targetState.ino !== publishedIdentity.ino + ) { + throw new Error("Recovery published output identity is uncertain") + } + await operations.unlink(target) + } + } + if (temporary !== null) { + const state = await optionalLstat(operations, temporary) + if (state !== null) { + if ( + temporaryIdentity === null || + state.isSymbolicLink() || + !sameDeviceAndInode(state, temporaryIdentity) + ) { + throw new Error("Recovery temporary output identity is uncertain") + } + await operations.unlink(temporary) + } + } + await proveOutputPathsAbsent({ directoryHandle, operations, target, temporary }) + publicationAttempted = false + targetPublished = false + publishedIdentity = null + temporary = null + temporaryCreationAttempted = false + temporaryIdentity = null + } + } catch (error) { + temporaryPathCollision = + temporaryCreationAttempted && !temporaryOpenCompleted && error?.code === "EEXIST" + const handleCloseFailure = + temporaryHandle === null ? null : await closeHandleWithRetries(temporaryHandle) + if (handleCloseFailure === null) temporaryHandle = null + const cleanupFailures = [] + let cleanupComplete = false + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + await cleanupReservationSetupFiles() + cleanupComplete = true + cleanupFailures.length = 0 + break + } catch (cleanupError) { + cleanupFailures.push(cleanupError) + } + } + const directoryCloseFailure = await closeHandleWithRetries(directoryHandle) + if (!cleanupComplete || temporary !== null) { + throw new RecoveryOutputCleanupUncertainError(cleanupFailures) + } + const closeFailures = [handleCloseFailure, directoryCloseFailure].filter( + (failure) => failure !== null, + ) + if (closeFailures.length > 0) { + throw new AggregateError( + [error, ...closeFailures], + "Recovery output setup handle cleanup failed", + ) + } + throw error + } + + async function cleanupReservationSetupFiles() { + if (temporaryCreationAttempted && temporary !== null) { + const state = await optionalLstat(operations, temporary) + if (state !== null) { + if (temporaryPathCollision) { + throw new Error("Recovery temporary output path is already occupied") + } + if (temporaryIdentity !== null) { + if (state.isSymbolicLink() || !sameDeviceAndInode(state, temporaryIdentity)) { + throw new Error("Recovery setup temporary identity is uncertain") + } + } else { + assertAmbiguousSetupTemporaryIdentity(state) + } + await operations.unlink(temporary) + } + } + await proveOutputPathsAbsent({ directoryHandle, operations, target, temporary }) + temporary = null + temporaryCreationAttempted = false + temporaryPathCollision = false + temporaryIdentity = null + } +} + +function assertReservedTemporaryIdentity(identity) { + if ( + !identity.isFile() || + identity.isSymbolicLink() || + identity.nlink !== 1n || + identity.size !== 0n || + (identity.mode & 0o777n) !== 0o600n + ) { + throw new Error("Recovery temporary output reservation is unsafe") + } +} + +function assertAmbiguousSetupTemporaryIdentity(identity) { + const currentUid = typeof process.getuid === "function" ? BigInt(process.getuid()) : null + if ( + !identity.isFile() || + identity.isSymbolicLink() || + identity.nlink !== 1n || + identity.size !== 0n || + (identity.mode & 0o777n) !== 0o600n || + (currentUid !== null && identity.uid !== currentUid) + ) { + throw new Error("Recovery ambiguous setup temporary identity is unsafe") + } +} + +async function proveOutputPathsAbsent({ directoryHandle, operations, target, temporary }) { + await directoryHandle.sync() + await assertPathAbsent(operations, target) + if (temporary !== null) await assertPathAbsent(operations, temporary) + await directoryHandle.sync() + await assertPathAbsent(operations, target) + if (temporary !== null) await assertPathAbsent(operations, temporary) +} + +async function assertPathAbsent(operations, target) { + if ((await optionalLstat(operations, target)) !== null) { + throw new Error("Recovery output path absence is uncertain") + } +} + +async function optionalLstat(operations, target) { + try { + return await operations.lstat(target, { bigint: true }) + } catch (error) { + if (error?.code === "ENOENT") return null + throw error + } +} + +async function closeHandleWithRetries(handle) { + let lastError = null + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + await handle.close() + return null + } catch (error) { + lastError = error + } + } + return lastError +} + +function sameDeviceAndInode(left, right) { + return left.dev === right.dev && left.ino === right.ino +} + +function normalizeOutputBytes(value, maximumBytes) { + if (!(value instanceof Uint8Array)) throw new Error("Recovery output bytes are invalid") + const bytes = Buffer.from(value) + if (bytes.byteLength < 1 || bytes.byteLength > maximumBytes) { + throw new Error("Recovery output bytes exceed their bound") + } + return bytes +} + +function canonicalFinalAuthorizationReceiptBytes(value, token) { + assertDeepFrozenData(value, "Recovery final authorization receipt") + const receipt = snapshotJson(value) + validateFinalAuthorizationReceipt(receipt) + assertCredentialFreeReceipt(receipt, token) + return normalizeOutputBytes( + Buffer.from(`${JSON.stringify(canonicalize(receipt))}\n`, "utf8"), + MAX_RECEIPT_BYTES, + ) +} + +function validateFinalAuthorizationReceipt(receipt) { + assertExactFields( + receipt, + [ + "schemaVersion", + "atomic", + "concurrencyAcknowledgement", + "freezeScope", + "evidenceCapturedAt", + "appliedAt", + "candidate", + "duplicates", + "finalAuthorization", + ], + "Recovery final authorization receipt", + ) + if ( + receipt.schemaVersion !== 1 || + receipt.atomic !== false || + !isCanonicalTimestamp(receipt.evidenceCapturedAt) || + !isCanonicalTimestamp(receipt.appliedAt) || + Date.parse(receipt.appliedAt) < Date.parse(receipt.evidenceCapturedAt) + ) { + throw new Error("Recovery final authorization receipt is invalid") + } + validateAcknowledgement(receipt.concurrencyAcknowledgement) + assertExactFields(receipt.freezeScope, ["mode", "releaseIds"], "Recovery freeze scope") + if ( + receipt.freezeScope.mode !== "operator-freeze-compare-before-write-v1" || + !arraysEqual( + receipt.freezeScope.releaseIds, + DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates.map(({ releaseId }) => releaseId), + ) + ) { + throw new Error("Recovery freeze scope is invalid") + } + assertExactFields( + receipt.candidate, + ["version", "commitSha", "releaseId"], + "Recovery receipt candidate", + ) + if ( + receipt.candidate.version !== DUPLICATE_DRAFT_RECOVERY_POLICY.version || + receipt.candidate.commitSha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha || + receipt.candidate.releaseId !== DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId + ) { + throw new Error("Recovery receipt candidate is invalid") + } + if ( + !Array.isArray(receipt.duplicates) || + receipt.duplicates.length !== DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates.length + ) { + throw new Error("Recovery receipt duplicate results are invalid") + } + for (const [index, duplicate] of receipt.duplicates.entries()) { + const expected = DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates[index] + if (duplicate?.outcome === "performed") { + assertExactFields( + duplicate, + ["releaseId", "outcome", "preWriteFence", "postWriteFence"], + "Recovery performed result", + ) + if (duplicate.releaseId !== expected.releaseId) { + throw new Error("Recovery performed result identity is invalid") + } + validateFence(duplicate.preWriteFence) + validateFence(duplicate.postWriteFence) + if ( + Date.parse(duplicate.preWriteFence.observedAt) > + Date.parse(duplicate.postWriteFence.observedAt) + ) { + throw new Error("Recovery performed result timeline is invalid") + } + continue + } + assertExactFields( + duplicate, + ["releaseId", "outcome", "priorFenceObservations", "verifiedAt", "projectionSha256"], + "Recovery preexisting result", + ) + if ( + duplicate.releaseId !== expected.releaseId || + duplicate.outcome !== "preexisting-quarantined" || + duplicate.priorFenceObservations !== null || + !isCanonicalTimestamp(duplicate.verifiedAt) || + !SHA256_PATTERN.test(duplicate.projectionSha256) + ) { + throw new Error("Recovery preexisting result is invalid") + } + } + assertExactFields( + receipt.finalAuthorization, + ["state", "disposition", "nextTransition", "conflicts", "diagnostics", "releaseId"], + "Recovery final observer result", + ) + if ( + receipt.finalAuthorization.state !== "CANDIDATE_ESCROWED" || + receipt.finalAuthorization.disposition !== "would-transition" || + receipt.finalAuthorization.nextTransition !== "publish-npm-packages" || + !arraysEqual(receipt.finalAuthorization.conflicts, []) || + !arraysEqual(receipt.finalAuthorization.diagnostics, []) || + receipt.finalAuthorization.releaseId !== DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId + ) { + throw new Error("Recovery final observer result is invalid") + } +} + +function validateAcknowledgement(value) { + assertExactFields( + value, + ["acknowledged", "atomic", "mode", "releaseIds"], + "Recovery concurrency acknowledgement", + ) + if ( + value.acknowledged !== true || + value.atomic !== false || + value.mode !== "operator-freeze-compare-before-write-v1" || + !arraysEqual( + value.releaseIds, + DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates.map(({ releaseId }) => releaseId), + ) + ) { + throw new Error("Recovery concurrency acknowledgement is invalid") + } +} + +function validateFence(value) { + assertExactFields( + value, + ["observedAt", "projectionSha256", "tagObjectSha"], + "Recovery write fence", + ) + if ( + !isCanonicalTimestamp(value.observedAt) || + !SHA256_PATTERN.test(value.projectionSha256) || + !SHA_PATTERN.test(value.tagObjectSha) + ) { + throw new Error("Recovery write fence is invalid") + } +} + +function assertExactFields(value, fields, label) { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + !arraysEqual(Object.keys(value).sort(), [...fields].sort()) + ) { + throw new Error(`${label} fields are invalid`) + } +} + +function assertDeepFrozenData(value, label, ancestors = new Set()) { + if (value === null || typeof value !== "object") return + if (isProxy(value) || ancestors.has(value) || !Object.isFrozen(value)) { + throw new Error(`${label} is not immutable plain data`) + } + const prototype = Object.getPrototypeOf(value) + if (!Array.isArray(value) && ![Object.prototype, null].includes(prototype)) { + throw new Error(`${label} is not immutable plain data`) + } + ancestors.add(value) + if (Array.isArray(value) && Object.keys(value).length !== value.length) { + throw new Error(`${label} is not immutable plain data`) + } + for (const key of Reflect.ownKeys(value)) { + if (Array.isArray(value) && key === "length") continue + const descriptor = typeof key === "string" ? Object.getOwnPropertyDescriptor(value, key) : null + if ( + typeof key !== "string" || + descriptor === null || + !descriptor.enumerable || + !("value" in descriptor) + ) { + throw new Error(`${label} is not immutable plain data`) + } + assertDeepFrozenData(descriptor.value, label, ancestors) + } + ancestors.delete(value) +} + +// Capture evidence legitimately carries the canonical Release body, which contains +// GitHub URLs, so it cannot use the receipt's stricter transport rule. The design +// still requires credential-free facts, so enforce that explicitly on the exact +// bytes about to be published rather than relying on the read path's scrubbing. +function assertCredentialFreeEvidence(bytes, token) { + if (!Buffer.isBuffer(bytes)) throw new Error("Recovery evidence bytes are invalid") + if (typeof token !== "string" || token.length === 0) { + throw new Error("Recovery GitHub credential is unavailable") + } + if (bytes.toString("utf8").includes(token)) { + throw new Error("Recovery evidence contains transport data") + } +} + +function assertCredentialFreeReceipt(value, token) { + if (typeof value === "string") { + if (value.includes(token) || /https?:\/\//iu.test(value)) { + throw new Error("Recovery final authorization receipt contains transport data") + } + return + } + if (Array.isArray(value)) { + for (const child of value) assertCredentialFreeReceipt(child, token) + return + } + if (value !== null && typeof value === "object") { + for (const [key, child] of Object.entries(value)) { + assertCredentialFreeReceipt(key, token) + assertCredentialFreeReceipt(child, token) + } + } +} + +function isCanonicalTimestamp(value) { + if (typeof value !== "string") return false + const milliseconds = Date.parse(value) + return Number.isSafeInteger(milliseconds) && new Date(milliseconds).toISOString() === value +} + +function arraysEqual(left, right) { + return ( + Array.isArray(left) && + Array.isArray(right) && + left.length === right.length && + left.every((value, index) => value === right[index]) + ) +} + +function concurrencyAcknowledgement() { + const releaseIds = Object.freeze( + DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates.map(({ releaseId }) => releaseId), + ) + return Object.freeze({ + acknowledged: true, + atomic: false, + mode: "operator-freeze-compare-before-write-v1", + releaseIds, + }) +} + +export function createProductionRecoveryObserver({ + root, + token, + reader, + environment, + fileSystem, + runGit, + createNormalObserver = createNormalProductionRecoveryObserver, +}) { + assertDuplicateDraftRecoveryReader(reader) + // Retain the exact repository environment gate used by the normal release + // observer without allowing it to supply credentials or candidate identity. + const repository = environmentDataProperty(environment, "GITHUB_REPOSITORY") + if (repository !== undefined && repository !== "cacheplane/dawnai") { + throw new TypeError("Recovery production repository is invalid") + } + if (typeof createNormalObserver !== "function") { + throw new TypeError("Recovery normal observer factory is invalid") + } + const normalObserver = createNormalObserver({ root, token, fileSystem, runGit }) + if (typeof normalObserver !== "function") { + throw new TypeError("Recovery normal observer is invalid") + } + return async ({ candidate }) => { + assertRecoveryObserverCandidate(candidate) + const before = await readRecoveryObserverBinding(reader, candidate) + const normal = snapshotJson(await normalObserver({ candidate })) + assertExactFields( + normal, + ["state", "disposition", "nextTransition", "conflicts", "diagnostics"], + "Recovery normal observer result", + ) + const after = await readRecoveryObserverBinding(reader, candidate) + if (!sameCanonicalData(before, after)) { + throw new Error("Recovery production Release binding drifted during final authorization") + } + return Object.freeze({ + state: normal.state, + disposition: normal.disposition, + nextTransition: normal.nextTransition, + conflicts: Object.freeze([...normal.conflicts]), + diagnostics: Object.freeze([...normal.diagnostics]), + releaseId: after.releaseId, + }) + } +} + +export function createNormalProductionRecoveryObserver({ root, token, fileSystem, runGit }) { + const git = createGitReader({ root, run: runGit }) + const github = createGitHubReader({ + owner: "cacheplane", + repo: "dawnai", + repositoryId: "1210070282", + token, + }) + const npm = createNpmReader() + const inventory = createProductionInventoryReader({ root, git }) + const attestations = createCliAttestationVerifier({ + repository: "cacheplane/dawnai", + token, + fileSystem, + }) + return async ({ candidate }) => { + const [managedInventory, marker] = await Promise.all([ + inventory.read({ ref: candidate.commitSha }), + readCandidateControllerMarker({ git, candidate }), + ]) + const observed = await observeProductionCandidate({ + candidate, + inventory: managedInventory, + marker, + git, + github, + npm, + attestations, + }) + const plan = planRelease({ + candidate, + observation: observed.observation, + mode: "controller", + }) + return { + state: plan.state, + disposition: plan.disposition, + nextTransition: plan.nextTransition, + conflicts: plan.conflicts, + diagnostics: observed.diagnostics, + } + } +} + +function assertRecoveryObserverCandidate(candidate) { + assertExactFields(candidate, ["version", "commitSha"], "Recovery observer candidate") + if ( + candidate.version !== DUPLICATE_DRAFT_RECOVERY_POLICY.version || + candidate.commitSha !== DUPLICATE_DRAFT_RECOVERY_POLICY.candidateSha + ) { + throw new Error("Recovery observer candidate identity is not exact") + } +} + +async function readRecoveryObserverBinding(reader, candidate) { + const inventory = snapshotJson(await reader.listCandidateReleases()) + if (!Array.isArray(inventory) || inventory.length !== 3) { + throw new Error("Recovery production candidate inventory is not exact") + } + const expected = [ + ...DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates, + { + releaseId: DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId, + tagName: "untagged-be0ff4bee4ba43b521a9", + }, + ].sort((left, right) => left.releaseId - right.releaseId) + inventory.sort((left, right) => left?.releaseId - right?.releaseId) + let canonicalSummary = null + for (const [index, release] of inventory.entries()) { + assertExactFields( + release, + [ + "releaseId", + "tagName", + "title", + "draft", + "prerelease", + "immutable", + "targetCommitish", + "marker", + ], + "Recovery production candidate Release", + ) + if ( + release.releaseId !== expected[index].releaseId || + release.tagName !== expected[index].tagName || + release.title !== `Dawn v${candidate.version}` || + release.draft !== true || + release.prerelease !== false || + release.immutable !== false || + release.targetCommitish !== "main" + ) { + throw new Error("Recovery production candidate Release identity is not exact") + } + if (release.releaseId === DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId) { + if (!isExactRecoveryCandidateMarker(release.marker, candidate)) { + throw new Error("Recovery production canonical Release marker is not exact") + } + canonicalSummary = release + } else if (release.marker !== null) { + throw new Error("Recovery production duplicate Release remains controller-visible") + } + } + if (canonicalSummary === null) { + throw new Error("Recovery production canonical Release identity is unavailable") + } + const snapshot = snapshotJson( + await reader.readReleaseSnapshot(DUPLICATE_DRAFT_RECOVERY_POLICY.canonicalReleaseId), + ) + assertExactFields( + snapshot, + [ + "releaseId", + "tagName", + "title", + "targetCommitish", + "draft", + "prerelease", + "immutable", + "body", + "marker", + "assets", + ], + "Recovery production canonical Release snapshot", + ) + const projection = normalizeDuplicateDraftReleaseProjection(snapshot) + if ( + projection.releaseId !== canonicalSummary.releaseId || + projection.tagName !== canonicalSummary.tagName || + projection.title !== canonicalSummary.title || + projection.targetCommitish !== canonicalSummary.targetCommitish || + projection.draft !== canonicalSummary.draft || + projection.prerelease !== canonicalSummary.prerelease || + projection.immutable !== canonicalSummary.immutable || + !sameCanonicalData(snapshot.marker, canonicalSummary.marker) + ) { + throw new Error("Recovery production canonical Release identity is not exact") + } + return { + releaseId: projection.releaseId, + inventory, + projection, + marker: snapshot.marker, + } +} + +function isExactRecoveryCandidateMarker(value, candidate) { + return ( + value !== null && + typeof value === "object" && + value.version === candidate.version && + value.commitSha === candidate.commitSha && + value.tag === `v${candidate.version}` + ) +} + +function sameCanonicalData(left, right) { + return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right)) +} + +export async function readCandidateControllerMarker({ git, candidate }) { + if ( + git === null || + typeof git?.showFile !== "function" || + candidate === null || + typeof candidate !== "object" || + !SHA_PATTERN.test(candidate.commitSha) + ) { + throw new TypeError("Recovery production controller marker authority is invalid") + } + const source = await git.showFile({ + ref: candidate.commitSha, + path: "scripts/release/controller-schema.json", + }) + if (typeof source !== "string") { + throw new TypeError("Recovery production controller marker is invalid") + } + const bytes = Buffer.from(source, "utf8") + if (bytes.byteLength < 1 || bytes.byteLength > 64 * 1024) { + throw new TypeError("Recovery production controller marker is outside bounds") + } + try { + return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) + } catch { + throw new TypeError("Recovery production controller marker is invalid") + } +} + +function environmentToken(environment) { + const token = environmentDataProperty(environment, "GITHUB_TOKEN") + if ( + typeof token !== "string" || + token.length === 0 || + token.length > 4_096 || + hasControlCharacters(token) + ) { + throw new Error("Recovery GitHub credential is unavailable") + } + return token +} + +function environmentDataProperty(environment, name) { + if (environment === null || typeof environment !== "object") { + throw new RecoveryInputError() + } + const descriptor = Object.getOwnPropertyDescriptor(environment, name) + if (descriptor === undefined) return undefined + if (!descriptor.enumerable || !("value" in descriptor)) { + throw new Error("Recovery environment is unsafe") + } + return descriptor.value +} + +function dataProperty(value, name) { + const descriptor = Object.getOwnPropertyDescriptor(value, name) + return descriptor === undefined ? undefined : descriptor.value +} + +function fileSystemOperations(fileSystem, methods) { + if (fileSystem === null || (typeof fileSystem !== "object" && typeof fileSystem !== "function")) { + throw new RecoveryInputError() + } + const output = Object.create(null) + for (const method of methods) { + const descriptor = Object.getOwnPropertyDescriptor(fileSystem, method) + if ( + descriptor === undefined || + !("value" in descriptor) || + typeof descriptor.value !== "function" + ) { + throw new RecoveryInputError() + } + output[method] = descriptor.value.bind(fileSystem) + } + return Object.freeze(output) +} + +async function assertDirectoryIdentity(operations, directory, expected) { + const actual = await operations.lstat(directory, { bigint: true }) + if ( + !actual.isDirectory() || + actual.isSymbolicLink() || + actual.dev !== expected.dev || + actual.ino !== expected.ino + ) { + throw new Error("Recovery output directory changed during operation") + } +} + +function sameFileIdentity(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 canonicalize(value) { + if (Array.isArray(value)) return value.map(canonicalize) + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, canonicalize(value[key])]), + ) + } + return value +} + +function hasControlCharacters(value) { + return [...value].some((character) => { + const codePoint = character.codePointAt(0) + return codePoint <= 31 || codePoint === 127 + }) +} + +function writeSuccessBestEffort(stream, message) { + let cleanupScheduled = false + const removeErrorListener = () => { + if (typeof stream.removeListener === "function") stream.removeListener("error", onError) + } + const scheduleCleanup = () => { + if (cleanupScheduled) return + cleanupScheduled = true + setImmediate(removeErrorListener) + } + const onError = () => { + removeErrorListener() + } + try { + if (typeof stream.once === "function" && typeof stream.removeListener === "function") { + stream.once("error", onError) + } + const result = stream.write(message, scheduleCleanup) + if (result !== null && typeof result === "object" && typeof result.then === "function") { + void result.then(scheduleCleanup, scheduleCleanup) + } + scheduleCleanup() + } catch { + scheduleCleanup() + } +} + +class RecoveryOutputCleanupUncertainError extends Error { + constructor(errors = []) { + super("Recovery output cleanup is uncertain") + this.name = "RecoveryOutputCleanupUncertainError" + this.errors = errors + } +} + +class RecoveryInputError extends Error { + constructor() { + super("Invalid duplicate draft recovery input") + } +} + +const executedPath = + process.argv[1] === undefined ? null : pathToFileURL(path.resolve(process.argv[1])).href +if (executedPath === import.meta.url) { + process.exitCode = await runDuplicateDraftRecoveryCli() +} diff --git a/scripts/release/test/duplicate-draft-recovery-adapters.test.mjs b/scripts/release/test/duplicate-draft-recovery-adapters.test.mjs new file mode 100644 index 000000000..a68e12257 --- /dev/null +++ b/scripts/release/test/duplicate-draft-recovery-adapters.test.mjs @@ -0,0 +1,3238 @@ +import assert from "node:assert/strict" +import { createHash } from "node:crypto" +import test from "node:test" +import { + canonicalRecoveryNotice, + canonicalRecoveryReceipt, + originalBodyAssetName, + recoveryReceiptAssetName, +} from "../duplicate-draft-recovery.mjs" +import { + createDuplicateDraftRecoveryReader, + createDuplicateDraftRecoveryWriter, +} from "../duplicate-draft-recovery-adapters.mjs" +import { CANONICAL_RELEASE_PACKAGE_ORDER, canonicalManifestBytes } from "../manifest.mjs" +import { canonicalReleaseBody, parseReleaseMarker } from "../metadata.mjs" + +const EXPECTED_METHODS = [ + "listCandidateReleases", + "readCandidatePublishJobs", + "readCandidateTag", + "readImmutableReleases", + "readNpmAbsence", + "readReleaseRuns", + "readReleaseSnapshot", + "readRepositoryState", + "readReviewedMergeAuthority", + "readWorkflowState", +] +const REVIEWED_COMMIT = "a".repeat(40) +const REVIEWED_HEAD = "b".repeat(40) +const TREE = "c".repeat(40) +const TAG_OBJECT = "d".repeat(40) +const CANDIDATE_SHA = "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8" +const BASE = "https://api.github.com/repos/cacheplane/dawnai" +const UPLOAD_BASE = "https://uploads.github.com/repos/cacheplane/dawnai" +const DUPLICATE_ID = 379982100 +const DUPLICATE_TAG = "untagged-a13939767dd2419ade01" +const SECOND_DUPLICATE_ID = 379986168 +const SECOND_DUPLICATE_TAG = "untagged-20706099efa3c38335a8" +const WRITER_TITLE = "Dawn v0.8.22" + +test("recovery writer exposes only the exact frozen mutation surface", () => { + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: async () => assert.fail("construction must not access the network"), + }) + + assert.deepEqual(Object.keys(writer).sort(), [ + "quarantineDuplicateBodyIfCurrent", + "uploadEvidenceAssetIfAbsentAndEqual", + ]) + assert.equal(Object.isFrozen(writer), true) + assert.equal( + Object.values(writer).every((method) => typeof method === "function"), + true, + ) + assert.throws( + () => + createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: async () => {}, + uploadOrigin: "https://evil.example", + }), + /schema|option|field/iu, + ) + assert.throws( + () => + createDuplicateDraftRecoveryWriter( + Object.assign(Object.create({ hiddenCapability() {} }), { + token: "secret-token", + fetchImpl: async () => {}, + }), + ), + /schema|option|field/iu, + ) +}) + +test("recovery writer uploads only exact candidate-derived evidence with pre/post snapshots", async () => { + const fixture = writerFixture() + const calls = [] + let uploaded = false + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: routingFetch(calls, async (url, init) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) return jsonResponse(candidateTagRef()) + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/releases/${DUPLICATE_ID}`) { + return jsonResponse(writerRelease(fixture.body)) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse( + uploaded ? [...fixture.rawAssets, fixture.archiveRawAsset] : fixture.rawAssets, + ) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(fixture.archiveBytes) + } + if ( + url === + `${UPLOAD_BASE}/releases/${DUPLICATE_ID}/assets?name=${encodeURIComponent(fixture.archiveName)}` + ) { + assert.equal(init.method, "POST") + assert.equal(init.redirect, "manual") + assert.equal(init.headers.Authorization, "Bearer secret-token") + assert.equal(init.headers["Content-Type"], "application/octet-stream") + assert.deepEqual(Buffer.from(init.body), fixture.archiveBytes) + uploaded = true + return jsonResponse( + { + id: fixture.archiveRawAsset.id, + name: fixture.archiveName, + digest: `sha256:${fixture.archiveSha256}`, + size: fixture.archiveBytes.byteLength, + state: "uploaded", + }, + 201, + ) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + + const receipt = await writer.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: fixture.untouchedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + }) + + assert.deepEqual(receipt, { + releaseId: DUPLICATE_ID, + assetId: fixture.archiveRawAsset.id, + name: fixture.archiveName, + status: "uploaded", + sha256: fixture.archiveSha256, + }) + assert.equal(Object.isFrozen(receipt), true) + assert.deepEqual( + calls.filter(({ init }) => init.method !== "GET").map(({ url, init }) => [url, init.method]), + [ + [ + `${UPLOAD_BASE}/releases/${DUPLICATE_ID}/assets?name=${encodeURIComponent(fixture.archiveName)}`, + "POST", + ], + ], + ) + assert.equal(calls.filter(({ url }) => url.includes("/git/ref/tags%2Fv0.8.22")).length, 2) + assert.deepEqual(calls.map(callKind), [ + "release", + "assets", + "tag-ref", + "tag-object", + "POST", + "tag-ref", + "tag-object", + "release", + "assets", + "asset-download", + ]) + assert.equal( + calls.some(({ url }) => /npm|actions|dispatch|DELETE/iu.test(url)), + false, + ) +}) + +test("recovery writer accepts an existing evidence asset only after exact download equality", async () => { + const fixture = writerFixture() + const calls = [] + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: routingFetch(calls, (url) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) return jsonResponse(candidateTagRef()) + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/releases/${DUPLICATE_ID}`) + return jsonResponse(writerRelease(fixture.body)) + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse([...fixture.rawAssets, fixture.archiveRawAsset]) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(fixture.archiveBytes) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + + const receipt = await writer.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: fixture.bodyArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + }) + + assert.equal(receipt.status, "existing") + assert.equal( + calls.every(({ init }) => init.method === "GET"), + true, + ) + assert.deepEqual(calls.map(callKind), [ + "release", + "assets", + "asset-download", + "tag-ref", + "tag-object", + ]) + + const unequal = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: async (url) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) return jsonResponse(candidateTagRef()) + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/releases/${DUPLICATE_ID}`) + return jsonResponse(writerRelease(fixture.body)) + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse([...fixture.rawAssets, fixture.archiveRawAsset]) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse( + Buffer.from("same-size-wrong-bytes".padEnd(fixture.archiveBytes.length)), + ) + } + assert.fail(`unexpected URL ${url}`) + }, + }) + await assert.rejects( + unequal.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: fixture.bodyArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + }), + /bytes|digest|snapshot/iu, + ) +}) + +test("recovery writer quarantines with exact non-atomic pre/post fence receipts", async () => { + const fixture = writerFixture() + const calls = [] + let quarantined = false + let releaseReads = 0 + const observedTimes = [ + Date.parse("2026-09-02T17:00:00.000Z"), + Date.parse("2026-09-02T17:00:01.000Z"), + ] + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + now: () => observedTimes.shift(), + fetchImpl: routingFetch(calls, async (url, init) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) return jsonResponse(candidateTagRef()) + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/releases/${DUPLICATE_ID}` && init.method === "PATCH") { + assert.deepEqual(JSON.parse(Buffer.from(init.body).toString("utf8")), { + body: fixture.notice, + }) + assert.equal(new Headers(init.headers).has("if-match"), false) + assert.equal(new Headers(init.headers).has("if-unmodified-since"), false) + assert.equal(Buffer.from(init.body).toString("utf8").includes("name"), false) + quarantined = true + return jsonResponse( + { + ...writerRelease(fixture.notice), + body: fixture.notice, + updated_at: "2026-09-02T17:00:02Z", + html_url: "https://github.com/cacheplane/dawnai/releases/tag/opaque", + author: { login: "operator-after" }, + }, + 200, + ) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}`) { + releaseReads += 1 + return jsonResponse({ + ...writerRelease(quarantined ? fixture.notice : fixture.body), + updated_at: `2026-09-02T17:00:0${releaseReads}Z`, + html_url: `https://github.com/cacheplane/dawnai/releases/${releaseReads}`, + author: { login: `operator-${releaseReads}` }, + }) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse([ + ...fixture.rawAssets, + fixture.archiveRawAsset, + fixture.receiptRawAsset, + ]) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(fixture.archiveBytes) + } + if (url === `${BASE}/releases/assets/${fixture.receiptRawAsset.id}`) { + return binaryResponse(fixture.receiptBytes) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + + const result = await writer.quarantineDuplicateBodyIfCurrent({ + expectedSnapshot: fixture.receiptArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + expectedBodySha256: fixture.archiveSha256, + expectedNotice: fixture.notice, + }) + + assert.deepEqual(result, { + atomic: false, + releaseId: DUPLICATE_ID, + outcome: "performed", + preWriteFence: { + observedAt: "2026-09-02T17:00:00.000Z", + projectionSha256: writerProjectionSha256(fixture, fixture.body), + tagObjectSha: TAG_OBJECT, + }, + postWriteFence: { + observedAt: "2026-09-02T17:00:01.000Z", + projectionSha256: writerProjectionSha256(fixture, fixture.notice), + tagObjectSha: TAG_OBJECT, + }, + }) + assert.equal(Object.isFrozen(result), true) + assert.equal(Object.isFrozen(result.preWriteFence), true) + assert.equal(Object.isFrozen(result.postWriteFence), true) + const patch = calls.find(({ init }) => init.method === "PATCH") + assert.ok(patch) + assert.deepEqual(Object.keys(JSON.parse(Buffer.from(patch.init.body))).sort(), ["body"]) + assert.equal(calls.filter(({ url }) => url.includes("/git/ref/tags%2Fv0.8.22")).length, 2) + assert.equal(calls.filter(({ init }) => init.method === "PATCH").length, 1) +}) + +test("recovery writer completes concurrent final pre-write tag and projection fences before PATCH", async () => { + const fixture = writerFixture() + const calls = [] + const preTagStarted = deferred() + const preReleaseStarted = deferred() + const releasePreTag = deferred() + const releasePreSnapshot = deferred() + const patchStarted = deferred() + let releaseReads = 0 + let quarantined = false + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + now: () => Date.parse(quarantined ? "2026-09-02T17:00:01Z" : "2026-09-02T17:00:00Z"), + fetchImpl: routingFetch(calls, async (url, init) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) { + if (!quarantined) { + preTagStarted.resolve() + await releasePreTag.promise + } + return jsonResponse(candidateTagRef()) + } + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/releases/${DUPLICATE_ID}` && init.method === "PATCH") { + patchStarted.resolve() + quarantined = true + return jsonResponse(writerRelease(fixture.notice)) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}`) { + releaseReads += 1 + if (releaseReads === 2) { + preReleaseStarted.resolve() + await releasePreSnapshot.promise + } + return jsonResponse(writerRelease(quarantined ? fixture.notice : fixture.body)) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse([ + ...fixture.rawAssets, + fixture.archiveRawAsset, + fixture.receiptRawAsset, + ]) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(fixture.archiveBytes) + } + if (url === `${BASE}/releases/assets/${fixture.receiptRawAsset.id}`) { + return binaryResponse(fixture.receiptBytes) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + + const operation = writer.quarantineDuplicateBodyIfCurrent({ + expectedSnapshot: fixture.receiptArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + expectedBodySha256: fixture.archiveSha256, + expectedNotice: fixture.notice, + }) + await Promise.all([preTagStarted.promise, preReleaseStarted.promise]) + assert.equal( + calls.some(({ init }) => init.method === "PATCH"), + false, + ) + releasePreTag.resolve() + await new Promise((resolve) => setImmediate(resolve)) + assert.equal( + calls.some(({ init }) => init.method === "PATCH"), + false, + ) + releasePreSnapshot.resolve() + await patchStarted.promise + await operation +}) + +test("recovery writer blocks pre/post projection drift including asset identity and size", async (t) => { + const fixture = writerFixture() + for (const drift of ["pre-tag", "pre-size", "pre-identity", "post-size"]) { + await t.test(drift, async () => { + const calls = [] + let assetReads = 0 + let quarantined = false + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + now: () => Date.parse("2026-09-02T17:00:00Z"), + fetchImpl: routingFetch(calls, async (url, init) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) { + return jsonResponse( + drift === "pre-tag" + ? { + ref: "refs/tags/v0.8.22", + object: { type: "tag", sha: "e".repeat(40) }, + } + : candidateTagRef(), + ) + } + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/git/tags/${"e".repeat(40)}`) { + return jsonResponse({ + sha: "e".repeat(40), + tag: "v0.8.22", + object: { type: "commit", sha: "f".repeat(40) }, + }) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}` && init.method === "PATCH") { + quarantined = true + return jsonResponse(writerRelease(fixture.notice)) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}`) { + return jsonResponse(writerRelease(quarantined ? fixture.notice : fixture.body)) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + assetReads += 1 + const assets = [...fixture.rawAssets, fixture.archiveRawAsset, fixture.receiptRawAsset] + const shouldDrift = + (["pre-size", "pre-identity"].includes(drift) && assetReads === 2) || + (drift === "post-size" && assetReads === 3) + if (!shouldDrift) return jsonResponse(assets) + return jsonResponse( + assets.map((asset, index) => + index === 0 + ? { + ...asset, + ...(drift === "pre-identity" + ? { id: asset.id + 10_000 } + : { size: asset.size + 1 }), + } + : asset, + ), + ) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(fixture.archiveBytes) + } + if (url === `${BASE}/releases/assets/${fixture.receiptRawAsset.id}`) { + return binaryResponse(fixture.receiptBytes) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + + await assert.rejects( + writer.quarantineDuplicateBodyIfCurrent({ + expectedSnapshot: fixture.receiptArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + expectedBodySha256: fixture.archiveSha256, + expectedNotice: fixture.notice, + }), + ) + assert.equal( + calls.filter(({ init }) => init.method === "PATCH").length, + drift.startsWith("pre-") ? 0 : 1, + ) + }) + } +}) + +test("recovery writer binds POST and PATCH fences to the evidence annotated tag object", async (t) => { + const fixture = writerFixture() + for (const method of ["POST", "PATCH"]) { + await t.test(method, async () => { + const harness = mutationFenceHarness(fixture, { + method, + failure: "replacement", + replaceTagObject: true, + }) + await assert.rejects( + harness.operation(), + (error) => error.code === "POST_WRITE_TAG_FENCE_CONFLICT", + ) + assert.equal(harness.calls.filter(({ init }) => init.method === method).length, 1) + }) + } +}) + +test("recovery writer requires an exact visible lowercase evidence tag object SHA", async () => { + const fixture = writerFixture() + let calls = 0 + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: async () => { + calls += 1 + assert.fail("invalid tag-object evidence must fail before network access") + }, + }) + const validUpload = uploadInput(fixture) + const validQuarantine = { + expectedSnapshot: fixture.receiptArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + expectedBodySha256: fixture.archiveSha256, + expectedNotice: fixture.notice, + } + for (const valid of [validUpload, validQuarantine]) { + const invoke = (input) => + Object.hasOwn(input, "name") + ? writer.uploadEvidenceAssetIfAbsentAndEqual(input) + : writer.quarantineDuplicateBodyIfCurrent(input) + const { expectedTagObjectSha: _missing, ...missing } = valid + await assert.rejects(invoke(missing), /schema|tag|sha/iu) + await assert.rejects( + invoke({ ...valid, expectedTagObjectSha: TAG_OBJECT.toUpperCase() }), + /tag|sha/iu, + ) + const hidden = { ...valid } + Object.defineProperty(hidden, "expectedTagObjectSha", { + value: TAG_OBJECT, + enumerable: false, + }) + await assert.rejects(invoke(hidden), /schema|tag|sha/iu) + } + assert.equal(calls, 0) +}) + +test("recovery writer rejects non-candidate inputs and concurrent drift before mutation", async () => { + const fixture = writerFixture() + let calls = 0 + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: async () => { + calls += 1 + assert.fail("invalid input must not access the network") + }, + }) + const baseInput = { + expectedSnapshot: fixture.untouchedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + } + for (const input of [ + { ...baseInput, expectedSnapshot: { ...fixture.untouchedSnapshot, releaseId: 379991871 } }, + { + ...baseInput, + expectedSnapshot: { ...fixture.untouchedSnapshot, tagName: "v0.8.22" }, + }, + { ...baseInput, name: "arbitrary.txt" }, + { ...baseInput, bytes: Buffer.from("arbitrary") }, + { ...baseInput, sha256: "0".repeat(64) }, + { + ...baseInput, + expectedSnapshot: { + ...fixture.untouchedSnapshot, + assets: [ + ...fixture.untouchedSnapshot.assets, + { id: 1001, name: "unexpected-evidence.txt", sha256: fixture.archiveSha256 }, + ], + evidenceAssets: ["body"], + }, + }, + { ...baseInput, extra: true }, + ]) { + await assert.rejects(writer.uploadEvidenceAssetIfAbsentAndEqual(input)) + } + const accessor = { ...baseInput } + Object.defineProperty(accessor, "name", { enumerable: true, get: () => fixture.archiveName }) + await assert.rejects(writer.uploadEvidenceAssetIfAbsentAndEqual(accessor), /schema|accessor/iu) + const symbol = { ...baseInput, [Symbol("hidden")]: true } + await assert.rejects(writer.uploadEvidenceAssetIfAbsentAndEqual(symbol), /schema|field/iu) + const inherited = Object.assign(Object.create({ hiddenCapability() {} }), baseInput) + await assert.rejects(writer.uploadEvidenceAssetIfAbsentAndEqual(inherited), /schema|field/iu) + assert.equal(calls, 0) + + const networkCalls = [] + const drifted = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: routingFetch(networkCalls, (url) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) return jsonResponse(candidateTagRef()) + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/releases/${DUPLICATE_ID}`) { + return jsonResponse({ ...writerRelease(fixture.body), name: "changed title" }) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse(fixture.rawAssets) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + await assert.rejects( + drifted.uploadEvidenceAssetIfAbsentAndEqual(baseInput), + /title|snapshot|metadata/iu, + ) + assert.equal( + networkCalls.some(({ init }) => init.method !== "GET"), + false, + ) +}) + +test("recovery writer bounds a stalled mutation response body", async () => { + const fixture = writerFixture() + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + timeoutMs: 20, + fetchImpl: async (url, init) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) return jsonResponse(candidateTagRef()) + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/releases/${DUPLICATE_ID}`) + return jsonResponse(writerRelease(fixture.body)) + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse(fixture.rawAssets) + } + if (url.startsWith(`${UPLOAD_BASE}/releases/${DUPLICATE_ID}/assets?name=`)) { + assert.equal(init.method, "POST") + return { + status: 201, + headers: new Headers({ "content-type": "application/json" }), + body: { + getReader() { + return { + read: () => new Promise(() => {}), + cancel: async () => {}, + } + }, + }, + } + } + assert.fail(`unexpected URL ${url}`) + }, + }) + + const operation = writer.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: fixture.untouchedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + }) + let guard + try { + await assert.rejects( + Promise.race([ + operation, + new Promise((_resolve, reject) => { + guard = setTimeout(() => reject(new Error("mutation response was not time-bounded")), 200) + }), + ]), + (error) => error.code === "MUTATION_OUTCOME_AMBIGUOUS", + ) + } finally { + clearTimeout(guard) + } + + const stalledFetch = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + timeoutMs: 20, + fetchImpl: async (url) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) return jsonResponse(candidateTagRef()) + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/releases/${DUPLICATE_ID}`) + return jsonResponse(writerRelease(fixture.body)) + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse(fixture.rawAssets) + } + if (url.startsWith(`${UPLOAD_BASE}/releases/${DUPLICATE_ID}/assets?name=`)) { + return new Promise(() => {}) + } + assert.fail(`unexpected URL ${url}`) + }, + }) + const stalledOperation = stalledFetch.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: fixture.untouchedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + }) + let fetchGuard + try { + await assert.rejects( + Promise.race([ + stalledOperation, + new Promise((_resolve, reject) => { + fetchGuard = setTimeout( + () => reject(new Error("mutation fetch was not time-bounded")), + 200, + ) + }), + ]), + (error) => error.code === "MUTATION_OUTCOME_AMBIGUOUS", + ) + } finally { + clearTimeout(fetchGuard) + } +}) + +test("recovery writer snapshots only exact intrinsic byte containers without invoking getters", async () => { + const fixture = writerFixture() + let networkCalls = 0 + let getterCalls = 0 + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: async () => { + networkCalls += 1 + assert.fail("malformed byte containers must fail before network access") + }, + }) + const inputFor = (bytes) => ({ + expectedSnapshot: fixture.untouchedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes, + sha256: fixture.archiveSha256, + }) + const ownByteLength = Uint8Array.from(fixture.archiveBytes) + Object.defineProperty(ownByteLength, "byteLength", { + configurable: true, + get() { + getterCalls += 1 + return fixture.archiveBytes.byteLength + }, + }) + const ownLength = Uint8Array.from(fixture.archiveBytes) + Object.defineProperty(ownLength, "length", { + configurable: true, + get() { + getterCalls += 1 + return fixture.archiveBytes.byteLength + }, + }) + const symbol = Uint8Array.from(fixture.archiveBytes) + Object.defineProperty(symbol, Symbol("hidden"), { value: true }) + const iterator = Uint8Array.from(fixture.archiveBytes) + Object.defineProperty(iterator, Symbol.iterator, { + value: function* customIterator() { + yield 0 + }, + }) + const customPrototype = Uint8Array.from(fixture.archiveBytes) + Object.setPrototypeOf(customPrototype, Object.create(Uint8Array.prototype)) + const proxy = new Proxy(Uint8Array.from(fixture.archiveBytes), {}) + let proxyTrapCalls = 0 + const trappedProxy = new Proxy(Uint8Array.from(fixture.archiveBytes), { + getPrototypeOf() { + proxyTrapCalls += 1 + throw new Error("getPrototypeOf trap must not run") + }, + ownKeys() { + proxyTrapCalls += 1 + throw new Error("ownKeys trap must not run") + }, + get() { + proxyTrapCalls += 1 + throw new Error("get trap must not run") + }, + }) + const oversized = new Uint8Array(64 * 1024 + 1) + for (const bytes of [ + ownByteLength, + ownLength, + symbol, + iterator, + customPrototype, + proxy, + trappedProxy, + oversized, + [], + { 0: 1, length: 1 }, + ]) { + await assert.rejects(writer.uploadEvidenceAssetIfAbsentAndEqual(inputFor(bytes))) + } + assert.equal(getterCalls, 0) + assert.equal(proxyTrapCalls, 0) + assert.equal(networkCalls, 0) +}) + +test("recovery writer bounds response streams by deadline, progress, chunks, and bytes", async (t) => { + const fixture = writerFixture() + await t.test("500k zero-length chunks stop within the 20ms operation bound", async () => { + let reads = 0 + const harness = uploadStreamHarness(fixture, { + timeoutMs: 20, + response: streamResponse(async () => { + reads += 1 + return reads <= 500_000 + ? { done: false, value: new Uint8Array(0) } + : { done: true, value: undefined } + }), + }) + await assert.rejects( + boundedForTest(harness.writer.uploadEvidenceAssetIfAbsentAndEqual(uploadInput(fixture)), 300), + (error) => + ["MUTATION_OUTCOME_AMBIGUOUS", "POST_WRITE_TAG_FENCE_CONFLICT"].includes(error.code), + ) + assert.ok(reads < 500_000) + }) + + await t.test("a stream that stalls after progress is time-bounded", async () => { + let reads = 0 + const harness = uploadStreamHarness(fixture, { + timeoutMs: 20, + response: streamResponse(async () => { + reads += 1 + if (reads === 1) return { done: false, value: Buffer.from("{") } + return new Promise(() => {}) + }), + }) + await assert.rejects( + boundedForTest(harness.writer.uploadEvidenceAssetIfAbsentAndEqual(uploadInput(fixture)), 300), + (error) => error.code === "MUTATION_OUTCOME_AMBIGUOUS", + ) + }) + + await t.test("excessive non-empty chunk count is rejected", async () => { + let reads = 0 + const harness = uploadStreamHarness(fixture, { + response: streamResponse(async () => { + reads += 1 + return reads <= 5_000 + ? { done: false, value: Uint8Array.of(32) } + : { done: true, value: undefined } + }), + }) + await assert.rejects( + harness.writer.uploadEvidenceAssetIfAbsentAndEqual(uploadInput(fixture)), + (error) => error.code === "MUTATION_OUTCOME_AMBIGUOUS", + ) + assert.ok(reads < 5_000) + }) + + await t.test("an exotic oversized chunk is rejected without invoking accessors", async () => { + let getterCalls = 0 + class ExoticChunk extends Uint8Array { + get byteLength() { + getterCalls += 1 + return super.byteLength + } + + get length() { + getterCalls += 1 + return super.length + } + } + const harness = uploadStreamHarness(fixture, { + maxResponseBytes: 64 * 1024, + response: streamResponse(async () => ({ + done: false, + value: new ExoticChunk(64 * 1024 + 1), + })), + }) + await assert.rejects( + harness.writer.uploadEvidenceAssetIfAbsentAndEqual(uploadInput(fixture)), + (error) => error.code === "MUTATION_OUTCOME_AMBIGUOUS", + ) + assert.equal(getterCalls, 0) + }) + + await t.test("a proxied chunk is rejected before invoking any proxy trap", async () => { + let proxyTrapCalls = 0 + const chunk = new Proxy(new Uint8Array(1), { + getPrototypeOf() { + proxyTrapCalls += 1 + throw new Error("getPrototypeOf trap must not run") + }, + ownKeys() { + proxyTrapCalls += 1 + throw new Error("ownKeys trap must not run") + }, + get() { + proxyTrapCalls += 1 + throw new Error("get trap must not run") + }, + }) + const harness = uploadStreamHarness(fixture, { + response: streamResponse(async () => ({ done: false, value: chunk })), + }) + await assert.rejects( + harness.writer.uploadEvidenceAssetIfAbsentAndEqual(uploadInput(fixture)), + (error) => error.code === "MUTATION_OUTCOME_AMBIGUOUS", + ) + assert.equal(proxyTrapCalls, 0) + }) + + await t.test("a normally chunked bounded JSON response succeeds", async () => { + const responseBytes = Buffer.from( + JSON.stringify({ + id: fixture.archiveRawAsset.id, + name: fixture.archiveName, + digest: `sha256:${fixture.archiveSha256}`, + size: fixture.archiveBytes.byteLength, + state: "uploaded", + }), + ) + let offset = 0 + const harness = uploadStreamHarness(fixture, { + response: streamResponse(async () => { + if (offset === responseBytes.byteLength) return { done: true, value: undefined } + const next = responseBytes.subarray(offset, offset + 7) + offset += next.byteLength + return { done: false, value: next } + }), + }) + assert.equal( + (await harness.writer.uploadEvidenceAssetIfAbsentAndEqual(uploadInput(fixture))).status, + "uploaded", + ) + }) +}) + +test("recovery writer applies bounded response controls to every remote read", async (t) => { + const fixture = writerFixture() + + await t.test("500k zero-length chunks on the initial Release GET stop promptly", async () => { + let reads = 0 + const harness = writerReadStreamHarness(fixture, { + location: "release", + timeoutMs: 20, + response: streamResponse(async () => { + reads += 1 + return reads <= 500_000 + ? { done: false, value: new Uint8Array(0) } + : { done: true, value: undefined } + }, 200), + }) + await assert.rejects( + boundedForTest(harness.operation(), 300), + (error) => error.code === "RELEASE_SNAPSHOT_UNAVAILABLE", + ) + assert.ok(reads < 500_000) + assert.equal(harness.writeCalls(), 0) + }) + + for (const location of ["assets", "tag", "download"]) { + await t.test(`${location} stream stalls are time-bounded`, async () => { + let reads = 0 + const harness = writerReadStreamHarness(fixture, { + location, + timeoutMs: 20, + response: streamResponse( + async () => { + reads += 1 + if (reads === 1) return { done: false, value: Buffer.from("{") } + return new Promise(() => {}) + }, + 200, + location === "download" ? { "content-type": "application/octet-stream" } : undefined, + ), + }) + await assert.rejects(boundedForTest(harness.operation(), 300)) + assert.equal(harness.writeCalls(), 0) + }) + } + + await t.test("read response chunk count is capped", async () => { + let reads = 0 + const harness = writerReadStreamHarness(fixture, { + location: "release", + response: streamResponse(async () => { + reads += 1 + return reads <= 5_000 + ? { done: false, value: Uint8Array.of(32) } + : { done: true, value: undefined } + }, 200), + }) + await assert.rejects(harness.operation()) + assert.ok(reads < 5_000) + assert.equal(harness.writeCalls(), 0) + }) + + await t.test("read response chunks share one cumulative wall-clock deadline", async () => { + const releaseBytes = Buffer.from(JSON.stringify(writerRelease(fixture.body))) + let offset = 0 + const harness = writerReadStreamHarness(fixture, { + location: "release", + timeoutMs: 20, + response: streamResponse(async () => { + await new Promise((resolve) => setTimeout(resolve, 8)) + const value = releaseBytes.subarray(offset, offset + 1) + offset += value.byteLength + return { done: false, value } + }, 200), + }) + await assert.rejects(boundedForTest(harness.operation(), 300)) + assert.ok(offset < releaseBytes.byteLength) + assert.equal(harness.writeCalls(), 0) + }) + + await t.test("normally chunked Release, assets, tag, and download reads succeed", async () => { + const harness = writerReadStreamHarness(fixture, { location: "normal" }) + const receipt = await harness.operation() + assert.equal(receipt.status, "existing") + assert.equal(harness.writeCalls(), 0) + }) +}) + +test("recovery writer enforces the 64 KiB evidence limit in its first streaming reader", async (t) => { + const fixture = writerFixture() + await t.test("a 100 KiB stream is cancelled before full buffering", async () => { + let reads = 0 + let cancellations = 0 + const harness = writerReadStreamHarness(fixture, { + location: "download", + response: countedBinaryResponse(100 * 1024, 1024, { + onRead: () => { + reads += 1 + }, + onCancel: () => { + cancellations += 1 + }, + }), + }) + await assert.rejects(harness.operation()) + assert.equal(reads, 65) + assert.equal(cancellations, 1) + assert.equal(harness.writeCalls(), 0) + }) + + await t.test("an exact 64 KiB stream reaches the downstream byte comparison", async () => { + let reads = 0 + let cancellations = 0 + const harness = writerReadStreamHarness(fixture, { + location: "download", + response: countedBinaryResponse(64 * 1024, 1024, { + onRead: () => { + reads += 1 + }, + onCancel: () => { + cancellations += 1 + }, + }), + }) + await assert.rejects(harness.operation()) + assert.equal(reads, 65) + assert.equal(cancellations, 0) + assert.equal(harness.writeCalls(), 0) + }) +}) + +test("recovery writer rejects raw or encoded credentials in redirect headers before follow", async (t) => { + const fixture = writerFixture() + const token = "secret-token" + for (const [label, location] of [ + ["raw", `https://objects.githubusercontent.com/${token}`], + ["percent-encoded", `https://objects.githubusercontent.com/${percentEncode(token)}`], + [ + "five-layer percent-encoded", + `https://objects.githubusercontent.com/${percentEncode(token, 5)}`, + ], + ]) { + await t.test(label, async () => { + const calls = [] + const writer = createDuplicateDraftRecoveryWriter({ + token, + fetchImpl: routingFetch(calls, (url) => { + if (url === `${BASE}/releases/${DUPLICATE_ID}`) { + return jsonResponse(writerRelease(fixture.body)) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse([...fixture.rawAssets, fixture.archiveRawAsset]) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(new Uint8Array(), 302, { location }) + } + assert.fail(`credential-bearing redirect must not be followed: ${url}`) + }), + }) + await assert.rejects( + writer.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: fixture.bodyArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + }), + (error) => !error.message.includes(token) && !JSON.stringify(error).includes(token), + ) + assert.equal( + calls.some(({ url }) => new URL(url).hostname === "objects.githubusercontent.com"), + false, + ) + }) + } +}) + +test("recovery writer fails closed when redirect decoding exhausts its safe bound", async () => { + const fixture = writerFixture() + const calls = [] + const location = `https://objects.githubusercontent.com/${percentEncode("x", 7)}` + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: routingFetch(calls, (url) => { + if (url === `${BASE}/releases/${DUPLICATE_ID}`) { + return jsonResponse(writerRelease(fixture.body)) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse([...fixture.rawAssets, fixture.archiveRawAsset]) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(new Uint8Array(), 302, { location }) + } + assert.fail(`unresolved redirect encoding must not be followed: ${url}`) + }), + }) + await assert.rejects( + writer.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: fixture.bodyArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + }), + ) + assert.equal( + calls.some(({ url }) => new URL(url).hostname === "objects.githubusercontent.com"), + false, + ) +}) + +test("recovery writer positive mutations cover both approved duplicate identities", async (t) => { + for (const identity of [ + { releaseId: DUPLICATE_ID, tagName: DUPLICATE_TAG }, + { releaseId: SECOND_DUPLICATE_ID, tagName: SECOND_DUPLICATE_TAG }, + ]) { + await t.test(String(identity.releaseId), async () => { + const fixture = writerFixture(identity) + await runPositiveIdentityMutations(fixture) + }) + } +}) + +test("recovery writer rejects each approved Release ID paired with the other opaque tag", async () => { + let calls = 0 + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: async () => { + calls += 1 + assert.fail("swapped duplicate identity must fail before network access") + }, + }) + for (const identity of [ + { releaseId: DUPLICATE_ID, tagName: SECOND_DUPLICATE_TAG }, + { releaseId: SECOND_DUPLICATE_ID, tagName: DUPLICATE_TAG }, + ]) { + const fixture = writerFixture(identity) + await assert.rejects(writer.uploadEvidenceAssetIfAbsentAndEqual(uploadInput(fixture))) + await assert.rejects( + writer.quarantineDuplicateBodyIfCurrent({ + expectedSnapshot: fixture.receiptArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + expectedBodySha256: fixture.archiveSha256, + expectedNotice: fixture.notice, + }), + ) + } + assert.equal(calls, 0) +}) + +test("recovery writer never sends configured credential bytes as evidence or notice content", async () => { + const fixture = writerFixture() + const token = "DAWN_DUPLICATE_DRAFT_RECOVERY" + let calls = 0 + const writer = createDuplicateDraftRecoveryWriter({ + token, + fetchImpl: async () => { + calls += 1 + assert.fail("credential-bearing content must fail before network access") + }, + }) + + await assert.rejects( + writer.quarantineDuplicateBodyIfCurrent({ + expectedSnapshot: fixture.receiptArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + expectedBodySha256: fixture.archiveSha256, + expectedNotice: fixture.notice, + }), + (error) => !error.message.includes(token) && !JSON.stringify(error).includes(token), + ) + assert.equal(calls, 0) +}) + +test("recovery writer rejects configured credentials in live response data without redaction", async (t) => { + const fixture = writerFixture() + const token = "secret-token" + for (const location of ["body", "response", "asset-bytes", "mutation-response"]) { + await t.test(location, async () => { + const calls = [] + const writer = createDuplicateDraftRecoveryWriter({ + token, + fetchImpl: routingFetch(calls, (url) => { + if (url === `${BASE}/releases/${DUPLICATE_ID}`) { + return jsonResponse({ + ...writerRelease(location === "body" ? `${fixture.body}\n${token}` : fixture.body), + ...(location === "response" ? { remote_note: token } : {}), + }) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse( + location === "asset-bytes" + ? [...fixture.rawAssets, fixture.archiveRawAsset] + : fixture.rawAssets, + ) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(Buffer.from(`${token}\n`)) + } + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) return jsonResponse(candidateTagRef()) + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url.startsWith(`${UPLOAD_BASE}/releases/${DUPLICATE_ID}/assets?name=`)) { + return jsonResponse( + { + id: fixture.archiveRawAsset.id, + name: fixture.archiveName, + digest: `sha256:${fixture.archiveSha256}`, + size: fixture.archiveBytes.byteLength, + state: "uploaded", + remote_note: token, + }, + 201, + ) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + const expectedSnapshot = + location === "asset-bytes" ? fixture.bodyArchivedSnapshot : fixture.untouchedSnapshot + + await assert.rejects( + writer.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + }), + (error) => !error.message.includes(token) && !JSON.stringify(error).includes(token), + ) + assert.equal( + calls.filter(({ init }) => init.method === "POST" || init.method === "PATCH").length, + location === "mutation-response" ? 1 : 0, + ) + if (location === "mutation-response") { + assert.equal(calls.filter(({ url }) => url.includes("/git/ref/tags%2Fv0.8.22")).length, 2) + } + }) + } +}) + +test("recovery writer rejects post-upload snapshot or candidate-tag drift", async (t) => { + const fixture = writerFixture() + for (const drift of ["asset inventory", "candidate tag"]) { + await t.test(drift, async () => { + let uploaded = false + let tagReads = 0 + let posts = 0 + const calls = [] + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: routingFetch(calls, async (url, init) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) { + tagReads += 1 + if (drift === "candidate tag" && tagReads === 2) { + return jsonResponse({ + ref: "refs/tags/v0.8.22", + object: { type: "tag", sha: "e".repeat(40) }, + }) + } + return jsonResponse(candidateTagRef()) + } + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/git/tags/${"e".repeat(40)}`) { + return jsonResponse({ + sha: "e".repeat(40), + tag: "v0.8.22", + object: { type: "commit", sha: "f".repeat(40) }, + }) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}`) + return jsonResponse(writerRelease(fixture.body)) + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + if (uploaded && drift === "candidate tag") { + return jsonResponse([...fixture.rawAssets, fixture.archiveRawAsset]) + } + return jsonResponse(fixture.rawAssets) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(fixture.archiveBytes) + } + if (url.startsWith(`${UPLOAD_BASE}/releases/${DUPLICATE_ID}/assets?name=`)) { + assert.equal(init.method, "POST") + posts += 1 + uploaded = true + return jsonResponse( + { + id: fixture.archiveRawAsset.id, + name: fixture.archiveName, + digest: `sha256:${fixture.archiveSha256}`, + size: fixture.archiveBytes.byteLength, + state: "uploaded", + }, + 201, + ) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + + await assert.rejects( + writer.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: fixture.untouchedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + }), + (error) => + ["MUTATION_OUTCOME_AMBIGUOUS", "POST_WRITE_TAG_FENCE_CONFLICT"].includes(error.code), + ) + assert.equal(posts, 1) + assert.deepEqual(calls.map(callKind), [ + "release", + "assets", + "tag-ref", + "tag-object", + "POST", + "tag-ref", + ...(drift === "candidate tag" ? [] : ["tag-object"]), + "release", + "assets", + ...(drift === "candidate tag" ? ["asset-download"] : []), + ]) + }) + } +}) + +test("recovery writer preserves immediate post-write tag fences on failure paths", async (t) => { + const fixture = writerFixture() + for (const method of ["POST", "PATCH"]) { + for (const failure of [ + "network", + "malformed-json", + "invalid-status", + "content-type", + "post-read", + ]) { + await t.test(`${method} ${failure}`, async () => { + const harness = mutationFenceHarness(fixture, { method, failure }) + await assert.rejects( + harness.operation(), + (error) => error.code === "MUTATION_OUTCOME_AMBIGUOUS", + ) + const pre = + method === "POST" + ? ["release", "assets", "tag-ref", "tag-object", "POST"] + : [ + "release", + "assets", + "asset-download", + "asset-download", + "tag-ref", + "release", + "tag-object", + "assets", + "asset-download", + "asset-download", + "PATCH", + ] + assert.deepEqual(harness.calls.map(callKind), [ + ...pre, + "tag-ref", + "tag-object", + "release", + "assets", + ...(failure === "post-read" + ? [] + : method === "POST" + ? ["asset-download"] + : ["asset-download", "asset-download"]), + ]) + assert.equal( + harness.calls.filter(({ init }) => init.method === method).length, + 1, + "issued mutations are never retried", + ) + }) + } + } +}) + +test("recovery reader exposes only the exact frozen read surface", () => { + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => assert.fail("construction must not execute commands"), + fetchImpl: async () => assert.fail("construction must not access the network"), + }) + + assert.deepEqual(Object.keys(reader).sort(), EXPECTED_METHODS) + assert.equal(Object.isFrozen(reader), true) + assert.equal( + Object.values(reader).every((method) => typeof method === "function"), + true, + ) +}) + +test("reviewed authority reads exact routes and proves local, remote, PR, trees, and CI", async () => { + const calls = [] + const runCalls = [] + const fetchImpl = routingFetch(calls, (url) => { + if (url === `${BASE}`) return repositoryResponse() + if (url === `${BASE}/git/ref/heads%2Fmain`) return jsonResponse(mainRef(REVIEWED_COMMIT)) + if (url === `${BASE}/commits/${REVIEWED_COMMIT}/pulls?per_page=2`) { + return jsonResponse([reviewedPull()]) + } + if (url === `${BASE}/git/commits/${REVIEWED_COMMIT}`) { + return jsonResponse({ sha: REVIEWED_COMMIT, tree: { sha: TREE } }) + } + if (url === `${BASE}/git/commits/${REVIEWED_HEAD}`) { + return jsonResponse({ sha: REVIEWED_HEAD, tree: { sha: TREE } }) + } + if (url === `${BASE}/commits/${REVIEWED_HEAD}/check-runs?per_page=100`) { + return jsonResponse({ + total_count: 1, + check_runs: [ + { + id: 98, + name: "validate", + head_sha: REVIEWED_HEAD, + status: "completed", + conclusion: "success", + check_suite: { id: 77 }, + }, + ], + }) + } + if (url === `${BASE}/actions/workflows/ci.yml/runs?head_sha=${REVIEWED_HEAD}&per_page=100`) { + return jsonResponse({ + total_count: 1, + workflow_runs: [ + { + id: 987654321, + run_attempt: 1, + name: "CI", + path: ".github/workflows/ci.yml", + head_sha: REVIEWED_HEAD, + head_branch: "reviewed-recovery", + event: "pull_request", + check_suite_id: 77, + status: "completed", + conclusion: "success", + }, + ], + }) + } + assert.fail(`unexpected URL ${url}`) + }) + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + fetchImpl, + token: "secret-token", + run: async (command, args, options) => { + runCalls.push([command, args, options]) + return `${REVIEWED_COMMIT}\n` + }, + }) + + assert.deepEqual(await reader.readReviewedMergeAuthority(REVIEWED_COMMIT), { + mergeCommitSha: REVIEWED_COMMIT, + mergeTreeSha: TREE, + pullRequestNumber: 789, + reviewedHeadSha: REVIEWED_HEAD, + reviewedTreeSha: TREE, + validateRunId: 987654321, + }) + assert.deepEqual( + runCalls.map(([command, args]) => [command, args]), + [["git", ["rev-list", "--first-parent", "--max-count=1", "HEAD"]]], + ) + assert.equal(calls.length, 7) + assert.equal( + calls.every(({ init }) => init.method === "GET" && init.redirect === "manual"), + true, + ) + assert.equal( + calls.every(({ init }) => init.headers.Authorization === "Bearer secret-token"), + true, + ) +}) + +test("reviewed authority rejects ambiguity, later main, unmerged PRs, tree drift, and failed CI", async (t) => { + const cases = [ + ["multiple PRs", { pulls: [reviewedPull(), { ...reviewedPull(), number: 790 }] }], + ["non-merged PR", { pulls: [{ ...reviewedPull(), merged_at: null }] }], + [ + "wrong base", + { pulls: [{ ...reviewedPull(), base: { ...reviewedPull().base, ref: "dev" } }] }, + ], + ["later main", { mainSha: "e".repeat(40) }], + ["local HEAD drift", { localHead: "f".repeat(40) }], + ["unequal trees", { headTree: "e".repeat(40) }], + ["failed validate", { checkConclusion: "failure" }], + [ + "calendar-invalid merge timestamp", + { pulls: [{ ...reviewedPull(), merged_at: "2026-02-31T00:00:00Z" }] }, + ], + ["duplicate check ID", { duplicateCheckId: true }], + ["duplicate CI run ID", { duplicateCiRunId: true }], + ] + for (const [name, overrides] of cases) { + await t.test(name, async () => { + const reader = reviewedReader(overrides) + await assert.rejects( + reader.readReviewedMergeAuthority(REVIEWED_COMMIT), + (error) => + typeof error.code === "string" && + !JSON.stringify(error).includes("secret-token") && + !error.message.includes("remote body"), + ) + }) + } +}) + +test("production reads bind repository, workflow, immutable setting, annotated tag, runs, and jobs", async () => { + const calls = [] + const fetchImpl = routingFetch(calls, (url) => { + if (url === BASE) return repositoryResponse() + if (url === `${BASE}/git/ref/heads%2Fmain`) return jsonResponse(mainRef(REVIEWED_COMMIT)) + if (url === `${BASE}/actions/workflows/260503756`) { + return jsonResponse({ + id: 260503756, + path: ".github/workflows/release.yml", + state: "disabled_manually", + }) + } + if (url === `${BASE}/immutable-releases`) return jsonResponse({ enabled: true }) + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) { + return jsonResponse({ + ref: "refs/tags/v0.8.22", + object: { type: "tag", sha: TAG_OBJECT }, + }) + } + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) { + return jsonResponse({ + sha: TAG_OBJECT, + tag: "v0.8.22", + object: { type: "commit", sha: CANDIDATE_SHA }, + }) + } + if (url === `${BASE}/actions/workflows/260503756/runs?per_page=100`) { + return jsonResponse({ total_count: 1, workflow_runs: [releaseRun(10)] }) + } + if (url === `${BASE}/actions/runs/10/jobs?filter=all&per_page=100`) { + return jsonResponse({ + total_count: 1, + jobs: [ + job(11, "publish-npm", { + conclusion: "skipped", + started_at: "2026-08-27T20:27:31Z", + completed_at: "2026-08-27T20:27:30Z", + }), + ], + }) + } + assert.fail(`unexpected URL ${url}`) + }) + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + fetchImpl, + run: async () => `${REVIEWED_COMMIT}\n`, + }) + + assert.deepEqual(await reader.readRepositoryState(), { + id: 1210070282, + nameWithOwner: "cacheplane/dawnai", + mainSha: REVIEWED_COMMIT, + }) + assert.deepEqual(await reader.readWorkflowState(), { + id: 260503756, + state: "disabled_manually", + }) + assert.deepEqual(await reader.readImmutableReleases(), { enabled: true }) + assert.deepEqual(await reader.readCandidateTag(), { + version: "0.8.22", + commitSha: CANDIDATE_SHA, + tagObjectSha: TAG_OBJECT, + }) + assert.deepEqual(await reader.readReleaseRuns(), { + runs: [ + { + id: 10, + runAttempt: 1, + status: "completed", + conclusion: "success", + headSha: CANDIDATE_SHA, + createdAt: "2026-09-01T00:00:00Z", + startedAt: "2026-09-01T00:00:01Z", + updatedAt: "2026-09-01T00:01:00Z", + }, + ], + candidateRuns: [ + { + id: 10, + runAttempt: 1, + status: "completed", + conclusion: "success", + headSha: CANDIDATE_SHA, + createdAt: "2026-09-01T00:00:00Z", + startedAt: "2026-09-01T00:00:01Z", + updatedAt: "2026-09-01T00:01:00Z", + }, + ], + }) + assert.equal((await reader.readCandidatePublishJobs(10, 1))[0].name, "publish-npm") + assert.equal( + calls.filter(({ url }) => url.includes("/actions/workflows/260503756/runs?")).length, + 1, + ) +}) + +test("repository reads reject string or drifted numeric IDs", async () => { + for (const id of ["1210070282", 1210070281]) { + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => { + if (url === BASE) { + return jsonResponse({ + id, + name: "dawnai", + full_name: "cacheplane/dawnai", + default_branch: "main", + owner: { login: "cacheplane" }, + }) + } + if (url === `${BASE}/git/ref/heads%2Fmain`) { + return jsonResponse(mainRef(REVIEWED_COMMIT)) + } + assert.fail(`unexpected URL ${url}`) + }, + }) + await assert.rejects( + reader.readRepositoryState(), + (error) => error.code === "REPOSITORY_IDENTITY_CONFLICT", + ) + } +}) + +test("Release and job observations reject malformed rows and incoherent terminal state", async () => { + const malformedReleaseReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => jsonResponse([{ id: 99 }]), + }) + await assert.rejects( + malformedReleaseReader.listCandidateReleases(), + (error) => error.code === "RELEASE_LIST_MALFORMED", + ) + + const malformedJobReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => + jsonResponse({ + total_count: 1, + jobs: [{ ...job(11, "publish-npm"), started_at: null }], + }), + }) + await assert.rejects( + malformedJobReader.readCandidatePublishJobs(10, 1), + (error) => error.code === "CANDIDATE_JOBS_MALFORMED", + ) + + const malformedRunReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => + jsonResponse({ + total_count: 1, + workflow_runs: [{ ...releaseRun(10), run_started_at: null }], + }), + }) + await assert.rejects( + malformedRunReader.readReleaseRuns(), + (error) => error.code === "RELEASE_RUNS_MALFORMED", + ) +}) + +test("remote timestamps require calendar-valid canonical ISO forms", async () => { + for (const timestamp of [ + "2026-02-31T00:00:00Z", + "2025-02-29T12:00:00.000Z", + "2026-01-01T24:00:00Z", + ]) { + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => + jsonResponse({ + total_count: 1, + jobs: [job(11, "publish-npm", { started_at: timestamp, completed_at: timestamp })], + }), + }) + await assert.rejects( + reader.readCandidatePublishJobs(10, 1), + (error) => error.code === "CANDIDATE_JOBS_MALFORMED", + ) + } + + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => + jsonResponse({ + total_count: 1, + jobs: [ + job(11, "publish-npm", { + started_at: "2024-02-29T23:59:59Z", + completed_at: "2024-02-29T23:59:59.123Z", + }), + ], + }), + }) + assert.equal( + (await reader.readCandidatePublishJobs(10, 1))[0].completedAt, + "2024-02-29T23:59:59.123Z", + ) +}) + +test("GitHub responses cannot echo configured credentials through values or keys", async () => { + const token = "secret-token" + const jobsReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + token, + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => + jsonResponse({ + total_count: 2, + jobs: [ + job(11, "publish-npm", { + status: "queued", + conclusion: null, + started_at: null, + completed_at: null, + }), + job(12, `prepare-${token}`), + ], + }), + }) + const jobs = await jobsReader.readCandidatePublishJobs(10, 1) + assert.equal(JSON.stringify(jobs).includes(token), false) + assert.equal(jobs[1].name, "prepare-[REDACTED]") + + const anonymousReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => + jsonResponse({ + total_count: 2, + jobs: [ + job(11, "publish-npm", { + status: "queued", + conclusion: null, + started_at: null, + completed_at: null, + }), + job(12, `prepare-${token}`), + ], + }), + }) + assert.equal((await anonymousReader.readCandidatePublishJobs(10, 1))[1].name, `prepare-${token}`) + + const bodyReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + token, + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => + jsonResponse({ + id: 260503756, + path: ".github/workflows/release.yml", + state: "disabled_manually", + [token]: "echo", + }), + }) + await assert.rejects(bodyReader.readWorkflowState(), (error) => { + assert.equal(error.code, "RELEASE_WORKFLOW_MALFORMED") + assert.equal(JSON.stringify(error).includes(token), false) + assert.equal(error.message.includes(token), false) + return true + }) +}) + +test("candidate jobs bind the run and exhaust every attempt through the current attempt", async () => { + const exactJobs = [ + job(21, "prepare"), + job(22, "publish-npm", { + conclusion: "skipped", + started_at: "2026-08-27T20:27:31Z", + completed_at: "2026-08-27T20:27:30Z", + }), + job(23, "prepare", { run_attempt: 2 }), + job(24, "publish-npm", { + run_attempt: 2, + conclusion: "skipped", + started_at: "2026-08-27T20:27:31Z", + completed_at: "2026-08-27T20:27:31Z", + }), + ] + const exactReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => { + assert.equal(url, `${BASE}/actions/runs/10/jobs?filter=all&per_page=100`) + return jsonResponse({ total_count: exactJobs.length, jobs: exactJobs }) + }, + }) + assert.deepEqual( + (await exactReader.readCandidatePublishJobs(10, 2)).map(({ runId, runAttempt, name }) => ({ + runId, + runAttempt, + name, + })), + [ + { runId: 10, runAttempt: 1, name: "prepare" }, + { runId: 10, runAttempt: 1, name: "publish-npm" }, + { runId: 10, runAttempt: 2, name: "prepare" }, + { runId: 10, runAttempt: 2, name: "publish-npm" }, + ], + ) + + for (const jobs of [ + [job(31, "publish-npm", { run_id: 11 }), job(32, "publish-npm", { run_attempt: 2 })], + [job(41, "publish-npm", { run_attempt: 2 })], + [job(42, "prepare"), job(43, "publish-npm", { run_attempt: 2 })], + [job(51, "publish-npm"), job(52, "publish-npm"), job(53, "publish-npm", { run_attempt: 2 })], + [ + job(61, "publish-npm"), + job(62, "publish-npm", { run_attempt: 2 }), + job(63, "publish-npm", { run_attempt: 3 }), + ], + [ + job(71, "publish-npm"), + job(72, "publish-npm", { run_attempt: 2 }), + job(72, "prepare", { run_attempt: 2 }), + ], + ]) { + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => jsonResponse({ total_count: jobs.length, jobs }), + }) + await assert.rejects( + reader.readCandidatePublishJobs(10, 2), + (error) => error.code === "CANDIDATE_JOBS_MALFORMED", + ) + } +}) + +test("skipped publish jobs preserve production scheduler timestamps without execution authority", async () => { + for (const [startedAt, completedAt] of [ + ["2026-08-27T20:27:31Z", "2026-08-27T20:27:30Z"], + ["2026-08-27T20:27:31Z", "2026-08-27T20:27:31Z"], + ]) { + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => + jsonResponse({ + total_count: 1, + jobs: [ + job(11, "publish-npm", { + conclusion: "skipped", + started_at: startedAt, + completed_at: completedAt, + }), + ], + }), + }) + assert.deepEqual(await reader.readCandidatePublishJobs(10, 1), [ + { + id: 11, + runId: 10, + runAttempt: 1, + name: "publish-npm", + status: "completed", + conclusion: "skipped", + startedAt, + completedAt, + }, + ]) + } +}) + +test("candidate Release discovery includes every marker-backed candidate history row", async () => { + const wrongShaBody = attachingBody("e".repeat(40)) + const rows = [ + releaseRow(400000001, { tag_name: "v0.8.22", draft: false, immutable: true }), + releaseRow(400000002, { body: wrongShaBody }), + releaseRow(400000003, { body: wrongShaBody, draft: false, immutable: true }), + releaseRow(400000004, { body: wrongShaBody, immutable: true }), + releaseRow(400000005), + ] + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => jsonResponse(rows), + }) + assert.deepEqual( + (await reader.listCandidateReleases()).map(({ releaseId }) => releaseId), + [400000001, 400000002, 400000003, 400000004], + ) +}) + +test("candidate Release discovery ignores unrelated Release titles", async () => { + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => + jsonResponse([ + releaseRow(400000010, { + name: "An unrelated historical Release", + tag_name: "v0.8.21", + }), + ]), + }) + + assert.deepEqual(await reader.listCandidateReleases(), []) +}) + +test("npm absence performs exact-version E404 plus package metadata confirmation", async () => { + const calls = [] + const packageName = "@dawn-ai/sdk" + const encoded = encodeURIComponent(packageName) + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: routingFetch(calls, (url) => { + if (url === `https://registry.npmjs.org/${encoded}/0.8.22`) { + return jsonResponse({ code: "E404", message: "secret remote body" }, 404) + } + if (url === `https://registry.npmjs.org/${encoded}`) { + return jsonResponse({ + name: packageName, + versions: { "0.8.21": packageVersion(packageName) }, + }) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + + assert.deepEqual(await reader.readNpmAbsence(packageName), { + name: packageName, + version: "0.8.22", + status: "absent", + }) + assert.deepEqual( + calls.map(({ url }) => url), + [`https://registry.npmjs.org/${encoded}/0.8.22`, `https://registry.npmjs.org/${encoded}`], + ) +}) + +test("release snapshots read complete assets and required recovery bytes through safe redirects", async () => { + const calls = [] + const releaseId = 379982100 + const originalBody = "canonical body\n" + const archiveBytes = Buffer.from(originalBody) + const archiveSha = sha256(archiveBytes) + const archiveName = `dawn-v0.8.22-duplicate-${releaseId}-original-body-${archiveSha}.txt` + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: routingFetch(calls, (url) => { + if (url === `${BASE}/releases/${releaseId}`) { + return jsonResponse(releaseFixture({ releaseId, body: originalBody })) + } + if (url === `${BASE}/releases/${releaseId}/assets?per_page=100`) { + return jsonResponse([ + asset(1, "base.tgz", Buffer.from("base")), + asset(2, archiveName, archiveBytes), + ]) + } + if (url === `${BASE}/releases/assets/2`) { + return binaryResponse(new Uint8Array(), 302, { + location: "https://objects.githubusercontent.com/recovery-archive", + }) + } + if (url === "https://objects.githubusercontent.com/recovery-archive") { + return binaryResponse(archiveBytes) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + + const snapshot = await reader.readReleaseSnapshot(releaseId, { + expectedOriginalBody: originalBody, + }) + assert.deepEqual(snapshot.evidenceAssets, ["body"]) + assert.equal(snapshot.assets[1].sha256, archiveSha) + assert.equal(snapshot.assets[1].size, archiveBytes.byteLength) + assert.equal(Object.hasOwn(snapshot.assets[1], "bytes"), false) +}) + +test("release capture rejects canonical and duplicate title drift", async () => { + for (const releaseId of [379991871, DUPLICATE_ID, SECOND_DUPLICATE_ID]) { + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => { + if (url === `${BASE}/releases/${releaseId}`) { + return jsonResponse({ + ...releaseFixture({ releaseId, body: "canonical body\n" }), + name: "Dawn v0.8.22 changed", + }) + } + if (url === `${BASE}/releases/${releaseId}/assets?per_page=100`) { + return jsonResponse([asset(1, "base.tgz", Buffer.from("base"))]) + } + assert.fail(`unexpected URL ${url}`) + }, + }) + + await assert.rejects( + reader.readReleaseSnapshot(releaseId, { + ...(releaseId === 379991871 ? {} : { expectedOriginalBody: "canonical body\n" }), + }), + (error) => error.code === "RELEASE_MALFORMED" || error.code === "RELEASE_TITLE_CONFLICT", + ) + } +}) + +test("downloaded recovery evidence rejects configured credential bytes and preserves anonymous bytes", async () => { + const releaseId = 379982100 + const token = "secret-token" + const originalBody = "canonical body\n" + const receiptBytes = Buffer.from(`{"credential":"${token}"}\n`) + const receiptName = `dawn-v0.8.22-duplicate-${releaseId}-recovery-receipt.json` + const createReader = (configuredToken) => + createDuplicateDraftRecoveryReader({ + root: "/workspace", + ...(configuredToken === null ? {} : { token: configuredToken }), + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => { + if (url === `${BASE}/releases/${releaseId}`) { + return jsonResponse(releaseFixture({ releaseId, body: originalBody })) + } + if (url === `${BASE}/releases/${releaseId}/assets?per_page=100`) { + return jsonResponse([asset(1, receiptName, receiptBytes)]) + } + if (url === `${BASE}/releases/assets/1`) return binaryResponse(receiptBytes) + assert.fail(`unexpected URL ${url}`) + }, + }) + + await assert.rejects( + createReader(token).readReleaseSnapshot(releaseId, { expectedOriginalBody: originalBody }), + (error) => { + assert.equal(error.code, "RECOVERY_ASSET_CREDENTIAL_CONFLICT") + assert.equal(JSON.stringify(error).includes(token), false) + assert.equal(error.message.includes(token), false) + return true + }, + ) + + const anonymous = await createReader(null).readReleaseSnapshot(releaseId, { + expectedOriginalBody: originalBody, + }) + assert.equal(anonymous.assets[0].bytes, receiptBytes.toString("utf8")) +}) + +test("release snapshots reject duplicate asset IDs and name collisions", async () => { + for (const field of ["id", "name"]) { + const first = asset(1, "first.tgz", Buffer.from("first")) + const second = asset(2, "second.tgz", Buffer.from("second")) + second[field] = first[field] + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => { + if (url === `${BASE}/releases/379982100`) { + return jsonResponse(releaseFixture({ releaseId: 379982100, body: "canonical body\n" })) + } + if (url === `${BASE}/releases/379982100/assets?per_page=100`) { + return jsonResponse([first, second]) + } + assert.fail(`unexpected URL ${url}`) + }, + }) + await assert.rejects( + reader.readReleaseSnapshot(379982100, { expectedOriginalBody: "canonical body\n" }), + (error) => error.code === "RELEASE_ASSETS_MALFORMED", + ) + } +}) + +test("candidate Release listing rejects unsafe pagination and does not expose remote bodies in errors", async () => { + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + token: "secret-token", + fetchImpl: async () => + jsonResponse([{ id: 1, body: "secret remote body" }], 200, { + link: '; rel="next"', + }), + }) + + await assert.rejects(reader.listCandidateReleases(), (error) => { + assert.equal(error.message.includes("secret-token"), false) + assert.equal(error.message.includes("secret remote body"), false) + return true + }) +}) + +test("Release workflow runs reject unsafe or incomplete pagination", async () => { + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => { + if (url.includes(`head_sha=${CANDIDATE_SHA}`)) { + return jsonResponse({ total_count: 0, workflow_runs: [] }) + } + return jsonResponse( + { + total_count: 101, + workflow_runs: Array.from({ length: 100 }, (_, index) => releaseRun(index + 1)), + }, + 200, + { + link: `; rel="next"`, + }, + ) + }, + }) + + await assert.rejects( + reader.readReleaseRuns(), + (error) => error.code === "PAGINATION_DRIFT" && !error.message.includes("evil.example"), + ) +}) + +test("recovery pagination rejects same-origin page jumps and total-count drift", async () => { + const jumpReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => + url.endsWith("page=3") + ? jsonResponse([]) + : jsonResponse( + Array.from({ length: 100 }, (_, index) => releaseRow(index + 1)), + 200, + { link: `<${BASE}/releases?per_page=100&page=3>; rel="next"` }, + ), + }) + await assert.rejects( + jumpReader.listCandidateReleases(), + (error) => error.code === "PAGINATION_DRIFT", + ) + + const totalsReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => + url.endsWith("page=2") + ? jsonResponse({ total_count: 102, jobs: [job(101, "publish-npm")] }) + : jsonResponse( + { + total_count: 101, + jobs: Array.from({ length: 100 }, (_, index) => job(index + 1, "prepare")), + }, + 200, + { link: `<${BASE}/actions/runs/10/jobs?filter=all&per_page=100&page=2>; rel="next"` }, + ), + }) + await assert.rejects( + totalsReader.readCandidatePublishJobs(10, 1), + (error) => error.code === "PAGINATION_DRIFT", + ) +}) + +test("recovery pagination exhausts hidden Release, asset, and job pages", async () => { + const releaseReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => + url.endsWith("page=2") + ? jsonResponse([ + releaseRow(400000000, { + tag_name: "v0.8.22", + draft: false, + immutable: true, + }), + ]) + : jsonResponse( + Array.from({ length: 100 }, (_, index) => releaseRow(index + 1)), + 200, + { link: `<${BASE}/releases?per_page=100&page=2>; rel="next"` }, + ), + }) + assert.deepEqual( + (await releaseReader.listCandidateReleases()).map(({ releaseId }) => releaseId), + [400000000], + ) + + const releaseId = 379982100 + const assetReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => { + if (url === `${BASE}/releases/${releaseId}`) { + return jsonResponse(releaseFixture({ releaseId, body: "canonical body\n" })) + } + if (url.endsWith("page=2")) { + return jsonResponse([asset(101, "hidden.tgz", Buffer.from("hidden"))]) + } + return jsonResponse( + Array.from({ length: 100 }, (_, index) => + asset(index + 1, `base-${index + 1}.tgz`, Buffer.from(`base-${index + 1}`)), + ), + 200, + { link: `<${BASE}/releases/${releaseId}/assets?per_page=100&page=2>; rel="next"` }, + ) + }, + }) + assert.equal( + (await assetReader.readReleaseSnapshot(releaseId, { expectedOriginalBody: "canonical body\n" })) + .assets.length, + 101, + ) + + const jobReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => + url.endsWith("page=2") + ? jsonResponse({ total_count: 101, jobs: [job(101, "publish-npm")] }) + : jsonResponse( + { + total_count: 101, + jobs: Array.from({ length: 100 }, (_, index) => job(index + 1, "prepare")), + }, + 200, + { link: `<${BASE}/actions/runs/10/jobs?filter=all&per_page=100&page=2>; rel="next"` }, + ), + }) + assert.equal((await jobReader.readCandidatePublishJobs(10, 1)).at(-1).name, "publish-npm") +}) + +test("terminal pagination rejects a later last page and accepts the current last page", async () => { + const incompleteReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => + jsonResponse( + Array.from({ length: 100 }, (_, index) => releaseRow(index + 1)), + 200, + { link: `<${BASE}/releases?per_page=100&page=2>; rel="last"` }, + ), + }) + await assert.rejects( + incompleteReader.listCandidateReleases(), + (error) => error.code === "PAGINATION_DRIFT", + ) + + const completeReader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => + url.endsWith("page=2") + ? jsonResponse([releaseRow(101)], 200, { + link: [ + `<${BASE}/releases?per_page=100&page=1>; rel="prev"`, + `<${BASE}/releases?per_page=100&page=1>; rel="first"`, + ].join(", "), + }) + : jsonResponse( + Array.from({ length: 100 }, (_, index) => releaseRow(index + 1)), + 200, + { + link: [ + `<${BASE}/releases?per_page=100&page=2>; rel="next"`, + `<${BASE}/releases?per_page=100&page=2>; rel="last"`, + ].join(", "), + }, + ), + }) + assert.deepEqual(await completeReader.listCandidateReleases(), []) +}) + +test("pagination requires a stable advertised last page across the operation", async () => { + for (const { firstLast, terminalLink } of [ + { + firstLast: 3, + terminalLink: `<${BASE}/releases?per_page=100&page=2>; rel="last"`, + }, + { firstLast: 3, terminalLink: null }, + ]) { + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => + url.endsWith("page=2") + ? jsonResponse( + [releaseRow(101)], + 200, + terminalLink === null ? {} : { link: terminalLink }, + ) + : jsonResponse( + Array.from({ length: 100 }, (_, index) => releaseRow(index + 1)), + 200, + { + link: [ + `<${BASE}/releases?per_page=100&page=2>; rel="next"`, + `<${BASE}/releases?per_page=100&page=${firstLast}>; rel="last"`, + ].join(", "), + }, + ), + }) + await assert.rejects( + reader.listCandidateReleases(), + (error) => error.code === "PAGINATION_DRIFT", + ) + } +}) + +test("Release pagination rejects a repeated unrelated ID on a later page", async () => { + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => + url.endsWith("page=2") + ? jsonResponse([releaseRow(50)], 200, { + link: `<${BASE}/releases?per_page=100&page=2>; rel="last"`, + }) + : jsonResponse( + Array.from({ length: 100 }, (_, index) => releaseRow(index + 1)), + 200, + { + link: [ + `<${BASE}/releases?per_page=100&page=2>; rel="next"`, + `<${BASE}/releases?per_page=100&page=2>; rel="last"`, + ].join(", "), + }, + ), + }) + await assert.rejects(reader.listCandidateReleases(), (error) => error.code === "PAGINATION_DRIFT") +}) + +test("recovery pagination enforces one cumulative response-byte budget", async () => { + const pages = [ + Array.from({ length: 100 }, (_, index) => releaseRow(index + 1)), + Array.from({ length: 100 }, (_, index) => releaseRow(index + 101)), + ] + const pageBytes = Buffer.byteLength(JSON.stringify(pages[0])) + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + maxResponseBytes: pageBytes + 100, + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async (url) => + url.endsWith("page=2") + ? jsonResponse(pages[1]) + : jsonResponse(pages[0], 200, { + link: `<${BASE}/releases?per_page=100&page=2>; rel="next"`, + }), + }) + await assert.rejects(reader.listCandidateReleases(), (error) => /LIMIT|LARGE/u.test(error.code)) +}) + +test("recovery pagination enforces one cumulative operation deadline", async () => { + const clock = [0, 0, 10] + const reader = createDuplicateDraftRecoveryReader({ + root: "/workspace", + timeoutMs: 10, + now: () => clock.shift() ?? 10, + run: async () => `${REVIEWED_COMMIT}\n`, + fetchImpl: async () => + jsonResponse( + Array.from({ length: 100 }, (_, index) => releaseRow(index + 1)), + 200, + { link: `<${BASE}/releases?per_page=100&page=2>; rel="next"` }, + ), + }) + await assert.rejects( + reader.listCandidateReleases(), + (error) => error.code === "RELEASE_LIST_UNAVAILABLE", + ) +}) + +function reviewedReader({ + pulls = [reviewedPull()], + mainSha = REVIEWED_COMMIT, + headTree = TREE, + checkConclusion = "success", + duplicateCheckId = false, + duplicateCiRunId = false, + localHead = REVIEWED_COMMIT, +} = {}) { + return createDuplicateDraftRecoveryReader({ + root: "/workspace", + token: "secret-token", + run: async () => `${localHead}\n`, + fetchImpl: async (url) => { + if (url === BASE) return repositoryResponse() + if (url === `${BASE}/git/ref/heads%2Fmain`) return jsonResponse(mainRef(mainSha)) + if (url === `${BASE}/commits/${REVIEWED_COMMIT}/pulls?per_page=2`) { + return jsonResponse(pulls) + } + if (url === `${BASE}/git/commits/${REVIEWED_COMMIT}`) { + return jsonResponse({ sha: REVIEWED_COMMIT, tree: { sha: TREE } }) + } + if (url === `${BASE}/git/commits/${REVIEWED_HEAD}`) { + return jsonResponse({ sha: REVIEWED_HEAD, tree: { sha: headTree } }) + } + if (url === `${BASE}/commits/${REVIEWED_HEAD}/check-runs?per_page=100`) { + return jsonResponse({ + total_count: duplicateCheckId ? 2 : 1, + check_runs: [ + { + id: 1, + name: "validate", + head_sha: REVIEWED_HEAD, + status: "completed", + conclusion: checkConclusion, + check_suite: { id: 77 }, + }, + ...(duplicateCheckId + ? [ + { + id: 1, + name: "unrelated", + head_sha: REVIEWED_HEAD, + status: "completed", + conclusion: "success", + check_suite: { id: 78 }, + }, + ] + : []), + ], + }) + } + if (url === `${BASE}/actions/workflows/ci.yml/runs?head_sha=${REVIEWED_HEAD}&per_page=100`) { + return jsonResponse({ + total_count: duplicateCiRunId ? 2 : 1, + workflow_runs: [ + { + id: 2, + run_attempt: 1, + name: "CI", + path: ".github/workflows/ci.yml", + head_sha: REVIEWED_HEAD, + head_branch: "reviewed-recovery", + event: "pull_request", + check_suite_id: 77, + status: "completed", + conclusion: checkConclusion, + }, + ...(duplicateCiRunId + ? [ + { + id: 2, + run_attempt: 1, + name: "Unrelated", + path: ".github/workflows/ci.yml", + head_sha: REVIEWED_HEAD, + head_branch: "reviewed-recovery", + event: "workflow_dispatch", + check_suite_id: 78, + status: "completed", + conclusion: "success", + }, + ] + : []), + ], + }) + } + assert.fail(`unexpected URL ${url}`) + }, + }) +} + +function reviewedPull() { + return { + number: 789, + state: "closed", + merged_at: "2026-09-01T00:00:00Z", + merge_commit_sha: REVIEWED_COMMIT, + base: { + ref: "main", + repo: { id: 1210070282, full_name: "cacheplane/dawnai" }, + }, + head: { sha: REVIEWED_HEAD }, + } +} + +function repositoryResponse() { + return jsonResponse({ + id: 1210070282, + name: "dawnai", + full_name: "cacheplane/dawnai", + default_branch: "main", + owner: { login: "cacheplane" }, + }) +} + +function mainRef(sha) { + return { ref: "refs/heads/main", object: { type: "commit", sha } } +} + +function releaseRun(id) { + return { + id, + run_attempt: 1, + status: "completed", + conclusion: "success", + head_sha: CANDIDATE_SHA, + path: ".github/workflows/release.yml", + created_at: "2026-09-01T00:00:00Z", + run_started_at: "2026-09-01T00:00:01Z", + updated_at: "2026-09-01T00:01:00Z", + } +} + +function job(id, name, overrides = {}) { + return { + id, + run_id: 10, + run_attempt: 1, + name, + status: "completed", + conclusion: "success", + started_at: "2026-09-01T00:00:00Z", + completed_at: "2026-09-01T00:01:00Z", + ...overrides, + } +} + +function candidateTagRef() { + return { + ref: "refs/tags/v0.8.22", + object: { type: "tag", sha: TAG_OBJECT }, + } +} + +function candidateTagObject() { + return { + sha: TAG_OBJECT, + tag: "v0.8.22", + object: { type: "commit", sha: CANDIDATE_SHA }, + } +} + +function writerRelease(body, { releaseId = DUPLICATE_ID, tagName = DUPLICATE_TAG } = {}) { + return { + id: releaseId, + tag_name: tagName, + name: WRITER_TITLE, + body, + draft: true, + prerelease: false, + immutable: false, + target_commitish: "main", + } +} + +function writerFixture({ releaseId = DUPLICATE_ID, tagName = DUPLICATE_TAG } = {}) { + const manifest = writerManifest() + const manifestSha256 = sha256(canonicalManifestBytes(manifest)) + const subjects = [ + { name: "manifest.json", sha256: manifestSha256 }, + ...manifest.packages.map((pkg) => ({ name: pkg.filename, sha256: pkg.sha256 })), + ] + const normalizedAssets = [ + { id: 1, name: "release-record.json", sha256: "e".repeat(64), size: 1 }, + ...subjects.map(({ name, sha256: digest }, index) => ({ + id: index + 2, + name, + sha256: digest, + size: 1, + })), + ...subjects.map(({ name }, index) => ({ + id: subjects.length + index + 2, + name: `${name}.intoto.jsonl`, + sha256: "f".repeat(64), + size: 1, + })), + ] + assert.equal(normalizedAssets.length, 45) + const baseAssetSetSha256 = sha256( + Buffer.from( + `${JSON.stringify(normalizedAssets.map(({ name, sha256: digest }) => ({ name, sha256: digest })))}\n`, + ), + ) + const marker = { + schemaVersion: 1, + epoch: "fixed-group-v1", + revision: 2, + phase: "ESCROWED", + version: "0.8.22", + commitSha: CANDIDATE_SHA, + tag: "v0.8.22", + manifestSha256, + releaseRecordSha256: "e".repeat(64), + baseAssetSetSha256, + attestationSet: { + repository: "cacheplane/dawnai", + workflow: ".github/workflows/release.yml", + sourceRef: "refs/tags/v0.8.22", + commitSha: CANDIDATE_SHA, + workflowRunId: 3, + runAttempt: 1, + subjects: subjects.map(({ name, sha256: digest }) => ({ + subjectName: name, + subjectSha256: digest, + bundleName: `${name}.intoto.jsonl`, + bundleSha256: "f".repeat(64), + })), + }, + npmEvidenceSha256: null, + smoke: null, + audit: null, + abandonmentSha256: null, + } + const body = canonicalReleaseBody({ marker, manifest }) + const archiveBytes = Buffer.from(body) + const archiveSha256 = sha256(archiveBytes) + const archiveName = originalBodyAssetName(releaseId, archiveSha256) + const receiptName = recoveryReceiptAssetName(releaseId) + const receiptBytes = canonicalRecoveryReceipt({ + repository: "cacheplane/dawnai", + version: "0.8.22", + candidateSha: CANDIDATE_SHA, + recoveryCommit: REVIEWED_COMMIT, + canonicalReleaseId: 379991871, + duplicateReleaseId: releaseId, + originalBodySha256: archiveSha256, + baseAssetSetSha256, + archiveAsset: { name: archiveName, sha256: archiveSha256 }, + }) + const receiptSha256 = sha256(receiptBytes) + const notice = canonicalRecoveryNotice({ + repository: "cacheplane/dawnai", + version: "0.8.22", + canonicalReleaseId: 379991871, + duplicateReleaseId: releaseId, + originalBodySha256: archiveSha256, + archiveAssetName: archiveName, + receiptAssetName: receiptName, + receiptSha256, + }) + const rawAssets = normalizedAssets.map(({ id, name, sha256: digest, size }) => ({ + id, + name, + digest: `sha256:${digest}`, + size, + })) + const archiveRawAsset = asset(1001, archiveName, archiveBytes) + const receiptRawAsset = asset(1002, receiptName, receiptBytes) + const snapshotBase = { + releaseId, + tagName, + title: WRITER_TITLE, + targetCommitish: "main", + draft: true, + prerelease: false, + immutable: false, + body, + marker: parseReleaseMarker(body), + assets: normalizedAssets, + } + const archiveAsset = { + id: archiveRawAsset.id, + name: archiveName, + sha256: archiveSha256, + size: archiveRawAsset.size, + } + const receiptAsset = { + id: receiptRawAsset.id, + name: receiptName, + sha256: receiptSha256, + size: receiptRawAsset.size, + bytes: receiptBytes.toString("utf8"), + } + return { + releaseId, + tagName, + body, + rawAssets, + archiveBytes, + archiveSha256, + archiveName, + archiveRawAsset, + receiptBytes, + receiptRawAsset, + notice, + untouchedSnapshot: { ...snapshotBase, evidenceAssets: [] }, + bodyArchivedSnapshot: { + ...snapshotBase, + assets: [...normalizedAssets, archiveAsset], + evidenceAssets: ["body"], + }, + receiptArchivedSnapshot: { + ...snapshotBase, + assets: [...normalizedAssets, archiveAsset, receiptAsset], + evidenceAssets: ["body", "receipt"], + }, + } +} + +function writerManifest() { + const packages = CANONICAL_RELEASE_PACKAGE_ORDER.map((name) => { + const filename = `${name.startsWith("@") ? name.slice(1).replaceAll("/", "-") : name}-0.8.22.tgz` + const bytes = Buffer.from(`package:${name}`) + const sha512 = createHash("sha512").update(bytes).digest("hex") + return { + name, + version: "0.8.22", + filename, + size: bytes.byteLength, + sha256: sha256(bytes), + sha512, + npmIntegrity: `sha512-${Buffer.from(sha512, "hex").toString("base64")}`, + access: "public", + } + }) + return { + schemaVersion: 1, + version: "0.8.22", + commitSha: CANDIDATE_SHA, + ci: { workflow: "CI", runId: 1, runAttempt: 1 }, + artifact: { + name: `release-v0.8.22-${CANDIDATE_SHA.slice(0, 12)}`, + prepareRunId: 2, + prepareRunAttempt: 1, + }, + packageOrder: [...CANONICAL_RELEASE_PACKAGE_ORDER], + packages, + } +} + +function attachingBody(commitSha) { + return canonicalReleaseBody({ + marker: { + schemaVersion: 1, + epoch: "fixed-group-v1", + revision: 1, + phase: "ATTACHING", + version: "0.8.22", + commitSha, + tag: "v0.8.22", + manifestSha256: "a".repeat(64), + releaseRecordSha256: "b".repeat(64), + baseAssetSetSha256: null, + attestationSet: null, + npmEvidenceSha256: null, + smoke: null, + audit: null, + abandonmentSha256: null, + }, + manifest: null, + }) +} + +function packageVersion(name) { + return { name, version: "0.8.21" } +} + +function releaseFixture({ releaseId, body }) { + return { + id: releaseId, + tag_name: "untagged-a13939767dd2419ade01", + name: WRITER_TITLE, + body, + draft: true, + prerelease: false, + immutable: false, + target_commitish: "main", + } +} + +function releaseRow(id, overrides = {}) { + return { + id, + tag_name: `untagged-unrelated-${id}`, + name: WRITER_TITLE, + body: null, + draft: true, + prerelease: false, + immutable: false, + target_commitish: "main", + ...overrides, + } +} + +function asset(id, name, bytes) { + return { + id, + name, + digest: `sha256:${sha256(bytes)}`, + size: bytes.byteLength, + } +} + +function writerProjectionSha256(fixture, body) { + const projection = { + releaseId: DUPLICATE_ID, + tagName: DUPLICATE_TAG, + title: WRITER_TITLE, + targetCommitish: "main", + draft: true, + prerelease: false, + immutable: false, + body, + assets: [...fixture.rawAssets, fixture.archiveRawAsset, fixture.receiptRawAsset].map( + ({ id, name, digest, size }) => ({ + id, + name, + sha256: digest.slice(7), + size, + }), + ), + } + return sha256(Buffer.from(JSON.stringify(canonicalizeForTest(projection)))) +} + +function canonicalizeForTest(value) { + if (Array.isArray(value)) return value.map(canonicalizeForTest) + if (value === null || typeof value !== "object") return value + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, canonicalizeForTest(value[key])]), + ) +} + +function deferred() { + let resolve + let reject + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise + reject = rejectPromise + }) + return { promise, resolve, reject } +} + +function sha256(bytes) { + return createHash("sha256").update(bytes).digest("hex") +} + +function routingFetch(calls, route) { + return async (url, init) => { + calls.push({ url, init }) + return route(url, init) + } +} + +function callKind({ url, init }) { + if (init.method !== "GET") return init.method + if (url.includes("/git/ref/tags%2Fv0.8.22")) return "tag-ref" + if (url.includes("/git/tags/")) return "tag-object" + if (url.includes("/releases/assets/")) return "asset-download" + if (url.includes("/assets?per_page=100")) return "assets" + if (url === `${BASE}/releases/${DUPLICATE_ID}`) return "release" + return url +} + +function uploadInput(fixture) { + return { + expectedSnapshot: fixture.untouchedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + } +} + +async function runPositiveIdentityMutations(fixture) { + let evidenceAssets = 0 + let quarantined = false + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + now: () => Date.parse("2026-09-02T17:00:00Z"), + fetchImpl: async (url, init) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) return jsonResponse(candidateTagRef()) + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/releases/${fixture.releaseId}` && init.method === "PATCH") { + quarantined = true + return jsonResponse( + writerRelease(fixture.notice, { + releaseId: fixture.releaseId, + tagName: fixture.tagName, + }), + ) + } + if (url === `${BASE}/releases/${fixture.releaseId}`) { + return jsonResponse( + writerRelease(quarantined ? fixture.notice : fixture.body, { + releaseId: fixture.releaseId, + tagName: fixture.tagName, + }), + ) + } + if (url === `${BASE}/releases/${fixture.releaseId}/assets?per_page=100`) { + return jsonResponse([ + ...fixture.rawAssets, + ...(evidenceAssets >= 1 ? [fixture.archiveRawAsset] : []), + ...(evidenceAssets >= 2 ? [fixture.receiptRawAsset] : []), + ]) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(fixture.archiveBytes) + } + if (url === `${BASE}/releases/assets/${fixture.receiptRawAsset.id}`) { + return binaryResponse(fixture.receiptBytes) + } + if ( + url === + `${UPLOAD_BASE}/releases/${fixture.releaseId}/assets?name=${encodeURIComponent(fixture.archiveName)}` + ) { + evidenceAssets = 1 + return jsonResponse({ ...fixture.archiveRawAsset, state: "uploaded" }, 201) + } + if ( + url === + `${UPLOAD_BASE}/releases/${fixture.releaseId}/assets?name=${encodeURIComponent(recoveryReceiptAssetName(fixture.releaseId))}` + ) { + evidenceAssets = 2 + return jsonResponse({ ...fixture.receiptRawAsset, state: "uploaded" }, 201) + } + assert.fail(`unexpected URL ${url}`) + }, + }) + + const archive = await writer.uploadEvidenceAssetIfAbsentAndEqual(uploadInput(fixture)) + assert.equal(archive.releaseId, fixture.releaseId) + assert.equal(archive.status, "uploaded") + const receipt = await writer.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: fixture.bodyArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: recoveryReceiptAssetName(fixture.releaseId), + bytes: fixture.receiptBytes, + sha256: sha256(fixture.receiptBytes), + }) + assert.equal(receipt.releaseId, fixture.releaseId) + assert.equal(receipt.status, "uploaded") + const quarantine = await writer.quarantineDuplicateBodyIfCurrent({ + expectedSnapshot: fixture.receiptArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + expectedBodySha256: fixture.archiveSha256, + expectedNotice: fixture.notice, + }) + assert.equal(quarantine.releaseId, fixture.releaseId) + assert.equal(quarantine.outcome, "performed") +} + +function uploadStreamHarness( + fixture, + { response, timeoutMs = 1_000, maxResponseBytes = 4 * 1024 * 1024 }, +) { + let uploaded = false + const calls = [] + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + timeoutMs, + maxResponseBytes, + fetchImpl: routingFetch(calls, (url, init) => { + if (url === `${BASE}/releases/${DUPLICATE_ID}`) + return jsonResponse(writerRelease(fixture.body)) + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return jsonResponse( + uploaded ? [...fixture.rawAssets, fixture.archiveRawAsset] : fixture.rawAssets, + ) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(fixture.archiveBytes) + } + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) return jsonResponse(candidateTagRef()) + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url.startsWith(`${UPLOAD_BASE}/releases/${DUPLICATE_ID}/assets?name=`)) { + assert.equal(init.method, "POST") + uploaded = true + return response + } + assert.fail(`unexpected URL ${url}`) + }), + }) + return { writer, calls } +} + +function writerReadStreamHarness(fixture, { location, response, timeoutMs = 1_000 }) { + const calls = [] + const routeResponse = (kind, value, contentType = "application/json") => { + if (location === kind) return response + if (location === "normal") { + const bytes = contentType === "application/json" ? Buffer.from(JSON.stringify(value)) : value + return chunkedResponse(bytes, 200, { "content-type": contentType }) + } + return contentType === "application/json" ? jsonResponse(value) : binaryResponse(value) + } + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + timeoutMs, + fetchImpl: routingFetch(calls, (url) => { + if (url === `${BASE}/releases/${DUPLICATE_ID}`) { + return routeResponse("release", writerRelease(fixture.body)) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + return routeResponse("assets", [...fixture.rawAssets, fixture.archiveRawAsset]) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return routeResponse("download", fixture.archiveBytes, "application/octet-stream") + } + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) { + return routeResponse("tag", candidateTagRef()) + } + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) { + return routeResponse("tag-object", candidateTagObject()) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + return { + operation: () => + writer.uploadEvidenceAssetIfAbsentAndEqual({ + expectedSnapshot: fixture.bodyArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + name: fixture.archiveName, + bytes: fixture.archiveBytes, + sha256: fixture.archiveSha256, + }), + writeCalls: () => calls.filter(({ init }) => init.method !== "GET").length, + } +} + +function mutationFenceHarness(fixture, { method, failure, replaceTagObject = false }) { + let mutated = false + let tagReads = 0 + const calls = [] + const writer = createDuplicateDraftRecoveryWriter({ + token: "secret-token", + fetchImpl: routingFetch(calls, (url, init) => { + if (url === `${BASE}/git/ref/tags%2Fv0.8.22`) { + tagReads += 1 + return jsonResponse( + replaceTagObject && tagReads === 2 + ? { + ref: "refs/tags/v0.8.22", + object: { type: "tag", sha: "e".repeat(40) }, + } + : candidateTagRef(), + ) + } + if (url === `${BASE}/git/tags/${TAG_OBJECT}`) return jsonResponse(candidateTagObject()) + if (url === `${BASE}/git/tags/${"e".repeat(40)}`) { + return jsonResponse({ + sha: "e".repeat(40), + tag: "v0.8.22", + object: { type: "commit", sha: CANDIDATE_SHA }, + }) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}` && init.method === "PATCH") { + mutated = true + return failedMutationResponse(failure, writerRelease(fixture.notice), 200) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}`) { + return jsonResponse( + writerRelease(mutated && method === "PATCH" ? fixture.notice : fixture.body), + ) + } + if (url === `${BASE}/releases/${DUPLICATE_ID}/assets?per_page=100`) { + if (mutated && failure === "post-read") throw new Error("post snapshot unavailable") + if (method === "PATCH") { + return jsonResponse([ + ...fixture.rawAssets, + fixture.archiveRawAsset, + fixture.receiptRawAsset, + ]) + } + return jsonResponse( + mutated ? [...fixture.rawAssets, fixture.archiveRawAsset] : fixture.rawAssets, + ) + } + if (url === `${BASE}/releases/assets/${fixture.archiveRawAsset.id}`) { + return binaryResponse(fixture.archiveBytes) + } + if (url === `${BASE}/releases/assets/${fixture.receiptRawAsset.id}`) { + return binaryResponse(fixture.receiptBytes) + } + if (url.startsWith(`${UPLOAD_BASE}/releases/${DUPLICATE_ID}/assets?name=`)) { + mutated = true + return failedMutationResponse( + failure, + { + id: fixture.archiveRawAsset.id, + name: fixture.archiveName, + digest: `sha256:${fixture.archiveSha256}`, + size: fixture.archiveBytes.byteLength, + state: "uploaded", + }, + 201, + ) + } + assert.fail(`unexpected URL ${url}`) + }), + }) + return { + calls, + operation: () => + method === "POST" + ? writer.uploadEvidenceAssetIfAbsentAndEqual(uploadInput(fixture)) + : writer.quarantineDuplicateBodyIfCurrent({ + expectedSnapshot: fixture.receiptArchivedSnapshot, + expectedTagObjectSha: TAG_OBJECT, + expectedBodySha256: fixture.archiveSha256, + expectedNotice: fixture.notice, + }), + } +} + +function failedMutationResponse(failure, value, successStatus) { + if (failure === "network") throw new Error("write response lost") + if (failure === "malformed-json") { + return binaryResponse(Buffer.from("not-json"), successStatus, { + "content-type": "application/json", + }) + } + if (failure === "invalid-status") return jsonResponse(value, 500) + if (failure === "content-type") { + return binaryResponse(Buffer.from(JSON.stringify(value)), successStatus, { + "content-type": "text/plain", + }) + } + return jsonResponse(value, successStatus) +} + +function streamResponse(read, status = 201, headers = {}) { + return { + status, + headers: new Headers({ "content-type": "application/json", ...headers }), + body: { + getReader() { + return { read, cancel: async () => {}, releaseLock() {} } + }, + }, + } +} + +function countedBinaryResponse(totalBytes, chunkBytes, { onRead, onCancel }) { + let offset = 0 + return { + status: 200, + headers: new Headers({ "content-type": "application/octet-stream" }), + body: { + getReader() { + return { + async read() { + onRead() + if (offset === totalBytes) return { done: true, value: undefined } + const size = Math.min(chunkBytes, totalBytes - offset) + offset += size + return { done: false, value: Buffer.alloc(size, 120) } + }, + async cancel() { + onCancel() + }, + releaseLock() {}, + } + }, + }, + } +} + +function percentEncode(value, layers = 1) { + let encoded = value + for (let layer = 0; layer < layers; layer += 1) { + encoded = [...Buffer.from(encoded, "utf8")] + .map((byte) => `%${byte.toString(16).padStart(2, "0")}`) + .join("") + } + return encoded +} + +function chunkedResponse(bytes, status = 200, headers = {}) { + let offset = 0 + return streamResponse( + async () => { + if (offset === bytes.byteLength) return { done: true, value: undefined } + const value = bytes.subarray(offset, offset + 257) + offset += value.byteLength + return { done: false, value } + }, + status, + headers, + ) +} + +async function boundedForTest(operation, milliseconds) { + let guard + try { + return await Promise.race([ + operation, + new Promise((_resolve, reject) => { + guard = setTimeout( + () => reject(new Error("operation exceeded test deadline")), + milliseconds, + ) + }), + ]) + } finally { + clearTimeout(guard) + } +} + +function jsonResponse(value, status = 200, headers = {}) { + return new Response(JSON.stringify(value), { + status, + headers: { "content-type": "application/json", ...headers }, + }) +} + +function binaryResponse(value, status = 200, headers = {}) { + return new Response(value, { + status, + headers: { "content-type": "application/octet-stream", ...headers }, + }) +} diff --git a/scripts/release/test/duplicate-draft-recovery-cli.test.mjs b/scripts/release/test/duplicate-draft-recovery-cli.test.mjs new file mode 100644 index 000000000..20ab82d30 --- /dev/null +++ b/scripts/release/test/duplicate-draft-recovery-cli.test.mjs @@ -0,0 +1,1260 @@ +import assert from "node:assert/strict" +import { execFile as execFileCallback } from "node:child_process" +import { EventEmitter } from "node:events" +import { constants as fsConstants } from "node:fs" +import * as fileSystem from "node:fs/promises" +import { + chmod, + link, + lstat, + mkdir, + mkdtemp, + readFile, + realpath, + symlink, + writeFile, +} from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import test from "node:test" +import { promisify } from "node:util" +import * as recoveryCliModule from "../recover-v0.8.22-duplicate-drafts.mjs" +import { + parseDuplicateDraftRecoveryCliArguments, + readCandidateControllerMarker, + runDuplicateDraftRecoveryCli, +} from "../recover-v0.8.22-duplicate-drafts.mjs" + +const execFile = promisify(execFileCallback) +const REVIEWED_COMMIT = "a".repeat(40) +const CAPTURE_PATH = ".dawn/release-recovery/v0.8.22-capture-01.json" +const APPLY_PATH = ".dawn/release-recovery/v0.8.22-apply-01.json" +const ACKNOWLEDGEMENT_FLAG = "--acknowledge-non-atomic-release-edit-freeze" +const UUID = "12345678-1234-1234-9234-123456789abc" + +test("parses only the exact capture and apply invocation grammars", () => { + const capture = parseDuplicateDraftRecoveryCliArguments([ + "capture", + "--reviewed-commit", + REVIEWED_COMMIT, + "--output", + CAPTURE_PATH, + ]) + assert.deepEqual(capture, { + command: "capture", + reviewedCommit: REVIEWED_COMMIT, + output: CAPTURE_PATH, + }) + assert.ok(Object.isFrozen(capture)) + + const apply = parseDuplicateDraftRecoveryCliArguments([ + "apply", + "--evidence", + CAPTURE_PATH, + ACKNOWLEDGEMENT_FLAG, + "--output", + APPLY_PATH, + ]) + assert.deepEqual(apply, { + command: "apply", + evidence: CAPTURE_PATH, + output: APPLY_PATH, + }) + assert.ok(Object.isFrozen(apply)) +}) + +test("rejects missing, duplicate, unknown, joined, reordered, valued, aliased, and unsafe arguments", () => { + const rejected = [ + [], + ["unknown"], + ["capture", "--reviewed-commit", REVIEWED_COMMIT, "--output"], + ["capture", "--reviewed-commit", REVIEWED_COMMIT, "--reviewed-commit", REVIEWED_COMMIT], + ["capture", `--reviewed-commit=${REVIEWED_COMMIT}`, "--output", CAPTURE_PATH], + ["capture", "--output", CAPTURE_PATH, "--reviewed-commit", REVIEWED_COMMIT], + ["capture", "--reviewed-commit", REVIEWED_COMMIT.toUpperCase(), "--output", CAPTURE_PATH], + ["capture", "--reviewed-commit", "a".repeat(39), "--output", CAPTURE_PATH], + ["capture", "--reviewed-commit", REVIEWED_COMMIT, "--unknown", CAPTURE_PATH], + ["apply", "--evidence", CAPTURE_PATH, "--output", APPLY_PATH], + ["apply", "--evidence", CAPTURE_PATH, ACKNOWLEDGEMENT_FLAG, "true", "--output", APPLY_PATH], + ["apply", "--evidence", CAPTURE_PATH, `${ACKNOWLEDGEMENT_FLAG}=true`, "--output", APPLY_PATH], + ["apply", "--evidence", CAPTURE_PATH, "--acknowledge-edit-freeze", "--output", APPLY_PATH], + ["apply", ACKNOWLEDGEMENT_FLAG, "--evidence", CAPTURE_PATH, "--output", APPLY_PATH], + ["apply", "--evidence", CAPTURE_PATH, ACKNOWLEDGEMENT_FLAG, "--evidence", CAPTURE_PATH], + ["apply", "--evidence", CAPTURE_PATH, ACKNOWLEDGEMENT_FLAG, "--output", CAPTURE_PATH], + ["capture", "--reviewed-commit", REVIEWED_COMMIT, "--output", `${CAPTURE_PATH}\n`], + ["capture", "--reviewed-commit", REVIEWED_COMMIT, "--output", `${CAPTURE_PATH}\0x`], + ] + for (const argv of rejected) { + assert.throws(() => parseDuplicateDraftRecoveryCliArguments(argv), /invalid|requires/iu) + } + + const accessor = ["capture", "--reviewed-commit", REVIEWED_COMMIT, "--output", CAPTURE_PATH] + Object.defineProperty(accessor, 4, { + enumerable: true, + get: () => CAPTURE_PATH, + }) + assert.throws(() => parseDuplicateDraftRecoveryCliArguments(accessor), /invalid/iu) +}) + +test("rejects every path outside the exact private recovery descendant", () => { + const rejected = [ + ".dawn/release-recovery", + ".dawn/release-recovery/", + ".dawn/elsewhere/evidence.json", + "nested/.dawn/release-recovery/evidence.json", + ".dawn/release-recovery/../evidence.json", + ".dawn/release-recovery/./evidence.json", + "./.dawn/release-recovery/evidence.json", + "/tmp/evidence.json", + "../.dawn/release-recovery/evidence.json", + ".dawn//release-recovery/evidence.json", + ] + for (const output of rejected) { + assert.throws( + () => + parseDuplicateDraftRecoveryCliArguments([ + "capture", + "--reviewed-commit", + REVIEWED_COMMIT, + "--output", + output, + ]), + /path|invalid/iu, + ) + } +}) + +test("capture constructs production dependencies only after validation and durably writes canonical evidence", async (t) => { + const root = await createPrivateRepository(t) + const calls = [] + const stdout = sink() + const stderr = sink() + const evidence = Object.freeze({ schemaVersion: 1, capturedAt: "now" }) + const canonicalBytes = Buffer.from('{"capturedAt":"now","schemaVersion":1}\n', "utf8") + const result = await runDuplicateDraftRecoveryCli({ + argv: ["capture", "--reviewed-commit", reviewedCommit(root), "--output", CAPTURE_PATH], + cwd: root, + environment: { GITHUB_TOKEN: "capture-token" }, + stdout, + stderr, + dependencies: { + randomUUID: () => UUID, + createDuplicateDraftRecoveryReader(input) { + calls.push(["reader", input.root, input.token]) + return Object.freeze({ kind: "reader" }) + }, + async captureDuplicateDraftRecoveryEvidence(input) { + calls.push(["capture", input.reviewedCommit, input.reader.kind]) + return evidence + }, + canonicalDuplicateDraftEvidence(value) { + calls.push(["canonical", value]) + return canonicalBytes + }, + }, + }) + + assert.equal(result, 0) + assert.equal(stdout.text, "Duplicate draft recovery evidence captured.\n") + assert.equal(stderr.text, "") + assert.deepEqual(calls, [ + ["reader", root, "capture-token"], + ["capture", reviewedCommit(root), "reader"], + ["canonical", evidence], + ]) + const output = path.join(root, CAPTURE_PATH) + assert.deepEqual(await readFile(output), canonicalBytes) + assert.equal((await lstat(output)).mode & 0o777, 0o600) + assert.deepEqual(await temporaryFiles(root), []) +}) + +test("capture refuses to write evidence containing the GitHub credential", async (t) => { + // Capture evidence carries the canonical Release body verbatim, so the + // credential-free guarantee is enforced on the exact bytes about to be + // published rather than left to the read path's scrubbing. + const root = await createPrivateRepository(t) + const stdout = sink() + const stderr = sink() + const result = await runDuplicateDraftRecoveryCli({ + argv: ["capture", "--reviewed-commit", reviewedCommit(root), "--output", CAPTURE_PATH], + cwd: root, + environment: { GITHUB_TOKEN: "capture-token" }, + stdout, + stderr, + dependencies: { + randomUUID: () => UUID, + createDuplicateDraftRecoveryReader: () => Object.freeze({ kind: "reader" }), + captureDuplicateDraftRecoveryEvidence: async () => + Object.freeze({ schemaVersion: 1, capturedAt: "now" }), + canonicalDuplicateDraftEvidence: () => + Buffer.from('{"body":"leaked capture-token","schemaVersion":1}\n', "utf8"), + }, + }) + + assert.notEqual(result, 0) + assert.equal(stdout.text, "") + await assert.rejects(readFile(path.join(root, CAPTURE_PATH))) + assert.deepEqual(await temporaryFiles(root), []) +}) + +test("apply parses canonical evidence before constructing dependencies and passes the exact frozen acknowledgement", async (t) => { + const root = await createPrivateRepository(t) + const evidenceBytes = Buffer.from('{"schemaVersion":1}\n', "utf8") + await writePrivateEvidence(root, evidenceBytes) + const parsedEvidence = Object.freeze({ + schemaVersion: 1, + reviewedAuthority: Object.freeze({ mergeCommitSha: reviewedCommit(root) }), + }) + const receipt = finalReceipt() + const events = [] + const stdout = sink() + const stderr = sink() + const result = await runDuplicateDraftRecoveryCli({ + argv: ["apply", "--evidence", CAPTURE_PATH, ACKNOWLEDGEMENT_FLAG, "--output", APPLY_PATH], + cwd: root, + environment: { GITHUB_TOKEN: "apply-token" }, + stdout, + stderr, + dependencies: { + randomUUID: () => UUID, + parseDuplicateDraftEvidence(bytes) { + events.push(["parse", Buffer.from(bytes).toString("utf8")]) + return parsedEvidence + }, + createDuplicateDraftRecoveryReader(input) { + events.push(["reader", input.token]) + return Object.freeze({ kind: "reader" }) + }, + createProductionRecoveryObserver(input) { + events.push(["observer", input.token, input.reader.kind]) + return async () => ({}) + }, + createDuplicateDraftRecoveryWriter(input) { + events.push(["writer", input.token]) + return Object.freeze({ kind: "writer" }) + }, + async applyDuplicateDraftRecovery(input) { + events.push(["apply", input.evidence]) + assert.deepEqual(input.concurrencyAcknowledgement, { + acknowledged: true, + atomic: false, + mode: "operator-freeze-compare-before-write-v1", + releaseIds: [379982100, 379986168], + }) + assert.ok(Object.isFrozen(input.concurrencyAcknowledgement)) + assert.ok(Object.isFrozen(input.concurrencyAcknowledgement.releaseIds)) + assert.deepEqual(await input.createWriter(), { kind: "writer" }) + return receipt + }, + }, + }) + + assert.equal(result, 0) + assert.equal(stdout.text, "Duplicate draft recovery authorization recorded.\n") + assert.equal(stderr.text, "") + assert.deepEqual(events, [ + ["parse", evidenceBytes.toString("utf8")], + ["reader", "apply-token"], + ["observer", "apply-token", "reader"], + ["apply", parsedEvidence], + ["writer", "apply-token"], + ]) + assert.equal( + await readFile(path.join(root, APPLY_PATH), "utf8"), + `${JSON.stringify(canonicalize(receipt))}\n`, + ) + assert.deepEqual(await temporaryFiles(root), []) +}) + +test("invalid paths and malformed evidence fail before token access or dependency construction", async (t) => { + const root = await createPrivateRepository(t) + await writePrivateEvidence(root, Buffer.from("not canonical", "utf8")) + let tokenReads = 0 + const environment = {} + Object.defineProperty(environment, "GITHUB_TOKEN", { + enumerable: true, + get() { + tokenReads += 1 + return "secret" + }, + }) + let constructions = 0 + const dependencies = { + parseDuplicateDraftEvidence() { + throw new Error("malformed canonical evidence") + }, + createDuplicateDraftRecoveryReader() { + constructions += 1 + }, + } + const malformed = await runDuplicateDraftRecoveryCli({ + argv: ["apply", "--evidence", CAPTURE_PATH, ACKNOWLEDGEMENT_FLAG, "--output", APPLY_PATH], + cwd: root, + environment, + stdout: sink(), + stderr: sink(), + dependencies, + }) + assert.equal(malformed, 1) + assert.equal(tokenReads, 0) + assert.equal(constructions, 0) + + const invalid = await runDuplicateDraftRecoveryCli({ + argv: ["capture", "--reviewed-commit", REVIEWED_COMMIT, "--output", "outside.json"], + cwd: root, + environment, + stdout: sink(), + stderr: sink(), + dependencies, + }) + assert.equal(invalid, 2) + assert.equal(tokenReads, 0) + assert.equal(constructions, 0) +}) + +test("rejects a recovery boundary or target that is not gitignored", async (t) => { + const root = await createPrivateRepository(t, { ignored: false }) + let constructions = 0 + const stderr = sink() + const result = await runDuplicateDraftRecoveryCli({ + argv: ["capture", "--reviewed-commit", reviewedCommit(root), "--output", CAPTURE_PATH], + cwd: root, + environment: { GITHUB_TOKEN: "secret" }, + stdout: sink(), + stderr, + dependencies: { + createDuplicateDraftRecoveryReader() { + constructions += 1 + }, + }, + }) + assert.equal(result, 1) + assert.equal(constructions, 0) + assert.equal(stderr.text, "Duplicate draft recovery failed.\n") +}) + +test("bounded private evidence reads reject symlinks, hardlinks, unsafe modes, empty files, oversized files, and invalid UTF-8", async (t) => { + const cases = [ + async (root) => symlink("real.json", path.join(root, CAPTURE_PATH)), + async (root) => { + const source = path.join(root, ".dawn/release-recovery/real.json") + await writeFile(source, "{}\n", { mode: 0o600 }) + await link(source, path.join(root, CAPTURE_PATH)) + }, + async (root) => + writeFile(path.join(root, CAPTURE_PATH), Buffer.alloc(0), { + mode: 0o600, + }), + async (root) => writeFile(path.join(root, CAPTURE_PATH), "{}\n", { mode: 0o644 }), + async (root) => + writeFile(path.join(root, CAPTURE_PATH), Buffer.alloc(512 * 1024 + 1, 0x61), { + mode: 0o600, + }), + async (root) => + writeFile(path.join(root, CAPTURE_PATH), Buffer.from([0xc3, 0x28]), { + mode: 0o600, + }), + ] + for (const [index, arrange] of cases.entries()) { + const root = await createPrivateRepository(t) + await arrange(root) + let parsed = false + const result = await runDuplicateDraftRecoveryCli({ + argv: ["apply", "--evidence", CAPTURE_PATH, ACKNOWLEDGEMENT_FLAG, "--output", APPLY_PATH], + cwd: root, + environment: { GITHUB_TOKEN: "secret" }, + stdout: sink(), + stderr: sink(), + dependencies: { + parseDuplicateDraftEvidence() { + parsed = true + return {} + }, + }, + }) + assert.equal(result, 1) + assert.equal(parsed, false, `case ${index} reached the canonical parser`) + } +}) + +test("write-once output refuses regular, symlink, and hardlink conflicts without overwriting or leaving a temp", async (t) => { + const arrangers = [ + async (root) => writeFile(path.join(root, APPLY_PATH), "existing", { mode: 0o600 }), + async (root) => symlink("existing.json", path.join(root, APPLY_PATH)), + async (root) => { + const source = path.join(root, ".dawn/release-recovery/existing.json") + await writeFile(source, "existing", { mode: 0o600 }) + await link(source, path.join(root, APPLY_PATH)) + }, + ] + for (const arrange of arrangers) { + const root = await createPrivateRepository(t) + await writePrivateEvidence(root, Buffer.from("{}\n", "utf8")) + await arrange(root) + const before = await lstat(path.join(root, APPLY_PATH)) + const result = await successfulApply(root) + assert.equal(result.code, 1) + const after = await lstat(path.join(root, APPLY_PATH)) + assert.equal(after.ino, before.ino) + assert.deepEqual(await temporaryFiles(root), []) + } +}) + +test("sanitizes all failures and never prints credentials, remote bodies, signed URLs, or stacks", async (t) => { + const root = await createPrivateRepository(t) + await writePrivateEvidence(root, Buffer.from("{}\n", "utf8")) + const stdout = sink() + const stderr = sink() + const token = "ghp_super-secret" + const result = await runDuplicateDraftRecoveryCli({ + argv: ["apply", "--evidence", CAPTURE_PATH, ACKNOWLEDGEMENT_FLAG, "--output", APPLY_PATH], + cwd: root, + environment: { GITHUB_TOKEN: token }, + stdout, + stderr, + dependencies: { + parseDuplicateDraftEvidence() { + throw new Error( + `${token} remote-body https://objects.githubusercontent.com/file?sig=secret\n at unsafe`, + ) + }, + }, + }) + assert.equal(result, 1) + assert.equal(stdout.text, "") + assert.equal(stderr.text, "Duplicate draft recovery failed.\n") + assert.doesNotMatch(stderr.text, /secret|remote-body|https:|\bat\b/iu) +}) + +test("refuses to serialize a malformed core receipt or invent missing fence history", async (t) => { + const root = await createPrivateRepository(t) + await writePrivateEvidence(root, Buffer.from("{}\n", "utf8")) + const stdout = sink() + const stderr = sink() + const code = await runDuplicateDraftRecoveryCli({ + argv: ["apply", "--evidence", CAPTURE_PATH, ACKNOWLEDGEMENT_FLAG, "--output", APPLY_PATH], + cwd: root, + environment: { GITHUB_TOKEN: "secret" }, + stdout, + stderr, + dependencies: { + randomUUID: () => UUID, + parseDuplicateDraftEvidence: () => + Object.freeze({ + reviewedAuthority: Object.freeze({ mergeCommitSha: reviewedCommit(root) }), + }), + createDuplicateDraftRecoveryReader: () => Object.freeze({}), + createProductionRecoveryObserver: () => async () => ({}), + createDuplicateDraftRecoveryWriter: () => Object.freeze({}), + applyDuplicateDraftRecovery: async () => + deepFreeze({ + ...structuredClone(finalReceipt()), + duplicates: [ + { + releaseId: 379982100, + outcome: "preexisting-quarantined", + priorFenceObservations: { invented: true }, + verifiedAt: "2026-09-01T00:02:00.000Z", + projectionSha256: "c".repeat(64), + }, + structuredClone(finalReceipt().duplicates[1]), + ], + }), + }, + }) + assert.equal(code, 1) + assert.equal(stdout.text, "") + assert.equal(stderr.text, "Duplicate draft recovery failed.\n") + await assert.rejects(lstat(path.join(root, APPLY_PATH)), { code: "ENOENT" }) + assert.deepEqual(await temporaryFiles(root), []) +}) + +test("scrubs credentials and inherited Git controls from every Git child", async (t) => { + const root = await createPrivateRepository(t) + const environments = [] + const runGit = async (command, args, options) => { + environments.push(structuredClone(options.env)) + const result = await execFile(command, args, options) + return result.stdout + } + const result = await runDuplicateDraftRecoveryCli({ + argv: ["capture", "--reviewed-commit", reviewedCommit(root), "--output", CAPTURE_PATH], + cwd: root, + environment: { + GITHUB_TOKEN: "never-in-git", + GIT_DIR: "/tmp/hostile", + GIT_WORK_TREE: "/tmp/hostile-tree", + GIT_CONFIG_GLOBAL: "/tmp/hostile-config", + GIT_OBJECT_DIRECTORY: "/tmp/hostile-objects", + GIT_ALTERNATE_OBJECT_DIRECTORIES: "/tmp/hostile-alternates", + }, + stdout: sink(), + stderr: sink(), + dependencies: { + randomUUID: () => UUID, + runGit, + createDuplicateDraftRecoveryReader({ root: readerRoot, token, run }) { + assert.equal(readerRoot, root) + assert.equal(token, "never-in-git") + return Object.freeze({ run }) + }, + async captureDuplicateDraftRecoveryEvidence({ reader }) { + await reader.run("git", ["rev-parse", "--show-toplevel"], { cwd: root }) + return Object.freeze({}) + }, + canonicalDuplicateDraftEvidence: () => Buffer.from("{}\n"), + }, + }) + assert.equal(result, 0) + assert.ok(environments.length >= 4) + for (const environment of environments) { + assert.equal(environment.GITHUB_TOKEN, undefined) + assert.deepEqual( + Object.keys(environment).filter((name) => name.startsWith("GIT_")), + [], + ) + assert.equal(typeof environment.PATH, "string") + assert.equal(environment.LC_ALL, "C") + } +}) + +test("global or info excludes cannot substitute for the reviewed repository gitignore rule", async (t) => { + const root = await createPrivateRepository(t, { ignored: false }) + const globalIgnore = path.join(root, "global-ignore") + await writeFile(globalIgnore, ".dawn/\n") + await execFile("git", ["-C", root, "config", "core.excludesFile", globalIgnore]) + await writeFile(path.join(root, ".git/info/exclude"), ".dawn/\n") + let constructed = false + const result = await runDuplicateDraftRecoveryCli({ + argv: ["capture", "--reviewed-commit", reviewedCommit(root), "--output", CAPTURE_PATH], + cwd: root, + environment: { GITHUB_TOKEN: "secret" }, + stdout: sink(), + stderr: sink(), + dependencies: { + createDuplicateDraftRecoveryReader() { + constructed = true + }, + }, + }) + assert.equal(result, 1) + assert.equal(constructed, false) +}) + +test("preflights directory fsync before apply mutation and rolls back a published target on later failure", async (t) => { + for (const { fault, expectedMutations } of [ + { fault: { failSyncAt: 1 }, expectedMutations: 0 }, + { fault: { failSyncAt: 2 }, expectedMutations: 1 }, + { fault: { failLink: true }, expectedMutations: 1 }, + { fault: { failLinkAfterPublication: true }, expectedMutations: 1 }, + { fault: { failSyncAt: 3 }, expectedMutations: 1 }, + { fault: { failTempUnlinkOnce: true }, expectedMutations: 1 }, + { fault: { failSyncAt: 4 }, expectedMutations: 1 }, + ]) { + const root = await createPrivateRepository(t) + await writePrivateEvidence(root, Buffer.from("{}\n")) + let mutations = 0 + const result = await successfulApply(root, { + fileSystem: faultingFileSystem(fault), + onApply: () => { + mutations += 1 + }, + }) + assert.equal(result.code, 1) + assert.equal(mutations, expectedMutations) + await assert.rejects(lstat(path.join(root, APPLY_PATH)), { code: "ENOENT" }) + assert.deepEqual(await temporaryFiles(root), []) + } +}) + +test("preflights directory fsync before capture credential or reader construction", async (t) => { + const root = await createPrivateRepository(t) + let credentialRead = false + let readerConstructed = false + const environment = new Proxy( + { GITHUB_TOKEN: "secret" }, + { + getOwnPropertyDescriptor(target, property) { + if (property === "GITHUB_TOKEN") credentialRead = true + return Reflect.getOwnPropertyDescriptor(target, property) + }, + }, + ) + const result = await runDuplicateDraftRecoveryCli({ + argv: ["capture", "--reviewed-commit", reviewedCommit(root), "--output", CAPTURE_PATH], + cwd: root, + environment, + stdout: sink(), + stderr: sink(), + dependencies: { + fileSystem: faultingFileSystem({ failSyncAt: 1 }), + randomUUID: () => UUID, + createDuplicateDraftRecoveryReader() { + readerConstructed = true + }, + }, + }) + assert.equal(result, 1) + assert.equal(credentialRead, false) + assert.equal(readerConstructed, false) +}) + +test("classifies reservation setup cleanup after an ambiguously created temporary file", async (t) => { + for (const { fault, expectedCode, expectedTemporaryFiles } of [ + { + fault: { failTempOpenAfterCreate: true, failTempUnlinkOnce: true }, + expectedCode: 1, + expectedTemporaryFiles: 0, + }, + { + fault: { failTempOpenAfterCreate: true, failTempUnlink: true }, + expectedCode: 3, + expectedTemporaryFiles: 1, + }, + ]) { + const root = await createPrivateRepository(t) + const stderr = sink() + const result = await runDuplicateDraftRecoveryCli({ + argv: ["capture", "--reviewed-commit", reviewedCommit(root), "--output", CAPTURE_PATH], + cwd: root, + environment: { GITHUB_TOKEN: "secret" }, + stdout: sink(), + stderr, + dependencies: { + fileSystem: faultingFileSystem(fault), + randomUUID: () => UUID, + }, + }) + assert.equal(result, expectedCode) + assert.equal( + stderr.text, + expectedCode === 3 + ? "Duplicate draft recovery output cleanup uncertain.\n" + : "Duplicate draft recovery failed.\n", + ) + assert.equal((await temporaryFiles(root)).length, expectedTemporaryFiles) + await assert.rejects(lstat(path.join(root, CAPTURE_PATH)), { code: "ENOENT" }) + } +}) + +test("never removes a preexisting randomized temporary-path collision", async (t) => { + const root = await createPrivateRepository(t) + const temporary = path.join( + root, + ".dawn/release-recovery/.v0.8.22-capture-01.json.12345678-1234-1234-9234-123456789abc.tmp", + ) + await writeFile(temporary, Buffer.alloc(0), { flag: "wx", mode: 0o600 }) + const stderr = sink() + const result = await runDuplicateDraftRecoveryCli({ + argv: ["capture", "--reviewed-commit", reviewedCommit(root), "--output", CAPTURE_PATH], + cwd: root, + environment: { GITHUB_TOKEN: "secret" }, + stdout: sink(), + stderr, + dependencies: { randomUUID: () => UUID }, + }) + assert.equal(result, 3) + assert.equal(stderr.text, "Duplicate draft recovery output cleanup uncertain.\n") + assert.equal((await lstat(temporary)).isFile(), true) +}) + +test("retries directory close independently without misclassifying clean output state", async (t) => { + for (const phase of ["setup", "abort"]) { + for (const failDirectoryCloseTimes of [1, 99]) { + const root = await createPrivateRepository(t) + await writePrivateEvidence(root, Buffer.from("{}\n")) + let directoryCloseCalls = 0 + const result = await successfulApply(root, { + fileSystem: faultingFileSystem({ + failSyncAt: phase === "setup" ? 1 : 2, + failDirectoryCloseTimes, + onDirectoryClose: () => { + directoryCloseCalls += 1 + }, + }), + }) + assert.equal(result.code, 1) + assert.equal(result.stderr, "Duplicate draft recovery failed.\n") + assert.equal(directoryCloseCalls, 2) + await assert.rejects(lstat(path.join(root, APPLY_PATH)), { code: "ENOENT" }) + assert.deepEqual(await temporaryFiles(root), []) + } + } +}) + +test("reports a distinct terminal state if output rollback cannot restore a clean directory", async (t) => { + const root = await createPrivateRepository(t) + await writePrivateEvidence(root, Buffer.from("{}\n")) + const result = await successfulApply(root, { + fileSystem: faultingFileSystem({ + failSyncAt: 3, + failTargetUnlink: path.join(root, APPLY_PATH), + }), + }) + assert.equal(result.code, 3) + assert.equal(result.stderr, "Duplicate draft recovery output cleanup uncertain.\n") + assert.equal((await lstat(path.join(root, APPLY_PATH))).isFile(), true) +}) + +test("a broken stdout never changes durable success into failure", async (t) => { + for (const stdout of [ + { + write: () => { + throw Object.assign(new Error("broken pipe"), { code: "EPIPE" }) + }, + }, + Object.assign(new EventEmitter(), { + write() { + queueMicrotask(() => + this.emit("error", Object.assign(new Error("broken pipe"), { code: "EPIPE" })), + ) + return false + }, + }), + ]) { + const root = await createPrivateRepository(t) + await writePrivateEvidence(root, Buffer.from("{}\n")) + const result = await successfulApply(root, { stdout }) + await new Promise((resolve) => setImmediate(resolve)) + assert.equal(result.code, 0) + assert.equal((await lstat(path.join(root, APPLY_PATH))).isFile(), true) + } +}) + +test("success output listeners are scoped across repeated imported runner calls", async (t) => { + const root = await createPrivateRepository(t) + await writePrivateEvidence(root, Buffer.from("{}\n")) + const stdout = Object.assign(new EventEmitter(), { + write(_value, callback) { + queueMicrotask(() => callback?.()) + return true + }, + }) + for (const output of [APPLY_PATH, ".dawn/release-recovery/v0.8.22-apply-02.json"]) { + const result = await successfulApply(root, { output, stdout }) + assert.equal(result.code, 0) + await new Promise((resolve) => setImmediate(resolve)) + assert.equal(stdout.listenerCount("error"), 0) + } + assert.throws(() => stdout.emit("error", new Error("unrelated later error")), /unrelated/u) +}) + +test("uses fixed POSIX Git isolation and fails closed on Windows", () => { + for (const platform of ["darwin", "linux"]) { + assert.deepEqual(recoveryCliModule.recoveryGitExecutionPolicy(platform), { + executable: "/usr/bin/git", + nullDevice: "/dev/null", + }) + } + assert.throws(() => recoveryCliModule.recoveryGitExecutionPolicy("win32"), /unavailable/iu) +}) + +test("rechecks the reviewed gitignore immediately before publication and rolls back on race", async (t) => { + const root = await createPrivateRepository(t) + await writePrivateEvidence(root, Buffer.from("{}\n")) + let shows = 0 + let mutations = 0 + const runGit = async (command, args, options) => { + const result = await execFile(command, args, options) + if (args.includes("show")) { + shows += 1 + if (shows === 3) await writeFile(path.join(root, ".gitignore"), "elsewhere/\n") + } + return result.stdout + } + const result = await successfulApply(root, { + runGit, + onApply: () => { + mutations += 1 + }, + }) + assert.equal(result.code, 1) + assert.equal(mutations, 1) + await assert.rejects(lstat(path.join(root, APPLY_PATH)), { code: "ENOENT" }) + assert.deepEqual(await temporaryFiles(root), []) +}) + +test("reads the controller schema only from the immutable candidate commit", async () => { + const candidate = { commitSha: "d".repeat(40) } + const source = await readFile("scripts/release/controller-schema.json", "utf8") + const calls = [] + const marker = await readCandidateControllerMarker({ + candidate, + git: { + async showFile(input) { + calls.push(input) + return source + }, + }, + }) + assert.deepEqual(calls, [ + { + ref: candidate.commitSha, + path: "scripts/release/controller-schema.json", + }, + ]) + assert.deepEqual(marker, JSON.parse(source)) +}) + +test("requires owned non-writable directory parents and exact recovery mode 0700", async (t) => { + for (const [target, mode] of [ + [".dawn", 0o777], + [".dawn/release-recovery", 0o755], + ]) { + const root = await createPrivateRepository(t) + await chmod(path.join(root, target), mode) + let constructed = false + const result = await runDuplicateDraftRecoveryCli({ + argv: ["capture", "--reviewed-commit", reviewedCommit(root), "--output", CAPTURE_PATH], + cwd: root, + environment: { GITHUB_TOKEN: "secret" }, + stdout: sink(), + stderr: sink(), + dependencies: { + createDuplicateDraftRecoveryReader() { + constructed = true + }, + }, + }) + assert.equal(result, 1) + assert.equal(constructed, false) + } +}) + +test("production recovery observer derives the canonical numeric Release ID from bracketed recovery reads", async () => { + assert.equal(typeof recoveryCliModule.createProductionRecoveryObserver, "function") + const calls = [] + const reader = productionObserverReader({ calls }) + const normalResult = { + state: "CANDIDATE_ESCROWED", + disposition: "would-transition", + nextTransition: "publish-npm-packages", + conflicts: [], + diagnostics: [], + } + const observer = recoveryCliModule.createProductionRecoveryObserver({ + ...productionObserverInput(reader), + createNormalObserver: () => async (input) => { + calls.push(["normal", input]) + return normalResult + }, + }) + + assert.deepEqual( + await observer({ + candidate: { + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + }, + }), + { ...normalResult, releaseId: 379991871 }, + ) + assert.deepEqual( + calls.map(([kind]) => kind), + ["list", "snapshot", "normal", "list", "snapshot"], + ) +}) + +test("production recovery observer rejects an alternate sole Release ID and a fourth candidate", async () => { + assert.equal(typeof recoveryCliModule.createProductionRecoveryObserver, "function") + const alternate = observerCandidate(400000000, { + tagName: "untagged-alternate", + marker: observerMarker(), + }) + for (const inventory of [[alternate], [...observerInventory(), alternate]]) { + let normalCalls = 0 + const observer = recoveryCliModule.createProductionRecoveryObserver({ + ...productionObserverInput(productionObserverReader({ inventories: [inventory] })), + createNormalObserver: () => async () => { + normalCalls += 1 + return { + state: "CANDIDATE_ESCROWED", + disposition: "would-transition", + nextTransition: "publish-npm-packages", + conflicts: [], + diagnostics: [], + } + }, + }) + await assert.rejects( + observer({ + candidate: { + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + }, + }), + /candidate|inventory|Release|identity/iu, + ) + assert.equal(normalCalls, 0) + } +}) + +test("production recovery observer wires the real normal controller observer by default", async () => { + // Guards the gate that runs AFTER both duplicates are already quarantined: a + // wiring error here would surface at the worst possible moment. Proves the real + // factory composes createGitReader/createGitHubReader/createNpmReader/ + // createProductionInventoryReader/createCliAttestationVerifier for real, and + // that createProductionRecoveryObserver reaches it when no seam is supplied. + assert.equal(typeof recoveryCliModule.createNormalProductionRecoveryObserver, "function") + const normal = recoveryCliModule.createNormalProductionRecoveryObserver({ + root: "/workspace", + token: "test-token", + fileSystem: {}, + runGit: async () => "", + }) + assert.equal(typeof normal, "function") + + const observer = recoveryCliModule.createProductionRecoveryObserver( + productionObserverInput(productionObserverReader({ calls: [] })), + ) + assert.equal(typeof observer, "function") +}) + +test("production recovery observer rejects bracket drift around normal classification", async () => { + assert.equal(typeof recoveryCliModule.createProductionRecoveryObserver, "function") + const after = observerCanonicalSnapshot() + after.assets = after.assets.map((asset, index) => + index === 0 ? { ...asset, size: asset.size + 1 } : asset, + ) + const reader = productionObserverReader({ + snapshots: [observerCanonicalSnapshot(), after], + }) + const observer = recoveryCliModule.createProductionRecoveryObserver({ + ...productionObserverInput(reader), + createNormalObserver: () => async () => ({ + state: "CANDIDATE_ESCROWED", + disposition: "would-transition", + nextTransition: "publish-npm-packages", + conflicts: [], + diagnostics: [], + }), + }) + + await assert.rejects( + observer({ + candidate: { + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + }, + }), + /binding drifted during final authorization/iu, + ) +}) + +async function successfulApply( + root, + { fileSystem: injectedFileSystem, onApply, output = APPLY_PATH, runGit, stdout = sink() } = {}, +) { + const stderr = sink() + const code = await runDuplicateDraftRecoveryCli({ + argv: ["apply", "--evidence", CAPTURE_PATH, ACKNOWLEDGEMENT_FLAG, "--output", output], + cwd: root, + environment: { GITHUB_TOKEN: "secret" }, + stdout, + stderr, + dependencies: { + randomUUID: () => UUID, + ...(injectedFileSystem === undefined ? {} : { fileSystem: injectedFileSystem }), + ...(runGit === undefined ? {} : { runGit }), + parseDuplicateDraftEvidence: () => + Object.freeze({ + reviewedAuthority: Object.freeze({ mergeCommitSha: reviewedCommit(root) }), + }), + createDuplicateDraftRecoveryReader: () => Object.freeze({}), + createProductionRecoveryObserver: () => async () => ({}), + createDuplicateDraftRecoveryWriter: () => Object.freeze({}), + applyDuplicateDraftRecovery: async () => { + onApply?.() + return finalReceipt() + }, + }, + }) + return { code, stdout: stdout.text, stderr: stderr.text } +} + +async function createPrivateRepository(t, { ignored = true } = {}) { + const created = await mkdtemp(path.join(os.tmpdir(), "dawn-recovery-cli-")) + const root = await realpath(created) + t.after(async () => { + const { rm } = await import("node:fs/promises") + await rm(root, { recursive: true, force: true }) + }) + await execFile("git", ["init", "--quiet", root]) + await writeFile(path.join(root, ".gitignore"), ignored ? ".dawn/\n" : "elsewhere/\n") + await mkdir(path.join(root, ".dawn/release-recovery"), { + recursive: true, + mode: 0o700, + }) + await execFile("git", ["-C", root, "add", ".gitignore"]) + await execFile("git", [ + "-C", + root, + "-c", + "user.name=Recovery Test", + "-c", + "user.email=recovery@example.invalid", + "commit", + "--quiet", + "-m", + "fixture", + ]) + const { stdout } = await execFile("git", ["-C", root, "rev-parse", "HEAD"]) + REVIEWED_COMMITS.set(root, stdout.trim()) + return root +} + +async function writePrivateEvidence(root, bytes) { + const target = path.join(root, CAPTURE_PATH) + await writeFile(target, bytes, { flag: "wx", mode: 0o600 }) + await chmod(target, 0o600) +} + +async function temporaryFiles(root) { + const { readdir } = await import("node:fs/promises") + return (await readdir(path.join(root, ".dawn/release-recovery"))).filter((name) => + name.endsWith(".tmp"), + ) +} + +function sink() { + return { + text: "", + write(value) { + this.text += value + return true + }, + } +} + +function productionObserverInput(reader) { + return { + root: "/workspace", + token: "test-token", + reader, + environment: {}, + fileSystem: {}, + runGit: async () => "", + } +} + +function observerMarker() { + return { + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + tag: "v0.8.22", + } +} + +function observerCandidate( + releaseId, + { tagName = "untagged-be0ff4bee4ba43b521a9", marker = null } = {}, +) { + return { + releaseId, + tagName, + title: "Dawn v0.8.22", + draft: true, + prerelease: false, + immutable: false, + targetCommitish: "main", + marker, + } +} + +function observerInventory() { + return [ + observerCandidate(379982100, { tagName: "untagged-a13939767dd2419ade01" }), + observerCandidate(379986168, { tagName: "untagged-20706099efa3c38335a8" }), + observerCandidate(379991871, { marker: observerMarker() }), + ] +} + +function observerCanonicalSnapshot() { + return { + ...observerCandidate(379991871, { marker: observerMarker() }), + body: "canonical body\n", + assets: [{ id: 1, name: "base.tgz", sha256: "a".repeat(64), size: 4 }], + } +} + +function productionObserverReader({ calls = [], inventories, snapshots } = {}) { + let inventoryRead = 0 + let snapshotRead = 0 + const methods = { + async readReviewedMergeAuthority() {}, + async readRepositoryState() {}, + async readCandidateTag() {}, + async readWorkflowState() {}, + async readImmutableReleases() {}, + async readReleaseRuns() {}, + async readCandidatePublishJobs() {}, + async readNpmAbsence() {}, + async readReleaseSnapshot(releaseId) { + calls.push(["snapshot", releaseId]) + const snapshot = snapshots?.[snapshotRead] ?? observerCanonicalSnapshot() + snapshotRead += 1 + return snapshot + }, + async listCandidateReleases() { + calls.push(["list"]) + const inventory = inventories?.[inventoryRead] ?? observerInventory() + inventoryRead += 1 + return inventory + }, + } + return Object.freeze(methods) +} + +const REVIEWED_COMMITS = new Map() + +function reviewedCommit(root) { + const commit = REVIEWED_COMMITS.get(root) + assert.match(commit, /^[0-9a-f]{40}$/u) + return commit +} + +function faultingFileSystem({ + failDirectoryCloseTimes = 0, + failLink, + failLinkAfterPublication, + failSyncAt, + failTargetUnlink, + failTempOpenAfterCreate, + failTempUnlink, + failTempUnlinkOnce, + onDirectoryClose, +} = {}) { + let syncCount = 0 + let tempUnlinkFailed = false + return Object.freeze({ + link: async (...args) => { + if (failLink) throw Object.assign(new Error("link fault"), { code: "EIO" }) + const result = await fileSystem.link(...args) + if (failLinkAfterPublication) { + throw Object.assign(new Error("ambiguous link fault"), { code: "EIO" }) + } + return result + }, + lstat: fileSystem.lstat.bind(fileSystem), + unlink: async (target) => { + if (target === failTargetUnlink) { + throw Object.assign(new Error("unlink fault"), { code: "EIO" }) + } + if (failTempUnlink && target.endsWith(".tmp")) { + throw Object.assign(new Error("temp unlink fault"), { code: "EIO" }) + } + if (failTempUnlinkOnce && target.endsWith(".tmp") && !tempUnlinkFailed) { + tempUnlinkFailed = true + throw Object.assign(new Error("temp unlink fault"), { code: "EIO" }) + } + return fileSystem.unlink(target) + }, + open: async (...args) => { + const handle = await fileSystem.open(...args) + const isDirectory = (args[1] & fsConstants.O_DIRECTORY) !== 0 + if (failTempOpenAfterCreate && String(args[0]).endsWith(".tmp")) { + await handle.close() + throw Object.assign(new Error("ambiguous temp open fault"), { code: "EIO" }) + } + let directoryCloseAttempts = 0 + return Object.freeze({ + read: handle.read.bind(handle), + writeFile: handle.writeFile.bind(handle), + stat: handle.stat.bind(handle), + async close() { + if (isDirectory) { + directoryCloseAttempts += 1 + onDirectoryClose?.() + if (directoryCloseAttempts <= failDirectoryCloseTimes) { + await handle.close() + throw Object.assign(new Error("directory close fault"), { code: "EIO" }) + } + } + return handle.close() + }, + async sync() { + syncCount += 1 + if (syncCount === failSyncAt) { + throw Object.assign(new Error("sync fault"), { code: "EIO" }) + } + return handle.sync() + }, + }) + }, + }) +} + +function finalReceipt() { + return deepFreeze({ + schemaVersion: 1, + atomic: false, + concurrencyAcknowledgement: { + acknowledged: true, + atomic: false, + mode: "operator-freeze-compare-before-write-v1", + releaseIds: [379982100, 379986168], + }, + freezeScope: { + mode: "operator-freeze-compare-before-write-v1", + releaseIds: [379982100, 379986168], + }, + evidenceCapturedAt: "2026-09-01T00:00:00.000Z", + appliedAt: "2026-09-01T00:03:00.000Z", + candidate: { + version: "0.8.22", + commitSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + releaseId: 379991871, + }, + duplicates: [ + { + releaseId: 379982100, + outcome: "performed", + preWriteFence: { + observedAt: "2026-09-01T00:01:00.000Z", + projectionSha256: "a".repeat(64), + tagObjectSha: "b".repeat(40), + }, + postWriteFence: { + observedAt: "2026-09-01T00:01:01.000Z", + projectionSha256: "b".repeat(64), + tagObjectSha: "b".repeat(40), + }, + }, + { + releaseId: 379986168, + outcome: "preexisting-quarantined", + priorFenceObservations: null, + verifiedAt: "2026-09-01T00:02:00.000Z", + projectionSha256: "c".repeat(64), + }, + ], + finalAuthorization: { + state: "CANDIDATE_ESCROWED", + disposition: "would-transition", + nextTransition: "publish-npm-packages", + conflicts: [], + diagnostics: [], + releaseId: 379991871, + }, + }) +} + +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 canonicalize(value) { + if (Array.isArray(value)) return value.map(canonicalize) + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, canonicalize(value[key])]), + ) + } + return value +} diff --git a/scripts/release/test/duplicate-draft-recovery.test.mjs b/scripts/release/test/duplicate-draft-recovery.test.mjs new file mode 100644 index 000000000..2762573f1 --- /dev/null +++ b/scripts/release/test/duplicate-draft-recovery.test.mjs @@ -0,0 +1,2550 @@ +import assert from "node:assert/strict" +import { createHash } from "node:crypto" +import test from "node:test" +import { + applyDuplicateDraftRecovery, + canonicalDuplicateDraftEvidence, + canonicalRecoveryNotice, + canonicalRecoveryReceipt, + captureDuplicateDraftRecoveryEvidence, + classifyDuplicateDraft, + DUPLICATE_DRAFT_RECOVERY_POLICY, + MAX_ARCHIVE_ASSET_BYTES, + originalBodyAssetName, + parseDuplicateDraftEvidence, + recoveryReceiptAssetName, + verifyDuplicateDraftEvidence, +} from "../duplicate-draft-recovery.mjs" +import { + createDuplicateDraftRecoveryReader, + createDuplicateDraftRecoveryWriter, + DuplicateDraftRecoveryReadError, +} from "../duplicate-draft-recovery-adapters.mjs" +import { CANONICAL_RELEASE_PACKAGE_ORDER, canonicalManifestBytes } from "../manifest.mjs" +import { canonicalReleaseBody, parseReleaseMarker } from "../metadata.mjs" + +const POLICY = { + repository: "cacheplane/dawnai", + version: "0.8.22", + candidateSha: "2a80deece2ff958fe7fde8fddeb4f99bed70a1c8", + canonicalReleaseId: 379991871, + duplicates: [ + { releaseId: 379982100, tagName: "untagged-a13939767dd2419ade01" }, + { releaseId: 379986168, tagName: "untagged-20706099efa3c38335a8" }, + ], +} +const CANONICAL_OPAQUE_TAG = "untagged-be0ff4bee4ba43b521a9" +const RELEASE_TITLE = "Dawn v0.8.22" +const GITHUB_BASE = "https://api.github.com/repos/cacheplane/dawnai" +const DECIMAL_ID = /^[1-9][0-9]*$/u + +// Match URLs by exact prefix rather than by interpolating the base into a +// RegExp: an unescaped "." in the hostname would let these matchers accept +// more hosts than the exact API origin. +function exactTrailingId(url, prefix, suffix = "") { + if (!url.startsWith(prefix) || !url.endsWith(suffix)) return null + const rest = url.slice(prefix.length, url.length - suffix.length) + return DECIMAL_ID.test(rest) ? Number(rest) : null +} + +const ORIGINAL_ASSETS = Array.from({ length: 45 }, (_, index) => ({ + id: 101 + index, + name: `asset-${String(index + 1).padStart(2, "0")}.json`, + sha256: "0123456789abcdef"[index % 16].repeat(64), + size: index + 1, +})) +const BASE_ASSET_SET_SHA256 = assetSetSha256(ORIGINAL_ASSETS) +const MANIFEST = createManifest() +const ORIGINAL_MARKER = createEscrowedMarker(MANIFEST) +const ORIGINAL_BODY = canonicalReleaseBody({ marker: ORIGINAL_MARKER, manifest: MANIFEST }) +const BODY_SHA256 = createHash("sha256").update(ORIGINAL_BODY, "utf8").digest("hex") + +function createManifest() { + const packages = CANONICAL_RELEASE_PACKAGE_ORDER.map((name) => { + const filename = `${name.startsWith("@") ? name.slice(1).replaceAll("/", "-") : name}-${POLICY.version}.tgz` + const bytes = Buffer.from(`package:${name}`, "utf8") + const sha512 = createHash("sha512").update(bytes).digest("hex") + return { + name, + version: POLICY.version, + filename, + size: bytes.byteLength, + sha256: createHash("sha256").update(bytes).digest("hex"), + sha512, + npmIntegrity: `sha512-${Buffer.from(sha512, "hex").toString("base64")}`, + access: "public", + } + }) + return { + schemaVersion: 1, + version: POLICY.version, + commitSha: POLICY.candidateSha, + ci: { workflow: "CI", runId: 1, runAttempt: 1 }, + artifact: { + name: `release-v${POLICY.version}-${POLICY.candidateSha.slice(0, 12)}`, + prepareRunId: 2, + prepareRunAttempt: 1, + }, + packageOrder: [...CANONICAL_RELEASE_PACKAGE_ORDER], + packages, + } +} + +function createEscrowedMarker(manifest) { + const subjects = [ + { + name: "manifest.json", + sha256: createHash("sha256").update(canonicalManifestBytes(manifest)).digest("hex"), + }, + ...manifest.packages.map((pkg) => ({ name: pkg.filename, sha256: pkg.sha256 })), + ] + return { + schemaVersion: 1, + epoch: "fixed-group-v1", + revision: 2, + phase: "ESCROWED", + version: POLICY.version, + commitSha: POLICY.candidateSha, + tag: `v${POLICY.version}`, + manifestSha256: createHash("sha256").update(canonicalManifestBytes(manifest)).digest("hex"), + releaseRecordSha256: "e".repeat(64), + baseAssetSetSha256: assetSetSha256(ORIGINAL_ASSETS), + attestationSet: { + repository: POLICY.repository, + workflow: ".github/workflows/release.yml", + sourceRef: `refs/tags/v${POLICY.version}`, + commitSha: POLICY.candidateSha, + workflowRunId: 3, + runAttempt: 1, + subjects: subjects.map(({ name, sha256 }) => ({ + subjectName: name, + subjectSha256: sha256, + bundleName: `${name}.intoto.jsonl`, + bundleSha256: "f".repeat(64), + })), + }, + npmEvidenceSha256: null, + smoke: null, + audit: null, + abandonmentSha256: null, + } +} + +function assetSetSha256(assets) { + return createHash("sha256") + .update(`${JSON.stringify(assets.map(({ name, sha256 }) => ({ name, sha256 })))}\n`, "utf8") + .digest("hex") +} + +function expectedFor(releaseId = POLICY.duplicates[0].releaseId) { + const duplicate = POLICY.duplicates.find((item) => item.releaseId === releaseId) + assert.ok(duplicate) + const recoveryReceipt = { + repository: POLICY.repository, + version: POLICY.version, + candidateSha: POLICY.candidateSha, + recoveryCommit: POLICY.candidateSha, + canonicalReleaseId: POLICY.canonicalReleaseId, + duplicateReleaseId: releaseId, + originalBodySha256: BODY_SHA256, + baseAssetSetSha256: BASE_ASSET_SET_SHA256, + archiveAsset: { + name: originalBodyAssetName(releaseId, BODY_SHA256), + sha256: BODY_SHA256, + }, + } + const receiptBytes = canonicalRecoveryReceipt(recoveryReceipt) + const receiptSha256 = createHash("sha256").update(receiptBytes).digest("hex") + return { + releaseId, + tagName: duplicate.tagName, + canonicalBody: ORIGINAL_BODY, + canonicalMarker: parseReleaseMarker(ORIGINAL_BODY), + originalBodySha256: BODY_SHA256, + originalAssets: ORIGINAL_ASSETS, + recoveryReceipt, + recoveryNotice: canonicalRecoveryNotice({ + repository: POLICY.repository, + version: POLICY.version, + canonicalReleaseId: POLICY.canonicalReleaseId, + duplicateReleaseId: releaseId, + originalBodySha256: BODY_SHA256, + archiveAssetName: originalBodyAssetName(releaseId, BODY_SHA256), + receiptAssetName: recoveryReceiptAssetName(releaseId), + receiptSha256, + }), + } +} + +function snapshot(overrides = {}, releaseId = POLICY.duplicates[0].releaseId) { + const expected = expectedFor(releaseId) + const duplicateIndex = POLICY.duplicates.findIndex( + (duplicate) => duplicate.releaseId === releaseId, + ) + const idOffset = (duplicateIndex + 1) * 1_000 + const duplicateOriginalAssets = expected.originalAssets.map((asset) => ({ + ...asset, + id: asset.id + idOffset, + })) + const evidenceAssets = overrides.evidenceAssets ?? [] + const canonicalReceiptBytes = canonicalRecoveryReceipt(expected.recoveryReceipt) + const receiptBytes = overrides.receiptBytes ?? canonicalReceiptBytes.toString("utf8") + const receiptSha256 = + overrides.receiptSha256 ?? createHash("sha256").update(canonicalReceiptBytes).digest("hex") + const evidence = evidenceAssets.map((kind) => ({ + id: idOffset + (kind === "body" ? 901 : 902), + name: + kind === "body" + ? originalBodyAssetName(expected.releaseId, expected.originalBodySha256) + : recoveryReceiptAssetName(expected.releaseId), + sha256: kind === "body" ? expected.originalBodySha256 : receiptSha256, + size: + kind === "body" + ? Buffer.byteLength(expected.canonicalBody, "utf8") + : Buffer.byteLength(receiptBytes, "utf8"), + ...(kind === "receipt" ? { bytes: receiptBytes } : {}), + })) + return { + releaseId: expected.releaseId, + tagName: expected.tagName, + title: RELEASE_TITLE, + targetCommitish: "main", + draft: true, + prerelease: false, + immutable: false, + body: overrides.quarantined ? expected.recoveryNotice : expected.canonicalBody, + marker: overrides.quarantined ? null : expected.canonicalMarker, + assets: [...duplicateOriginalAssets, ...evidence], + evidenceAssets, + ...Object.fromEntries( + Object.entries(overrides).filter( + ([key]) => key !== "quarantined" && key !== "receiptSha256" && key !== "receiptBytes", + ), + ), + } +} + +test("exports the exact frozen duplicate draft recovery policy", () => { + assert.deepEqual(DUPLICATE_DRAFT_RECOVERY_POLICY, POLICY) + assert.equal(Object.isFrozen(DUPLICATE_DRAFT_RECOVERY_POLICY), true) + assert.equal(Object.isFrozen(DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates), true) + assert.equal(Object.isFrozen(DUPLICATE_DRAFT_RECOVERY_POLICY.duplicates[0]), true) + assert.throws(() => { + DUPLICATE_DRAFT_RECOVERY_POLICY.version = "0.8.23" + }, TypeError) +}) + +test("classifies each exact resumable duplicate state", () => { + for (const duplicate of POLICY.duplicates) { + const expected = expectedFor(duplicate.releaseId) + assert.equal( + classifyDuplicateDraft(snapshot({ evidenceAssets: [] }, duplicate.releaseId), expected), + "untouched", + ) + assert.equal( + classifyDuplicateDraft(snapshot({ evidenceAssets: ["body"] }, duplicate.releaseId), expected), + "body-archived", + ) + assert.equal( + classifyDuplicateDraft( + snapshot({ evidenceAssets: ["body", "receipt"] }, duplicate.releaseId), + expected, + ), + "receipt-archived", + ) + assert.equal( + classifyDuplicateDraft( + snapshot({ quarantined: true, evidenceAssets: ["body", "receipt"] }, duplicate.releaseId), + expected, + ), + "quarantined", + ) + } +}) + +test("compares original asset namespaces without conflating cross-Release asset IDs", () => { + const expected = expectedFor() + const duplicate = snapshot() + + assert.notEqual(duplicate.assets[0].id, expected.originalAssets[0].id) + assert.equal(classifyDuplicateDraft(duplicate, expected), "untouched") + + const observation = recoveryObservation() + const evidence = parseDuplicateDraftEvidence(canonicalDuplicateDraftEvidence(observation)) + assert.equal( + evidence.releases.duplicates[0].assets[0].id, + observation.releases.duplicates[0].assets[0].id, + ) + assert.notEqual( + evidence.releases.duplicates[0].assets[0].id, + evidence.releases.canonical.assets[0].id, + ) +}) + +test("rejects duplicate asset IDs and name collisions within each observed Release", () => { + const expected = expectedFor() + for (const field of ["id", "name"]) { + const conflicting = snapshot() + conflicting.assets[1][field] = conflicting.assets[0][field] + assert.throws(() => classifyDuplicateDraft(conflicting, expected), /asset|unique/iu) + + const observation = recoveryObservation() + observation.releases.duplicates[0].assets[1][field] = + observation.releases.duplicates[0].assets[0][field] + assert.throws(() => canonicalDuplicateDraftEvidence(observation), /asset|unique/iu) + } +}) + +test("rejects identity, marker, body, asset, and evidence conflicts", () => { + const expected = expectedFor() + const cases = [ + ["wrong Release ID", { releaseId: POLICY.canonicalReleaseId }], + ["exact candidate tag", { tagName: `v${POLICY.version}` }], + [ + "changed original asset", + (() => { + const assets = structuredClone(snapshot().assets) + assets[0].sha256 = "e".repeat(64) + return { assets } + })(), + ], + [ + "extra asset", + { assets: [...snapshot().assets, { id: 999, name: "extra.txt", sha256: "f".repeat(64) }] }, + ], + ["noncanonical marker", { marker: { ...ORIGINAL_MARKER, phase: "ATTACHING" } }], + [ + "malformed notice", + { quarantined: true, evidenceAssets: ["body", "receipt"], body: "recovery\n" }, + ], + ["receipt without body archive", { evidenceAssets: ["receipt"] }], + ["unknown evidence combination", { evidenceAssets: ["body", "body"] }], + ] + for (const [name, changes] of cases) { + assert.throws(() => classifyDuplicateDraft(snapshot(changes), expected), undefined, name) + } +}) + +test("does not allow caller expectations to collude on a non-policy opaque tag", () => { + const expected = expectedFor() + const colludingExpected = { ...expected, tagName: "untagged-operator-invented" } + assert.throws(() => + classifyDuplicateDraft(snapshot({ tagName: colludingExpected.tagName }), colludingExpected), + ) +}) + +test("rejects a receipt asset whose digest is not the canonical derived receipt", () => { + const expected = expectedFor() + assert.throws(() => + classifyDuplicateDraft( + snapshot({ evidenceAssets: ["body", "receipt"], receiptSha256: "e".repeat(64) }), + expected, + ), + ) +}) + +test("rejects a receipt asset whose bytes do not equal its canonical digest", () => { + const expected = expectedFor() + assert.throws(() => + classifyDuplicateDraft( + snapshot({ evidenceAssets: ["body", "receipt"], receiptBytes: "tampered receipt\n" }), + expected, + ), + ) +}) + +test("rejects receipt bytes for another duplicate under this duplicate's receipt name", () => { + const expected = expectedFor() + const otherReceipt = expectedFor(POLICY.duplicates[1].releaseId).recoveryReceipt + const otherBytes = canonicalRecoveryReceipt(otherReceipt) + assert.throws(() => + classifyDuplicateDraft( + snapshot({ + evidenceAssets: ["body", "receipt"], + receiptBytes: otherBytes.toString("utf8"), + receiptSha256: createHash("sha256").update(otherBytes).digest("hex"), + }), + { ...expected, recoveryReceipt: otherReceipt }, + ), + ) +}) + +test("rejects receipt bytes for a different original body under this duplicate's receipt name", () => { + const expected = expectedFor() + const wrongBodySha256 = "e".repeat(64) + const wrongReceipt = { + ...expected.recoveryReceipt, + originalBodySha256: wrongBodySha256, + archiveAsset: { + name: originalBodyAssetName(expected.releaseId, wrongBodySha256), + sha256: wrongBodySha256, + }, + } + const wrongBytes = canonicalRecoveryReceipt(wrongReceipt) + assert.throws(() => + classifyDuplicateDraft( + snapshot({ + evidenceAssets: ["body", "receipt"], + receiptBytes: wrongBytes.toString("utf8"), + receiptSha256: createHash("sha256").update(wrongBytes).digest("hex"), + }), + { ...expected, recoveryReceipt: wrongReceipt }, + ), + ) +}) + +test("rejects canonical-looking notices with an invalid schema, type, or duplicate identity", () => { + const expected = expectedFor() + const notice = JSON.parse(expected.recoveryNotice) + for (const change of [ + { schemaVersion: 2 }, + { type: "RECOVERY" }, + { duplicateReleaseId: POLICY.duplicates[1].releaseId }, + ]) { + const malformedExpected = { + ...expected, + recoveryNotice: `${JSON.stringify({ ...notice, ...change })}\n`, + } + assert.throws(() => + classifyDuplicateDraft( + snapshot({ + quarantined: true, + evidenceAssets: ["body", "receipt"], + body: malformedExpected.recoveryNotice, + }), + malformedExpected, + ), + ) + } +}) + +test("rejects a quarantine notice that points at a wrong-body archive", () => { + const expected = expectedFor() + const wrongBodySha256 = "e".repeat(64) + const wrongNotice = canonicalRecoveryNotice({ + repository: POLICY.repository, + version: POLICY.version, + canonicalReleaseId: POLICY.canonicalReleaseId, + duplicateReleaseId: expected.releaseId, + originalBodySha256: wrongBodySha256, + archiveAssetName: originalBodyAssetName(expected.releaseId, wrongBodySha256), + receiptAssetName: recoveryReceiptAssetName(expected.releaseId), + receiptSha256: createHash("sha256") + .update(canonicalRecoveryReceipt(expected.recoveryReceipt)) + .digest("hex"), + }) + assert.throws(() => + classifyDuplicateDraft( + snapshot({ + quarantined: true, + evidenceAssets: ["body", "receipt"], + body: wrongNotice, + }), + { ...expected, recoveryNotice: wrongNotice }, + ), + ) +}) + +test("rejects a detached body and marker pair that is not a canonical Dawn body", () => { + const expected = expectedFor() + const detachedBody = "detached operator body\n" + const detachedBodySha256 = createHash("sha256").update(detachedBody, "utf8").digest("hex") + const detachedReceipt = { + ...expected.recoveryReceipt, + originalBodySha256: detachedBodySha256, + archiveAsset: { + name: originalBodyAssetName(expected.releaseId, detachedBodySha256), + sha256: detachedBodySha256, + }, + } + assert.throws(() => + classifyDuplicateDraft(snapshot({ body: detachedBody }), { + ...expected, + canonicalBody: detachedBody, + originalBodySha256: detachedBodySha256, + recoveryReceipt: detachedReceipt, + }), + ) +}) + +test("rejects an arbitrary base-asset digest even in an otherwise valid canonical body", () => { + const expected = expectedFor() + const arbitraryDigest = "1".repeat(64) + const marker = { ...expected.canonicalMarker, baseAssetSetSha256: arbitraryDigest } + const body = canonicalReleaseBody({ marker, manifest: MANIFEST }) + const bodySha256 = createHash("sha256").update(body, "utf8").digest("hex") + const recoveryReceipt = { + ...expected.recoveryReceipt, + originalBodySha256: bodySha256, + baseAssetSetSha256: arbitraryDigest, + archiveAsset: { + name: originalBodyAssetName(expected.releaseId, bodySha256), + sha256: bodySha256, + }, + } + assert.throws(() => + classifyDuplicateDraft(snapshot({ body, marker }), { + ...expected, + canonicalBody: body, + canonicalMarker: marker, + originalBodySha256: bodySha256, + recoveryReceipt, + }), + ) +}) + +test("derives bounded candidate-specific evidence asset names", () => { + const bodyName = originalBodyAssetName(POLICY.duplicates[0].releaseId, BODY_SHA256) + const receiptName = recoveryReceiptAssetName(POLICY.duplicates[0].releaseId) + assert.match(bodyName, /^dawn-v0\.8\.22-duplicate-379982100-original-body-[0-9a-f]{64}\.txt$/u) + assert.equal(receiptName, "dawn-v0.8.22-duplicate-379982100-recovery-receipt.json") + assert.ok(Buffer.byteLength(bodyName, "ascii") <= 255) + assert.ok(Buffer.byteLength(receiptName, "ascii") <= 255) + assert.throws(() => originalBodyAssetName(POLICY.canonicalReleaseId, BODY_SHA256)) + assert.throws(() => originalBodyAssetName(POLICY.duplicates[0].releaseId, "A".repeat(64))) +}) + +test("creates canonical newline-terminated receipt and notice bytes", () => { + const releaseId = POLICY.duplicates[0].releaseId + const archiveAssetName = originalBodyAssetName(releaseId, BODY_SHA256) + const receiptAssetName = recoveryReceiptAssetName(releaseId) + const receipt = canonicalRecoveryReceipt({ + repository: POLICY.repository, + version: POLICY.version, + candidateSha: POLICY.candidateSha, + recoveryCommit: POLICY.candidateSha, + canonicalReleaseId: POLICY.canonicalReleaseId, + duplicateReleaseId: releaseId, + originalBodySha256: BODY_SHA256, + baseAssetSetSha256: BASE_ASSET_SET_SHA256, + archiveAsset: { name: archiveAssetName, sha256: BODY_SHA256 }, + }) + assert.ok(Buffer.isBuffer(receipt)) + assert.equal(receipt.toString("utf8").endsWith("\n"), true) + assert.equal( + receipt.toString("utf8"), + `{"archiveAsset":{"name":"${archiveAssetName}","sha256":"${BODY_SHA256}"},"baseAssetSetSha256":"${BASE_ASSET_SET_SHA256}","candidateSha":"${POLICY.candidateSha}","canonicalReleaseId":${POLICY.canonicalReleaseId},"duplicateReleaseId":${releaseId},"originalBodySha256":"${BODY_SHA256}","recoveryCommit":"${POLICY.candidateSha}","repository":"${POLICY.repository}","schemaVersion":1,"version":"${POLICY.version}"}\n`, + ) + assert.deepEqual(JSON.parse(receipt), { + schemaVersion: 1, + repository: POLICY.repository, + version: POLICY.version, + candidateSha: POLICY.candidateSha, + recoveryCommit: POLICY.candidateSha, + canonicalReleaseId: POLICY.canonicalReleaseId, + duplicateReleaseId: releaseId, + originalBodySha256: BODY_SHA256, + baseAssetSetSha256: BASE_ASSET_SET_SHA256, + archiveAsset: { name: archiveAssetName, sha256: BODY_SHA256 }, + }) + + const notice = canonicalRecoveryNotice({ + repository: POLICY.repository, + version: POLICY.version, + canonicalReleaseId: POLICY.canonicalReleaseId, + duplicateReleaseId: releaseId, + originalBodySha256: BODY_SHA256, + archiveAssetName, + receiptAssetName, + receiptSha256: createHash("sha256") + .update(canonicalRecoveryReceipt(expectedFor(releaseId).recoveryReceipt)) + .digest("hex"), + }) + assert.equal(typeof notice, "string") + assert.equal(notice.endsWith("\n"), true) + assert.equal( + notice, + `{"archiveAssetName":"${archiveAssetName}","candidateSha":"${POLICY.candidateSha}","canonicalReleaseId":${POLICY.canonicalReleaseId},"duplicateReleaseId":${releaseId},"originalBodySha256":"${BODY_SHA256}","receiptAssetName":"${receiptAssetName}","receiptSha256":"${createHash( + "sha256", + ) + .update(canonicalRecoveryReceipt(expectedFor(releaseId).recoveryReceipt)) + .digest( + "hex", + )}","repository":"${POLICY.repository}","schemaVersion":1,"type":"DAWN_DUPLICATE_DRAFT_RECOVERY","version":"${POLICY.version}"}\n`, + ) + assert.equal(notice.includes("DAWN_RELEASE_CONTROLLER_MARKER"), false) + assert.match(notice, /379991871/u) + assert.match(notice, /379982100/u) +}) + +test("rejects malformed canonical receipt and notice inputs", () => { + assert.throws(() => canonicalRecoveryReceipt({}), undefined) + assert.throws( + () => + canonicalRecoveryNotice({ + repository: POLICY.repository, + version: POLICY.version, + canonicalReleaseId: POLICY.canonicalReleaseId, + duplicateReleaseId: POLICY.duplicates[0].releaseId, + originalBodySha256: BODY_SHA256, + archiveAssetName: "bad asset name", + receiptAssetName: recoveryReceiptAssetName(POLICY.duplicates[0].releaseId), + receiptSha256: "b".repeat(64), + }), + undefined, + ) + assert.throws( + () => + canonicalRecoveryNotice({ + repository: POLICY.repository, + version: POLICY.version, + canonicalReleaseId: POLICY.canonicalReleaseId, + duplicateReleaseId: POLICY.duplicates[0].releaseId, + originalBodySha256: BODY_SHA256, + archiveAssetName: `${originalBodyAssetName(POLICY.duplicates[0].releaseId, BODY_SHA256)}\n