5 Commits

Author SHA1 Message Date
Fredrik Johansson
1c73f1b1ef ci: bump Node to 24 to match local lockfile; add history gossip proposal
Some checks failed
Build / Build & release (push) Failing after 7m6s
package-lock.json was generated with npm 11 (Node 24). CI was running
Node 20 which resolves @emnapi deps differently, causing npm ci to fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 22:53:35 +02:00
Fredrik Johansson
b2b5c8c7cb ci: fix webkit dep for Ubuntu 24.04 (Noble)
Some checks failed
Build / Build & release (push) Failing after 7m5s
libwebkit2gtk-4.0-dev was dropped in Noble; use 4.1 and pass
-tags webkit2_41 to wails build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 22:28:04 +02:00
Fredrik Johansson
b6ff30de78 ci: rewrite workflow as single job to avoid upload-artifact
Some checks failed
Build / Build & release (push) Failing after 5m15s
Gitea act_runner intercepts actions/upload-artifact regardless of version
tag and uses an incompatible built-in. Restructured as one job that builds
all server binaries (cross-compiled) and the Linux desktop app, then
publishes directly to the release — no artifact handoff needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 22:02:43 +02:00
Fredrik Johansson
1bd719fa58 ci: downgrade artifact actions to v3 for Gitea GHES compatibility
upload-artifact@v4 and download-artifact@v4 are not supported on GHES.
Also drop merge-multiple (v4-only) and adjust release glob to artifacts/*/*.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 21:53:07 +02:00
Fredrik Johansson
be297d3a49 docs: add QUICKSTART.md for non-technical users
Three paths: browser-only, desktop app download, headless daemon.
Covers invites, anchor server basics, and mobile install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 21:46:37 +02:00
3 changed files with 276 additions and 72 deletions

View File

@@ -7,55 +7,8 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
# ── Server binaries (no CGo, cross-compile freely) ─────────────────────────── build:
name: Build & release
server:
name: Server binaries
runs-on: ubuntu-latest
strategy:
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
- goos: windows
goarch: amd64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build daemon + anchor
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: "0"
run: |
SUFFIX="${{ matrix.goos }}-${{ matrix.goarch }}"
[ "${{ matrix.goos }}" = "windows" ] && EXT=".exe" || EXT=""
go build -trimpath -ldflags="-s -w" -o "dist/waste-daemon-${SUFFIX}${EXT}" ./cmd/daemon
go build -trimpath -ldflags="-s -w" -o "dist/waste-anchor-${SUFFIX}${EXT}" ./cmd/anchor
- uses: actions/upload-artifact@v4
with:
name: server-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/
# ── Desktop app (Wails, requires CGo + webview libs) ─────────────────────────
# Runs only on Linux amd64 with the default runner.
# For macOS/Windows desktop builds, add self-hosted runners with those platforms
# and duplicate this job (adjusting the runs-on and platform deps).
desktop-linux:
name: Desktop app (Linux amd64)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -67,7 +20,7 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '24'
- name: Install Wails CLI - name: Install Wails CLI
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
@@ -77,9 +30,34 @@ jobs:
sudo apt-get update -q sudo apt-get update -q
sudo apt-get install -y \ sudo apt-get install -y \
libgtk-3-dev \ libgtk-3-dev \
libwebkit2gtk-4.0-dev \ libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev libayatana-appindicator3-dev
# ── Server binaries (CGO_ENABLED=0, cross-compile freely) ──────────────
- name: Build server binaries
run: |
mkdir -p dist
build() {
local GOOS=$1 GOARCH=$2
local SUFFIX="${GOOS}-${GOARCH}"
local EXT=""
[ "$GOOS" = "windows" ] && EXT=".exe"
CGO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH \
go build -trimpath -ldflags="-s -w" \
-o "dist/waste-daemon-${SUFFIX}${EXT}" ./cmd/daemon
CGO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH \
go build -trimpath -ldflags="-s -w" \
-o "dist/waste-anchor-${SUFFIX}${EXT}" ./cmd/anchor
}
build linux amd64
build linux arm64
build darwin amd64
build darwin arm64
build windows amd64
# ── Desktop app (Linux amd64, CGo + Wails) ─────────────────────────────
- name: Build frontend - name: Build frontend
run: | run: |
cd web cd web
@@ -89,32 +67,15 @@ jobs:
- name: Build desktop app - name: Build desktop app
run: | run: |
mkdir -p dist
cd cmd/app cd cmd/app
wails build -trimpath -ldflags="-s -w" -o ../../dist/waste-linux-amd64 wails build -trimpath -ldflags="-s -w" -tags webkit2_41 -o ../../dist/waste-linux-amd64
- uses: actions/upload-artifact@v4 # ── Publish release (tags only) ─────────────────────────────────────────
with:
name: desktop-linux-amd64
path: dist/waste-linux-amd64
# ── Release: collect all artifacts and publish ────────────────────────────────
release:
name: Publish release
needs: [server, desktop-linux]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts/
merge-multiple: true
- name: Create release - name: Create release
if: startsWith(github.ref, 'refs/tags/')
uses: https://gitea.com/actions/gitea-release-action@main uses: https://gitea.com/actions/gitea-release-action@main
with: with:
token: ${{ secrets.RELEASE_TOKEN }} token: ${{ secrets.RELEASE_TOKEN }}
files: artifacts/* files: dist/*
prerelease: ${{ contains(github.ref_name, '-') }} prerelease: ${{ contains(github.ref_name, '-') }}

154
PROPOSAL-history-gossip.md Normal file
View File

@@ -0,0 +1,154 @@
# Proposal: P2P Message History Gossip
## Goals
- New peers joining a network can retrieve recent message history from existing peers
- No central storage — history lives only in peer daemons (SQLite)
- No new trust requirements — history is shared only over already-established encrypted DataChannels
- No duplicates in the local store or UI
- No conflicts — history gossip is append-only and idempotent
---
## Privacy Model
History is shared **peer-to-peer over the encrypted mesh**, never via the anchor. The anchor remains dumb — it sees only signaling blobs. A peer only receives history from peers they have successfully completed a YAW/2 handshake with, so the same trust boundary as live messages applies.
A peer can choose not to share history by ignoring `history_request` messages — the protocol is advisory, not mandatory.
---
## Wire Protocol
Two new YAW/2 extension messages (added to EXTENSIONS.md):
### `history_request`
Sent by a newly-connected peer to one or more existing peers shortly after the handshake completes.
```json
{
"type": "history_request",
"room": "general",
"since": "2026-01-01T00:00:00Z",
"limit": 200
}
```
| Field | Type | Description |
|---------|-----------------|----------------------------------------------------------|
| `room` | string | Room name to request history for. One request per room. |
| `since` | ISO 8601 or "" | Only return messages newer than this timestamp. Empty = return up to `limit` most recent. |
| `limit` | int (max 500) | Maximum number of messages to return. Responder may return fewer. |
### `history_chunk`
Response from an existing peer. May be sent in multiple chunks if `limit` is large.
```json
{
"type": "history_chunk",
"room": "general",
"messages": [
{
"id": "sha256:<hex>",
"from": "<peer-alias>",
"from_id": "<hex-pubkey>",
"body": "hello",
"ts": "2026-06-01T12:00:00Z",
"room": "general"
}
],
"done": true
}
```
| Field | Type | Description |
|------------|---------|----------------------------------------------------------|
| `messages` | array | Ordered oldest-first. |
| `done` | bool | `true` on the final chunk. Receiver may display after this. |
---
## Message Identity and Deduplication
Each message has a **content-addressed ID**:
```
id = "sha256:" + hex(SHA-256(from_id || room || ts || body))
```
- Computed by the original sender and included in every live message going forward
- The SQLite `messages` table gains an `id TEXT UNIQUE` column
- On insert, use `INSERT OR IGNORE` — receiving the same message twice (live or via gossip) is a no-op
- The UI sorts by `ts`, so late-arriving history slots in correctly without reordering visible messages
Legacy messages (before this feature) have no `id`. They are assigned a local-only ID on migration and are never gossipped (they have no canonical ID the receiver could deduplicate against).
---
## Daemon-Side Implementation
### SQLite schema change
```sql
ALTER TABLE messages ADD COLUMN msg_id TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_msg_id ON messages(msg_id) WHERE msg_id IS NOT NULL;
```
### Handling `history_request`
```
peer sends history_request{room, since, limit}
→ query SQLite: SELECT * FROM messages WHERE room=? AND ts>? ORDER BY ts ASC LIMIT ?
→ send history_chunk{room, messages, done:true}
```
Responder enforces:
- `limit` capped at 500
- Only messages the responder itself received or sent (no re-gossipping of gossipped history to avoid amplification)
- Rate limit: one `history_request` per peer per room per 60 seconds
### Handling `history_chunk`
```
for each message in chunk:
INSERT OR IGNORE INTO messages (msg_id, room, from_alias, from_id, body, ts) VALUES (...)
emit IPC event: history_loaded{room, count}
```
### When to request
- After handshake completes with the **first** peer in a network (only ask one peer — avoids fan-out)
- Request rooms the local peer knows about (from its own SQLite `rooms` table)
- If no rooms known yet: request `"general"` only; discover others from incoming live messages
---
## IPC / UI Integration
New IPC event emitted after history is loaded:
```json
{ "type": "history_loaded", "network_id": "...", "room": "general", "count": 47 }
```
The web UI and TUI insert a visual separator above the first gossipped message:
```
── 47 earlier messages ─────────────────────────────
[12:03] alice: hey
[12:04] bob: yo
── live ─────────────────────────────────────────────
[14:22] you joined
```
---
## What This Does Not Do
- **No conflict resolution** — messages are immutable append-only records; there is nothing to conflict
- **No ordering guarantee beyond timestamp** — if two peers sent messages at the same millisecond, both are stored; the UI sorts by `ts` then `msg_id` for a stable tiebreak
- **No full sync** — gossip is bounded by `limit` and `since`; it is not a replication protocol
- **No anchor involvement** — the anchor never sees history
- **No history from peers who were offline** — if no peer with history is online when you join, you get nothing (acceptable given the trust model)

89
QUICKSTART.md Normal file
View File

@@ -0,0 +1,89 @@
# waste — quick start
waste is a private, encrypted chat and file sharing app for people you trust.
No accounts, no phone numbers, no central server that knows your messages.
Pick the option that fits you best.
---
## Option 1 — Just open it in your browser
If someone is running a waste anchor server and has shared the URL with you:
1. Open the URL in any modern browser
2. Enter your name and a network name your group has agreed on
3. Done — you're in
On mobile, tap **Share → Add to Home Screen** to install it as an app icon.
To invite someone: click the 🔗 button in the sidebar and share the link.
> Your identity and messages stay in your browser. Nothing is stored on the server — the server only helps peers find each other.
---
## Option 2 — Desktop app (recommended for regular use)
Download the latest `waste` binary for your platform from the [releases page](../../releases).
**Linux / macOS:**
```bash
chmod +x waste-linux-amd64 # or waste-darwin-arm64, etc.
./waste-linux-amd64
```
**Windows:** double-click `waste-windows-amd64.exe`.
The app opens a window with the waste UI. Enter your name, the anchor URL, and a network name to join. Your identity is saved between sessions in your config directory (`~/.config/waste` on Linux, `~/Library/Application Support/waste` on macOS, `%APPDATA%\waste` on Windows).
On Linux and Windows a tray icon appears — closing the window hides to tray rather than quitting. Right-click the tray icon to reopen or quit.
---
## Option 3 — Run the daemon manually (headless / power users)
If you want the daemon running in the background without the desktop UI — on a server, over SSH, or with the web UI in a browser pointed at your local machine:
```bash
# Download waste-daemon from the releases page, then:
./waste-daemon -alias yourname -anchor wss://your-anchor-server/ws
```
Then open the web UI in a browser at the anchor URL, or point the web UI's daemon mode at `ws://127.0.0.1:17338`.
Full flag reference:
| Flag | Default | Description |
|---|---|---|
| `-alias` | `anon` | Your display name |
| `-anchor` | — | Anchor server WebSocket URL |
| `-data-dir` | `~/.waste` | Where identity and messages are stored |
| `-download-dir` | same as data-dir | Where received files are saved |
| `-ipc-port` | `17337` | Local TCP IPC port |
| `-ws-port` | `0` (off) | WebSocket IPC port (needed for web UI) |
| `-turn-url` | — | TURN relay URL (fixes mobile/CGNAT) |
| `-turn-secret` | — | TURN shared secret |
---
## Inviting someone
1. Click `Ctrl+I` in the TUI, or click **Generate invite** in the web UI
2. Share the `waste:...` link with your friend (Signal, email, anything)
3. They open it in a browser or pass it to `waste-daemon --join 'waste:...'`
Invite links encode the anchor URL and network name. The anchor never sees your messages.
---
## Running your own anchor server
The anchor is a tiny signaling server that helps peers find each other — it never sees plaintext messages or file contents. You need a VPS with a domain and TLS.
```bash
# On your VPS:
./waste-anchor -bind 127.0.0.1:8080
```
Put it behind nginx with a `/ws` WebSocket proxy and serve the web UI static files at `/`. See [README.md](README.md#hosting-on-a-vps) for the full nginx setup.