fix: the backup key survives recovering the primary, and PLATFORMS.md gets its ninth column back - #600
Merged
Merged
Conversation
… gets its ninth column back Closes #581, and the four doc corrections the docs lane could not reach. RECOVERING ONE KEY DESTROYED THE OTHER. A database restored without its secret.key reads every destination back with an empty StreamKey -- the fail-closed rule -- and flags it. The sealed bytes are still there, and putting the right key file back returns every destination. keepsSealedKey protected those bytes, but it required BOTH halves to be empty. So an operator who gave up on the primary and retyped it took the re-sealing branch for both columns, and sealStreamKey("") returns nil bytes: backup_stream_key_enc became NULL. The act of recovering one half destroyed the other, silently, and no later secret.key could bring it back. The guard's own doc reasons correctly about why the ciphertext is worth keeping; that reasoning was applied to the pair rather than to each half. The READ path condemns both together, which is right -- neither can be shown to be readable. The write path inherited the coupling, where it is wrong. Now decided per half, with the four cases spelled out. PLATFORMS.MD HAD LOST A COLUMN SEPARATOR. Its header carried eight cells where the separator and every data row carry nine: "Viewers" and "Start / end" were fused, so the page rendered with every column after them labelled with its neighbour's name -- Moderation's ticks under "Viewers", and so on. The drift test stayed green through it, and the reason is worth recording: its regexp matches DATA rows, and the cell count is what tells the capability table apart from the others, so a header short one `|` still parsed and every data row still counted right. It now compares the header's cell count to the column list, which is the one thing that ties what a reader sees at the top of the table to what the test compares underneath it. Verified by removing the separator again. Also: CONTRIBUTING.md was the seventh site of the Go floor, still saying 1.26.5 against go.mod's 1.27.0. config.example.yaml never mentioned transcription, so the only way to discover the block was to read the struct. And the CHANGELOG said nothing about TLS moving to :443, which makes a console unreachable after an upgrade behind a firewall that only opens 8080. Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
Both appear in the destination preset catalogue and PLATFORMS.md records them as unverified -- "not built, and the platform's API not confirmed either way". This confirms them either way. It proposes building neither. VIMEO IS GATED, NOT MISSING. Its OAuth is open to any app -- authorization code, implicit, client credentials and DEVICE CODE, which is the flow this repo already implements. Its live API is complete: create, activate, end, ingest status, RTMP destinations, M3U8 playback. And the reference says, verbatim, "our live API is available only to Vimeo Enterprise customers". An operator can therefore authenticate successfully and still be unable to create an event, which for a self-hosted product's audience is the same wall as LinkedIn Live's partner gate. Recording it as "unverified" understates what is known; the obstacle is commercial, not technical. TROVO IS OPEN AND FULLY DOCUMENTED. Seven scopes, each with a documented endpoint: stream key by channel_details_self, metadata by channel_update_self, chat send, moderation via chat commands under manage_messages, and viewer counts needing no scope and no token at all. Broadcast start/end genuinely does not exist, which matches Twitch and Kick rather than being a gap. Two things an implementer would otherwise be bitten by, so they are quoted rather than summarised: a refresh token holds at most FIFTY access tokens at once, so a client refreshing on a timer instead of on expiry exhausts it and the failure arrives as a refused refresh; and the documented rate limit is a real number, 1200/min, which the 2026-08-16 file's rule permits relying on -- unlike YouTube's undocumented broadcast cap. A THIRD FETCH TRAP, added to the two that file already records. developer.vimeo.com answers HTTP 200 with a body containing only the word "Vimeo" to a non-browser fetch: the docs render client-side. A fetcher trusting the status code concludes the page is empty, and one trusting its own memory fills the gap from training data. Every Vimeo claim here was read from a rendered page. Three things are named as NOT established rather than guessed: Trovo's code-to-token exchange endpoint, whether Trovo supports PKCE, and whether any Vimeo tier below Enterprise exposes live. PLATFORMS.md is deliberately not edited. A cell that says "Works" must mean polyemesis does it today. Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
…ring an app A decision recorded, not a change made. Nothing here is built. Every self-hoster currently registers four developer applications to connect four platforms, and the friction is wildly uneven: Twitch is minutes, YouTube is more clicks, Facebook is App Review measured in days. HOW OBS ACTUALLY SOLVES IT, read from its source rather than assumed. It runs a broker: TwitchAuth.cpp points at OAUTH_BASE_URL, which CMakeLists defaults to https://auth.obsproject.com/, and the client secret is not in the binary at all because the broker holds it. The client ID is compiled in and obfuscated. YouTube is the exception and the exception is the useful part: ui-config.h.in ships a YOUTUBE_SECRET alongside its client id, while Twitch ships an id only. Google treats an installed app's secret as non-confidential and Twitch does not. THE PLATFORM'S OWN RULES DECIDE WHETHER A BROKER IS NEEDED, and they differ per platform -- so any design treating "OAuth" as one thing will be wrong for at least one of the four. And a build without credentials loses the feature outright: feature-twitch.cmake disables the whole integration when TWITCH_CLIENTID is empty. That is the honest cost of the model, and it lands harder here than on OBS because this project's users build from source more often. THE RECOMMENDATION IS A HYBRID DEFAULTING TO BRING-YOUR-OWN, and the argument is not operational. README sells a single static binary with no runtime dependencies and COMPARISON sells self-hosting as what distinguishes this from restream.io. A required call to a server we operate makes every install depend on our uptime and routes every operator's tokens through us. As an option it is straightforwardly good; as the only path it contradicts the product. Two cheaper wins are recorded ahead of it. Kick needs no broker at all -- it is OAuth 2.1 with PKCE, a public client, so a client id could ship today with no secret held anywhere. And Facebook cannot be fixed by any of this, because App Review gates the PERMISSIONS rather than the credentials. So this is worth pricing as two and a half platforms, not four. If the broker is built, Cloudflare Workers fits in two routes and four secrets with no database, and it must be STATELESS: sign the state parameter rather than storing a verifier in KV. That is not an optimisation. It is what makes "the broker stores nothing about anyone" a true sentence, which for this product is worth more than the code it saves. It still sees every token in transit, which is unavoidable and is written down rather than buried. Four preconditions are listed, including the one nobody enjoys: the project would be accepting each platform's developer terms on behalf of every operator who uses the default path. Claude-Session: https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL
The pages build refuses any file in docs/ that is in neither PUBLISHED nor NOT_PUBLISHED, so that publishing one is a decision somebody wrote down rather than the default. Adding the design note without classifying it broke that gate, which is the gate working. NOT_PUBLISHED, matching its three siblings: it records what a broker would take and explicitly builds nothing. Publishing a design note for unshipped work reads as a roadmap commitment on a page that otherwise documents what exists.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Closes #581 and the four doc corrections the docs lane could not reach.
Recovering one key destroyed the other — #581
A database restored without its
secret.keyreads every destination back with an emptyStreamKey(the fail-closed rule) and flags it. The sealed bytes are still there — put the right key file back and every destination returns.keepsSealedKeyprotected those bytes, but required both halves to be empty. So an operator who gave up on the primary and retyped it took the re-sealing branch for both columns — andsealStreamKey("")returns nil bytes, sobackup_stream_key_encbecame NULL.The act of recovering one half destroyed the other, silently, and no later
secret.keycould bring it back.The guard's own doc reasons correctly about why the ciphertext is worth keeping — that reasoning was applied to the pair rather than to each half. The read path condemns both together, which is right: neither can be shown to be readable. The write path inherited that coupling, where it's wrong. Now decided per half, four cases spelled out.
PLATFORMS.md had lost a column separator
Its header carried eight cells where the separator and every data row carry nine —
ViewersandStart / endwere fused, so the page rendered with every column after them labelled with its neighbour's name.The drift test stayed green through it, and why is worth recording: its regexp matches data rows, and the cell count is what tells the capability table apart from the others — so a header short one
|still parsed and every data row still counted right.It now compares the header's cell count to the column list, which is the one thing tying what a reader sees at the top of the table to what the test compares underneath it. Verified by removing the separator again: it fails with "the capability table header has 7 capability columns, want 8".
Three more
CONTRIBUTING.mdwas the seventh site of the Go floor, still 1.26.5 againstgo.mod's 1.27.0config.example.yamlnever mentionedtranscription— the only way to find the block was to read the structCHANGELOG.mdsaid nothing about TLS moving to :443, which makes a console unreachable after an upgrade behind a firewall that only opens 8080Both new guards mutation-verified.
gofmt,go vet, full suite green.https://claude.ai/code/session_01A8N3W5ct9SZtHK9sCDD9cL