mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-26 15:38:52 -07:00
[studio] Complete editor uploads and queued cloud builds (#135)
* [studio] Complete editor uploads and queued cloud builds * [studio] Protect unpublished source and remove unused notification RPC
This commit is contained in:
@@ -1111,6 +1111,14 @@ export const SanitizeRequest = z.object({
|
||||
/** `POST /api/sanitize/v1/isPure` — whether the text is free of profanity. */
|
||||
export const IsPureResponse = z.object({ IsPure: z.boolean() })
|
||||
|
||||
/** Studio sends repeated Values form fields and consumes this result envelope. */
|
||||
export const BulkAllowedRequest = z.object({ Values: z.array(z.string()) })
|
||||
export const BulkAllowedResponse = z.object({
|
||||
Success: z.boolean(),
|
||||
Error: z.string().nullable(),
|
||||
Value: z.object({ ArePure: z.array(z.boolean()) }).nullable(),
|
||||
})
|
||||
|
||||
/** `GET /api/keepsakes/globalconfig` — the keepsake feature switches. */
|
||||
export const KeepsakeConfig = z.object({
|
||||
KeepsakeFeatureEnabled: z.boolean(),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Hono } from 'hono'
|
||||
import { describeRoute } from 'hono-openapi'
|
||||
|
||||
import { validateStudioBuildValidationToken } from '@repo/jwt'
|
||||
|
||||
import charadesAprilWords from '../../static/charades-april.json'
|
||||
import charadesWords from '../../static/charades.json'
|
||||
import communityBoard from '../../static/community-board.json'
|
||||
@@ -8,6 +10,9 @@ import { authedId, unauthorized } from '../http'
|
||||
import {
|
||||
AUTHED,
|
||||
BareString,
|
||||
BulkAllowedRequest,
|
||||
BulkAllowedResponse,
|
||||
form,
|
||||
idParam,
|
||||
IsPureResponse,
|
||||
json,
|
||||
@@ -144,6 +149,42 @@ export const gameplayRoutes = new Hono<App>({ strict: false })
|
||||
}
|
||||
)
|
||||
|
||||
// Studio checks all component text in one request, preserving duplicates and order.
|
||||
.post(
|
||||
'/api/sanitize/v2/isAllowed/bulk',
|
||||
describeRoute({
|
||||
tags: ['Gameplay'],
|
||||
summary: 'Check Studio component text in bulk',
|
||||
description:
|
||||
'Checks repeated Values form fields using the existing profanity policy. ArePure matches the input order, including duplicate and empty strings. Accepts an account token or a dedicated Studio build-validation credential with no account identity.',
|
||||
security: AUTHED,
|
||||
requestBody: form(BulkAllowedRequest, 'Repeated Values fields'),
|
||||
responses: {
|
||||
200: json(BulkAllowedResponse, 'Ordered results in a Studio result envelope'),
|
||||
400: json(BulkAllowedResponse, 'Invalid form values'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
if (
|
||||
(await authedId(c)) === null &&
|
||||
!(await validateStudioBuildValidationToken(c.req.raw, await c.env.JWT_SECRET.get()))
|
||||
)
|
||||
return unauthorized(c)
|
||||
const body = await c.req.parseBody({ all: true }).catch(() => null)
|
||||
const field = body?.Values
|
||||
const values = field === undefined ? [] : Array.isArray(field) ? field : [field]
|
||||
if (body === null || values.some((value) => typeof value !== 'string')) {
|
||||
return c.json({ Success: false, Error: 'Values must be strings.', Value: null }, 400)
|
||||
}
|
||||
return c.json({
|
||||
Success: true,
|
||||
Error: null,
|
||||
Value: { ArePure: values.map((value) => !containsSwears(value as string)) },
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// ---- Activities -----------------------------------------------------------
|
||||
// Word bank for the Charades activity. The client requests the list by
|
||||
// activity name (`.../words/Charades`); other activities have no data yet.
|
||||
|
||||
@@ -4756,6 +4756,59 @@ describe('public endpoints', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/sanitize/v2/isAllowed/bulk', () => {
|
||||
const bulk = async (body: URLSearchParams | FormData, authed = true) =>
|
||||
exports.default.fetch(`${ORIGIN}/api/sanitize/v2/isAllowed/bulk`, {
|
||||
method: 'POST',
|
||||
headers: authed ? await bearer('42') : {},
|
||||
body,
|
||||
})
|
||||
|
||||
test('401s without a token', async () => {
|
||||
expect((await bulk(new URLSearchParams('Values=hello'), false)).status).toBe(401)
|
||||
})
|
||||
|
||||
test('preserves repeated values, order, empty text and duplicates in the Studio envelope', async () => {
|
||||
const body = new URLSearchParams()
|
||||
for (const text of ['MakerRoom', 'fuck this', '', 'Scunthorpe', 'MakerRoom', 'sh1t'])
|
||||
body.append('Values', text)
|
||||
const res = await bulk(body)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
Success: true,
|
||||
Error: null,
|
||||
Value: { ArePure: [true, false, true, true, true, false] },
|
||||
})
|
||||
})
|
||||
|
||||
test('accepts a single multipart value and an empty batch', async () => {
|
||||
const body = new FormData()
|
||||
body.append('Values', 'a$$hole')
|
||||
expect(await (await bulk(body)).json()).toEqual({
|
||||
Success: true,
|
||||
Error: null,
|
||||
Value: { ArePure: [false] },
|
||||
})
|
||||
expect(await (await bulk(new URLSearchParams())).json()).toEqual({
|
||||
Success: true,
|
||||
Error: null,
|
||||
Value: { ArePure: [] },
|
||||
})
|
||||
})
|
||||
|
||||
test('rejects file values rather than marking them clean', async () => {
|
||||
const body = new FormData()
|
||||
body.append('Values', new Blob(['hello']), 'text.txt')
|
||||
const res = await bulk(body)
|
||||
expect(res.status).toBe(400)
|
||||
expect(await res.json()).toEqual({
|
||||
Success: false,
|
||||
Error: 'Values must be strings.',
|
||||
Value: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/sanitize/v1/isPure', () => {
|
||||
const isPure = async (Value?: string, authed = true) =>
|
||||
exports.default.fetch(`${ORIGIN}/api/sanitize/v1/isPure`, {
|
||||
@@ -10343,6 +10396,7 @@ describe('openapi', () => {
|
||||
'POST /api/rooms/v1/verifyRole',
|
||||
'POST /api/sanitize/v1',
|
||||
'POST /api/sanitize/v1/isPure',
|
||||
'POST /api/sanitize/v2/isAllowed/bulk',
|
||||
'POST /api/v1/progression/bulk',
|
||||
'POST /outfits/bulk',
|
||||
'POST /statsigUserProperties',
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { adminSecretsStore, env, SELF } from 'cloudflare:test'
|
||||
import { beforeAll, expect, it } from 'vitest'
|
||||
|
||||
import { generateStudioBuildValidationToken, validateAndGetAccountId } from '@repo/jwt'
|
||||
|
||||
import type { Env } from '../../context'
|
||||
|
||||
declare module 'cloudflare:test' {
|
||||
interface ProvidedEnv extends Env {}
|
||||
}
|
||||
beforeAll(async () => {
|
||||
await adminSecretsStore(env.JWT_SECRET).create('test-key')
|
||||
})
|
||||
it('allows build validation credentials to check text without granting an account identity', async () => {
|
||||
const token = await generateStudioBuildValidationToken('123456', 'test-key')
|
||||
const headers = { Authorization: 'Bearer ' + token }
|
||||
expect(
|
||||
await validateAndGetAccountId(new Request('https://example.com', { headers }), 'test-key')
|
||||
).toBeNull()
|
||||
const response = await SELF.fetch('https://example.com/api/sanitize/v2/isAllowed/bulk', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: new URLSearchParams('Values=hello&Values=fuck'),
|
||||
})
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toEqual({
|
||||
Success: true,
|
||||
Error: null,
|
||||
Value: { ArePure: [true, false] },
|
||||
})
|
||||
expect(
|
||||
(
|
||||
await SELF.fetch('https://example.com/api/PlayerCheer/v1/create', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: new URLSearchParams(),
|
||||
})
|
||||
).status
|
||||
).toBe(401)
|
||||
})
|
||||
it('rejects an invalid build validation signature', async () => {
|
||||
const token = await generateStudioBuildValidationToken('123456', 'wrong-key')
|
||||
expect(
|
||||
(
|
||||
await SELF.fetch('https://example.com/api/sanitize/v2/isAllowed/bulk', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: 'Bearer ' + token },
|
||||
body: new URLSearchParams('Values=hello'),
|
||||
})
|
||||
).status
|
||||
).toBe(401)
|
||||
})
|
||||
Reference in New Issue
Block a user