27 Commits

Author SHA1 Message Date
b96ad82cf7 Add account deletion terms section 2026-09-11 15:44:26 +05:00
5278509046 Исправленная документация 2026-08-01 13:03:59 -07:00
026f69b179 Окончательная документация для RCC 2026-08-01 12:39:01 -07:00
94bc883205 Добавлена докуметация по RCC 2026-08-01 07:58:52 -07:00
12f6384856 Фикс ошибок 2026-08-01 03:39:07 -07:00
bff945e701 Исправлен скролл к обьектам 2026-08-01 03:12:07 -07:00
354c8e5489 Мобильная адаптация, дока 2026-08-01 02:59:25 -07:00
gitea-actions[bot]
4bc65f71cf chore(proto): sync from rcc 39a3eb7 2026-08-01 02:40:03 -07:00
aea0b94598 Фикс 404 при обновелнии страницы 2026-07-31 06:30:45 -07:00
7af3c46bfe test 2026-07-31 06:25:50 -07:00
gitea-actions[bot]
e8c221bda4 chore(proto): sync from rcc 3d78f79 2026-07-31 06:18:26 -07:00
871c84cedd Новый лендинг 2026-07-31 06:06:07 -07:00
rosetta
8b2af4db62 Improve support and technical references 2026-07-31 05:06:37 -07:00
rosetta
33d4574583 Refine documentation and support experience 2026-07-31 04:32:34 -07:00
rosetta
179e2e65c6 Add encryption reference and refine documentation 2026-07-30 18:16:03 -07:00
rosetta
5d26ef6d89 Redesign landing and generate Proto docs 2026-07-30 17:49:58 -07:00
rosetta
b902298ad6 Align content pages with landing grid 2026-07-30 17:00:15 -07:00
rosetta
650d73830f Move support into React Router 2026-07-30 16:47:54 -07:00
rosetta
c81c3a1189 Add Sites worker entrypoint 2026-07-30 16:35:19 -07:00
rosetta
856fb60579 Move legal pages to React Router 2026-07-30 16:34:18 -07:00
4daf0ffba9 Merge pull request 'Обновить страницы поддержки и документов' (#5) from k1ngsterr1/landing:faq-animation into main
Reviewed-on: Rosetta/landing#5
2026-07-14 15:29:18 +00:00
f4bfc996df Обновить страницы поддержки и документов 2026-07-13 22:56:40 +05:00
10b7b33d89 Merge pull request 'Выровнять FAQ и анимировать раскрытие' (#4) from k1ngsterr1/landing:faq-animation into main
Reviewed-on: Rosetta/landing#4
2026-07-13 14:47:52 +00:00
8c3418488c Сделать FAQ одноколоночным 2026-07-13 10:02:13 +05:00
b1bf162d56 Убрать подчёркивание email поддержки 2026-07-13 07:50:33 +05:00
b722e6d952 Выровнять FAQ и анимировать раскрытие 2026-07-13 07:45:57 +05:00
b494f94a56 Merge pull request 'Добавить страницу поддержки Rosetta' (#3) from k1ngsterr1/landing:apple-support-page into main
Reviewed-on: Rosetta/landing#3
2026-07-13 01:07:51 +00:00
58 changed files with 10298 additions and 1873 deletions

3
.openai/hosting.json Normal file
View File

@@ -0,0 +1,3 @@
{
"project_id": "appgprj_6a6bdf07874c81919af23407be23ff1a"
}

View File

@@ -0,0 +1,37 @@
import { copyFile, mkdir } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
const root = process.cwd();
const outputDirectory = resolve(root, 'dist');
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 copyFile(
resolve(root, 'worker', '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'));
}),
);

View File

@@ -3,26 +3,23 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rosetta - Secure & Private Messaging | End-to-End Encrypted Chat</title> <title>Rosetta — Private Messaging</title>
<meta name="description" content="Rosetta is a secure, fast, and private messaging app with military-grade end-to-end encryption. Download for Windows, Linux, and Android. No phone number required." /> <meta name="description" content="Rosetta is private messaging built around strong encryption, local message history, and user-controlled identity." />
<meta name="keywords" content="secure messaging, encrypted chat, private messenger, end-to-end encryption, secure communication" /> <meta name="keywords" content="Rosetta, encrypted messaging, private messenger, secure chat, protocol documentation" />
<meta name="author" content="Rosetta Team" /> <meta name="author" content="Rosetta" />
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" /> <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="https://rosetta.im/" /> <meta property="og:url" content="https://rosetta.im/" />
<meta property="og:title" content="Rosetta - Secure Messaging for Everyone" /> <meta property="og:title" content="Rosetta — Private Messaging" />
<meta property="og:description" content="End-to-end encrypted messaging app with zero-knowledge architecture. Download for Windows, Linux, and Android." /> <meta property="og:description" content="Private messaging with strong encryption, local history, and user-controlled identity." />
<meta property="og:image" content="/og-image.png" />
<meta property="og:site_name" content="Rosetta" /> <meta property="og:site_name" content="Rosetta" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://rosetta.im/" /> <meta name="twitter:url" content="https://rosetta.im/" />
<meta name="twitter:title" content="Rosetta - Secure Messaging for Everyone" /> <meta name="twitter:title" content="Rosetta — Private Messaging" />
<meta name="twitter:description" content="End-to-end encrypted messaging app with zero-knowledge architecture." /> <meta name="twitter:description" content="Private messaging with strong encryption, local history, and user-controlled identity." />
<meta name="twitter:image" content="/og-image.png" />
<link rel="canonical" href="https://rosetta.im/" /> <link rel="canonical" href="https://rosetta.im/" />
<link rel="icon" href="favicon.ico" /> <link rel="icon" href="favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/rosetta-logo.png" />
<meta name="theme-color" content="#0066ff" /> <meta name="theme-color" content="#0066ff" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
<meta name="format-detection" content="telephone=no" /> <meta name="format-detection" content="telephone=no" />
@@ -31,7 +28,7 @@
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "SoftwareApplication", "@type": "SoftwareApplication",
"name": "Rosetta", "name": "Rosetta",
"description": "Secure end-to-end encrypted messaging application", "description": "Private messaging built around strong encryption, local message history, and user-controlled identity.",
"url": "https://rosetta.im", "url": "https://rosetta.im",
"applicationCategory": "CommunicationApplication", "applicationCategory": "CommunicationApplication",
"operatingSystem": ["Windows", "Linux", "Android"], "operatingSystem": ["Windows", "Linux", "Android"],
@@ -39,13 +36,7 @@
"@type": "Offer", "@type": "Offer",
"price": "0", "price": "0",
"priceCurrency": "USD" "priceCurrency": "USD"
}, }
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.8",
"ratingCount": "1500"
},
"image": "/og-image.png"
} }
</script> </script>
</head> </head>

44
known-issues.json Normal file
View File

@@ -0,0 +1,44 @@
[
{
"id": "lost-recovery-phrase",
"title": "Lost recovery phrase",
"summary": "The recovery phrase cannot be restored by Rosetta.",
"solution": "Rosetta does not keep a copy of your recovery phrase or private key. If another device is still signed in, secure any data you need and create a new account before access is lost. Never send a recovery phrase to support.",
"keywords": ["lost", "recovery", "phrase", "seed", "restore", "account", "login"]
},
{
"id": "new-device-confirmation",
"title": "New device requires confirmation",
"summary": "A restored account may require approval before the new device is trusted.",
"solution": "Keep the existing Rosetta device online and approve the new device when prompted. Check that the recovery phrase was entered in the original order. If no trusted device remains available, contact support with the client version and operating system, but never include private keys.",
"keywords": ["new device", "confirmation", "approve", "trusted", "restore", "handshake", "login"]
},
{
"id": "attachment-stuck",
"title": "Attachment remains in sending state",
"summary": "Large media can remain pending when the upload or connection is interrupted.",
"solution": "Keep Rosetta open and connected until the transfer finishes. Confirm that the file is still available locally, retry the message, and verify that the current client version is installed. Include the attachment type and approximate size when contacting support.",
"keywords": ["attachment", "file", "photo", "video", "upload", "sending", "stuck", "pending", "media"]
},
{
"id": "missing-media-permission",
"title": "Photos, files, or microphone are unavailable",
"summary": "The operating system may have denied a required permission.",
"solution": "Open the operating-system privacy settings for Rosetta and enable access to Photos, Files, or Microphone as needed. Restart Rosetta after changing permissions and select the attachment again.",
"keywords": ["permission", "microphone", "photo", "camera", "file", "voice", "denied", "unavailable"]
},
{
"id": "message-not-delivered",
"title": "Message is not delivered",
"summary": "Delivery can be delayed by connectivity, client state, or an unavailable recipient.",
"solution": "Check the network connection and confirm that Rosetta reports a connected state. Leave the application open briefly and retry once. If the problem continues, provide support with the sender platform, recipient platform, approximate time, and client version. Do not include message content or encryption material.",
"keywords": ["message", "deliver", "delivered", "waiting", "error", "offline", "connection", "send"]
},
{
"id": "security-report",
"title": "Report abuse or a security issue",
"summary": "Security reports should contain reproducible technical details without secrets.",
"solution": "Email support@rosetta.im with the affected platform, client version, expected behavior, actual behavior, and reproduction steps. Never include a recovery phrase, private key, decrypted private messages, or another person's personal information.",
"keywords": ["security", "abuse", "report", "vulnerability", "privacy", "spam", "scam"]
}
]

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;
}
}

79
package-lock.json generated
View File

@@ -17,7 +17,8 @@
"events": "^3.3.0", "events": "^3.3.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-icons": "^5.5.0" "react-icons": "^5.5.0",
"react-router-dom": "^7.18.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
@@ -36,7 +37,8 @@
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.46.4", "typescript-eslint": "^8.46.4",
"vite": "^7.2.4" "vite": "^7.2.4",
"yaml": "^2.9.0"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@@ -2132,6 +2134,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3286,6 +3301,44 @@
} }
} }
}, },
"node_modules/react-router": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
"integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz",
"integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==",
"license": "MIT",
"dependencies": {
"react-router": "7.18.2"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-style-singleton": { "node_modules/react-style-singleton": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
@@ -3393,6 +3446,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3831,6 +3890,22 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -4,8 +4,9 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "generate:proto": "node scripts/generate-proto-docs.mjs",
"build": "tsc -b && vite build", "dev": "npm run generate:proto && vite",
"build": "npm run generate:proto && tsc -b && vite build && node build/prepare-sites-build.mjs",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },
@@ -19,7 +20,8 @@
"events": "^3.3.0", "events": "^3.3.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-icons": "^5.5.0" "react-icons": "^5.5.0",
"react-router-dom": "^7.18.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
@@ -38,6 +40,7 @@
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.46.4", "typescript-eslint": "^8.46.4",
"vite": "^7.2.4" "vite": "^7.2.4",
"yaml": "^2.9.0"
} }
} }

529
proto/layer.1.yml Normal file
View File

@@ -0,0 +1,529 @@
version: 1
enums:
#Type of attachment
AttachmentType:
type: int8
values:
- id: 0
name: IMAGE #Image
- id: 1
name: MESSAGES #Reply/Forwarded messages
- id: 2
name: FILE #File
- id: 3
name: AVATAR #Avatar
- id: 4
name: CALL #Call
- id: 5
name: VOICE #Voice message
- id: 6
name: VIDEO_CIRCLE #Video circle
#Solution by device after accept or decline request to attach new device
DeviceSolution:
type: int8
values:
- id: 0
name: ACCEPT #Accept device to log in to account
- id: 1
name: DECLINE #Decline device
#Handshake Stage (need for set device confirm view after pass seed)
HandshakeStage:
type: int8
values:
- id: 0
name: COMPLETED
- id: 1
name: NEED_DEVICE_VERIFICATION
GroupStatus:
type: int8
values:
- id: 0
name: JOINED
- id: 1
name: INVALID
- id: 2
name: NOT_JOINED
- id: 3
name: BANNED
NotificationAction:
type: int8
values:
- id: 0
name: SUBSCRIBE
- id: 1
name: UNSUBSCRIBE
SignalType:
type: int8
values:
- id: 0
name: CALL
- id: 1
name: KEY_EXCHANGE
- id: 2
name: ACTIVE_CALL
- id: 3
name: END_CALL
- id: 4
name: ACTIVE
- id: 5
name: END_CALL_BECAUSE_PEER_DISCONNECTED
- id: 6
name: END_CALL_BECAUSE_BUSY
- id: 7
name: ACCEPT
- id: 8
name: RINGING_TIMEOUT
NetworkStatus:
type: int8
values:
- id: 0
name: ONLINE
- id: 1
name: OFFLINE
SyncStatus:
type: int8
values:
- id: 0
name: NOT_NEEDED
- id: 1
name: BATCH_START
- id: 2
name: BATCH_END
WebRTCType:
type: int8
values:
- id: 0
name: OFFER
- id: 1
name: ANSWER
- id: 2
name: ICE_CANDIDATE
ResultCode:
type: int8
values:
- id: 0
name: SUCCESS
- id: 1
name: ERROR
- id: 2
name: INVALID
- id: 3
name: USERNAME_TAKEN
TokenType:
type: int8
values:
- id: 0
name: FCM
- id: 1
name: VoIPApns
types:
Attachment:
fields:
- id: 1
name: id
type: string
- id: 2
name: blob?
type: string
- id: 3
name: type
type: AttachmentType
- id: 4
name: preview
type: string
- id: 5
name: transport
type: AttachmentTransport
AttachmentTransport:
fields:
- id: 1
name: transportTag
type: string
- id: 2
name: transportServer
type: string
NetworkDevice:
fields:
- id: 1
name: networkStatus
type: NetworkStatus
- id: 2
name: deviceName
type: string
- id: 3
name: deviceOs
type: string
- id: 4
name: deviceId
type: string
- id: 5
name: deviceSolution
type: DeviceSolution
PKNetworkStatus:
fields:
- id: 1
name: publicKey
type: string
- id: 2
name: networkStatus
type: NetworkStatus
SearchInfo:
fields:
- id: 1
name: username
type: string
- id: 2
name: title
type: string
- id: 3
name: publicKey
type: string
- id: 4
name: verified
type: int32
- id: 5
name: networkStatus
type: NetworkStatus
RTCIceServer:
fields:
- id: 1
name: url
type: string
- id: 2
name: username
type: string
- id: 3
name: credential
type: string
- id: 4
name: transport
type: string
packets:
Handshake:
id: 0
fields:
- id: 1
name: publicKey
type: string
- id: 2
name: privateKey
type: string
- id: 3
name: deviceId
type: string
- id: 4
name: deviceName
type: string
- id: 5
name: deviceOs
type: string
- id: 6
name: handshakeStage?
type: HandshakeStage
UserInfo:
id: 1
fields:
- id: 1
name: privateKey
type: string
- id: 2
name: username
type: string
- id: 3
name: title
type: string
Result:
id: 2
fields:
- id: 1
name: resultCode
type: ResultCode
Search:
id: 3
fields:
- id: 1
name: privateKey
type: string
- id: 2
name: search
type: string
- id: 3
name: searchInfo?
type: SearchInfo[]
OnlineSubscribe:
id: 4
fields:
- id: 1
name: privateKey
type: string
- id: 2
name: publicKeys
type: string[]
OnlineState:
id: 5
fields:
- id: 1
name: pkNetworkStatuses
type: PKNetworkStatus[]
Message:
id: 6
fields:
- id: 1
name: fromPublicKey
type: string
- id: 2
name: toPublicKey
type: string
- id: 3
name: privateKey
type: string
- id: 4
name: content
type: string
- id: 5
name: chachaKey
type: string
- id: 6
name: timestamp
type: int64
- id: 7
name: messageId
type: string
- id: 8
name: attachments?
type: Attachment[]
- id: 9
name: aesChachaKey
type: string
Read:
id: 7
fields:
- id: 1
name: fromPublicKey
type: string
- id: 2
name: toPublicKey
type: string
- id: 3
name: privateKey
type: string
Delivery:
id: 8
fields:
- id: 1
name: messageId
type: string
- id: 2
name: toPublicKey
type: string
DeviceNew:
id: 9
fields:
- id: 1
name: ipAddress
type: string
- id: 2
name: deviceId
type: string
- id: 3
name: deviceName
type: string
- id: 4
name: deviceOs
type: string
RequestUpdate:
id: 10
fields:
- id: 1
name: server?
type: string
Typeing:
id: 11
fields:
- id: 1
name: fromPublicKey
type: string
- id: 2
name: toPublicKey
type: string
- id: 3
name: privateKey
type: string
RequestTransport:
id: 15
fields:
- id: 1
name: server?
type: string
PushNotification:
id: 16
fields:
- id: 1
name: notificationToken
type: string
- id: 2
name: action
type: NotificationAction
- id: 3
name: tokenType
type: TokenType
- id: 4
name: deviceId
type: string
GroupCreate:
id: 17
fields:
- id: 1
name: groupId
type: string
GroupInfo:
id: 18
fields:
- id: 1
name: groupId
type: string
- id: 2
name: membersPKs?
type: string[]
GroupInviteInfo:
id: 19
fields:
- id: 1
name: groupId
type: string
- id: 2
name: membersCount?
type: int32
- id: 3
name: status?
type: GroupStatus
GroupJoin:
id: 20
fields:
- id: 1
name: groupId
type: string
- id: 2
name: status?
type: GroupStatus
- id: 3
name: groupString
type: string
GroupLeave:
id: 21
fields:
- id: 1
name: groupId
type: string
GroupBan:
id: 22
fields:
- id: 1
name: groupId
type: string
- id: 2
name: publicKey
type: string
DeviceList:
id: 23
fields:
- id: 1
name: devices
type: NetworkDevice[]
DeviceResolve:
id: 24
fields:
- id: 1
name: deviceId
type: string
- id: 2
name: solution
type: DeviceSolution
Sync:
id: 25
fields:
- id: 1
name: syncStatus
type: SyncStatus
- id: 2
name: timestamp
type: int64
SignalPeer:
id: 26
fields:
- id: 1
name: src
type: string
- id: 2
name: dst
type: string
- id: 3
name: sharedPublic
type: string
- id: 4
name: signalType
type: SignalType
- id: 5
name: callId
type: string
- id: 6
name: joinToken
type: string
WebRTC:
id: 27
fields:
- id: 1
name: sdpOrCandidate
type: string
- id: 2
name: type
type: WebRTCType
IceServers:
id: 28
fields:
- id: 1
name: iceServers?
type: RTCIceServer[]

527
proto/layer.2.yml Normal file
View File

@@ -0,0 +1,527 @@
version: 2
description: Remove avatars as attachments, add video
enums:
AttachmentType:
type: int8
values:
- id: 0
name: IMAGE
- id: 1
name: MESSAGES
- id: 2
name: FILE
- id: 4
name: CALL
- id: 5
name: VOICE
- id: 6
name: VIDEO_CIRCLE
- id: 7
name: VIDEO
DeviceSolution:
type: int8
values:
- id: 0
name: ACCEPT
- id: 1
name: DECLINE
HandshakeStage:
type: int8
values:
- id: 0
name: COMPLETED
- id: 1
name: NEED_DEVICE_VERIFICATION
GroupStatus:
type: int8
values:
- id: 0
name: JOINED
- id: 1
name: INVALID
- id: 2
name: NOT_JOINED
- id: 3
name: BANNED
NotificationAction:
type: int8
values:
- id: 0
name: SUBSCRIBE
- id: 1
name: UNSUBSCRIBE
SignalType:
type: int8
values:
- id: 0
name: CALL
- id: 1
name: KEY_EXCHANGE
- id: 2
name: ACTIVE_CALL
- id: 3
name: END_CALL
- id: 4
name: ACTIVE
- id: 5
name: END_CALL_BECAUSE_PEER_DISCONNECTED
- id: 6
name: END_CALL_BECAUSE_BUSY
- id: 7
name: ACCEPT
- id: 8
name: RINGING_TIMEOUT
NetworkStatus:
type: int8
values:
- id: 0
name: ONLINE
- id: 1
name: OFFLINE
SyncStatus:
type: int8
values:
- id: 0
name: NOT_NEEDED
- id: 1
name: BATCH_START
- id: 2
name: BATCH_END
WebRTCType:
type: int8
values:
- id: 0
name: OFFER
- id: 1
name: ANSWER
- id: 2
name: ICE_CANDIDATE
ResultCode:
type: int8
values:
- id: 0
name: SUCCESS
- id: 1
name: ERROR
- id: 2
name: INVALID
- id: 3
name: USERNAME_TAKEN
TokenType:
type: int8
values:
- id: 0
name: FCM
- id: 1
name: VoIPApns
types:
Attachment:
fields:
- id: 1
name: id
type: string
- id: 2
name: blob?
type: string
- id: 3
name: type
type: AttachmentType
- id: 4
name: preview
type: string
- id: 5
name: transport
type: AttachmentTransport
AttachmentTransport:
fields:
- id: 1
name: transportTag
type: string
- id: 2
name: transportServer
type: string
NetworkDevice:
fields:
- id: 1
name: networkStatus
type: NetworkStatus
- id: 2
name: deviceName
type: string
- id: 3
name: deviceOs
type: string
- id: 4
name: deviceId
type: string
- id: 5
name: deviceSolution
type: DeviceSolution
PKNetworkStatus:
fields:
- id: 1
name: publicKey
type: string
- id: 2
name: networkStatus
type: NetworkStatus
SearchInfo:
fields:
- id: 1
name: username
type: string
- id: 2
name: title
type: string
- id: 3
name: publicKey
type: string
- id: 4
name: verified
type: int32
- id: 5
name: networkStatus
type: NetworkStatus
RTCIceServer:
fields:
- id: 1
name: url
type: string
- id: 2
name: username
type: string
- id: 3
name: credential
type: string
- id: 4
name: transport
type: string
packets:
Handshake:
id: 0
fields:
- id: 1
name: publicKey
type: string
- id: 2
name: privateKey
type: string
- id: 3
name: deviceId
type: string
- id: 4
name: deviceName
type: string
- id: 5
name: deviceOs
type: string
- id: 6
name: handshakeStage?
type: HandshakeStage
UserInfo:
id: 1
fields:
- id: 1
name: privateKey
type: string
- id: 2
name: username
type: string
- id: 3
name: title
type: string
Result:
id: 2
fields:
- id: 1
name: resultCode
type: ResultCode
Search:
id: 3
fields:
- id: 1
name: privateKey
type: string
- id: 2
name: search
type: string
- id: 3
name: searchInfo?
type: SearchInfo[]
OnlineSubscribe:
id: 4
fields:
- id: 1
name: privateKey
type: string
- id: 2
name: publicKeys
type: string[]
OnlineState:
id: 5
fields:
- id: 1
name: pkNetworkStatuses
type: PKNetworkStatus[]
Message:
id: 6
fields:
- id: 1
name: fromPublicKey
type: string
- id: 2
name: toPublicKey
type: string
- id: 3
name: privateKey
type: string
- id: 4
name: content
type: string
- id: 5
name: chachaKey
type: string
- id: 6
name: timestamp
type: int64
- id: 7
name: messageId
type: string
- id: 8
name: attachments?
type: Attachment[]
- id: 9
name: aesChachaKey
type: string
Read:
id: 7
fields:
- id: 1
name: fromPublicKey
type: string
- id: 2
name: toPublicKey
type: string
- id: 4
name: timestamp
type: int64
Delivery:
id: 8
fields:
- id: 1
name: messageId
type: string
- id: 2
name: toPublicKey
type: string
DeviceNew:
id: 9
fields:
- id: 1
name: ipAddress
type: string
- id: 2
name: deviceId
type: string
- id: 3
name: deviceName
type: string
- id: 4
name: deviceOs
type: string
RequestUpdate:
id: 10
fields:
- id: 1
name: server?
type: string
Typeing:
id: 11
fields:
- id: 1
name: fromPublicKey
type: string
- id: 2
name: toPublicKey
type: string
- id: 3
name: privateKey
type: string
RequestTransport:
id: 15
fields:
- id: 1
name: server?
type: string
PushNotification:
id: 16
fields:
- id: 1
name: notificationToken
type: string
- id: 2
name: action
type: NotificationAction
- id: 3
name: tokenType
type: TokenType
- id: 4
name: deviceId
type: string
GroupCreate:
id: 17
fields:
- id: 1
name: groupId?
type: string
GroupInfo:
id: 18
fields:
- id: 1
name: groupId
type: string
- id: 2
name: membersPKs?
type: string[]
GroupInviteInfo:
id: 19
fields:
- id: 1
name: groupId
type: string
- id: 2
name: membersCount?
type: int32
- id: 3
name: status?
type: GroupStatus
GroupJoin:
id: 20
fields:
- id: 1
name: groupId
type: string
- id: 2
name: status?
type: GroupStatus
- id: 3
name: groupString
type: string
GroupLeave:
id: 21
fields:
- id: 1
name: groupId
type: string
GroupBan:
id: 22
fields:
- id: 1
name: groupId
type: string
- id: 2
name: publicKey
type: string
DeviceList:
id: 23
fields:
- id: 1
name: devices
type: NetworkDevice[]
DeviceResolve:
id: 24
fields:
- id: 1
name: deviceId
type: string
- id: 2
name: solution
type: DeviceSolution
Sync:
id: 25
fields:
- id: 1
name: syncStatus
type: SyncStatus
- id: 2
name: timestamp
type: int64
SignalPeer:
id: 26
fields:
- id: 1
name: src
type: string
- id: 2
name: dst
type: string
- id: 3
name: sharedPublic?
type: string
- id: 4
name: signalType
type: SignalType
- id: 5
name: callId?
type: string
- id: 6
name: joinToken?
type: string
WebRTC:
id: 27
fields:
- id: 1
name: sdpOrCandidate
type: string
- id: 2
name: type
type: WebRTCType
IceServers:
id: 28
fields:
- id: 1
name: iceServers?
type: RTCIceServer[]

