export interface OpenApiBuildOptions { baseUrl: string; appVersion: string; } export function buildOpenApiDocument(options: OpenApiBuildOptions): unknown { const { baseUrl, appVersion } = options; const roomIdPathParameter = { name: 'roomId', in: 'path', required: true, schema: { type: 'string' } }; const userIdPathParameter = { name: 'userId', in: 'path', required: true, schema: { type: 'string' } }; const messageIdPathParameter = { name: 'messageId', in: 'path', required: true, schema: { type: 'string' } }; const sinceTimestampQueryParameter = { name: 'sinceTimestamp', in: 'query', required: true, schema: { type: 'integer', minimum: 0, format: 'int64' } }; return { openapi: '3.1.0', info: { title: 'MetoYou Local Desktop API', version: appVersion, description: 'Authenticated local HTTP API exposed by the MetoYou desktop app. ' + 'Authentication is performed against a configured signaling server. ' + 'Bearer tokens issued here are scoped to this device only.' }, servers: [{ url: baseUrl }], components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'opaque' } }, schemas: { Error: { type: 'object', required: ['error'], properties: { error: { type: 'string' }, errorCode: { type: 'string' } } }, LoginRequest: { type: 'object', required: [ 'username', 'password', 'serverUrl' ], properties: { username: { type: 'string' }, password: { type: 'string' }, serverUrl: { type: 'string', format: 'uri', description: 'Base URL of the signaling server to authenticate against. Must be in the allowed list configured in the desktop app.' } } }, LoginResponse: { type: 'object', required: [ 'token', 'expiresAt', 'user' ], properties: { token: { type: 'string' }, expiresAt: { type: 'integer', format: 'int64' }, user: { $ref: '#/components/schemas/AuthUser' } } }, AuthUser: { type: 'object', required: [ 'id', 'username', 'displayName' ], properties: { id: { type: 'string' }, username: { type: 'string' }, displayName: { type: 'string' } } }, Profile: { type: 'object', properties: { id: { type: 'string' }, username: { type: 'string' }, displayName: { type: 'string' }, description: { type: 'string' }, avatarUrl: { type: 'string' }, status: { type: 'string' } } }, Room: { type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' } }, additionalProperties: true }, User: { type: 'object', properties: { id: { type: 'string' }, oderId: { type: 'string' }, username: { type: 'string' }, displayName: { type: 'string' }, status: { type: 'string' }, role: { type: 'string' }, isOnline: { type: 'boolean' } }, additionalProperties: true }, Message: { type: 'object', properties: { id: { type: 'string' }, roomId: { type: 'string' }, channelId: { type: 'string' }, senderId: { type: 'string' }, senderName: { type: 'string' }, content: { type: 'string' }, timestamp: { type: 'integer', format: 'int64' }, editedAt: { type: 'integer', format: 'int64' }, isDeleted: { type: 'boolean' } }, additionalProperties: true }, Reaction: { type: 'object', properties: { id: { type: 'string' }, messageId: { type: 'string' }, userId: { type: 'string' }, oderId: { type: 'string' }, emoji: { type: 'string' }, timestamp: { type: 'integer', format: 'int64' } }, additionalProperties: true }, Attachment: { type: 'object', properties: { id: { type: 'string' }, messageId: { type: 'string' }, filename: { type: 'string' }, size: { type: 'integer' }, mime: { type: 'string' }, isImage: { type: 'boolean' }, filePath: { type: 'string' }, savedPath: { type: 'string' } }, additionalProperties: true }, Ban: { type: 'object', properties: { oderId: { type: 'string' }, roomId: { type: 'string' }, userId: { type: 'string' }, bannedBy: { type: 'string' }, displayName: { type: 'string' }, reason: { type: 'string' }, expiresAt: { type: 'integer', format: 'int64' }, timestamp: { type: 'integer', format: 'int64' } }, additionalProperties: true }, PluginDataValue: { type: 'object', properties: { value: {} } }, MetaValue: { type: 'object', properties: { key: { type: 'string' }, value: { type: ['string', 'null'] } } } } }, security: [{ bearerAuth: [] }], paths: { '/api/health': { get: { security: [], summary: 'Liveness probe', responses: { '200': { description: 'Service is alive', content: { 'application/json': { schema: { type: 'object', properties: { status: { type: 'string' }, version: { type: 'string' } } } } } } } } }, '/api/openapi.json': { get: { security: [], summary: 'OpenAPI specification', responses: { '200': { description: 'This document' } } } }, '/api/auth/login': { post: { security: [], summary: 'Exchange username/password (validated by a signaling server) for a bearer token', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/LoginRequest' } } } }, responses: { '200': { description: 'Token issued', content: { 'application/json': { schema: { $ref: '#/components/schemas/LoginResponse' } } } }, '401': { description: 'Invalid credentials', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } }, '403': { description: 'Signaling server URL not allowed', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } } } } }, '/api/auth/logout': { post: { summary: 'Revoke the current bearer token', responses: { '204': { description: 'Token revoked' } } } }, '/api/profile': { get: { summary: 'Get the current user profile', responses: { '200': { description: 'Current user profile', content: { 'application/json': { schema: { $ref: '#/components/schemas/Profile' } } } }, '404': { description: 'No current user is set on this device', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } } } } }, '/api/rooms': { get: { summary: 'List rooms (servers) known to this device', responses: { '200': { description: 'Rooms array', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Room' } } } } } } } }, '/api/rooms/{roomId}': { get: { summary: 'Get a room by id', parameters: [roomIdPathParameter], responses: { '200': { description: 'Room details', content: { 'application/json': { schema: { $ref: '#/components/schemas/Room' } } } }, '404': { description: 'Room not found', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } } } } }, '/api/rooms/{roomId}/users': { get: { summary: 'List users known for a room', parameters: [roomIdPathParameter], responses: { '200': { description: 'Users array', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/User' } } } } } } } }, '/api/rooms/{roomId}/messages': { get: { summary: 'List messages for a room', parameters: [ roomIdPathParameter, { name: 'limit', in: 'query', required: false, schema: { type: 'integer', minimum: 1, maximum: 500 } }, { name: 'offset', in: 'query', required: false, schema: { type: 'integer', minimum: 0 } } ], responses: { '200': { description: 'Messages array', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Message' } } } } } } } }, '/api/rooms/{roomId}/messages/since': { get: { summary: 'List room messages after a timestamp', parameters: [roomIdPathParameter, sinceTimestampQueryParameter], responses: { '200': { description: 'Messages array', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Message' } } } } } } } }, '/api/rooms/{roomId}/bans': { get: { summary: 'List active bans for a room', parameters: [roomIdPathParameter], responses: { '200': { description: 'Bans array', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Ban' } } } } } } } }, '/api/rooms/{roomId}/bans/{userId}': { get: { summary: 'Check whether a user is banned in a room', parameters: [roomIdPathParameter, userIdPathParameter], responses: { '200': { description: 'Ban status', content: { 'application/json': { schema: { type: 'object', required: ['isBanned'], properties: { isBanned: { type: 'boolean' } } } } } } } } }, '/api/messages/{messageId}': { get: { summary: 'Get a message by id', parameters: [messageIdPathParameter], responses: { '200': { description: 'Message details', content: { 'application/json': { schema: { $ref: '#/components/schemas/Message' } } } }, '404': { description: 'Message not found', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } } } } }, '/api/messages/{messageId}/reactions': { get: { summary: 'List reactions for a message', parameters: [messageIdPathParameter], responses: { '200': { description: 'Reactions array', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Reaction' } } } } } } } }, '/api/messages/{messageId}/attachments': { get: { summary: 'List attachments for a message', parameters: [messageIdPathParameter], responses: { '200': { description: 'Attachments array', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Attachment' } } } } } } } }, '/api/users/{userId}': { get: { summary: 'Get a user by id', parameters: [userIdPathParameter], responses: { '200': { description: 'User details', content: { 'application/json': { schema: { $ref: '#/components/schemas/User' } } } }, '404': { description: 'User not found', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } } } } }, '/api/attachments': { get: { summary: 'List all attachments stored on this device', responses: { '200': { description: 'Attachments array', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Attachment' } } } } } } } }, '/api/plugin-data': { get: { summary: 'Read a plugin data value', parameters: [ { name: 'pluginId', in: 'query', required: true, schema: { type: 'string' } }, { name: 'key', in: 'query', required: true, schema: { type: 'string' } }, { name: 'scope', in: 'query', required: true, schema: { type: 'string', enum: ['local', 'server'] } }, { name: 'serverId', in: 'query', required: false, schema: { type: 'string' } } ], responses: { '200': { description: 'Plugin data value', content: { 'application/json': { schema: { $ref: '#/components/schemas/PluginDataValue' } } } } } } }, '/api/meta/{key}': { get: { summary: 'Read a desktop metadata value', parameters: [{ name: 'key', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'Metadata value', content: { 'application/json': { schema: { $ref: '#/components/schemas/MetaValue' } } } } } } } } }; }