forked from Rosetta/landing
Compare commits
34 Commits
8efaafb833
...
account-de
| Author | SHA1 | Date | |
|---|---|---|---|
| b96ad82cf7 | |||
| 5278509046 | |||
| 026f69b179 | |||
| 94bc883205 | |||
| 12f6384856 | |||
| bff945e701 | |||
| 354c8e5489 | |||
|
|
4bc65f71cf | ||
| aea0b94598 | |||
| 7af3c46bfe | |||
|
|
e8c221bda4 | ||
| 871c84cedd | |||
|
|
8b2af4db62 | ||
|
|
33d4574583 | ||
|
|
179e2e65c6 | ||
|
|
5d26ef6d89 | ||
|
|
b902298ad6 | ||
|
|
650d73830f | ||
|
|
c81c3a1189 | ||
|
|
856fb60579 | ||
| 4daf0ffba9 | |||
| f4bfc996df | |||
| 10b7b33d89 | |||
| 8c3418488c | |||
| b1bf162d56 | |||
| b722e6d952 | |||
| b494f94a56 | |||
| a7de198be1 | |||
| a572661681 | |||
| 2eab462ed6 | |||
| 7e17f46152 | |||
| a26785eb18 | |||
| 580a0da008 | |||
| 633c338c16 |
3
.openai/hosting.json
Normal file
3
.openai/hosting.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"project_id": "appgprj_6a6bdf07874c81919af23407be23ff1a"
|
||||||
|
}
|
||||||
37
build/prepare-sites-build.mjs
Normal file
37
build/prepare-sites-build.mjs
Normal 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'));
|
||||||
|
}),
|
||||||
|
);
|
||||||
33
index.html
33
index.html
@@ -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 macOS, Windows, and Linux. 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 macOS, Windows, Linux." />
|
<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,21 +28,15 @@
|
|||||||
"@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", "macOS", "Linux", "Android"],
|
"operatingSystem": ["Windows", "Linux", "Android"],
|
||||||
"offers": {
|
"offers": {
|
||||||
"@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
44
known-issues.json
Normal 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
19
nginx.conf
Normal 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
79
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -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
529
proto/layer.1.yml
Normal 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
527
proto/layer.2.yml
Normal 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
656
proto/layer.3.yml
Normal 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
|
||||||
BIN
public/rosetta-logo.png
Normal file
BIN
public/rosetta-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -1,15 +1,51 @@
|
|||||||
<?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-02-16</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>
|
||||||
|
<loc>https://rosetta.im/support/</loc>
|
||||||
|
<lastmod>2026-07-30</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://rosetta.im/privacy/</loc>
|
||||||
|
<lastmod>2026-07-11</lastmod>
|
||||||
|
<changefreq>yearly</changefreq>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://rosetta.im/terms/</loc>
|
||||||
|
<lastmod>2026-07-11</lastmod>
|
||||||
|
<changefreq>yearly</changefreq>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
|
|||||||
188
scripts/generate-proto-docs.mjs
Normal file
188
scripts/generate-proto-docs.mjs
Normal 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.`,
|
||||||
|
);
|
||||||
108
src/App.tsx
108
src/App.tsx
@@ -1,22 +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';
|
|
||||||
|
|
||||||
|
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">
|
||||||
|
<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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Container,
|
|
||||||
Flex,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
useComputedColorScheme,
|
|
||||||
useMantineTheme,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import classes from './DownloadCenter.module.css';
|
|
||||||
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { FaApple, 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();
|
|
||||||
|
|
||||||
return (data.items || []).map((item: UpdateItem) => ({
|
|
||||||
platform: item.platform,
|
|
||||||
arch: item.arch,
|
|
||||||
version: item.version,
|
|
||||||
link: new URL(item.downloadUrl, 'https://sdu.rosetta.im').toString(),
|
|
||||||
})).sort((a : any, b : any) => {
|
|
||||||
const platformOrder = ['darwin', '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 colorScheme = useComputedColorScheme();
|
|
||||||
const [kernels, setKernels] = useState<DownloadFeature[]>([]);
|
|
||||||
const [targetPlatforms, setTargetPlatforms] = useState<string[]>(['darwin', 'linux', 'win32', 'android']);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchUpdates().then(setKernels);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const switchTarget = (value: string) => {
|
|
||||||
let targetPlatforms: string[] = [];
|
|
||||||
switch(value){
|
|
||||||
case 'All':
|
|
||||||
targetPlatforms = ['darwin', 'linux', 'win32', 'android'];
|
|
||||||
break;
|
|
||||||
case 'Windows':
|
|
||||||
targetPlatforms = ['win32'];
|
|
||||||
break;
|
|
||||||
case 'macOS':
|
|
||||||
targetPlatforms = ['darwin'];
|
|
||||||
break;
|
|
||||||
case 'Android':
|
|
||||||
targetPlatforms = ['android'];
|
|
||||||
break;
|
|
||||||
case 'Linux':
|
|
||||||
targetPlatforms = ['linux'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
setTargetPlatforms(targetPlatforms);
|
|
||||||
}
|
|
||||||
|
|
||||||
const translatePlatformToOsName = (platform: string) => {
|
|
||||||
switch(platform){
|
|
||||||
case 'darwin':
|
|
||||||
return 'macOS';
|
|
||||||
case 'linux':
|
|
||||||
return 'Linux';
|
|
||||||
case 'win32':
|
|
||||||
return 'Windows';
|
|
||||||
case 'android':
|
|
||||||
return 'Android';
|
|
||||||
default:
|
|
||||||
return platform;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const translatePlatformAndArchToCpuName = (platform : string, arch: string) => {
|
|
||||||
switch(platform){
|
|
||||||
case 'darwin':
|
|
||||||
return arch === 'arm64' ? 'Apple Silicon' : 'Intel';
|
|
||||||
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 === 'darwin' && <FaApple size={50} color={
|
|
||||||
colorScheme == 'dark' ? 'white' : 'black'
|
|
||||||
} />}
|
|
||||||
{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 == 'darwin' && <>
|
|
||||||
{feature.arch == 'arm64' && <>
|
|
||||||
Download this version if you have a Mac with Apple Silicon (M1, M2 chips).
|
|
||||||
</>}
|
|
||||||
{feature.arch == 'x64' && <>
|
|
||||||
Download this version if you have a Mac with an Intel processor, which was most likely manufactured before 2020.
|
|
||||||
</>}
|
|
||||||
</>}
|
|
||||||
{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', 'macOS', 'Linux', 'Android']}></Switch>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex gap="xl" justify={'center'} wrap={'wrap'} align={'center'} mt={50}>
|
|
||||||
{features}
|
|
||||||
</Flex>
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { IconGauge, IconLock, IconMessage2, IconServer, IconUser, IconUsersGroup } from '@tabler/icons-react';
|
|
||||||
import { Container, Flex, SimpleGrid, Text, ThemeIcon, Title } from '@mantine/core';
|
|
||||||
import classes from './FeaturesGrid.module.css';
|
|
||||||
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
|
|
||||||
|
|
||||||
export 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: React.FC<any>;
|
|
||||||
title: React.ReactNode;
|
|
||||||
description: React.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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
139
src/components/LegalPage/LegalPage.module.css
Normal file
139
src/components/LegalPage/LegalPage.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/components/LegalPage/LegalPage.tsx
Normal file
101
src/components/LegalPage/LegalPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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 macOS, Windows, Linux.',
|
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;
|
||||||
|
}
|
||||||
|
|||||||
51
src/components/ScrollToTop/ScrollToTop.tsx
Normal file
51
src/components/ScrollToTop/ScrollToTop.tsx
Normal 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;
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
16
src/components/SiteFooter/SiteFooter.module.css
Normal file
16
src/components/SiteFooter/SiteFooter.module.css
Normal 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));
|
||||||
|
}
|
||||||
78
src/components/SiteFooter/SiteFooter.tsx
Normal file
78
src/components/SiteFooter/SiteFooter.tsx
Normal 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}>
|
||||||
|
© {year} ROSETTA CLOUD SERVICES LIMITED
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
src/components/SiteHeader/SiteHeader.module.css
Normal file
63
src/components/SiteHeader/SiteHeader.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/components/SiteHeader/SiteHeader.tsx
Normal file
82
src/components/SiteHeader/SiteHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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
3843
src/generated/protoDocs.ts
Normal file
File diff suppressed because it is too large
Load Diff
41
src/main.tsx
41
src/main.tsx
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
78
src/pages/EncryptionPage.module.css
Normal file
78
src/pages/EncryptionPage.module.css
Normal 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;
|
||||||
|
}
|
||||||
412
src/pages/EncryptionPage.tsx
Normal file
412
src/pages/EncryptionPage.tsx
Normal 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'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'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'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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
159
src/pages/LandingPage.module.css
Normal file
159
src/pages/LandingPage.module.css
Normal 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
334
src/pages/LandingPage.tsx
Normal 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
191
src/pages/PrivacyPage.tsx
Normal 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 ("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.
|
||||||
|
</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'
|
||||||
|
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 "Last updated" 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
256
src/pages/ProtoPage.module.css
Normal file
256
src/pages/ProtoPage.module.css
Normal 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
999
src/pages/ProtoPage.tsx
Normal 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'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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
119
src/pages/RccPage.module.css
Normal file
119
src/pages/RccPage.module.css
Normal 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
249
src/pages/RccPage.tsx
Normal 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'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'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'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'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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
179
src/pages/SupportPage.module.css
Normal file
179
src/pages/SupportPage.module.css
Normal 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
242
src/pages/SupportPage.tsx
Normal 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
278
src/pages/TermsPage.tsx
Normal 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 ("Terms") govern
|
||||||
|
your use of Rosetta, an end-to-end encrypted messaging application operated by
|
||||||
|
ROSETTA CLOUD SERVICES LIMITED ("Rosetta", "we",
|
||||||
|
"us", or "our").
|
||||||
|
</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'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 "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.
|
||||||
|
</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
|
||||||
|
"terrorist supporting" 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'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' 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
42
src/services/updates.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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
45
src/types/proto.ts
Normal 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
34
worker/index.js
Normal 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;
|
||||||
Reference in New Issue
Block a user