656
proto/layer.3.yml Normal file
View File

@@ -0,0 +1,656 @@
version: 3
description: Add avatars and more CDN safety, remove transport server
enums:
#тип вложния
AttachmentType:
type: int8
values:
- id: 0
name: IMAGE #изображение
- id: 1
name: MESSAGES #сообщения (forwarded/reply)
- id: 2
name: FILE #файл
- id: 4
name: CALL #звонок
- id: 5
name: VOICE #голосовое сообщение
- id: 6
name: VIDEO_CIRCLE #видео сообщение (кружок)
- id: 7
name: VIDEO #видео
#решение по девайсу, принять или отклонить вход
DeviceSolution:
type: int8
values:
- id: 0
name: ACCEPT #принять
- id: 1
name: DECLINE #отклонить
#статус хэндшейка
HandshakeStage:
type: int8
values:
- id: 0
name: COMPLETED #пройден
- id: 1
name: NEED_DEVICE_VERIFICATION #не пройден (приложение рендерит UI подтвердите с другого устройства)
#статус в группе
GroupStatus:
type: int8
values:
- id: 0
name: JOINED #в группе
- id: 1
name: INVALID #не валидная группа
- id: 2
name: NOT_JOINED #можно зайт
- id: 3
name: BANNED #заблокирован
NotificationAction:
type: int8
values:
- id: 0
name: SUBSCRIBE
- id: 1
name: UNSUBSCRIBE
SignalType:
type: int8
values:
- id: 0
name: CALL
- id: 1
name: KEY_EXCHANGE
- id: 2
name: ACTIVE_CALL
- id: 3
name: END_CALL
- id: 4
name: ACTIVE
- id: 5
name: END_CALL_BECAUSE_PEER_DISCONNECTED
- id: 6
name: END_CALL_BECAUSE_BUSY
- id: 7
name: ACCEPT
- id: 8
name: RINGING_TIMEOUT
NetworkStatus:
type: int8
values:
- id: 0
name: ONLINE
- id: 1
name: OFFLINE
SyncStatus:
type: int8
values:
- id: 0
name: NOT_NEEDED
- id: 1
name: BATCH_START
- id: 2
name: BATCH_END
#Тип WebRTC сигнала
WebRTCType:
type: int8
values:
- id: 0
name: OFFER #Offer
- id: 1
name: ANSWER #Answer
- id: 2
name: ICE_CANDIDATE #Estabilishment candidate
ResultCode:
type: int8
values:
- id: 0
name: SUCCESS
- id: 1
name: ERROR
- id: 2
name: INVALID #Любая ошибка смысл которой зависит от контекста, например в случае с файловой системой - нет файла
- id: 3
name: USERNAME_TAKEN #Имя пользователя занято
- id: 4
name: OVERSIZE #Слишком большой размер файла
- id: 5
name: OVERLIFE #Слишком большой TTL
TokenType:
type: int8
values:
- id: 0
name: FCM
- id: 1
name: VoIPApns
#Тип медиа файла профиля (пока только фото)
ProfileMediaType:
type: uint8
values:
- id: 0
name: PROFILE_PHOTO
types:
Attachment:
fields:
- id: 1
name: id #Случайная строка
type: string
- id: 2
name: blob? #Только очень коротких типов
type: string
- id: 3
name: type #Тип вложения
type: AttachmentType
- id: 4
name: preview #Превью
type: string
- id: 6
name: objectId #Выданный сервером objectId от cdn
type: string
#Медиа-файлы профиля такие как аватарки (фото) и будущие истории
ProfileMedia:
fields:
- id: 1
name: preview #превью (для фото blurhash)
type: string
- id: 2
name: objectId #обьект на cdn
type: string
- id: 3
name: type #тип медиа
type: ProfileMediaType
- id: 4
name: timestamp #дата публикации
type: int64
- id: 5
name: mediaKey #ключ шифрования от медиа
type: string
NetworkDevice:
fields:
- id: 1
name: networkStatus
type: NetworkStatus
- id: 2
name: deviceName
type: string
- id: 3
name: deviceOs
type: string
- id: 4
name: deviceId
type: string
- id: 5
name: deviceSolution
type: DeviceSolution
PKNetworkStatus:
fields:
- id: 1
name: publicKey
type: string
- id: 2
name: networkStatus
type: NetworkStatus
#Информация при поиске пользователя (приходит в ответ на пакет PacketSearch)
SearchInfo:
fields:
- id: 1
name: username #имя для поиска
type: string
- id: 2
name: title #отображаемое имя пользователя
type: string
- id: 3
name: publicKey #публичный ключ
type: string
- id: 4
name: verified #верификация
type: int32
- id: 5
name: networkStatus #онлайн пользователь или нет
type: NetworkStatus
- id: 6
name: avatar? #аватар пользователя если есть
type: ProfileMedia
#Информация которая используется только при входе в профиль пользователя
ProfileInfo:
fields:
- id: 1
name: username #имя для поиска
type: string
- id: 2
name: title #отображаемое имя пользователя
type: string
- id: 3
name: publicKey #публичный ключ
type: string
- id: 4
name: verified #верификация
type: int32
- id: 5
name: networkStatus #онлайн пользователь или нет
type: NetworkStatus
- id: 6
name: media? #все активное медиа (включая будущие истории)
type: ProfileMedia[]
RTCIceServer:
fields:
- id: 1
name: url
type: string
- id: 2
name: username
type: string
- id: 3
name: credential
type: string
- id: 4
name: transport
type: string
packets:
Handshake:
id: 0
fields:
- id: 1
name: publicKey
type: string
- id: 2
name: privateKey
type: string
- id: 3
name: deviceId
type: string
- id: 4
name: deviceName
type: string
- id: 5
name: deviceOs
type: string
- id: 6
name: handshakeStage?
type: HandshakeStage
UserInfo:
id: 1
fields:
- id: 1
name: privateKey
type: string
- id: 2
name: username
type: string
- id: 3
name: title
type: string
Result:
id: 2
fields:
- id: 1
name: resultCode
type: ResultCode
Search:
id: 3
fields:
- id: 1
name: privateKey
type: string
- id: 2
name: search
type: string
- id: 3
name: searchInfo?
type: SearchInfo[]
OnlineSubscribe:
id: 4
fields:
- id: 1
name: privateKey
type: string
- id: 2
name: publicKeys
type: string[]
OnlineState:
id: 5
fields:
- id: 1
name: pkNetworkStatuses
type: PKNetworkStatus[]
Message:
id: 6
fields:
- id: 1
name: fromPublicKey
type: string
- id: 2
name: toPublicKey
type: string
- id: 3
name: privateKey
type: string
- id: 4
name: content
type: string
- id: 5
name: chachaKey
type: string
- id: 6
name: timestamp
type: int64
- id: 7
name: messageId
type: string
- id: 8
name: attachments?
type: Attachment[]
- id: 9
name: aesChachaKey
type: string
Read:
id: 7
fields:
- id: 1
name: fromPublicKey
type: string
- id: 2
name: toPublicKey
type: string
- id: 4
name: timestamp
type: int64
Delivery:
id: 8
fields:
- id: 1
name: messageId
type: string
- id: 2
name: toPublicKey
type: string
DeviceNew:
id: 9
fields:
- id: 1
name: ipAddress
type: string
- id: 2
name: deviceId
type: string
- id: 3
name: deviceName
type: string
- id: 4
name: deviceOs
type: string
RequestUpdate:
id: 10
fields:
- id: 1
name: server?
type: string
Typeing:
id: 11
fields:
- id: 1
name: fromPublicKey
type: string
- id: 2
name: toPublicKey
type: string
- id: 3
name: privateKey
type: string
#Запрос транспортного сервера
RequestTransport:
id: 15
fields:
- id: 1 #сервер > клиент
name: server? #выбранный транспортный сервер
type: string
PushNotification:
id: 16
fields:
- id: 1
name: notificationToken
type: string
- id: 2
name: action
type: NotificationAction
- id: 3
name: tokenType
type: TokenType
- id: 4
name: deviceId
type: string
GroupCreate:
id: 17
fields:
- id: 1
name: groupId?
type: string
GroupInfo:
id: 18
fields:
- id: 1
name: groupId
type: string
- id: 2
name: membersPKs?
type: string[]
GroupInviteInfo:
id: 19
fields:
- id: 1
name: groupId
type: string
- id: 2
name: membersCount?
type: int32
- id: 3
name: status?
type: GroupStatus
GroupJoin:
id: 20
fields:
- id: 1
name: groupId
type: string
- id: 2
name: status?
type: GroupStatus
- id: 3
name: groupString
type: string
GroupLeave:
id: 21
fields:
- id: 1
name: groupId
type: string
GroupBan:
id: 22
fields:
- id: 1
name: groupId
type: string
- id: 2
name: publicKey
type: string
DeviceList:
id: 23
fields:
- id: 1
name: devices
type: NetworkDevice[]
DeviceResolve:
id: 24
fields:
- id: 1
name: deviceId
type: string
- id: 2
name: solution
type: DeviceSolution
Sync:
id: 25
fields:
- id: 1
name: syncStatus
type: SyncStatus
- id: 2
name: timestamp
type: int64
SignalPeer:
id: 26
fields:
- id: 1
name: src
type: string
- id: 2
name: dst
type: string
- id: 3
name: sharedPublic?
type: string
- id: 4
name: signalType
type: SignalType
- id: 5
name: callId?
type: string
- id: 6
name: joinToken?
type: string
WebRTC:
id: 27
fields:
- id: 1
name: sdpOrCandidate
type: string
- id: 2
name: type
type: WebRTCType
IceServers:
id: 28
fields:
- id: 1
name: iceServers?
type: RTCIceServer[]
#Запрос "посадочного места" у сервера при загрузке файла
UploadCandidate:
id: 29
fields:
- id: 1 #клиент > сервер
name: size #размер загружаемого файла (декларируемый)
type: uint32
- id: 2 #сервер > клиент
name: objectId? #выделенный сервером objectId
type: string
- id: 3 #сервер > клиент
name: chunkLength? #размер чанка
type: uint32
- id: 4 #сервер > клиент
name: chunks? #количество чанков
type: uint32
- id: 5 #сервер > клиент
name: host? #хост, где сохранен файл
type: string
- id: 6 #клиент > сервер
name: ttl? #ttl в секундах, после которого файл будет удален с CDN и с таблицы, по умолчанию 7 дней, не допускается использование слишком больших значений
type: uint32
#Запрос скачивания файла с сервера, нужно передать objectId, а дальнейшую информацию вернет сервер
DownloadCandidate:
id: 30
fields:
- id: 1 #сервер > клиент
name: size? #размер файла
type: uint32
- id: 2 #клиент > сервер
name: objectId #выделенный сервером objectId
type: string
- id: 3 #сервер > клиент
name: chunkLength? #размер чанка
type: uint32
- id: 4 #сервер > клиент
name: chunks? #количество чанков
type: uint32
- id: 5 #сервер > клиент
name: host? #сервер с которого скачивать файл
type: string
#Установить аватар профиля
SetAvatar:
id: 31
fields:
- id: 1 #клиент > сервер
name: objectId #выделенный сервером objectId при отправке пакета UploadCandidate
type: string
- id: 2 #клиент > сервер
name: blurhash #превью аватара, строка blurhash
type: string
- id: 3 #клиент > сервер
name: mediaKey #ключ шифрования
type: string
#Удаляет аватар или фотографию из профиля
RemoveAvatar:
id: 32
fields:
- id: 1 #клиент > сервер
name: objectId #objectId аватара, аватар или фотография профиля должна при этом стоять у профиля отправителя пакета
type: string
#Запрос профиля
Profile:
id: 33
fields:
- id: 1 #клиент > сервер
name: publicKey
type: string
- id: 2 #сервер > клиент
name: info?
type: ProfileInfo

View File

@@ -1,231 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Privacy Policy for Rosetta</title>
<meta
name="description"
content="Privacy Policy for Rosetta by ROSETTA CLOUD SERVICES LIMITED: how data is handled in the end-to-end encrypted messenger."
/>
<link rel="canonical" href="https://rosetta.im/privacy/" />
<style>
:root {
color-scheme: light dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.6;
color: #1f2937;
background: #ffffff;
}
body {
margin: 0;
background: linear-gradient(180deg, rgba(0, 102, 255, 0.04), rgba(255, 255, 255, 0));
}
main {
width: min(880px, calc(100% - 40px));
margin: 0 auto;
padding: 56px 0 72px;
}
a {
color: #0066ff;
}
h1 {
margin: 0 0 8px;
font-size: clamp(32px, 5vw, 48px);
line-height: 1.1;
}
h2 {
margin-top: 36px;
font-size: 22px;
line-height: 1.25;
}
p,
li {
font-size: 16px;
}
.muted {
color: #6b7280;
}
.back {
display: inline-block;
margin-bottom: 28px;
text-decoration: none;
}
.address {
white-space: pre-line;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f3f4f6;
background: #101113;
}
body {
background: linear-gradient(180deg, rgba(0, 102, 255, 0.12), rgba(16, 17, 19, 0));
}
.muted {
color: #a1a1aa;
}
}
</style>
</head>
<body>
<main>
<a class="back" href="/">Back to home</a>
<h1>Privacy Policy for Rosetta</h1>
<p class="muted">Last updated: May 22, 2026</p>
<p>
Rosetta IM is operated by ROSETTA CLOUD SERVICES LIMITED ("Rosetta",
"we", "us", or "our"). This Privacy Policy explains how we handle
information when you use Rosetta, our end-to-end encrypted messaging
application, and our website at https://rosetta.im.
</p>
<h2>1. Information We Collect</h2>
<p>
Rosetta is designed to minimize data collection. Registration does not
require a phone number, email address, or real name. The app uses
cryptographic keys and technical identifiers needed to operate the
service.
</p>
<ul>
<li>
Account and cryptographic data: public keys, account identifiers, and
related technical data needed to identify users and deliver encrypted
messages. Private keys are intended to remain on your device.
</li>
<li>
Encrypted messages and media in transit: message text, files, and
media are encrypted before they are sent. Our servers are designed to
relay encrypted payloads and encrypted message keys, not to read
message contents.
</li>
<li>
Device and app data: app version, operating system, platform,
architecture, update request data, IP address, timestamps, and server
logs needed for security, reliability, abuse prevention, and update
delivery.
</li>
<li>
Support communications: if you contact us, we may process your email
address, message content, and any information you choose to provide.
</li>
</ul>
<h2>2. Information We Do Not Collect for Advertising</h2>
<p>
We do not use third-party advertising SDKs, advertising trackers, or
cross-app tracking. We do not sell your personal information.
</p>
<h2>3. How We Use Information</h2>
<p>We use information only to:</p>
<ul>
<li>provide, maintain, and secure Rosetta;</li>
<li>route encrypted messages between users;</li>
<li>deliver app updates and downloads;</li>
<li>detect abuse, prevent fraud, and protect service integrity;</li>
<li>respond to support requests;</li>
<li>comply with legal obligations.</li>
</ul>
<h2>4. End-to-End Encryption</h2>
<p>
Rosetta is designed so that message content is encrypted before it
reaches our servers. Our servers are not intended to have access to
decrypted message contents or decrypted media. Messages are stored on
users' devices rather than as readable message history on our servers.
</p>
<h2>5. Sharing of Information</h2>
<p>We may share limited information only with:</p>
<ul>
<li>infrastructure and hosting providers that help us operate Rosetta;</li>
<li>professional advisers, if necessary;</li>
<li>
authorities or other parties when required by law or to protect rights,
safety, and security.
</li>
</ul>
<p>
Any service provider that processes user data for us must protect it
consistently with this Privacy Policy.
</p>
<h2>6. Retention</h2>
<p>
Encrypted message data in transit is processed only as needed to deliver
the service. Technical logs and support communications are retained only
for as long as reasonably necessary for security, reliability, support,
legal, and operational purposes.
</p>
<h2>7. Your Choices and Rights</h2>
<p>
Depending on where you live, you may have rights to access, correct,
delete, or object to certain processing of your personal information. To
make a request, contact us at
<a href="mailto:admin@rosetta.im">admin@rosetta.im</a>.
</p>
<p>
Because Rosetta minimizes account data and uses cryptographic
identifiers, some requests may require you to provide information that
helps us verify control of the relevant account or keys.
</p>
<h2>8. Children</h2>
<p>
Rosetta is not directed to children under 13, or the minimum age required
in your jurisdiction. If we learn that we have collected personal
information from a child without required consent, we will take
appropriate steps to delete it.
</p>
<h2>9. International Transfers</h2>
<p>
ROSETTA CLOUD SERVICES LIMITED may process information in countries other
than where you live. Where required, we use appropriate safeguards for
international transfers.
</p>
<h2>10. Security</h2>
<p>
We use technical and organizational measures designed to protect
information. No system is perfectly secure, and users are responsible for
protecting their devices, private keys, and account access.
</p>
<h2>11. Changes to This Policy</h2>
<p>
We may update this Privacy Policy from time to time. If we make material
changes, we will update the "Last updated" date and, where appropriate,
provide additional notice.
</p>
<h2>12. Contact</h2>
<p class="address">ROSETTA CLOUD SERVICES LIMITED
Registered address:
UNIT 704C 7/F BLK 3 NAN FUNG IND CITY
18 TIN HAU RD
TUEN MUN
HONG KONG</p>
<p>
Email: <a href="mailto:admin@rosetta.im">admin@rosetta.im</a><br />
Support: <a href="/support/">https://rosetta.im/support/</a>
</p>
<p>Phone: <a href="tel:+85228910030">+852 2891 0030</a></p>
</main>
</body>
</html>

View File

@@ -1,20 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
xmlns:mobile="http://www.mobile.googlebot.com/schemas/mobile/1.0"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:mobile="http://www.mobile.googlebot.com/schemas/mobile/1.0"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url> <url>
<loc>https://rosetta.im/</loc> <loc>https://rosetta.im/</loc>
<lastmod>2026-07-11</lastmod> <lastmod>2026-07-30</lastmod>
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>1.0</priority> <priority>1.0</priority>
</url> </url>
<url>
<loc>https://rosetta.im/proto/</loc>
<lastmod>2026-08-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://rosetta.im/rcc/</loc>
<lastmod>2026-08-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://rosetta.im/proto/docs/</loc>
<lastmod>2026-07-31</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://rosetta.im/encryption/</loc>
<lastmod>2026-07-30</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url> <url>
<loc>https://rosetta.im/support/</loc> <loc>https://rosetta.im/support/</loc>
<lastmod>2026-07-11</lastmod> <lastmod>2026-07-30</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.8</priority> <priority>0.8</priority>
</url> </url>

View File

