10 Commits

21 changed files with 867 additions and 144 deletions

View File

@@ -1,11 +1,37 @@
import { copyFile, mkdir } from 'node:fs/promises'; import { copyFile, mkdir } from 'node:fs/promises';
import { resolve } from 'node:path'; import { dirname, resolve } from 'node:path';
const root = process.cwd(); const root = process.cwd();
const outputDirectory = resolve(root, 'dist');
const serverDirectory = resolve(root, 'dist', 'server'); const serverDirectory = resolve(root, 'dist', 'server');
const appShell = resolve(outputDirectory, 'index.html');
const browserRoutes = [
'encryption',
'privacy',
'proto',
'proto/docs',
'rcc',
'support',
'terms',
];
await mkdir(serverDirectory, { recursive: true }); await mkdir(serverDirectory, { recursive: true });
await copyFile( await copyFile(
resolve(root, 'worker', 'index.js'), resolve(root, 'worker', 'index.js'),
resolve(serverDirectory, 'index.js'), resolve(serverDirectory, 'index.js'),
); );
// Emit both clean-URL and directory-index shells for every client-side route.
// The directory copy also supports basic Nginx static configurations that do
// not rewrite missing paths to the root application shell.
await Promise.all(
browserRoutes.map(async (route) => {
const routeFile = resolve(outputDirectory, `${route}.html`);
const routeDirectory = resolve(outputDirectory, route);
await mkdir(dirname(routeFile), { recursive: true });
await mkdir(routeDirectory, { recursive: true });
await copyFile(appShell, routeFile);
await copyFile(appShell, resolve(routeDirectory, 'index.html'));
}),
);

19
nginx.conf Normal file
View File

@@ -0,0 +1,19 @@
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location /assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
add_header Cache-Control "no-cache" always;
try_files $uri $uri/ /index.html;
}
}

View File

@@ -2,57 +2,56 @@ version: 3
description: Add avatars and more CDN safety, remove transport server description: Add avatars and more CDN safety, remove transport server
enums: enums:
#Тип вложния #тип вложния
AttachmentType: AttachmentType:
type: int8 type: int8
values: values:
- id: 0 - id: 0
name: IMAGE #Изображение name: IMAGE #изображение
- id: 1 - id: 1
name: MESSAGES #Сообщения (forwarded/reply) name: MESSAGES #сообщения (forwarded/reply)
- id: 2 - id: 2
name: FILE #Файл name: FILE #файл
- id: 4 - id: 4
name: CALL #Звонок name: CALL #звонок
- id: 5 - id: 5
name: VOICE #Голосовое сообщение name: VOICE #голосовое сообщение
- id: 6 - id: 6
name: VIDEO_CIRCLE #Видео сообщение (кружок) name: VIDEO_CIRCLE #видео сообщение (кружок)
- id: 7 - id: 7
name: VIDEO #Видео name: VIDEO #видео
#Решение по девайсу, принять или отклонить вход #решение по девайсу, принять или отклонить вход
DeviceSolution: DeviceSolution:
type: int8 type: int8
values: values:
- id: 0 - id: 0
name: ACCEPT #Принять name: ACCEPT #принять
- id: 1 - id: 1
name: DECLINE #Отклонить name: DECLINE #отклонить
#Статус хэндшейка #статус хэндшейка
HandshakeStage: HandshakeStage:
type: int8 type: int8
values: values:
- id: 0 - id: 0
name: COMPLETED #Пройден name: COMPLETED #пройден
- id: 1 - id: 1
name: NEED_DEVICE_VERIFICATION #Не пройден (приложение рендерит UI подтвердите с другого устройства) name: NEED_DEVICE_VERIFICATION #не пройден (приложение рендерит UI подтвердите с другого устройства)
#Статус в группе #статус в группе
GroupStatus: GroupStatus:
type: int8 type: int8
values: values:
- id: 0 - id: 0
name: JOINED #В группе name: JOINED #в группе
- id: 1 - id: 1
name: INVALID #Не валидная группа name: INVALID #не валидная группа
- id: 2 - id: 2
name: NOT_JOINED #Можно зайт name: NOT_JOINED #можно зайт
- id: 3 - id: 3
name: BANNED #Заблокирован name: BANNED #заблокирован
#
NotificationAction: NotificationAction:
type: int8 type: int8
values: values:

View File

@@ -8,10 +8,16 @@
</url> </url>
<url> <url>
<loc>https://rosetta.im/proto/</loc> <loc>https://rosetta.im/proto/</loc>
<lastmod>2026-07-30</lastmod> <lastmod>2026-08-01</lastmod>
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.9</priority> <priority>0.9</priority>
</url> </url>
<url>
<loc>https://rosetta.im/rcc/</loc>
<lastmod>2026-08-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url> <url>
<loc>https://rosetta.im/proto/docs/</loc> <loc>https://rosetta.im/proto/docs/</loc>
<lastmod>2026-07-31</lastmod> <lastmod>2026-07-31</lastmod>

View File

