@volter/twin-youtube
v0.1.35
Published
Local YouTube Data API v3 twin (resumable uploads, media served with Range) with a youtube.com mirror, built on @volter/world-core.
Downloads
2,376
Readme
@volter/twin-youtube
A local, stateful, vendor-faithful twin of the YouTube Data API v3
(https://www.googleapis.com/youtube/v3/*), built on the shared @volter/world-core kernel.
Point an unmodified client at it — Google's own generated @googleapis/youtube, or plain fetch —
and get vendor-correct responses offline: the real list envelope, real part selection,
string-typed statistics, the real Google error envelope, a modeled daily quota that actually
runs out, and videos.insert through the real resumable upload protocol, with the uploaded bytes
kept and served back with HTTP Range. A youtube.com mirror shows the channel and its videos as a
creator sees them.
bun packages/twin/youtube/src/cli.ts serve --port 12180
# then, from your app:
YOUTUBE_TWIN_URL=http://127.0.0.1:12180 node --require @volter/world-core/inject app.js
# youtube.com over the same state: sign in, channel page, watch page, upload
bun packages/twin/youtube/src/cli.ts mirror --port 12181Coverage
Partial and honest. The manifest
(src/youtube-capabilities.ts) is the real vendor surface as the denominator — enumerated
top-down from the vendor's own reference nav, every resource × every published method — so
coverage reads well under 100% and growing the denominator later will lower it further. That is
success, not regression.
The manifest declares 101 done of 209 capabilities; the gate's recorded baseline (census.json) is the floor it may not fall below.
What is modeled
| Area | State |
|---|---|
| channels.list | id / forHandle / forUsername filters; snippet, statistics, contentDetails, status, topicDetails, brandingSettings parts |
| videos.list | id filter + chart=mostPopular ordering over real state; snippet, contentDetails, statistics, status, player parts |
| search.list | q, type, channelId, order, publishedAfter/Before, pagination, the youtube#searchResult id shape |
| playlists | list / insert / update / delete (the OAuth write surface); update and delete of another channel's playlist are 403 playlistForbidden |
| playlistItems | list / insert / delete, with dense 0..n-1 positions maintained across both (in another channel's playlist, 403 playlistItemsNotAccessible); a channel's UU… uploads playlist lists its videos newest first |
| videos.insert | uploadType=resumable (a random session id in Location; chunked PUT with Content-Range on the session URI, which is its own credential (no bearer needed); every chunk but the last must be a multiple of 256 KiB, and the twin keeps such a chunk in whole 256 KiB units (its own rule; Google documents only the multiple); 308 Resume Incomplete with Range; bytes held plus a body past X-Upload-Content-Length refused, with or without Content-Range; 201 with the video; 404 for an unknown session or one past the twin's one-week lifetime), uploadType=multipart and uploadType=media; metadata validated at the first request (invalidTitle, invalidDescription, invalidCategoryId, mediaBodyRequired); charged to the upload bucket. A new video reads uploadStatus: uploaded; processing (duration and definition read from the MP4's own headers) lands on a later read, by World time |
| thumbnails.set | uploadType=media or multipart; only the video's channel may set it (403 forbidden); a video.thumbnail entry that performs as thumbnails.set on its own (on an uploaded, published or pulled video); the image stored and served at the URLs the response names (invalidImage, mediaBodyRequired, videoNotFound) |
| media | the uploaded bytes at <base>/videoplayback?v=<id> with Range (206 / 416, an open-ended range capped at 8 MiB, a whole GET streamed, HEAD from the size) and custom thumbnails at <base>/vi/<id>/<size>.jpg, under the twin's public base, read at the branch or its ancestors; a private video plays only for its channel's token (access_token=), else 403; an unlisted one plays for anyone with the link |
| reference data | videoCategories, i18nLanguages, i18nRegions, videoAbuseReportReasons |
| auth | API key as ?key= and as X-Goog-Api-Key; writes require an OAuth bearer token registered through /_twin/tokens (kept in the log as its SHA-256 — unsalted: the kernel offers no per-World secret to key an HMAC with): it is its channel for channels.list?mine=true and for every write it carries, and a bearer never registered is Google's 401 authError on mine and on every write |
| quota | all three documented daily buckets, with the vendor's own per-method unit costs |
| errors | the real Google envelope — unknownPart, missingRequiredParameter, incompatibleParameters, invalidFilters, invalidPageToken, quotaExceeded, rateLimitExceeded, playlistNotFound, videoNotFound, and 403 on owner-only parts |
| connector | pull channels + videos over an injected client, idempotent, chunked at 50 ids/call; a World's uploaded video performs on the real root as a resumable upload of its bytes (with its custom thumbnail), charged to the pack's rate budget |
| mirror | youtube.com: sign-in, the channel page, the watch page with the player, the upload dialog |
Everything else the vendor publishes — subscriptions, comments/commentThreads, captions,
channelSections, activities, watermarks, members and the whole Live Streaming family — is a
filed todo in the manifest, and an unmodeled route 404s like the vendor rather than faking a
success.
Wire facts this twin gets right (and most mocks get wrong)
statisticscounts are STRINGS.viewCount,subscriberCount,videoCount,likeCount,commentCount— all strings on the wire. So iscontentDetails.caption("true"/"false").- An unknown id on a list endpoint is
200withitems: [], never a 404. partselects the response. Ask forsnippetand you do not getstatistics.- Multi-value parameters arrive in two forms.
?part=a,b(what a hand-built client sends) and?part=a&part=b(what Google's generated SDK sends). Both are accepted; supporting only the first silently drops every value after the first. - Owner-only parts 403.
fileDetails/processingDetails/suggestionsonvideos.list,auditDetails/contentOwnerDetailsonchannels.list. - Reads take an API key; writes take OAuth. A write with only a key is
401 Login Required.
Quota
The twin models the vendor's current three-bucket quota, not the widely-repeated legacy one:
100 search.list calls, 100 videos.insert calls, and 10,000 units/day for everything else,
with reads at 1 unit and writes at 50
(getting-started §Quota usage,
determine_quota_cost). The
often-cited search.list = 100 units and videos.insert = 1600 units figures are stale.
Exhausting a bucket returns the real 403 quotaExceeded (domain youtube.quota) — and returns it
instead of serving the call. Arm it in a test through the twin-only route:
curl -XPOST localhost:12180/youtube/v3/_twin/quota -d '{"unitsAllowance":2}'Seeding (twin-only routes)
YouTube exposes no API route that creates a channel, and no OAuth consent a World can run for every test. So channels, metadata-only videos and access tokens are seeded through twin-only routes that sit outside the vendor's surface and that a real client never calls — deliberately absent from the manifest, because counting scaffolding as coverage would be padding:
POST /youtube/v3/_twin/channelsPOST /youtube/v3/_twin/videos(a video with metadata and no media)POST /youtube/v3/_twin/tokens—{ "token", "channelId" }: a bearer that signs in as the channelPOST /youtube/v3/_twin/quota·GET /youtube/v3/_twin/quota
A video with media, playlists and playlist items are not seeded this way: they have a real
vendor write path (videos.insert, playlists.insert, playlistItems.insert), so they are created
through it. Resumable sessions and their staged bytes live in the service's byte annex, not in the
event log; only the finished upload is a video.create action.
Fidelity
src/youtube-sdk.integration.test.ts drives Google's own generated client,
@googleapis/youtube@^34, unmodified at the twin. The only thing configured is the SDK's own
public rootUrl option — configuration, not modification. That test earned its place immediately:
it caught the repeated-key parameter form described above, which a hand-written fetch test could
not have surfaced.
The mirror
world-youtube mirror serves youtube.com over the twin's state on one origin, and a served World
mounts the same shell at <vendor>/mirror/. A creator uploads a video and reads their channel in
the browser, so the rule in Adding a twin — "when
someone does this vendor's core job, do they open a browser or write code?" — asks for one.
- Sign-in is Google's two steps: the channel's
@handle, then the bearer registered through/_twin/tokensin place of a password, checked bychannels.list?mine=true. Both stay in the tab'ssessionStorage. - The channel page reads
channels.list?forHandle=, then the channel'sUU…uploads playlist andvideos.list: banner, avatar, name,@handle, subscriber and video counts (the channel'sstatistics), and the Videos tab's grid (Latest / Popular / Oldest) with duration badges. A thumbnail is the custom one set throughthumbnails.set, else a frame of the video's own bytes. - The watch page plays
videoplayback?v=<id>in an HTML5<video>under youtube.com's player chrome (seeking is a Range read; the signed-in token rides the URL asaccess_token, so a private video plays for its own channel), with the title, the channel row, the description box and the channel's other uploads beside it. - Upload (Create) is the resumable
videos.insertfrom the browser: select a file, set title, description and visibility, publish, and land on the video's watch page.
The twin serves no channel art, so the banner and avatar are drawn from the channel's id and title.
Rate budget
Live calls go through one guarded choke point, liveYouTubeExecute, with a persistent
fail-closed ledger (src/youtube-budget.ts). YouTube's published scheme is already a weighted
unit budget, so the declaration reproduces the vendor's own numbers — a read costs 1, a write costs
50 — rather than inventing weights. See that file's header for how the daily allowance becomes an
hourly ceiling (400 units/hour ⇒ 9,600/day, under the real 10,000) and why the 60-unit/60s burst
sub-ceiling is what it is.
Raw calls to www.googleapis.com outside that factory are banned, and
src/youtube-budget.test.ts enforces the ban over tracked files.
Connector
pullYouTubeChannels / pullYouTubeVideos / syncYouTubeFromReal pull real metadata over an
injected client (the real API in prod, a fake in every test), fold it through the kernel observation path, and
are idempotent — a re-pull of identical state appends nothing.
A World's upload performs with its bytes. performYouTubeAction sends a video.create entry to
the vendor through the resumable videos.insert over the credential the kernel's executor sets at a
real boundary. The pack's rate budget (budgetedYouTubeExecute, one ledger per sealed credential — every World and branch performing with one Google key shares it; a twin-only perform with none sealed uses its control root's) charges
the insert ONCE, at the session start, at YouTube's documented 1 quota — the session's chunk PUTs are
never charged, as Google's quota counts the insert and not the bytes, so a started upload always
finishes or fails on the vendor's own answer. The session URI is kept in the byte annex as soon as it
opens, so a perform after a crash asks that session where it stands before opening another. The session POST carries the entry's snippet and status; the session URI's
path is anchored at /upload/youtube/v3/ (the executor adds the root's origin and prefix); the bytes
cross in 4 MiB PUT chunks read as ranges from the byte annex (the branch's or an ancestor's),
following each 308's Range. A 5xx or a failed request is recovered by asking the session where
it stands, with exponential backoff, at most five times in a row — so a final chunk whose answer was
lost is never uploaded twice — and a chunk the vendor keeps none of twice in a row fails the perform.
The 201's video id is the entry's external id, and the video's custom thumbnail, if the World set
one, crosses with it through thumbnails.set — the latest image, which covers every thumbnail entry
of that video written so far. A thumbnail set later (on an uploaded, published or pulled video) is its
own video.thumbnail entry and crosses on its own perform; an entry already covered, or superseded by
a later one still to perform, sends nothing, so several thumbnails make one thumbnails.set with the
latest image. A thumbnail entry whose video is not at YouTube (its upload reverted or never performed,
and never pulled) is refused with that reason, never sent with a local id.
The twin's own accounting — registered tokens, the quota ledger, a channel's upload count — is
bookkeeping (_-typed subjects), never a deployable entry.
Privacy in listings: the uploads playlist lists a channel's private videos to every caller. Google's playlistItems reference does not say what another channel sees there, so the twin does not guess.
Playlist push is a filed gap, not a stub. Those writes require an OAuth 2.0 user credential the
API-key client never mints, so youTubeRequestForAction returns null and a push is an honest
no-op that issues no request (youtube.connector.push_oauth_writes, todo).
Injection
Set YOUTUBE_TWIN_URL and the injector routes YouTube traffic here. Two hosts are intercepted, and
both matter:
youtube.googleapis.com— what Google's own generated client actually calls. Every one of@googleapis/youtube's 83 endpoint methods defaults torootUrl || 'https://youtube.googleapis.com/'. Not shared with any other vendor, so no path test applies.www.googleapis.com— the legacy alias, which also serves v3 and is what hand-built clients send (the priority consumer uses exactly this). Shared withgoogleauth(Google's OAuth2 token exchange), soVENDOR_HOSTSdisambiguates the two by path —/youtube/v3/*and/upload/youtube/v3/*are this twin's; everything else on that host stays googleauth's.
A §9 review caught this pack originally matching only
www.googleapis.com, which meant a default-configured SDK escaped the injector entirely and tunnelled to the real Google. The pack's own SDK fidelity test could not catch it, because that test setsrootUrlat the twin and so never exercises the SDK's default. This is the openai/posthog/supermemory/xai incident class: a vendor key existed, but not for the host the SDK really uses.
See the PATH-AWARE DISAMBIGUATION header in packages/world-core/inject.cjs and the
assertions in scripts/vendor-hosts.test.ts, which now pin both hosts, the /upload/ prefix, and —
structurally — that every post-TLS handler resolves with a path.
If your app also uses real Google auth
Set GOOGLEAUTH_TWIN_URL too, or Google auth calls will fail with a 502.
Because www.googleapis.com is shared, running the YouTube twin makes the world intercept the
whole host — the decision to intercept happens at TLS-handshake time, when only the hostname is
known, before any path exists. Once TLS is terminated there is no way back to a passthrough tunnel.
So a world with YOUTUBE_TWIN_URL set but GOOGLEAUTH_TWIN_URL unset will answer
https://www.googleapis.com/oauth2/v1/certs (which google-auth-library fetches) with:
502 no twin serves www.googleapis.com for path "/oauth2/v1/certs".That is the honest answer rather than a wrong one — the alternative, before paths were considered at
all, was routing those requests into whichever twin happened to be declared first — but it is a
new constraint. The 502 body names the variable to set. Hosts this twin does not claim
(youtube.googleapis.com is its own) are unaffected.