@@ -1,556 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rosetta Support</title>
<meta
name="description"
content="Contact Rosetta Support for help with account access, recovery phrases, messaging, media, privacy, and security."
/>
<link rel="canonical" href="https://rosetta.im/support/" />
<meta name="robots" content="index, follow" />
<meta name="theme-color" content="#0a67ff" />
<style>
:root {
color-scheme: light dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #f6f8fc;
line-height: 1.5;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background: #f6f8fc;
}
a {
color: #075ed3;
}
a:focus-visible {
outline: 3px solid #8dc0ff;
outline-offset: 3px;
}
.page {
width: min(1120px, calc(100% - 40px));
margin: 0 auto;
padding: 24px 0 64px;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
color: inherit;
font-size: 18px;
font-weight: 760;
letter-spacing: 0;
text-decoration: none;
}
.brand-mark {
width: 34px;
height: 34px;
border-radius: 8px;
overflow: hidden;
}
.brand-mark img {
display: block;
width: 100%;
height: 100%;
}
.home-link {
color: #334155;
font-size: 15px;
font-weight: 600;
text-decoration: none;
}
.hero {
max-width: 760px;
padding: 88px 0 58px;
}
.eyebrow {
margin: 0 0 12px;
color: #075ed3;
font-size: 14px;
font-weight: 750;
text-transform: uppercase;
}
h1,
h2,
h3,
p {
margin-top: 0;
}
h1 {
max-width: 650px;
margin-bottom: 20px;
font-size: clamp(38px, 7vw, 68px);
font-weight: 760;
letter-spacing: 0;
line-height: 1.04;
}
.intro {
max-width: 620px;
margin-bottom: 28px;
color: #4b5568;
font-size: clamp(18px, 2.2vw, 21px);
line-height: 1.5;
}
.contact-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 50px;
padding: 12px 18px;
border-radius: 8px;
background: #0a67ff;
color: #ffffff;
font-size: 16px;
font-weight: 720;
text-decoration: none;
}
.contact-button:hover {
background: #0055ca;
}
.quick-links {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-bottom: 72px;
}
.quick-link {
display: block;
min-height: 138px;
padding: 22px;
border: 1px solid #dbe2ee;
border-radius: 8px;
background: #ffffff;
color: inherit;
text-decoration: none;
}
.quick-link:hover {
border-color: #a7c9ff;
box-shadow: 0 10px 24px rgba(26, 61, 110, 0.08);
}
.quick-link strong {
display: block;
margin-bottom: 8px;
font-size: 17px;
}
.quick-link span {
color: #64748b;
font-size: 15px;
line-height: 1.4;
}
.section {
padding: 52px 0;
border-top: 1px solid #dbe2ee;
}
.section-heading {
display: grid;
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
gap: 32px;
align-items: start;
margin-bottom: 28px;
}
h2 {
margin-bottom: 0;
font-size: clamp(28px, 4vw, 38px);
letter-spacing: 0;
line-height: 1.12;
}
.section-copy {
margin-bottom: 0;
color: #526176;
font-size: 17px;
}
.faq {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
details {
padding: 19px 20px;
border: 1px solid #dbe2ee;
border-radius: 8px;
background: #ffffff;
}
summary {
cursor: pointer;
color: #1d293d;
font-size: 17px;
font-weight: 710;
list-style: none;
}
summary::-webkit-details-marker {
display: none;
}
summary::after {
float: right;
color: #075ed3;
content: "+";
font-size: 22px;
font-weight: 500;
line-height: 0.8;
}
details[open] summary::after {
content: "-";
}
details p {
margin: 14px 0 0;
color: #526176;
font-size: 15px;
}
.contact-panel {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(240px, 0.9fr);
gap: 28px;
padding: 32px;
border-radius: 8px;
background: #0e1e39;
color: #ffffff;
}
.contact-panel h2 {
margin-bottom: 12px;
}
.contact-panel p {
max-width: 580px;
margin-bottom: 0;
color: #cbd7ed;
font-size: 17px;
}
.contact-details {
align-self: center;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 8px;
background: rgba(255, 255, 255, 0.07);
}
.contact-details strong,
.contact-details a {
display: block;
}
.contact-details strong {
margin-bottom: 6px;
font-size: 15px;
}
.contact-details a {
color: #ffffff;
font-size: 18px;
font-weight: 700;
overflow-wrap: anywhere;
}
.legal-links {
display: flex;
flex-wrap: wrap;
gap: 18px;
padding-top: 32px;
}
.legal-links a {
color: #526176;
font-size: 15px;
}
@media (max-width: 720px) {
.page {
width: min(100% - 32px, 1120px);
padding-top: 16px;
}
.hero {
padding: 64px 0 42px;
}
.quick-links,
.faq,
.section-heading,
.contact-panel {
grid-template-columns: 1fr;
}
.quick-links {
margin-bottom: 52px;
}
.section {
padding: 40px 0;
}
.contact-panel {
gap: 20px;
padding: 24px;
}
}
@media (prefers-color-scheme: dark) {
:root {
color: #eff4ff;
background: #0c111a;
}
body {
background: #0c111a;
}
a,
.eyebrow {
color: #72afff;
}
.home-link,
.quick-link,
summary {
color: #eef4ff;
}
.intro,
.section-copy,
.quick-link span,
details p,
.legal-links a {
color: #aab8cf;
}
.quick-link,
details {
border-color: #29354a;
background: #141c29;
}
.quick-link:hover {
border-color: #427fc7;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
}
.section {
border-color: #29354a;
}
.contact-button {
background: #3986ff;
}
.contact-button:hover {
background: #5b9bff;
}
}
</style>
</head>
<body>
<main class="page">
<header class="topbar">
<a class="brand" href="/" aria-label="Rosetta home">
<span class="brand-mark"><img src="/rosetta-logo.png" alt="" /></span>
Rosetta
</a>
<a class="home-link" href="/">Back to home</a>
</header>
<section class="hero" aria-labelledby="support-title">
<p class="eyebrow">Rosetta Support</p>
<h1 id="support-title">Help with your Rosetta account and messages.</h1>
<p class="intro">
Find answers about account access, recovery phrases, private messaging,
shared media, and privacy. For anything else, contact our support team.
</p>
<a class="contact-button" href="mailto:admin@rosetta.im?subject=Rosetta%20Support%20Request">
Email Rosetta Support
</a>
</section>
<nav class="quick-links" aria-label="Support topics">
<a class="quick-link" href="#account">
<strong>Account and access</strong>
<span>Recovery phrases, device access, and account security.</span>
</a>
<a class="quick-link" href="#messaging">
<strong>Messaging and media</strong>
<span>Messages, photos, videos, files, voice messages, and groups.</span>
</a>
<a class="quick-link" href="#privacy">
<strong>Privacy and safety</strong>
<span>Security questions, privacy information, and abuse reports.</span>
</a>
</nav>
<section class="section" id="account" aria-labelledby="account-heading">
<div class="section-heading">
<h2 id="account-heading">Account and access</h2>
<p class="section-copy">
Rosetta accounts are accessed with a recovery phrase instead of a phone number.
Keep your phrase private and stored somewhere safe.
</p>
</div>
<div class="faq">
<details>
<summary>How do I access my account on a new device?</summary>
<p>
Install Rosetta and enter your recovery phrase in the exact order it was created.
Rosetta may ask you to confirm a new device to protect your account.
</p>
</details>
<details>
<summary>I lost my recovery phrase. Can it be restored?</summary>
<p>
No. Your recovery phrase is the key to your account. Rosetta cannot recover it
for you, so keep it private and backed up before you need it.
</p>
</details>
<details>
<summary>Why am I asked to confirm a new device?</summary>
<p>
New-device confirmation helps prevent someone who has obtained a recovery phrase
from silently accessing your account on another device.
</p>
</details>
<details>
<summary>How do I report an account-access problem?</summary>
<p>
Email us with the device model, Rosetta version, and a short description of what
happened. Do not include your recovery phrase or private keys.
</p>
</details>
</div>
</section>
<section class="section" id="messaging" aria-labelledby="messaging-heading">
<div class="section-heading">
<h2 id="messaging-heading">Messaging and media</h2>
<p class="section-copy">
Rosetta supports private chats, groups, photos, videos, files, and voice messages.
Attachment choices can vary depending on your device permissions and the selected item.
</p>
</div>
<div class="faq">
<details>
<summary>How do I send a photo, video, or file?</summary>
<p>
Open a chat, tap the attachment button next to the message field, choose the item
you want to share, then send it. Allow Photos or Files access when iOS asks for it.
</p>
</details>
<details>
<summary>Why is an attachment still sending?</summary>
<p>
Media uploads depend on the file size and your connection. Keep Rosetta open and
connected until the send indicator completes. You can retry a message that failed.
</p>
</details>
<details>
<summary>How do I send a voice message?</summary>
<p>
In a chat, press and hold the microphone control to record, then release it to send.
Microphone permission is required the first time you record.
</p>
</details>
<details>
<summary>How do I get help with a group?</summary>
<p>
Contact us with the group name and a description of the issue. Do not send private
recovery phrases, message contents, or encryption keys.
</p>
</details>
</div>
</section>
<section class="section" id="privacy" aria-labelledby="privacy-heading">
<div class="section-heading">
<h2 id="privacy-heading">Privacy and safety</h2>
<p class="section-copy">
Rosetta is designed for private communication. If you have a security concern or a
question about data handling, contact us directly.
</p>
</div>
<div class="faq">
<details>
<summary>How is message content protected?</summary>
<p>
Rosetta is designed to encrypt message content before it is sent. For details about
how we handle data, read the Rosetta Privacy Policy.
</p>
</details>
<details>
<summary>How do I report abuse or a security issue?</summary>
<p>
Email <a href="mailto:admin@rosetta.im?subject=Rosetta%20Security%20Report">admin@rosetta.im</a>
with a description of the concern. Do not include recovery phrases or private keys.
</p>
</details>
</div>
</section>
<section class="contact-panel" aria-labelledby="contact-heading">
<div>
<h2 id="contact-heading">Need more help?</h2>
<p>
Send us a message with your device model, app version, and a short explanation of the
issue. Never send a recovery phrase, private key, or private message content.
</p>
</div>
<div class="contact-details">
<strong>Rosetta Support</strong>
<a href="mailto:admin@rosetta.im?subject=Rosetta%20Support%20Request">admin@rosetta.im</a>
</div>
</section>
<footer class="legal-links">
<a href="/">Rosetta home</a>
<a href="/privacy/">Privacy Policy</a>
<a href="/terms/">Terms of Use</a>
</footer>
</main>
</body>
</html>

View File

@@ -1,260 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Terms of Use for Rosetta</title>
<meta
name="description"
content="Terms of Use and End User License Agreement for Rosetta by ROSETTA CLOUD SERVICES LIMITED."
/>
<link rel="canonical" href="https://rosetta.im/terms/" />
<style>
:root {
color-scheme: light dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.6;
color: #1f2937;
background: #ffffff;
}
body {
margin: 0;
background: linear-gradient(180deg, rgba(0, 102, 255, 0.04), rgba(255, 255, 255, 0));
}
main {
width: min(880px, calc(100% - 40px));
margin: 0 auto;
padding: 56px 0 72px;
}
a {
color: #0066ff;
}
h1 {
margin: 0 0 8px;
font-size: clamp(32px, 5vw, 48px);
line-height: 1.1;
}
h2 {
margin-top: 36px;
font-size: 22px;
line-height: 1.25;
}
p,
li {
font-size: 16px;
}
.muted {
color: #6b7280;
}
.back {
display: inline-block;
margin-bottom: 28px;
text-decoration: none;
}
.address {
white-space: pre-line;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f3f4f6;
background: #101113;
}
body {
background: linear-gradient(180deg, rgba(0, 102, 255, 0.12), rgba(16, 17, 19, 0));
}
.muted {
color: #a1a1aa;
}
}
</style>
</head>
<body>
<main>
<a class="back" href="/">Back to home</a>
<h1>Terms of Use for Rosetta</h1>
<p class="muted">Last updated: May 22, 2026</p>
<p>
These Terms of Use and End User License Agreement ("Terms") govern your
use of Rosetta, an end-to-end encrypted messaging application operated by
ROSETTA CLOUD SERVICES LIMITED ("Rosetta", "we", "us", or "our").
</p>
<p>
By downloading, installing, or using Rosetta, you agree to these Terms.
</p>
<h2>1. Agreement Between You and Rosetta</h2>
<p>
These Terms are concluded between you and ROSETTA CLOUD SERVICES LIMITED
only, and not with Apple. We, not Apple, are solely responsible for
Rosetta and its content.
</p>
<h2>2. License</h2>
<p>
We grant you a limited, non-exclusive, non-transferable, revocable
license to use Rosetta for personal or lawful business communication on
Apple-branded products that you own or control, as permitted by the Apple
Media Services Terms and Conditions, including applicable Family Sharing,
volume purchasing, or Legacy Contacts rules.
</p>
<p>
You may not copy, modify, reverse engineer, decompile, resell,
sublicense, rent, lease, or distribute Rosetta except as permitted by
applicable law.
</p>
<h2>3. Privacy</h2>
<p>
Your use of Rosetta is also governed by our Privacy Policy, available at
<a href="/privacy/">https://rosetta.im/privacy/</a>.
</p>
<h2>4. Encrypted Messaging</h2>
<p>
Rosetta is designed for end-to-end encrypted communication. You are
responsible for protecting your device, private keys, and access
credentials. If you lose access to your device or keys, we may not be
able to restore your messages or account data.
</p>
<h2>5. Acceptable Use</h2>
<p>You agree not to use Rosetta to:</p>
<ul>
<li>violate any law or regulation;</li>
<li>infringe the rights of others;</li>
<li>send spam, malware, scams, threats, harassment, or unlawful content;</li>
<li>
attempt to disrupt, overload, reverse engineer, or compromise Rosetta
or its infrastructure;
</li>
<li>impersonate another person or misrepresent your identity;</li>
<li>use Rosetta for any prohibited or sanctioned activity.</li>
</ul>
<h2>6. No Emergency Use</h2>
<p>
Rosetta is not designed for emergency calls, emergency messaging, or
communications where failure or delay could cause death, personal injury,
or serious damage. Use official emergency channels when needed.
</p>
<h2>7. User Content</h2>
<p>
You retain ownership of the content you create or send through Rosetta.
You grant us the limited rights necessary to operate the service, such as
routing encrypted messages and maintaining service security. We do not
claim ownership of your messages or files.
</p>
<h2>8. Updates and Availability</h2>
<p>
We may update, modify, suspend, or discontinue Rosetta or parts of the
service at any time. Some features may require an internet connection or
a compatible device and operating system.
</p>
<h2>9. Fees</h2>
<p>
Rosetta is currently offered as a free application unless stated
otherwise. If paid features, subscriptions, or in-app purchases are
introduced through Apple, Apple's payment and subscription terms will
apply.
</p>
<h2>10. Maintenance and Support</h2>
<p>
ROSETTA CLOUD SERVICES LIMITED is solely responsible for providing
maintenance and support for Rosetta. Apple has no obligation to provide
maintenance or support services for Rosetta.
</p>
<p>
Support contact: <a href="mailto:admin@rosetta.im">admin@rosetta.im</a><br />
Support centre: <a href="/support/">https://rosetta.im/support/</a>
</p>
<h2>11. Warranty</h2>
<p>
Rosetta is provided "as is" and "as available" to the maximum extent
permitted by law. If Rosetta fails to conform to any applicable warranty,
you may notify Apple, and Apple may refund the purchase price, if any,
for the app. To the maximum extent permitted by law, Apple has no other
warranty obligation for Rosetta.
</p>
<h2>12. Product Claims</h2>
<p>
ROSETTA CLOUD SERVICES LIMITED, not Apple, is responsible for addressing
any claims relating to Rosetta, including product liability claims, legal
or regulatory claims, and claims under consumer protection or similar
laws.
</p>
<h2>13. Intellectual Property Claims</h2>
<p>
If a third party claims that Rosetta or your use of Rosetta infringes
intellectual property rights, ROSETTA CLOUD SERVICES LIMITED, not Apple,
is responsible for the investigation, defense, settlement, and discharge
of that claim.
</p>
<h2>14. Legal Compliance</h2>
<p>
You represent that you are not located in a country subject to a U.S.
Government embargo or designated by the U.S. Government as a "terrorist
supporting" country, and that you are not listed on any U.S. Government
list of prohibited or restricted parties.
</p>
<h2>15. Third-Party Beneficiary</h2>
<p>
Apple and Apple's subsidiaries are third-party beneficiaries of these
Terms. Upon your acceptance of these Terms, Apple has the right to
enforce these Terms against you as a third-party beneficiary.
</p>
<h2>16. Termination</h2>
<p>
We may suspend or terminate access to Rosetta if you violate these Terms,
create risk for other users, or use the service unlawfully. You may stop
using Rosetta at any time.
</p>
<h2>17. Limitation of Liability</h2>
<p>
To the maximum extent permitted by law, ROSETTA CLOUD SERVICES LIMITED
will not be liable for indirect, incidental, special, consequential, or
punitive damages, or for loss of data, profits, goodwill, or business
opportunities arising from your use of Rosetta.
</p>
<h2>18. Governing Law</h2>
<p>
These Terms are governed by the laws of Hong Kong, unless local consumer
protection law requires otherwise.
</p>
<h2>19. Contact</h2>
<p class="address">ROSETTA CLOUD SERVICES LIMITED
Registered address:
UNIT 704C 7/F BLK 3 NAN FUNG IND CITY
18 TIN HAU RD
TUEN MUN
HONG KONG</p>
<p>Email: <a href="mailto:admin@rosetta.im">admin@rosetta.im</a></p>
<p>Phone: <a href="tel:+85228910030">+852 2891 0030</a></p>
</main>
</body>
</html>

View File

@@ -0,0 +1,188 @@
import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import { basename, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isMap, isSeq, parseDocument, stringify } from 'yaml';
const projectRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
const sourceDirectory = resolve(
process.env.ROSETTA_PROTO_DIR || resolve(projectRoot, 'proto'),
);
const outputPath = resolve(projectRoot, 'src/generated/protoDocs.ts');
async function exists(path) {
try {
await access(path);
return true;
} catch {
return false;
}
}
function capitalizeFirstLetter(value) {
return value.trim().replace(/\p{L}/u, (letter) => letter.toLocaleUpperCase());
}
function cleanComment(...comments) {
const description = comments
.filter(Boolean)
.join('\n')
.split('\n')
.map((line) => line.replace(/^\s*#?\s?/, '').trim())
.filter(Boolean)
.join(' ');
return capitalizeFirstLetter(description);
}
function mapFields(node) {
if (!isMap(node)) {
return [];
}
const fields = node.get('fields', true);
if (!isSeq(fields)) {
return [];
}
return fields.items.map((field) => {
const json = field.toJSON();
const nameNode = field.get('name', true);
const typeNode = field.get('type', true);
const rawName = String(json.name);
const optional = rawName.endsWith('?');
return {
id: Number(json.id),
name: optional ? rawName.slice(0, -1) : rawName,
type: String(json.type),
optional,
description: cleanComment(
field.commentBefore,
field.comment,
nameNode?.commentBefore,
nameNode?.comment,
typeNode?.comment,
),
};
});
}
function mapNamedCollection(node, kind) {
if (!isMap(node)) {
return [];
}
return node.items.map((pair) => {
const name = String(pair.key.value);
const description = cleanComment(
pair.key.commentBefore,
pair.key.comment,
pair.value?.commentBefore,
pair.value?.comment,
);
const yaml = stringify(
{ [name]: pair.value.toJSON() },
{ indent: 2, lineWidth: 0 },
).trimEnd();
if (kind === 'enum') {
const values = pair.value.get('values', true);
return {
name,
type: String(pair.value.get('type')),
description,
yaml,
values: isSeq(values)
? values.items.map((value) => {
const json = value.toJSON();
const nameNode = value.get('name', true);
return {
id: Number(json.id),
name: String(json.name),
description: cleanComment(
value.commentBefore,
value.comment,
nameNode?.commentBefore,
nameNode?.comment,
),
};
})
: [],
};
}
const fields = mapFields(pair.value);
return {
name,
id: kind === 'packet' ? Number(pair.value.get('id')) : undefined,
description,
yaml,
fields,
};
});
}
async function readLayer(path) {
const source = await readFile(path, 'utf8');
const document = parseDocument(source, { keepSourceTokens: true });
if (document.errors.length > 0) {
throw new Error(
`${basename(path)}: ${document.errors.map((error) => error.message).join('; ')}`,
);
}
return {
version: Number(document.get('version')),
description: capitalizeFirstLetter(String(document.get('description') || '')),
source: basename(path),
enums: mapNamedCollection(document.get('enums', true), 'enum'),
types: mapNamedCollection(document.get('types', true), 'type'),
packets: mapNamedCollection(document.get('packets', true), 'packet'),
};
}
let sourceFiles = [];
try {
sourceFiles = (await readdir(sourceDirectory))
.filter((file) => /^layer\..+\.ya?ml$/i.test(file))
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
.map((file) => resolve(sourceDirectory, file));
} catch {
sourceFiles = [];
}
if (sourceFiles.length === 0) {
if (await exists(outputPath)) {
console.log(
`Proto sources are unavailable at ${sourceDirectory}; keeping the committed generated documentation.`,
);
process.exit(0);
}
throw new Error(`No YAML schema files found in ${sourceDirectory}`);
}
const layers = [];
for (const file of sourceFiles) {
layers.push(await readLayer(file));
}
layers.sort((a, b) => a.version - b.version);
const generatedSource = `/* This file is generated by scripts/generate-proto-docs.mjs. */\nimport type { ProtoLayer } from '../types/proto';\n\nexport const protoLayers = ${JSON.stringify(layers, null, 2)} satisfies ProtoLayer[];\n`;
await mkdir(resolve(projectRoot, 'src/generated'), { recursive: true });
await writeFile(outputPath, generatedSource);
const totals = layers.reduce(
(result, layer) => ({
enums: result.enums + layer.enums.length,
types: result.types + layer.types.length,
packets: result.packets + layer.packets.length,
}),
{ enums: 0, types: 0, packets: 0 },
);
console.log(
`Generated Proto docs from ${layers.length} layers: ${totals.packets} packets, ${totals.types} types, ${totals.enums} enums.`,
);

View File

@@ -1,24 +1,102 @@
import '@mantine/core/styles.css'; import '@mantine/core/styles.css';
import { MantineProvider } from '@mantine/core'; import { Button, Center, createTheme, Loader, MantineProvider, Stack, Text, Title } from '@mantine/core';
import { HeroTitle } from './components/HeroTitle/HeroTitle'; import { Component, lazy, Suspense, type ReactNode } from 'react';
import { DownloadCenter } from './components/DownloadCenter/DownloadCenter'; import { Route, Routes } from 'react-router-dom';
import { FeaturesGrid } from './components/FeaturesGrid/FeaturesGrid'; import { ScrollToTop } from './components/ScrollToTop/ScrollToTop';
import { SEO } from './components/SEO/SEO'; import './style.css';
import './style.css'
import { MessageSteps } from './components/MessageSteps/MessageSteps';
import { Footer } from './components/Footer/Footer';
const LandingPage = lazy(() =>
import('./pages/LandingPage').then((module) => ({ default: module.LandingPage })),
);
const EncryptionPage = lazy(() =>
import('./pages/EncryptionPage').then((module) => ({ default: module.EncryptionPage })),
);
const ProtoPage = lazy(() =>
import('./pages/ProtoPage').then((module) => ({ default: module.ProtoPage })),
);
const ProtoDocsPage = lazy(() =>
import('./pages/ProtoPage').then((module) => ({ default: module.ProtoDocsPage })),
);
const RccPage = lazy(() =>
import('./pages/RccPage').then((module) => ({ default: module.RccPage })),
);
const SupportPage = lazy(() =>
import('./pages/SupportPage').then((module) => ({ default: module.SupportPage })),
);
const PrivacyPage = lazy(() =>
import('./pages/PrivacyPage').then((module) => ({ default: module.PrivacyPage })),
);
const TermsPage = lazy(() =>
import('./pages/TermsPage').then((module) => ({ default: module.TermsPage })),
);
const theme = createTheme({
primaryColor: 'blue',
defaultRadius: 'md',
fontFamily:
'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
headings: {
fontFamily:
'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
},
});
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> <MantineProvider theme={theme}>
<SEO /> <PageErrorBoundary>
<HeroTitle></HeroTitle> <ScrollToTop />
<FeaturesGrid></FeaturesGrid> <Suspense
<MessageSteps></MessageSteps> fallback={
<DownloadCenter></DownloadCenter> <Center mih="100vh">
<Footer></Footer> <Loader aria-label="Loading page" />
</Center>
}
>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/encryption" element={<EncryptionPage />} />
<Route path="/proto" element={<ProtoPage />} />
<Route path="/proto/docs" element={<ProtoDocsPage />} />
<Route path="/rcc" element={<RccPage />} />
<Route path="/support" element={<SupportPage />} />
<Route path="/privacy" element={<PrivacyPage />} />
<Route path="/terms" element={<TermsPage />} />
</Routes>
</Suspense>
</PageErrorBoundary>
</MantineProvider> </MantineProvider>
) );
} }

