Repository navigation
TS19: parse outside answers and stored data at the boundary - #29
Merged
Merged
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Twitch OAuth and Helix answers, the AuthStore channel registry, GitHub and Cloudflare REST answers, CHANNEL_ORIGINS, daily backups and custom character meta now go through hand-written parsers in server/parse.ts. An answer that is not JSON or has a field of the wrong type gets that route's existing upstream error instead of a crash or a coerced value; valid answers behave as before.
…ly backups module parse.ts is in the strict-tests-shared program, which has no Workers types; importing the type from backups.ts pulled CompressionStream into it (TS2345).
Finesssee
force-pushed
the
ts/ts19-outside-parsers
branch
from
October 11, 2026 19:48
67f062e to
d9d4293
Compare
… every channel; promote says a merge may have happened
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.
TS19 of the strict-TypeScript program. Scope on the Twitch side is the OAuth and Helix answers that
server/auth.tsreads; the Twitch reads ineventsub.tsandaccounts.tsstay unparsed (see Left alone). Answers from outside services and data read back from storage are now checked at the boundary by hand-written parsers inserver/parse.ts. They use the same style as the body parsers: each returnsParsed<T>, and no dependencies are added.When a parse fails, the route answers with the error it already gives when that upstream fails. It never crashes and never coerces a value. A valid answer passes through as the same object, so it behaves as before.
Parsers (server/parse.ts)
parseTwitchTokensPOST /oauth2/token:access_tokenandrefresh_tokenstrings, object kept as sent (it is sealed whole)access()refreshparseTwitchValidation/oauth2/validate:client_idstring,user_idstring when sent,scopesstring array or null when sentaccess()parseHelixUsers/helix/users:datarows withid,login,display_namestrings,created_atandbroadcaster_typestrings when sentparseHelixPagedataan arrayparseModerators/helix/moderation/moderators:datarows that are objects,user_ida string when sent (a row without one names nobody)access()parseChannelRows/list:{key, value}rows, each value a channel record (id,loginstrings,enabledAtnumber,pausedAtnumber,pausedByowner/broadcaster,review {reasons, at}when set)listRecordsparseChannelOriginsCHANNEL_ORIGINS: JSON object of channel to URL, answers each originchannelOriginsparseUploadMetacustom_characters.meta: a JSON objectparseBackupExportexportedAtstring,profiles/purchases/buildsrows with auserIdstring, profiles withusernamereadBackupparseCfEnvelope,parseAnalytics,parseCfDeployments,parseCfVersionscf(), usage, versionsparseGhTree,parseGhContent,parseGhRef,parseGhSaved,parseGhPulls,parseGhPr,parseGhRuns,parseGhPull,parseGhMerge,ignoreAnswergh()and every GitHub routegh()andcf()now take the parser as an argument.ApiResult<T>is{ok:true, data:T}or{ok:false, status, message}, so theas ApiResult<T>casts and the local REST shape types are gone.Changed responses
Only answers that are not JSON or have the wrong shape change. Every row below is pinned by a test.
/auth/callback{}Twitch identity mismatchTwitch identity validation failed/auth/callback<html>Twitch token exchange failed/auth/callbackaccess_tokenTwitch token exchange failed/auth/callback<html>Twitch identity validation failed/auth/callbackuser_id: 2(number)Twitch identity mismatchTwitch identity validation failed/auth/callback<html>/auth/callback?signup=1{}never_streameduncheckedaccess(){pagination:{}}moderator:false, cached a minuteCurrent moderator role cannot be verified, nothing cachedaccess()refresh_tokenTwitch authorization unavailableBroadcaster must reconnect Twitchaccess()scopes: "moderation:read"(a string)String.includesmatched), mod grantedTwitch authorization unavailablelistRecordsenabledAt: "1000"next to good onesregistry records skipped channel:<login>GET /api/dev/channels,/api/channels,/api/picker, sign-up, backups, EventSub routingchannelOrigins["https://chat.miolaf.xyz"]{"0":"https://chat.miolaf.xyz"}{}/catalog["a"]{"0":"a", id, url, custom}{id, url, custom}readBackupuserId: 11{ok:false, error:"invalid_backup"}readBackup{ok:false, error:"invalid_backup"}GET /api/dev/backups?…&day=upstream_error(the route's existing error for an unreadable backup)GET /api/dev/runs<html>upstream_errorgithub_errorList workflow runs: GitHub 200 unreadable answerGET /api/dev/code/tree<html>upstream_errorgithub_errorRead tree: GitHub 200 unreadable answerGET /api/dev/code/file<html>not_a_filegithub_errorRead file: GitHub 200 unreadable answerPOST /api/dev/hotfix<html>pr:null,prError:"Pull request not opened; open it manually"POST /api/dev/promoteshagithub_errorMerge pull request (it may have merged; check GitHub): GitHub 200 unreadable answer, nothing dispatchedGET /api/dev/versions<html>upstream_error{deployments:[], versions:[], error:"Cloudflare API error 200"}usage()(diagnostics)nullerror:"Analytics unavailable", no statuserror:"Analytics query failed",status:200Tests (tests/parse.test.ts)
Each parser has valid, wrong-type and missing-field cases with literal expected values. These tests failed on the parent commit and pass on this branch (red run: 17 pass, 16 fail).
bun run check: 359 tests, 359 pass.Judgment calls
parseChannelRowskeeps the good rows and names the bad keys;listRecordslogs them and lists the rest. A list that is not an array is stillAuth storage unavailable. Refusing the whole list would have taken the picker, sign-up, EventSub routing and the backup cron down for every channel over one corrupt record.parseModeratorsskips a row withoutuser_idinstead of refusing the answer. That row can't match the user, so the answer stays "not a moderator", which still fails closed. Auser_idof the wrong type, a non-object row or a missingdatais refused.tests/registry.test.mjsserves a Helix users body for the moderators URL and depends on this.user_idon validate is optional, because an app token has none. It then fails the existing identity check (403) as before.parseHelixUsers, so it now needsloginanddisplay_nametoo. Real Helix answers always carry them.validateUploadstored.upstream_error. The route throws into the handler's catch, which was already the answer for a stored backup that wasn't JSON.BackupExport.exportedAtis retyped from number to string.exportHeadwrites the ISO time.unreadable answerand go throughghError, so they answer 502github_errorinstead of 503 ornot_a_file.content/commit, may be null, matching what those APIs send.Left alone (outside scope)
handleAuthis returned withoutawaitinside the worker's try, so a throw there escapes the catch.accounts.ts,appTokenineventsub.ts, the room JSON reads in developer.ts (exportData, diagnostics, restore),listBackupsrows, AuthStorerecord()/consume()reads, and the catalog and board reads inworker.ts.twitch()stub intests/registry.test.mjsanswers the moderators URL with a users body.Audit
An Opus audit at d9d4293 found no blockers. Fixed in 33d297f:
promote, after an unreadable merge answer, says the merge may have happened.pausedBy: "broadcaster",pausedAtwithoutpausedBy, and an extra field passing through.Kept as is: the owner lookup needs
loginanddisplay_name(real Helix answers carry both).