@@ -18,14 +18,20 @@ async function exists(path) {
} }
} }
function capitalizeFirstLetter(value) {
return value.trim().replace(/\p{L}/u, (letter) => letter.toLocaleUpperCase());
}
function cleanComment(...comments) { function cleanComment(...comments) {
return comments const description = comments
.filter(Boolean) .filter(Boolean)
.join('\n') .join('\n')
.split('\n') .split('\n')
.map((line) => line.replace(/^\s*#?\s?/, '').trim()) .map((line) => line.replace(/^\s*#?\s?/, '').trim())
.filter(Boolean) .filter(Boolean)
.join(' '); .join(' ');
return capitalizeFirstLetter(description);
} }
function mapFields(node) { function mapFields(node) {
@@ -128,7 +134,7 @@ async function readLayer(path) {
return { return {
version: Number(document.get('version')), version: Number(document.get('version')),
description: String(document.get('description') || ''), description: capitalizeFirstLetter(String(document.get('description') || '')),
source: basename(path), source: basename(path),
enums: mapNamedCollection(document.get('enums', true), 'enum'), enums: mapNamedCollection(document.get('enums', true), 'enum'),
types: mapNamedCollection(document.get('types', true), 'type'), types: mapNamedCollection(document.get('types', true), 'type'),

View File

@@ -1,7 +1,7 @@
import '@mantine/core/styles.css'; import '@mantine/core/styles.css';
import { Center, createTheme, Loader, MantineProvider } from '@mantine/core'; import { Button, Center, createTheme, Loader, MantineProvider, Stack, Text, Title } from '@mantine/core';
import { lazy, Suspense } from 'react'; import { Component, lazy, Suspense, type ReactNode } from 'react';
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import { ScrollToTop } from './components/ScrollToTop/ScrollToTop'; import { ScrollToTop } from './components/ScrollToTop/ScrollToTop';
import './style.css'; import './style.css';
@@ -18,6 +18,9 @@ const ProtoPage = lazy(() =>
const ProtoDocsPage = lazy(() => const ProtoDocsPage = lazy(() =>
import('./pages/ProtoPage').then((module) => ({ default: module.ProtoDocsPage })), import('./pages/ProtoPage').then((module) => ({ default: module.ProtoDocsPage })),
); );
const RccPage = lazy(() =>
import('./pages/RccPage').then((module) => ({ default: module.RccPage })),
);
const SupportPage = lazy(() => const SupportPage = lazy(() =>
import('./pages/SupportPage').then((module) => ({ default: module.SupportPage })), import('./pages/SupportPage').then((module) => ({ default: module.SupportPage })),
); );
@@ -39,27 +42,61 @@ const theme = createTheme({
}, },
}); });
class PageErrorBoundary extends Component<
{ children: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return (
<Center mih="100vh" p="xl">
<Stack align="center" gap="md" maw={460}>
<Title order={2} ta="center">
This page could not be loaded
</Title>
<Text c="dimmed" ta="center">
Rosetta may have been updated while this tab was open. Reload the page to continue.
</Text>
<Button onClick={() => window.location.reload()}>Reload page</Button>
</Stack>
</Center>
);
}
return this.props.children;
}
}
export default function App() { export default function App() {
return ( return (
<MantineProvider theme={theme}> <MantineProvider theme={theme}>
<ScrollToTop /> <PageErrorBoundary>
<Suspense <ScrollToTop />
fallback={ <Suspense
<Center mih="100vh"> fallback={
<Loader aria-label="Loading page" /> <Center mih="100vh">
</Center> <Loader aria-label="Loading page" />
} </Center>
> }
<Routes> >
<Route path="/" element={<LandingPage />} /> <Routes>
<Route path="/encryption" element={<EncryptionPage />} /> <Route path="/" element={<LandingPage />} />
<Route path="/proto" element={<ProtoPage />} /> <Route path="/encryption" element={<EncryptionPage />} />
<Route path="/proto/docs" element={<ProtoDocsPage />} /> <Route path="/proto" element={<ProtoPage />} />
<Route path="/support" element={<SupportPage />} /> <Route path="/proto/docs" element={<ProtoDocsPage />} />
<Route path="/privacy" element={<PrivacyPage />} /> <Route path="/rcc" element={<RccPage />} />
<Route path="/terms" element={<TermsPage />} /> <Route path="/support" element={<SupportPage />} />
</Routes> <Route path="/privacy" element={<PrivacyPage />} />
</Suspense> <Route path="/terms" element={<TermsPage />} />
</Routes>
</Suspense>
</PageErrorBoundary>
</MantineProvider> </MantineProvider>
); );
} }

View File

@@ -1,18 +1,22 @@
.page { .page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0;
min-height: 100vh; min-height: 100vh;
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9)); background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
} }
.shell { .shell {
flex: 1; flex: 1;
width: 100%;
min-width: 0;
padding-top: 72px; padding-top: 72px;
padding-bottom: 96px; padding-bottom: 96px;
} }
.main { .main {
width: 100%; width: 100%;
min-width: 0;
max-width: 900px; max-width: 900px;
margin-inline: auto; margin-inline: auto;
padding: clamp(30px, 5vw, 60px); padding: clamp(30px, 5vw, 60px);
@@ -21,6 +25,7 @@
} }
.intro { .intro {
min-width: 0;
max-width: 760px; max-width: 760px;
padding-bottom: 36px; padding-bottom: 36px;
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
@@ -69,6 +74,7 @@
} }
.section { .section {
min-width: 0;
margin-top: 44px; margin-top: 44px;
padding-top: 36px; padding-top: 36px;
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));

View File