View File

@@ -1,12 +0,0 @@
.control {
height: 54px;
padding-left: 38px;
padding-right: 38px;
@media (max-width: $mantine-breakpoint-sm) {
height: 54px;
padding-left: 18px;
padding-right: 18px;
flex: 1;
}
}

View File

@@ -1,50 +0,0 @@
import { Button } from "@mantine/core";
import classes from './Download.module.css';
import { RosettaLogo } from "../RosettaLogo/RosettaLogo";
interface DownloadProps {
href: string;
}
export function Download(props: DownloadProps) {
/*const protocol = useMemo(() => {
let protocol = new Protocol('ws://127.0.0.1:3000');
protocol.addSupportedPacket(new PacketRequestUpdate());
protocol.addSupportedPacket(new PacketKernelUpdate());
return protocol;
}, []);
useEffect(() => {
if(!protocol){
return;
}
const platform = getPlatformTranslated();
let packet = new PacketRequestUpdate();
packet.setPlatform(platform);
packet.setKernelVersion("0.0.0");
packet.setArch("x64");
protocol.sendPacket(packet);
}, [protocol]);
protocol.waitPacket(0x0D, (packet : PacketKernelUpdate) => {
console.info(packet);
});*/
return (
<Button.Group>
<Button
size="xl"
component="a"
className={classes.control}
variant={'default'}
href={props.href}
leftSection={
<RosettaLogo size={25}></RosettaLogo>
}
>
Get Rosetta
</Button>
</Button.Group>
);
}

View File

@@ -1,41 +0,0 @@
.wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: var(--mantine-spacing-xl);
padding-bottom: var(--mantine-spacing-xl);
background-color: #0066ff05;
}
.title {
font-size: 34px;
font-weight: 500;
@media (max-width: $mantine-breakpoint-sm) {
font-size: 24px;
}
}
.description {
max-width: 600px;
margin: auto;
}
.card {
width: 300px;
height: 300px;
border: 1px solid light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
.cardTitle {
&::after {
content: '';
display: block;
background-color: var(--mantine-color-blue-filled);
width: 45px;
height: 2px;
margin-top: var(--mantine-spacing-sm);
}
}

View File

@@ -1,157 +0,0 @@
import {
Button,
Card,
Container,
Flex,
Text,
Title,
useMantineTheme,
} from '@mantine/core';
import classes from './DownloadCenter.module.css';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
import { useEffect, useState } from 'react';
import { FaWindows } from 'react-icons/fa';
import { FcAndroidOs, FcLinux } from 'react-icons/fc';
import { Switch } from '../Switch/Switch';
import { IconDownload } from '@tabler/icons-react';
interface DownloadFeature {
platform: string;
arch: string;
link: string;
version: string;
}
interface UpdateItem {
platform: string;
arch: string;
version: string;
downloadUrl: string;
}
const fetchUpdates = async (): Promise<DownloadFeature[]> => {
try {
const response = await fetch('https://sdu.rosetta.im/updates/all');
const data = (await response.json()) as { items?: UpdateItem[] };
return (data.items || []).map((item) => ({
platform: item.platform,
arch: item.arch,
version: item.version,
link: new URL(item.downloadUrl, 'https://sdu.rosetta.im').toString(),
})).sort((a, b) => {
const platformOrder = ['linux', 'win32', 'android'];
const archOrder = ['arm64', 'x64', 'universal']
const platformComparison = platformOrder.indexOf(a.platform) - platformOrder.indexOf(b.platform);
if (platformComparison !== 0) {
return platformComparison;
}
return archOrder.indexOf(a.arch) - archOrder.indexOf(b.arch);
});
} catch (error) {
console.error('Failed to fetch updates:', error);
return [];
}
};
export function DownloadCenter() {
const theme = useMantineTheme();
const [kernels, setKernels] = useState<DownloadFeature[]>([]);
const [targetPlatforms, setTargetPlatforms] = useState<string[]>(['linux', 'win32', 'android']);
useEffect(() => {
fetchUpdates().then(setKernels);
}, []);
const switchTarget = (value: string) => {
let targetPlatforms: string[] = [];
switch(value){
case 'All':
targetPlatforms = ['linux', 'win32', 'android'];
break;
case 'Windows':
targetPlatforms = ['win32'];
break;
case 'Android':
targetPlatforms = ['android'];
break;
case 'Linux':
targetPlatforms = ['linux'];
break;
}
setTargetPlatforms(targetPlatforms);
}
const translatePlatformToOsName = (platform: string) => {
switch(platform){
case 'linux':
return 'Linux';
case 'win32':
return 'Windows';
case 'android':
return 'Android';
default:
return platform;
}
}
const translatePlatformAndArchToCpuName = (platform : string, arch: string) => {
switch(platform){
case 'linux':
return arch === 'arm64' ? 'ARM64' : 'x64';
case 'win32':
return arch === 'arm64' ? 'ARM64' : 'x64';
case 'android':
return 'universal';
default:
return arch;
}
}
const features = kernels.filter((v) => targetPlatforms.includes(v.platform)).map((feature) => (
<Card key={feature.link} radius="md" className={classes.card} padding="xl">
<Flex direction={'column'} align="center" justify={'center'}>
{feature.platform === 'linux' && <FcLinux size={50} />}
{feature.platform === 'win32' && <FaWindows size={50} color={theme.colors.blue[6]} />}
{feature.platform === 'android' && <FcAndroidOs size={50} />}
<Text fz="lg" ta={'center'} fw={500} mt="md">
{translatePlatformToOsName(feature.platform)} - {translatePlatformAndArchToCpuName(feature.platform, feature.arch)}
</Text>
<Text c="dimmed" fz="sm" ta={'center'} mt="xs">
{feature.platform == 'linux' && <>This version is for Linux and comes as an AppImage for the GUI versions.</>}
{feature.platform == 'win32' && <>Download this version of you have computer on Windows 10 or later.</>}
{feature.platform == 'android' && <>This version is for Android devices and all architectures.</>}
</Text>
<Button component={'a'} href={feature.link} variant={'subtle'} mt={'sm'} leftSection={
<IconDownload size={16}></IconDownload>
}>Get {feature.version} now</Button>
</Flex>
</Card>
));
return (
<div className={classes.wrapper}>
<Container id={'kernels'} mb={'xl'} size="lg" mt={'xl'}>
<Flex justify="center" align="center" direction="row" gap={'md'}>
<Title order={2} className={classes.title} ta="center">
Download
</Title>
<RosettaLogo size={35}></RosettaLogo>
</Flex>
<Text c="dimmed" className={classes.description} ta="center" mt="md">
Choose your platform and get started with Rosetta today. Enjoy secure and fast messaging across all your devices.
</Text>
<Flex justify={'center'} mt={'lg'}>
<Switch onChange={switchTarget} data={['All', 'Windows', 'Linux', 'Android']}></Switch>
</Flex>
<Flex gap="xl" justify={'center'} wrap={'wrap'} align={'center'} mt={50}>
{features}
</Flex>
</Container>
</div>
);
}

View File

@@ -1,29 +0,0 @@
.wrapper {
display: flex;
justify-content: flex-start;
flex-direction: column;
background-color: #0066ff05;
min-height: 100vh;
padding-top: var(--mantine-spacing-xl);
padding-bottom: var(--mantine-spacing-xl);
}
.title {
font-family: Outfit, var(--mantine-font-family);
font-weight: 500;
margin-bottom: var(--mantine-spacing-md);
text-align: center;
@media (max-width: $mantine-breakpoint-sm) {
font-size: 28px;
text-align: left;
}
}
.description {
text-align: center;
@media (max-width: $mantine-breakpoint-sm) {
text-align: left;
}
}

View File

@@ -1,98 +0,0 @@
import { IconGauge, IconLock, IconMessage2, IconServer, IconUser, IconUsersGroup, type TablerIcon } from '@tabler/icons-react';
import { Container, Flex, SimpleGrid, Text, ThemeIcon, Title } from '@mantine/core';
import type { ReactNode } from 'react';
import classes from './FeaturesGrid.module.css';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
const MOCKDATA = [
{
icon: IconGauge,
title: 'Performance',
description:
'Despite encryption, the messenger remains fast and convenient.',
},
{
icon: IconUser,
title: 'Privacy focused',
description:
'We encrypt everything, even media files, so that no one can access your files.',
},
{
icon: IconServer,
title: 'Server does not save messages',
description:
'The server does not store messages. All messages are stored only on your devices.',
},
{
icon: IconLock,
title: 'Secure by default',
description:
'The messenger uses asymmetric encryption to hide message keys. ChaCha20 is used for message encryption, and AES-256 is used for encrypting your media files.',
},
{
icon: IconMessage2,
title: 'Regular Updates',
description:
'We strive to update the messenger as often as possible to fix bugs and add new features. This is not an abandoned project.',
},
{
icon: IconUsersGroup,
title: 'No trackers or ads',
description:
'Registration without phone number, email, or other personal data. Only public or private keys. We do not use trackers and do not show ads.',
}
];
interface FeatureProps {
icon: TablerIcon;
title: ReactNode;
description: ReactNode;
}
export function Feature({ icon: Icon, title, description }: FeatureProps) {
return (
<div>
<ThemeIcon variant="light" size={40} radius={40}>
<Icon size={18} stroke={1.5} />
</ThemeIcon>
<Text mt="sm" mb={7}>
{title}
</Text>
<Text size="sm" c="dimmed" lh={1.6}>
{description}
</Text>
</div>
);
}
export function FeaturesGrid() {
const features = MOCKDATA.map((feature, index) => <Feature {...feature} key={index} />);
return (
<div className={classes.wrapper}>
<Container>
<Flex justify={'center'} mb={'lg'} align={'center'}>
<RosettaLogo size={50}></RosettaLogo>
</Flex>
<Title ta="center" className={classes.title}>
Here’s what we offer
</Title>
<Container size={560} p={0}>
<Text size="sm" className={classes.description}>
Rosetta offers a wide range of features that ensure security, privacy, and ease of use. Explore the key features that make Rosetta an excellent choice for messaging.
</Text>
</Container>
<SimpleGrid
mt={60}
cols={{ base: 1, sm: 2, md: 3 }}
spacing={{ base: 'xl', md: 50 }}
verticalSpacing={{ base: 'xl', md: 50 }}
>
{features}
</SimpleGrid>
</Container>
</div>
);
}

View File

@@ -1,17 +0,0 @@
.footer {
padding: var(--mantine-spacing-xl) var(--mantine-spacing-md);
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}
.legal {
max-width: 720px;
margin: 0 auto;
}
.links {
display: flex;
justify-content: center;
gap: var(--mantine-spacing-md);
flex-wrap: wrap;
margin-top: var(--mantine-spacing-xs);
}

View File

@@ -1,27 +0,0 @@
import { Anchor, Text } from '@mantine/core';
import classes from './Footer.module.css';
const LEGAL_NAME = 'ROSETTA CLOUD SERVICES LIMITED';
export function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className={classes.footer}>
<Text c="dimmed" fz="sm" ta="center" className={classes.legal}>
&copy; {currentYear} {LEGAL_NAME}. All rights reserved.
</Text>
<nav className={classes.links} aria-label="Legal information">
<Anchor href="/support/" size="sm" c="dimmed">
Support
</Anchor>
<Anchor href="/privacy/" size="sm" c="dimmed">
Privacy Policy
</Anchor>
<Anchor href="/terms/" size="sm" c="dimmed">
Terms of Use
</Anchor>
</nav>
</footer>
);
}

View File

@@ -1,59 +0,0 @@
.wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
height: 100vh;
}
.inner {
position: relative;
}
.title {
font-family: Outfit, var(--mantine-font-family);
font-size: 62px;
font-weight: 500;
line-height: 1.1;
margin: 0;
word-wrap: normal;
padding: 0;
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
@media (max-width: $mantine-breakpoint-sm) {
font-size: 42px;
line-height: 1.2;
}
}
.description {
margin-top: var(--mantine-spacing-xl);
font-size: 24px;
@media (max-width: $mantine-breakpoint-sm) {
font-size: 18px;
}
}
.controls {
margin-top: calc(var(--mantine-spacing-xl) * 2);
@media (max-width: $mantine-breakpoint-sm) {
margin-top: var(--mantine-spacing-xl);
}
}
.control {
height: 54px;
padding-left: 38px;
padding-right: 38px;
@media (max-width: $mantine-breakpoint-sm) {
height: 54px;
padding-left: 18px;
padding-right: 18px;
flex: 1;
}
}

View File

@@ -1,27 +0,0 @@
import { Container, Group, Text } from '@mantine/core';
import classes from './HeroTitle.module.css';
import { Download } from '../Download/Download';
export function HeroTitle() {
return (
<div className={classes.wrapper}>
<Container className={classes.inner}>
<h1 className={classes.title}>
Rosetta is{' '}
<Text component="span" variant="gradient" gradient={{ from: 'blue', to: 'cyan' }} inherit>
secure
</Text>{' '}
messaging for everyone
</h1>
<Text className={classes.description} c="dimmed">
Rosetta is designed to ensure secure messaging. We use comprehensive encryption, which keeps the app fast and convenient, yet secure.
</Text>
<Group className={classes.controls}>
<Download href="#kernels"></Download>
</Group>
</Container>
</div>
);
}

View File

@@ -0,0 +1,139 @@
.page {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 100vh;
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
}
.shell {
flex: 1;
width: 100%;
min-width: 0;
padding-top: 72px;
padding-bottom: 96px;
}
.main {
width: 100%;
min-width: 0;
max-width: 900px;
margin-inline: auto;
padding: clamp(30px, 5vw, 60px);
border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
.intro {
min-width: 0;
max-width: 760px;
padding-bottom: 36px;
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}
.title {
margin-top: var(--mantine-spacing-sm);
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-size: clamp(28px, 4vw, 36px);
font-weight: 600;
letter-spacing: -0.03em;
line-height: 1.08;
}
.lead {
max-width: 780px;
margin: 40px 0 0;
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
font-size: 17px;
line-height: 1.72;
}
.text,
.list {
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
font-size: var(--mantine-font-size-md);
line-height: 1.7;
}
.text {
margin: 0 0 var(--mantine-spacing-md);
}
.text:last-child,
.address:last-child {
margin-bottom: 0;
}
.list {
margin-bottom: var(--mantine-spacing-md);
padding-left: var(--mantine-spacing-xs);
}
.list li::marker {
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
}
.section {
min-width: 0;
margin-top: 44px;
padding-top: 36px;
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}
.sectionTitle {
margin: 0;
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
font-size: clamp(19px, 3vw, 23px);
font-weight: 650;
line-height: 1.25;
}
.sectionContent {
min-width: 0;
margin-top: var(--mantine-spacing-md);
}
.document a {
color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-3));
text-underline-offset: 3px;
overflow-wrap: anywhere;
}
.address {
margin: var(--mantine-spacing-md) 0;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
}
.address p {
margin: 0;
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
line-height: 1.65;
white-space: pre-line;
}
@media (max-width: 48em) {
.shell {
padding-top: 40px;
padding-bottom: 64px;
}
.intro {
padding-bottom: 36px;
}
.section {
margin-top: 36px;
padding-top: 30px;
}
}
@media (max-width: 36em) {
.shell {
padding-inline: var(--mantine-spacing-sm);
}
.main {
padding: 28px 22px;
}
}

View File

@@ -0,0 +1,101 @@
import type { ReactNode } from 'react';
import {
Box,
Container,
List,
Paper,
Text,
Title,
} from '@mantine/core';
import { SiteFooter } from '../SiteFooter/SiteFooter';
import { SiteHeader } from '../SiteHeader/SiteHeader';
import classes from './LegalPage.module.css';
interface LegalPageProps {
title: string;
updated?: string;
eyebrow?: string;
children: ReactNode;
}
interface LegalSectionProps {
title: string;
children: ReactNode;
anchorId?: string;
}
export function LegalPage({
title,
updated,
eyebrow = 'Rosetta Legal',
children,
}: LegalPageProps) {
return (
<Box className={classes.page}>
<SiteHeader />
<Container className={classes.shell}>
<Paper component="main" withBorder radius="sm" className={classes.main}>
<Box className={classes.intro}>
<Text tt="uppercase" fw={700} fz="xs" c="dimmed" lts="0.08em">
{eyebrow}
</Text>
<Title order={1} className={classes.title}>
{title}
</Title>
{updated && (
<Text c="dimmed" mt="md">
Last updated: {updated}
</Text>
)}
</Box>
<Box component="article" className={classes.document}>
{children}
</Box>
</Paper>
</Container>
<SiteFooter />
</Box>
);
}
export function LegalSection({ title, children, anchorId }: LegalSectionProps) {
return (
<Box component="section" id={anchorId} className={classes.section}>
<Title order={2} className={classes.sectionTitle}>
{title}
</Title>
<Box className={classes.sectionContent}>{children}</Box>
</Box>
);
}
export function LegalText({
children,
lead = false,
}: {
children: ReactNode;
lead?: boolean;
}) {
return (
<Text component="p" className={lead ? classes.lead : classes.text}>
{children}
</Text>
);
}
export function LegalList({ children }: { children: ReactNode }) {
return (
<List spacing="xs" className={classes.list}>
{children}
</List>
);
}
export function LegalAddress({ children }: { children: ReactNode }) {
return (
<Paper radius="sm" p="md" className={classes.address}>
<Text component="p">{children}</Text>
</Paper>
);
}

View File

@@ -1,28 +0,0 @@
.wrapper {
display: flex;
justify-content: flex-start;
flex-direction: column;
min-height: 100vh;
padding-top: var(--mantine-spacing-xl);
padding-bottom: var(--mantine-spacing-xl);
}
.title {
font-family: Outfit, var(--mantine-font-family);
font-weight: 500;
margin-bottom: var(--mantine-spacing-md);
text-align: center;
@media (max-width: $mantine-breakpoint-sm) {
font-size: 28px;
text-align: left;
}
}
.description {
text-align: center;
@media (max-width: $mantine-breakpoint-sm) {
text-align: left;
}
}

View File

@@ -1,45 +0,0 @@
import { Container, Flex, Text, Title } from '@mantine/core';
import classes from './MessageSteps.module.css';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
import { Switch } from '../Switch/Switch';
import { useState } from 'react';
import { Sender } from '../Sender/Sender';
import { Recipient } from '../Recipient/Recipient';
export function MessageSteps() {
const [show, setShow] = useState('Sender');
return (
<div className={classes.wrapper}>
<Container>
<Flex justify={'center'} mb={'lg'} align={'center'}>
<RosettaLogo size={50}></RosettaLogo>
</Flex>
<Title ta="center" className={classes.title}>
How is my message sent?
</Title>
<Container size={560} p={0}>
<Text size="sm" className={classes.description}>
See why Rosetta is truly secure. Notice how your message is sent and received from you to the recipient. The steps highlighted in blue are what you see, while the steps not highlighted are what happens behind the scenes.
</Text>
</Container>
<Flex align={'center'} justify={'center'} mt={'xl'}>
<Switch data={[
'Sender',
'Recipient'
]} onChange={(v) => setShow(v)}></Switch>
</Flex>
<Flex justify={'center'} align={'center'} mt={'xl'} gap={'xl'}>
<Flex>
{show == 'Sender' && <Sender></Sender>}
{show == 'Recipient' && <Recipient></Recipient>}
</Flex>
</Flex>
</Container>
</div>
);
}

View File

@@ -1,45 +0,0 @@
import { Text, Timeline } from "@mantine/core";
import { IconDeviceDesktop, IconKey, IconLockAccess, IconMessage, IconMessage2, IconServer } from "@tabler/icons-react";
export function Recipient() {
return (
<Timeline reverseActive active={0} bulletSize={40} lineWidth={2}>
<Timeline.Item bullet={<IconServer size={20} />} title="Retranslate">
<Text c="dimmed" size="sm">
Server retranslate message to recipient.
</Text>
<Text size="xs" mt={4}>After retranslate server deletes message from memory.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconMessage size={20} />} title="Receive message">
<Text c="dimmed" size="sm">
Recipient device receives encrypted message and encrypted ChaCha20 key from server.
</Text>
<Text size="xs" mt={4}>Message now encrypted.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconLockAccess size={20} />} title="Decrypting ChaCha20 key">
<Text c="dimmed" size="sm">
Decrypt ChaCha20 key with recipient private key using RSA-4096 decryption.
</Text>
<Text size="xs" mt={4}>Message still encrypted.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconKey size={20} />} title="Decrypting message using ChaCha20">
<Text c="dimmed" size="sm">
Decrypting text of message or attachments using decrypted ChaCha20 key from previous step.
</Text>
<Text size="xs" mt={4}>Message text is available.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconDeviceDesktop size={20} />} title="Save message on your device">
<Text c="dimmed" size="sm">
Encrypt decrypted text with AES-256 for save in your device.
</Text>
<Text size="xs" mt={4}>Need for future access, because message not save on server.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconMessage2 size={20} />} title="Show message">
<Text c="dimmed" size="sm">
Message available for reading in your chat.
</Text>
<Text size="xs" mt={4}>Final step.</Text>
</Timeline.Item>
</Timeline>
)
}

View File

