mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 751496ed48 | |||
| 734a004f23 | |||
| 01ce3ec99e | |||
| d9b26f9fb4 | |||
| 1e3bb94660 | |||
| 90882f91ef | |||
| 5157428e74 | |||
| 47ca7563b7 | |||
| f7829bbebb | |||
| 6093da4dc6 | |||
| 583e15745f | |||
| bd8c7edc3a | |||
| bd4c53f9c7 | |||
| 5440d136fe | |||
| c9ec6ce3e0 | |||
| 652f1cef4f | |||
| 0f1f8bde1b | |||
| fa4e4039b3 | |||
| cb466d3865 | |||
| 54be9f9a39 | |||
| 47a9c08f48 | |||
| b0bcab77e3 | |||
| 6624bcc1ca | |||
| ca391ca83b | |||
| 1438a4d41f | |||
| c9cf7c49dc | |||
| bcbd46d658 | |||
| 4d489ec992 |
@@ -7,7 +7,5 @@ extension/
|
||||
website/
|
||||
scripts/
|
||||
*.md
|
||||
*.bat
|
||||
*.sh
|
||||
.env
|
||||
server/.env
|
||||
|
||||
@@ -52,19 +52,40 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Sync Protocol Constants
|
||||
run: |
|
||||
chmod +x ./scripts/sync-constants.sh
|
||||
./scripts/sync-constants.sh
|
||||
- name: Extract version from tag
|
||||
id: version
|
||||
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Extension Zip
|
||||
- name: Inject version into source files
|
||||
run: |
|
||||
zip -r koala-sync-extension.zip extension/ -x "*.DS_Store*"
|
||||
VERSION=${{ steps.version.outputs.VERSION }}
|
||||
echo "Injecting version $VERSION from tag $GITHUB_REF_NAME..."
|
||||
|
||||
# 1. extension/manifest.base.json
|
||||
jq --arg v "$VERSION" '.version = $v' extension/manifest.base.json > tmp.json && mv tmp.json extension/manifest.base.json
|
||||
echo " ✓ manifest.base.json -> $VERSION"
|
||||
|
||||
# 2. shared/constants.js — APP_VERSION
|
||||
sed -i "s/export const APP_VERSION = '.*'/export const APP_VERSION = '$VERSION'/" shared/constants.js
|
||||
echo " ✓ shared/constants.js -> $VERSION"
|
||||
|
||||
# 3. package.json
|
||||
jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json
|
||||
echo " ✓ package.json -> $VERSION"
|
||||
|
||||
echo "Version injection complete."
|
||||
|
||||
- name: Build Extensions
|
||||
run: |
|
||||
npm install
|
||||
npm run build:extension
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: koala-sync-extension.zip
|
||||
files: |
|
||||
dist/koalasync-chrome.zip
|
||||
dist/koalasync-firefox.zip
|
||||
name: Release ${{ github.ref_name }}
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
|
||||
+1
-2
@@ -38,8 +38,7 @@ coverage/
|
||||
# KoalaSync Specific
|
||||
# We ignore the synced files in the extension folder to ensure
|
||||
# the root 'shared/' remains the Single Source of Truth.
|
||||
extension/shared/*
|
||||
!extension/shared/README.md
|
||||
extension/shared/
|
||||
|
||||
# Temporary scratch files
|
||||
scratch/
|
||||
|
||||
+43
-21
@@ -15,31 +15,41 @@ KoalaSync is a specialized tool for **synchronized video playback** across multi
|
||||
- **Identity**: Users are identified by a unique hex `peerId` combined with a customizable `username`.
|
||||
|
||||
## 2. Repository Structure
|
||||
- `extension/`: Chrome Extension (Manifest V3). Contains background service worker, content scripts, and popup UI.
|
||||
- `extension/`: Browser Extension (Chrome & Firefox, Manifest V3). Contains background service worker, content scripts, and popup UI.
|
||||
- `server/`: Node.js Relay Server using Socket.IO (WebSocket-only).
|
||||
- `website/`: **Landing Page** & Invitation Bridge (Marketing, Tutorials, and Downloads).
|
||||
- `shared/`: **Single Source of Truth** for protocol constants and event names.
|
||||
- `scripts/`: Utility scripts (e.g., `sync-constants.sh`).
|
||||
- `scripts/`: Development utilities (e.g., `build-extension.js`).
|
||||
- `docker-compose.yml`: Root-level orchestration for the relay server.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Single Source of Truth**: `shared/constants.js` and `shared/blacklist.js` are the master files. They must be synchronized to the `extension/shared/` directory using `.\scripts\sync-constants.bat` or `./scripts/sync-constants.sh`.
|
||||
> **Single Source of Truth**: `shared/constants.js` and `shared/blacklist.js` are the master files. They must be synchronized to the `extension/shared/` directory using `node scripts/build-extension.js`.
|
||||
> - **Extension Modules** (`background.js`, `popup.js`) import directly from `./shared/constants.js`.
|
||||
> - **Content Scripts** (`content.js`) use a **manual synchronous mirror** to prevent race conditions during page load. Always verify parity after sync.
|
||||
> - **Content Scripts** (`content.js`) use a **marker-injected synchronous copy** of the constants. The build script automatically replaces the marked blocks — no manual mirroring needed.
|
||||
|
||||
## 3. Mandatory Reading
|
||||
Before touching any code, you MUST read the following documents in order:
|
||||
1. [ARCHITECTURE.md](ARCHITECTURE.md) – Detailed communication flows, Dual Heartbeat, and two-phase sync protocol.
|
||||
1. [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) – Detailed communication flows, Dual Heartbeat, and two-phase sync protocol.
|
||||
2. [extension/README.md](extension/README.md) – Extension components, tab structure, and loading process.
|
||||
3. [SYNC_GUIDE.md](SYNC_GUIDE.md) – Protocol constants and synchronization requirements.
|
||||
3. [docs/SYNC_GUIDE.md](docs/SYNC_GUIDE.md) – Protocol constants and synchronization requirements.
|
||||
|
||||
## 4. The "Vanilla JS Mirror" Pattern
|
||||
To avoid boot-time race conditions in Manifest V3 without a bundler, the following architectural trade-off is enforced:
|
||||
- **Synchronous Execution**: `content.js` MUST execute synchronously to catch early media events.
|
||||
- **Manual Mirroring**: `content.js` maintains a manual mirror of the `EVENTS` constants from `shared/constants.js`.
|
||||
- **Maintenance**: Developers must ensure that any changes to `shared/constants.js` are manually reflected in `content.js` after running the sync scripts.
|
||||
- **Automated Injection**: The build script (`node scripts/build-extension.js`) automatically injects `EVENTS` and `HEARTBEAT_INTERVAL` into `content.js` using marker-based replacement (see `scripts/README.md` for marker details).
|
||||
- **Maintenance**: After modifying `shared/constants.js`, simply run the build script. No manual mirroring is required.
|
||||
|
||||
## 5. Design Guidelines
|
||||
## 5. File Responsibility Map
|
||||
|
||||
| File | Responsibility |
|
||||
|:-----|:---------------|
|
||||
| `background.js` | WebSocket client, state orchestrator, event router, session persistence |
|
||||
| `content.js` | Video element detection, media control, event origin detection (loop prevention) |
|
||||
| `popup.js` | UI rendering, user input handling, peer display, invitation link generation |
|
||||
| `bridge.js` | Landing page ↔ extension communication for invitation join flow |
|
||||
| `server/index.js` | Room management, message relay, rate limiting, authentication, peer lifecycle |
|
||||
|
||||
## 6. Design Guidelines
|
||||
The popup UI follows a strict design system. Do not modify these variables or the layout structure without explicit approval.
|
||||
- **Font**: System font stack. **MANDATORY**: No external CDNs or Google Fonts to ensure 100% privacy.
|
||||
- **Popup Width**: Fixed at `320px`.
|
||||
@@ -53,9 +63,10 @@ The popup UI follows a strict design system. Do not modify these variables or th
|
||||
| `--success` | `#22c55e` | Success states / Online dot |
|
||||
| `--error` | `#ef4444` | Errors / Offline dot |
|
||||
|
||||
## 5. Non-Negotiables (Core Logic)
|
||||
## 7. Non-Negotiables (Core Logic)
|
||||
The following features are critical and must not be removed or fundamentally altered:
|
||||
- **Two-Phase Force Sync**: The `Prepare` → `ACK` → `Execute` flow ensures all peers are buffered before playback resumes.
|
||||
- **Episode Auto-Sync**: Ensures series binges stay perfectly synced. A lobby initiates during title transitions, freezing peers until everyone is ready.
|
||||
- **Dual Heartbeat**:
|
||||
- **Background Heartbeat (30s)**: Ensures session persistence even without a video element.
|
||||
- **Content Heartbeat (15s)**: Transmits current video metadata (time, title).
|
||||
@@ -66,33 +77,44 @@ The following features are critical and must not be removed or fundamentally alt
|
||||
- **SW Keep-alive**: Use of `chrome.alarms` to prevent the Manifest V3 Service Worker from suspending.
|
||||
- **Diagnostics**: The "Dev" tab provides real-time access to the underlying `<video>` state for troubleshooting.
|
||||
- **Persistence**: `peerId` and `username` must be stored to remain stable across sessions.
|
||||
- **Room ID Format**: Room IDs are restricted to `[a-zA-Z0-9-]` only (alphanumeric + hyphens). This is enforced server-side.
|
||||
|
||||
## 6. Technical Constraints
|
||||
## 8. Technical Constraints
|
||||
- **No Bundler**: The extension uses plain ES Modules. Do not introduce build steps or npm packages into the `extension/` folder.
|
||||
- **Manual Protocol**: `background.js` implements a subset of the Socket.IO wire protocol natives.
|
||||
- **Manual Protocol**: `background.js` implements a subset of the Socket.IO wire protocol natively.
|
||||
- **Server Transport**: Restricted to `websocket` only. Polling is disabled.
|
||||
- **Docker Context**: The Docker build must run from the **Repo Root**.
|
||||
- **Manifest Settings**: `run_at` must remain `document_idle`, and `all_frames` must remain `false`.
|
||||
|
||||
## 7. Security & Deployment
|
||||
## 9. Security & Deployment
|
||||
- **Tokens**: Security tokens are intentionally managed via `shared/constants.js` and server `.env`.
|
||||
- **Environment**: `.env` is excluded via `.gitignore`. Only `.env.example` should be committed.
|
||||
- **Revocation**: `MIN_VERSION` check on the server is used to deprecate old extension versions.
|
||||
- **Invitation Links**: Correctly propagate server URLs, Room IDs, and Passwords via the URL hash to the bridge.
|
||||
|
||||
## 8. Common Workflows
|
||||
## 10. Common Workflows
|
||||
|
||||
### Releasing a New Version (CRITICAL WORKFLOW FOR AI AGENTS)
|
||||
> [!CAUTION]
|
||||
> **AI AGENTS MUST FOLLOW THIS EXACT SEQUENCE WHEN RELEASING A NEW VERSION OR TAGGING.**
|
||||
> The CI pipeline automatically injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json`. You do NOT need to manually bump version numbers.
|
||||
1. Commit all code changes and push to `main`.
|
||||
2. Create and push a new tag. **MANDATORY**: Tags MUST start with a `v` (e.g., `v1.4.0`). The GitHub Actions release workflow is strictly configured to ignore any tags without the `v` prefix.
|
||||
3. The CI will extract the version from the tag (e.g., `v1.4.0` → `1.4.0`), inject it into all source files, build the extension artifacts, publish the Docker image, and create a GitHub Release.
|
||||
4. Verify the release builds on GitHub Actions.
|
||||
|
||||
### Adding a Protocol Event
|
||||
1. Add the event name to `shared/constants.js`.
|
||||
2. Run the sync script (`.\scripts\sync-constants.bat` or `./scripts/sync-constants.sh`).
|
||||
2. Run the build script (`node scripts/build-extension.js`).
|
||||
3. Implement the handler in `server/index.js` and `background.js`.
|
||||
|
||||
### Testing Locally
|
||||
1. Load `extension/` as an "Unpacked Extension" in Chrome.
|
||||
2. Start the server from the root: `docker-compose up --build`.
|
||||
3. Use **different browser profiles** or vendors to test multi-peer logic.
|
||||
4. Use the **Dev tab** to verify real-time video element metadata.
|
||||
1. Run the build script: `node scripts/build-extension.js`.
|
||||
2. Load `dist/chrome/` as an "Unpacked Extension" in Chrome (or `dist/firefox/` in Firefox).
|
||||
3. Start the server from the root: `docker-compose up --build`.
|
||||
4. Use **different browser profiles** or vendors to test multi-peer logic.
|
||||
5. Use the **Dev tab** to verify real-time video element metadata.
|
||||
|
||||
### Locking Old Versions
|
||||
1. Increase `APP_VERSION` in `shared/constants.js`.
|
||||
2. Update `MIN_VERSION` in the server's `.env` file and restart.
|
||||
1. Update `MIN_VERSION` in the server's `.env` file to the minimum acceptable version.
|
||||
2. Restart the server. Older extensions will be rejected with a "Version too old" error.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Contributing to KoalaSync
|
||||
|
||||
Thank you for your interest in contributing to KoalaSync! We welcome all contributions, from bug reports to new features.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Prerequisites
|
||||
- Node.js (v18+)
|
||||
- Docker (for local server testing)
|
||||
|
||||
### 2. Setup
|
||||
1. Clone the repository.
|
||||
2. Run `npm install` in the root directory to install build dependencies.
|
||||
3. Run the build script to synchronize protocol constants and generate browser bundles:
|
||||
```bash
|
||||
node scripts/build-extension.js
|
||||
```
|
||||
|
||||
### 3. Testing Locally
|
||||
1. Load `dist/chrome/` as an "Unpacked Extension" in Chrome (`chrome://extensions/` → Developer Mode → Load Unpacked).
|
||||
2. For Firefox, load `dist/firefox/` via `about:debugging` → "Load Temporary Add-on".
|
||||
3. Start the relay server: `docker-compose up --build`.
|
||||
4. Use **two different browser profiles** (or Chrome + Firefox) to test multi-peer synchronization.
|
||||
5. Use the extension's **Dev tab** to verify real-time video element metadata (`readyState`, `currentTime`, `paused`).
|
||||
|
||||
### 4. Protocol Synchronization
|
||||
KoalaSync uses a "Single Source of Truth" for protocol constants in `shared/constants.js`.
|
||||
- **CRITICAL**: If you modify the constants, you MUST run the build script:
|
||||
```bash
|
||||
node scripts/build-extension.js
|
||||
```
|
||||
This will automatically synchronize the changes to the extension and generate the browser-specific bundles in the `dist/` folder.
|
||||
|
||||
### 5. Code Standards
|
||||
- **Vanilla JS**: The extension must remain dependency-free. Do not add npm packages to the `extension/` directory.
|
||||
- **Privacy**: Do not add external requests (CDNs, fonts, analytics, etc.).
|
||||
- **Comments**: Maintain the existing documentation style, especially for complex sync logic.
|
||||
- **Room IDs**: Room IDs are restricted to `[a-zA-Z0-9-]` (alphanumeric + hyphens only). Ensure any UI that generates room IDs follows this constraint.
|
||||
|
||||
### 6. Version Numbers
|
||||
> [!IMPORTANT]
|
||||
> **Do NOT manually bump version numbers.** The CI pipeline automatically injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json` during release builds. Manually changing version numbers in a PR will cause conflicts.
|
||||
|
||||
## Pull Request Process
|
||||
1. Create a new branch for your feature or bugfix.
|
||||
2. Ensure your code is tested locally (Chrome and Firefox).
|
||||
3. Update relevant documentation (e.g., `docs/ARCHITECTURE.md` if you change the protocol).
|
||||
4. Submit your PR with a clear description of the changes.
|
||||
|
||||
## Bug Reports
|
||||
When reporting a bug, please include:
|
||||
- **Browser**: Chrome / Firefox / Edge + version number.
|
||||
- **Extension Version**: Visible in the popup's Dev tab.
|
||||
- **Dev Tab Output**: Copy the connection status, logs, and video debug info from the Dev tab.
|
||||
- **Steps to Reproduce**: A clear sequence of actions that triggers the issue.
|
||||
|
||||
## Security
|
||||
If you find a security vulnerability, please do not open a public issue. Instead, refer to our [SECURITY.md](SECURITY.md) for responsible disclosure instructions.
|
||||
@@ -0,0 +1,37 @@
|
||||
# KoalaSync - Production Caddy Configuration Example
|
||||
# Replace domains and paths with your actual setup.
|
||||
|
||||
# 1. Marketing Website & Invitation Bridge
|
||||
sync.koalastuff.net {
|
||||
root * /var/www/koalasync/website
|
||||
file_server
|
||||
encode zstd gzip
|
||||
|
||||
# Security Headers
|
||||
header {
|
||||
# Prevent FLoC tracking
|
||||
Permissions-Policy interest-cohort=()
|
||||
# Security best practices
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options nosniff
|
||||
X-Frame-Options DENY
|
||||
Referrer-Policy no-referrer-when-downgrade
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Relay Server (Socket.IO / WebSocket)
|
||||
syncserver.koalastuff.net {
|
||||
reverse_proxy localhost:3000 {
|
||||
# Ensure WebSocket support is explicitly handled if needed
|
||||
# (Caddy usually handles this automatically)
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
|
||||
# Security Headers for the relay
|
||||
header {
|
||||
X-Content-Type-Options nosniff
|
||||
X-Frame-Options DENY
|
||||
Referrer-Policy no-referrer
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# Privacy Policy
|
||||
|
||||
**KoalaSync does not collect, store, or sell any personal data.**
|
||||
|
||||
KoalaSync is designed with a **Security-First & Volatile** architecture. This means we prioritize keeping your data out of persistent storage, though certain technical data must be processed temporarily to ensure service stability and security.
|
||||
|
||||
## 1. Data Processing (In-Memory Only)
|
||||
KoalaSync does not use a database. All active session data exists only in the server's RAM and is purged immediately when no longer needed.
|
||||
- **Session Data**: To synchronize playback, the server must temporarily hold your `peerId`, `username`, and the `title` of the video you are watching. Additionally, playback metadata (`mediaTitle`, `playbackState`, `currentTime`, `volume`, `muted`) is held per peer for the duration of the session. All of this is deleted as soon as you leave the room.
|
||||
- **Room Passwords**: If you set a room password, it is stored only as a secure **bcrypt hash** in RAM. The server never sees or stores your plaintext password.
|
||||
- **Routing Maps**: The server maintains ephemeral lookup tables (`socketToRoom`, `peerToSocket`) to route messages between peers. These contain only transport identifiers and are purged on disconnect.
|
||||
|
||||
### Data Retention
|
||||
| Data Type | Maximum Retention | Trigger for Deletion |
|
||||
|:----------|:------------------|:---------------------|
|
||||
| Session data (peerId, username, video metadata) | Duration of session | User leaves room or disconnects |
|
||||
| Room state | 2 hours max | Last peer leaves, or inactivity timeout |
|
||||
| Failed auth lockout records | 15 minutes | Automatic expiry |
|
||||
| Auth failure records | 1 hour | Periodic cleanup |
|
||||
| Connection rate-limit counters | 60 seconds | Automatic expiry |
|
||||
| Event rate-limit counters | 10 seconds | Automatic expiry + periodic cleanup |
|
||||
|
||||
## 2. Security & Rate Limiting
|
||||
To prevent abuse and brute-force attacks, the following data is processed:
|
||||
- **Brute-Force Protection**: If multiple failed password attempts are detected, the server stores the `IP address` and `Room ID` in a temporary RAM-based lockout list for a maximum of 15 minutes.
|
||||
- **Connection Rate Limiting**: IP addresses are tracked for 60 seconds to prevent connection-flooding (DoS) attacks.
|
||||
- **Event Rate Limiting**: Per-socket event counters are tracked for 10-second windows to prevent event-spamming. These are keyed by ephemeral socket IDs and cleaned up periodically.
|
||||
- **Console Logging**: The official relay server (`syncserver.koalastuff.net`) outputs connection events (including IP addresses) to the server console for real-time monitoring. These logs are ephemeral and are not archived, sold, or linked to any persistent user identity.
|
||||
|
||||
## 3. Extension Permissions
|
||||
The browser extension requires the following permissions:
|
||||
- `storage`: To remember your local preferences (username, server URL, room settings).
|
||||
- `tabs` & `scripting`: To detect and control video elements on the pages you choose to sync.
|
||||
- `<all_urls>` (host permission): Required to detect `<video>` elements on any website the user chooses to synchronize. The extension only activates on the specific tab the user has actively selected — it does not scan, monitor, or interact with any other tabs or pages.
|
||||
- `alarms`: To keep the background service worker alive during active sync sessions.
|
||||
- `notifications`: To display sync status updates (e.g., "Peer joined", "Force Sync initiated").
|
||||
- **No History Access**: We do not read, store, or transmit your browsing history. We only interact with the specific tab you have actively selected for synchronization.
|
||||
|
||||
## 4. Zero Third-Party Requests
|
||||
KoalaSync is completely self-contained:
|
||||
- **No CDNs or External Libraries**: All scripts and styles are self-hosted.
|
||||
- **No Analytics**: We do not use Google Analytics, tracking pixels, or any third-party telemetry.
|
||||
- **No External Fonts**: We use system font stacks to prevent tracking via font services.
|
||||
|
||||
## 5. Self-Hosted Instances
|
||||
This privacy policy applies to the **official KoalaSync relay server** at `syncserver.koalastuff.net`. If you choose to self-host a relay server using our open-source Docker image, the data handling practices of that instance are the responsibility of the server operator.
|
||||
|
||||
---
|
||||
|
||||
**Auditable & Open Source**: Because KoalaSync is open source, you can verify these claims by reviewing the [Server Source Code](https://github.com/Shik3i/KoalaSync/blob/main/server/index.js) and the [Extension Logic](https://github.com/Shik3i/KoalaSync/blob/main/extension/content.js).
|
||||
@@ -1,73 +1,89 @@
|
||||
# KoalaSync
|
||||
|
||||
KoalaSync is a premium, lightweight Chrome Extension and Relay Server for synchronized video playback across any website (YouTube, Twitch, Netflix, and custom HTML5 players).
|
||||
<p align="center">
|
||||
<a href="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml"><img src="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml/badge.svg" alt="Release Status"></a>
|
||||
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/github/v/release/Shik3i/KoalaSync" alt="GitHub release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Shik3i/KoalaSync?color=blue" alt="License"></a>
|
||||
<img src="https://img.shields.io/badge/Browser-Chrome%20|%20Firefox-blueviolet" alt="Cross Browser">
|
||||
</p>
|
||||
|
||||
**Latest Version**: `v1.2.0` (Episode Auto-Sync)
|
||||
KoalaSync is a premium, lightweight Browser Extension and Relay Server for synchronized video playback across any website—YouTube, Twitch, Netflix, and custom HTML5 players. Built with a focus on **Data Sovereignty** and **Extreme Performance**.
|
||||
|
||||
> [!TIP]
|
||||
> **New Developers & AI Agents**: Please read [AI_INIT.md](AI_INIT.md) before starting work.
|
||||
### 🌟 Why KoalaSync?
|
||||
|
||||
## Repository Structure
|
||||
- `extension/`: Chrome Extension (Manifest V3, Vanilla JS).
|
||||
- `server/`: Node.js + Socket.IO Relay Server (Containerized).
|
||||
- `website/`: Marketing landing page & **Invitation Bridge**.
|
||||
- `shared/`: Protocol constants and domain blacklist.
|
||||
- `scripts/`: Development utilities for protocol synchronization.
|
||||
* **🛡️ Security-First**: Volatile RAM-based relay with built-in brute-force protection and zero-persistence architecture.
|
||||
* **📡 Direct Logic**: Custom wire protocol implementation for frame-perfect synchronization.
|
||||
* **🛠️ Clean Build**: Dependency-free extension runtime with no library overhead.
|
||||
* **🌐 Universal**: Works on any website with a `<video>` tag.
|
||||
|
||||
> [!NOTE]
|
||||
> For deep technical dives, see [ARCHITECTURE.md](ARCHITECTURE.md) and [SYNC_GUIDE.md](SYNC_GUIDE.md).
|
||||
---
|
||||
|
||||
### ✨ Key Features
|
||||
|
||||
## Key Features
|
||||
- **Global Synchronization**: Synchronize Play, Pause, and Seeking on any website with a `<video>` tag.
|
||||
- **Episode Auto-Sync**: Perfectly sync series binges. All peers wait until everyone has loaded the next episode before starting together (v1.2.0+).
|
||||
- **Smart Matching**: Automatically highlights and sorts tabs containing matching video titles.
|
||||
- **Noise Filtering**: Built-in domain blacklist to hide non-video sites from selection.
|
||||
- **Smart Identity**: Customizable usernames combined with unique hexadecimal peer IDs.
|
||||
- **Episode Auto-Sync**: Perfectly sync series binges. All peers wait until everyone has loaded the next episode before starting together.
|
||||
- **Smart Matching**: Automatically highlights tabs containing matching video titles.
|
||||
- **Dual Heartbeat Architecture**: Robust session tracking that prevents ghost rooms and stale connections.
|
||||
- **Zero-Latency Relay**: Custom Socket.IO wire protocol implementation for maximum performance.
|
||||
- **Integrated Diagnostics**: A dedicated "Dev" tab for real-time video state debugging.
|
||||
- **Seamless Invitations**: Smart invitation links that automatically configure the server and room credentials for your friends.
|
||||
- **Zero-Latency Relay**: Custom wire protocol implementation for maximum performance.
|
||||
- **Seamless Invitations**: Smart links that automatically configure server and room credentials for your friends.
|
||||
|
||||
---
|
||||
|
||||
## Setup Instructions
|
||||
### 📂 Repository Structure
|
||||
|
||||
### 1. Relay Server (Docker)
|
||||
The server runs on Node.js using Socket.IO, containerized for easy deployment.
|
||||
- `extension/`: Browser Extension (Chrome & Firefox).
|
||||
- `server/`: Node.js + Socket.IO Relay Server (Containerized).
|
||||
- `website/`: Marketing landing page & Invitation Bridge.
|
||||
- `shared/`: **Single Source of Truth** for protocol constants.
|
||||
- `scripts/`: Automated build and synchronization utilities.
|
||||
- `docs/`: Technical deep-dives ([Architecture](docs/ARCHITECTURE.md), [Sync Guide](docs/SYNC_GUIDE.md)).
|
||||
|
||||
---
|
||||
|
||||
### 🚀 Quick Start
|
||||
|
||||
#### For Users (Installation)
|
||||
The easiest way to install KoalaSync is to download the pre-compiled version from the [Releases](https://github.com/Shik3i/KoalaSync/releases) page.
|
||||
1. Download the latest `koalasync-chrome.zip` or `koalasync-firefox.zip`.
|
||||
2. Extract the file and load it as an "Unpacked Extension" in your browser's Developer Mode.
|
||||
|
||||
#### For Developers (Building)
|
||||
To build the extension from source and synchronize protocol constants:
|
||||
```bash
|
||||
# From the root directory
|
||||
docker-compose up -d --build
|
||||
npm install
|
||||
node scripts/build-extension.js
|
||||
```
|
||||
The server will be available at `ws://localhost:3000`.
|
||||
The compiled artifacts will be available in the `dist/` directory.
|
||||
|
||||
### 2. Chrome Extension
|
||||
1. **Synchronize Protocol**: From the root directory, run the sync script to copy the master constants to the extension folder:
|
||||
```bash
|
||||
./scripts/sync-constants.sh
|
||||
```
|
||||
2. Open Chrome and go to `chrome://extensions/`.
|
||||
3. Enable **Developer mode** (top right).
|
||||
4. Click **Load unpacked**.
|
||||
5. Select the `extension/` folder.
|
||||
#### For Self-Hosting (Docker)
|
||||
Deploy your own private relay server using our official image:
|
||||
```bash
|
||||
# Pull the latest image
|
||||
docker pull ghcr.io/shik3i/koalasync:latest
|
||||
|
||||
## Usage
|
||||
1. Open the extension and go to the **Settings** tab to set your **Username**.
|
||||
2. Go to the **Room** tab, enter your Server URL (default: `ws://localhost:3000`), and click **Join / Create Room**.
|
||||
3. In the **Sync** tab, select the tab containing the video you want to sync.
|
||||
4. Share the **Invite Link** from the Room tab. When your friends click it, they will automatically join your room and server.
|
||||
5. Use **Force Sync** to perfectly align everyone to your current timestamp.
|
||||
# Or use our example compose file
|
||||
cp docker-compose.example.yml docker-compose.yml
|
||||
docker-compose up -d
|
||||
```
|
||||
The server will be available at `ws://localhost:3000`. See [docker-compose.example.yml](docker-compose.example.yml) for advanced configuration.
|
||||
|
||||
## Technical Details
|
||||
- **Manifest V3**: Uses a persistent Service Worker with Alarm-based keep-alive.
|
||||
- **Manual Socket.IO Protocol**: The extension implements the Socket.IO v4 wire protocol natively for extreme performance and zero dependencies.
|
||||
- **Dead Peer Pruning**: The server automatically prunes peers after 5 minutes of total inactivity (detected via dual heartbeats).
|
||||
- **Two-Phase Sync**: Ensures all peers are buffered (`readyState >= 3`) before resuming playback.
|
||||
To connect your extension to a self-hosted server, open the popup → **Room** tab → select **Custom Server** → enter your server's WebSocket URL (e.g., `ws://localhost:3000`).
|
||||
|
||||
## Security & Privacy
|
||||
> [!IMPORTANT]
|
||||
> **Privacy First**: KoalaSync stores no data on disk. All room states exist only in RAM and are purged immediately when empty. There is zero telemetry, tracking, or analytics.
|
||||
> **⚠️ Note**: `ws://` only works for `localhost`. If you deploy to a real domain, you **must** use `wss://` (e.g., `wss://sync.yourdomain.com`). This requires a TLS-terminating reverse proxy (e.g., Caddy, Nginx, or Traefik) in front of the relay server. See [Caddyfile.example](Caddyfile.example) for a production-ready template.
|
||||
|
||||
## Troubleshooting
|
||||
- **Logs**: Check the **Dev** tab in the extension popup for live connection logs and video state diagnostics.
|
||||
- **Handshake**: Verify you see `Joined Namespace /` in the logs.
|
||||
- **Permissions**: Ensure the target site hasn't blocked script injection (rare for most video sites).
|
||||
---
|
||||
|
||||
### 📖 Documentation & Links
|
||||
|
||||
- **[PRIVACY.md](PRIVACY.md)**: Data Handling and Privacy Policy.
|
||||
- **[CONTRIBUTING.md](CONTRIBUTING.md)**: How to help make KoalaSync better.
|
||||
- **[HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)**: Step-by-step walkthrough of the complete user flow.
|
||||
- **[ARCHITECTURE.md](docs/ARCHITECTURE.md)**: Deep-dive into the two-phase sync and heartbeat logic.
|
||||
- **[SECURITY.md](SECURITY.md)**: Disclosure policy and security practices.
|
||||
- **[Caddyfile.example](Caddyfile.example)**: Production Caddy configuration for website and relay.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>Built with ❤️ by <a href="https://github.com/Shik3i">Shik3i</a>. KoalaSync is Open Source under the <a href="LICENSE">MIT License</a>.</sub>
|
||||
</div>
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We take the security of our users and their data very seriously. We actively support and patch the latest stable releases of KoalaSync.
|
||||
|
||||
| Version | Supported |
|
||||
| -------------- | ------------------ |
|
||||
| Latest Release | :white_check_mark: |
|
||||
| Older Versions | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability within KoalaSync (e.g., related to the Node.js relay server, WebSocket wire protocol, or the Chrome/Firefox browser extension), please **DO NOT** report it by creating a public GitHub issue.
|
||||
|
||||
Publicly disclosing a vulnerability before a patch is available puts our users at risk. Instead, please send an email privately to the project administrator at:
|
||||
**koalasync_admin@koalamail.rocks**
|
||||
|
||||
### What to expect
|
||||
1. **Acknowledgment**: You should receive an acknowledgment of your report within 48 hours.
|
||||
2. **Investigation**: We will investigate the issue, confirm its severity, and work on a patch.
|
||||
3. **Resolution**: We will notify you when the patch is deployed to the Chrome Web Store, Mozilla Add-on Store, and our GitHub Docker releases.
|
||||
4. **Disclosure**: Once the fix is confirmed and users have had time to update, we will publicly acknowledge your contribution in our release notes (unless you prefer to remain anonymous).
|
||||
@@ -1,43 +0,0 @@
|
||||
# KoalaSync Protocol Synchronization Guide
|
||||
|
||||
## Why do we need to sync?
|
||||
KoalaSync uses a "Single Source of Truth" for its communication protocol constants located in the root `shared/` directory. However, Chrome Extensions (Manifest V3) are strictly sandboxed and **cannot load or import files from outside their root directory**.
|
||||
|
||||
To ensure that the extension and the relay server are always using the exact same event names and protocol versions, we maintain a mirrored copy of the shared files within the `extension/shared/` folder.
|
||||
|
||||
## When should you run the sync script?
|
||||
You MUST run the synchronization script in any of the following scenarios:
|
||||
1. **After a fresh `git clone` or `git pull`** (as the synced files are ignored by git).
|
||||
2. **After modifying** `shared/constants.js`.
|
||||
3. **After modifying** `shared/blacklist.js`.
|
||||
4. **Before committing** changes to the repository if any protocol-related files were touched.
|
||||
5. **Before deploying** the server or releasing the extension.
|
||||
|
||||
## How to sync
|
||||
|
||||
### On Windows
|
||||
Run the batch script from the repository root:
|
||||
```powershell
|
||||
.\scripts\sync-constants.bat
|
||||
```
|
||||
|
||||
### On macOS / Linux
|
||||
Run the shell script from the repository root:
|
||||
```bash
|
||||
./scripts/sync-constants.sh
|
||||
```
|
||||
|
||||
## What does it do?
|
||||
The script performs the following actions:
|
||||
1. Ensures the `extension/shared/` directory exists.
|
||||
2. Copies `shared/constants.js` to `extension/shared/constants.js`.
|
||||
3. Copies `shared/blacklist.js` to `extension/shared/blacklist.js`.
|
||||
|
||||
## Protocol Versioning
|
||||
As of v1.0.0-RC5, the system enforces a strict `protocolVersion` check during the `JOIN_ROOM` handshake.
|
||||
- The version is defined in `shared/constants.js`.
|
||||
- If the extension and server versions mismatch, the server will reject the connection with an `Incompatible protocol version` error.
|
||||
- **Always run the sync script** after bumping the version number to ensure both components are updated.
|
||||
|
||||
> [!CAUTION]
|
||||
> **NEVER** edit the files inside `extension/shared/` directly. They will be overwritten the next time the sync script is run. Always edit the files in the root `shared/` directory and then run the sync script.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 462 KiB |
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
koala-sync:
|
||||
image: ghcr.io/shik3i/koalasync:latest
|
||||
container_name: KoalaSync
|
||||
restart: always
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- PORT=3000
|
||||
- MIN_VERSION=1.0.0
|
||||
- MAX_ROOMS=100
|
||||
- MAX_PEERS_PER_ROOM=50
|
||||
# KoalaSync uses in-memory storage for the relay,
|
||||
# so no persistent database volume is required.
|
||||
pids_limit: 2048
|
||||
# Example for custom network (e.g., Unraid/Macvlan)
|
||||
# networks:
|
||||
# custom_network:
|
||||
# ipv4_address: 192.168.1.XXX
|
||||
|
||||
# networks:
|
||||
# custom_network:
|
||||
# external: true
|
||||
# name: br0
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
server:
|
||||
|
||||
@@ -16,7 +16,7 @@ This document describes the communication flows and internal logic of the KoalaS
|
||||
## 2. Media Event Synchronization
|
||||
When a user interacts with a video:
|
||||
1. **Detection**: `content.js` listens to native events (`play`, `pause`, `seeked`) on the `<video>` element.
|
||||
2. **Prevention of Loops**: Uses `lastTargetState` to distinguish between user actions and programmatic actions triggered by the extension.
|
||||
2. **Prevention of Loops**: Uses an `expectedEvents` Set to distinguish between user actions and programmatic actions. Expected events are consumed on match and expire via timeout.
|
||||
3. **Reporting**: `content.js` sends a `CONTENT_EVENT` to `background.js`.
|
||||
4. **Relay**: The Server forwards the event to all other peers in the room.
|
||||
5. **Execution**: Remote peers receive the command and call `video.play()`, `video.pause()`, or `video.currentTime = targetTime`.
|
||||
@@ -24,26 +24,44 @@ When a user interacts with a video:
|
||||
## 3. Two-Phase Force Sync
|
||||
Ensures all peers are frame-perfect and buffered before resuming:
|
||||
1. **Prepare**: Initiator sends `FORCE_SYNC_PREPARE` with the target timestamp.
|
||||
2. **Buffer**: Peers seek and pause. Once buffered (`readyState >= 3`), they send a `FORCE_SYNC_ACK`.
|
||||
3. **Execute**: Once the Initiator collects ACKs (or after a 5s timeout), they send `FORCE_SYNC_EXECUTE`.
|
||||
2. **Buffer**: Peers seek and pause. Once buffered (`readyState >= 3`), they send a `FORCE_SYNC_ACK`. (Note: `content.js` limits polling to 8000ms).
|
||||
3. **Execute**: Once the Initiator collects ACKs (or after an 8.5s timeout), they send `FORCE_SYNC_EXECUTE`.
|
||||
> [!IMPORTANT]
|
||||
> **Network Transit Buffer Rule**: The orchestrator (`background.js`) must always use a timeout at least 500ms longer than the worker (`content.js`) to account for IPC and network transit time. Never align them exactly 1:1, as this will introduce a race condition on slow connections.
|
||||
4. **Resume**: All peers call `play()` simultaneously.
|
||||
|
||||
## 4. Peer Lifecycle & Dual Heartbeat
|
||||
## 4. Episode Auto-Sync
|
||||
Maintains continuous synchronized viewing when watching series:
|
||||
1. **Detection**: `content.js` monitors the Media Session API for title changes.
|
||||
2. **Lobby Creation**: When a new title is detected, the peer initiates an `EPISODE_LOBBY` and broadcasts the new title.
|
||||
3. **Wait State**: All peers freeze their video until they have also loaded the exact same title.
|
||||
4. **Mid-Lobby Joins**: If a new user joins the room during an active lobby, the lobby initiator broadcasts the active lobby state so the newcomer can sync up.
|
||||
5. **Resume**: Once all peers report `EPISODE_READY`, the lobby is resolved and playback resumes perfectly.
|
||||
|
||||
## 5. Peer Lifecycle & Dual Heartbeat
|
||||
To maintain a clean room state and eliminate "Ghost Peers":
|
||||
- **Session Heartbeat (Background)**: Every 30 seconds, `background.js` sends an "I'm alive" signal to the server. This keeps you in the room even if no video is playing.
|
||||
- **Video Heartbeat (Content)**: Every 15 seconds, `content.js` sends current playback metadata (time, title, state) if a video is found.
|
||||
- **Server Pruning**: The server runs a "Reaper" every 2 minutes. If a peer has sent **zero** activity (no events and no heartbeats) for 5 minutes, they are forcefully disconnected.
|
||||
- **Immediate Cleanup**: Rooms are deleted instantly when the last peer leaves or disconnects.
|
||||
|
||||
## 5. Security & Stability
|
||||
> [!CAUTION]
|
||||
> **Identity Rule**: Differentiate between `peerId` and `socket.id`. Use `socket.id` exclusively for ephemeral transport routing on the server. Use `peerId` exclusively for identity, state management, and room tracking across the stack.
|
||||
|
||||
## 6. Broadcast Protocol & Routing
|
||||
KoalaSync uses a megaphone routing approach to minimize server logic:
|
||||
- **`emit()` Broadcast Behavior**: Any `emit()` from the extension client is unconditionally broadcast to **all other peers in the room**. It is not a direct message.
|
||||
- **Storm Prevention**: When dispatching state updates in response to a new user joining (e.g., an active lobby state), ensure ONLY the initiator (or a designated leader) calls `emit()` to prevent $O(N)$ broadcast storms.
|
||||
|
||||
## 7. Security & Stability
|
||||
- **Service Worker Lifecycle**: Uses `chrome.alarms` to prevent the Manifest V3 service worker from suspending while in an active room.
|
||||
- **Rate Limiting**: Server-side per-socket and per-IP rate limits to prevent sync-spamming or DoS.
|
||||
- **Noise Filtering**: Uses a curated blacklist of domains (Search Engines, Social Media) to declutter the "Target Tab" selector in the popup.
|
||||
- **Diagnostics**: A "Dev" tab provides real-time access to the underlying `<video>` state (`readyState`, `paused`, `currentTime`) for easier troubleshooting.
|
||||
|
||||
## 6. Constant Synchronization & Consistency
|
||||
## 8. Constant Synchronization & Consistency
|
||||
To maintain a "Single Source of Truth" across the server and extension without using a bundler:
|
||||
- **Relay Server & Extension Modules**: `background.js` and `popup.js` import constants directly from `shared/constants.js`.
|
||||
- **Content Scripts**: To ensure zero-latency execution, `content.js` uses a manual mirror of `EVENTS`.
|
||||
- **Synchronization**: The `./scripts/sync-constants.sh` script ensures that the `shared/` folder within the `extension/` directory is kept up-to-date with the root `shared/` source.
|
||||
- **Verification**: Any protocol change requires a manual verification sweep across all three constant locations (Shared, Server, and Content Script Mirror).
|
||||
- **Content Scripts**: To ensure zero-latency execution, `content.js` uses a synchronized copy of `EVENTS` and constants.
|
||||
- **Automation**: The `node scripts/build-extension.js` script automatically injects these constants into `content.js` during the build process, eliminating the risk of manual mirror mismatch.
|
||||
- **Verification**: Any protocol change is automatically propagated across the stack by running the build script.
|
||||
@@ -0,0 +1,213 @@
|
||||
# KoalaSync — How It Works (Step-by-Step)
|
||||
|
||||
This guide walks through the complete user flow of KoalaSync, from creating a room to synchronized playback. It is designed for **store reviewers**, **end-users**, and **manual testers** to understand exactly what happens at each step, what data is sent, and where it goes.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Installing the Extension
|
||||
|
||||
1. Download the extension from the [Releases](https://github.com/Shik3i/KoalaSync/releases) page (or install from the Chrome Web Store / Firefox Add-ons).
|
||||
2. The extension adds a small icon to your browser toolbar.
|
||||
3. On first install, a unique 8-character **Peer ID** is generated locally and stored in `chrome.storage.local`. This ID is never sent to any external service — it only travels to the relay server when you join a room.
|
||||
|
||||
> **What's stored locally**: `peerId` (8-char hex), `username` (customizable), `serverUrl`, `filterNoise` preference. All stored via `chrome.storage.sync` and `chrome.storage.local`.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Connecting to the Relay Server
|
||||
|
||||
When you open the extension popup, the background service worker connects to the relay server:
|
||||
|
||||
1. **WebSocket Handshake**: `background.js` opens a WebSocket to `wss://syncserver.koalastuff.net/socket.io/?EIO=4&transport=websocket`.
|
||||
2. **Security Checks** (server-side):
|
||||
- The server checks the client's **IP rate limit** (max 10 connections per 60 seconds).
|
||||
- The server validates the **authentication token** (hardcoded in `shared/constants.js`) to verify this is a legitimate KoalaSync client.
|
||||
- The server checks the **extension version** against `MIN_VERSION` to reject outdated clients.
|
||||
3. **Connection Established**: The server responds with an Engine.IO handshake (`0{...}`), followed by a Socket.IO namespace join (`40`). The connection status dot in the popup turns green.
|
||||
|
||||
> **Data sent to server**: `token` (authentication), `version` (e.g., `1.3.1`). No personal data is transmitted during connection.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Creating a Room
|
||||
|
||||
Click **"Create Room"** in the popup's Room tab:
|
||||
|
||||
1. The extension generates a random Room ID (e.g., `happy-koala-42`) and a random 6-character password.
|
||||
2. Room IDs are restricted to `[a-zA-Z0-9-]` (alphanumeric + hyphens only).
|
||||
3. The extension emits a `JOIN_ROOM` event to the server.
|
||||
|
||||
> **Data sent in `JOIN_ROOM`**:
|
||||
> ```json
|
||||
> {
|
||||
> "roomId": "happy-koala-42",
|
||||
> "password": "x7k2m9",
|
||||
> "peerId": "a1b2c3d4",
|
||||
> "username": "MyName",
|
||||
> "tabTitle": "YouTube - My Video",
|
||||
> "protocolVersion": "1.0.0"
|
||||
> }
|
||||
> ```
|
||||
|
||||
4. **Server-side processing**:
|
||||
- All fields are **sanitized**: `roomId` is stripped of invalid characters and clamped to 64 chars; `peerId` clamped to 16 chars; `password` clamped to 128 chars; `username` clamped to 30 chars.
|
||||
- The server **hashes the password** with bcrypt and stores the hash in RAM (the plaintext is never stored).
|
||||
- A new room object is created in memory with the peer's data.
|
||||
- The server responds with `ROOM_DATA` containing the list of peers in the room.
|
||||
|
||||
5. **Popup updates**: The Room tab switches to the "Active Room" view, showing your Room ID and an invitation link.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Sharing an Invitation Link
|
||||
|
||||
Click the **📋 Copy** button next to the invite link:
|
||||
|
||||
1. The extension constructs a URL in this format:
|
||||
```
|
||||
https://sync.koalastuff.net/join.html#join:<roomId>:<password>:<serverFlag>:<encodedServerUrl>
|
||||
```
|
||||
- `serverFlag`: `0` for official server, `1` for custom server.
|
||||
- `encodedServerUrl`: Only populated if using a custom server.
|
||||
|
||||
2. **Important**: The room credentials are in the **URL hash** (`#`), which means they are **never sent to the web server** — the hash fragment stays entirely in the browser. The landing page server never sees your room ID or password.
|
||||
|
||||
3. Send this link to your friend via any messaging app.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Your Friend Opens the Invitation Link
|
||||
|
||||
When your friend opens the link in their browser:
|
||||
|
||||
1. **`join.html` loads** on `sync.koalastuff.net`. The page displays "INVITATION DETECTED" with the Room ID.
|
||||
|
||||
2. **Extension detection**: The page checks for `document.documentElement.dataset.koalasyncInstalled`, which is set by `bridge.js` (a content script injected only on `sync.koalastuff.net`).
|
||||
|
||||
3. **If the extension IS installed**:
|
||||
- The page shows "Joining room automatically..."
|
||||
- After 500ms, the page dispatches a `KOALASYNC_JOIN_REQUEST` custom DOM event with `{ roomId, password, useCustomServer, serverUrl }`.
|
||||
- `bridge.js` catches this event and forwards it to `background.js` via `chrome.runtime.sendMessage`.
|
||||
- `background.js` stores the credentials in `chrome.storage.sync` and emits `JOIN_ROOM` to the server.
|
||||
- The server validates the password against the stored bcrypt hash.
|
||||
- On success, the server responds with `ROOM_DATA` and broadcasts `PEER_STATUS { status: 'joined' }` to all existing peers.
|
||||
- The join page updates to show "✅ Successfully joined!".
|
||||
|
||||
4. **If the extension is NOT installed**:
|
||||
- The page shows download links (Chrome Web Store / GitHub).
|
||||
- The user installs the extension, returns to the link, and the flow continues from step 3.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Selecting a Video Tab
|
||||
|
||||
Both users now need to select which browser tab contains the video to sync:
|
||||
|
||||
1. Open a video on any website (YouTube, Twitch, Netflix, etc.).
|
||||
2. In the extension popup → **Sync** tab → use the **"Target Tab"** dropdown.
|
||||
3. The dropdown lists all open tabs, filtered to exclude noise (search engines, social media — configurable via Settings).
|
||||
4. Tabs with a **matching video title** are highlighted with a ⭐ prefix for easy identification.
|
||||
5. Selecting a tab causes `background.js` to set `currentTabId` and inject `content.js` into that tab via `chrome.scripting.executeScript`.
|
||||
|
||||
> **What `content.js` does on injection**: Finds the first `<video>` element on the page and attaches event listeners for `play`, `pause`, `seeked`, `timeupdate`, and `volumechange`. It uses an `expectedEvents` Set to distinguish between user actions and programmatic actions (loop prevention).
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Synchronized Playback
|
||||
|
||||
When User A presses **Play** on their video:
|
||||
|
||||
1. `content.js` detects the native `play` event on the `<video>` element.
|
||||
2. It checks the `expectedEvents` Set — if this event was expected (caused by a remote command), it's consumed silently. If not, it's a **user action**.
|
||||
3. For user actions, `content.js` sends `{ type: 'CONTENT_EVENT', action: 'play', payload: { currentTime, ... } }` to `background.js`.
|
||||
4. `background.js` adds an `actionTimestamp` and emits the `PLAY` event to the server.
|
||||
5. **Server relay**: The server sanitizes all fields (strings clamped, numbers validated, booleans type-checked) and constructs a clean `relayPayload` with `senderId` set to User A's `peerId`. The raw client data is never forwarded directly.
|
||||
6. The server broadcasts the sanitized payload to all other peers in the room.
|
||||
7. User B's `background.js` receives the `PLAY` event and calls `routeToContent()`, which sends a `SERVER_COMMAND` message to User B's `content.js`.
|
||||
8. User B's `content.js` adds `'playing'` to its `expectedEvents` Set (so it won't echo the event back), then calls `video.play()`.
|
||||
|
||||
> **The same flow applies to Pause and Seek**, with Seek additionally sending `targetTime` for the time position.
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Force Sync (Two-Phase Protocol)
|
||||
|
||||
If videos drift out of sync, either user can click **"Force Sync"**:
|
||||
|
||||
### Phase 1 — Prepare
|
||||
1. The initiator's `content.js` captures the current `video.currentTime` as the `targetTime`.
|
||||
2. `background.js` emits `FORCE_SYNC_PREPARE` with `{ targetTime }` to all peers.
|
||||
3. All peers (including the initiator) **pause** their video and **seek** to `targetTime`.
|
||||
4. Each peer's `content.js` polls `video.readyState` until it reaches `≥ 3` (buffered enough to play), with an 8-second timeout.
|
||||
5. Once buffered, each peer sends `FORCE_SYNC_ACK` back.
|
||||
|
||||
### Phase 2 — Execute
|
||||
6. Once all ACKs are received (or after 8.5 seconds), the initiator emits `FORCE_SYNC_EXECUTE`.
|
||||
7. All peers call `video.play()` simultaneously, achieving frame-perfect sync.
|
||||
|
||||
> **Why two phases?** Without buffering confirmation, peers with slower connections would start playing before they've loaded the target timestamp, causing immediate desync.
|
||||
|
||||
---
|
||||
|
||||
## Step 9: Heartbeat & Peer Health
|
||||
|
||||
While in a room, two heartbeats keep the session alive:
|
||||
|
||||
| Heartbeat | Interval | Source | Purpose |
|
||||
|:----------|:---------|:-------|:--------|
|
||||
| **Background** | 30 seconds | `background.js` | Signals "I'm still connected" even without a video |
|
||||
| **Content** | 15 seconds | `content.js` | Sends video metadata: `currentTime`, `mediaTitle`, `playbackState`, `volume`, `muted` |
|
||||
|
||||
- **Server Reaper**: Every 2 minutes, the server checks for peers with no activity for 5+ minutes and disconnects them ("dead peer pruning").
|
||||
- **Room Cleanup**: Empty rooms are deleted immediately. Inactive rooms are pruned after 2 hours.
|
||||
|
||||
---
|
||||
|
||||
## Step 10: Leaving a Room
|
||||
|
||||
When a user clicks **"Leave"** or closes their browser:
|
||||
|
||||
1. `background.js` emits `LEAVE_ROOM` (or the WebSocket `disconnect` fires automatically).
|
||||
2. The server calls `removePeerFromRoom()`, which:
|
||||
- Removes the peer from the room's `peers` Set, `peerIds` Map, and `peerData` Map.
|
||||
- Removes the socket from the global `socketToRoom` and `peerToSocket` maps.
|
||||
- Broadcasts `PEER_STATUS { status: 'left' }` to remaining peers.
|
||||
- If the room is now empty, **deletes the room entirely** — no data persists.
|
||||
3. The event rate-limit counter for that socket is also cleaned up.
|
||||
|
||||
> **After disconnect, zero data about the user remains on the server.** There is no database, no log file, no analytics record. The session existed only in RAM and is now gone.
|
||||
|
||||
---
|
||||
|
||||
## Episode Auto-Sync Flow
|
||||
|
||||
When watching a series and an episode ends:
|
||||
|
||||
1. `content.js` monitors the [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API) for title changes.
|
||||
2. When a new title is detected, the peer broadcasts `EPISODE_LOBBY` with the expected new title.
|
||||
3. All peers' videos freeze. The UI shows an "Episode Lobby" card with peer readiness status.
|
||||
4. Each peer's `content.js` polls for the new title to appear in the `<video>` element's metadata.
|
||||
5. Once a peer detects the matching title, they send `EPISODE_READY`.
|
||||
6. When all peers report ready, the lobby resolves and playback resumes simultaneously.
|
||||
|
||||
---
|
||||
|
||||
## Data Flow Summary
|
||||
|
||||
```
|
||||
┌─────────────┐ WebSocket ┌──────────────┐ WebSocket ┌─────────────┐
|
||||
│ Extension │ ←─────────────────→│ Relay Server │←──────────────────→│ Extension │
|
||||
│ (User A) │ JOIN_ROOM │ (RAM only) │ JOIN_ROOM │ (User B) │
|
||||
│ │ PLAY/PAUSE/SEEK │ │ PLAY/PAUSE/SEEK │ │
|
||||
│ │ FORCE_SYNC_* │ Sanitizes & │ FORCE_SYNC_* │ │
|
||||
│ │ PEER_STATUS │ relays only │ PEER_STATUS │ │
|
||||
│ │ EPISODE_* │ │ EPISODE_* │ │
|
||||
└──────┬──────┘ └───────────────┘ └──────┬──────┘
|
||||
│ │
|
||||
┌────┴─────┐ ┌─────┴────┐
|
||||
│ content │ Listens to <video> events │ content │
|
||||
│ .js │ Controls playback │ .js │
|
||||
└──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
> **The relay server is a pure message forwarder.** It never interprets video content, accesses URLs, or stores session history. All media control happens locally inside each user's browser via the `<video>` DOM API.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Technical Documentation
|
||||
|
||||
This directory contains deep-dives into the KoalaSync protocol and architecture.
|
||||
|
||||
- [HOW_IT_WORKS.md](HOW_IT_WORKS.md): Step-by-step walkthrough of every user flow, from room creation to synchronized playback. Ideal for store reviewers and manual testers.
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md): Communication flows, Dual Heartbeat, and Sync logic.
|
||||
- [SYNC_GUIDE.md](SYNC_GUIDE.md): Protocol constants and sync requirements.
|
||||
@@ -0,0 +1,37 @@
|
||||
# KoalaSync Protocol Synchronization Guide
|
||||
|
||||
## Why do we need to sync?
|
||||
KoalaSync uses a "Single Source of Truth" for its communication protocol constants located in the root `shared/` directory. However, Browser Extensions (Manifest V3) are strictly sandboxed and **cannot load or import files from outside their root directory**.
|
||||
|
||||
To ensure that the extension and the relay server are always using the exact same event names and protocol versions, we maintain a mirrored copy of the shared files within the `extension/shared/` folder.
|
||||
|
||||
## When should you run the build script?
|
||||
You MUST run the build script in any of the following scenarios:
|
||||
1. **After a fresh `git clone` or `git pull`** (as the synced files are ignored by git).
|
||||
2. **After modifying** `shared/constants.js`.
|
||||
3. **After modifying** `shared/blacklist.js`.
|
||||
4. **Before committing** changes to the repository if any protocol-related files were touched.
|
||||
5. **Before deploying** the server or releasing the extension.
|
||||
|
||||
## How to sync
|
||||
|
||||
Run the Node.js build script from the repository root:
|
||||
```bash
|
||||
node scripts/build-extension.js
|
||||
```
|
||||
|
||||
## What does it do?
|
||||
The build script performs the following actions:
|
||||
1. Synchronizes protocol constants by copying `shared/constants.js`, `shared/blacklist.js`, and `shared/README.md` into `extension/shared/`.
|
||||
2. Injects `EVENTS` and `HEARTBEAT_INTERVAL` into `content.js` via marker-based replacement.
|
||||
3. Compiles browser-specific manifest files.
|
||||
4. Packages the final ready-to-publish extension artifacts for Chrome and Firefox into the `dist/` directory.
|
||||
|
||||
## Protocol Versioning
|
||||
The system enforces a strict `protocolVersion` check during the `JOIN_ROOM` handshake.
|
||||
- The version is defined in `shared/constants.js`.
|
||||
- If the extension and server versions mismatch, the server will reject the connection with an `Incompatible protocol version` error.
|
||||
- **Always run the build script** after bumping the version number to ensure both components are updated.
|
||||
|
||||
> [!CAUTION]
|
||||
> **NEVER** edit the files inside `extension/shared/` directly. They will be overwritten the next time the build script is run. Always edit the files in the root `shared/` directory and then run the build script.
|
||||
+13
-7
@@ -1,6 +1,6 @@
|
||||
# KoalaSync Chrome Extension
|
||||
# KoalaSync Browser Extension
|
||||
|
||||
A Manifest V3 Chrome Extension for synchronized video playback across any website.
|
||||
A Manifest V3 Browser Extension (Chrome & Firefox) for synchronized video playback across any website.
|
||||
|
||||
## Key Features
|
||||
- **Manifest V3**: Optimized Service Worker architecture with session persistence.
|
||||
@@ -20,14 +20,20 @@ KoalaSync requires `<all_urls>` permission to detect and interact with video ele
|
||||
- **No Browsing History**: We do not track or store your browsing history.
|
||||
- **State Management**: Sensitive data (Room Passwords) is stored locally using `chrome.storage`.
|
||||
- **Zero Telemetry**: No analytics or external tracking scripts.
|
||||
- **Zero Runtime Dependencies**: The extension is built with pure Vanilla JS and contains no external libraries or tracking scripts, ensuring maximum performance and privacy.
|
||||
|
||||
## Installation
|
||||
1. **Sync Protocol**: Run `./scripts/sync-constants.sh` (macOS/Linux) or `scripts\sync-constants.bat` (Windows) from the root.
|
||||
1. **Prepare Extension**: From the repository root, run:
|
||||
```bash
|
||||
node scripts/build-extension.js
|
||||
```
|
||||
2. Open Chrome and go to `chrome://extensions/`.
|
||||
3. Enable **Developer mode** (top right).
|
||||
4. Click **Load unpacked** and select the `extension` folder.
|
||||
4. Click **Load unpacked** and select the `dist/chrome` folder.
|
||||
|
||||
## Development
|
||||
If you modify `shared/constants.js`, you must synchronize the changes across the extension and server:
|
||||
- **Windows**: Run `scripts\sync-constants.bat`
|
||||
- **Linux/macOS**: Run `scripts/sync-constants.sh`
|
||||
If you modify `shared/constants.js`, you must synchronize the changes by running the build script from the root:
|
||||
```bash
|
||||
node scripts/build-extension.js
|
||||
```
|
||||
This ensures that the `extension/shared` folder is updated with the latest protocol constants.
|
||||
|
||||
+15
-4
@@ -542,6 +542,10 @@ function handleServerEvent(event, data) {
|
||||
currentRoom.peers.push(createPeerData(data));
|
||||
if (storageInitialized) chrome.storage.session.set({ currentRoom });
|
||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
|
||||
|
||||
if (episodeLobby && episodeLobby.initiatorPeerId === peerId) {
|
||||
emit(EVENTS.EPISODE_LOBBY, { peerId, expectedTitle: episodeLobby.expectedTitle });
|
||||
}
|
||||
}
|
||||
} else if (data.status === 'left') {
|
||||
currentRoom.peers = currentRoom.peers.filter(p => (p.peerId || p) !== data.peerId);
|
||||
@@ -552,6 +556,13 @@ function handleServerEvent(event, data) {
|
||||
if (episodeLobby) {
|
||||
checkEpisodeLobbyPeerDeparture();
|
||||
}
|
||||
|
||||
if (isForceSyncInitiator) {
|
||||
const peerCount = currentRoom.peers ? currentRoom.peers.length : 1;
|
||||
if (forceSyncAcks.size >= peerCount) {
|
||||
executeForceSync();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Heartbeat/Update: Update tabTitle for matching
|
||||
const peer = currentRoom.peers.find(p => (p.peerId || p) === data.peerId);
|
||||
@@ -691,7 +702,7 @@ function executeEpisodeLobby() {
|
||||
// Trigger a standard Force Sync at targetTime 0.0
|
||||
isForceSyncInitiator = true;
|
||||
forceSyncAcks.clear();
|
||||
const deadline = Date.now() + 5000;
|
||||
const deadline = Date.now() + 8500;
|
||||
chrome.storage.session.set({
|
||||
isForceSyncInitiator: true,
|
||||
forceSyncAcks: [],
|
||||
@@ -707,7 +718,7 @@ function executeEpisodeLobby() {
|
||||
addLog('Force Sync (Episode): Timeout waiting for ACKs, executing anyway...', 'warn');
|
||||
executeForceSync();
|
||||
}
|
||||
}, 5000);
|
||||
}, 8500);
|
||||
}
|
||||
|
||||
function checkEpisodeLobbyCompletion() {
|
||||
@@ -943,7 +954,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
if (message.action === EVENTS.FORCE_SYNC_PREPARE) {
|
||||
isForceSyncInitiator = true;
|
||||
forceSyncAcks.clear();
|
||||
const deadline = Date.now() + 5000;
|
||||
const deadline = Date.now() + 8500;
|
||||
chrome.storage.session.set({
|
||||
isForceSyncInitiator: true,
|
||||
forceSyncAcks: [],
|
||||
@@ -958,7 +969,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
addLog('Force Sync: Timeout waiting for ACKs, executing anyway...', 'warn');
|
||||
executeForceSync();
|
||||
}
|
||||
}, 5000);
|
||||
}, 8500);
|
||||
}
|
||||
addToHistory(message.action, 'You');
|
||||
emit(message.action, { ...message.payload, peerId });
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* KoalaSync Bridge Script
|
||||
* Injected into koalasync.shik3i.net to facilitate communication between
|
||||
* Injected into sync.koalastuff.net to facilitate communication between
|
||||
* the landing page and the extension.
|
||||
*/
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
}
|
||||
window.koalaSyncInjected = true;
|
||||
|
||||
// Local Protocol Constants (Mirroring shared/constants.js)
|
||||
// --- SHARED_EVENTS_INJECT_START ---
|
||||
// This block is automatically updated by /scripts/build-extension.js
|
||||
const EVENTS = {
|
||||
PLAY: "play",
|
||||
PAUSE: "pause",
|
||||
@@ -26,6 +27,7 @@
|
||||
EPISODE_LOBBY: "episode_lobby",
|
||||
EPISODE_READY: "episode_ready"
|
||||
};
|
||||
// --- SHARED_EVENTS_INJECT_END ---
|
||||
|
||||
let expectedEvents = new Set();
|
||||
let expectedTimeouts = {};
|
||||
@@ -504,6 +506,10 @@
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// --- SHARED_HEARTBEAT_INJECT_START ---
|
||||
const HEARTBEAT_INTERVAL_VAL = 15000;
|
||||
// --- SHARED_HEARTBEAT_INJECT_END ---
|
||||
|
||||
// Heartbeat
|
||||
let heartbeatErrorCount = 0;
|
||||
const heartbeatInterval = setInterval(() => {
|
||||
@@ -530,7 +536,7 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 15000);
|
||||
}, HEARTBEAT_INTERVAL_VAL);
|
||||
|
||||
// Initial Setup
|
||||
setupListeners();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "KoalaSync",
|
||||
"version": "1.2.1",
|
||||
"description": "Synchronize video playback across different tabs and users.",
|
||||
"version": "1.3.1",
|
||||
"description": "Watch party extension to synchronize video playback on YouTube, Twitch, Netflix, and HTML5 sites in real-time with friends.",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"tabs",
|
||||
@@ -22,13 +22,9 @@
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://koalasync.shik3i.net/*"],
|
||||
"matches": ["https://sync.koalastuff.net/*"],
|
||||
"js": ["bridge.js"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
+12
-2
@@ -31,9 +31,19 @@
|
||||
font-size: 18px;
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--accent);
|
||||
text-align: center;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
h1 img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
@@ -192,7 +202,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>KoalaSync</h1>
|
||||
<h1><img src="icons/icon128.png" alt="KoalaSync Logo">KoalaSync</h1>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="tab-room">Room</button>
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ function updateUI(roomId, password, useCustomServer = false, serverUrl = '') {
|
||||
if (elements.activeRoomId) elements.activeRoomId.textContent = roomId;
|
||||
if (elements.activeServer) {
|
||||
elements.activeServer.textContent = useCustomServer ? (serverUrl || 'Custom Server') : 'Official Server';
|
||||
elements.activeServer.title = useCustomServer ? (serverUrl || '') : 'sync.shik3i.net';
|
||||
elements.activeServer.title = useCustomServer ? (serverUrl || '') : 'syncserver.koalastuff.net';
|
||||
}
|
||||
} else {
|
||||
updatePeerList([]);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# ⚠️ READ BEFORE EDITING
|
||||
|
||||
This directory is a **MIRROR** of the root `/shared` folder.
|
||||
|
||||
**DO NOT edit these files directly.** Any changes made here will be overwritten the next time the synchronization script is run.
|
||||
|
||||
### Proper Workflow:
|
||||
1. **Edit** the source files in the root `[repo_root]/shared/` directory.
|
||||
2. **Run** the synchronization script:
|
||||
- **Windows**: `[repo_root]\scripts\sync-constants.bat`
|
||||
- **Linux/macOS**: `[repo_root]/scripts/sync-constants.sh`
|
||||
3. **Verify** that the changes have propagated to this folder.
|
||||
|
||||
Failure to follow this protocol will result in out-of-sync components and broken protocol logic.
|
||||
Generated
+1155
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "koalasync",
|
||||
"version": "1.3.1",
|
||||
"description": "KoalaSync Build Scripts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:extension": "node scripts/build-extension.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"fs-extra": "^11.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# Development Scripts
|
||||
|
||||
This directory contains utility scripts for the KoalaSync development workflow.
|
||||
|
||||
## build-extension.js
|
||||
|
||||
The primary build tool for KoalaSync. This Node.js script automates two critical tasks:
|
||||
|
||||
1. **Protocol Synchronization**: Copies the "Single Source of Truth" constants (`shared/constants.js`) and the domain blacklist (`shared/blacklist.js`) from the root `/shared` directory into the `extension/shared/` directory.
|
||||
2. **Content Script Injection**: Injects protocol constants directly into `content.js` using marker-based replacement. This is necessary because `content.js` executes synchronously and cannot use ES module imports.
|
||||
3. **Artifact Generation**: Compiles the extension into browser-specific bundles for Chrome and Firefox, located in the `dist/` directory.
|
||||
|
||||
### Usage
|
||||
|
||||
From the **repository root**, run:
|
||||
|
||||
```bash
|
||||
node scripts/build-extension.js
|
||||
```
|
||||
|
||||
### Why this script exists
|
||||
KoalaSync uses **Vanilla JS** in the extension to maintain zero runtime dependencies and maximum privacy. Since we don't use a bundler (like Webpack or Vite) inside the extension, this script serves as our lightweight "pre-build" step to ensure that the protocol constants remain synchronized between the extension and the relay server.
|
||||
|
||||
### Content Injection Markers
|
||||
|
||||
The build script uses marker comments in `content.js` to locate and replace constant blocks:
|
||||
|
||||
| Marker Pair | Injected Value | Source |
|
||||
|:---|:---|:---|
|
||||
| `SHARED_EVENTS_INJECT_START` / `END` | The full `EVENTS` object | `shared/constants.js` |
|
||||
| `SHARED_HEARTBEAT_INJECT_START` / `END` | `HEARTBEAT_INTERVAL` value | `shared/constants.js` |
|
||||
|
||||
> **⚠️ Do NOT remove or modify these marker comments in `content.js`.** They are required for the build script to function. If the markers are missing, the build will fail with a clear error message.
|
||||
@@ -0,0 +1,177 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const archiver = require('archiver');
|
||||
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const extDir = path.join(rootDir, 'extension');
|
||||
const distDir = path.join(rootDir, 'dist');
|
||||
const baseManifestPath = path.join(extDir, 'manifest.base.json');
|
||||
|
||||
// Ensure dist directory exists
|
||||
if (fs.existsSync(distDir)) {
|
||||
fs.rmSync(distDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
|
||||
// Sync shared constants from root /shared to /extension/shared
|
||||
console.log('Syncing protocol constants...');
|
||||
const masterSharedDir = path.join(rootDir, 'shared');
|
||||
const extSharedDir = path.join(extDir, 'shared');
|
||||
|
||||
if (!fs.existsSync(extSharedDir)) {
|
||||
fs.mkdirSync(extSharedDir, { recursive: true });
|
||||
}
|
||||
|
||||
const sharedFiles = ['constants.js', 'blacklist.js', 'README.md'];
|
||||
for (const file of sharedFiles) {
|
||||
const src = path.join(masterSharedDir, file);
|
||||
const dest = path.join(extSharedDir, file);
|
||||
if (!fs.existsSync(src)) {
|
||||
throw new Error(`CRITICAL: Source shared file missing: ${src}. Aborting build to prevent broken artifacts.`);
|
||||
}
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
console.log('✓ constants.js, blacklist.js, and README.md synced to extension/shared/');
|
||||
|
||||
// Read the base manifest
|
||||
const baseManifest = JSON.parse(fs.readFileSync(baseManifestPath, 'utf8'));
|
||||
|
||||
// Helper to copy files, ignoring manifest.json and manifest.base.json
|
||||
// Also injects shared constants into content.js
|
||||
function copyExtensionFiles(targetDir) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
|
||||
// Read master constants for injection
|
||||
const masterConstantsPath = path.join(rootDir, 'shared', 'constants.js');
|
||||
const constantsContent = fs.readFileSync(masterConstantsPath, 'utf8');
|
||||
|
||||
// Robust Extraction using flexible regex
|
||||
const eventsMatch = constantsContent.match(/export const EVENTS\s*=\s*({[\s\S]+?});/);
|
||||
const heartbeatMatch = constantsContent.match(/export const HEARTBEAT_INTERVAL\s*=\s*(\d+);/);
|
||||
|
||||
if (!eventsMatch) {
|
||||
throw new Error('CRITICAL: Could not find EVENTS object in shared/constants.js');
|
||||
}
|
||||
if (!heartbeatMatch) {
|
||||
throw new Error('CRITICAL: Could not find HEARTBEAT_INTERVAL in shared/constants.js');
|
||||
}
|
||||
|
||||
const eventsObject = eventsMatch[1];
|
||||
const heartbeatVal = heartbeatMatch[1];
|
||||
|
||||
const items = fs.readdirSync(extDir);
|
||||
for (const item of items) {
|
||||
if (item === 'manifest.json' || item === 'manifest.base.json') continue;
|
||||
|
||||
const srcPath = path.join(extDir, item);
|
||||
const destPath = path.join(targetDir, item);
|
||||
|
||||
if (fs.lstatSync(srcPath).isDirectory()) {
|
||||
fs.cpSync(srcPath, destPath, { recursive: true });
|
||||
} else {
|
||||
if (item === 'content.js') {
|
||||
// Perform injection
|
||||
let content = fs.readFileSync(srcPath, 'utf8');
|
||||
|
||||
// 1. Inject Events
|
||||
const eStart = '// --- SHARED_EVENTS_INJECT_START ---';
|
||||
const eEnd = '// --- SHARED_EVENTS_INJECT_END ---';
|
||||
const ePattern = new RegExp(`${eStart}[\\s\\S]+?${eEnd}`);
|
||||
const eRep = `${eStart}\n // This block is automatically updated by /scripts/build-extension.js\n const EVENTS = ${eventsObject};\n ${eEnd}`;
|
||||
|
||||
if (ePattern.test(content)) {
|
||||
content = content.replace(ePattern, eRep);
|
||||
} else {
|
||||
console.warn('⚠️ WARNING: Event markers not found in content.js');
|
||||
}
|
||||
|
||||
// 2. Inject Heartbeat
|
||||
const hStart = '// --- SHARED_HEARTBEAT_INJECT_START ---';
|
||||
const hEnd = '// --- SHARED_HEARTBEAT_INJECT_END ---';
|
||||
const hPattern = new RegExp(`${hStart}[\\s\\S]+?${hEnd}`);
|
||||
const hRep = `${hStart}\n const HEARTBEAT_INTERVAL_VAL = ${heartbeatVal};\n ${hEnd}`;
|
||||
|
||||
if (hPattern.test(content)) {
|
||||
content = content.replace(hPattern, hRep);
|
||||
} else {
|
||||
console.warn('⚠️ WARNING: Heartbeat markers not found in content.js');
|
||||
}
|
||||
|
||||
fs.writeFileSync(destPath, content);
|
||||
console.log('✓ Injected shared constants into content.js');
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to zip a directory
|
||||
function zipDirectory(sourceDir, outPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
const stream = fs.createWriteStream(outPath);
|
||||
|
||||
archive
|
||||
.directory(sourceDir, false)
|
||||
.on('error', err => reject(err))
|
||||
.pipe(stream);
|
||||
|
||||
stream.on('close', () => resolve());
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
async function buildBrowser(browserName, manifestModifier) {
|
||||
console.log(`Building for ${browserName}...`);
|
||||
const browserDistDir = path.join(distDir, browserName);
|
||||
|
||||
// 1. Copy files
|
||||
copyExtensionFiles(browserDistDir);
|
||||
|
||||
// 2. Modify and write manifest
|
||||
const browserManifest = manifestModifier(JSON.parse(JSON.stringify(baseManifest)));
|
||||
fs.writeFileSync(
|
||||
path.join(browserDistDir, 'manifest.json'),
|
||||
JSON.stringify(browserManifest, null, 2)
|
||||
);
|
||||
|
||||
// 3. Zip it
|
||||
const zipPath = path.join(distDir, `koalasync-${browserName}.zip`);
|
||||
await zipDirectory(browserDistDir, zipPath);
|
||||
console.log(`Successfully built and zipped ${browserName} -> ${zipPath}`);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// Build Chrome
|
||||
await buildBrowser('chrome', (manifest) => {
|
||||
manifest.background = {
|
||||
service_worker: "background.js",
|
||||
type: "module"
|
||||
};
|
||||
return manifest;
|
||||
});
|
||||
|
||||
// Build Firefox
|
||||
await buildBrowser('firefox', (manifest) => {
|
||||
manifest.background = {
|
||||
scripts: ["background.js"],
|
||||
type: "module"
|
||||
};
|
||||
manifest.browser_specific_settings = {
|
||||
gecko: {
|
||||
id: "koalasync@koalastuff.net"
|
||||
}
|
||||
};
|
||||
return manifest;
|
||||
});
|
||||
|
||||
console.log('Build complete!');
|
||||
} catch (error) {
|
||||
console.error('Build failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,12 +0,0 @@
|
||||
@echo off
|
||||
REM KoalaSync - Protocol Synchronization Script (Windows)
|
||||
REM
|
||||
REM This script copies the master constants.js file from the shared directory
|
||||
REM to the extension directory. Since Chrome Extensions cannot load files
|
||||
REM outside their root, this manual sync is required after any changes to
|
||||
REM the shared protocol.
|
||||
|
||||
if not exist extension\shared mkdir extension\shared
|
||||
copy /y shared\constants.js extension\shared\constants.js
|
||||
copy /y shared\blacklist.js extension\shared\blacklist.js
|
||||
echo ✓ constants.js and blacklist.js synced to extension\shared\
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/sh
|
||||
# KoalaSync - Protocol Synchronization Script (Linux/macOS)
|
||||
#
|
||||
# This script copies the master constants.js file from the shared directory
|
||||
# to the extension directory. Since Chrome Extensions cannot load files
|
||||
# outside their root, this manual sync is required after any changes to
|
||||
# the shared protocol.
|
||||
|
||||
mkdir -p extension/shared
|
||||
cp shared/constants.js extension/shared/constants.js
|
||||
cp shared/blacklist.js extension/shared/blacklist.js
|
||||
echo "✓ constants.js and blacklist.js synced to extension/shared/"
|
||||
+10
-5
@@ -13,17 +13,21 @@ A high-performance Node.js relay server for synchronized video playback.
|
||||
Copy `.env.example` to `.env` and configure your settings.
|
||||
```bash
|
||||
PORT=3000
|
||||
MAX_ROOMS=100
|
||||
MAX_PEERS_PER_ROOM=20
|
||||
MAX_ROOMS=1000
|
||||
MAX_PEERS_PER_ROOM=50
|
||||
MIN_VERSION=1.0.0
|
||||
```
|
||||
|
||||
### Docker (Recommended)
|
||||
The server is designed to run in a Docker container.
|
||||
The server is available as a pre-built image on GHCR.
|
||||
```bash
|
||||
# Build from the repository root
|
||||
# Pull from GHCR
|
||||
docker pull ghcr.io/shik3i/koalasync:latest
|
||||
|
||||
# Or build from the repository root
|
||||
docker build -t koala-sync-server -f server/Dockerfile .
|
||||
```
|
||||
See [docker-compose.example.yml](../docker-compose.example.yml) in the root directory for a ready-to-use configuration.
|
||||
|
||||
### Manual Setup
|
||||
```bash
|
||||
@@ -34,5 +38,6 @@ npm start
|
||||
|
||||
## Security
|
||||
- **Rate Limiting**: IP-based connection limits and socket-based event limits.
|
||||
- **Token Handshake**: Requires a valid token defined in `shared/constants.js`.
|
||||
- **Token Handshake**: Requires a valid token defined in the root `shared/constants.js`.
|
||||
- **Single Source of Truth**: The server imports constants directly from the root `shared/` directory.
|
||||
- **In-Memory**: Rooms are automatically pruned after 2 hours of inactivity.
|
||||
|
||||
+55
-19
@@ -23,7 +23,7 @@ const httpServer = createServer(app);
|
||||
// Socket.IO setup with security constraints
|
||||
const io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: ["https://koalasync.shik3i.net"],
|
||||
origin: ["https://sync.koalastuff.net"],
|
||||
methods: ["GET", "POST"]
|
||||
},
|
||||
maxHttpBufferSize: 1024, // 1KB max per message
|
||||
@@ -85,7 +85,7 @@ setInterval(() => {
|
||||
|
||||
const eventCounts = new Map(); // socketId -> { count, resetTime }
|
||||
|
||||
// Clean up connection counts to prevent memory leak
|
||||
// Clean up connection counts and event counts to prevent memory leak
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [ip, entry] of connectionCounts.entries()) {
|
||||
@@ -93,6 +93,11 @@ setInterval(() => {
|
||||
connectionCounts.delete(ip);
|
||||
}
|
||||
}
|
||||
for (const [socketId, entry] of eventCounts.entries()) {
|
||||
if (now > entry.resetTime) {
|
||||
eventCounts.delete(socketId);
|
||||
}
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
function checkConnectionRate(ip) {
|
||||
@@ -182,6 +187,12 @@ io.on('connection', (socket) => {
|
||||
if (clientVersion) {
|
||||
const [cMaj, cMin, cPatch] = clientVersion.split('.').map(Number);
|
||||
const [mMaj, mMin, mPatch] = MIN_VERSION.split('.').map(Number);
|
||||
if (isNaN(cMaj) || isNaN(cMin) || isNaN(cPatch)) {
|
||||
log('AUTH', `Invalid version format (${clientVersion}) from ${clientIp}`);
|
||||
socket.emit(EVENTS.ERROR, { message: 'Invalid version format' });
|
||||
socket.disconnect(true);
|
||||
return;
|
||||
}
|
||||
const tooOld = cMaj < mMaj || (cMaj === mMaj && cMin < mMin) || (cMaj === mMaj && cMin === mMin && cPatch < mPatch);
|
||||
if (tooOld) {
|
||||
log('AUTH', `Version too old (${clientVersion}) from ${clientIp}`);
|
||||
@@ -200,15 +211,17 @@ io.on('connection', (socket) => {
|
||||
return;
|
||||
}
|
||||
if (!payload || typeof payload.roomId !== 'string') return;
|
||||
const { password, peerId, protocolVersion } = payload;
|
||||
|
||||
// --- M-2: Sanitize and clamp all string fields ---
|
||||
const roomId = String(payload.roomId || '').substring(0, 64);
|
||||
// --- S-1 & S-5: Sanitize and clamp all incoming fields ---
|
||||
const password = typeof payload.password === 'string' ? payload.password.substring(0, 128) : null;
|
||||
const peerId = typeof payload.peerId === 'string' ? payload.peerId.substring(0, 16) : null;
|
||||
const protocolVersion = typeof payload.protocolVersion === 'string' ? payload.protocolVersion.substring(0, 16) : null;
|
||||
const roomId = String(payload.roomId || '').replace(/[^a-zA-Z0-9\-]/g, '').substring(0, 64);
|
||||
const username = typeof payload.username === 'string' ? payload.username.substring(0, 30) : null;
|
||||
const tabTitle = typeof payload.tabTitle === 'string' ? payload.tabTitle.substring(0, 100) : null;
|
||||
const mediaTitle = typeof payload.mediaTitle === 'string' ? payload.mediaTitle.substring(0, 100) : null;
|
||||
|
||||
if (!roomId) return; // Guard: empty after sanitization
|
||||
if (!roomId || !peerId) return; // Guard: empty or invalid after sanitization
|
||||
|
||||
try {
|
||||
// Protocol check
|
||||
@@ -329,24 +342,45 @@ io.on('connection', (socket) => {
|
||||
if (room) {
|
||||
room.lastActivity = Date.now();
|
||||
|
||||
// Update peer metadata and lastSeen
|
||||
// Sanitize mutable string fields to enforce the same length
|
||||
// limits as JOIN_ROOM — the relay path is otherwise unbounded.
|
||||
const clamp = (val, max) => typeof val === 'string' ? val.substring(0, max) : val;
|
||||
// --- S-2 & S-3: Sanitize ALL relay fields (strings, numbers, booleans) ---
|
||||
const clamp = (val, max) => typeof val === 'string' ? val.substring(0, max) : undefined;
|
||||
const clampNum = (val, min, max) => typeof val === 'number' && Number.isFinite(val) ? Math.max(min, Math.min(max, val)) : undefined;
|
||||
const validState = (val) => (val === 'playing' || val === 'paused') ? val : undefined;
|
||||
const validBool = (val) => typeof val === 'boolean' ? val : undefined;
|
||||
|
||||
const existing = room.peerData.get(socket.id) || { peerId: mapping.peerId };
|
||||
room.peerData.set(socket.id, {
|
||||
...existing,
|
||||
username: data.username !== undefined ? clamp(data.username, 30) : existing.username,
|
||||
tabTitle: data.tabTitle !== undefined ? clamp(data.tabTitle, 100) : existing.tabTitle,
|
||||
mediaTitle: data.mediaTitle !== undefined ? clamp(data.mediaTitle, 100) : existing.mediaTitle,
|
||||
playbackState: data.playbackState !== undefined ? data.playbackState : existing.playbackState,
|
||||
currentTime: data.currentTime !== undefined ? data.currentTime : existing.currentTime,
|
||||
volume: data.volume !== undefined ? data.volume : existing.volume,
|
||||
muted: data.muted !== undefined ? data.muted : existing.muted,
|
||||
username: data.username !== undefined ? (clamp(data.username, 30) ?? existing.username) : existing.username,
|
||||
tabTitle: data.tabTitle !== undefined ? (clamp(data.tabTitle, 100) ?? existing.tabTitle) : existing.tabTitle,
|
||||
mediaTitle: data.mediaTitle !== undefined ? (clamp(data.mediaTitle, 100) ?? existing.mediaTitle) : existing.mediaTitle,
|
||||
playbackState: data.playbackState !== undefined ? (validState(data.playbackState) ?? existing.playbackState) : existing.playbackState,
|
||||
currentTime: data.currentTime !== undefined ? (clampNum(data.currentTime, 0, 86400) ?? existing.currentTime) : existing.currentTime,
|
||||
volume: data.volume !== undefined ? (clampNum(data.volume, 0, 1) ?? existing.volume) : existing.volume,
|
||||
muted: data.muted !== undefined ? (validBool(data.muted) ?? existing.muted) : existing.muted,
|
||||
lastSeen: Date.now()
|
||||
});
|
||||
|
||||
socket.to(mapping.roomId).emit(eventName, { ...data, senderId: mapping.peerId });
|
||||
// --- S-3: Construct clean relay payload — never forward raw client data ---
|
||||
const relayPayload = {
|
||||
senderId: mapping.peerId,
|
||||
currentTime: clampNum(data.currentTime, 0, 86400),
|
||||
targetTime: clampNum(data.targetTime, 0, 86400),
|
||||
playbackState: validState(data.playbackState),
|
||||
username: clamp(data.username, 30),
|
||||
tabTitle: clamp(data.tabTitle, 100),
|
||||
mediaTitle: clamp(data.mediaTitle, 100),
|
||||
volume: clampNum(data.volume, 0, 1),
|
||||
muted: validBool(data.muted),
|
||||
peerId: typeof data.peerId === 'string' ? data.peerId.substring(0, 16) : undefined,
|
||||
status: typeof data.status === 'string' ? data.status.substring(0, 16) : undefined,
|
||||
expectedTitle: clamp(data.expectedTitle, 100),
|
||||
title: clamp(data.title, 100),
|
||||
actionTimestamp: clampNum(data.actionTimestamp, 0, Number.MAX_SAFE_INTEGER),
|
||||
};
|
||||
// Strip undefined keys for clean wire format
|
||||
Object.keys(relayPayload).forEach(k => relayPayload[k] === undefined && delete relayPayload[k]);
|
||||
socket.to(mapping.roomId).emit(eventName, relayPayload);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -370,7 +404,9 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
|
||||
socket.on(EVENTS.EVENT_ACK, (data) => {
|
||||
if (!data.targetId) return;
|
||||
if (!data || typeof data !== 'object') return;
|
||||
if (typeof data.targetId !== 'string') return;
|
||||
if (data.actionTimestamp !== undefined && (typeof data.actionTimestamp !== 'number' || !Number.isFinite(data.actionTimestamp))) return;
|
||||
|
||||
const senderMapping = socketToRoom.get(socket.id);
|
||||
const targetSocketId = peerToSocket.get(data.targetId);
|
||||
|
||||
+22
-12
@@ -4,20 +4,30 @@ This directory contains constants and protocol definitions used by both the exte
|
||||
|
||||
## Syncing with the Extension
|
||||
> [!IMPORTANT]
|
||||
> Every time this file is modified, you must run `scripts/sync-constants.sh` to keep the extension's copy up to date.
|
||||
> Every time this directory is modified, you must run `node scripts/build-extension.js` to keep the extension's copy up to date.
|
||||
|
||||
Because Chrome Extensions cannot load files outside their root directory, `constants.js` must be copied to `extension/shared/constants.js` whenever it is modified.
|
||||
Because Browser Extensions (Manifest V3) cannot load files outside their root directory, all files in this directory must be copied to `extension/shared/` whenever they are modified. The build script handles this automatically.
|
||||
|
||||
## Security & Versioning Constants
|
||||
- `OFFICIAL_SERVER_TOKEN`: A 32-byte hex token required to connect to the official relay server.
|
||||
- `APP_VERSION`: The current version of the extension. Used by the server to enforce minimum version requirements (Revocation). This must always be in sync with `manifest.json`.
|
||||
- `APP_VERSION`: The current version of the extension. Automatically injected from the git tag during CI release builds.
|
||||
- `OFFICIAL_SERVER_URL`: The default endpoint for the official KoalaSync relay.
|
||||
- `ROOM_DATA`: Server response with current room state (peers).
|
||||
- `PLAY`: Sync command to start playback.
|
||||
- `PAUSE`: Sync command to pause playback.
|
||||
- `SEEK`: Sync command to change the current time.
|
||||
- `PEER_STATUS`: Heartbeat or join/leave notification for peers.
|
||||
- `FORCE_SYNC_PREPARE`: Phase 1 of Force Sync (Pause & Seek).
|
||||
- `FORCE_SYNC_ACK`: Peer confirmation of Phase 1 readiness.
|
||||
- `FORCE_SYNC_EXECUTE`: Phase 2 of Force Sync (Start Playback).
|
||||
- `ERROR`: Generic error message from the server.
|
||||
|
||||
## Protocol Events
|
||||
For the complete and current event list, see the `EVENTS` object in [`constants.js`](constants.js). Key events include:
|
||||
|
||||
| Event | Direction | Purpose |
|
||||
|:------|:----------|:--------|
|
||||
| `JOIN_ROOM` | Client → Server | Request to join a room with credentials |
|
||||
| `LEAVE_ROOM` | Client → Server | Leave the current room |
|
||||
| `ROOM_DATA` | Server → Client | Current room state (peers list) |
|
||||
| `PLAY` / `PAUSE` / `SEEK` | Bidirectional relay | Media control commands |
|
||||
| `PEER_STATUS` | Bidirectional relay | Heartbeat or join/leave notification |
|
||||
| `FORCE_SYNC_PREPARE` | Bidirectional relay | Phase 1: Pause & seek to target time |
|
||||
| `FORCE_SYNC_ACK` | Bidirectional relay | Phase 1 confirmation: peer is buffered |
|
||||
| `FORCE_SYNC_EXECUTE` | Bidirectional relay | Phase 2: Resume playback simultaneously |
|
||||
| `EVENT_ACK` | Server → Client | Delivery confirmation for UI feedback |
|
||||
| `EPISODE_LOBBY` | Bidirectional relay | Episode transition: waiting for all peers |
|
||||
| `EPISODE_READY` | Bidirectional relay | Episode confirmation: peer has loaded |
|
||||
| `GET_ROOMS` / `ROOM_LIST` | Client ↔ Server | Room discovery |
|
||||
| `ERROR` | Server → Client | Error message |
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
* blacklist.js
|
||||
*
|
||||
* ⚠️ WARNING: This is the SINGLE SOURCE OF TRUTH.
|
||||
* If you edit this file, you MUST run /scripts/sync-constants.bat
|
||||
* If you edit this file, you MUST run: node scripts/build-extension.js
|
||||
* to propagate changes to the extension and relay server.
|
||||
*
|
||||
* Domains to be filtered out from the tab selection dropdown to reduce "noise".
|
||||
@@ -78,7 +78,7 @@ export const BLACKLIST_DOMAINS = [
|
||||
'myanimelist.net',
|
||||
|
||||
// Development & Utilities
|
||||
'timer.shik3i.net',
|
||||
'timer.koalastuff.net',
|
||||
'localhost',
|
||||
'zoom.us',
|
||||
'teams.microsoft.com',
|
||||
|
||||
+4
-4
@@ -2,15 +2,15 @@
|
||||
* KoalaSync Shared Constants & Protocol Definitions
|
||||
*
|
||||
* ⚠️ WARNING: This is the SINGLE SOURCE OF TRUTH.
|
||||
* If you edit this file, you MUST run /scripts/sync-constants.bat
|
||||
* If you edit this file, you MUST run: node scripts/build-extension.js
|
||||
* to propagate changes to the extension and relay server.
|
||||
*/
|
||||
|
||||
export const PROTOCOL_VERSION = "1.0.0";
|
||||
export const APP_VERSION = "1.2.1";
|
||||
export const APP_VERSION = "1.3.1";
|
||||
|
||||
export const OFFICIAL_SERVER_URL = 'wss://sync.shik3i.net';
|
||||
export const OFFICIAL_LANDING_PAGE_URL = 'https://koalasync.shik3i.net';
|
||||
export const OFFICIAL_SERVER_URL = 'wss://syncserver.koalastuff.net';
|
||||
export const OFFICIAL_LANDING_PAGE_URL = 'https://sync.koalastuff.net';
|
||||
export const OFFICIAL_SERVER_TOKEN = '62170b705234c4f4807a9b22420bb93cf1a2aacfa4c5d3b47804482babb8eb50';
|
||||
|
||||
export const EVENTS = {
|
||||
|
||||
+28
-26
@@ -1,31 +1,39 @@
|
||||
# KoalaSync Landing Page
|
||||
# KoalaSync Website & Invitation Bridge
|
||||
|
||||
This directory contains the static marketing website for KoalaSync. It is built using vanilla HTML, CSS, and JavaScript to ensure maximum performance, zero tracking, and easy hosting.
|
||||
This directory contains the KoalaSync website. It serves a dual purpose: it is both the **marketing landing page** and the **technical bridge** for joining synchronized rooms.
|
||||
|
||||
## Features
|
||||
- **Privacy First**: No external fonts, scripts, or trackers.
|
||||
- **Modern Tech Aesthetic**: Pure CSS animated gradients and glassmorphism.
|
||||
- **Smart Join**: Integrated bridge for communication with the KoalaSync browser extension.
|
||||
## Core Roles
|
||||
|
||||
### 1. Marketing & Onboarding
|
||||
Provides a premium, bilingual (EN/DE) overview of features, setup instructions, and direct links to the extension stores.
|
||||
|
||||
### 2. The Invitation Bridge (`join.html`)
|
||||
The website handles incoming invitation links. When a user clicks a link like `sync.koalastuff.net/join.html#join:roomID:pass`, the website:
|
||||
- **Detects the Extension**: Verifies if KoalaSync is installed via the `bridge.js` content script.
|
||||
- **Privacy-First Handshake**: The room credentials (ID/Password) are stored in the **URL Hash (#)**. This ensures the sensitive credentials **never reach the web server** and are processed entirely within the user's browser.
|
||||
- **Auto-Join**: If the extension is detected, it automatically triggers the join flow without requiring user input.
|
||||
|
||||
## Architecture
|
||||
|
||||
The website is 100% **Static HTML, CSS, and JS**.
|
||||
- **Zero Backend**: No Node.js, PHP, or databases are required to host the website.
|
||||
- **Zero Tracking**: All assets (fonts, icons) are self-hosted to prevent third-party tracking.
|
||||
- **Responsive**: Fully optimized for mobile with a native-feel hamburger menu.
|
||||
|
||||
## Hosting with Caddy
|
||||
|
||||
Caddy is the recommended web server for KoalaSync due to its automatic HTTPS and simple configuration.
|
||||
Caddy is the recommended web server. It provides automatic HTTPS and high-performance static file serving.
|
||||
|
||||
### Example Caddyfile
|
||||
### Recommended Caddyfile
|
||||
|
||||
To host the website on `koalasync.shik3i.net`, you can use the following configuration:
|
||||
For a more comprehensive configuration that includes the Relay Server reverse proxy, see the root [Caddyfile.example](../Caddyfile.example).
|
||||
|
||||
```caddy
|
||||
koalasync.shik3i.net {
|
||||
# Path to the website directory
|
||||
sync.koalastuff.net {
|
||||
root * /var/www/koalasync/website
|
||||
|
||||
# Enable static file serving
|
||||
file_server
|
||||
|
||||
# Enable Gzip/Zstd compression
|
||||
encode zstd gzip
|
||||
|
||||
|
||||
# Security Headers
|
||||
header {
|
||||
# Prevent FLoC tracking
|
||||
@@ -36,16 +44,10 @@ koalasync.shik3i.net {
|
||||
X-Frame-Options DENY
|
||||
Referrer-Policy no-referrer-when-downgrade
|
||||
}
|
||||
|
||||
# Custom 404 page
|
||||
handle_errors {
|
||||
rewrite * /{err.status_code}.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deployment Steps
|
||||
1. Copy the contents of this folder to `/var/www/koalasync/website` on your server.
|
||||
2. Update the path in your `Caddyfile`.
|
||||
3. Reload Caddy: `caddy reload`.
|
||||
## Local Development
|
||||
|
||||
1. Open `index.html` directly in any browser.
|
||||
2. To test the invitation flow locally, use a local server (e.g., `npx serve .`) and navigate to `http://localhost:5000/join.html#join:test-room:test-pass`.
|
||||
|
||||
+14
-7
@@ -161,14 +161,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (success) {
|
||||
if (icon) icon.textContent = '✅';
|
||||
title.textContent = 'Erfolgreich!';
|
||||
const isDE = document.documentElement.classList.contains('lang-de');
|
||||
title.textContent = isDE ? 'Erfolgreich!' : 'Success!';
|
||||
|
||||
let count = 3;
|
||||
const updateCountdown = () => {
|
||||
desc.innerHTML = `Du bist dem Raum beigetreten. <br><span style="color:var(--accent); font-weight:bold;">Dieser Tab schließt sich in ${count} Sekunden...</span>`;
|
||||
const closingMsg = isDE
|
||||
? `Du bist dem Raum beigetreten. <br><span style="color:var(--accent); font-weight:bold;">Dieser Tab schließt sich in ${count} Sekunden...</span>`
|
||||
: `You joined the room. <br><span style="color:var(--accent); font-weight:bold;">This tab will close in ${count} seconds...</span>`;
|
||||
desc.innerHTML = closingMsg;
|
||||
if (count <= 0) {
|
||||
window.close();
|
||||
desc.textContent = 'Beitritt erfolgreich! Du kannst diesen Tab jetzt manuell schließen.';
|
||||
desc.textContent = isDE ? 'Beitritt erfolgreich! Du kannst diesen Tab jetzt manuell schließen.' : 'Joined successfully! You can close this tab manually.';
|
||||
} else {
|
||||
count--;
|
||||
setTimeout(updateCountdown, 1000);
|
||||
@@ -176,12 +180,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
updateCountdown();
|
||||
|
||||
actions.innerHTML = '<button class="primary" onclick="window.close()" style="background:var(--success); width: 100%;">TAB JETZT SCHLIESSEN</button>';
|
||||
const closeLabel = isDE ? 'TAB JETZT SCHLIESSEN' : 'CLOSE TAB NOW';
|
||||
actions.innerHTML = `<button class="primary" onclick="window.close()" style="background:var(--success); width: 100%;">${closeLabel}</button>`;
|
||||
} else {
|
||||
if (icon) icon.textContent = '❌';
|
||||
title.textContent = 'Fehler';
|
||||
desc.textContent = `Beitritt fehlgeschlagen: ${message}`;
|
||||
actions.innerHTML = '<button class="primary" onclick="location.reload()" style="width: 100%;">ERNEUT VERSUCHEN</button>';
|
||||
const isDE = document.documentElement.classList.contains('lang-de');
|
||||
title.textContent = isDE ? 'Fehler' : 'Error';
|
||||
desc.textContent = isDE ? `Beitritt fehlgeschlagen: ${message}` : `Join failed: ${message}`;
|
||||
const retryLabel = isDE ? 'ERNEUT VERSUCHEN' : 'TRY AGAIN';
|
||||
actions.innerHTML = `<button class="primary" onclick="location.reload()" style="width: 100%;">${retryLabel}</button>`;
|
||||
}
|
||||
} else {
|
||||
const banner = document.getElementById('koala-banner');
|
||||
|
||||
+97
-19
@@ -1,11 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Datenschutz | KoalaSync</title>
|
||||
<title>Datenschutz / Privacy Policy | KoalaSync</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" type="image/png" href="assets/logo.png">
|
||||
<meta name="robots" content="noindex">
|
||||
<script>
|
||||
(function() {
|
||||
var savedLang = localStorage.getItem('koala_lang');
|
||||
var browserLang = navigator.language.startsWith('de') ? 'de' : 'en';
|
||||
var activeLang = savedLang || browserLang;
|
||||
document.documentElement.classList.add('lang-' + activeLang);
|
||||
document.documentElement.lang = activeLang;
|
||||
})();
|
||||
|
||||
function toggleLanguage(e) {
|
||||
if(e) e.preventDefault();
|
||||
var html = document.documentElement;
|
||||
var currentIsEnglish = html.classList.contains('lang-en');
|
||||
var newLang = currentIsEnglish ? 'de' : 'en';
|
||||
html.classList.remove('lang-en', 'lang-de');
|
||||
html.classList.add('lang-' + newLang);
|
||||
html.lang = newLang;
|
||||
localStorage.setItem('koala_lang', newLang);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-blobs">
|
||||
@@ -20,54 +41,110 @@
|
||||
<img src="assets/logo.png" alt="KoalaSync Logo">
|
||||
<span>KoalaSync</span>
|
||||
</a>
|
||||
<button class="hamburger" onclick="document.querySelector('.nav-links').classList.toggle('open')" aria-label="Menu">☰</button>
|
||||
<div class="nav-links">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="index.html"><span lang="de">Startseite</span><span lang="en">Home</span></a>
|
||||
<a href="https://github.com/shik3i/KoalaSync" target="_blank">GitHub</a>
|
||||
<a class="lang-toggle" onclick="toggleLanguage(event)">🌍 EN/DE</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="legal-content">
|
||||
<div class="legal-card" data-reveal style="padding: 2rem;">
|
||||
<h1>Datenschutz</h1>
|
||||
<p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
|
||||
<h1 lang="de">Datenschutz</h1>
|
||||
<h1 lang="en">Privacy Policy</h1>
|
||||
<p lang="de" style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
|
||||
Sicherheit & Privatsphäre
|
||||
</p>
|
||||
<p lang="en" style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
|
||||
Security & Privacy
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h2>1. Hosting & Logfiles</h2>
|
||||
<p>
|
||||
<h2>
|
||||
<span lang="de">1. Hosting & Logfiles</span>
|
||||
<span lang="en">1. Hosting & Logfiles</span>
|
||||
</h2>
|
||||
<p lang="de">
|
||||
Diese Seite wird auf einem privaten Server gehostet. Zur Gewährleistung der Stabilität werden standardmäßige Server-Logs (IP, Browser, Zeit) erhoben, aber nicht mit Personen verknüpft und nach 7 Tagen automatisch gelöscht.
|
||||
</p>
|
||||
<p lang="en">
|
||||
This site is hosted on a private server. To ensure stability, standard server logs (IP, browser, time) are collected, but not linked to individuals and are automatically deleted after 7 days.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Keine Drittanbieter & Tracking</h2>
|
||||
<p>
|
||||
<h2>
|
||||
<span lang="de">2. Keine Drittanbieter & Open Source</span>
|
||||
<span lang="en">2. No Third Parties & Open Source</span>
|
||||
</h2>
|
||||
<p lang="de">
|
||||
KoalaSync verzichtet bewusst auf Analyse-Tools, Tracking-Cookies oder Werbenetzwerke. Wir laden keine Ressourcen von Drittanbietern (wie Google Fonts) nach, um Ihre Privatsphäre maximal zu schützen.
|
||||
</p>
|
||||
<p lang="de" style="margin-top: 0.5rem;">
|
||||
Da KoalaSync vollständig Open Source ist, kann zudem jede Zeile Code auf unserem <a href="https://github.com/shik3i/KoalaSync" target="_blank" style="color: var(--accent);">GitHub-Repository</a> öffentlich eingesehen und auf Sicherheit geprüft werden.
|
||||
</p>
|
||||
<p lang="en">
|
||||
KoalaSync deliberately avoids analytics tools, tracking cookies, or advertising networks. We do not load any third-party resources (such as Google Fonts) to maximize the protection of your privacy.
|
||||
</p>
|
||||
<p lang="en" style="margin-top: 0.5rem;">
|
||||
Since KoalaSync is 100% open-source, every single line of code can also be publicly viewed and audited for security on our <a href="https://github.com/shik3i/KoalaSync" target="_blank" style="color: var(--accent);">GitHub repository</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Relay-Server Architektur</h2>
|
||||
<p>
|
||||
<h2>
|
||||
<span lang="de">3. Relay-Server Architektur</span>
|
||||
<span lang="en">3. Relay Server Architecture</span>
|
||||
</h2>
|
||||
<p lang="de">
|
||||
Unser Relay-Server arbeitet ausschließlich im Arbeitsspeicher (RAM). Nachrichten zwischen Teilnehmern werden nicht auf Festplatten gespeichert und sind flüchtig. Sobald ein Raum geschlossen wird, werden alle zugehörigen Metadaten sofort gelöscht.
|
||||
</p>
|
||||
<p lang="en">
|
||||
Our relay server operates exclusively in memory (RAM). Messages between participants are not stored on hard drives and are volatile. As soon as a room is closed, all associated metadata is immediately deleted.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. Brute-Force Schutz</h2>
|
||||
<p>
|
||||
<h2>
|
||||
<span lang="de">4. Browser-Erweiterung (Extension)</span>
|
||||
<span lang="en">4. Browser Extension</span>
|
||||
</h2>
|
||||
<p lang="de">
|
||||
Um die geräteübergreifende Synchronisation zu ermöglichen, erfasst die KoalaSync Browser-Erweiterung temporär Daten des aktuell aktiven Video-Tabs (z. B. Tab-Titel, Medien-Metadaten wie den Videotitel sowie den Wiedergabestatus). Diese Daten werden ausschließlich zur Synchronisation an die anderen Teilnehmer in Ihrem Raum gesendet. Es wird ausdrücklich <strong>kein allgemeiner Browserverlauf (Browsing History)</strong> ausgelesen, gespeichert oder übermittelt.
|
||||
</p>
|
||||
<p lang="en">
|
||||
To enable cross-device synchronization, the KoalaSync browser extension temporarily captures data from the currently active video tab (e.g., tab title, media metadata like the video title, and playback state). This data is exclusively sent to other participants in your room for synchronization. We explicitly <strong>do not read, store, or transmit your general browsing history</strong>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
<span lang="de">5. Brute-Force Schutz</span>
|
||||
<span lang="en">5. Brute-Force Protection</span>
|
||||
</h2>
|
||||
<p lang="de">
|
||||
Zur Sicherheit unserer Nutzer speichern wir fehlgeschlagene Login-Versuche (IP-Adresse und Raum-ID) für maximal 15 Minuten im RAM, um automatisierte Angriffe zu verhindern. Diese Daten werden danach rückstandslos gelöscht.
|
||||
</p>
|
||||
<p lang="en">
|
||||
For the security of our users, we store failed login attempts (IP address and room ID) for a maximum of 15 minutes in RAM to prevent automated attacks. This data is deleted without a trace afterwards.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5. Ihre Rechte</h2>
|
||||
<p>
|
||||
<h2>
|
||||
<span lang="de">6. Ihre Rechte</span>
|
||||
<span lang="en">6. Your Rights</span>
|
||||
</h2>
|
||||
<p lang="de">
|
||||
Sie haben das Recht auf Auskunft, Berichtigung oder Löschung Ihrer Daten. Da wir jedoch keine personenbezogenen Daten dauerhaft speichern, ist eine Zuordnung zu Ihrer Person in der Regel technisch nicht möglich.
|
||||
</p>
|
||||
<p>Kontakt bei Fragen: <span style="color: var(--accent); cursor: pointer; text-decoration: underline;" onclick="this.innerHTML='koalasync_datenschutz' + '@' + 'koalamail.rocks'">[E-Mail anzeigen]</span></p>
|
||||
<p lang="en">
|
||||
You have the right to information, correction, or deletion of your data. However, since we do not store any personal data permanently, linking data to your person is technically impossible in most cases.
|
||||
</p>
|
||||
<p lang="de">Kontakt bei Fragen: <span style="color: var(--accent); cursor: pointer; text-decoration: underline;" onclick="this.innerHTML='koalasync_datenschutz' + '@' + 'koalamail.rocks'">[E-Mail anzeigen]</span></p>
|
||||
<p lang="en">Contact for questions: <span style="color: var(--accent); cursor: pointer; text-decoration: underline;" onclick="this.innerHTML='koalasync_datenschutz' + '@' + 'koalamail.rocks'">[Show Email]</span></p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
@@ -75,10 +152,11 @@
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2026 KoalaSync. Open source under the MIT License.</p>
|
||||
<p style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
|
||||
<p lang="de" style="font-size: 0.8rem; margin-top: 0.5rem;">Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.</p>
|
||||
<p lang="en" style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; gap: 1.5rem;">
|
||||
<a href="impressum.html" style="color: var(--text-muted); text-decoration: none;">Impressum</a>
|
||||
<a href="datenschutz.html" style="color: var(--text-muted); text-decoration: none;">Datenschutz</a>
|
||||
<a href="impressum.html" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Impressum</span><span lang="en">Legal Notice</span></a>
|
||||
<a href="datenschutz.html" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Datenschutz</span><span lang="en">Privacy Policy</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
+84
-24
@@ -1,11 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Impressum | KoalaSync</title>
|
||||
<title>Impressum / Legal Notice | KoalaSync</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" type="image/png" href="assets/logo.png">
|
||||
<meta name="robots" content="noindex">
|
||||
<script>
|
||||
(function() {
|
||||
var savedLang = localStorage.getItem('koala_lang');
|
||||
var browserLang = navigator.language.startsWith('de') ? 'de' : 'en';
|
||||
var activeLang = savedLang || browserLang;
|
||||
document.documentElement.classList.add('lang-' + activeLang);
|
||||
document.documentElement.lang = activeLang;
|
||||
})();
|
||||
|
||||
function toggleLanguage(e) {
|
||||
if(e) e.preventDefault();
|
||||
var html = document.documentElement;
|
||||
var currentIsEnglish = html.classList.contains('lang-en');
|
||||
var newLang = currentIsEnglish ? 'de' : 'en';
|
||||
html.classList.remove('lang-en', 'lang-de');
|
||||
html.classList.add('lang-' + newLang);
|
||||
html.lang = newLang;
|
||||
localStorage.setItem('koala_lang', newLang);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-blobs">
|
||||
@@ -20,57 +41,95 @@
|
||||
<img src="assets/logo.png" alt="KoalaSync Logo">
|
||||
<span>KoalaSync</span>
|
||||
</a>
|
||||
<button class="hamburger" onclick="document.querySelector('.nav-links').classList.toggle('open')" aria-label="Menu">☰</button>
|
||||
<div class="nav-links">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="index.html"><span lang="de">Startseite</span><span lang="en">Home</span></a>
|
||||
<a href="https://github.com/shik3i/KoalaSync" target="_blank">GitHub</a>
|
||||
<a class="lang-toggle" onclick="toggleLanguage(event)">🌍 EN/DE</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="legal-content">
|
||||
<div class="legal-card" data-reveal style="padding: 2rem;">
|
||||
<h1>Impressum</h1>
|
||||
<p style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
|
||||
<h1 lang="de">Impressum</h1>
|
||||
<h1 lang="en">Legal Notice</h1>
|
||||
<p lang="de" style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
|
||||
Transparenz & Identifikation
|
||||
</p>
|
||||
<p lang="en" style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
|
||||
Transparency & Identification
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h2>Betreiber & Kontakt</h2>
|
||||
<p>Administrator KoalaSync (Privatperson)</p>
|
||||
<p>E-Mail: <span style="color: var(--accent); cursor: pointer; text-decoration: underline;" onclick="this.innerHTML='koalasync_admin' + '@' + 'koalamail.rocks'">[E-Mail anzeigen]</span></p>
|
||||
<h2>
|
||||
<span lang="de">Betreiber & Kontakt</span>
|
||||
<span lang="en">Operator & Contact</span>
|
||||
</h2>
|
||||
<p lang="de">Administrator KoalaSync (Privatperson)</p>
|
||||
<p lang="en">Administrator KoalaSync (Private Individual)</p>
|
||||
<p lang="de">E-Mail: <span style="color: var(--accent); cursor: pointer; text-decoration: underline;" onclick="this.innerHTML='koalasync_admin' + '@' + 'koalamail.rocks'">[E-Mail anzeigen]</span></p>
|
||||
<p lang="en">E-Mail: <span style="color: var(--accent); cursor: pointer; text-decoration: underline;" onclick="this.innerHTML='koalasync_admin' + '@' + 'koalamail.rocks'">[Show Email]</span></p>
|
||||
</section>
|
||||
|
||||
<section style="opacity: 0.8;">
|
||||
<h2>Privatprojekt-Hinweis</h2>
|
||||
<p>
|
||||
<h2>
|
||||
<span lang="de">Privatprojekt-Hinweis</span>
|
||||
<span lang="en">Private Project Notice</span>
|
||||
</h2>
|
||||
<p lang="de">
|
||||
Diese Website ist ein rein privates Hobby-Projekt und dient keinen geschäftsmäßigen Zwecken.
|
||||
Eine Impressumspflicht nach § 5 DDG (ehemals TMG) besteht daher nicht.
|
||||
Diese Angaben erfolgen rein freiwillig zur Transparenz gegenüber der Community.
|
||||
</p>
|
||||
<p lang="en">
|
||||
This website is a purely private hobby project and does not serve any commercial purposes.
|
||||
Therefore, there is no obligation to provide a legal notice according to § 5 DDG (formerly TMG).
|
||||
This information is provided voluntarily for transparency towards the community.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Haftung für Inhalte</h2>
|
||||
<p>
|
||||
<h2>
|
||||
<span lang="de">Haftung für Inhalte</span>
|
||||
<span lang="en">Liability for Content</span>
|
||||
</h2>
|
||||
<p lang="de">
|
||||
Gemäß § 7 Abs.1 DDG sind wir für eigene Inhalte verantwortlich. Nach §§ 8 bis 10 DDG sind wir jedoch nicht verpflichtet,
|
||||
übermittelte oder gespeicherte fremde Informationen zu überwachen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Haftung für Links</h2>
|
||||
<p>
|
||||
Unser Angebot enthält Links zu externen Websites Dritter. Auf deren Inhalte haben wir keinen Einfluss und
|
||||
können daher keine Gewähr für diese fremden Inhalte übernehmen.
|
||||
<p lang="en">
|
||||
According to § 7 Abs.1 DDG we are responsible for our own content. According to §§ 8 to 10 DDG, however, we are not obligated to monitor transmitted or stored third-party information.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Urheberrecht</h2>
|
||||
<p>
|
||||
<h2>
|
||||
<span lang="de">Haftung für Links</span>
|
||||
<span lang="en">Liability for Links</span>
|
||||
</h2>
|
||||
<p lang="de">
|
||||
Unser Angebot enthält Links zu externen Websites Dritter. Auf deren Inhalte haben wir keinen Einfluss und
|
||||
können daher keine Gewähr für diese fremden Inhalte übernehmen.
|
||||
</p>
|
||||
<p lang="en">
|
||||
Our offer contains links to external third-party websites. We have no influence on their content and therefore cannot assume any liability for these external contents.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
<span lang="de">Urheberrecht</span>
|
||||
<span lang="en">Copyright</span>
|
||||
</h2>
|
||||
<p lang="de">
|
||||
Die durch die Seitenbetreiber erstellten Inhalte auf diesen Seiten unterliegen dem deutschen Urheberrecht.
|
||||
Vervielfältigung, Bearbeitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung.
|
||||
</p>
|
||||
<p lang="en">
|
||||
The content and works created by the site operators on these pages are subject to German copyright law.
|
||||
Duplication, processing, and any kind of exploitation outside the limits of copyright require written consent.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
@@ -78,10 +137,11 @@
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2026 KoalaSync. Open source under the MIT License.</p>
|
||||
<p style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
|
||||
<p lang="de" style="font-size: 0.8rem; margin-top: 0.5rem;">Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.</p>
|
||||
<p lang="en" style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; gap: 1.5rem;">
|
||||
<a href="impressum.html" style="color: var(--text-muted); text-decoration: none;">Impressum</a>
|
||||
<a href="datenschutz.html" style="color: var(--text-muted); text-decoration: none;">Datenschutz</a>
|
||||
<a href="impressum.html" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Impressum</span><span lang="en">Legal Notice</span></a>
|
||||
<a href="datenschutz.html" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Datenschutz</span><span lang="en">Privacy Policy</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
+85
-30
@@ -10,8 +10,33 @@
|
||||
<link rel="icon" type="image/png" href="assets/logo.png">
|
||||
<meta property="og:title" content="KoalaSync | Sync your videos">
|
||||
<meta property="og:description" content="Watch together, stay in sync. Privacy-first video synchronization.">
|
||||
<meta property="og:image" content="assets/hero.png">
|
||||
<meta property="og:image" content="https://sync.koalastuff.net/assets/logo.png">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var savedLang = localStorage.getItem('koala_lang');
|
||||
var browserLang = navigator.language.startsWith('de') ? 'de' : 'en';
|
||||
var activeLang = savedLang || browserLang;
|
||||
document.documentElement.classList.add('lang-' + activeLang);
|
||||
document.documentElement.lang = activeLang;
|
||||
var titles = { en: 'KoalaSync | Real-time Video Synchronization for Friends', de: 'KoalaSync | Echtzeit-Video-Synchronisation für Freunde' };
|
||||
document.title = titles[activeLang] || titles.en;
|
||||
})();
|
||||
|
||||
function toggleLanguage(e) {
|
||||
if(e) e.preventDefault();
|
||||
var html = document.documentElement;
|
||||
var currentIsEnglish = html.classList.contains('lang-en');
|
||||
var newLang = currentIsEnglish ? 'de' : 'en';
|
||||
html.classList.remove('lang-en', 'lang-de');
|
||||
html.classList.add('lang-' + newLang);
|
||||
html.lang = newLang;
|
||||
localStorage.setItem('koala_lang', newLang);
|
||||
var titles = { en: 'KoalaSync | Real-time Video Synchronization for Friends', de: 'KoalaSync | Echtzeit-Video-Synchronisation für Freunde' };
|
||||
document.title = titles[newLang] || titles.en;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -27,10 +52,12 @@
|
||||
<img src="assets/logo.png" alt="KoalaSync Logo">
|
||||
<span>KoalaSync</span>
|
||||
</div>
|
||||
<button class="hamburger" onclick="document.querySelector('.nav-links').classList.toggle('open')" aria-label="Menu">☰</button>
|
||||
<div class="nav-links">
|
||||
<a href="#features">Features</a>
|
||||
<a href="#how-it-works">How it works</a>
|
||||
<a href="#features"><span lang="en">Features</span><span lang="de">Funktionen</span></a>
|
||||
<a href="#how-it-works"><span lang="en">How it works</span><span lang="de">So funktioniert's</span></a>
|
||||
<a href="https://github.com/Shik3i/KoalaSync" target="_blank">GitHub</a>
|
||||
<a class="lang-toggle" onclick="toggleLanguage(event)">🌍 EN/DE</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -38,13 +65,23 @@
|
||||
<header class="hero">
|
||||
<div class="container hero-grid">
|
||||
<div class="hero-text">
|
||||
<div class="version-badge" data-reveal>v1.2.0 OUT NOW</div>
|
||||
<h1 data-reveal>Watch Together.<br>Sync Perfectly.</h1>
|
||||
<p data-reveal>KoalaSync brings friends closer through synchronized video playback. No lag, no tracking, just shared moments.</p>
|
||||
<div class="version-badge" data-reveal><span lang="en">v1.3.1 OUT NOW</span><span lang="de">v1.3.1 JETZT VERFÜGBAR</span></div>
|
||||
<h1 data-reveal>
|
||||
<span lang="en">Watch Together.<br>Sync Perfectly.</span>
|
||||
<span lang="de">Gemeinsam schauen.<br>Perfekt synchron.</span>
|
||||
</h1>
|
||||
<h2 class="hero-subtitle" data-reveal>
|
||||
<span lang="en">The ultimate free watch party extension for YouTube, Twitch, and local MP4s.</span>
|
||||
<span lang="de">Die ultimative, kostenlose Watch-Party-Erweiterung für YouTube, Twitch und lokale MP4s.</span>
|
||||
</h2>
|
||||
<div class="cta-group" data-reveal>
|
||||
<a href="#" class="btn btn-primary">
|
||||
<img src="assets/chrome.svg" width="20" style="filter: brightness(100)">
|
||||
Add to Chrome
|
||||
<span lang="en">Add to Chrome</span><span lang="de">Zu Chrome hinzufügen</span>
|
||||
</a>
|
||||
<a href="#" class="btn btn-primary" style="background: #e66000;">
|
||||
🦊
|
||||
<span lang="en">Add to Firefox</span><span lang="de">Zu Firefox hinzufügen</span>
|
||||
</a>
|
||||
<a href="https://github.com/Shik3i/KoalaSync" class="btn btn-secondary">
|
||||
GitHub Repo
|
||||
@@ -56,29 +93,37 @@
|
||||
|
||||
<section id="features">
|
||||
<div class="container">
|
||||
<h2 style="font-size: 2.5rem; text-align: center; margin-bottom: 1rem;">Why KoalaSync?</h2>
|
||||
<p style="text-align: center; color: var(--text-muted); margin-bottom: 4rem;">Built for performance, privacy, and simplicity.</p>
|
||||
<h2 style="font-size: 2.5rem; text-align: center; margin-bottom: 1rem;">
|
||||
<span lang="en">Why KoalaSync?</span><span lang="de">Warum KoalaSync?</span>
|
||||
</h2>
|
||||
<p style="text-align: center; color: var(--text-muted); margin-bottom: 4rem;">
|
||||
<span lang="en">Built for performance, privacy, and simplicity.</span><span lang="de">Entwickelt für Leistung, Privatsphäre und Einfachheit.</span>
|
||||
</p>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-card" data-reveal>
|
||||
<div class="feature-icon">⚡</div>
|
||||
<h3>Real-time Sync</h3>
|
||||
<p>Proprietary two-phase synchronization protocol ensures sub-millisecond precision across all peers.</p>
|
||||
<h3><span lang="en">Real-time Sync</span><span lang="de">Echtzeit-Synchronisation</span></h3>
|
||||
<p lang="en">Proprietary two-phase synchronization protocol ensures sub-millisecond precision across all peers.</p>
|
||||
<p lang="de">Das proprietäre Zwei-Phasen-Synchronisationsprotokoll gewährleistet Sub-Millisekunden-Präzision für alle Teilnehmer.</p>
|
||||
</div>
|
||||
<div class="feature-card" data-reveal>
|
||||
<div class="feature-icon">🎬</div>
|
||||
<h3>Episode Auto-Sync</h3>
|
||||
<p>New in v1.2.0: Perfectly sync series binges. All peers wait until everyone has loaded the next episode.</p>
|
||||
<h3><span lang="en">Episode Auto-Sync</span><span lang="de">Automatische Episoden-Synchronisation</span></h3>
|
||||
<p lang="en">New in v1.3.1: Perfectly sync series binges. All peers wait until everyone has loaded the next episode.</p>
|
||||
<p lang="de">Neu in v1.3.1: Perfekt synchronisiertes Serien-Bingeing. Alle warten, bis jeder die nächste Episode geladen hat.</p>
|
||||
</div>
|
||||
<div class="feature-card" data-reveal>
|
||||
<div class="feature-icon">🛡️</div>
|
||||
<h3>Privacy First</h3>
|
||||
<p>Zero data persistence. Our relay server runs entirely in RAM and collects no telemetry or logs.</p>
|
||||
<h3><span lang="en">Privacy First</span><span lang="de">Privatsphäre zuerst</span></h3>
|
||||
<p lang="en">Zero data persistence. Our relay server runs entirely in RAM and collects no telemetry or logs.</p>
|
||||
<p lang="de">Keine Datenspeicherung. Unser Relay-Server läuft komplett im RAM und sammelt weder Telemetrie noch Logs.</p>
|
||||
</div>
|
||||
<div class="feature-card" data-reveal>
|
||||
<div class="feature-icon">⭐</div>
|
||||
<h3>Smart Match</h3>
|
||||
<p>Find the right tab instantly. KoalaSync highlights and sorts matching video tabs for you.</p>
|
||||
<h3><span lang="en">Smart Match</span><span lang="de">Intelligente Zuordnung</span></h3>
|
||||
<p lang="en">Find the right tab instantly. KoalaSync highlights and sorts matching video tabs for you.</p>
|
||||
<p lang="de">Finden Sie sofort den richtigen Tab. KoalaSync markiert und sortiert passende Video-Tabs für Sie.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,30 +132,35 @@
|
||||
|
||||
<section id="how-it-works" style="background: rgba(255,255,255,0.02)">
|
||||
<div class="container">
|
||||
<h2 style="font-size: 2.5rem; text-align: center; margin-bottom: 4rem;">Getting Started</h2>
|
||||
<h2 style="font-size: 2.5rem; text-align: center; margin-bottom: 4rem;">
|
||||
<span lang="en">Getting Started</span><span lang="de">Loslegen</span>
|
||||
</h2>
|
||||
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<div class="step-text" data-reveal>
|
||||
<div class="step-num">01</div>
|
||||
<h3>Install Extension</h3>
|
||||
<p>Add KoalaSync to your browser from the Chrome Web Store or download the latest developer ZIP from GitHub.</p>
|
||||
<h3><span lang="en">Install Extension</span><span lang="de">Erweiterung installieren</span></h3>
|
||||
<p lang="en">Add KoalaSync to your browser from the Chrome Web Store or download the latest developer ZIP from GitHub.</p>
|
||||
<p lang="de">Fügen Sie KoalaSync aus dem Chrome Web Store zu Ihrem Browser hinzu oder laden Sie die neueste Entwickler-ZIP von GitHub herunter.</p>
|
||||
</div>
|
||||
<div style="background: var(--card); height: 200px; border-radius: 20px; border: 1px solid var(--glass-border);"></div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-text" data-reveal>
|
||||
<div class="step-num">02</div>
|
||||
<h3>Create a Room</h3>
|
||||
<p>Open the extension, enter a Room ID and Password, and hit Join. Your secure synchronization space is ready.</p>
|
||||
<h3><span lang="en">Create a Room</span><span lang="de">Raum erstellen</span></h3>
|
||||
<p lang="en">Open the extension, enter a Room ID and Password, and hit Join. Your secure synchronization space is ready.</p>
|
||||
<p lang="de">Öffnen Sie die Erweiterung, geben Sie eine Raum-ID und ein Passwort ein und klicken Sie auf Beitreten. Ihr sicherer Synchronisationsraum ist bereit.</p>
|
||||
</div>
|
||||
<div style="background: var(--card); height: 200px; border-radius: 20px; border: 1px solid var(--glass-border);"></div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-text" data-reveal>
|
||||
<div class="step-num">03</div>
|
||||
<h3>Share & Sync</h3>
|
||||
<p>Send the invite link to your friends. Once they join, select your video tab and enjoy perfectly synced playback.</p>
|
||||
<h3><span lang="en">Share & Sync</span><span lang="de">Teilen & Synchronisieren</span></h3>
|
||||
<p lang="en">Send the invite link to your friends. Once they join, select your video tab and enjoy perfectly synced playback.</p>
|
||||
<p lang="de">Senden Sie den Einladungslink an Ihre Freunde. Sobald sie beitreten, wählen Sie Ihren Video-Tab aus und genießen Sie die perfekt synchronisierte Wiedergabe.</p>
|
||||
</div>
|
||||
<div style="background: var(--card); height: 200px; border-radius: 20px; border: 1px solid var(--glass-border);"></div>
|
||||
</div>
|
||||
@@ -120,19 +170,24 @@
|
||||
|
||||
<section style="text-align: center;">
|
||||
<div class="container">
|
||||
<h2 data-reveal>Ready to sync?</h2>
|
||||
<p data-reveal style="margin-bottom: 2rem; color: var(--text-muted);">Join thousands of users watching together.</p>
|
||||
<a href="https://github.com/Shik3i/KoalaSync" class="btn btn-primary" data-reveal>View on GitHub</a>
|
||||
<h2 data-reveal><span lang="en">Ready to sync?</span><span lang="de">Bereit zum Synchronisieren?</span></h2>
|
||||
<p data-reveal style="margin-bottom: 2rem; color: var(--text-muted);">
|
||||
<span lang="en">Join thousands of users watching together.</span><span lang="de">Schließen Sie sich Tausenden von Nutzern an, die bereits zusammen schauen.</span>
|
||||
</p>
|
||||
<a href="https://github.com/Shik3i/KoalaSync" class="btn btn-primary" data-reveal>
|
||||
<span lang="en">View on GitHub</span><span lang="de">Auf GitHub ansehen</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2026 KoalaSync. Open source under the MIT License.</p>
|
||||
<p style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
|
||||
<p lang="en" style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
|
||||
<p lang="de" style="font-size: 0.8rem; margin-top: 0.5rem;">Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.</p>
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; gap: 1.5rem;">
|
||||
<a href="impressum.html" style="color: var(--text-muted); text-decoration: none;">Impressum</a>
|
||||
<a href="datenschutz.html" style="color: var(--text-muted); text-decoration: none;">Datenschutz</a>
|
||||
<a href="impressum.html" style="color: var(--text-muted); text-decoration: none;"><span lang="en">Legal Notice</span><span lang="de">Impressum</span></a>
|
||||
<a href="datenschutz.html" style="color: var(--text-muted); text-decoration: none;"><span lang="en">Privacy Policy</span><span lang="de">Datenschutz</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
+34
-6
@@ -1,11 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Beitreten | KoalaSync</title>
|
||||
<title>Join Room | KoalaSync</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" type="image/png" href="assets/logo.png">
|
||||
<meta name="robots" content="noindex">
|
||||
<script>
|
||||
(function() {
|
||||
var savedLang = localStorage.getItem('koala_lang');
|
||||
var browserLang = navigator.language.startsWith('de') ? 'de' : 'en';
|
||||
var activeLang = savedLang || browserLang;
|
||||
document.documentElement.classList.add('lang-' + activeLang);
|
||||
document.documentElement.lang = activeLang;
|
||||
var titles = { en: 'Join Room | KoalaSync', de: 'Raum beitreten | KoalaSync' };
|
||||
document.title = titles[activeLang] || titles.en;
|
||||
})();
|
||||
|
||||
function toggleLanguage(e) {
|
||||
if(e) e.preventDefault();
|
||||
var html = document.documentElement;
|
||||
var currentIsEnglish = html.classList.contains('lang-en');
|
||||
var newLang = currentIsEnglish ? 'de' : 'en';
|
||||
html.classList.remove('lang-en', 'lang-de');
|
||||
html.classList.add('lang-' + newLang);
|
||||
html.lang = newLang;
|
||||
localStorage.setItem('koala_lang', newLang);
|
||||
var titles = { en: 'Join Room | KoalaSync', de: 'Raum beitreten | KoalaSync' };
|
||||
document.title = titles[newLang] || titles.en;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-blobs">
|
||||
@@ -20,19 +45,22 @@
|
||||
<img src="assets/logo.png" alt="KoalaSync Logo">
|
||||
<span>KoalaSync</span>
|
||||
</a>
|
||||
<button class="hamburger" onclick="document.querySelector('.nav-links').classList.toggle('open')" aria-label="Menu">☰</button>
|
||||
<div class="nav-links">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="https://github.com/shik3i/KoalaSync" target="_blank">GitHub</a>
|
||||
<a class="lang-toggle" onclick="toggleLanguage(event)">🌍 EN/DE</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="legal-content join-card">
|
||||
<div class="legal-card" id="join-container" data-reveal style="padding: 2.5rem;">
|
||||
<div class="room-badge">INVITATION DETECTED</div>
|
||||
<h1 id="join-title" style="font-size: 2rem; margin-bottom: 1rem;">Ready to sync?</h1>
|
||||
<div class="room-badge"><span lang="en">INVITATION DETECTED</span><span lang="de">EINLADUNG ERKANNT</span></div>
|
||||
<h1 id="join-title" style="font-size: 2rem; margin-bottom: 1rem;"><span lang="en">Ready to sync?</span><span lang="de">Bereit zum Synchronisieren?</span></h1>
|
||||
<p id="join-desc" style="text-align: center; color: var(--text-muted); margin-bottom: 2rem; font-size: 0.9rem;">
|
||||
You've been invited to join a synchronized session.
|
||||
<span lang="en">You've been invited to join a synchronized session.</span>
|
||||
<span lang="de">Du wurdest eingeladen, einer synchronisierten Sitzung beizutreten.</span>
|
||||
</p>
|
||||
|
||||
<div id="room-info-box" style="background: rgba(255,255,255,0.03); padding: 2rem; border-radius: 20px; margin-bottom: 2rem; border: 1px solid var(--glass-border); text-align: center; position: relative; overflow: hidden;">
|
||||
@@ -41,7 +69,7 @@
|
||||
</div>
|
||||
|
||||
<div id="join-actions" style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<div style="text-align: center; color: var(--text-muted); font-size: 0.8rem;">Detecting extension...</div>
|
||||
<div style="text-align: center; color: var(--text-muted); font-size: 0.8rem;"><span lang="en">Detecting extension...</span><span lang="de">Erweiterung wird erkannt...</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://sync.koalastuff.net/sitemap.xml
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://sync.koalastuff.net/</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://sync.koalastuff.net/impressum.html</loc>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://sync.koalastuff.net/datenschutz.html</loc>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
+51
-1
@@ -176,13 +176,14 @@ nav {
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.hero-text p {
|
||||
.hero-text p, .hero-subtitle {
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 3rem;
|
||||
max-width: 700px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.cta-group {
|
||||
@@ -398,11 +399,60 @@ footer {
|
||||
}
|
||||
.cta-group {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.nav-links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 1rem 2rem;
|
||||
gap: 1rem;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
.nav-links.open {
|
||||
display: flex;
|
||||
}
|
||||
.hamburger {
|
||||
display: flex;
|
||||
}
|
||||
.legal-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Language Toggle --- */
|
||||
html.lang-en [lang="de"],
|
||||
html.lang-de [lang="en"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.lang-toggle {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
color: var(--text-muted);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.lang-toggle:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* --- Hamburger Menu --- */
|
||||
.hamburger {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user