[studio] beta studio (#114)

* rrs stuff

* studio service yay

* rrs whitelist

* moree studiooooooo fellowwww

* rec room studio room loading yay

* fixes yay

* fix 2 because idk  yayuh

* Update cdn.app.ts

* ccontent length fix?

* Preserve content-length and upgrade metadata

Return a real zero-filled byte array for CDN HEAD responses so the runtime preserves Content-Length while still sending no payload. In notify, re-create the Request with its original headers, set OWNER_HEADER from the validated token, and forward that Request to hub.fetch to avoid losing WebSocket upgrade metadata (which could trigger 500s in Durable Objects). These changes ensure the editor's size checks and WebSocket upgrades remain reliable.

* fix x524234

* shirt thing?

* [auth] remove grant table as we already have audit_log for now

* rename table to generic device_grant

* cleanup/lint

---------

Co-authored-by: Devin Zuczek <devin.zuczek@gmail.com>
This commit is contained in:
Crasho
2026-10-05 19:39:22 +01:00
committed by GitHub
parent 0900bcac25
commit 26270e9c25
52 changed files with 18231 additions and 103 deletions
+57 -3
View File
@@ -105,7 +105,16 @@ export const FakeCachedLogin = CachedLogin.extend({
/** OAuth-shaped error body. Always HTTP 400 except `server_error` (500). */
export const OAuthError = z.object({
error: z.enum(['invalid_grant', 'invalid_request', 'server_error']),
error: z.enum([
'invalid_grant',
'invalid_request',
'invalid_client',
'server_error',
// RFC 8628. Studio polls until it sees something other than authorization_pending.
'authorization_pending',
'expired_token',
'access_denied',
]),
error_description: z.string(),
})
@@ -128,8 +137,17 @@ export const TokenResponse = z.object({
*/
export const TokenRequest = z.object({
grant_type: z
.enum(['create_account', 'cached_login', 'refresh_token', 'password'])
.describe('Anything unrecognised (including absent) is treated as a password grant'),
.enum([
'create_account',
'cached_login',
'refresh_token',
'password',
'urn:ietf:params:oauth:grant-type:device_code',
])
.describe(
'Anything unrecognised (including absent) is treated as a password grant, except the ' +
'Studio device-code URN, which polls a code from POST /connect/deviceauthorization'
),
account_id: z.string().optional().describe('Numeric account id, as a string'),
username: z
.string()
@@ -156,6 +174,15 @@ export const TokenRequest = z.object({
'Meta: `{"Nonce":…,"AppId":…,"Source":…}`'
),
refresh_token: z.string().optional().describe('Required on a refresh_token grant'),
client_id: z.string().optional().describe('Studio device grant: `recroom.studio`'),
client_secret: z
.string()
.optional()
.describe('Studio device grant: the secret embedded in the editor'),
device_code: z
.string()
.optional()
.describe('Studio device-code grant: the device_code from /connect/deviceauthorization'),
device_id: z
.string()
.optional()
@@ -171,6 +198,33 @@ export const TokenRequest = z.object({
),
})
/** `POST /connect/deviceauthorization` form body. Studio's editor sends exactly these two. */
export const DeviceAuthorizationRequest = z.object({
client_id: z.string().describe('`recroom.studio`'),
client_secret: z.string().describe('The secret compiled into Rec Room Studio'),
})
/** RFC 8628 response. Property names match Studio's `DeviceAuthorizationResponse`. */
export const DeviceAuthorizationResponse = z.object({
device_code: z.string(),
user_code: z.string().describe('Shown in the editor; also on verification_uri_complete'),
verification_uri: z.string().describe('WWW `/device`, without the code'),
verification_uri_complete: z
.string()
.describe('The URL Studio opens in the browser, `verification_uri?user_code=`'),
expires_in: z.int().describe('Seconds until the code dies. Studio treats this as seconds'),
interval: z.int().describe('Seconds Studio waits between polls'),
})
/** `POST /connect/device/approve` and `/connect/device/deny` form body. */
export const DeviceDecisionRequest = z.object({
user_code: z.string().describe('The code Studio is showing, spaces and hyphens ignored'),
})
export const DeviceDecisionResponse = z.object({
ok: z.literal(true),
})
/** `POST /account/me/changepassword` form body. */
export const ChangePasswordRequest = z.object({
newPassword: z.string().describe('Required; empty is rejected'),