@@ -3,30 +3,31 @@ import { useEffect } from 'react';
interface SEOProps { interface SEOProps {
title?: string; title?: string;
description?: string; description?: string;
ogImage?: string;
keywords?: string; keywords?: string;
canonical?: string;
} }
export function SEO({ export function SEO({
title = 'Rosetta - Secure Messaging for Everyone', title = 'Rosetta — Private Messaging',
description = 'End-to-end encrypted messaging app with military-grade security. Download for Windows, Linux, and Android.', description = 'Private messaging built around strong encryption, local message history, and user-controlled identity.',
ogImage = '/og-image.png', keywords = 'Rosetta, encrypted messaging, private messenger, secure chat',
keywords = 'secure messaging, encrypted chat, private messenger' canonical = 'https://rosetta.im/'
}: SEOProps) { }: SEOProps) {
useEffect(() => { useEffect(() => {
// Update title
document.title = title; document.title = title;
// Update meta tags
updateMetaTag('name', 'description', description); updateMetaTag('name', 'description', description);
updateMetaTag('name', 'keywords', keywords); updateMetaTag('name', 'keywords', keywords);
updateMetaTag('property', 'og:title', title); updateMetaTag('property', 'og:title', title);
updateMetaTag('property', 'og:description', description); updateMetaTag('property', 'og:description', description);
updateMetaTag('property', 'og:image', ogImage); updateMetaTag('property', 'og:url', canonical);
updateMetaTag('name', 'twitter:title', title); updateMetaTag('name', 'twitter:title', title);
updateMetaTag('name', 'twitter:description', description); updateMetaTag('name', 'twitter:description', description);
updateMetaTag('name', 'twitter:image', ogImage); updateMetaTag('name', 'twitter:url', canonical);
}, [title, description, ogImage, keywords]); removeMetaTag('property', 'og:image');
removeMetaTag('name', 'twitter:image');
updateCanonical(canonical);
}, [title, description, keywords, canonical]);
return null; return null;
} }
@@ -42,3 +43,19 @@ function updateMetaTag(attr: string, name: string, content: string) {
element.setAttribute('content', content); element.setAttribute('content', content);
} }
function removeMetaTag(attr: string, name: string) {
document.querySelector(`meta[${attr}="${name}"]`)?.remove();
}
function updateCanonical(href: string) {
let element = document.querySelector<HTMLLinkElement>('link[rel="canonical"]');
if (!element) {
element = document.createElement('link');
element.rel = 'canonical';
document.head.appendChild(element);
}
element.href = href;
}

View File

@@ -0,0 +1,51 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export function ScrollToTop() {
const { pathname, hash } = useLocation();
useEffect(() => {
if (hash) {
let targetId: string;
try {
targetId = decodeURIComponent(hash.slice(1));
} 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' });
});
return () => window.cancelAnimationFrame(frame);
}, [pathname, hash]);
return null;
}

View File

@@ -1,38 +0,0 @@
import { Text, Timeline } from "@mantine/core";
import { IconDeviceDesktop, IconKering, IconKey, IconMessage, IconServer } from "@tabler/icons-react";
export function Sender() {
return (
<Timeline active={0} bulletSize={40} lineWidth={2}>
<Timeline.Item bullet={<IconMessage size={20} />} title="Enter message">
<Text c="dimmed" size="sm">
You enter message text in chat.
</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconDeviceDesktop size={20} />} title="Encrypt for you">
<Text c="dimmed" size="sm">
Message encrypted for your device using AES-256 encryption.
</Text>
<Text size="xs" mt={4}>Need for safety save message on your device.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconKey size={20} />} title="Encrypt for opponent">
<Text c="dimmed" size="sm">
Encrypt message for recipient using ChaCha20 encryption.
</Text>
<Text size="xs" mt={4}>Encrypt message with chacha20 with key.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconKering size={20} />} title="Encrypt ChaCha20 key to send">
<Text c="dimmed" size="sm">
Encrypt key of previous step with recipient public key using RSA-4096 encryption.
</Text>
<Text size="xs" mt={4}>Needs for send encrypted key to opponent.</Text>
</Timeline.Item>
<Timeline.Item bullet={<IconServer size={20} />} title="Send encrypted data to server">
<Text c="dimmed" size="sm">
Send encrypted message and encrypted ChaCha20 key to server for delivery to recipient.
</Text>
<Text size="xs" mt={4}>See next steps in recipient timeline.</Text>
</Timeline.Item>
</Timeline>
)
}

View File

@@ -0,0 +1,16 @@
.footer {
padding: 72px 0 32px;
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
}
.footer a {
width: fit-content;
text-decoration: none;
}
.copyright {
margin-top: 56px;
padding-top: var(--mantine-spacing-lg);
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
}

View File

@@ -0,0 +1,78 @@
import { Anchor, Container, Group, SimpleGrid, Stack, Text } from '@mantine/core';
import { Link } from 'react-router-dom';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
import classes from './SiteFooter.module.css';
export function SiteFooter() {
const year = new Date().getFullYear();
return (
<footer className={classes.footer}>
<Container>
<SimpleGrid cols={{ base: 1, xs: 2, md: 4 }} spacing="xl">
<Stack gap="sm">
<Group gap="sm">
<RosettaLogo size={34} />
<Text fw={700} fz="lg">
Rosetta
</Text>
</Group>
<Text c="dimmed" size="sm" maw={320} lh={1.6}>
Private messaging built around strong encryption, local history, and
user-controlled identity.
</Text>
</Stack>
<Stack gap="xs">
<Text fw={600} size="sm">
Product
</Text>
<Anchor component={Link} to="/#features" c="dimmed" size="sm">
Features
</Anchor>
<Anchor component={Link} to="/#apps" c="dimmed" size="sm">
Apps
</Anchor>
<Anchor component={Link} to="/support" c="dimmed" size="sm">
Support
</Anchor>
</Stack>
<Stack gap="xs">
<Text fw={600} size="sm">
Developers
</Text>
<Anchor component={Link} to="/proto" c="dimmed" size="sm">
Proto
</Anchor>
<Anchor component={Link} to="/encryption" c="dimmed" size="sm">
Encryption
</Anchor>
<Anchor component={Link} to="/rcc" c="dimmed" size="sm">
RCC
</Anchor>
</Stack>
<Stack gap="xs">
<Text fw={600} size="sm">
Legal
</Text>
<Anchor component={Link} to="/privacy" c="dimmed" size="sm">
Privacy Policy
</Anchor>
<Anchor component={Link} to="/terms" c="dimmed" size="sm">
Terms of Use
</Anchor>
<Anchor href="mailto:support@rosetta.im" c="dimmed" size="sm">
support@rosetta.im
</Anchor>
</Stack>
</SimpleGrid>
<Text c="dimmed" size="xs" className={classes.copyright}>
&copy; {year} ROSETTA CLOUD SERVICES LIMITED
</Text>
</Container>
</footer>
);
}

View File

@@ -0,0 +1,63 @@
.header {
position: sticky;
z-index: 50;
top: 0;
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
background: light-dark(rgba(255, 255, 255, 0.92), rgba(26, 27, 30, 0.92));
backdrop-filter: blur(16px);
}
.inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 72px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
text-decoration: none !important;
}
.brand:hover {
text-decoration: none !important;
}
.brand img {
border-radius: 9px;
}
.link {
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
font-size: var(--mantine-font-size-sm);
font-weight: 500;
text-decoration: none !important;
transition: color 120ms ease;
}
.link:hover,
.link[data-active] {
color: var(--mantine-color-blue-6);
text-decoration: none !important;
}
.burger {
display: none;
}
.drawerTitle {
font-weight: 700;
}
@media (max-width: 48em) {
.navigation {
display: none;
}
.burger {
display: block;
}
}

View File

@@ -0,0 +1,82 @@
import {
Anchor,
Burger,
Container,
Drawer,
Group,
Stack,
Text,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { Link, useLocation } from 'react-router-dom';
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
import classes from './SiteHeader.module.css';
const NAVIGATION = [
{ label: 'Features', to: '/#features' },
{ label: 'Apps', to: '/#apps' },
{ label: 'Encryption', to: '/encryption' },
{ label: 'Proto', to: '/proto' },
{ label: 'Support', to: '/support' },
];
export function SiteHeader() {
const [opened, { close, toggle }] = useDisclosure(false);
const { pathname } = useLocation();
const links = NAVIGATION.map(({ label, to }) => {
const active =
(to === '/proto' && pathname.startsWith('/proto')) ||
(to === '/encryption' && pathname === '/encryption') ||
(to === '/support' && pathname === '/support');
return (
<Anchor
key={to}
component={Link}
to={to}
onClick={close}
className={classes.link}
data-active={active || undefined}
>
{label}
</Anchor>
);
});
return (
<header className={classes.header}>
<Container className={classes.inner}>
<Anchor component={Link} to="/" className={classes.brand} aria-label="Rosetta">
<RosettaLogo size={36} />
<Text component="span" fw={700} fz="lg">
Rosetta
</Text>
</Anchor>
<Group component="nav" gap="xl" className={classes.navigation}>
{links}
</Group>
<Burger
opened={opened}
onClick={toggle}
size="sm"
className={classes.burger}
aria-label="Toggle navigation"
/>
</Container>
<Drawer
opened={opened}
onClose={close}
position="right"
size="xs"
title="Rosetta"
classNames={{ title: classes.drawerTitle }}
>
<Stack gap="lg">{links}</Stack>
</Drawer>
</header>
);
}

View File

@@ -1,37 +0,0 @@
.root {
position: relative;
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
width: fit-content;
border-radius: var(--mantine-radius-md);
padding: 5px;
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
}
.control {
padding: 7px 12px;
line-height: 1;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-2));
border-radius: var(--mantine-radius-md);
font-size: var(--mantine-font-size-sm);
transition: color 100ms ease;
font-weight: 500;
@mixin hover {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
}
&[data-active] {
color: var(--mantine-color-white);
}
}
.controlLabel {
position: relative;
z-index: 1;
}
.indicator {
background-color: var(--mantine-primary-color-filled);
border-radius: var(--mantine-radius-md);
}

View File

@@ -1,51 +0,0 @@
import { useState } from 'react';
import { FloatingIndicator, UnstyledButton } from '@mantine/core';
import classes from './Switch.module.css';
interface SwitchProps {
data: string[];
onChange?: (value: string) => void;
}
export function Switch(props: SwitchProps) {
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
const [controlsRefs, setControlsRefs] = useState<Record<string, HTMLButtonElement | null>>({});
const [active, setActive] = useState(0);
const setControlRef = (index: number) => (node: HTMLButtonElement) => {
controlsRefs[index] = node;
setControlsRefs(controlsRefs);
};
const changeActive = (index: number) => {
setActive(index);
if(!props.onChange){
return;
}
props.onChange(props.data[index]);
}
const controls = props.data.map((item, index) => (
<UnstyledButton
key={item}
className={classes.control}
ref={setControlRef(index)}
onClick={() => changeActive(index)}
mod={{ active: active === index }}
>
<span className={classes.controlLabel}>{item}</span>
</UnstyledButton>
));
return (
<div className={classes.root} ref={setRootRef}>
{controls}
<FloatingIndicator
target={controlsRefs[active]}
parent={rootRef}
className={classes.indicator}
/>
</div>
);
}

3843
src/generated/protoDocs.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,46 @@
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
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(
<App /> <BrowserRouter>
<App />
</BrowserRouter>
) )

View File

@@ -0,0 +1,78 @@
.subheading {
margin: 28px 0 10px;
color: light-dark(var(--mantine-color-dark-8), var(--mantine-color-dark-0));
font-size: 17px;
font-weight: 650;
}
.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;
}
.table {
margin: 18px 0 24px;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
}
.references {
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
font-size: var(--mantine-font-size-sm);
line-height: 1.7;
}
.callout {
margin: 18px 0 26px;
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));
}
.vectorGroup {
margin: 18px 0 24px;
border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
}
.vectorValue {
padding: 14px 0;
border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
}
.vectorValue code {
display: block;
margin-top: 7px;
padding: 10px 12px;
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
font-size: 11px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
}
.marker {
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;
}
.result {
margin: 18px 0;
padding: 12px 14px;
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-red-9), var(--mantine-color-red-2));
font-size: var(--mantine-font-size-sm);
font-weight: 400;
}

View File

