[api] 2025 inventions

This commit is contained in:
Devin Zuczek
2026-08-29 14:40:57 -04:00
parent 750413f240
commit 5aea5a4cf9
5 changed files with 1488 additions and 73 deletions
+559 -1
View File
@@ -63,7 +63,11 @@ import { getWarningsAgainst, SCHEMA_DDL as WARNINGS_SCHEMA_DDL } from '../../war
import type { SavedImage } from '@repo/domain'
import type { Env } from '../../context'
import type { EventTag, PlayerEvent, PlayerEventEnvelope, PlayerEventResult } from '../../events-db'
import type { InventionSaveResult, SavedInvention } from '../../inventions-db'
import type {
InventionSaveResult,
InventionSaveV9Result,
SavedInvention,
} from '../../inventions-db'
declare module 'cloudflare:test' {
interface ProvidedEnv extends Env {}
@@ -1808,6 +1812,557 @@ describe('public endpoints', () => {
expect((await one.json()) as SavedInvention).toMatchObject({ InventionId: saved.InventionId })
})
test('POST /api/inventions/v9/save answers the enveloped result the client reads', async () => {
const body = {
name: '082926 13:42:46',
description: 'No description yet',
imageName: 'invention/2026-08-29/52c1e282-76f5-4974-975f-d85060884085.jpg',
hasBetaContent: false,
instantiationCost: 101,
lightsCost: 0,
chipsCost: 0,
cloudVariablesCost: 0,
aiCost: 0,
ugcVersion: 1,
creationRoomId: 398,
inventionDataFilename: '2026-08-29/cb608051-f38b-4ef2-aa8a-a26eb0195b2b.inv',
referencedInventions: [],
referencedUnityAssetIds: [],
creatorAccountRole: 255,
convertedFromInventionId: null,
displayMetadataJson: '{"0":0,"99":0}',
longDescription: '',
tagsRequest: { AutoTags: ['small'], CustomTags: null },
}
const res = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5151')), 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
expect(res.status).toBe(200)
// v9 is ENVELOPED where v6 is bare. The client checks Success and then reads
// Value.Invention.InventionId — unguarded, so a true Success with a null Value is
// the one shape that takes it down.
const result = (await res.json()) as InventionSaveV9Result
expect(result.Success).toBe(true)
expect(result.Error).toBeNull()
expect(result.error_id).toBeNull()
const value = result.Value
if (value === null) throw new Error('Value must not be null on a successful save')
expect(value.Status).toBe(0)
expect(Object.keys(value).sort()).toEqual([
'Invention',
'InventionVersion',
'Status',
'TagsResponse',
])
const saved = value.Invention
expect(saved.InventionId).toBeGreaterThan(0)
expect(saved.CreatorPlayerId).toBe(5151)
expect(saved.Name).toBe(body.name)
expect(saved.CreationRoomId).toBe(398)
expect(saved.DisplayMetadataJson).toBe('{"0":0,"99":0}')
// UgcVersion is an INVENTION field here, next to the version numbers — not a
// version one, where its twin HasBetaContent lives.
expect(saved.UgcVersion).toBe(1)
expect(saved.CurrentVersionNumber).toBe(1)
expect(saved.LatestVersionNumber).toBe(1)
// The v9 RRInvention has no nested version, no Referenced* and no IsPublished —
// the client reads publication from FirstPublishedAt.
expect(saved).not.toHaveProperty('CurrentVersion')
expect(saved).not.toHaveProperty('ReferencedInventions')
expect(saved).not.toHaveProperty('IsPublished')
expect(saved.FirstPublishedAt).toBeNull()
// Costs, the blob and the beta flag ride on the version beside it. No AICost: the
// request sends one and this DTO has nowhere to put it.
expect(value.InventionVersion).toMatchObject({
InventionId: saved.InventionId,
VersionNumber: 1,
InstantiationCost: 101,
HasBetaContent: false,
BlobName: body.inventionDataFilename,
UgcAccessibility: null,
ReferencedInventions: [],
ReferencedUnityAssetIds: [],
})
expect(value.InventionVersion).not.toHaveProperty('AICost')
// The tagsRequest is applied as `v1/settags` would have applied it, and answered
// the way settags answers: a result code and the bare tag NAMES.
expect(value.TagsResponse).toEqual({ Result: 0, Tags: ['small'] })
const details = await exports.default.fetch(
`${ORIGIN}/api/inventions/v1/details?inventionId=${saved.InventionId}`
)
expect(await details.json()).toEqual({ Tags: [{ Tag: 'small', Type: 2 }] })
// Stored once, read by every version: the older lookup still serves the record it
// always did, nested CurrentVersion and all.
const one = await exports.default.fetch(
`${ORIGIN}/api/inventions/v1?inventionId=${saved.InventionId}`
)
expect((await one.json()) as SavedInvention).toMatchObject({
InventionId: saved.InventionId,
IsPublished: false,
CurrentVersion: { BlobName: body.inventionDataFilename },
})
})
test('POST /api/inventions/v9/save leaves the v9-only keys off the stored record', async () => {
// The same body a v6 client sends, posted at v9: nothing is back-filled, so the
// record is the one v6 has always stored. The response still carries the full v9
// projection — those fields have defaults there, not absences.
const res = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5152')), 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Bare Save', inventionDataFilename: 'bare.inv' }),
})
expect(res.status).toBe(200)
const value = ((await res.json()) as InventionSaveV9Result).Value
if (value === null) throw new Error('Value must not be null on a successful save')
expect(value.Invention.UgcVersion).toBe(0)
expect(value.Invention.DisplayMetadataJson).toBeNull()
expect(value.InventionVersion.HasBetaContent).toBe(false)
expect(value.TagsResponse).toEqual({ Result: 0, Tags: [] })
const one = await exports.default.fetch(
`${ORIGIN}/api/inventions/v1?inventionId=${value.Invention.InventionId}`
)
const stored = (await one.json()) as SavedInvention
expect(stored).not.toHaveProperty('Tags')
expect(stored).not.toHaveProperty('UgcVersion')
expect(stored).not.toHaveProperty('ReferencedUnityAssetIds')
expect(stored.CurrentVersion).not.toHaveProperty('HasBetaContent')
})
test('POST /api/inventions/v9/save refuses through the envelope, never a bare error', async () => {
// A refusal the client can show is Success:false with a null Value — the branch
// that reads Error and nothing else. A bare `{ error }` body would deserialize to
// a null envelope and take the client down instead of failing the save.
const res = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5153')), 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'No Blob' }),
})
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
Value: null,
Success: false,
Error: 'inventionDataFilename is required',
error_id: null,
})
// Even the 401 answers the envelope: an empty body is a null envelope to the
// client, which is the crash, not a refusal.
const anon = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Anon', inventionDataFilename: 'anon.inv' }),
})
expect(anon.status).toBe(401)
expect((await anon.json()) as InventionSaveV9Result).toMatchObject({
Value: null,
Success: false,
})
})
test('POST /api/inventions/v9/save keeps the save when a tag breaks the tag rule', async () => {
// The reply carries a tag result of its own, so the two outcomes are separate: a
// hyphen in a tag must not cost the player the build they just saved.
const res = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5154')), 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Tagged Badly',
inventionDataFilename: 'tagged-badly.inv',
tagsRequest: { AutoTags: ['small'], CustomTags: ['bad-tag'] },
}),
})
expect(res.status).toBe(200)
const result = (await res.json()) as InventionSaveV9Result
expect(result.Success).toBe(true)
const value = result.Value
if (value === null) throw new Error('a refused tag must not refuse the save')
expect(value.Invention.InventionId).toBeGreaterThan(0)
// Non-zero result, and the whole list dropped rather than the offending tag alone —
// the creator re-submits it through `v1/settags` and sees what took.
expect(value.TagsResponse).toEqual({ Result: 1, Tags: [] })
const details = await exports.default.fetch(
`${ORIGIN}/api/inventions/v1/details?inventionId=${value.Invention.InventionId}`
)
expect(await details.json()).toEqual({ Tags: [] })
// The invention is on the creator's shelf regardless.
const mine = await exports.default.fetch(`${ORIGIN}/api/inventions/v2/mine`, {
headers: await bearer('5154'),
})
expect(((await mine.json()) as SavedInvention[]).map((i) => i.InventionId)).toEqual([
value.Invention.InventionId,
])
})
test('PUT /api/inventions/v2/metadata edits only the fields that arent null', async () => {
const save = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5160')), 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Before Edit',
description: 'the original description',
imageName: 'invention/before.jpg',
inventionDataFilename: 'before-edit.inv',
longDescription: 'the original blurb',
tagsRequest: { AutoTags: ['small'], CustomTags: null },
}),
})
const inventionId = ((await save.json()) as InventionSaveV9Result).Value?.Invention.InventionId
expect(inventionId).toBeGreaterThan(0)
// The client sends the whole shape every time and marks what it isn't touching as
// null — so a null Name must not blank the name.
const res = await exports.default.fetch(`${ORIGIN}/api/inventions/v2/metadata`, {
method: 'PUT',
headers: { ...(await bearer('5160')), 'Content-Type': 'application/json' },
body: JSON.stringify({
InventionId: inventionId,
Name: null,
Description: 'devin test No description yet',
LongDescription: null,
ImageName: null,
TagsRequest: null,
}),
})
expect(res.status).toBe(200)
const result = (await res.json()) as InventionSaveV9Result
expect(result.Success).toBe(true)
const value = result.Value
if (value === null) throw new Error('Value must not be null on a successful edit')
// The edit answers the UPDATED invention — the client re-renders the detail page
// from it — in the same envelope the save answers.
expect(value.Invention.Description).toBe('devin test No description yet')
expect(value.Invention.Name).toBe('Before Edit')
expect(value.Invention.ImageName).toBe('invention/before.jpg')
expect(value.Invention.InventionId).toBe(inventionId)
// A null TagsRequest leaves the stored tags alone, and they are still reported: the
// list is what the invention HAS, not what this call changed.
expect(value.TagsResponse).toEqual({ Result: 0, Tags: ['small'] })
// And it stuck — including the long description, which the v9 Invention DTO has no
// key for but the record keeps.
const one = await exports.default.fetch(
`${ORIGIN}/api/inventions/v1?inventionId=${inventionId}`
)
expect((await one.json()) as SavedInvention).toMatchObject({
Name: 'Before Edit',
Description: 'devin test No description yet',
LongDescription: 'the original blurb',
Tags: [{ Tag: 'small', Type: 2 }],
})
})
test('PUT /api/inventions/v2/metadata treats an empty string as a clear, not a null', async () => {
const save = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5161')), 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Clear Me',
description: 'to be cleared',
imageName: 'invention/clear-me.jpg',
inventionDataFilename: 'clear-me.inv',
longDescription: 'blurb to be cleared',
}),
})
const inventionId = ((await save.json()) as InventionSaveV9Result).Value?.Invention.InventionId
const res = await exports.default.fetch(`${ORIGIN}/api/inventions/v2/metadata`, {
method: 'PUT',
headers: { ...(await bearer('5161')), 'Content-Type': 'application/json' },
body: JSON.stringify({
InventionId: inventionId,
Name: null,
Description: '',
LongDescription: '',
ImageName: '',
TagsRequest: { AutoTags: ['large'], CustomTags: ['puzzle'] },
}),
})
const value = ((await res.json()) as InventionSaveV9Result).Value
if (value === null) throw new Error('Value must not be null on a successful edit')
expect(value.Invention.Description).toBe('')
expect(value.Invention.ImageName).toBe('')
// TagsRequest replaces both lists wholesale, auto first, then custom.
expect(value.TagsResponse).toEqual({ Result: 0, Tags: ['large', 'puzzle'] })
// An empty name is not how a name is cleared — nothing can draw a nameless
// invention, so it fails the same rule a save holds it to and nothing is written.
const named = await exports.default.fetch(`${ORIGIN}/api/inventions/v2/metadata`, {
method: 'PUT',
headers: { ...(await bearer('5161')), 'Content-Type': 'application/json' },
body: JSON.stringify({ InventionId: inventionId, Name: '' }),
})
expect(named.status).toBe(200)
expect((await named.json()) as InventionSaveV9Result).toMatchObject({
Value: null,
Success: false,
})
const one = await exports.default.fetch(
`${ORIGIN}/api/inventions/v1?inventionId=${inventionId}`
)
expect(((await one.json()) as SavedInvention).Name).toBe('Clear Me')
})
test('PUT /api/inventions/v2/metadata refuses another creators invention in-band', async () => {
const save = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5162')), 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Not Yours', inventionDataFilename: 'not-yours.inv' }),
})
const inventionId = ((await save.json()) as InventionSaveV9Result).Value?.Invention.InventionId
// Someone else's invention and an unknown one are domain answers, not transport
// ones — the client's own status enum has NotCreator and DoesNotExist members — so
// they come back 200 in the envelope, where the message reaches a human.
const theirs = await exports.default.fetch(`${ORIGIN}/api/inventions/v2/metadata`, {
method: 'PUT',
headers: { ...(await bearer('5163')), 'Content-Type': 'application/json' },
body: JSON.stringify({ InventionId: inventionId, Description: 'hijacked' }),
})
expect(theirs.status).toBe(200)
expect(await theirs.json()).toEqual({
Value: null,
Success: false,
Error: 'Not your invention',
error_id: null,
})
const missing = await exports.default.fetch(`${ORIGIN}/api/inventions/v2/metadata`, {
method: 'PUT',
headers: { ...(await bearer('5162')), 'Content-Type': 'application/json' },
body: JSON.stringify({ InventionId: 987654, Description: 'nobody' }),
})
expect(missing.status).toBe(200)
expect((await missing.json()) as InventionSaveV9Result).toMatchObject({
Value: null,
Error: 'No such invention',
})
// A missing token is the one refusal that stays a transport failure — but it still
// answers the envelope, because an unparseable body crashes the client.
const anon = await exports.default.fetch(`${ORIGIN}/api/inventions/v2/metadata`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ InventionId: inventionId, Description: 'anon' }),
})
expect(anon.status).toBe(401)
expect((await anon.json()) as InventionSaveV9Result).toMatchObject({
Value: null,
Success: false,
})
// Untouched throughout.
const one = await exports.default.fetch(
`${ORIGIN}/api/inventions/v1?inventionId=${inventionId}`
)
expect(((await one.json()) as SavedInvention).Description).toBe('No description yet')
})
test('PUT /api/inventions/v2/metadata keeps the edit when a tag breaks the tag rule', async () => {
const save = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5164')), 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Tag Trouble',
inventionDataFilename: 'tag-trouble.inv',
tagsRequest: { AutoTags: ['small'], CustomTags: null },
}),
})
const inventionId = ((await save.json()) as InventionSaveV9Result).Value?.Invention.InventionId
const res = await exports.default.fetch(`${ORIGIN}/api/inventions/v2/metadata`, {
method: 'PUT',
headers: { ...(await bearer('5164')), 'Content-Type': 'application/json' },
body: JSON.stringify({
InventionId: inventionId,
Description: 'edited anyway',
TagsRequest: { AutoTags: ['small'], CustomTags: ['bad-tag'] },
}),
})
const value = ((await res.json()) as InventionSaveV9Result).Value
if (value === null) throw new Error('a refused tag must not refuse the edit')
// The metadata edit lands; the tags are what didn't.
expect(value.Invention.Description).toBe('edited anyway')
expect(value.TagsResponse).toEqual({ Result: 1, Tags: [] })
const details = await exports.default.fetch(
`${ORIGIN}/api/inventions/v1/details?inventionId=${inventionId}`
)
expect(await details.json()).toEqual({ Tags: [] })
})
test('POST /api/inventions/v4/publish publishes with the permission and accessibility sent', async () => {
const save = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5170')), 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Publish Me', inventionDataFilename: 'publish-me.inv' }),
})
const saved = ((await save.json()) as InventionSaveV9Result).Value?.Invention
expect(saved?.FirstPublishedAt).toBeNull()
const res = await exports.default.fetch(`${ORIGIN}/api/inventions/v4/publish`, {
method: 'POST',
headers: { ...(await bearer('5170')), 'Content-Type': 'application/json' },
body: JSON.stringify({
InventionId: saved?.InventionId,
Permission: 20,
Accessibility: 1,
Price: null,
}),
})
expect(res.status).toBe(200)
const result = (await res.json()) as InventionSaveV9Result
expect(result.Success).toBe(true)
const value = result.Value
if (value === null) throw new Error('Value must not be null on a successful publish')
// Publishing narrows what everyone else gets down to what the sheet sent, and dates
// the invention — the client reads publication from FirstPublishedAt, not a flag.
expect(value.Invention.GeneralPermission).toBe(20)
expect(value.Invention.Accessibility).toBe(1)
expect(typeof value.Invention.FirstPublishedAt).toBe('string')
expect(value.Invention.Price).toBe(0)
// And it's findable now: the record the older reads serve says published, and it
// turns up in search.
const one = await exports.default.fetch(
`${ORIGIN}/api/inventions/v1?inventionId=${value.Invention.InventionId}`
)
expect((await one.json()) as SavedInvention).toMatchObject({ IsPublished: true })
const found = await exports.default.fetch(`${ORIGIN}/api/inventions/v2/search?value=Publish Me`)
expect(((await found.json()) as SavedInvention[]).map((i) => i.InventionId)).toContain(
value.Invention.InventionId
)
// Taken back out: the browse tests below assert the exact published catalogue, and
// a test that publishes something publicly is a test that changes it.
await env.DB.prepare('DELETE FROM invention WHERE id = ?1')
.bind(value.Invention.InventionId)
.run()
})
test('POST /api/inventions/v4/publish keeps an unlisted invention out of the feeds', async () => {
const save = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5171')), 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Quietly Published',
inventionDataFilename: 'quietly-published.inv',
creationRoomId: 4171,
}),
})
const inventionId = ((await save.json()) as InventionSaveV9Result).Value?.Invention.InventionId
const res = await exports.default.fetch(`${ORIGIN}/api/inventions/v4/publish`, {
method: 'POST',
headers: { ...(await bearer('5171')), 'Content-Type': 'application/json' },
body: JSON.stringify({ InventionId: inventionId, Permission: 20, Accessibility: 2 }),
})
const value = ((await res.json()) as InventionSaveV9Result).Value
expect(value?.Invention.Accessibility).toBe(2)
// Unlisted is published — it is reachable by id, which is the whole point of it —
// but it is not something anyone comes across.
const one = await exports.default.fetch(
`${ORIGIN}/api/inventions/v1?inventionId=${inventionId}`
)
expect((await one.json()) as SavedInvention).toMatchObject({
InventionId: inventionId,
IsPublished: true,
})
const found = await exports.default.fetch(
`${ORIGIN}/api/inventions/v2/search?value=Quietly Published`
)
expect(await found.json()).toEqual([])
const room = await exports.default.fetch(`${ORIGIN}/api/inventions/v1/room?id=4171`)
expect(await room.json()).toEqual([])
})
test('POST /api/inventions/v4/publish leaves a price and a first-publish date alone', async () => {
const save = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5172')), 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'For Sale', inventionDataFilename: 'for-sale.inv' }),
})
const inventionId = ((await save.json()) as InventionSaveV9Result).Value?.Invention.InventionId
const publish = async (body: Record<string, unknown>) => {
const res = await exports.default.fetch(`${ORIGIN}/api/inventions/v4/publish`, {
method: 'POST',
headers: { ...(await bearer('5172')), 'Content-Type': 'application/json' },
body: JSON.stringify({ InventionId: inventionId, ...body }),
})
const value = ((await res.json()) as InventionSaveV9Result).Value
if (value === null) throw new Error('Value must not be null on a successful publish')
return value.Invention
}
const first = await publish({ Permission: 80, Accessibility: 1, Price: 250 })
expect(first.Price).toBe(250)
// A republish that says nothing about money must not give away something that was
// for sale, and must not re-date the first publish.
const again = await publish({ Permission: 20, Accessibility: 1, Price: null })
expect(again.Price).toBe(250)
expect(again.GeneralPermission).toBe(20)
expect(again.FirstPublishedAt).toBe(first.FirstPublishedAt)
// A negative price is dropped rather than stored.
expect((await publish({ Price: -5 })).Price).toBe(250)
// Out of the published catalogue again — see the note in the publish test above.
await env.DB.prepare('DELETE FROM invention WHERE id = ?1').bind(inventionId).run()
})
test('POST /api/inventions/v4/publish refuses another creators invention in-band', async () => {
const save = await exports.default.fetch(`${ORIGIN}/api/inventions/v9/save`, {
method: 'POST',
headers: { ...(await bearer('5173')), 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Theirs Alone', inventionDataFilename: 'theirs-alone.inv' }),
})
const inventionId = ((await save.json()) as InventionSaveV9Result).Value?.Invention.InventionId
const theirs = await exports.default.fetch(`${ORIGIN}/api/inventions/v4/publish`, {
method: 'POST',
headers: { ...(await bearer('5174')), 'Content-Type': 'application/json' },
body: JSON.stringify({ InventionId: inventionId, Permission: 20, Accessibility: 1 }),
})
expect(theirs.status).toBe(200)
expect(await theirs.json()).toEqual({
Value: null,
Success: false,
Error: 'Not your invention',
error_id: null,
})
const anon = await exports.default.fetch(`${ORIGIN}/api/inventions/v4/publish`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ InventionId: inventionId, Permission: 20, Accessibility: 1 }),
})
expect(anon.status).toBe(401)
expect((await anon.json()) as InventionSaveV9Result).toMatchObject({ Value: null })
// Still unpublished throughout.
const one = await exports.default.fetch(
`${ORIGIN}/api/inventions/v1?inventionId=${inventionId}`
)
expect((await one.json()) as SavedInvention).toMatchObject({
IsPublished: false,
FirstPublishedAt: null,
})
})
test('GET /api/inventions/v2/mine lists bought inventions alongside the callers own', async () => {
// Account 6100 creates one; 6101 buys it (the econ worker's buyInvention writes
// exactly this row) and also creates one of their own.
@@ -6317,7 +6872,9 @@ describe('openapi', () => {
'POST /api/inventions/v1/settags',
'POST /api/inventions/v1/update',
'POST /api/inventions/v1/updateprice',
'POST /api/inventions/v4/publish',
'POST /api/inventions/v6/save',
'POST /api/inventions/v9/save',
'POST /api/messages/v1/friendOnlineStatus',
'POST /api/messages/v1/sendMultiple',
'POST /api/messages/v2/send',
@@ -6350,6 +6907,7 @@ describe('openapi', () => {
'POST /outfits/bulk',
'POST /statsigUserProperties',
'PUT /api/customAvatarItems/v1/{id}',
'PUT /api/inventions/v2/metadata',
'PUT /api/playerevents/v2/{eventId}/accessibility',
'PUT /api/playerevents/v2/{eventId}/description',
'PUT /api/playerevents/v2/{eventId}/name',