Add singleton GitHub publication command - #14
Conversation
Bind singleton publication to the exact saved Plan bytes and strong ETag. Reconcile ambiguous mutation outcomes without duplicating the request, and validate the complete publication identity before exposing its repository URL.
Technical reviewCloned and ran it: The thing I went looking for is the one claim that involves showing a user something a server said. The printed URL is reconstructed, not inspected
stdout.write(`${repository.html_url}\n`);Which is safe only because value.html_url === `https://github.com/${value.full_name}`The URL is not checked for looking correct. It is rebuilt from const GITHUB_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;within byte limits. So the only string that can ever reach the terminal is That is the correct shape for this, and it is stricter than the The rest of the projection is equally tight. (value.tree_sha === null) !== (value.commit_sha === null)rejects a half-populated repository rather than carrying an inconsistent state forward. Ambiguity is resolved by asking, not by repeating
This is the same three-outcome discipline as firstdraft#279 and #281, which is worth noting because the three landed independently and agree. The new command did not miss authenticationcli#13 added bearer auth to three commands. A fourth arriving separately is exactly where that gets forgotten: Covered. What the green suite does and does not showWorth stating plainly, since the body is already candid about it:
So 130 passing tests demonstrate that the client is internally consistent against a contract this same pull request defines. They do not demonstrate interoperation, because there is nothing on the other end yet. Every fixture is a statement of intent rather than an observation. Centralizing the provisional shape in one module and one fixture is the right mitigation, since it gives the eventual server exactly one place to disagree with. The thing to avoid is reading this suite later as evidence that publication works end to end. On current form that seems unlikely to happen here, given how carefully #265 and #263 separated harness proof from journey proof. No findings |
Lesson: your terminal is an untrusted output sinkputs response["repository_url"]One line, prints a URL that came back from an API. It looks like the least dangerous line of code you will write today. Terminals are not passive. They interpret control characters, and a string arriving from a server can contain them. Carriage return moves the cursor to the start of the line, so everything after it overwrites what came before: The user sees only the second URL. Scrollback contains both, but nobody scrolls back. ANSI escape sequences move the cursor anywhere, set colours, and clear regions. Bidirectional overrides reorder glyphs without changing bytes, so And that is before the user copies the "URL" and pastes it into a shell. Two ways to defend, and one of them is betterSanitize: strip or escape the dangerous characters. puts url.gsub(/[[:cntrl:]]/, "")This works only as well as your blocklist. Did you get bidi overrides? Zero-width joiners? Whatever gets added to Unicode next? Reconstruct: never print the string you were given. Build it from validated pieces and require the original to match. That is what this PR does: value.html_url === `https://github.com/${value.full_name}`
Blocklists need you to think of every attack. Allowlists need you to think of every legitimate value. The second list is shorter, and you already know it, because you wrote the format. Where this shows up in RailsViews are mostly handled for you. ERB escapes by default, and The places that are not handled: puts "Imported #{row['name']}" # rake task, CSV from a customer
logger.info("callback: #{params[:state]}") # log file someone will `cat`
system("open #{url}") # shell, a different injection
File.write(path, "# #{user_supplied}\n") # a file someone will read in a terminalRake tasks and scripts are where this concentrates, because they print external data to a terminal with no framework in between, and they are usually run by someone with credentials loaded. The habitFor anything you print that did not originate in your own process, ask what it can contain. If the answer is "arbitrary bytes from a server," the fix is not to clean it up. The fix is to derive what you print from values you have validated, and to treat any mismatch as an error rather than as something to repair. The compressed version: do not print what they sent you. Print what you can prove, and check that it matches. |
Summary
firstdraft plan publishbound to exact locally pinned Plan bytes and strong ETagProvisional server contract
The server route is intentionally not landed yet. This client centralizes the provisional exact
{ project, compilation, publication }response in one module and test fixture.compilation.head_source_sha256binds the Publication to the saved Plan digest. The Plan ETag inIf-Matchis an application precondition on the Project head, not the validator of the absent Publication representation.This PR does not change
plan compile --output, publish npm, touch a live GitHub repository, or claim a live server smoke.Verification
PATH="/Users/sandbox2/.asdf/shims:$PATH" npm run check