@@ -0,0 +1,412 @@
import { Anchor, Box, Code, List, Paper, Table, Text, Title } from '@mantine/core';
import {
LegalList,
LegalPage,
LegalSection,
LegalText,
} from '../components/LegalPage/LegalPage';
import { SEO } from '../components/SEO/SEO';
import classes from './EncryptionPage.module.css';
const ATTACHMENT_LAYOUT = [
['Magic', '4 bytes', 'ASCII RAC1 (52 41 43 31)'],
['Salt', '16 bytes', 'Random per attachment encryption session'],
['IV', '12 bytes', 'Random for every encrypted chunk'],
['Ciphertext + tag', 'Variable', 'AES-256-GCM output and authentication tag'],
];
const MESSAGE_TEST_VECTOR = {
plaintextHex: '48656c6c6f20526f7365747461',
contentKey: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
nonce: '202122232425262728292a2b2c2d2e2f3031323334353637',
ciphertextAndTag: '553c21a7141746e44bdb24c9299d28c1e78d59d10231cdfe5d9aa1d200',
recipientPrivate: '0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20',
recipientPublic:
'0484bf7562262bbd6940085748f3be6afa52ae317155181ece31b66351ccffa4b08cc43d63b2859d469fee15f31c9edb5324266e6fd0407e87382d60fc4511acd8',
ephemeralPrivate: '2122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40',
ephemeralPublic:
'04207bba70bc66309baa582a6ac120fd52d68026c51f6326f8ccedcbd2c1b7eb828c18ff7dbee879a4335a05294dea1e99e251f4b3e3b020b507f87064993fb202',
sharedPoint:
'04f40bb548d147b2c3cdcf635658c3fca87a3c1f85c7ac006e9b9e80d4dc70d84de0be123a86ba5e43cadc1364b8e4ea9542e63a1a8ad1a50263c6080826ef2357',
wrappingKey: 'f40bb548d147b2c3cdcf635658c3fca87a3c1f85c7ac006e9b9e80d4dc70d84d',
wrappingIv: '4142434445464748494a4b4c4d4e4f50',
messageSecret:
'000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637',
wrappedSecret:
'44651a40b3dd1ceecbb8f2acfd8229e60a6554fbfaea9838142e898c89d18ede34cc3328ddb3cab3eea77008120228ba4518a0b9b8022a510b79565b806fd208',
envelope:
'NDE0MjQzNDQ0NTQ2NDc0ODQ5NGE0YjRjNGQ0ZTRmNTA6NDQ2NTFhNDBiM2RkMWNlZWNiYjhmMmFjZmQ4MjI5ZTYwYTY1NTRmYmZhZWE5ODM4MTQyZTg5OGM4OWQxOGVkZTM0Y2MzMzI4ZGRiM2NhYjNlZWE3NzAwODEyMDIyOGJhNDUxOGEwYjliODAyMmE1MTBiNzk1NjViODA2ZmQyMDg6MDQyMDdiYmE3MGJjNjYzMDliYWE1ODJhNmFjMTIwZmQ1MmQ2ODAyNmM1MWY2MzI2ZjhjY2VkY2JkMmMxYjdlYjgyOGMxOGZmN2RiZWU4NzlhNDMzNWEwNTI5NGRlYTFlOTllMjUxZjRiM2UzYjAyMGI1MDdmODcwNjQ5OTNmYjIwMg==',
};
function TechnicalCode({ children }: { children: string }) {
return (
<Code block className={classes.code}>
{children}
</Code>
);
}
function TechnicalValue({ label, value }: { label: string; value: string }) {
return (
<Box className={classes.vectorValue}>
<Text fw={650} size="xs" c="dimmed">
{label}
</Text>
<Code block>{value}</Code>
</Box>
);
}
export function EncryptionPage() {
return (
<>
<SEO
title="Rosetta Encryption"
description="Technical documentation for Rosetta message, key-envelope, group, local-storage, and attachment encryption."
canonical="https://rosetta.im/encryption/"
keywords="Rosetta encryption, XChaCha20-Poly1305, AES-256-GCM, secp256k1, encrypted attachments"
/>
<LegalPage eyebrow="Technical documentation" title="Encryption">
<LegalText lead>
This document describes the current Rosetta desktop implementation. It covers
direct messages, per-message keys, recipient key envelopes, group messages,
local storage, and encrypted attachments. It is an implementation reference,
not a claim of formal verification or an independent security audit.
</LegalText>
<LegalSection title="1. Hybrid encryption model">
<LegalText>
Rosetta does not encrypt an entire message or file with public-key
cryptography. Public-key operations are comparatively slow and are not
designed for large payloads. They are used only to establish and protect
the small secret required by the content cipher.
</LegalText>
<LegalText>
The message text itself is encrypted with a symmetric AEAD cipher because
symmetric cryptography is fast and appropriate for arbitrary amounts of
data. In other words, the asymmetric part protects the key; the unique
symmetric key protects the message.
</LegalText>
<LegalText>
Every direct message receives its own fresh 32-byte content key and its own
fresh 24-byte nonce. A key is not reused as the general content key for the
next message.
</LegalText>
</LegalSection>
<LegalSection title="2. Direct-message encryption">
<Title order={3} className={classes.subheading}>
Content cipher
</Title>
<LegalText>
The client generates the content key and nonce with the platform
cryptographic random number generator. XChaCha20-Poly1305 encrypts the
UTF-8 message and produces an authentication tag. A modified ciphertext or
an incorrect key causes decryption to fail.
</LegalText>
<TechnicalCode>{`contentKey = randomBytes(32) # 256 bits
nonce = randomBytes(24) # 192 bits
ciphertext = XChaCha20-Poly1305(contentKey, nonce, UTF8(message))`}</TechnicalCode>
<Title order={3} className={classes.subheading}>
Recipient key envelope
</Title>
<LegalList>
<List.Item>
The client creates an ephemeral secp256k1 key pair for the message.
</List.Item>
<List.Item>
ECDH combines the ephemeral private key with the recipient&apos;s
long-term public key.
</List.Item>
<List.Item>
The x-coordinate of the shared point is used as a 32-byte AES wrapping
key.
</List.Item>
<List.Item>
The 56-byte message secret — content key followed by nonce — is wrapped
with AES-CBC and PKCS#7 padding using a random 16-byte IV.
</List.Item>
<List.Item>
The envelope contains the IV, encrypted message secret, and ephemeral
public key. The content key is never sent in plaintext.
</List.Item>
</LegalList>
<TechnicalCode>{`messageSecret = contentKey || nonce
ephemeral = secp256k1.randomKeyPair()
sharedPoint = ECDH(ephemeral.private, recipient.public)
wrappingKey = sharedPoint.x[0..31]
keyEnvelope = Base64(iv || AES-CBC(wrappingKey, messageSecret) || ephemeral.public)`}</TechnicalCode>
<LegalText>
The packet sent to the service contains routing identifiers, timestamp,
message ID, encrypted content, recipient key envelope, and attachment
descriptors. The service does not receive the direct-message content key
or plaintext message.
</LegalText>
</LegalSection>
<LegalSection title="3. Reproducible example: Hello Rosetta">
<Paper className={classes.callout} radius="sm" p="md">
<Text size="sm" lh={1.7}>
<strong>Test vector.</strong> The values below were calculated with the
same XChaCha20-Poly1305, secp256k1 ECDH, and AES-CBC operations used by
the client. Fixed keys and IVs make the result reproducible; they are
deliberately unsafe for production and must never be reused.
</Text>
</Paper>
<Title order={3} className={classes.subheading}>
Sender: encode and encrypt the message
</Title>
<LegalText>
The UTF-8 string <Code>Hello Rosetta</Code> occupies 13 bytes. The sender
would normally generate the content key and nonce randomly. This example
substitutes fixed sequences so that every value can be checked exactly.
</LegalText>
<Box className={classes.vectorGroup}>
<TechnicalValue label="Plaintext (UTF-8)" value="Hello Rosetta" />
<TechnicalValue
label="Plaintext bytes (hex)"
value={MESSAGE_TEST_VECTOR.plaintextHex}
/>
<TechnicalValue
label="Content key · 32 bytes (test only)"
value={MESSAGE_TEST_VECTOR.contentKey}
/>
<TechnicalValue
label="XChaCha20 nonce · 24 bytes (test only)"
value={MESSAGE_TEST_VECTOR.nonce}
/>
<TechnicalValue
label="XChaCha20-Poly1305 ciphertext + 16-byte authentication tag (hex)"
value={MESSAGE_TEST_VECTOR.ciphertextAndTag}
/>
</Box>
<LegalText>
The result contains 13 encrypted content bytes followed by the 16-byte
Poly1305 authentication tag. Any change to either part causes{' '}
<Text component="span" className={classes.marker}>
authenticated decryption to fail
</Text>
.
</LegalText>
<Title order={3} className={classes.subheading}>
Sender: protect the message key for the recipient
</Title>
<LegalText>
The 32-byte content key and 24-byte nonce are joined into one logical
56-byte message secret. An ephemeral secp256k1 key pair and the recipient
public key produce the shared ECDH point. Its 32-byte x-coordinate becomes
the AES key that wraps the message secret.
</LegalText>
<Box className={classes.vectorGroup}>
<TechnicalValue
label="Recipient private key (test only)"
value={MESSAGE_TEST_VECTOR.recipientPrivate}
/>
<TechnicalValue
label="Recipient public key · uncompressed"
value={MESSAGE_TEST_VECTOR.recipientPublic}
/>
<TechnicalValue
label="Ephemeral private key (test only)"
value={MESSAGE_TEST_VECTOR.ephemeralPrivate}
/>
<TechnicalValue
label="Ephemeral public key · uncompressed"
value={MESSAGE_TEST_VECTOR.ephemeralPublic}
/>
<TechnicalValue
label="ECDH shared point"
value={MESSAGE_TEST_VECTOR.sharedPoint}
/>
<TechnicalValue
label="AES wrapping key · shared-point x-coordinate"
value={MESSAGE_TEST_VECTOR.wrappingKey}
/>
<TechnicalValue
label="Message secret · content key || nonce"
value={MESSAGE_TEST_VECTOR.messageSecret}
/>
<TechnicalValue
label="AES-CBC IV · 16 bytes (test only)"
value={MESSAGE_TEST_VECTOR.wrappingIv}
/>
<TechnicalValue
label="Wrapped message secret · AES-CBC + PKCS#7 (hex)"
value={MESSAGE_TEST_VECTOR.wrappedSecret}
/>
<TechnicalValue
label="Transmitted key envelope · Base64(ivHex:cipherHex:ephemeralPublicHex)"
value={MESSAGE_TEST_VECTOR.envelope}
/>
</Box>
<Title order={3} className={classes.subheading}>
Recipient: recover and decrypt
</Title>
<LegalList>
<List.Item>
Base64 decoding separates the wrapping IV, wrapped message secret, and
ephemeral public key.
</List.Item>
<List.Item>
ECDH with the recipient private key and the transmitted ephemeral
public key produces the same shared point and wrapping key.
</List.Item>
<List.Item>
AES-CBC decryption and PKCS#7 removal recover the exact 56-byte message
secret shown above.
</List.Item>
<List.Item>
The first 32 bytes become the XChaCha20 key and the remaining 24 bytes
become its nonce. Poly1305 verifies the encrypted content before it is
accepted.
</List.Item>
</LegalList>
<TechnicalCode>{`recovered shared point matches: true
recovered message secret: ${MESSAGE_TEST_VECTOR.messageSecret}
XChaCha20-Poly1305 decrypt: 48656c6c6f20526f7365747461
UTF-8 decode: Hello Rosetta`}</TechnicalCode>
<Text className={classes.result}>
Authenticated plaintext recovered: Hello Rosetta
</Text>
<LegalText>
In a real message, the content key, nonce, ephemeral private key, and
AES-CBC IV are newly generated. Therefore the ciphertext and envelope will
differ even when the plaintext and recipient are identical.
</LegalText>
</LegalSection>
<LegalSection title="4. Attachment encryption">
<LegalText>
Files, images, audio, and video are encrypted before upload. Binary data is
split into independent chunks so that large files can be streamed, retried,
cached, and decrypted by range without loading the complete file into
memory.
</LegalText>
<LegalText>
The attachment password is derived from the secret of the message that
carries the attachment. PBKDF2-SHA-256 with 100,000 iterations and a random
16-byte salt derives an AES-256-GCM key. Each chunk receives a new random
12-byte IV. AES-GCM authenticates the encrypted chunk and rejects modified
data before plaintext is returned.
</LegalText>
<TechnicalCode>
RAC1 || salt[16] || iv[12] || AES-256-GCM-ciphertext-and-tag
</TechnicalCode>
<Table.ScrollContainer minWidth={560}>
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
<Table.Thead>
<Table.Tr>
<Table.Th>Segment</Table.Th>
<Table.Th>Length</Table.Th>
<Table.Th>Purpose</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{ATTACHMENT_LAYOUT.map(([segment, length, purpose]) => (
<Table.Tr key={segment}>
<Table.Td>{segment}</Table.Td>
<Table.Td>{length}</Table.Td>
<Table.Td>{purpose}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
<LegalText>
The CDN stores ciphertext and transfer metadata such as object ID, size,
and chunk count. It does not receive the attachment decryption secret.
</LegalText>
</LegalSection>
<LegalSection title="5. Groups and compatibility format">
<LegalText>
A group uses a shared group secret available to its members. Group messages
do not carry a new recipient envelope for every member. The current group
and inline-payload compatibility codec compresses the input with DEFLATE,
derives an AES key with PBKDF2, and encrypts it with AES-CBC using a random
IV.
</LegalText>
<TechnicalCode>{`input
→ DEFLATE
→ PBKDF2-SHA-256(password, "rosetta", 1,000 iterations)
→ AES-CBC with a random 16-byte IV
→ Base64(iv) : Base64(ciphertext)`}</TechnicalCode>
<LegalText>
This compatibility format provides confidentiality but does not include an
AEAD authentication tag. Binary attachments use the RAC1 AES-GCM
format described above.
</LegalText>
</LegalSection>
<LegalSection title="6. Account keys and local storage">
<LegalText>
The recovery phrase is expanded to a BIP39 seed. SHA-256 of that seed
becomes the 32-byte secp256k1 private key, and the client derives the
compressed public key used as the account identity.
</LegalText>
<LegalText>
The private key, recovery phrase, readable message cache, and sender-side
copy of the message secret are encrypted locally with material derived from
the user&apos;s password or account private key. The plaintext account
password is not sent to the service.
</LegalText>
</LegalSection>
<LegalSection title="7. Security boundaries">
<LegalText>
End-to-end encryption protects direct-message content and binary attachment
contents. It does not hide sender and recipient identifiers, timestamps,
message IDs, delivery state, attachment size, object IDs, network address,
or request timing.
</LegalText>
<LegalText>
The direct-message ciphertext has AEAD integrity, but the current recipient
key envelope is not a digital signature. Compromise of a recipient&apos;s
long-term private key can expose previously captured key envelopes, so the
current design does not claim long-term forward secrecy. Group keys also
require rotation when membership changes.
</LegalText>
</LegalSection>
<LegalSection title="8. Terminology and references">
<LegalList>
<List.Item>
<strong>AEAD:</strong> authenticated encryption that provides
confidentiality and detects modification.
</List.Item>
<List.Item>
<strong>ECDH:</strong> elliptic-curve key agreement used to calculate a
shared secret without transmitting it.
</List.Item>
<List.Item>
<strong>Nonce / IV:</strong> a value that must not repeat for a given
key and encryption mode.
</List.Item>
<List.Item>
<strong>Forward secrecy:</strong> protection of old session keys after a
long-term key is compromised.
</List.Item>
</LegalList>
<Text className={classes.references}>
References:{' '}
<Anchor href="https://csrc.nist.gov/pubs/sp/800/38/d/final">
NIST SP 800-38D
</Anchor>
,{' '}
<Anchor href="https://www.rfc-editor.org/rfc/rfc8018">RFC 8018</Anchor>,{' '}
<Anchor href="https://www.secg.org/sec2-v2.pdf">SEC 2</Anchor>, and{' '}
<Anchor href="https://github.com/paulmillr/noble-ciphers">
noble-ciphers
</Anchor>
.
</Text>
</LegalSection>
</LegalPage>
</>
);
}

View File

@@ -0,0 +1,159 @@
.hero {
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
.heroContainer {
display: grid;
place-items: center;
min-height: 620px;
padding-top: 88px;
padding-bottom: 96px;
}
.heroCopy {
width: 100%;
max-width: 760px;
}
.heroTitle {
margin-top: 30px;
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-size: clamp(44px, 6vw, 64px);
font-weight: 600;
letter-spacing: -0.04em;
line-height: 1.06;
text-align: center;
}
.heroDescription {
max-width: 650px;
margin-top: 24px;
font-size: 19px;
line-height: 1.65;
}
.featuresSection,
.appsSection,
.encryptionSection {
padding: 104px 0;
}
.featuresSection {
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
}
.appsSection {
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
.sectionHeading {
max-width: 720px;
}
.sectionHeading h2 {
margin-top: var(--mantine-spacing-xs);
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-size: clamp(34px, 5vw, 48px);
font-weight: 600;
letter-spacing: -0.035em;
line-height: 1.1;
}
.sectionHeading > p:last-child {
margin-top: var(--mantine-spacing-md);
font-size: 18px;
line-height: 1.65;
}
.featureTitle,
.appTitle {
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
font-size: 19px;
font-weight: 600;
}
.appCard {
min-height: 280px;
border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
}
.downloadButton {
min-height: 42px;
}
.encryptionSection {
padding-top: 0;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
.encryptionCard {
display: flex;
align-items: center;
justify-content: space-between;
gap: 48px;
padding: 52px;
background: linear-gradient(135deg, var(--mantine-color-blue-8), var(--mantine-color-blue-5));
}
.encryptionCard h2 {
font-size: clamp(30px, 4vw, 42px);
font-weight: 600;
letter-spacing: -0.03em;
line-height: 1.12;
}
.encryptionButton {
flex: 0 0 auto;
color: var(--mantine-color-blue-8) !important;
}
@media (max-width: 62em) {
.heroContainer {
min-height: 560px;
}
}
@media (max-width: 48em) {
.heroContainer {
min-height: 500px;
padding-top: 64px;
padding-bottom: 64px;
}
.heroTitle {
font-size: clamp(40px, 12vw, 54px);
}
.heroDescription {
font-size: 18px;
}
.featuresSection,
.appsSection,
.encryptionSection {
padding: 76px 0;
}
.encryptionSection {
padding-top: 0;
}
.encryptionCard {
align-items: flex-start;
flex-direction: column;
padding: 34px 28px;
}
}
@media (max-width: 36em) {
.heroActions {
align-items: stretch;
flex-direction: column;
width: 100%;
}
.heroActions :global(.mantine-Button-root) {
width: 100%;
}
}

334
src/pages/LandingPage.tsx Normal file
View File

@@ -0,0 +1,334 @@
import {
Box,
Button,
Card,
Container,
Group,
Paper,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
} from '@mantine/core';
import {
IconArrowDown,
IconBolt,
IconBrandAndroid,
IconBrandApple,
IconBrandWindows,
IconCode,
IconDatabaseOff,
IconDeviceDesktop,
IconFileTextShield,
IconLock,
IconPhoneOff,
type TablerIcon,
} from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { RosettaLogo } from '../components/RosettaLogo/RosettaLogo';
import { SEO } from '../components/SEO/SEO';
import { SiteFooter } from '../components/SiteFooter/SiteFooter';
import { SiteHeader } from '../components/SiteHeader/SiteHeader';
import { fetchUpdates, type DownloadRelease } from '../services/updates';
import classes from './LandingPage.module.css';
const FEATURES: Array<{ icon: TablerIcon; title: string; description: string }> = [
{
icon: IconLock,
title: 'Private by design',
description:
'Messages and media are encrypted before they leave your device, with keys kept under your control.',
},
{
icon: IconPhoneOff,
title: 'No phone number',
description:
'Create an identity without sharing a phone number, email address, or advertising profile.',
},
{
icon: IconDatabaseOff,
title: 'Local message history',
description:
'Readable message history lives on your devices instead of becoming a permanent server archive.',
},
{
icon: IconFileTextShield,
title: 'Encrypted media',
description:
'Photos, videos, voice messages, and files use the same privacy-first transport model.',
},
{
icon: IconBolt,
title: 'Fast across platforms',
description:
'A focused protocol and native clients keep everyday conversations responsive on desktop and mobile.',
},
{
icon: IconCode,
title: 'Documented protocol',
description:
'Every packet, type, enum, and field is published from the protocol source schemas.',
},
];
const PLATFORMS: Array<{
key: string;
title: string;
description: string;
icon: TablerIcon;
}> = [
{
key: 'android',
title: 'Rosetta for Android',
description: 'Private messaging for Android phones and tablets.',
icon: IconBrandAndroid,
},
{
key: 'darwin',
title: 'Rosetta for macOS',
description: 'Native packages for Apple silicon and Intel-based Macs.',
icon: IconBrandApple,
},
{
key: 'win32',
title: 'Rosetta for Windows',
description: 'A native desktop client for Windows 10 and later.',
icon: IconBrandWindows,
},
{
key: 'linux',
title: 'Rosetta for Linux',
description: 'Desktop builds for x64 and ARM64 Linux systems.',
icon: IconDeviceDesktop,
},
];
function architectureName(arch: string) {
if (arch === 'arm64') {
return 'ARM64';
}
if (arch === 'universal') {
return 'Universal';
}
return arch;
}
function DownloadApps() {
const [releases, setReleases] = useState<DownloadRelease[]>([]);
const [loading, setLoading] = useState(true);
const [failed, setFailed] = useState(false);
useEffect(() => {
const controller = new AbortController();
let active = true;
fetchUpdates(controller.signal)
.then((items) => {
if (active) {
setReleases(items);
}
})
.catch((error: unknown) => {
if (active && error instanceof Error && error.name !== 'AbortError') {
setFailed(true);
}
})
.finally(() => {
if (active) {
setLoading(false);
}
});
return () => {
active = false;
controller.abort();
};
}, []);
const groupedReleases = useMemo(
() =>
releases.reduce<Record<string, DownloadRelease[]>>((groups, release) => {
groups[release.platform] ||= [];
groups[release.platform].push(release);
return groups;
}, {}),
[releases],
);
return (
<Box component="section" id="apps" className={classes.appsSection}>
<Container>
<Box className={classes.sectionHeading}>
<Text c="blue" fw={700} size="sm">
Rosetta Apps
</Text>
<Title order={2}>One conversation, every device.</Title>
<Text c="dimmed">
Select your platform. Available versions are loaded directly from the
Rosetta update service.
</Text>
</Box>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg" mt={48}>
{PLATFORMS.map(({ key, title, description, icon: Icon }) => {
const platformReleases = groupedReleases[key] || [];
return (
<Card key={key} withBorder radius="lg" padding="xl" className={classes.appCard}>
<ThemeIcon size={52} radius="xl" variant="light">
<Icon size={27} stroke={1.6} />
</ThemeIcon>
<Title order={3} mt="lg" className={classes.appTitle}>
{title}
</Title>
<Text c="dimmed" size="sm" mt="xs" lh={1.6}>
{description}
</Text>
<Stack gap="xs" mt="xl">
{platformReleases.map((release) => (
<Button
key={release.link}
component="a"
href={release.link}
variant="light"
justify="space-between"
rightSection={<IconArrowDown size={16} />}
className={classes.downloadButton}
>
{architectureName(release.arch)} · {release.version}
</Button>
))}
{loading && (
<Text c="dimmed" size="sm">
Checking the latest release…
</Text>
)}
{!loading && platformReleases.length === 0 && (
<Text c="dimmed" size="sm">
{failed
? 'Release service is temporarily unavailable.'
: 'No active release for this platform.'}
</Text>
)}
</Stack>
</Card>
);
})}
</SimpleGrid>
</Container>
</Box>
);
}
export function LandingPage() {
return (
<>
<SEO
title="Rosetta — Private Messaging"
description="Rosetta is private messaging built around strong encryption, local message history, and user-controlled identity."
canonical="https://rosetta.im/"
keywords="Rosetta, encrypted messaging, private messenger, secure chat"
/>
<SiteHeader />
<main>
<Box component="section" className={classes.hero}>
<Container className={classes.heroContainer}>
<Stack align="center" gap={0} className={classes.heroCopy}>
<RosettaLogo size={82} />
<Title order={1} className={classes.heroTitle}>
Private messaging, simply.
</Title>
<Text className={classes.heroDescription} c="dimmed" ta="center">
Rosetta keeps identity, conversations, and media under your control
— without requiring a phone number or building a readable message
archive on the server.
</Text>
<Group mt={32} className={classes.heroActions}>
<Button component="a" href="#apps" size="lg" radius="md">
Download Rosetta
</Button>
<Button
component={Link}
to="/proto"
size="lg"
radius="md"
variant="default"
>
Explore Proto
</Button>
</Group>
</Stack>
</Container>
</Box>
<Box component="section" id="features" className={classes.featuresSection}>
<Container>
<Box className={classes.sectionHeading}>
<Text c="blue" fw={700} size="sm">
Why Rosetta
</Text>
<Title order={2}>Simple to use. Difficult to compromise.</Title>
<Text c="dimmed">
Privacy is built into the account model, transport, and storage
architecture — not added as an optional mode.
</Text>
</Box>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing={42} mt={56}>
{FEATURES.map(({ icon: Icon, title, description }) => (
<div key={title}>
<ThemeIcon size={44} radius="xl" variant="light">
<Icon size={21} stroke={1.7} />
</ThemeIcon>
<Title order={3} className={classes.featureTitle} mt="md">
{title}
</Title>
<Text c="dimmed" size="sm" mt="xs" lh={1.65}>
{description}
</Text>
</div>
))}
</SimpleGrid>
</Container>
</Box>
<DownloadApps />
<Box component="section" className={classes.encryptionSection}>
<Container>
<Paper radius="xl" className={classes.encryptionCard}>
<Box>
<Text c="blue.2" fw={700} size="sm">
Rosetta Encryption
</Text>
<Title order={2} c="white" mt="xs">
How Rosetta protects every message and attachment.
</Title>
<Text c="blue.0" mt="md" maw={650} lh={1.65}>
Follow the complete encryption path from per-message keys and
XChaCha20-Poly1305 to recipient key envelopes and authenticated
attachment chunks.
</Text>
</Box>
<Button
component={Link}
to="/encryption"
color="blue"
variant="white"
size="lg"
className={classes.encryptionButton}
>
Read Encryption Documentation
</Button>
</Paper>
</Container>
</Box>
</main>
<SiteFooter />
</>
);
}

191
src/pages/PrivacyPage.tsx Normal file
View File

@@ -0,0 +1,191 @@
import { Anchor, List } from '@mantine/core';
import { Link } from 'react-router-dom';
import {
LegalAddress,
LegalList,
LegalPage,
LegalSection,
LegalText,
} from '../components/LegalPage/LegalPage';
import { SEO } from '../components/SEO/SEO';
export function PrivacyPage() {
return (
<>
<SEO
title="Privacy Policy for Rosetta"
description="Privacy Policy for Rosetta by ROSETTA CLOUD SERVICES LIMITED: how data is handled in the end-to-end encrypted messenger."
canonical="https://rosetta.im/privacy/"
keywords="Rosetta privacy policy, encrypted messenger privacy, data protection"
/>
<LegalPage title="Privacy Policy" updated="May 22, 2026">
<LegalText lead>
Rosetta IM is operated by ROSETTA CLOUD SERVICES LIMITED (&quot;Rosetta&quot;,
&quot;we&quot;, &quot;us&quot;, or &quot;our&quot;). This Privacy Policy
explains how we handle information when you use Rosetta, our end-to-end
encrypted messaging application, and our website at https://rosetta.im.
</LegalText>
<LegalSection title="1. Information We Collect">
<LegalText>
Rosetta is designed to minimize data collection. Registration does not
require a phone number, email address, or real name. The app uses
cryptographic keys and technical identifiers needed to operate the service.
</LegalText>
<LegalList>
<List.Item>
Account and cryptographic data: public keys, account identifiers, and
related technical data needed to identify users and deliver encrypted
messages. Private keys are intended to remain on your device.
</List.Item>
<List.Item>
Encrypted messages and media in transit: message text, files, and media
are encrypted before they are sent. Our servers are designed to relay
encrypted payloads and encrypted message keys, not to read message
contents.
</List.Item>
<List.Item>
Device and app data: app version, operating system, platform,
architecture, update request data, IP address, timestamps, and server
logs needed for security, reliability, abuse prevention, and update
delivery.
</List.Item>
<List.Item>
Support communications: if you contact us, we may process your email
address, message content, and any information you choose to provide.
</List.Item>
</LegalList>
</LegalSection>
<LegalSection title="2. Information We Do Not Collect for Advertising">
<LegalText>
We do not use third-party advertising SDKs, advertising trackers, or
cross-app tracking. We do not sell your personal information.
</LegalText>
</LegalSection>
<LegalSection title="3. How We Use Information">
<LegalText>We use information only to:</LegalText>
<LegalList>
<List.Item>provide, maintain, and secure Rosetta;</List.Item>
<List.Item>route encrypted messages between users;</List.Item>
<List.Item>deliver app updates and downloads;</List.Item>
<List.Item>
detect abuse, prevent fraud, and protect service integrity;
</List.Item>
<List.Item>respond to support requests;</List.Item>
<List.Item>comply with legal obligations.</List.Item>
</LegalList>
</LegalSection>
<LegalSection title="4. End-to-End Encryption">
<LegalText>
Rosetta is designed so that message content is encrypted before it reaches
our servers. Our servers are not intended to have access to decrypted
message contents or decrypted media. Messages are stored on users&apos;
devices rather than as readable message history on our servers.
</LegalText>
</LegalSection>
<LegalSection title="5. Sharing of Information">
<LegalText>We may share limited information only with:</LegalText>
<LegalList>
<List.Item>
infrastructure and hosting providers that help us operate Rosetta;
</List.Item>
<List.Item>professional advisers, if necessary;</List.Item>
<List.Item>
authorities or other parties when required by law or to protect rights,
safety, and security.
</List.Item>
</LegalList>
<LegalText>
Any service provider that processes user data for us must protect it
consistently with this Privacy Policy.
</LegalText>
</LegalSection>
<LegalSection title="6. Retention">
<LegalText>
Encrypted message data in transit is processed only as needed to deliver the
service. Technical logs and support communications are retained only for as
long as reasonably necessary for security, reliability, support, legal,
and operational purposes.
</LegalText>
</LegalSection>
<LegalSection title="7. Your Choices and Rights">
<LegalText>
Depending on where you live, you may have rights to access, correct, delete,
or object to certain processing of your personal information. To make a
request, contact us at{' '}
<Anchor href="mailto:admin@rosetta.im">Admin@rosetta.im</Anchor>.
</LegalText>
<LegalText>
Because Rosetta minimizes account data and uses cryptographic identifiers,
some requests may require you to provide information that helps us verify
control of the relevant account or keys.
</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 title="8. Children">
<LegalText>
Rosetta is not directed to children under 13, or the minimum age required in
your jurisdiction. If we learn that we have collected personal information
from a child without required consent, we will take appropriate steps to
delete it.
</LegalText>
</LegalSection>
<LegalSection title="9. International Transfers">
<LegalText>
ROSETTA CLOUD SERVICES LIMITED may process information in countries other
than where you live. Where required, we use appropriate safeguards for
international transfers.
</LegalText>
</LegalSection>
<LegalSection title="10. Security">
<LegalText>
We use technical and organizational measures designed to protect
information. No system is perfectly secure, and users are responsible for
protecting their devices, private keys, and account access.
</LegalText>
</LegalSection>
<LegalSection title="11. Changes to This Policy">
<LegalText>
We may update this Privacy Policy from time to time. If we make material
changes, we will update the &quot;Last updated&quot; date and, where
appropriate, provide additional notice.
</LegalText>
</LegalSection>
<LegalSection title="12. Contact">
<LegalAddress>
{'ROSETTA CLOUD SERVICES LIMITED\nRegistered address:\nUNIT 704C 7/F BLK 3 NAN FUNG IND CITY\n18 TIN HAU RD\nTUEN MUN\nHONG KONG'}
</LegalAddress>
<LegalText>
Email: <Anchor href="mailto:admin@rosetta.im">Admin@rosetta.im</Anchor>
<br />
Support:{' '}
<Anchor component={Link} to="/support">
Rosetta Support
</Anchor>
</LegalText>
<LegalText>
Phone: <Anchor href="tel:+85228910030">+852 2891 0030</Anchor>
</LegalText>
</LegalSection>
</LegalPage>
</>
);
}

View File

@@ -0,0 +1,256 @@
.protoCode {
width: 100%;
min-width: 0;
max-width: 100%;
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;
}
.comparisonTable {
margin: 18px 0 24px;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
}
.protoSubheading {
margin: 30px 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;
}
.layerExamples {
min-width: 0;
margin: 18px 0 24px;
}
.layerExamples > * {
min-width: 0;
}
.layerExamples .protoCode {
height: calc(100% - 30px);
margin-bottom: 0;
}
.docsLinks {
margin-top: 44px;
gap: 28px;
}
.docsLink {
display: inline-block;
font-size: var(--mantine-font-size-sm);
font-weight: 600;
text-decoration: none;
}
.page {
padding: 40px 0 104px;
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
}
.stats {
margin: 0 0 52px;
}
.docsGrid {
display: grid;
width: 100%;
min-width: 0;
grid-template-columns: 220px minmax(0, 1fr);
gap: 44px;
align-items: start;
}
.sidebar {
position: sticky;
top: 92px;
min-width: 0;
max-height: calc(100vh - 112px);
padding-right: 10px;
overflow-y: auto;
scrollbar-width: thin;
}
.sidebar a {
width: fit-content;
max-width: 100%;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
text-decoration: none;
overflow-wrap: anywhere;
}
.sidebar a:hover {
color: var(--mantine-color-blue-6);
text-decoration: none;
}
.layerDescription {
min-width: 0;
padding: 12px;
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-white), var(--mantine-color-dark-8));
}
.layerDescription code {
display: inline-block;
max-width: 100%;
font-size: 11px;
overflow-wrap: anywhere;
}
.documentation {
min-width: 0;
}
.filters {
display: grid;
gap: var(--mantine-spacing-sm);
margin-bottom: 16px;
}
.schemaSection {
scroll-margin-top: 96px;
min-width: 0;
margin-top: 64px;
}
.schemaSection > h2 {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-size: 28px;
font-weight: 600;
letter-spacing: -0.02em;
}
.definition {
scroll-margin-top: 96px;
min-width: 0;
margin-top: 38px;
padding-top: 32px;
border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
}
.definition h3 {
margin-top: 5px;
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
font-size: 23px;
font-weight: 600;
letter-spacing: -0.015em;
}
.yamlDetails {
min-width: 0;
max-width: 100%;
margin: 20px 0;
}
.yamlSummary {
width: fit-content;
color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-3));
font-size: var(--mantine-font-size-sm);
font-weight: 600;
cursor: pointer;
user-select: none;
}
.yamlBlock {
min-width: 0;
max-width: 100%;
margin-top: 10px;
padding: 12px 12px 16px 18px;
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));
}
.yamlBlock code {
display: block;
width: 100%;
min-width: 0;
max-width: 100%;
padding: 0;
background: transparent;
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;
}
.table {
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
border-radius: var(--mantine-radius-xs);
}
.tableScroll {
width: 100%;
min-width: 0;
max-width: 100%;
}
.table th {
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
}
.typeLink {
font-family: var(--mantine-font-family-monospace);
font-size: var(--mantine-font-size-sm);
font-weight: 600;
text-decoration: none;
}
.primitive {
color: light-dark(var(--mantine-color-violet-8), var(--mantine-color-violet-3));
}
@media (max-width: 62em) {
.docsGrid {
grid-template-columns: minmax(0, 1fr);
}
.sidebar {
position: static;
max-height: none;
padding: var(--mantine-spacing-lg);
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
border-radius: var(--mantine-radius-sm);
overflow: visible;
}
}
@media (max-width: 48em) {
.page {
padding-top: 28px;
}
.docsGrid {
gap: 28px;
}
.schemaSection {
margin-top: 48px;
}
.definition {
margin-top: 32px;
}
}