@@ -21,6 +21,7 @@ interface LegalPageProps {
interface LegalSectionProps { interface LegalSectionProps {
title: string; title: string;
children: ReactNode; children: ReactNode;
anchorId?: string;
} }
export function LegalPage({ export function LegalPage({
@@ -58,9 +59,9 @@ export function LegalPage({
); );
} }
export function LegalSection({ title, children }: LegalSectionProps) { export function LegalSection({ title, children, anchorId }: LegalSectionProps) {
return ( return (
<Box component="section" className={classes.section}> <Box component="section" id={anchorId} className={classes.section}>
<Title order={2} className={classes.sectionTitle}> <Title order={2} className={classes.sectionTitle}>
{title} {title}
</Title> </Title>

View File

@@ -5,15 +5,42 @@ export function ScrollToTop() {
const { pathname, hash } = useLocation(); const { pathname, hash } = useLocation();
useEffect(() => { useEffect(() => {
const frame = window.requestAnimationFrame(() => { if (hash) {
if (hash) { let targetId: string;
const target = document.getElementById(decodeURIComponent(hash.slice(1)));
if (target) { try {
target.scrollIntoView(); targetId = decodeURIComponent(hash.slice(1));
return; } catch {
} targetId = hash.slice(1);
} }
const scrollToTarget = () => {
const target = document.getElementById(targetId);
if (!target) {
return false;
}
target.scrollIntoView({ block: 'start' });
return true;
};
if (scrollToTarget()) {
return;
}
const observer = new MutationObserver(() => {
if (scrollToTarget()) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
return () => observer.disconnect();
}
const frame = window.requestAnimationFrame(() => {
window.scrollTo({ top: 0, left: 0, behavior: 'instant' }); window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
}); });

View File

@@ -9,7 +9,7 @@ export function SiteFooter() {
return ( return (
<footer className={classes.footer}> <footer className={classes.footer}>
<Container> <Container>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xl"> <SimpleGrid cols={{ base: 1, xs: 2, md: 4 }} spacing="xl">
<Stack gap="sm"> <Stack gap="sm">
<Group gap="sm"> <Group gap="sm">
<RosettaLogo size={34} /> <RosettaLogo size={34} />
@@ -33,14 +33,23 @@ export function SiteFooter() {
<Anchor component={Link} to="/#apps" c="dimmed" size="sm"> <Anchor component={Link} to="/#apps" c="dimmed" size="sm">
Apps Apps
</Anchor> </Anchor>
<Anchor component={Link} to="/encryption" c="dimmed" size="sm"> <Anchor component={Link} to="/support" c="dimmed" size="sm">
Encryption Support
</Anchor> </Anchor>
</Stack>
<Stack gap="xs">
<Text fw={600} size="sm">
Developers
</Text>
<Anchor component={Link} to="/proto" c="dimmed" size="sm"> <Anchor component={Link} to="/proto" c="dimmed" size="sm">
Proto Proto
</Anchor> </Anchor>
<Anchor component={Link} to="/support" c="dimmed" size="sm"> <Anchor component={Link} to="/encryption" c="dimmed" size="sm">
Support Encryption
</Anchor>
<Anchor component={Link} to="/rcc" c="dimmed" size="sm">
RCC
</Anchor> </Anchor>
</Stack> </Stack>

View File

@@ -2402,101 +2402,101 @@ export const protoLayers = [
{ {
"id": 0, "id": 0,
"name": "IMAGE", "name": "IMAGE",
"description": "" "description": "Изображение"
}, },
{ {
"id": 1, "id": 1,
"name": "MESSAGES", "name": "MESSAGES",
"description": "" "description": "Сообщения (forwarded/reply)"
}, },
{ {
"id": 2, "id": 2,
"name": "FILE", "name": "FILE",
"description": "" "description": "Файл"
}, },
{ {
"id": 4, "id": 4,
"name": "CALL", "name": "CALL",
"description": "" "description": "Звонок"
}, },
{ {
"id": 5, "id": 5,
"name": "VOICE", "name": "VOICE",
"description": "" "description": "Голосовое сообщение"
}, },
{ {
"id": 6, "id": 6,
"name": "VIDEO_CIRCLE", "name": "VIDEO_CIRCLE",
"description": "" "description": "Видео сообщение (кружок)"
}, },
{ {
"id": 7, "id": 7,
"name": "VIDEO", "name": "VIDEO",
"description": "" "description": "Видео"
} }
] ]
}, },
{ {
"name": "DeviceSolution", "name": "DeviceSolution",
"type": "int8", "type": "int8",
"description": "", "description": "Решение по девайсу, принять или отклонить вход",
"yaml": "DeviceSolution:\n type: int8\n values:\n - id: 0\n name: ACCEPT\n - id: 1\n name: DECLINE", "yaml": "DeviceSolution:\n type: int8\n values:\n - id: 0\n name: ACCEPT\n - id: 1\n name: DECLINE",
"values": [ "values": [
{ {
"id": 0, "id": 0,
"name": "ACCEPT", "name": "ACCEPT",
"description": "" "description": "Принять"
}, },
{ {
"id": 1, "id": 1,
"name": "DECLINE", "name": "DECLINE",
"description": "" "description": "Отклонить"
} }
] ]
}, },
{ {
"name": "HandshakeStage", "name": "HandshakeStage",
"type": "int8", "type": "int8",
"description": "", "description": "Статус хэндшейка",
"yaml": "HandshakeStage:\n type: int8\n values:\n - id: 0\n name: COMPLETED\n - id: 1\n name: NEED_DEVICE_VERIFICATION", "yaml": "HandshakeStage:\n type: int8\n values:\n - id: 0\n name: COMPLETED\n - id: 1\n name: NEED_DEVICE_VERIFICATION",
"values": [ "values": [
{ {
"id": 0, "id": 0,
"name": "COMPLETED", "name": "COMPLETED",
"description": "" "description": "Пройден"
}, },
{ {
"id": 1, "id": 1,
"name": "NEED_DEVICE_VERIFICATION", "name": "NEED_DEVICE_VERIFICATION",
"description": "" "description": "Не пройден (приложение рендерит UI подтвердите с другого устройства)"
} }
] ]
}, },
{ {
"name": "GroupStatus", "name": "GroupStatus",
"type": "int8", "type": "int8",
"description": "", "description": "Статус в группе",
"yaml": "GroupStatus:\n type: int8\n values:\n - id: 0\n name: JOINED\n - id: 1\n name: INVALID\n - id: 2\n name: NOT_JOINED\n - id: 3\n name: BANNED", "yaml": "GroupStatus:\n type: int8\n values:\n - id: 0\n name: JOINED\n - id: 1\n name: INVALID\n - id: 2\n name: NOT_JOINED\n - id: 3\n name: BANNED",
"values": [ "values": [
{ {
"id": 0, "id": 0,
"name": "JOINED", "name": "JOINED",
"description": "" "description": "В группе"
}, },
{ {
"id": 1, "id": 1,
"name": "INVALID", "name": "INVALID",
"description": "" "description": "Не валидная группа"
}, },
{ {
"id": 2, "id": 2,
"name": "NOT_JOINED", "name": "NOT_JOINED",
"description": "" "description": "Можно зайт"
}, },
{ {
"id": 3, "id": 3,
"name": "BANNED", "name": "BANNED",
"description": "" "description": "Заблокирован"
} }
] ]
}, },
@@ -2615,23 +2615,23 @@ export const protoLayers = [
{ {
"name": "WebRTCType", "name": "WebRTCType",
"type": "int8", "type": "int8",
"description": "", "description": "Тип WebRTC сигнала",
"yaml": "WebRTCType:\n type: int8\n values:\n - id: 0\n name: OFFER\n - id: 1\n name: ANSWER\n - id: 2\n name: ICE_CANDIDATE", "yaml": "WebRTCType:\n type: int8\n values:\n - id: 0\n name: OFFER\n - id: 1\n name: ANSWER\n - id: 2\n name: ICE_CANDIDATE",
"values": [ "values": [
{ {
"id": 0, "id": 0,
"name": "OFFER", "name": "OFFER",
"description": "" "description": "Offer"
}, },
{ {
"id": 1, "id": 1,
"name": "ANSWER", "name": "ANSWER",
"description": "" "description": "Answer"
}, },
{ {
"id": 2, "id": 2,
"name": "ICE_CANDIDATE", "name": "ICE_CANDIDATE",
"description": "" "description": "Estabilishment candidate"
} }
] ]
}, },
@@ -2654,22 +2654,22 @@ export const protoLayers = [
{ {
"id": 2, "id": 2,
"name": "INVALID", "name": "INVALID",
"description": "любая ошибка смысл которой зависит от контекста, например в случае с файловой системой - нет файла" "description": "Любая ошибка смысл которой зависит от контекста, например в случае с файловой системой - нет файла"
}, },
{ {
"id": 3, "id": 3,
"name": "USERNAME_TAKEN", "name": "USERNAME_TAKEN",
"description": "" "description": "Имя пользователя занято"
}, },
{ {
"id": 4, "id": 4,
"name": "OVERSIZE", "name": "OVERSIZE",
"description": "слишком большой размер файла" "description": "Слишком большой размер файла"
}, },
{ {
"id": 5, "id": 5,
"name": "OVERLIFE", "name": "OVERLIFE",
"description": "слишком большой TTL" "description": "Слишком большой TTL"
} }
] ]
}, },
@@ -2716,35 +2716,35 @@ export const protoLayers = [
"name": "id", "name": "id",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "" "description": "Случайная строка"
}, },
{ {
"id": 2, "id": 2,
"name": "blob", "name": "blob",
"type": "string", "type": "string",
"optional": true, "optional": true,
"description": "" "description": "Только очень коротких типов"
}, },
{ {
"id": 3, "id": 3,
"name": "type", "name": "type",
"type": "AttachmentType", "type": "AttachmentType",
"optional": false, "optional": false,
"description": "" "description": "Тип вложения"
}, },
{ {
"id": 4, "id": 4,
"name": "preview", "name": "preview",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "" "description": "Превью"
}, },
{ {
"id": 6, "id": 6,
"name": "objectId", "name": "objectId",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "" "description": "Выданный сервером objectId от cdn"
} }
] ]
}, },
@@ -2758,35 +2758,35 @@ export const protoLayers = [
"name": "preview", "name": "preview",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "превью (для фото blurhash)" "description": "Превью (для фото blurhash)"
}, },
{ {
"id": 2, "id": 2,
"name": "objectId", "name": "objectId",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "обьект на cdn" "description": "Обьект на cdn"
}, },
{ {
"id": 3, "id": 3,
"name": "type", "name": "type",
"type": "ProfileMediaType", "type": "ProfileMediaType",
"optional": false, "optional": false,
"description": "тип медиа" "description": "Тип медиа"
}, },
{ {
"id": 4, "id": 4,
"name": "timestamp", "name": "timestamp",
"type": "int64", "type": "int64",
"optional": false, "optional": false,
"description": "дата публикации" "description": "Дата публикации"
}, },
{ {
"id": 5, "id": 5,
"name": "mediaKey", "name": "mediaKey",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "ключ шифрования от медиа" "description": "Ключ шифрования от медиа"
} }
] ]
}, },
@@ -2863,42 +2863,42 @@ export const protoLayers = [
"name": "username", "name": "username",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "имя для поиска" "description": "Имя для поиска"
}, },
{ {
"id": 2, "id": 2,
"name": "title", "name": "title",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "отображаемое имя пользователя" "description": "Отображаемое имя пользователя"
}, },
{ {
"id": 3, "id": 3,
"name": "publicKey", "name": "publicKey",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "публичный ключ" "description": "Публичный ключ"
}, },
{ {
"id": 4, "id": 4,
"name": "verified", "name": "verified",
"type": "int32", "type": "int32",
"optional": false, "optional": false,
"description": "верификация" "description": "Верификация"
}, },
{ {
"id": 5, "id": 5,
"name": "networkStatus", "name": "networkStatus",
"type": "NetworkStatus", "type": "NetworkStatus",
"optional": false, "optional": false,
"description": "онлайн пользователь или нет" "description": "Онлайн пользователь или нет"
}, },
{ {
"id": 6, "id": 6,
"name": "avatar", "name": "avatar",
"type": "ProfileMedia", "type": "ProfileMedia",
"optional": true, "optional": true,
"description": "аватар пользователя если есть" "description": "Аватар пользователя если есть"
} }
] ]
}, },
@@ -2912,42 +2912,42 @@ export const protoLayers = [
"name": "username", "name": "username",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "имя для поиска" "description": "Имя для поиска"
}, },
{ {
"id": 2, "id": 2,
"name": "title", "name": "title",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "отображаемое имя пользователя" "description": "Отображаемое имя пользователя"
}, },
{ {
"id": 3, "id": 3,
"name": "publicKey", "name": "publicKey",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "публичный ключ" "description": "Публичный ключ"
}, },
{ {
"id": 4, "id": 4,
"name": "verified", "name": "verified",
"type": "int32", "type": "int32",
"optional": false, "optional": false,
"description": "верификация" "description": "Верификация"
}, },
{ {
"id": 5, "id": 5,
"name": "networkStatus", "name": "networkStatus",
"type": "NetworkStatus", "type": "NetworkStatus",
"optional": false, "optional": false,
"description": "онлайн пользователь или нет" "description": "Онлайн пользователь или нет"
}, },
{ {
"id": 6, "id": 6,
"name": "media", "name": "media",
"type": "ProfileMedia[]", "type": "ProfileMedia[]",
"optional": true, "optional": true,
"description": "все активное медиа (включая будущие истории)" "description": "Все активное медиа (включая будущие истории)"
} }
] ]
}, },
@@ -3361,7 +3361,7 @@ export const protoLayers = [
"name": "server", "name": "server",
"type": "string", "type": "string",
"optional": true, "optional": true,
"description": "выбранный транспортный сервер" "description": "Выбранный транспортный сервер"
} }
] ]
}, },
@@ -3690,42 +3690,42 @@ export const protoLayers = [
"name": "size", "name": "size",
"type": "uint32", "type": "uint32",
"optional": false, "optional": false,
"description": "размер загружаемого файла (декларируемый)" "description": "Размер загружаемого файла (декларируемый)"
}, },
{ {
"id": 2, "id": 2,
"name": "objectId", "name": "objectId",
"type": "string", "type": "string",
"optional": true, "optional": true,
"description": "выделенный сервером objectId" "description": "Выделенный сервером objectId"
}, },
{ {
"id": 3, "id": 3,
"name": "chunkLength", "name": "chunkLength",
"type": "uint32", "type": "uint32",
"optional": true, "optional": true,
"description": "размер чанка" "description": "Размер чанка"
}, },
{ {
"id": 4, "id": 4,
"name": "chunks", "name": "chunks",
"type": "uint32", "type": "uint32",
"optional": true, "optional": true,
"description": "количество чанков" "description": "Количество чанков"
}, },
{ {
"id": 5, "id": 5,
"name": "host", "name": "host",
"type": "string", "type": "string",
"optional": true, "optional": true,
"description": "хост, где сохранен файл" "description": "Хост, где сохранен файл"
}, },
{ {
"id": 6, "id": 6,
"name": "ttl", "name": "ttl",
"type": "uint32", "type": "uint32",
"optional": true, "optional": true,
"description": "ttl в секундах, после которого файл будет удален с CDN и с таблицы, по умолчанию 7 дней, не допускается использование слишком больших значений" "description": "Ttl в секундах, после которого файл будет удален с CDN и с таблицы, по умолчанию 7 дней, не допускается использование слишком больших значений"
} }
] ]
}, },
@@ -3740,35 +3740,35 @@ export const protoLayers = [
"name": "size", "name": "size",
"type": "uint32", "type": "uint32",
"optional": true, "optional": true,
"description": "размер файла" "description": "Размер файла"
}, },
{ {
"id": 2, "id": 2,
"name": "objectId", "name": "objectId",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "выделенный сервером objectId" "description": "Выделенный сервером objectId"
}, },
{ {
"id": 3, "id": 3,
"name": "chunkLength", "name": "chunkLength",
"type": "uint32", "type": "uint32",
"optional": true, "optional": true,
"description": "размер чанка" "description": "Размер чанка"
}, },
{ {
"id": 4, "id": 4,
"name": "chunks", "name": "chunks",
"type": "uint32", "type": "uint32",
"optional": true, "optional": true,
"description": "количество чанков" "description": "Количество чанков"
}, },
{ {
"id": 5, "id": 5,
"name": "host", "name": "host",
"type": "string", "type": "string",
"optional": true, "optional": true,
"description": "сервер с которого скачивать файл" "description": "Сервер с которого скачивать файл"
} }
] ]
}, },
@@ -3783,21 +3783,21 @@ export const protoLayers = [
"name": "objectId", "name": "objectId",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "выделенный сервером objectId при отправке пакета UploadCandidate" "description": "Выделенный сервером objectId при отправке пакета UploadCandidate"
}, },
{ {
"id": 2, "id": 2,
"name": "blurhash", "name": "blurhash",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "превью аватара, строка blurhash" "description": "Превью аватара, строка blurhash"
}, },
{ {
"id": 3, "id": 3,
"name": "mediaKey", "name": "mediaKey",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "ключ шифрования" "description": "Ключ шифрования"
} }
] ]
}, },
@@ -3812,7 +3812,7 @@ export const protoLayers = [
"name": "objectId", "name": "objectId",
"type": "string", "type": "string",
"optional": false, "optional": false,
"description": "objectId аватара, аватар или фотография профиля должна при этом стоять у профиля отправителя пакета" "description": "ObjectId аватара, аватар или фотография профиля должна при этом стоять у профиля отправителя пакета"
} }
] ]
}, },

