// ── module-uo's OpenAPI fragment: the shared half ────────────────────────── // // The tags and component schemas every `#swagger.*` annotation under // `server/router/**` refers to. `scripts/swaggerFragment.js` feeds this to // swagger-autogen; the per-endpoint detail lives beside each route, exactly as // it does in core. // // These 31 schemas were core's until phase 3 — they sat in // `website/server/swagger/swagger.js` describing routes core no longer serves, // which is what an extraction leaves behind if nobody looks (the inert-leaf // class slice 4 found in `api/client.js`). They moved with the routes. // // **Two rules about names, and both are the merged document's, not this file's** // (docs/website/MODULE_API.md §6.1a): // // • **What this module DEFINES is namespaced `Uo…`.** Core merges started // modules' fragments into one `/api/docs.json`, and core wins every key // collision — so an un-namespaced `ShardStatus` from a second game's module // would silently lose to, or clobber, this one. The prefix is what makes two // modules able to describe the same idea. // • **What core defines is referenced by CORE's name.** The annotations point // at `#/components/schemas/Error` and `ValidationError` and this file does // not redefine them: they resolve in the merged spec, where core's // definitions are. Shipping our own copy would be a collision core drops, // which is the correct outcome arrived at the expensive way. // // Tag NAMES are core's originals (`Public · Shard`, not `Uo · Shard`). A tag is // how the docs UI groups operations, and core stopped declaring these four in // the same slice this file started — nothing collides, and renaming them would // churn every reader's bookmark for no gain. module.exports = { tags: [ { name: 'Public · Shard', description: 'Live shard data ingested from the uo-link sidecar (status, feed, economy, IDOC, characters)' }, { name: 'Public · Atlas', description: 'Spawn atlas / bestiary — static shard content parsed from the shard\'s own ServUO tree, independent of the sidecar' }, { name: 'Player · Shard', description: 'Link an in-game account and read its roster / vendors (uo-link)' }, { name: 'Admin · Shard', description: 'uo-link sidecar connection config, live status and town crier (admin only)' }, ], components: { schemas: { // ── uo-link shard data ────────────────────────────────────────────── UoShardStatus: { type: 'object', description: 'Public shard status (GET /public/shard/status).', properties: { enabled: { type: 'boolean', example: true }, status: { type: 'string', example: 'connected', description: 'connected | reconnecting | disconnected | error' }, pluginConnected: { type: 'boolean', description: 'Is the shard link up right now?', example: true }, lastEventAt: { type: 'string', format: 'date-time', nullable: true }, onlineCount: { type: 'integer', example: 12 }, economy: { $ref: '#/components/schemas/UoShardEconomyPoint' }, }, }, UoShardEvent: { type: 'object', description: 'A logged shard event.', properties: { id: { type: 'integer', example: 4821 }, kind: { type: 'string', example: 'vendor.sale' }, t: { type: 'integer', description: 'Event time, epoch ms.', example: 1783720195626 }, bootId: { type: 'string', nullable: true, example: 'boot-abc123' }, payload: { type: 'object', additionalProperties: true, description: 'The full event object.' }, createdAt: { type: 'string', format: 'date-time' }, }, }, UoShardEconomyPoint: { type: 'object', nullable: true, description: 'One gold-supply sample.', properties: { accounts: { type: 'integer', nullable: true, example: 240 }, gold: { type: 'integer', nullable: true, example: 1028983421 }, t: { type: 'integer', description: 'Sample time, epoch ms.', example: 1783720000000 }, }, }, UoShardOnlinePlayer: { type: 'object', description: 'A LINKED player online now (only accounts linked to a website user are listed).', properties: { serial: { type: 'string', example: '0x24C' }, name: { type: 'string', example: 'Darrow' }, map: { type: 'string', nullable: true, example: 'Trammel' }, x: { type: 'integer', nullable: true, example: 1402 }, y: { type: 'integer', nullable: true, example: 1604 }, z: { type: 'integer', nullable: true, example: 0 }, }, }, UoShardVendorSale: { type: 'object', description: 'A player-vendor sale (visible only to the linked owner).', properties: { t: { type: 'integer', description: 'Sale time, epoch ms.', example: 1783720195626 }, itemType: { type: 'string', example: 'Longsword' }, amount: { type: 'integer', example: 1 }, price: { type: 'integer', example: 100 }, commission: { type: 'integer', nullable: true, example: 5 }, ownerAcct: { type: 'string', example: 'whitlocktech' }, }, }, UoShardHouse: { type: 'object', description: 'A house at its current decay stage.', properties: { serial: { type: 'string', example: '0x4004705F' }, stage: { type: 'string', example: 'IDOC' }, map: { type: 'string', nullable: true, example: 'Trammel' }, x: { type: 'integer', nullable: true }, y: { type: 'integer', nullable: true }, z: { type: 'integer', nullable: true }, region: { type: 'string', nullable: true }, name: { type: 'string', nullable: true, example: 'An Unnamed House' }, ownerSerial: { type: 'string', nullable: true }, ownerAcct: { type: 'string', nullable: true }, builtOn: { type: 'string', format: 'date-time', nullable: true }, lastRefreshed: { type: 'string', format: 'date-time', nullable: true }, isIdoc: { type: 'boolean', example: true }, updatedAt: { type: 'string', format: 'date-time' }, }, }, UoShardPointsBoard: { type: 'object', description: "One point system's leaderboard (Protocol 3.0 points.board). The shard carries ~25 separate point currencies; each publishes its own board. The display name may arrive as a literal string, a cliloc id, or both — resolve clilocs client-side.", properties: { system: { type: 'string', example: 'QueensLoyalty', description: "The shard's PointsType name; the board's stable key." }, nameString: { type: 'string', nullable: true, example: "Queen's Loyalty" }, nameNumber: { type: 'integer', nullable: true, example: 1114938, description: 'Cliloc id, 0 when the name is a literal.' }, maxPoints: { type: 'integer', nullable: true, example: 30000 }, players: { type: 'integer', nullable: true, example: 842, description: 'Players actually holding points in this system.' }, showOnGump: { type: 'boolean', example: true, description: "The shard's own 'is this player-facing?' flag." }, top: { type: 'array', description: 'The ranked players, best first. Capped by the shard (10 by default). Empty when nobody has scored yet.', items: { type: 'object', properties: { rank: { type: 'integer', example: 1 }, serial: { type: 'string', example: '0x1A2B' }, name: { type: 'string', example: 'Darrow', description: 'Omitted when the leaderboards `name` field is gated above the caller.' }, points: { type: 'integer', example: 29500 }, }, }, }, t: { type: 'integer', nullable: true, description: 'Frame time, epoch ms.' }, updatedAt: { type: 'string', format: 'date-time' }, }, }, UoShardMarketLocation: { type: 'object', nullable: true, description: "Where a vendor is standing. ONE nested object rather than flat map/x/y/region because it is one admin-configurable field (`market.location`) — the whole object is omitted when that field is gated above the caller.", properties: { map: { type: 'string', nullable: true, example: 'Trammel' }, x: { type: 'integer', nullable: true, example: 1421 }, y: { type: 'integer', nullable: true, example: 1699 }, z: { type: 'integer', nullable: true, example: 0 }, region: { type: 'string', nullable: true, example: 'Britain' }, house: { type: 'string', nullable: true, example: "Darrow's Villa", description: "The house SIGN's name, not the house type. Null for a vendor standing outside one." }, }, }, UoShardMarketListing: { type: 'object', description: 'One priced listing on a player vendor, carrying enough of its shop to be actionable without a second request.', properties: { serial: { type: 'string', example: '0x40012ABC' }, itemId: { type: 'integer', example: 3922, description: 'ItemID (the art/graphic id).' }, hue: { type: 'integer', example: 0 }, amount: { type: 'integer', example: 1 }, price: { type: 'integer', example: 25000 }, name: { type: 'string', nullable: true, description: "The item's own literal name, set by a player. Null for most items." }, cliloc: { type: 'integer', nullable: true, example: 1023721, description: "The item's LabelNumber." }, displayName: { type: 'string', nullable: true, example: 'quarter staff', description: 'Resolved server-side from `name` (preferred, being player-set and more specific) else `cliloc`. Null on a shard with no cliloc table configured — render the item id.', }, child: { type: 'boolean', example: false, description: 'Priced by an enclosing container rather than itself, exactly as the in-game Vendor Search reports it.' }, vendor: { type: 'object', properties: { serial: { type: 'string', example: '0x40001234' }, shopName: { type: 'string', nullable: true, example: "Darrow's Bargains" }, ownerSerial: { type: 'string', nullable: true, example: '0x1A2B', description: 'Omitted when the market `ownerSerial` field is gated above the caller.' }, ownerName: { type: 'string', nullable: true, example: 'Darrow', description: 'Omitted when the market `ownerName` field is gated above the caller.' }, location: { $ref: '#/components/schemas/UoShardMarketLocation' }, updatedAt: { type: 'string', format: 'date-time', description: 'When the shard last published this shop.' }, }, }, }, }, UoShardMarketPage: { type: 'object', description: 'A page of marketplace listings plus the unpaginated total and the staleness stamp.', properties: { listings: { type: 'array', items: { $ref: '#/components/schemas/UoShardMarketListing' } }, total: { type: 'integer', example: 1284, description: 'Matching listings, ignoring paging.' }, limit: { type: 'integer', example: 50 }, offset: { type: 'integer', example: 0 }, vendors: { type: 'integer', example: 137, description: 'Vendors in the whole index.' }, staleAt: { type: 'string', format: 'date-time', nullable: true, description: 'The OLDEST vendor row. The shard sweeps vendors round-robin, so the index can be a full cycle behind and a client must say so rather than implying live prices.', }, }, }, UoShardMarketVendor: { type: 'object', description: 'One player vendor and its listings.', properties: { serial: { type: 'string', example: '0x40001234' }, shopName: { type: 'string', nullable: true, example: "Darrow's Bargains" }, ownerSerial: { type: 'string', nullable: true }, ownerName: { type: 'string', nullable: true, example: 'Darrow' }, location: { $ref: '#/components/schemas/UoShardMarketLocation' }, count: { type: 'integer', example: 250, description: 'Listings the shard published for this shop.' }, total: { type: 'integer', example: 3104, description: 'Listings the shop actually holds.' }, truncated: { type: 'boolean', example: true, description: '`total` exceeds `count` — the shop holds more than the shard publishes per frame.' }, updatedAt: { type: 'string', format: 'date-time' }, items: { type: 'array', items: { $ref: '#/components/schemas/UoShardMarketListing' } }, }, }, UoShardMarketMeta: { type: 'object', description: 'Marketplace size, staleness and the filter options a client needs to build its UI.', properties: { vendors: { type: 'integer', example: 137 }, items: { type: 'integer', example: 18422 }, staleAt: { type: 'string', format: 'date-time', nullable: true }, freshAt: { type: 'string', format: 'date-time', nullable: true }, maps: { type: 'array', items: { type: 'string' }, example: ['Felucca', 'Trammel'], description: "Facets that actually hold vendors. From the shard's own data — never a hardcoded list." }, regions: { type: 'array', items: { type: 'string' }, example: ['Britain', 'Luna'] }, }, }, UoShardFeatures: { type: 'object', description: "The shard features the caller may reach, plus the audience rung they resolved to. Drives client nav so it never renders a link that would 403.", properties: { level: { type: 'string', enum: ['anonymous', 'logged_in', 'player', 'staff', 'admin'], example: 'anonymous', }, features: { type: 'array', items: { type: 'string' }, example: ['status', 'activity', 'champs', 'guilds', 'governors', 'houses', 'presence'], }, }, }, UoShardFeatureVisibility: { type: 'object', description: 'Visibility settings for one shard feature.', properties: { enabled: { type: 'boolean', example: true }, audience: { type: 'string', enum: ['anonymous', 'logged_in', 'player', 'staff', 'admin'], description: 'Minimum rung that may reach this feature. Each rung implies the ones below it.', example: 'anonymous', }, stream: { type: 'boolean', description: "Whether this feature's event kinds fan out over SSE at all.", example: true, }, fieldRules: { type: 'object', additionalProperties: { type: 'string' }, description: 'Per-field rung overrides for the sensitive fields this feature exposes. acct / webId are admin-only always and are rejected here.', example: { location: 'staff' }, }, }, }, UoShardVisibilityConfig: { type: 'object', properties: { ladder: { type: 'array', items: { type: 'string' }, example: ['anonymous', 'logged_in', 'player', 'staff', 'admin'], }, lockedFields: { type: 'array', items: { type: 'string' }, example: ['acct', 'webId'] }, defaults: { type: 'object', additionalProperties: { $ref: '#/components/schemas/UoShardFeatureVisibility' }, }, features: { type: 'object', additionalProperties: { $ref: '#/components/schemas/UoShardFeatureVisibility' }, }, }, }, UoShardVisibilityUpdate: { type: 'object', required: ['features'], properties: { features: { type: 'object', additionalProperties: { $ref: '#/components/schemas/UoShardFeatureVisibility' }, example: { market: { enabled: true, audience: 'player', stream: false, fieldRules: { ownerName: 'player' } } }, }, }, }, // ── Spawn atlas (Protocol 3.0 Part C) ──────────────────────────────── // Static shard content, parsed from the shard's own ServUO tree. Nothing // here comes from the sidecar, so it stays populated while the shard is // down. Facet names are whatever the shard's files declare — the examples // below are stock ServUO, not a fixed list. UoAtlasCreature: { type: 'object', description: 'A creature in the bestiary. `places`/`points`/`alsoHere` are present only on the single-creature route.', properties: { slug: { type: 'string', example: 'lizardman' }, name: { type: 'string', example: 'Lizardman' }, total: { type: 'integer', description: 'How many can be alive at once, summed across every spawner.', example: 214 }, points: { type: 'integer', description: 'How many spawners mention this creature.', example: 62 }, facets: { type: 'object', additionalProperties: { type: 'integer' }, description: "This creature's share per facet.", example: { Felucca: 96, Trammel: 88, Tokuno: 30 }, }, art: { type: 'string', nullable: true, description: 'Operator-supplied art under uploads/atlas/. NULL on a fresh import — the repo ships no creature art.' }, places: { type: 'array', description: 'Where it spawns, aggregated by resolved place. The answer the atlas exists to give.', items: { type: 'object', properties: { facet: { type: 'string', example: 'Trammel' }, label: { type: 'string', description: 'Resolved region, else nearest landmark group, else "Wilderness".', example: 'Shrines' }, spawners: { type: 'integer', example: 7 }, maxAlive: { type: 'integer', example: 21 }, }, }, }, spawners: { type: 'array', description: 'The individual spawners. Named separately from `points` (the count) so one key never means two things.', items: { $ref: '#/components/schemas/UoAtlasSpawner' }, }, spawnersTruncated: { type: 'boolean', description: 'True when the spawner list was cut at the requested bound.', example: false }, alsoHere: { type: 'array', description: 'Creatures sharing a spawner with this one.', items: { type: 'object', properties: { slug: { type: 'string', example: 'lizardman-warrior' }, name: { type: 'string', example: 'Lizardman Warrior' }, shared: { type: 'integer', example: 12 }, }, }, }, }, }, UoAtlasSpawner: { type: 'object', description: 'One ServUO spawner, with the place its coordinates resolved to.', properties: { id: { type: 'integer' }, facet: { type: 'string', example: 'Felucca' }, name: { type: 'string', nullable: true, description: "The spawner's own name in the ServUO file." }, x: { type: 'integer', example: 5411 }, y: { type: 'integer', example: 1234 }, width: { type: 'integer' }, height: { type: 'integer' }, range: { type: 'integer', description: 'Spawn radius.' }, maxCount: { type: 'integer', description: 'How many of THIS creature this spawner keeps alive.', example: 3 }, minDelay: { type: 'integer', description: 'Respawn window, in SECONDS. Normalised at parse time — the source stores minutes or seconds per record, decided by its own DelayInSec flag.', example: 300 }, maxDelay: { type: 'integer', example: 600 }, todStart: { type: 'integer', description: 'Meaningless unless todMode is non-zero.' }, todEnd: { type: 'integer' }, todMode: { type: 'integer' }, region: { type: 'string', nullable: true, example: 'Despise' }, landmark: { type: 'string', nullable: true, example: 'Covetous' }, label: { type: 'string', description: 'Region, else landmark group, else "Wilderness".', example: 'Despise' }, }, }, UoAtlasCreaturePage: { type: 'object', properties: { total: { type: 'integer', description: 'Matching creatures before pagination.', example: 800 }, limit: { type: 'integer', example: 50 }, offset: { type: 'integer', example: 0 }, creatures: { type: 'array', items: { $ref: '#/components/schemas/UoAtlasCreature' } }, }, }, UoAtlasRegion: { type: 'object', description: 'A named region, flattened out of the shard\'s nested Regions.xml.', properties: { facet: { type: 'string', example: 'Felucca' }, name: { type: 'string', example: 'Despise' }, type: { type: 'string', nullable: true, description: 'ServUO region class.', example: 'DungeonRegion' }, priority: { type: 'integer', example: 50 }, parent: { type: 'string', nullable: true, example: 'Britain' }, rects: { type: 'array', description: 'The rectangles that placed each spawn point.', items: { type: 'object', additionalProperties: true }, }, }, }, UoAtlasLandmark: { type: 'object', properties: { facet: { type: 'string', example: 'Trammel' }, name: { type: 'string', example: 'Level 1' }, group: { type: 'string', nullable: true, description: 'Innermost enclosing parent — the label worth showing.', example: 'Covetous' }, x: { type: 'integer', example: 5411 }, y: { type: 'integer', example: 1234 }, z: { type: 'integer', example: 0 }, }, }, UoAtlasChampion: { type: 'object', description: 'A CONFIGURED champion altar. Not the live board — see GET /public/shard/champs for that.', properties: { slug: { type: 'string', example: 'felucca-deceit' }, name: { type: 'string', example: 'Deceit' }, group: { type: 'string', nullable: true, description: 'Spawn group; one altar active per group.', example: 'Dungeons' }, type: { type: 'string', nullable: true, description: 'NULL when the champion is drawn at activation.', example: 'UnholyTerror' }, randomType: { type: 'boolean', example: false }, facet: { type: 'string', example: 'Felucca' }, x: { type: 'integer' }, y: { type: 'integer' }, z: { type: 'integer' }, radius: { type: 'integer', example: 60 }, label: { type: 'string', nullable: true, example: 'Deceit' }, }, }, UoAtlasMeta: { type: 'object', description: 'What atlas is loaded. Game-world facts only: the ServUO path, source hashes and any pending refresh are operator detail and live on the admin status route.', properties: { importedAt: { type: 'string', format: 'date-time', nullable: true }, generatedAt: { type: 'string', format: 'date-time', nullable: true }, counts: { type: 'object', nullable: true, additionalProperties: true, example: { facets: 6, points: 6455, creatures: 800, regions: 387, landmarks: 558, champions: 25, unresolvedPoints: 1086 }, }, facets: { type: 'array', items: { type: 'string' }, example: ['Felucca', 'Ilshenar', 'Malas', 'TerMur', 'Tokuno', 'Trammel'] }, }, }, UoAtlasStatus: { type: 'object', description: 'Admin view of atlas state: where the tree is, whether it is readable, whether it has drifted from what is loaded, and any refresh staged for review.', properties: { configured: { type: 'boolean', example: true }, path: { type: 'string', example: '/srv/servuo' }, treeReadable: { type: 'boolean', example: true }, drift: { type: 'boolean', nullable: true, description: 'True when the tree\'s source hashes differ from the loaded atlas. NULL when the tree could not be read.', example: false }, facets: { type: 'array', items: { type: 'string' } }, importedAt: { type: 'string', format: 'date-time', nullable: true }, counts: { type: 'object', nullable: true, additionalProperties: true }, pending: { type: 'object', nullable: true, description: 'A refresh that was parsed but NOT applied because it would remove a facet. `status` is pending or rejected.', additionalProperties: true, }, }, }, UoAtlasRefreshResult: { type: 'object', description: 'Outcome of a refresh. Reported rather than thrown, so an unreadable tree is an answer and not a 500.', properties: { status: { type: 'string', enum: ['skipped', 'unavailable', 'unchanged', 'imported', 'needsReview', 'failed', 'rejected', 'none'], example: 'imported', }, reason: { type: 'string', nullable: true }, path: { type: 'string', nullable: true }, counts: { type: 'object', nullable: true, additionalProperties: true }, addedFacets: { type: 'array', items: { type: 'string' } }, removedFacets: { type: 'array', items: { type: 'string' } }, }, }, UoClilocStatus: { type: 'object', description: 'Admin view of cliloc state: where the converted file is, whether it is readable, how many entries are loaded, and whether the file has drifted from them. `configured: false` is a supported state — item names then render as ids.', properties: { configured: { type: 'boolean', example: true }, path: { type: 'string', example: '/srv/uo-client' }, file: { type: 'string', nullable: true, description: 'The file actually resolved, when the path is a directory.', example: '/srv/uo-client/clilocs.tsv' }, fileReadable: { type: 'boolean', example: true }, problem: { type: 'string', nullable: true, description: 'Why the file cannot be used, when it cannot. Set (with code COMPRESSED) for a readable-but-unconverted client file.', example: null }, code: { type: 'string', nullable: true, description: 'Machine-readable cause of `problem`.', enum: ['NO_PATH', 'NOT_FOUND', 'NO_FILE', 'UNREADABLE', 'COMPRESSED'] }, drift: { type: 'boolean', nullable: true, description: 'True when any source hash differs from the loaded table. NULL when the sources could not be read or are not usable.', example: false }, count: { type: 'integer', description: 'Entries currently loaded.', example: 67496 }, sources: { type: 'array', items: { type: 'string' }, description: 'Every source found now, root-relative, base first then overlays in merge order.', example: ['clilocs.plain', 'custom/uomysticmoon.tsv'], }, loadedSources: { type: 'array', nullable: true, description: 'What each source contributed at the last import.', items: { type: 'object', properties: { label: { type: 'string', example: 'custom/uomysticmoon.tsv' }, kind: { type: 'string', enum: ['base', 'custom'], example: 'custom' }, entries: { type: 'integer', example: 37 }, added: { type: 'integer', description: 'Ids this source introduced.', example: 25 }, overrode: { type: 'integer', description: 'Ids it replaced from an earlier source.', example: 12 }, }, }, }, missingSources: { type: 'array', items: { type: 'string' }, description: 'Sources loaded previously and now absent. An import refuses these without `approve`.', example: [], }, importedAt: { type: 'string', format: 'date-time', nullable: true }, sourceBytes: { type: 'integer', nullable: true, example: 4973525 }, }, }, UoClilocRefreshResult: { type: 'object', description: 'Outcome of a cliloc refresh. Reported rather than thrown, so a missing or compressed file is an answer and not a 500.', properties: { status: { type: 'string', enum: ['skipped', 'unavailable', 'unchanged', 'imported', 'needsReview', 'failed'], description: '`needsReview` means a previously-loaded source has vanished and nothing was applied; re-run with `approve` to accept it.', example: 'imported', }, reason: { type: 'string', nullable: true }, code: { type: 'string', nullable: true, description: 'Machine-readable cause. `COMPRESSED` means the client\'s own Cliloc.enu was supplied instead of a converted one.', enum: ['NO_PATH', 'NOT_FOUND', 'NO_FILE', 'UNREADABLE', 'COMPRESSED', 'TRUNCATED', 'EMPTY', 'NOT_BUFFER'], }, path: { type: 'string', nullable: true }, file: { type: 'string', nullable: true }, count: { type: 'integer', nullable: true, description: 'Entries stored (blank strings are dropped).', example: 67496 }, parsed: { type: 'integer', nullable: true, description: 'Entries read across every source before blanks were dropped.', example: 123527 }, blank: { type: 'integer', nullable: true, example: 55994 }, sources: { type: 'array', nullable: true, description: 'Per-source breakdown: what each file contributed and how much of it overrode an earlier source.', items: { type: 'object', properties: { label: { type: 'string' }, kind: { type: 'string', enum: ['base', 'custom'] }, entries: { type: 'integer' }, added: { type: 'integer' }, overrode: { type: 'integer' }, }, }, }, missingSources: { type: 'array', nullable: true, items: { type: 'string' }, description: 'On `needsReview`: the sources that vanished. Nothing was applied.', }, acceptedMissing: { type: 'array', nullable: true, items: { type: 'string' }, description: 'On `imported` with `approve`: the vanished sources the admin accepted.', }, }, }, UoShardLinkRequest: { type: 'object', required: ['code'], properties: { code: { type: 'string', description: 'The one-time code shown by [link in game.', example: 'AB12CD' }, }, }, UoShardLinkResult: { type: 'object', properties: { linked: { type: 'boolean', example: true }, account: { type: 'string', example: 'whitlocktech' }, }, }, UoShardLink: { type: 'object', description: 'A linked in-game account (GET /player/shard/accounts).', properties: { account: { type: 'string', example: 'whitlocktech' }, userId: { type: 'integer', example: 42 }, charName: { type: 'string', nullable: true, example: 'Darrow' }, linkedAt: { type: 'string', format: 'date-time' }, }, }, UoTownCrierRequest: { type: 'object', required: ['id', 'lines'], properties: { id: { type: 'string', maxLength: 64, description: 'Re-posting the same id replaces the prior entry.', example: 'news-42' }, lines: { type: 'array', items: { type: 'string', maxLength: 200 }, example: ['Hear ye!', 'Market tax is now 5%.'] }, durationSec: { type: 'integer', minimum: 1, maximum: 86400, example: 3600 }, }, }, }, }, }