999
src/pages/ProtoPage.tsx Normal file
View File

@@ -0,0 +1,999 @@
import {
ActionIcon,
Anchor,
Badge,
Box,
Code,
Container,
CopyButton,
Group,
List,
Paper,
Select,
SimpleGrid,
Stack,
Table,
Text,
TextInput,
ThemeIcon,
Title,
Tooltip,
} from '@mantine/core';
import {
IconBraces,
IconCheck,
IconCopy,
IconList,
IconPackage,
IconSearch,
} from '@tabler/icons-react';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { SEO } from '../components/SEO/SEO';
import {
LegalList,
LegalPage,
LegalSection,
LegalText,
} from '../components/LegalPage/LegalPage';
import { SiteFooter } from '../components/SiteFooter/SiteFooter';
import { SiteHeader } from '../components/SiteHeader/SiteHeader';
import { protoLayers } from '../generated/protoDocs';
import type {
ProtoEnum,
ProtoField,
ProtoLayer,
ProtoPacket,
ProtoType,
} from '../types/proto';
import classes from './ProtoPage.module.css';
const PRIMITIVE_TYPES = new Set([
'bool',
'bytes',
'double',
'float',
'int8',
'int16',
'int32',
'int64',
'string',
'uint8',
'uint16',
'uint32',
'uint64',
]);
function slug(value: string) {
return value.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/_/g, '-').toLowerCase();
}
function entityMatches(
entity: ProtoPacket | ProtoType | ProtoEnum,
query: string,
) {
if (!query) {
return true;
}
const searchable = [
entity.name,
entity.description,
'fields' in entity
? entity.fields.map((field) => `${field.name} ${field.type} ${field.description}`).join(' ')
: entity.values.map((value) => `${value.name} ${value.description}`).join(' '),
]
.join(' ')
.toLowerCase();
return searchable.includes(query);
}
function TypeReference({
type,
layer,
onNavigate,
}: {
type: string;
layer: ProtoLayer;
onNavigate: (targetId: string) => void;
}) {
const array = type.endsWith('[]');
const baseType = array ? type.slice(0, -2) : type;
const targetType = layer.types.find((item) => item.name === baseType);
const targetEnum = layer.enums.find((item) => item.name === baseType);
if (targetType || targetEnum) {
const targetId = `${targetType ? 'type' : 'enum'}-${slug(baseType)}`;
return (
<Anchor
href={`#${targetId}`}
onClick={(event) => {
event.preventDefault();
onNavigate(targetId);
}}
className={classes.typeLink}
>
{baseType}
{array && '[]'}
</Anchor>
);
}
if (PRIMITIVE_TYPES.has(baseType)) {
return <Code className={classes.primitive}>{type}</Code>;
}
return <Code>{type}</Code>;
}
function FieldTable({
fields,
layer,
onNavigate,
}: {
fields: ProtoField[];
layer: ProtoLayer;
onNavigate: (targetId: string) => void;
}) {
if (fields.length === 0) {
return (
<Text c="dimmed" size="sm">
This schema has no fields.
</Text>
);
}
return (
<Table.ScrollContainer minWidth={620} className={classes.tableScroll}>
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
<Table.Thead>
<Table.Tr>
<Table.Th>ID</Table.Th>
<Table.Th>Field</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Description</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{fields.map((field) => (
<Table.Tr key={`${field.id}-${field.name}`}>
<Table.Td>
<Code>{field.id}</Code>
</Table.Td>
<Table.Td>
<Text component="span" ff="monospace" fw={600} size="sm">
{field.name}
</Text>
{field.optional && (
<Badge size="xs" variant="light" ml="xs">
Optional
</Badge>
)}
</Table.Td>
<Table.Td>
<TypeReference
type={field.type}
layer={layer}
onNavigate={onNavigate}
/>
</Table.Td>
<Table.Td>
<Text c={field.description ? undefined : 'dimmed'} size="sm">
{field.description || 'No field description in the schema.'}
</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
);
}
function YamlSnippet({ value }: { value: string }) {
return (
<Box className={classes.yamlBlock}>
<Group justify="flex-end" mb="xs">
<CopyButton value={value} timeout={1600}>
{({ copied, copy }) => (
<Tooltip label={copied ? 'Copied' : 'Copy YAML'}>
<ActionIcon
onClick={copy}
variant="subtle"
color={copied ? 'teal' : 'blue'}
aria-label="Copy YAML schema"
>
{copied ? <IconCheck size={17} /> : <IconCopy size={17} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
<Code block>{value}</Code>
</Box>
);
}
function HiddenYaml({ value }: { value: string }) {
return (
<Box component="details" className={classes.yamlDetails}>
<Box component="summary" className={classes.yamlSummary}>
See YAML schema
</Box>
<YamlSnippet value={value} />
</Box>
);
}
function PacketDefinition({
packet,
layer,
onNavigate,
}: {
packet: ProtoPacket;
layer: ProtoLayer;
onNavigate: (targetId: string) => void;
}) {
return (
<Box component="article" id={`packet-${slug(packet.name)}`} className={classes.definition}>
<Text c="blue" fw={700} size="xs" tt="uppercase">
Packet · ID {packet.id}
</Text>
<Title order={3}>{packet.name}</Title>
<Text c="dimmed" mt="sm" lh={1.65}>
{packet.description || `Packet ${packet.name} in protocol layer ${layer.version}.`}
</Text>
<HiddenYaml value={packet.yaml} />
<FieldTable fields={packet.fields} layer={layer} onNavigate={onNavigate} />
</Box>
);
}
function TypeDefinition({
type,
layer,
onNavigate,
}: {
type: ProtoType;
layer: ProtoLayer;
onNavigate: (targetId: string) => void;
}) {
return (
<Box component="article" id={`type-${slug(type.name)}`} className={classes.definition}>
<Text c="blue" fw={700} size="xs" tt="uppercase">
Type
</Text>
<Title order={3}>{type.name}</Title>
<Text c="dimmed" mt="sm" lh={1.65}>
{type.description || `Type ${type.name} in protocol layer ${layer.version}.`}
</Text>
<HiddenYaml value={type.yaml} />
<FieldTable fields={type.fields} layer={layer} onNavigate={onNavigate} />
</Box>
);
}
function EnumDefinition({ item, layer }: { item: ProtoEnum; layer: ProtoLayer }) {
return (
<Box component="article" id={`enum-${slug(item.name)}`} className={classes.definition}>
<Text c="blue" fw={700} size="xs" tt="uppercase">
Enum · {item.type}
</Text>
<Title order={3}>{item.name}</Title>
<Text c="dimmed" mt="sm" lh={1.65}>
{item.description || `Enumeration ${item.name} in protocol layer ${layer.version}.`}
</Text>
<HiddenYaml value={item.yaml} />
<Table.ScrollContainer minWidth={520} className={classes.tableScroll}>
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
<Table.Thead>
<Table.Tr>
<Table.Th>Value</Table.Th>
<Table.Th>Name</Table.Th>
<Table.Th>Description</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{item.values.map((value) => (
<Table.Tr key={`${value.id}-${value.name}`}>
<Table.Td>
<Code>{value.id}</Code>
</Table.Td>
<Table.Td>
<Text ff="monospace" fw={600} size="sm">
{value.name}
</Text>
</Table.Td>
<Table.Td>
<Text c={value.description ? undefined : 'dimmed'} size="sm">
{value.description || 'No value description in the schema.'}
</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
</Box>
);
}
export function ProtoPage() {
return (
<>
<SEO
title="Rosetta Proto"
description="A technical overview of the Rosetta TLV protocol and its versioned schemas."
canonical="https://rosetta.im/proto/"
keywords="Rosetta protocol, TLV, packet schema, binary protocol"
/>
<LegalPage eyebrow="Protocol" title="Proto">
<LegalText lead>
Rosetta uses a compact binary protocol based on TLV fields. A packet starts
with a two-byte unsigned packet ID. The remaining payload is a sequence of
typed fields that can be decoded without relying on their physical order.
</LegalText>
<LegalSection title="Packet layout">
<LegalText>
Each field begins with a one-byte field ID and a one-byte wire type. Fixed
numeric and boolean types are followed directly by their value. Strings,
byte arrays, nested types, and other variable-length values include a
two-byte unsigned length before the value bytes.
</LegalText>
<Code block className={classes.protoCode}>
{`packet_id:uint16
field_id:uint8 | wire_type:uint8 | [length:uint16] | value
field_id:uint8 | wire_type:uint8 | [length:uint16] | value
...`}
</Code>
</LegalSection>
<LegalSection title="TLV behavior">
<LegalList>
<List.Item>
<strong>Tag:</strong> the decimal field ID defined in the selected
schema layer.
</List.Item>
<List.Item>
<strong>Type:</strong> the wire type used to interpret the value.
</List.Item>
<List.Item>
<strong>Length:</strong> present for strings, byte arrays, and nested
values; fixed-width primitives do not need it.
</List.Item>
<List.Item>
<strong>Value:</strong> the serialized field data in network byte order.
</List.Item>
<List.Item>
Optional fields are omitted when absent. Lists are represented by
repeating the same field ID. Nested types are encoded as
length-delimited TLV payloads.
</List.Item>
</LegalList>
</LegalSection>
<LegalSection title="Protocol versioning">
<LegalText>
Each <Code>layer.N.yml</Code> file is a complete protocol snapshot. Its
top-level <Code>version</Code> is the layer number. RCC sorts all layers by
that number, rejects duplicate versions, and builds a timeline for every
packet, field, type, and enum value. A newer layer repeats the definitions
that still exist and adds, removes, or makes fields optional as needed.
</LegalText>
<LegalText>
Packet IDs should remain stable, and RCC explicitly validates field-ID
reuse inside every packet and type. An existing field ID cannot be assigned
to a different field name or type in a later layer. A new field receives a
new ID; an optional field may be omitted by older peers. The server
generator keeps version-aware codecs for the supported range, while a
client generator produces the current layer.
</LegalText>
<LegalText>
RCC does not negotiate a layer over the network by itself. The connection
or handshake code selects a version supported by both peers, then passes
that number to the generated server codec. The registry exposes the
minimum and maximum compiled versions, and the codec includes only fields
whose lifetime contains the selected version. TLV fields that are not used
by that layer do not become properties of the decoded packet.
</LegalText>
<Title order={3} className={classes.protoSubheading}>
Two simple layers
</Title>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md" className={classes.layerExamples}>
<Box>
<Text fw={650} size="sm">Layer 1</Text>
<Code block className={classes.protoCode}>
{`version: 1
packets:
Profile:
id: 10
fields:
- id: 1
name: username
type: string`}
</Code>
</Box>
<Box>
<Text fw={650} size="sm">Layer 2</Text>
<Code block className={classes.protoCode}>
{`version: 2
packets:
Profile:
id: 10
fields:
- id: 1
name: username
type: string
- id: 2
name: displayName?
type: string`}
</Code>
</Box>
</SimpleGrid>
<LegalText>
Layer 2 preserves packet ID <Code>10</Code> and field ID
<Code> 1</Code>, then introduces optional field ID <Code>2</Code>. A layer 1
peer sends and reads only <Code>username</Code>. A layer 2 peer can include
<Code> displayName</Code>, but does not require it.
</LegalText>
<Title order={3} className={classes.protoSubheading}>
Simplified server output
</Title>
<LegalText>
In server mode RCC combines both snapshots into one model and a codec that
receives the negotiated version. The generated source is more verbose; the
following excerpt keeps only the versioning decisions.
</LegalText>
<Code block className={classes.protoCode}>
{`public class PacketProfile extends Packet {
public String username;
public String displayName; // introduced in layer 2
}
public class PacketProfileCodec {
private static final int F_USERNAME = 1;
private static final int F_DISPLAY_NAME = 2;
public PacketProfile decode(ByteBuf data, int version) {
TlvReader reader = new TlvReader(data);
PacketProfile packet = new PacketProfile();
packet.username = requireField(reader.getString(F_USERNAME));
if (version >= 2) {
packet.displayName = reader.getString(F_DISPLAY_NAME);
}
return packet;
}
public ByteBuf encode(PacketProfile packet, int version) {
TlvWriter writer = new TlvWriter();
writer.writeString(F_USERNAME, packet.username);
if (version >= 2 && packet.displayName != null) {
writer.writeString(F_DISPLAY_NAME, packet.displayName);
}
return writer.getBuffer();
}
}`}
</Code>
<LegalText>
RCC also writes the supported interval to the generated registry. A packet
codec can reject a layer older than the packet&apos;s introduction. Removed
fields remain available to the server model and codec only for the layers
in which they existed; enum values receive equivalent
<Code> isSupportedInVersion</Code> checks.
</LegalText>
<Code block className={classes.protoCode}>
{`public final class RccGeneratedPacketRegistry {
public static final int MIN_VERSION = 1;
public static final int MAX_VERSION = 2;
public static final int PACKET_PROFILE_ID = 10;
}`}
</Code>
<Title order={3} className={classes.protoSubheading}>
Latest client output
</Title>
<LegalText>
A TypeScript client is generated from the latest layer, so application code
sees the current shape and the optional marker directly.
</LegalText>
<Code block className={classes.protoCode}>
{`export class PacketProfile extends Packet {
username!: string;
displayName?: string;
}
export class RccGeneratedPacketRegistry {
static readonly VERSION = 2;
static readonly PACKET_PROFILE_ID = 10;
}`}
</Code>
</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">
<LegalText>
RCC reads versioned files named <Code>layer.N.yml</Code>. The top-level
<Code> version</Code> identifies the protocol layer, while
<Code> description</Code> records its purpose. Definitions are divided into
enumerations, reusable types, and packets. The YAML is a build-time schema;
it is not transmitted with every network request.
</LegalText>
<Code block className={classes.protoCode}>
{`version: 3
description: Add avatars and more CDN safety
enums:
HandshakeStage:
type: int8
values:
- id: 0
name: COMPLETED
- id: 1
name: NEED_DEVICE_VERIFICATION
types:
NetworkDevice:
fields:
- id: 1
name: networkStatus
type: NetworkStatus
- id: 2
name: deviceName
type: string
packets:
Handshake:
id: 0
fields:
- id: 1
name: publicKey
type: string
- id: 6
name: handshakeStage?
type: HandshakeStage`}
</Code>
<LegalList>
<List.Item>
An enum declares its integer wire type and the stable decimal ID of each
value.
</List.Item>
<List.Item>
A type is a reusable TLV structure. It has fields but no packet ID of
its own.
</List.Item>
<List.Item>
A packet has a two-byte packet ID. Each field has its own one-byte ID,
name, and primitive, enum, or reusable type.
</List.Item>
<List.Item>
A question mark at the end of a field name makes that field optional;
the suffix is removed from the generated property name. The field is
omitted from the wire payload when no value is present.
</List.Item>
<List.Item>
A <Code>[]</Code> suffix declares a list. RCC serializes a list by
repeating the same field ID for every item.
</List.Item>
<List.Item>
YAML comments become generated API documentation. RCC also checks type
references, required fields, and stable field-ID use between layers.
</List.Item>
</LegalList>
</LegalSection>
<LegalSection title="From schema to codec">
<LegalText>
RCC converts every definition into a model and a codec. Required fields are
checked before encoding and after decoding. Enum names remain readable in
application code, but only their numeric values are written to the network.
The server build can retain codecs for several layers while a client build
uses the current layer.
</LegalText>
<Code block className={classes.protoCode}>
{`# RCC schema
Result:
id: 2
fields:
- id: 1
name: resultCode
type: ResultCode
# Generated codec behavior
packet id 2
writer.writeInt8(fieldId: 1, value: ResultCode.SUCCESS /* 0 */)
reader.getInt8(fieldId: 1) -> ResultCode.SUCCESS`}
</Code>
</LegalSection>
<LegalSection title="Wire-size example compared with JSON">
<LegalText>
For a <Code>Result</Code> packet whose value is
<Code> ResultCode.SUCCESS</Code>, the complete RCC/TLV payload is five
bytes. The packet ID occupies two bytes; the field ID, wire type, and
<Code> int8</Code> value occupy one byte each.
</LegalText>
<Code block className={classes.protoCode}>
{`00 02 | 01 01 00
^^^^^ ^^^^^^^^
packet field 1 · INT8 · SUCCESS (0)`}
</Code>
<Table.ScrollContainer minWidth={560} className={classes.tableScroll}>
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.comparisonTable}>
<Table.Thead>
<Table.Tr>
<Table.Th>Encoding</Table.Th>
<Table.Th>Payload</Table.Th>
<Table.Th>Size</Table.Th>
<Table.Th>TLV reduction</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
<Table.Td>RCC / TLV</Table.Td>
<Table.Td><Code>00 02 01 01 00</Code></Table.Td>
<Table.Td>5 bytes</Table.Td>
<Table.Td>—</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>JSON with enum name</Table.Td>
<Table.Td><Code>{'{"resultCode":"SUCCESS"}'}</Code></Table.Td>
<Table.Td>24 bytes</Table.Td>
<Table.Td>79%</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>JSON with numeric enum</Table.Td>
<Table.Td><Code>{'{"resultCode":0}'}</Code></Table.Td>
<Table.Td>16 bytes</Table.Td>
<Table.Td>69%</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
</Table.ScrollContainer>
<LegalText>
This is a deliberately small packet and excludes transport framing and
compression. The percentage varies with the data: long strings and files
dominate their field names, while small control packets receive the largest
relative benefit from numeric IDs and binary values.
</LegalText>
</LegalSection>
<Group className={classes.docsLinks}>
<Anchor component={Link} to="/proto/docs" className={classes.docsLink}>
See docs of PROTO →
</Anchor>
<Anchor component={Link} to="/rcc" className={classes.docsLink}>
See docs of RCC →
</Anchor>
</Group>
</LegalPage>
</>
);
}
export function ProtoDocsPage() {
const latestLayer = protoLayers.at(-1)!;
const [selectedVersion, setSelectedVersion] = useState(String(latestLayer.version));
const [search, setSearch] = useState('');
const layer =
protoLayers.find((item) => String(item.version) === selectedVersion) || latestLayer;
const query = search.trim().toLowerCase();
const packets = layer.packets.filter((packet) => entityMatches(packet, query));
const types = layer.types.filter((type) => entityMatches(type, query));
const enums = layer.enums.filter((item) => entityMatches(item, query));
const resultCount = packets.length + types.length + enums.length;
const navigateToDefinition = (targetId: string) => {
setSearch('');
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
window.history.replaceState(null, '', `#${targetId}`);
document.getElementById(targetId)?.scrollIntoView({ block: 'start' });
});
});
};
return (
<>
<SEO
title="Rosetta Proto Documentation"
description="Generated Rosetta protocol documentation for packets, types, enums, fields, and schema layers."
canonical="https://rosetta.im/proto/docs/"
keywords="Rosetta protocol, packet schema, YAML documentation, Proto"
/>
<SiteHeader />
<main className={classes.page}>
<Container>
<Box className={classes.docsGrid}>
<aside className={classes.sidebar}>
<Stack gap="lg">
<Select
label="Protocol layer"
value={selectedVersion}
onChange={(value) => {
if (value) {
setSelectedVersion(value);
setSearch('');
}
}}
data={protoLayers.map((item) => ({
value: String(item.version),
label: `Layer ${item.version}${item === latestLayer ? ' · latest' : ''}`,
}))}
/>
<Box className={classes.layerDescription}>
<Text fw={700} size="xs" tt="uppercase" c="dimmed">
Description
</Text>
<Text size="xs" mt={6} lh={1.55}>
{layer.description || 'Initial protocol schema.'}
</Text>
<Code mt="xs">{layer.source}</Code>
</Box>
<Stack gap={6}>
<Text fw={700} size="xs" tt="uppercase" c="dimmed">
Contents
</Text>
<Anchor
href="#packets"
size="sm"
onClick={(event) => {
event.preventDefault();
navigateToDefinition('packets');
}}
>
Packets ({layer.packets.length})
</Anchor>
<Anchor
href="#types"
size="sm"
onClick={(event) => {
event.preventDefault();
navigateToDefinition('types');
}}
>
Types ({layer.types.length})
</Anchor>
<Anchor
href="#enums"
size="sm"
onClick={(event) => {
event.preventDefault();
navigateToDefinition('enums');
}}
>
Enums ({layer.enums.length})
</Anchor>
</Stack>
<Stack gap={4}>
<Text fw={700} size="xs" tt="uppercase" c="dimmed">
Packets
</Text>
{layer.packets.map((packet) => {
const targetId = `packet-${slug(packet.name)}`;
return (
<Anchor
key={packet.name}
href={`#${targetId}`}
size="xs"
onClick={(event) => {
event.preventDefault();
navigateToDefinition(targetId);
}}
>
{packet.name}
</Anchor>
);
})}
</Stack>
<Stack gap={4}>
<Text fw={700} size="xs" tt="uppercase" c="dimmed">
Types
</Text>
{layer.types.map((type) => {
const targetId = `type-${slug(type.name)}`;
return (
<Anchor
key={type.name}
href={`#${targetId}`}
size="xs"
onClick={(event) => {
event.preventDefault();
navigateToDefinition(targetId);
}}
>
{type.name}
</Anchor>
);
})}
</Stack>
<Stack gap={4}>
<Text fw={700} size="xs" tt="uppercase" c="dimmed">
Enums
</Text>
{layer.enums.map((item) => {
const targetId = `enum-${slug(item.name)}`;
return (
<Anchor
key={item.name}
href={`#${targetId}`}
size="xs"
onClick={(event) => {
event.preventDefault();
navigateToDefinition(targetId);
}}
>
{item.name}
</Anchor>
);
})}
</Stack>
</Stack>
</aside>
<Box className={classes.documentation}>
<Box className={classes.filters}>
<TextInput
label="Search"
value={search}
onChange={(event) => setSearch(event.currentTarget.value)}
leftSection={<IconSearch size={17} />}
placeholder="Packets, types, fields, or enums"
aria-label="Search Proto documentation"
/>
<Text c="dimmed" size="xs">
{resultCount} definitions in layer {layer.version}
</Text>
</Box>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md" className={classes.stats}>
{[
[IconPackage, packets.length, 'Packets'],
[IconBraces, types.length, 'Types'],
[IconList, enums.length, 'Enums'],
].map(([Icon, value, label]) => {
const StatIcon = Icon as typeof IconPackage;
return (
<Paper key={String(label)} withBorder radius="sm" p="md">
<Group>
<ThemeIcon variant="light" radius="xl">
<StatIcon size={18} />
</ThemeIcon>
<div>
<Text fw={700} fz="lg">
{String(value)}
</Text>
<Text c="dimmed" size="xs">
{String(label)}
</Text>
</div>
</Group>
</Paper>
);
})}
</SimpleGrid>
{packets.length > 0 && (
<Box component="section" id="packets" className={classes.schemaSection}>
<Title order={2}>Packets</Title>
<Text c="dimmed" mt="xs">
Network packets and their serialized YAML fields.
</Text>
{packets.map((packet) => (
<PacketDefinition
key={packet.name}
packet={packet}
layer={layer}
onNavigate={navigateToDefinition}
/>
))}
</Box>
)}
{types.length > 0 && (
<Box component="section" id="types" className={classes.schemaSection}>
<Title order={2}>Types</Title>
<Text c="dimmed" mt="xs">
Reusable structured values referenced by packet fields.
</Text>
{types.map((type) => (
<TypeDefinition
key={type.name}
type={type}
layer={layer}
onNavigate={navigateToDefinition}
/>
))}
</Box>
)}
{enums.length > 0 && (
<Box component="section" id="enums" className={classes.schemaSection}>
<Title order={2}>Enums</Title>
<Text c="dimmed" mt="xs">
Named protocol constants and their serialized numeric values.
</Text>
{enums.map((item) => (
<EnumDefinition key={item.name} item={item} layer={layer} />
))}
</Box>
)}
{resultCount === 0 && (
<Paper withBorder radius="lg" p="xl" ta="center">
<Text fw={600}>No schema definitions found.</Text>
<Text c="dimmed" size="sm" mt="xs">
Try another search term.
</Text>
</Paper>
)}
</Box>
</Box>
</Container>
</main>
<SiteFooter />
</>
);
}

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

@@ -0,0 +1,179 @@
.page {
min-height: 70vh;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
.hero {
position: relative;
overflow: visible;
background: light-dark(var(--mantine-color-blue-0), rgba(25, 113, 194, 0.16));
}
.heroInner {
position: relative;
min-height: 390px;
padding-top: 76px;
padding-bottom: 88px;
}
.heroContent {
position: relative;
z-index: 2;
}
.heroContent h1 {
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
font-size: clamp(34px, 6vw, 48px);
font-weight: 600;
letter-spacing: -0.035em;
}
.heroDescription {
max-width: 580px;
font-size: 16px;
}
.searchWrap {
position: relative;
width: min(100%, 680px);
margin-top: 24px;
}
.searchInput input {
height: 56px;
padding-inline: 50px;
border-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
box-shadow: none;
font-size: 16px;
}
.searchInput input:focus {
border-color: var(--mantine-color-blue-5);
}
.suggestions {
position: absolute;
z-index: 10;
top: calc(100% + 8px);
right: 0;
left: 0;
padding: 8px;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
}
.suggestion {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 12px;
align-items: start;
padding: 13px 12px;
border-radius: var(--mantine-radius-sm);
color: inherit;
text-decoration: none;
}
.suggestion svg {
margin-top: 2px;
color: var(--mantine-color-blue-6);
}
.suggestion:hover {
background: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-6));
text-decoration: none;
}
.decorativeIcon {
position: absolute;
z-index: 1;
color: var(--mantine-color-blue-6);
opacity: 0.62;
box-shadow: var(--mantine-shadow-sm);
}
.iconOne {
top: 62px;
left: 5%;
transform: rotate(-9deg);
}
.iconTwo {
top: 215px;
left: 12%;
transform: rotate(8deg);
}
.iconThree {
top: 70px;
right: 6%;
transform: rotate(10deg);
}
.iconFour {
right: 14%;
bottom: 64px;
transform: rotate(-7deg);
}
.content {
padding-top: 88px;
padding-bottom: 104px;
}
.content h2 {
font-size: 26px;
font-weight: 600;
}
.sectionDescription {
max-width: 620px;
}
.issue {
scroll-margin-top: 96px;
border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
}
.issue h3 {
font-size: 18px;
font-weight: 650;
}
.contact {
display: flex;
align-items: center;
justify-content: space-between;
gap: 32px;
margin-top: 72px;
padding: 32px;
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
}
@media (max-width: 48em) {
.heroInner {
min-height: 360px;
padding-top: 64px;
}
.decorativeIcon {
opacity: 0.28;
}
.iconTwo,
.iconFour {
display: none;
}
.content {
padding-top: 72px;
padding-bottom: 80px;
}
.contact {
align-items: flex-start;
flex-direction: column;
margin-top: 56px;
padding: 26px 22px;
}
}

242
src/pages/SupportPage.tsx Normal file
View File

@@ -0,0 +1,242 @@
import {
ActionIcon,
Anchor,
Box,
Button,
Card,
Container,
Group,
Paper,
SimpleGrid,
Stack,
Text,
TextInput,
ThemeIcon,
Title,
} from '@mantine/core';
import {
IconBook2,
IconHelpCircle,
IconLifebuoy,
IconMail,
IconMessageQuestion,
IconSearch,
IconShieldQuestion,
IconX,
} from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import knownIssues from '../../known-issues.json';
import { SEO } from '../components/SEO/SEO';
import { SiteFooter } from '../components/SiteFooter/SiteFooter';
import { SiteHeader } from '../components/SiteHeader/SiteHeader';
import classes from './SupportPage.module.css';
export function SupportPage() {
const [query, setQuery] = useState('');
const normalizedQuery = query.trim().toLowerCase();
const suggestions = useMemo(() => {
if (!normalizedQuery) {
return [];
}
const terms = normalizedQuery
.split(/\s+/)
.map((term) => term.replace(/[^a-z0-9-]/g, ''))
.filter((term) => term.length > 2);
return knownIssues
.map((issue) => {
const searchable = [issue.title, issue.summary, issue.solution, ...issue.keywords]
.join(' ')
.toLowerCase();
const score = terms.filter((term) => searchable.includes(term)).length;
return { issue, score };
})
.filter(({ score }) => score > 0)
.sort((left, right) => right.score - left.score)
.slice(0, 5);
}, [normalizedQuery]);
return (
<>
<SEO
title="Rosetta Support"
description="Find known Rosetta issues or contact support for help with account access, messaging, media, privacy, and security."
canonical="https://rosetta.im/support/"
keywords="Rosetta support, known issues, account recovery, messaging help"
/>
<SiteHeader />
<main className={classes.page}>
<Box component="section" className={classes.hero}>
<Container className={classes.heroInner}>
<ThemeIcon
className={`${classes.decorativeIcon} ${classes.iconOne}`}
size={50}
radius="xl"
variant="white"
aria-hidden="true"
>
<IconHelpCircle size={26} />
</ThemeIcon>
<ThemeIcon
className={`${classes.decorativeIcon} ${classes.iconTwo}`}
size={44}
radius="xl"
variant="white"
aria-hidden="true"
>
<IconMessageQuestion size={22} />
</ThemeIcon>
<ThemeIcon
className={`${classes.decorativeIcon} ${classes.iconThree}`}
size={54}
radius="xl"
variant="white"
aria-hidden="true"
>
<IconLifebuoy size={27} />
</ThemeIcon>
<ThemeIcon
className={`${classes.decorativeIcon} ${classes.iconFour}`}
size={42}
radius="xl"
variant="white"
aria-hidden="true"
>
<IconShieldQuestion size={21} />
</ThemeIcon>
<Stack align="center" gap="sm" className={classes.heroContent}>
<Title order={1}>How can we help?</Title>
<Text c="blue.9" ta="center" className={classes.heroDescription}>
Search account access, messaging, media, privacy, and security help.
</Text>
<Box className={classes.searchWrap}>
<TextInput
value={query}
onChange={(event) => setQuery(event.currentTarget.value)}
leftSection={<IconSearch size={21} />}
rightSection={
query ? (
<ActionIcon
variant="subtle"
color="gray"
onClick={() => setQuery('')}
aria-label="Clear support search"
>
<IconX size={18} />
</ActionIcon>
) : null
}
placeholder="Describe your problem"
aria-label="Search Rosetta support"
size="lg"
radius="sm"
className={classes.searchInput}
/>
{normalizedQuery && (
<Paper
withBorder
shadow="lg"
radius="md"
className={classes.suggestions}
>
{suggestions.length > 0 ? (
<Stack gap={0}>
{suggestions.map(({ issue }) => (
<Anchor
key={issue.id}
href={`#${issue.id}`}
onClick={() => setQuery('')}
className={classes.suggestion}
>
<IconHelpCircle size={18} />
<Box>
<Text fw={600} size="sm">
{issue.title}
</Text>
<Text c="dimmed" size="xs" mt={3}>
{issue.summary}
</Text>
</Box>
</Anchor>
))}
</Stack>
) : (
<Box p="md">
<Text fw={600} size="sm">
No matching known issue
</Text>
<Text c="dimmed" size="xs" mt={4}>
Try fewer words or contact the support team below.
</Text>
</Box>
)}
</Paper>
)}
</Box>
</Stack>
</Container>
</Box>
<Container className={classes.content}>
<Box component="section">
<Group gap="sm">
<ThemeIcon variant="light" radius="xl">
<IconBook2 size={18} />
</ThemeIcon>
<Title order={2}>Known issues</Title>
</Group>
<Text c="dimmed" mt="xs" className={classes.sectionDescription}>
Common situations and the information needed to resolve them.
</Text>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg" mt="xl">
{knownIssues.map((issue) => (
<Card
component="article"
id={issue.id}
key={issue.id}
withBorder
radius="md"
padding="xl"
className={classes.issue}
>
<Title order={3}>{issue.title}</Title>
<Text c="dimmed" size="sm" mt="sm" lh={1.65}>
{issue.summary}
</Text>
<Text size="sm" mt="lg" lh={1.7}>
{issue.solution}
</Text>
</Card>
))}
</SimpleGrid>
</Box>
<Paper component="section" radius="lg" className={classes.contact}>
<Box>
<Title order={2}>Still need help?</Title>
<Text c="dimmed" size="sm" mt="xs" lh={1.7} maw={620}>
Include your operating system, Rosetta version, approximate time,
and reproduction steps. Never send a recovery phrase or private key.
</Text>
</Box>
<Button
component="a"
href="mailto:support@rosetta.im?subject=Rosetta%20Support%20Request"
leftSection={<IconMail size={17} />}
variant="default"
>
support@rosetta.im
</Button>
</Paper>
</Container>
</main>
<SiteFooter />
</>
);
}

278
src/pages/TermsPage.tsx Normal file
View File

@@ -0,0 +1,278 @@
import { Anchor, List } from '@mantine/core';
import { Link } from 'react-router-dom';
import {
LegalAddress,
LegalList,
LegalPage,
LegalSection,
LegalText,
} from '../components/LegalPage/LegalPage';
import { SEO } from '../components/SEO/SEO';
export function TermsPage() {
return (
<>
<SEO
title="Terms of Use for Rosetta"
description="Terms of Use and End User License Agreement for Rosetta by ROSETTA CLOUD SERVICES LIMITED."
canonical="https://rosetta.im/terms/"
keywords="Rosetta terms of use, end user license agreement, encrypted messenger terms"
/>
<LegalPage title="Terms of Use" updated="May 22, 2026">
<LegalText lead>
These Terms of Use and End User License Agreement (&quot;Terms&quot;) govern
your use of Rosetta, an end-to-end encrypted messaging application operated by
ROSETTA CLOUD SERVICES LIMITED (&quot;Rosetta&quot;, &quot;we&quot;,
&quot;us&quot;, or &quot;our&quot;).
</LegalText>
<LegalText>
By downloading, installing, or using Rosetta, you agree to these Terms.
</LegalText>
<LegalSection title="1. Agreement Between You and Rosetta">
<LegalText>
These Terms are concluded between you and ROSETTA CLOUD SERVICES LIMITED
only, and not with Apple. We, not Apple, are solely responsible for Rosetta
and its content.
</LegalText>
</LegalSection>
<LegalSection title="2. License">
<LegalText>
We grant you a limited, non-exclusive, non-transferable, revocable license
to use Rosetta for personal or lawful business communication on
Apple-branded products that you own or control, as permitted by the Apple
Media Services Terms and Conditions, including applicable Family Sharing,
volume purchasing, or Legacy Contacts rules.
</LegalText>
<LegalText>
You may not copy, modify, reverse engineer, decompile, resell, sublicense,
rent, lease, or distribute Rosetta except as permitted by applicable law.
</LegalText>
</LegalSection>
<LegalSection title="3. Privacy">
<LegalText>
Your use of Rosetta is also governed by our Privacy Policy, available at{' '}
<Anchor component={Link} to="/privacy">
Privacy Policy
</Anchor>
.
</LegalText>
</LegalSection>
<LegalSection title="4. Encrypted Messaging">
<LegalText>
Rosetta is designed for end-to-end encrypted communication. You are
responsible for protecting your device, private keys, and access
credentials. If you lose access to your device or keys, we may not be able
to restore your messages or account data.
</LegalText>
</LegalSection>
<LegalSection title="5. Acceptable Use">
<LegalText>You agree not to use Rosetta to:</LegalText>
<LegalList>
<List.Item>violate any law or regulation;</List.Item>
<List.Item>infringe the rights of others;</List.Item>
<List.Item>
send spam, malware, scams, threats, harassment, or unlawful content;
</List.Item>
<List.Item>
attempt to disrupt, overload, reverse engineer, or compromise Rosetta or
its infrastructure;
</List.Item>
<List.Item>
impersonate another person or misrepresent your identity;
</List.Item>
<List.Item>use Rosetta for any prohibited or sanctioned activity.</List.Item>
</LegalList>
</LegalSection>
<LegalSection title="6. No Emergency Use">
<LegalText>
Rosetta is not designed for emergency calls, emergency messaging, or
communications where failure or delay could cause death, personal injury,
or serious damage. Use official emergency channels when needed.
</LegalText>
</LegalSection>
<LegalSection title="7. User Content">
<LegalText>
You retain ownership of the content you create or send through Rosetta. You
grant us the limited rights necessary to operate the service, such as
routing encrypted messages and maintaining service security. We do not
claim ownership of your messages or files.
</LegalText>
</LegalSection>
<LegalSection title="8. Updates and Availability">
<LegalText>
We may update, modify, suspend, or discontinue Rosetta or parts of the
service at any time. Some features may require an internet connection or a
compatible device and operating system.
</LegalText>
</LegalSection>
<LegalSection title="9. Fees">
<LegalText>
Rosetta is currently offered as a free application unless stated otherwise.
If paid features, subscriptions, or in-app purchases are introduced through
Apple, Apple&apos;s payment and subscription terms will apply.
</LegalText>
</LegalSection>
<LegalSection title="10. Maintenance and Support">
<LegalText>
ROSETTA CLOUD SERVICES LIMITED is solely responsible for providing
maintenance and support for Rosetta. Apple has no obligation to provide
maintenance or support services for Rosetta.
</LegalText>
<LegalText>
Support contact:{' '}
<Anchor href="mailto:admin@rosetta.im">Admin@rosetta.im</Anchor>
<br />
Support centre:{' '}
<Anchor component={Link} to="/support">
Rosetta Support
</Anchor>
</LegalText>
</LegalSection>
<LegalSection title="11. Warranty">
<LegalText>
Rosetta is provided &quot;as is&quot; and &quot;as available&quot; to the
maximum extent permitted by law. If Rosetta fails to conform to any
applicable warranty, you may notify Apple, and Apple may refund the purchase
price, if any, for the app. To the maximum extent permitted by law, Apple
has no other warranty obligation for Rosetta.
</LegalText>
</LegalSection>
<LegalSection title="12. Product Claims">
<LegalText>
ROSETTA CLOUD SERVICES LIMITED, not Apple, is responsible for addressing any
claims relating to Rosetta, including product liability claims, legal or
regulatory claims, and claims under consumer protection or similar laws.
</LegalText>
</LegalSection>
<LegalSection title="13. Intellectual Property Claims">
<LegalText>
If a third party claims that Rosetta or your use of Rosetta infringes
intellectual property rights, ROSETTA CLOUD SERVICES LIMITED, not Apple, is
responsible for the investigation, defense, settlement, and discharge of
that claim.
</LegalText>
</LegalSection>
<LegalSection title="14. Legal Compliance">
<LegalText>
You represent that you are not located in a country subject to a U.S.
Government embargo or designated by the U.S. Government as a
&quot;terrorist supporting&quot; country, and that you are not listed on any
U.S. Government list of prohibited or restricted parties.
</LegalText>
</LegalSection>
<LegalSection title="15. Third-Party Beneficiary">
<LegalText>
Apple and Apple&apos;s subsidiaries are third-party beneficiaries of these
Terms. Upon your acceptance of these Terms, Apple has the right to enforce
these Terms against you as a third-party beneficiary.
</LegalText>
</LegalSection>
<LegalSection title="16. Termination">
<LegalText>
We may suspend or terminate access to Rosetta if you violate these Terms,
create risk for other users, or use the service unlawfully. You may stop
using Rosetta at any time.
</LegalText>
</LegalSection>
<LegalSection title="17. Limitation of Liability">
<LegalText>
To the maximum extent permitted by law, ROSETTA CLOUD SERVICES LIMITED will
not be liable for indirect, incidental, special, consequential, or punitive
damages, or for loss of data, profits, goodwill, or business opportunities
arising from your use of Rosetta.
</LegalText>
</LegalSection>
<LegalSection title="18. Governing Law">
<LegalText>
These Terms are governed by the laws of Hong Kong, unless local consumer
protection law requires otherwise.
</LegalText>
</LegalSection>
<LegalSection title="19. Contact">
<LegalAddress>
{'ROSETTA CLOUD SERVICES LIMITED\nRegistered address:\nUNIT 704C 7/F BLK 3 NAN FUNG IND CITY\n18 TIN HAU RD\nTUEN MUN\nHONG KONG'}
</LegalAddress>
<LegalText>
Email: <Anchor href="mailto:admin@rosetta.im">Admin@rosetta.im</Anchor>
</LegalText>
<LegalText>
Phone: <Anchor href="tel:+85228910030">+852 2891 0030</Anchor>
</LegalText>
</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>
</>
);
}

42
src/services/updates.ts Normal file
View File

@@ -0,0 +1,42 @@
export interface DownloadRelease {
platform: string;
arch: string;
version: string;
link: string;
}
interface UpdateItem {
platform: string;
arch: string;
version: string;
downloadUrl: string;
}
const PLATFORM_ORDER = ['darwin', 'linux', 'win32', 'android'];
const ARCH_ORDER = ['arm64', 'x64', 'universal'];
export async function fetchUpdates(signal?: AbortSignal): Promise<DownloadRelease[]> {
const response = await fetch('https://sdu.rosetta.im/updates/all', { signal });
if (!response.ok) {
throw new Error(`Update server returned ${response.status}`);
}
const data = (await response.json()) as { items?: UpdateItem[] };
return (data.items || [])
.map((item) => ({
platform: item.platform,
arch: item.arch,
version: item.version,
link: new URL(item.downloadUrl, 'https://sdu.rosetta.im').toString(),
}))
.sort((a, b) => {
const platformComparison =
PLATFORM_ORDER.indexOf(a.platform) - PLATFORM_ORDER.indexOf(b.platform);
if (platformComparison !== 0) {
return platformComparison;
}
return ARCH_ORDER.indexOf(a.arch) - ARCH_ORDER.indexOf(b.arch);
});
}

View File

@@ -1,3 +1,22 @@
*{ * {
scroll-behavior: smooth; box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
min-width: 320px;
margin: 0;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
}
a {
text-underline-offset: 3px;
}
::selection {
background: var(--mantine-color-blue-1);
color: var(--mantine-color-blue-9);
} }

45
src/types/proto.ts Normal file
View File

@@ -0,0 +1,45 @@
export interface ProtoField {
id: number;
name: string;
type: string;
optional: boolean;
description: string;
}
export interface ProtoPacket {
name: string;
id: number;
description: string;
yaml: string;
fields: ProtoField[];
}
export interface ProtoType {
name: string;
description: string;
yaml: string;
fields: ProtoField[];
}
export interface ProtoEnumValue {
id: number;
name: string;
description: string;
}
export interface ProtoEnum {
name: string;
type: string;
description: string;
yaml: string;
values: ProtoEnumValue[];
}
export interface ProtoLayer {
version: number;
description: string;
source: string;
enums: ProtoEnum[];
types: ProtoType[];
packets: ProtoPacket[];
}

34
worker/index.js Normal file
View File

@@ -0,0 +1,34 @@
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 = {
async fetch(request, env) {
const response = await env.ASSETS.fetch(request);
if (
response.status !== 404 ||
request.method !== 'GET' ||
!request.headers.get('accept')?.includes('text/html')
) {
return withFreshHtml(response);
}
const indexUrl = new URL('/index.html', request.url);
const indexResponse = await env.ASSETS.fetch(new Request(indexUrl, request));
return withFreshHtml(indexResponse);
},
};
export default worker;