View File

@@ -2,6 +2,42 @@ import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import App from './App.tsx' import App from './App.tsx'
const PRELOAD_RELOAD_KEY = 'rosetta:preload-reload-at'
const PRELOAD_RELOAD_COOLDOWN = 10_000
if (import.meta.env.PROD) {
window.addEventListener('vite:preloadError', (event) => {
let lastReload = 0
try {
lastReload = Number(window.sessionStorage.getItem(PRELOAD_RELOAD_KEY) || 0)
} catch {
// Storage can be unavailable in restricted browser modes.
}
if (Date.now() - lastReload < PRELOAD_RELOAD_COOLDOWN) {
return
}
event.preventDefault()
try {
window.sessionStorage.setItem(PRELOAD_RELOAD_KEY, String(Date.now()))
} catch {
// Reload recovery still works when storage is unavailable.
}
window.location.reload()
})
window.setTimeout(() => {
try {
window.sessionStorage.removeItem(PRELOAD_RELOAD_KEY)
} catch {
// Nothing to clear when storage is unavailable.
}
}, PRELOAD_RELOAD_COOLDOWN)
}
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<BrowserRouter> <BrowserRouter>

View File

@@ -31,9 +31,9 @@
.callout { .callout {
margin: 18px 0 26px; margin: 18px 0 26px;
border: 1px solid light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-9)); border: 1px solid light-dark(var(--mantine-color-red-3), var(--mantine-color-red-9));
border-left: 3px solid var(--mantine-color-blue-6); border-left: 3px solid var(--mantine-color-red-6);
background: light-dark(var(--mantine-color-blue-0), rgba(25, 113, 194, 0.12)); background: light-dark(var(--mantine-color-red-0), rgba(250, 82, 82, 0.1));
} }
.vectorGroup { .vectorGroup {
@@ -58,17 +58,21 @@
} }
.marker { .marker {
padding: 1px 3px; padding: 0 0.14em;
background: light-dark(var(--mantine-color-blue-1), rgba(25, 113, 194, 0.24)); border-radius: 2px;
color: inherit; background: light-dark(rgba(250, 82, 82, 0.13), rgba(250, 82, 82, 0.2));
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-2));
font-weight: inherit;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
} }
.result { .result {
margin: 18px 0; margin: 18px 0;
padding: 12px 14px; padding: 12px 14px;
border-left: 3px solid var(--mantine-color-blue-6); border-left: 3px solid var(--mantine-color-red-6);
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7)); background: light-dark(var(--mantine-color-red-0), rgba(250, 82, 82, 0.1));
color: light-dark(var(--mantine-color-blue-9), var(--mantine-color-blue-2)); color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-2));
font-size: var(--mantine-font-size-sm); font-size: var(--mantine-font-size-sm);
font-weight: 650; font-weight: 400;
} }

