2026-02-20 08:16:43 -05:00
|
|
|
import { z } from 'zod';
|
|
|
|
|
|
|
|
|
|
export const itemSchema = z.object({
|
|
|
|
|
id: z.string(),
|
2026-02-24 18:48:08 -05:00
|
|
|
type: z.string().min(1),
|
2026-02-20 08:16:43 -05:00
|
|
|
title: z.string(),
|
|
|
|
|
x: z.number().int(),
|
|
|
|
|
y: z.number().int(),
|
|
|
|
|
createdBy: z.string(),
|
2026-02-27 02:27:16 -05:00
|
|
|
updatedBy: z.string(),
|
2026-02-20 08:16:43 -05:00
|
|
|
createdAt: z.number().int(),
|
|
|
|
|
updatedAt: z.number().int(),
|
|
|
|
|
version: z.number().int(),
|
|
|
|
|
capabilities: z.array(z.string()),
|
2026-02-21 16:13:48 -05:00
|
|
|
useSound: z.string().optional(),
|
2026-02-21 16:01:40 -05:00
|
|
|
emitSound: z.string().optional(),
|
2026-02-20 08:16:43 -05:00
|
|
|
params: z.record(z.string(), z.unknown()),
|
|
|
|
|
carrierId: z.string().nullable().optional(),
|
2026-02-27 01:32:25 -05:00
|
|
|
display: z.record(z.string(), z.string()).optional(),
|
2026-02-20 08:16:43 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const welcomeMessageSchema = z.object({
|
|
|
|
|
type: z.literal('welcome'),
|
|
|
|
|
id: z.string(),
|
2026-02-24 19:52:38 -05:00
|
|
|
player: z.object({
|
|
|
|
|
id: z.string(),
|
|
|
|
|
nickname: z.string(),
|
|
|
|
|
x: z.number().int(),
|
|
|
|
|
y: z.number().int(),
|
|
|
|
|
}),
|
2026-02-20 08:16:43 -05:00
|
|
|
users: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
id: z.string(),
|
|
|
|
|
nickname: z.string(),
|
|
|
|
|
x: z.number().int(),
|
|
|
|
|
y: z.number().int(),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
items: z.array(itemSchema).optional(),
|
2026-02-21 19:12:58 -05:00
|
|
|
worldConfig: z
|
|
|
|
|
.object({
|
|
|
|
|
gridSize: z.number().int().positive(),
|
2026-02-24 19:52:38 -05:00
|
|
|
movementTickMs: z.number().int().positive().optional(),
|
|
|
|
|
movementMaxStepsPerTick: z.number().int().positive().optional(),
|
2026-02-21 19:12:58 -05:00
|
|
|
})
|
|
|
|
|
.optional(),
|
2026-02-22 18:20:13 -05:00
|
|
|
serverInfo: z
|
|
|
|
|
.object({
|
|
|
|
|
instanceId: z.string(),
|
|
|
|
|
version: z.string().optional(),
|
|
|
|
|
})
|
|
|
|
|
.optional(),
|
2026-02-24 22:03:10 -05:00
|
|
|
auth: z
|
|
|
|
|
.object({
|
|
|
|
|
authenticated: z.boolean(),
|
|
|
|
|
userId: z.string().nullable().optional(),
|
|
|
|
|
username: z.string().nullable().optional(),
|
|
|
|
|
role: z.string().nullable().optional(),
|
2026-02-27 03:37:20 -05:00
|
|
|
permissions: z.array(z.string()).optional(),
|
2026-02-27 03:49:28 -05:00
|
|
|
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
|
2026-02-24 22:35:29 -05:00
|
|
|
policy: z
|
|
|
|
|
.object({
|
|
|
|
|
usernameMinLength: z.number().int().positive(),
|
|
|
|
|
usernameMaxLength: z.number().int().positive(),
|
|
|
|
|
passwordMinLength: z.number().int().positive(),
|
|
|
|
|
passwordMaxLength: z.number().int().positive(),
|
|
|
|
|
})
|
|
|
|
|
.optional(),
|
2026-02-24 22:03:10 -05:00
|
|
|
})
|
|
|
|
|
.optional(),
|
2026-02-21 19:12:58 -05:00
|
|
|
uiDefinitions: z
|
|
|
|
|
.object({
|
2026-02-24 18:48:08 -05:00
|
|
|
itemTypeOrder: z.array(z.string().min(1)),
|
2026-02-21 19:12:58 -05:00
|
|
|
itemTypes: z.array(
|
|
|
|
|
z.object({
|
2026-02-24 18:48:08 -05:00
|
|
|
type: z.string().min(1),
|
2026-02-21 19:12:58 -05:00
|
|
|
label: z.string().optional(),
|
2026-02-21 20:47:02 -05:00
|
|
|
tooltip: z.string().optional(),
|
2026-02-21 19:12:58 -05:00
|
|
|
editableProperties: z.array(z.string()),
|
2026-02-24 18:48:08 -05:00
|
|
|
capabilities: z.array(z.string()).optional(),
|
2026-02-21 20:47:02 -05:00
|
|
|
propertyMetadata: z
|
|
|
|
|
.record(
|
|
|
|
|
z.string(),
|
|
|
|
|
z.object({
|
|
|
|
|
valueType: z.enum(['boolean', 'text', 'number', 'list', 'sound']).optional(),
|
2026-02-24 18:48:08 -05:00
|
|
|
label: z.string().optional(),
|
2026-02-21 20:47:02 -05:00
|
|
|
tooltip: z.string().optional(),
|
2026-02-22 03:50:52 -05:00
|
|
|
maxLength: z.number().int().positive().optional(),
|
2026-02-24 18:48:08 -05:00
|
|
|
options: z.array(z.string()).optional(),
|
|
|
|
|
visibleWhen: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
2026-02-21 20:47:02 -05:00
|
|
|
range: z
|
|
|
|
|
.object({
|
|
|
|
|
min: z.number(),
|
|
|
|
|
max: z.number(),
|
|
|
|
|
step: z.number().optional(),
|
|
|
|
|
})
|
|
|
|
|
.optional(),
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.optional(),
|
2026-02-21 19:12:58 -05:00
|
|
|
globalProperties: z.record(z.string(), z.unknown()).optional(),
|
|
|
|
|
}),
|
|
|
|
|
),
|
2026-02-27 03:49:28 -05:00
|
|
|
adminMenu: z
|
|
|
|
|
.object({
|
|
|
|
|
actions: z.array(z.object({ id: z.string(), label: z.string() })),
|
|
|
|
|
})
|
|
|
|
|
.optional(),
|
2026-02-21 19:12:58 -05:00
|
|
|
})
|
|
|
|
|
.optional(),
|
2026-02-20 08:16:43 -05:00
|
|
|
});
|
|
|
|
|
|
2026-02-24 22:03:10 -05:00
|
|
|
export const authRequiredSchema = z.object({
|
|
|
|
|
type: z.literal('auth_required'),
|
|
|
|
|
message: z.string(),
|
2026-02-24 22:35:29 -05:00
|
|
|
authPolicy: z
|
|
|
|
|
.object({
|
|
|
|
|
usernameMinLength: z.number().int().positive(),
|
|
|
|
|
usernameMaxLength: z.number().int().positive(),
|
|
|
|
|
passwordMinLength: z.number().int().positive(),
|
|
|
|
|
passwordMaxLength: z.number().int().positive(),
|
|
|
|
|
})
|
|
|
|
|
.optional(),
|
2026-02-24 22:03:10 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const authResultSchema = z.object({
|
|
|
|
|
type: z.literal('auth_result'),
|
|
|
|
|
ok: z.boolean(),
|
|
|
|
|
message: z.string(),
|
|
|
|
|
sessionToken: z.string().optional(),
|
|
|
|
|
username: z.string().optional(),
|
|
|
|
|
role: z.string().optional(),
|
2026-02-27 03:37:20 -05:00
|
|
|
permissions: z.array(z.string()).optional(),
|
2026-02-27 03:49:28 -05:00
|
|
|
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
|
2026-02-24 22:03:10 -05:00
|
|
|
nickname: z.string().optional(),
|
2026-02-24 22:35:29 -05:00
|
|
|
authPolicy: z
|
|
|
|
|
.object({
|
|
|
|
|
usernameMinLength: z.number().int().positive(),
|
|
|
|
|
usernameMaxLength: z.number().int().positive(),
|
|
|
|
|
passwordMinLength: z.number().int().positive(),
|
|
|
|
|
passwordMaxLength: z.number().int().positive(),
|
|
|
|
|
})
|
|
|
|
|
.optional(),
|
2026-02-24 22:03:10 -05:00
|
|
|
});
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
export const signalMessageSchema = z.object({
|
|
|
|
|
type: z.literal('signal'),
|
|
|
|
|
senderId: z.string(),
|
|
|
|
|
senderNickname: z.string().optional(),
|
|
|
|
|
x: z.number().int().optional(),
|
|
|
|
|
y: z.number().int().optional(),
|
|
|
|
|
targetId: z.string().optional(),
|
|
|
|
|
sdp: z.any().optional(),
|
|
|
|
|
ice: z.any().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const updatePositionSchema = z.object({
|
|
|
|
|
type: z.literal('update_position'),
|
|
|
|
|
id: z.string(),
|
|
|
|
|
x: z.number().int(),
|
|
|
|
|
y: z.number().int(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-24 20:55:02 -05:00
|
|
|
export const teleportCompleteSchema = z.object({
|
|
|
|
|
type: z.literal('teleport_complete'),
|
|
|
|
|
id: z.string(),
|
|
|
|
|
x: z.number().int(),
|
|
|
|
|
y: z.number().int(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
export const updateNicknameSchema = z.object({
|
|
|
|
|
type: z.literal('update_nickname'),
|
|
|
|
|
id: z.string(),
|
|
|
|
|
nickname: z.string().min(1).max(32),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const userLeftSchema = z.object({
|
|
|
|
|
type: z.literal('user_left'),
|
|
|
|
|
id: z.string(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const chatMessageSchema = z.object({
|
|
|
|
|
type: z.literal('chat_message'),
|
|
|
|
|
message: z.string(),
|
|
|
|
|
senderId: z.string().optional(),
|
|
|
|
|
senderNickname: z.string().optional(),
|
|
|
|
|
system: z.boolean().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const pongSchema = z.object({
|
|
|
|
|
type: z.literal('pong'),
|
|
|
|
|
clientSentAt: z.number().int(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const nicknameResultSchema = z.object({
|
|
|
|
|
type: z.literal('nickname_result'),
|
|
|
|
|
accepted: z.boolean(),
|
|
|
|
|
requestedNickname: z.string(),
|
|
|
|
|
effectiveNickname: z.string(),
|
|
|
|
|
reason: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const itemUpsertSchema = z.object({
|
|
|
|
|
type: z.literal('item_upsert'),
|
|
|
|
|
item: itemSchema,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const itemRemoveSchema = z.object({
|
|
|
|
|
type: z.literal('item_remove'),
|
|
|
|
|
itemId: z.string(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const itemActionResultSchema = z.object({
|
|
|
|
|
type: z.literal('item_action_result'),
|
|
|
|
|
ok: z.boolean(),
|
2026-02-25 01:11:47 -05:00
|
|
|
action: z.enum(['add', 'pickup', 'drop', 'delete', 'use', 'secondary_use', 'update']),
|
2026-02-20 08:16:43 -05:00
|
|
|
message: z.string(),
|
|
|
|
|
itemId: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const itemUseSoundSchema = z.object({
|
|
|
|
|
type: z.literal('item_use_sound'),
|
|
|
|
|
itemId: z.string(),
|
|
|
|
|
sound: z.string(),
|
|
|
|
|
x: z.number().int(),
|
|
|
|
|
y: z.number().int(),
|
2026-02-27 01:10:32 -05:00
|
|
|
range: z.number().int().positive().optional(),
|
2026-02-20 08:16:43 -05:00
|
|
|
});
|
|
|
|
|
|
2026-02-27 01:05:23 -05:00
|
|
|
export const itemClockAnnounceSchema = z.object({
|
|
|
|
|
type: z.literal('item_clock_announce'),
|
|
|
|
|
itemId: z.string(),
|
|
|
|
|
sounds: z.array(z.string()),
|
|
|
|
|
x: z.number().int(),
|
|
|
|
|
y: z.number().int(),
|
2026-02-27 01:12:24 -05:00
|
|
|
range: z.number().int().positive().optional(),
|
2026-02-27 01:05:23 -05:00
|
|
|
});
|
|
|
|
|
|
2026-02-22 23:42:17 -05:00
|
|
|
export const itemPianoNoteSchema = z.object({
|
|
|
|
|
type: z.literal('item_piano_note'),
|
|
|
|
|
itemId: z.string(),
|
|
|
|
|
senderId: z.string(),
|
|
|
|
|
keyId: z.string(),
|
|
|
|
|
midi: z.number().int().min(0).max(127),
|
|
|
|
|
on: z.boolean(),
|
|
|
|
|
instrument: z.string(),
|
2026-02-23 00:22:36 -05:00
|
|
|
voiceMode: z.enum(['mono', 'poly']),
|
|
|
|
|
octave: z.number().int().min(-2).max(2),
|
2026-02-22 23:42:17 -05:00
|
|
|
attack: z.number().int().min(0).max(100),
|
|
|
|
|
decay: z.number().int().min(0).max(100),
|
2026-02-23 00:05:01 -05:00
|
|
|
release: z.number().int().min(0).max(100),
|
|
|
|
|
brightness: z.number().int().min(0).max(100),
|
2026-02-22 23:42:17 -05:00
|
|
|
x: z.number().int(),
|
|
|
|
|
y: z.number().int(),
|
|
|
|
|
emitRange: z.number().int().min(1),
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-24 19:56:44 -05:00
|
|
|
export const itemPianoStatusSchema = z.object({
|
|
|
|
|
type: z.literal('item_piano_status'),
|
|
|
|
|
itemId: z.string(),
|
|
|
|
|
event: z.enum([
|
|
|
|
|
'use_mode_entered',
|
|
|
|
|
'record_started',
|
|
|
|
|
'record_paused',
|
|
|
|
|
'record_resumed',
|
|
|
|
|
'record_stopped',
|
|
|
|
|
'playback_started',
|
|
|
|
|
'playback_stopped',
|
|
|
|
|
]),
|
|
|
|
|
recordingState: z.enum(['idle', 'recording', 'paused', 'playback']).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-27 03:37:20 -05:00
|
|
|
export const authPermissionsSchema = z.object({
|
|
|
|
|
type: z.literal('auth_permissions'),
|
|
|
|
|
role: z.string(),
|
|
|
|
|
permissions: z.array(z.string()),
|
2026-02-27 03:49:28 -05:00
|
|
|
adminMenuActions: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
|
2026-02-27 03:37:20 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const adminRoleSummarySchema = z.object({
|
|
|
|
|
id: z.number().int(),
|
|
|
|
|
name: z.string(),
|
|
|
|
|
isSystem: z.boolean(),
|
|
|
|
|
userCount: z.number().int().nonnegative(),
|
|
|
|
|
permissions: z.array(z.string()),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const adminRolesListSchema = z.object({
|
|
|
|
|
type: z.literal('admin_roles_list'),
|
|
|
|
|
roles: z.array(adminRoleSummarySchema),
|
|
|
|
|
permissionKeys: z.array(z.string()),
|
2026-02-27 04:12:37 -05:00
|
|
|
permissionTooltips: z.record(z.string(), z.string()).optional(),
|
2026-02-27 03:37:20 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const adminUsersListSchema = z.object({
|
|
|
|
|
type: z.literal('admin_users_list'),
|
|
|
|
|
users: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
id: z.string(),
|
|
|
|
|
username: z.string(),
|
|
|
|
|
role: z.string(),
|
|
|
|
|
status: z.enum(['active', 'disabled']),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const adminActionResultSchema = z.object({
|
|
|
|
|
type: z.literal('admin_action_result'),
|
|
|
|
|
ok: z.boolean(),
|
|
|
|
|
action: z.enum([
|
|
|
|
|
'role_create',
|
|
|
|
|
'role_update_permissions',
|
|
|
|
|
'role_delete',
|
|
|
|
|
'user_set_role',
|
|
|
|
|
'user_ban',
|
|
|
|
|
'user_unban',
|
|
|
|
|
]),
|
|
|
|
|
message: z.string(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-20 08:16:43 -05:00
|
|
|
export const incomingMessageSchema = z.discriminatedUnion('type', [
|
2026-02-24 22:03:10 -05:00
|
|
|
authRequiredSchema,
|
|
|
|
|
authResultSchema,
|
2026-02-20 08:16:43 -05:00
|
|
|
welcomeMessageSchema,
|
|
|
|
|
signalMessageSchema,
|
|
|
|
|
updatePositionSchema,
|
2026-02-24 20:55:02 -05:00
|
|
|
teleportCompleteSchema,
|
2026-02-20 08:16:43 -05:00
|
|
|
updateNicknameSchema,
|
|
|
|
|
userLeftSchema,
|
|
|
|
|
chatMessageSchema,
|
|
|
|
|
pongSchema,
|
|
|
|
|
nicknameResultSchema,
|
|
|
|
|
itemUpsertSchema,
|
|
|
|
|
itemRemoveSchema,
|
|
|
|
|
itemActionResultSchema,
|
|
|
|
|
itemUseSoundSchema,
|
2026-02-27 01:05:23 -05:00
|
|
|
itemClockAnnounceSchema,
|
2026-02-22 23:42:17 -05:00
|
|
|
itemPianoNoteSchema,
|
2026-02-24 19:56:44 -05:00
|
|
|
itemPianoStatusSchema,
|
2026-02-27 03:37:20 -05:00
|
|
|
authPermissionsSchema,
|
|
|
|
|
adminRolesListSchema,
|
|
|
|
|
adminUsersListSchema,
|
|
|
|
|
adminActionResultSchema,
|
2026-02-20 08:16:43 -05:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export type IncomingMessage = z.infer<typeof incomingMessageSchema>;
|
|
|
|
|
|
|
|
|
|
export type OutgoingMessage =
|
2026-02-24 22:03:10 -05:00
|
|
|
| { type: 'auth_register'; username: string; password: string; email?: string }
|
|
|
|
|
| { type: 'auth_login'; username: string; password: string }
|
|
|
|
|
| { type: 'auth_resume'; sessionToken: string }
|
|
|
|
|
| { type: 'auth_logout' }
|
2026-02-27 03:37:20 -05:00
|
|
|
| { type: 'admin_roles_list' }
|
|
|
|
|
| { type: 'admin_role_create'; name: string }
|
|
|
|
|
| { type: 'admin_role_update_permissions'; role: string; permissions: string[] }
|
|
|
|
|
| { type: 'admin_role_delete'; role: string; replacementRole: string }
|
2026-02-27 03:49:28 -05:00
|
|
|
| { type: 'admin_users_list'; action?: 'set_role' | 'ban' | 'unban' }
|
2026-02-27 03:37:20 -05:00
|
|
|
| { type: 'admin_user_set_role'; username: string; role: string }
|
|
|
|
|
| { type: 'admin_user_ban'; username: string }
|
|
|
|
|
| { type: 'admin_user_unban'; username: string }
|
2026-02-20 08:16:43 -05:00
|
|
|
| { type: 'signal'; targetId: string; sdp?: RTCSessionDescriptionInit; ice?: RTCIceCandidateInit }
|
|
|
|
|
| { type: 'update_position'; x: number; y: number }
|
2026-02-24 21:01:21 -05:00
|
|
|
| { type: 'teleport_complete'; x: number; y: number }
|
2026-02-20 08:16:43 -05:00
|
|
|
| { type: 'update_nickname'; nickname: string }
|
|
|
|
|
| { type: 'chat_message'; message: string }
|
|
|
|
|
| { type: 'ping'; clientSentAt: number }
|
2026-02-24 18:48:08 -05:00
|
|
|
| { type: 'item_add'; itemType: string }
|
2026-02-20 08:16:43 -05:00
|
|
|
| { type: 'item_pickup'; itemId: string }
|
|
|
|
|
| { type: 'item_drop'; itemId: string; x: number; y: number }
|
|
|
|
|
| { type: 'item_delete'; itemId: string }
|
|
|
|
|
| { type: 'item_use'; itemId: string }
|
2026-02-25 01:11:47 -05:00
|
|
|
| { type: 'item_secondary_use'; itemId: string }
|
2026-02-22 23:42:17 -05:00
|
|
|
| { type: 'item_piano_note'; itemId: string; keyId: string; midi: number; on: boolean }
|
2026-02-23 02:00:01 -05:00
|
|
|
| { type: 'item_piano_recording'; itemId: string; action: 'toggle_record' | 'playback' | 'stop_playback' | 'stop_record' }
|
2026-02-20 08:16:43 -05:00
|
|
|
| {
|
|
|
|
|
type: 'item_update';
|
|
|
|
|
itemId: string;
|
|
|
|
|
title?: string;
|
|
|
|
|
params?: Record<string, unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type RemoteUser = {
|
|
|
|
|
id: string;
|
|
|
|
|
nickname: string;
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
};
|