View File

@@ -185,10 +185,11 @@ keyEnvelope = Base64(iv || AES-CBC(wrappingKey, messageSecret) || ephemeral.publ
</Box> </Box>
<LegalText> <LegalText>
The result contains 13 encrypted content bytes followed by the 16-byte The result contains 13 encrypted content bytes followed by the 16-byte
Poly1305 authentication tag.{' '} Poly1305 authentication tag. Any change to either part causes{' '}
<Text component="span" className={classes.marker}> <Text component="span" className={classes.marker}>
Any change to either part causes authenticated decryption to fail. authenticated decryption to fail
</Text> </Text>
.
</LegalText> </LegalText>
<Title order={3} className={classes.subheading}> <Title order={3} className={classes.subheading}>

View File

@@ -126,6 +126,14 @@ export function PrivacyPage() {
some requests may require you to provide information that helps us verify some requests may require you to provide information that helps us verify
control of the relevant account or keys. control of the relevant account or keys.
</LegalText> </LegalText>
<LegalText>
For step-by-step deletion instructions, including what data may remain, see
the{' '}
<Anchor component={Link} to="/terms#account-deletion">
Account Deletion section of the Terms of Use
</Anchor>
.
</LegalText>
</LegalSection> </LegalSection>
<LegalSection title="8. Children"> <LegalSection title="8. Children">

View File

@@ -1,4 +1,7 @@
.protoCode { .protoCode {
width: 100%;
min-width: 0;
max-width: 100%;
margin: 18px 0; margin: 18px 0;
padding: 16px; padding: 16px;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
@@ -23,18 +26,37 @@
font-weight: 650; font-weight: 650;
} }
.important {
padding: 0 0.14em;
border-radius: 2px;
background: light-dark(rgba(250, 82, 82, 0.13), rgba(250, 82, 82, 0.2));
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-2));
font-weight: inherit;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
.layerExamples { .layerExamples {
min-width: 0;
margin: 18px 0 24px; margin: 18px 0 24px;
} }
.layerExamples > * {
min-width: 0;
}
.layerExamples .protoCode { .layerExamples .protoCode {
height: calc(100% - 30px); height: calc(100% - 30px);
margin-bottom: 0; margin-bottom: 0;
} }
.docsLinks {
margin-top: 44px;
gap: 28px;
}
.docsLink { .docsLink {
display: inline-block; display: inline-block;
margin-top: 44px;
font-size: var(--mantine-font-size-sm); font-size: var(--mantine-font-size-sm);
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
@@ -51,6 +73,8 @@
.docsGrid { .docsGrid {
display: grid; display: grid;
width: 100%;
min-width: 0;
grid-template-columns: 220px minmax(0, 1fr); grid-template-columns: 220px minmax(0, 1fr);
gap: 44px; gap: 44px;
align-items: start; align-items: start;
@@ -59,6 +83,7 @@
.sidebar { .sidebar {
position: sticky; position: sticky;
top: 92px; top: 92px;
min-width: 0;
max-height: calc(100vh - 112px); max-height: calc(100vh - 112px);
padding-right: 10px; padding-right: 10px;
overflow-y: auto; overflow-y: auto;
@@ -67,8 +92,10 @@
.sidebar a { .sidebar a {
width: fit-content; width: fit-content;
max-width: 100%;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
text-decoration: none; text-decoration: none;
overflow-wrap: anywhere;
} }
.sidebar a:hover { .sidebar a:hover {
@@ -77,6 +104,7 @@
} }
.layerDescription { .layerDescription {
min-width: 0;
padding: 12px; padding: 12px;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
border-radius: var(--mantine-radius-xs); border-radius: var(--mantine-radius-xs);
@@ -85,7 +113,9 @@
.layerDescription code { .layerDescription code {
display: inline-block; display: inline-block;
max-width: 100%;
font-size: 11px; font-size: 11px;
overflow-wrap: anywhere;
} }
.documentation { .documentation {
@@ -100,6 +130,7 @@
.schemaSection { .schemaSection {
scroll-margin-top: 96px; scroll-margin-top: 96px;
min-width: 0;
margin-top: 64px; margin-top: 64px;
} }
@@ -112,6 +143,7 @@
.definition { .definition {
scroll-margin-top: 96px; scroll-margin-top: 96px;
min-width: 0;
margin-top: 38px; margin-top: 38px;
padding-top: 32px; padding-top: 32px;
border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
@@ -126,6 +158,8 @@
} }
.yamlDetails { .yamlDetails {
min-width: 0;
max-width: 100%;
margin: 20px 0; margin: 20px 0;
} }
@@ -139,6 +173,8 @@
} }
.yamlBlock { .yamlBlock {
min-width: 0;
max-width: 100%;
margin-top: 10px; margin-top: 10px;
padding: 12px 12px 16px 18px; padding: 12px 12px 16px 18px;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
@@ -148,6 +184,9 @@
.yamlBlock code { .yamlBlock code {
display: block; display: block;
width: 100%;
min-width: 0;
max-width: 100%;
padding: 0; padding: 0;
background: transparent; background: transparent;
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0)); color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
@@ -162,6 +201,12 @@
border-radius: var(--mantine-radius-xs); border-radius: var(--mantine-radius-xs);
} }
.tableScroll {
width: 100%;
min-width: 0;
max-width: 100%;
}
.table th { .table th {
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7)); background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
} }
@@ -179,7 +224,7 @@
@media (max-width: 62em) { @media (max-width: 62em) {
.docsGrid { .docsGrid {
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr);
} }
.sidebar { .sidebar {

View File

@@ -145,7 +145,7 @@ function FieldTable({
} }
return ( return (
<Table.ScrollContainer minWidth={620}> <Table.ScrollContainer minWidth={620} className={classes.tableScroll}>
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}> <Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
@@ -285,7 +285,7 @@ function EnumDefinition({ item, layer }: { item: ProtoEnum; layer: ProtoLayer })
{item.description || `Enumeration ${item.name} in protocol layer ${layer.version}.`} {item.description || `Enumeration ${item.name} in protocol layer ${layer.version}.`}
</Text> </Text>
<HiddenYaml value={item.yaml} /> <HiddenYaml value={item.yaml} />
<Table.ScrollContainer minWidth={520}> <Table.ScrollContainer minWidth={520} className={classes.tableScroll}>
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}> <Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
@@ -517,6 +517,56 @@ export class RccGeneratedPacketRegistry {
</Code> </Code>
</LegalSection> </LegalSection>
<LegalSection title="RCC: one schema contract, two generation modes">
<LegalText>
Rosetta Code Compiler (RCC) is the build-time compiler behind the protocol.
It reads every <Code>layer.N.yml</Code> snapshot, turns the YAML into a
typed protocol model, validates that the history is safe, and emits source
code for the selected language. Applications import generated models and
codecs; they do not parse the YAML at runtime.
</LegalText>
<Code block className={classes.protoCode}>
{`proto/layer.*.yml
│
├─ parse packets, reusable types, enums and comments
├─ validate references, required fields and stable field IDs
│
├─ mode=server → all layers + version-aware codecs + registry
└─ mode=client → latest layer + latest codecs + version constant`}
</Code>
<LegalList>
<List.Item>
<strong>Server generation</strong> preserves the protocol timeline.
Java codecs receive the negotiated layer, include only fields active in
that layer, and expose the compiled minimum and maximum versions.
</List.Item>
<List.Item>
<strong>Client generation</strong> selects the{' '}
<Text component="span" className={classes.important}>
highest layer
</Text>
{' '}and creates the current packet, type, enum, codec, and registry
sources without carrying historical shapes into the application.
</List.Item>
<List.Item>
<strong>Schema validation</strong> fails the build for unknown types or
unsafe field-ID reuse, so incompatible protocol edits are caught before
generated sources reach a server or client package.
</List.Item>
<List.Item>
<strong>Generated documentation</strong> carries schema comments into
Javadoc or TSDoc, including field direction, wire type, optionality, and
accessor contracts.
</List.Item>
</LegalList>
<LegalText>
The RCC guide includes separate client and version-aware server examples.{' '}
Run <Code className={classes.important}>npx rcc --list-languages</Code> for
the authoritative list of language targets in the installed compiler
version.
</LegalText>
</LegalSection>
<LegalSection title="How an RCC YAML layer is structured"> <LegalSection title="How an RCC YAML layer is structured">
<LegalText> <LegalText>
RCC reads versioned files named <Code>layer.N.yml</Code>. The top-level RCC reads versioned files named <Code>layer.N.yml</Code>. The top-level
@@ -624,7 +674,7 @@ reader.getInt8(fieldId: 1) -> ResultCode.SUCCESS`}
^^^^^ ^^^^^^^^ ^^^^^ ^^^^^^^^
packet field 1 · INT8 · SUCCESS (0)`} packet field 1 · INT8 · SUCCESS (0)`}
</Code> </Code>
<Table.ScrollContainer minWidth={560}> <Table.ScrollContainer minWidth={560} className={classes.tableScroll}>
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.comparisonTable}> <Table verticalSpacing="sm" horizontalSpacing="md" className={classes.comparisonTable}>
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
@@ -664,9 +714,14 @@ packet field 1 · INT8 · SUCCESS (0)`}
</LegalText> </LegalText>
</LegalSection> </LegalSection>
<Anchor component={Link} to="/proto/docs" className={classes.docsLink}> <Group className={classes.docsLinks}>
See docs of PROTO → <Anchor component={Link} to="/proto/docs" className={classes.docsLink}>
</Anchor> See docs of PROTO →
</Anchor>
<Anchor component={Link} to="/rcc" className={classes.docsLink}>
See docs of RCC →
</Anchor>
</Group>
</LegalPage> </LegalPage>
</> </>
); );

View File

@@ -0,0 +1,119 @@
.code {
margin: 18px 0;
padding: 16px;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
border-radius: var(--mantine-radius-xs);
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
font-size: 12px;
line-height: 1.65;
overflow-x: auto;
white-space: pre;
}
.pipeline {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr);
gap: 12px;
align-items: center;
margin: 22px 0;
}
.pipeline > span:nth-child(odd) {
padding: 14px;
border: 1px solid light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-9));
border-radius: var(--mantine-radius-xs);
background: light-dark(var(--mantine-color-blue-0), rgba(25, 113, 194, 0.12));
color: light-dark(var(--mantine-color-blue-9), var(--mantine-color-blue-1));
font-size: var(--mantine-font-size-sm);
font-weight: 650;
line-height: 1.45;
text-align: center;
}
.pipeline > span:nth-child(even) {
color: var(--mantine-color-blue-6);
font-weight: 700;
}
.step {
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
gap: 16px;
margin-top: 30px;
}
.step:first-child {
margin-top: 0;
}
.stepNumber {
display: grid;
width: 34px;
height: 34px;
place-items: center;
border-radius: 50%;
background: var(--mantine-color-blue-6);
color: var(--mantine-color-white);
font-size: var(--mantine-font-size-sm);
font-weight: 700;
}
.stepBody {
min-width: 0;
padding-bottom: 24px;
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}
.step:last-child .stepBody {
padding-bottom: 0;
border-bottom: 0;
}
.subheading {
margin: 3px 0 12px;
color: light-dark(var(--mantine-color-dark-8), var(--mantine-color-dark-0));
font-size: 17px;
font-weight: 650;
}
.important {
padding: 0 0.14em;
border-radius: 2px;
background: light-dark(rgba(250, 82, 82, 0.13), rgba(250, 82, 82, 0.2));
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-2));
font-weight: inherit;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
.note {
margin: 18px 0;
border: 1px solid light-dark(var(--mantine-color-red-3), var(--mantine-color-red-9));
border-left: 3px solid var(--mantine-color-red-6);
background: light-dark(var(--mantine-color-red-0), rgba(250, 82, 82, 0.1));
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
}
@media (max-width: 48em) {
.pipeline {
grid-template-columns: minmax(0, 1fr);
}
.pipeline > span:nth-child(even) {
transform: rotate(90deg);
text-align: center;
}
}
@media (max-width: 36em) {
.step {
grid-template-columns: 28px minmax(0, 1fr);
gap: 12px;
}
.stepNumber {
width: 28px;
height: 28px;
}
}

249
src/pages/RccPage.tsx Normal file
View File

@@ -0,0 +1,249 @@
import { Anchor, Box, Code, List, Paper, Text, Title } from '@mantine/core';
import type { ReactNode } from 'react';
import {
LegalList,
LegalPage,
LegalSection,
LegalText,
} from '../components/LegalPage/LegalPage';
import { SEO } from '../components/SEO/SEO';
import classes from './RccPage.module.css';
const GITEA_REPOSITORY = 'https://git.rosetta.im/Rosetta/rcc';
function TechnicalCode({ children }: { children: string }) {
return (
<Code block className={classes.code}>
{children}
</Code>
);
}
function Step({ number, title, children }: { number: number; title: string; children: ReactNode }) {
return (
<Box className={classes.step}>
<Text className={classes.stepNumber}>{number}</Text>
<Box className={classes.stepBody}>
<Title order={3} className={classes.subheading}>
{title}
</Title>
{children}
</Box>
</Box>
);
}
export function RccPage() {
return (
<>
<SEO
title="Rosetta Code Compiler (RCC)"
description="Install RCC and generate validated protocol models, codecs, and registries from versioned Rosetta YAML schemas."
canonical="https://rosetta.im/rcc/"
keywords="RCC, Rosetta Code Compiler, code generation, protocol codecs, YAML protocol"
/>
<LegalPage eyebrow="Developer tooling" title="Rosetta Code Compiler">
<LegalText lead>
RCC compiles Rosetta&apos;s versioned YAML protocol into typed models, codecs,
and packet registries. A server build can retain the{' '}
<Text component="span" className={classes.important}>
complete protocol history
</Text>
; a client build stays small by generating only the{' '}
<Text component="span" className={classes.important}>
latest layer
</Text>
.
</LegalText>
<LegalSection title="What RCC produces">
<LegalText>
RCC treats <Code>proto/layer.*.yml</Code> as the protocol&apos;s source of
truth. Before writing code, it resolves primitive, enum, and reusable type
references and rejects incompatible reuse of a field ID between layers.
</LegalText>
<Box className={classes.pipeline}>
<Text component="span">Versioned YAML schemas</Text>
<Text component="span" aria-hidden="true">→</Text>
<Text component="span">Parse + validate</Text>
<Text component="span" aria-hidden="true">→</Text>
<Text component="span">Models + codecs + registry</Text>
</Box>
<LegalList>
<List.Item>
Packets become typed packet classes; reusable schema types become model
classes; enums preserve their numeric wire values.
</List.Item>
<List.Item>
Codecs encode and decode TLV fields and verify required fields before
encoding and after decoding.
</List.Item>
<List.Item>
The generated registry maps packet IDs to codecs and exposes the
compiled protocol version or version range.
</List.Item>
<List.Item>
YAML comments become Javadoc or TSDoc for generated declarations and
accessors.
</List.Item>
</LegalList>
</LegalSection>
<LegalSection title="Install RCC from Rosetta Gitea">
<LegalText>
Add Rosetta&apos;s npm package registry to the project, then install RCC. The
registry setting can live in a project-level <Code>.npmrc</Code> so CI and
local builds resolve the same compiler package.
</LegalText>
<TechnicalCode>{`# .npmrc
registry=https://git.rosetta.im/api/packages/Rosetta/npm/
# configure read-only registry authentication
npm config set -- \\
'//git.rosetta.im/api/packages/Rosetta/npm/:_authToken' \\
'b5e64acbf1fdbe2d6d2bc1fac95333ef6f905fff'
# install the compiler
npm install rcc
# query languages from the installed RCC version
npx rcc --list-languages
# inspect the rest of the CLI
npx rcc --help`}</TechnicalCode>
<LegalText>
RCC source is hosted in the{' '}
<Anchor href={GITEA_REPOSITORY} target="_blank" rel="noreferrer">
Rosetta/rcc Gitea repository
</Anchor>
.{' '}
<Text component="span" className={classes.important}>
Pin the dependency version
</Text>
{' '}in the project lockfile so generated output remains reproducible.
</LegalText>
</LegalSection>
<LegalSection title="Generate from schemas, step by step">
<Step number={1} title="Create versioned schema layers">
<LegalText>
Put complete protocol snapshots in one directory and name them
<Code> layer.N.yml</Code>. Increment the top-level version, keep packet
and field IDs stable, and repeat definitions that remain part of the
protocol.
</LegalText>
<TechnicalCode>{`proto/
├── layer.1.yml
├── layer.2.yml
└── layer.3.yml`}</TechnicalCode>
</Step>
<Step number={2} title="Choose client or server output">
<LegalText>
Use <Code>client</Code> for the current application package. RCC selects
the highest schema version. Use <Code>server</Code> when the runtime must
decode connections negotiated on older supported layers. Full
multi-version codec generation is currently implemented by the Java
backend.
</LegalText>
</Step>
<Step number={3} title="Generate Java sources">
<LegalText>
Server generation combines all layers and emits version-aware codecs,
models, enums, and a registry with <Code>MIN_VERSION</Code> and
<Code> MAX_VERSION</Code>.
</LegalText>
<TechnicalCode>{`npx rcc \\
--mode server \\
--lang java \\
--protoDir ./proto \\
--outDir ./server/src/generated/java \\
--basePackage im.rosetta.network \\
--clean`}</TechnicalCode>
<LegalText>
For a Java client, change the mode and output directory. The generated
sources describe only the latest layer.
</LegalText>
<TechnicalCode>{`npx rcc \\
--mode client \\
--lang java \\
--protoDir ./proto \\
--outDir ./client/src/generated/java \\
--basePackage im.rosetta.network \\
--clean`}</TechnicalCode>
</Step>
<Step number={4} title="Generate TypeScript sources">
<LegalText>
TypeScript is the current latest-layer client target. It produces
models, enums, codecs, and <Code>RccGeneratedPacketRegistry.VERSION</Code>.
</LegalText>
<TechnicalCode>{`npx rcc \\
--mode client \\
--lang typescript \\
--protoDir ./proto \\
--outDir ./client/src/generated/protocol \\
--clean`}</TechnicalCode>
<Paper className={classes.note} radius="sm" p="md">
<Text size="sm" lh={1.7}>
Always run{' '}
<Code className={classes.important}>npx rcc --list-languages</Code>{' '}
before configuring a generator. The installed compiler&apos;s output is
the source of truth for available language targets.
</Text>
</Paper>
</Step>
<Step number={5} title="Run all targets from one prebuild config">
<LegalText>
Keep client and server generation together in
<Code> rcc-prebuild.yml</Code>. Paths are resolved relative to the
config file; <Code>clean: true</Code> clears only each configured output
directory.
</LegalText>
<TechnicalCode>{`watch: false
protoDir: ./proto
clean: true
compilers:
- name: TypeScript client
mode: client
lang: typescript
output: ./client/src/generated/protocol
- name: Java server
mode: server
lang: java
output: ./server/src/generated/java
basePackage: im.rosetta.network`}</TechnicalCode>
<TechnicalCode>{`npx rcc -f ./rcc-prebuild.yml`}</TechnicalCode>
</Step>
<Step number={6} title="Compile and package the generated sources">
<LegalText>
Add the output directory to the language build, then run generation
before compilation in local development and CI. RCC writes source
files; the surrounding Gradle, npm, or application build is responsible
for compiling and publishing the final server or client package.
</LegalText>
</Step>
</LegalSection>
<LegalSection title="CLI reference">
<TechnicalCode>{`npx rcc --mode <client|server> [options]
npx rcc -f <filename.yml>
npx rcc --list-languages
--mode, -m client | server
--lang, -l language returned by --list-languages
--protoDir, -p schema directory (default: ./proto)
--outDir, -o generated source directory (default: ./generated)
--basePackage, -b Java base package (default: im.rosetta.generated)
--clean clean the output directory before generation
--file, -f prebuild YAML config`}</TechnicalCode>
</LegalSection>
</LegalPage>
</>
);
}

View File

@@ -218,6 +218,60 @@ export function TermsPage() {
Phone: <Anchor href="tel:+85228910030">+852 2891 0030</Anchor> Phone: <Anchor href="tel:+85228910030">+852 2891 0030</Anchor>
</LegalText> </LegalText>
</LegalSection> </LegalSection>
<LegalSection title="20. Account Deletion" anchorId="account-deletion">
<LegalText>
You can permanently delete your Rosetta account in the app. This action
cannot be undone. Rosetta accounts use cryptographic identities, so no phone
number or email address is required to create or delete one.
</LegalText>
<LegalText>To delete the account currently signed in on an Android device:</LegalText>
<LegalList>
<List.Item>Open Rosetta and sign in to the account you want to remove.</List.Item>
<List.Item>Open Settings, then choose Safety.</List.Item>
<List.Item>Select Delete Account.</List.Item>
<List.Item>Review the confirmation and select Delete.</List.Item>
</LegalList>
<LegalText>
Deleting an account removes its local account record, message history,
conversations, cached profile media, and other account-scoped data from the
device where you complete the deletion. It also ends the signed-in session
for that account on that device.
</LegalText>
<LegalText>
Rosetta is designed so readable message history stays on users&apos; devices,
not as a readable server-side message archive. Encrypted data in transit is
processed only as needed to deliver the service. Deletion does not remove
messages, attachments, or other data that other people have already received
and stored on their own devices.
</LegalText>
<LegalText>
Limited technical logs and support communications may be retained only for
as long as reasonably necessary for security, reliability, support, legal,
and operational purposes, as described in our{' '}
<Anchor component={Link} to="/privacy">
Privacy Policy
</Anchor>
.
</LegalText>
<LegalText>
If you cannot sign in or need help with an account-deletion request, email{' '}
<Anchor href="mailto:admin@rosetta.im?subject=Rosetta%20Account%20Deletion%20Request">
admin@rosetta.im
</Anchor>
. Include the public account identifier, if available, and enough detail for
us to locate the request. We may ask you to verify control of the account or
its keys before acting on a request. Never send your private key or recovery
phrase by email or to support.
</LegalText>
<LegalText>
For help, contact{' '}
<Anchor href="mailto:support@rosetta.im?subject=Rosetta%20Account%20Deletion%20Help">
support@rosetta.im
</Anchor>
.
</LegalText>
</LegalSection>
</LegalPage> </LegalPage>
</> </>
); );

View File

@@ -1,3 +1,18 @@
function withFreshHtml(response) {
if (!response.headers.get('content-type')?.includes('text/html')) {
return response;
}
const headers = new Headers(response.headers);
headers.set('Cache-Control', 'no-cache');
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}
const worker = { const worker = {
async fetch(request, env) { async fetch(request, env) {
const response = await env.ASSETS.fetch(request); const response = await env.ASSETS.fetch(request);
@@ -7,11 +22,12 @@ const worker = {
request.method !== 'GET' || request.method !== 'GET' ||
!request.headers.get('accept')?.includes('text/html') !request.headers.get('accept')?.includes('text/html')
) { ) {
return response; return withFreshHtml(response);
} }
const indexUrl = new URL('/index.html', request.url); const indexUrl = new URL('/index.html', request.url);
return env.ASSETS.fetch(new Request(indexUrl, request)); const indexResponse = await env.ASSETS.fetch(new Request(indexUrl, request));
return withFreshHtml(indexResponse);
}, },
}; };