commit 0cce04b5b87b074240b8f11c599488635d5e4d0f Author: Kyle Isom Date: Tue Mar 24 19:42:38 2026 -0700 Initialize eng-pad-server with project documentation - README.md: project overview, quick start, build commands - CLAUDE.md: AI dev context, source tree, key conventions - ARCHITECTURE.md: full system spec covering data model, auth (password + FIDO2/U2F), gRPC sync API, REST API, SVG/JPG/PDF rendering, web UI, configuration, deployment, security - PROJECT_PLAN.md: 11 phases with discrete checkboxable steps - PROGRESS.md: decision log and completion tracking Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..e44734f --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,313 @@ +# ARCHITECTURE.md — eng-pad-server + +## 1. System Overview + +eng-pad-server is a read-only sync and viewing service for eng-pad +engineering notebooks. The Android app is the sole writer; the server +receives complete notebook data via gRPC and serves it through a web UI. + +``` +Android App eng-pad-server ++-----------+ gRPC/TLS +------------------+ +| eng-pad | ───────────────> | Sync Service | +| (writer) | username/pass | (gRPC :9443) | ++-----------+ in metadata +------------------+ + │ + ▼ + +──────────────+ + │ SQLite DB │ + +──────────────+ + │ + ┌────────────┼────────────┐ + ▼ ▼ ▼ + REST API Web UI Share Links + (:8443) (:8080) (/s/:token) + JSON htmx/SVG No auth +``` + +## 2. Data Model + +### Users + +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER PK | Auto-increment | +| username | TEXT UNIQUE | Login identifier | +| password_hash | TEXT | Argon2id hash | +| created_at | INTEGER | Epoch millis | +| updated_at | INTEGER | Epoch millis | + +### WebAuthn Credentials + +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER PK | Auto-increment | +| user_id | INTEGER FK | References users(id) CASCADE | +| credential_id | BLOB UNIQUE | WebAuthn credential ID | +| public_key | BLOB | COSE public key | +| name | TEXT | User-assigned label | +| sign_count | INTEGER | Signature counter | +| created_at | INTEGER | Epoch millis | + +### Notebooks + +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER PK | Auto-increment | +| user_id | INTEGER FK | References users(id) CASCADE | +| remote_id | INTEGER | App-side notebook ID | +| title | TEXT | Notebook title | +| page_size | TEXT | "REGULAR" or "LARGE" | +| synced_at | INTEGER | Last sync epoch millis | + +UNIQUE(user_id, remote_id) + +### Pages + +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER PK | Auto-increment | +| notebook_id | INTEGER FK | References notebooks(id) CASCADE | +| remote_id | INTEGER | App-side page ID | +| page_number | INTEGER | 1-based page number | + +UNIQUE(notebook_id, remote_id) + +### Strokes + +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER PK | Auto-increment | +| page_id | INTEGER FK | References pages(id) CASCADE | +| pen_size | REAL | Width in canonical points (300 DPI) | +| color | INTEGER | ARGB packed int | +| style | TEXT | "plain", "dashed", "arrow", "double_arrow" | +| point_data | BLOB | Packed LE floats: [x0,y0,x1,y1,...] | +| stroke_order | INTEGER | Z-order within page | + +### Share Links + +| Column | Type | Description | +|--------|------|-------------| +| id | INTEGER PK | Auto-increment | +| notebook_id | INTEGER FK | References notebooks(id) CASCADE | +| token | TEXT UNIQUE | 32-byte random, URL-safe base64 | +| expires_at | INTEGER | Epoch millis, NULL = never | +| created_at | INTEGER | Epoch millis | + +## 3. Authentication + +### gRPC (Android App Sync) + +- Username and password sent in gRPC metadata on every RPC +- Unary interceptor verifies against Argon2id hash +- TLS required — plaintext rejected +- No tokens, no login RPC + +### Web UI (Browser) + +- `POST /v1/auth/login` — password → bearer token +- Token in `HttpOnly; Secure; SameSite=Strict` cookie +- 24h TTL, SHA-256 keyed lookup with cache + +### FIDO2/U2F (Web UI Only) + +- Register keys after password login +- Login with key as password alternative +- Multiple keys per user, user-assigned labels +- `go-webauthn/webauthn` library + +### Shareable Links + +- Token in URL, no auth required +- 32-byte `crypto/rand`, URL-safe base64 +- Optional expiry (default: never) +- Expired → 410 Gone +- Revocable via gRPC API or web UI + +## 4. gRPC API + +Service: `engpad.v1.EngPadSync` + +| RPC | Description | Auth | +|-----|-------------|------| +| SyncNotebook | Push complete notebook (upsert) | user/pass | +| DeleteNotebook | Remove notebook from server | user/pass | +| ListNotebooks | List user's synced notebooks | user/pass | +| CreateShareLink | Generate shareable URL | user/pass | +| RevokeShareLink | Invalidate a share link | user/pass | +| ListShareLinks | List links for a notebook | user/pass | + +### Sync Semantics + +`SyncNotebook` is a full replacement: all pages and strokes for the +notebook are deleted and re-inserted. The server mirrors exactly what +the tablet has. No incremental sync, no conflict resolution. + +Keyed by (user_id, remote_id) where remote_id is the app-side +notebook ID. + +## 5. REST API + +### Auth Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| POST | /v1/auth/login | None | Password login → token | +| POST | /v1/auth/webauthn/register/begin | Bearer | Start key registration | +| POST | /v1/auth/webauthn/register/finish | Bearer | Complete registration | +| POST | /v1/auth/webauthn/login/begin | None | Start key login | +| POST | /v1/auth/webauthn/login/finish | None | Complete key login | + +### Notebook Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | /v1/notebooks | Bearer | List notebooks | +| GET | /v1/notebooks/:id | Bearer | Notebook + page list | +| GET | /v1/notebooks/:id/pages/:num/svg | Bearer | Page as SVG | +| GET | /v1/notebooks/:id/pages/:num/jpg | Bearer | Page as JPG (300 DPI) | +| GET | /v1/notebooks/:id/pdf | Bearer | Full notebook PDF | + +### Share Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | /s/:token | None | Notebook view | +| GET | /s/:token/pages/:num/svg | None | Page SVG | +| GET | /s/:token/pages/:num/jpg | None | Page JPG | +| GET | /s/:token/pdf | None | Notebook PDF | + +## 6. Rendering + +### SVG + +Strokes rendered as SVG `` elements. Coordinates scaled from +300 DPI to 72 DPI (×0.24) for standard SVG/PDF point units. + +- `stroke-linecap="round"`, `stroke-linejoin="round"` +- Dashed strokes: `stroke-dasharray="7.2 4.8"` +- Arrow heads: separate `` elements +- No grid — grid is a tablet writing aid only +- `viewBox` matches physical page dimensions in points + +### JPG + +Server-side rasterization at 300 DPI using Go's `image` package. +White background, strokes rendered with the same coordinate system. + +### PDF + +Generated with a Go PDF library. Coordinates in 72 DPI (native PDF +points). One page per notebook page. + +## 7. Web Interface + +Built with Go `html/template` + htmx. Embedded via `//go:embed`. + +### Pages + +| Route | Template | Description | +|-------|----------|-------------| +| /login | login.html | Login form (password + WebAuthn) | +| /notebooks | notebooks.html | Notebook list | +| /notebooks/:id | notebook.html | Page grid with thumbnails | +| /notebooks/:id/pages/:num | page.html | Full page SVG viewer | +| /s/:token | notebook.html | Shared notebook (no auth) | +| /s/:token/pages/:num | page.html | Shared page viewer | + +### Security + +- CSRF via signed double-submit cookies +- Session cookie: HttpOnly, Secure, SameSite=Strict +- html/template auto-escaping + +## 8. Configuration + +```toml +[server] +listen_addr = ":8443" +grpc_addr = ":9443" +tls_cert = "/srv/eng-pad-server/certs/cert.pem" +tls_key = "/srv/eng-pad-server/certs/key.pem" + +[web] +listen_addr = ":8080" +base_url = "https://pad.metacircular.net" + +[database] +path = "/srv/eng-pad-server/eng-pad-server.db" + +[auth] +token_ttl = "24h" +argon2_memory = 65536 +argon2_time = 3 +argon2_threads = 4 + +[webauthn] +rp_display_name = "Engineering Pad" +rp_id = "pad.metacircular.net" +rp_origins = ["https://pad.metacircular.net"] + +[log] +level = "info" +``` + +## 9. Deployment + +### Container + +Multi-stage Docker build: +1. Builder: `golang:1.25-alpine`, `CGO_ENABLED=0`, stripped binary +2. Runtime: `alpine:latest`, non-root user + +### systemd + +| Unit | Purpose | +|------|---------| +| eng-pad-server.service | Main service | +| eng-pad-server-backup.service | Oneshot backup | +| eng-pad-server-backup.timer | Daily 02:00 UTC | + +Security hardening: NoNewPrivileges, ProtectSystem=strict, +ReadWritePaths=/srv/eng-pad-server. + +### Data Directory + +``` +/srv/eng-pad-server/ +├── eng-pad-server.toml +├── eng-pad-server.db +├── certs/ +│ ├── cert.pem +│ └── key.pem +└── backups/ +``` + +## 10. Security + +- TLS 1.3 minimum, no fallback +- Argon2id for password hashing +- `crypto/rand` for all tokens and nonces +- `crypto/subtle` for constant-time comparisons +- No secrets in logs +- Default deny: unauthenticated requests rejected +- Share links: scoped to single notebook, optional expiry, revocable +- Graceful shutdown: SIGINT/SIGTERM → drain → close DB → exit + +## 11. CLI Commands + +| Command | Purpose | +|---------|---------| +| server | Start the service | +| init | Create database, first user | +| snapshot | Database backup (VACUUM INTO) | +| status | Health check | + +## 12. Future Work + +- MCIAS integration for auth delegation +- Per-page share links (URL structure already supports it) +- Notebook version history (store previous syncs) +- WebSocket notifications for real-time sync status +- Thumbnail generation for notebook list diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b6e1924 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,131 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +eng-pad-server is a Go service that receives engineering notebook data from the eng-pad Android app via gRPC, stores it in SQLite, and serves read-only views through a web UI. It supports password + FIDO2/U2F authentication and shareable links. + +## Build Commands + +```bash +make all # vet → lint → test → build +make eng-pad-server # build binary with version injection +make build # compile all packages +make test # run tests +make vet # go vet +make lint # golangci-lint +make proto # regenerate gRPC code +make proto-lint # buf lint + breaking changes +make clean # remove binary + +# Run a single test: +go test -run TestFunctionName ./internal/... +``` + +## Architecture + +- **Go 1.25+**, pure-Go dependencies, `CGO_ENABLED=0` +- **gRPC API**: receives notebook sync from the Android app (password auth per-request over TLS) +- **REST API**: JSON over HTTPS for web viewing, auth endpoints +- **Web UI**: Go `html/template` + htmx, SVG page rendering +- **SQLite**: via `modernc.org/sqlite`, WAL mode, foreign keys +- **Auth**: Argon2id passwords + FIDO2/U2F via `go-webauthn/webauthn` +- **Router**: chi (lightweight, stdlib-compatible) + +## Project Documents + +- **ARCHITECTURE.md** — full system specification +- **PROJECT_PLAN.md** — implementation steps with checkboxes +- **PROGRESS.md** — completion tracking and decisions + +**Keep PROJECT_PLAN.md and PROGRESS.md in sync.** + +## Source Tree + +``` +eng-pad-server/ +├── cmd/ +│ └── eng-pad-server/ CLI entry point (cobra) +│ ├── main.go +│ ├── server.go server subcommand +│ └── init.go init subcommand +├── internal/ +│ ├── auth/ +│ │ ├── argon2.go Password hashing +│ │ ├── tokens.go Bearer token generation/validation +│ │ └── webauthn.go FIDO2/U2F integration +│ ├── config/ +│ │ └── config.go TOML configuration +│ ├── db/ +│ │ ├── db.go Database setup, pragmas +│ │ └── migrations.go Schema migrations +│ ├── grpcserver/ +│ │ ├── server.go gRPC server setup +│ │ ├── sync.go SyncNotebook handler +│ │ ├── share.go Share link RPCs +│ │ └── interceptors.go Auth interceptor +│ ├── server/ +│ │ ├── server.go REST server setup +│ │ ├── routes.go Route registration +│ │ ├── auth.go Login/register endpoints +│ │ ├── notebooks.go Notebook/page endpoints +│ │ └── middleware.go Auth middleware +│ ├── render/ +│ │ ├── svg.go Page → SVG rendering +│ │ ├── jpg.go Page → JPG rendering +│ │ └── pdf.go Notebook → PDF rendering +│ ├── share/ +│ │ └── share.go Share link token management +│ └── webserver/ +│ ├── server.go Web UI server setup +│ ├── routes.go Template routes +│ └── handlers.go htmx handlers +├── proto/engpad/ +│ └── v1/ +│ └── sync.proto gRPC service definition +├── gen/engpad/ +│ └── v1/ Generated Go gRPC code +├── web/ +│ ├── embed.go //go:embed directive +│ ├── templates/ +│ │ ├── layout.html Shared HTML skeleton +│ │ ├── login.html Login page +│ │ ├── notebooks.html Notebook list +│ │ ├── notebook.html Single notebook view +│ │ └── page.html Page viewer (SVG embed) +│ └── static/ +│ └── htmx.min.js +├── deploy/ +│ ├── docker/ +│ │ └── docker-compose.yml +│ ├── systemd/ +│ │ ├── eng-pad-server.service +│ │ └── eng-pad-server-backup.timer +│ ├── scripts/ +│ │ └── install.sh +│ └── examples/ +│ └── eng-pad-server.toml +├── Dockerfile +├── Makefile +├── buf.yaml +├── .golangci.yaml +├── .gitignore +├── CLAUDE.md This file +├── README.md +├── ARCHITECTURE.md Full system spec +├── PROJECT_PLAN.md Implementation steps +└── PROGRESS.md Completion tracking +``` + +## Key Conventions + +- Stroke point data: packed little-endian floats `[x0,y0,x1,y1,...]` — same binary format as the Android app +- Canonical coordinates: 300 DPI. Scaled to 72 DPI for SVG/PDF output (×0.24) +- Page sizes: REGULAR (2550×3300 pts), LARGE (3300×5100 pts) +- Stroke styles: "plain", "dashed", "arrow", "double_arrow" +- No grid rendering — grid is a tablet writing aid only +- Share link tokens: 32-byte `crypto/rand`, URL-safe base64 +- gRPC auth: username+password in metadata, verified per-request +- Web auth: password login → bearer token in session cookie +- TLS 1.3 minimum, no exceptions diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 0000000..28a4c85 --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,27 @@ +# PROGRESS.md — eng-pad-server Implementation Progress + +This file tracks completed work and decisions. Updated after every step. +See PROJECT_PLAN.md for the full step list. + +## Completed + +_(none yet)_ + +## In Progress + +Phase 0: Project Setup + +## Decisions + +- **Language**: Go (Metacircular standard) +- **Database**: SQLite via `modernc.org/sqlite` (pure Go, no CGo) +- **Auth**: Argon2id passwords + FIDO2/U2F via `go-webauthn/webauthn` +- **gRPC auth**: username/password in metadata per-request (no tokens) +- **Web auth**: password → bearer token in session cookie +- **Rendering**: SVG for web viewing, JPG/PDF for export +- **Sync model**: full notebook replacement (upsert), no incremental sync +- **Share links**: 32-byte random token, optional expiry, scoped to notebook +- **Grid**: not rendered server-side (tablet writing aid only) +- **Coordinate system**: 300 DPI canonical, scaled to 72 DPI for SVG/PDF +- **FIDO2/U2F**: web UI login only, not gRPC sync +- **Server is read-only**: mirrors tablet exactly, no content modification diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md new file mode 100644 index 0000000..426cd71 --- /dev/null +++ b/PROJECT_PLAN.md @@ -0,0 +1,148 @@ +# PROJECT_PLAN.md — eng-pad-server Implementation Steps + +This file tracks all implementation steps. Check off steps as they are +completed and log them in PROGRESS.md. + +## Phase 0: Project Setup + +- [ ] 0.1: Initialize Go module (`git.wntrmute.dev/kyle/eng-pad-server`) +- [ ] 0.2: Create Makefile with standard targets +- [ ] 0.3: Configure `.golangci.yaml` +- [ ] 0.4: Create `.gitignore` +- [ ] 0.5: Create example config `deploy/examples/eng-pad-server.toml` +- **Verify:** `make build` + +## Phase 1: Database + Config + +- [ ] 1.1: TOML config loading + - `internal/config/config.go` +- [ ] 1.2: SQLite database setup (WAL, foreign keys, busy timeout) + - `internal/db/db.go` +- [ ] 1.3: Schema migrations (users, notebooks, pages, strokes, share_links, webauthn_credentials) + - `internal/db/migrations.go` +- [ ] 1.4: Unit tests for migrations +- **Verify:** `make test` + +## Phase 2: Auth — Password + +- [ ] 2.1: Argon2id password hashing + verification + - `internal/auth/argon2.go` +- [ ] 2.2: Bearer token generation, storage, validation + - `internal/auth/tokens.go` +- [ ] 2.3: User creation (for `init` command) +- [ ] 2.4: Unit tests for auth +- **Verify:** `make test` + +## Phase 3: CLI + +- [ ] 3.1: Cobra CLI scaffold + - `cmd/eng-pad-server/main.go` +- [ ] 3.2: `init` command — create DB, prompt for admin user + - `cmd/eng-pad-server/init.go` +- [ ] 3.3: `server` command — start gRPC + REST + web servers + - `cmd/eng-pad-server/server.go` +- [ ] 3.4: `snapshot` command — VACUUM INTO backup +- [ ] 3.5: `status` command — health check +- **Verify:** `make all && ./eng-pad-server init` + +## Phase 4: gRPC Sync Service + +- [ ] 4.1: Proto definitions + - `proto/engpad/v1/sync.proto` +- [ ] 4.2: Generate Go code + - `make proto` +- [ ] 4.3: gRPC server setup with TLS + - `internal/grpcserver/server.go` +- [ ] 4.4: Auth interceptor (username/password from metadata) + - `internal/grpcserver/interceptors.go` +- [ ] 4.5: SyncNotebook handler (upsert: delete + re-insert) + - `internal/grpcserver/sync.go` +- [ ] 4.6: DeleteNotebook handler +- [ ] 4.7: ListNotebooks handler +- [ ] 4.8: Unit tests for sync +- **Verify:** `make test` + manual gRPC test with `grpcurl` + +## Phase 5: Rendering + +- [ ] 5.1: SVG rendering — strokes to SVG path elements + - `internal/render/svg.go` +- [ ] 5.2: JPG rendering — rasterize page at 300 DPI + - `internal/render/jpg.go` +- [ ] 5.3: PDF rendering — notebook to multi-page PDF + - `internal/render/pdf.go` +- [ ] 5.4: Unit tests — verify SVG output, JPG dimensions, PDF page count +- **Verify:** `make test` + +## Phase 6: REST API + +- [ ] 6.1: chi router setup with TLS + - `internal/server/server.go`, `routes.go` +- [ ] 6.2: Auth middleware (bearer token validation) + - `internal/server/middleware.go` +- [ ] 6.3: Login endpoint + - `internal/server/auth.go` +- [ ] 6.4: Notebook/page endpoints (JSON metadata) + - `internal/server/notebooks.go` +- [ ] 6.5: Rendering endpoints (SVG, JPG, PDF) +- [ ] 6.6: Unit tests for API +- **Verify:** `make test` + manual curl + +## Phase 7: Share Links + +- [ ] 7.1: Token generation + storage + - `internal/share/share.go` +- [ ] 7.2: gRPC RPCs — CreateShareLink, RevokeShareLink, ListShareLinks + - `internal/grpcserver/share.go` +- [ ] 7.3: REST endpoints — /s/:token routes +- [ ] 7.4: Expiry enforcement (check on access, periodic cleanup) +- [ ] 7.5: Unit tests +- **Verify:** `make test` + +## Phase 8: Web UI + +- [ ] 8.1: Template skeleton — layout.html, navigation + - `web/templates/layout.html` +- [ ] 8.2: Login page (password + WebAuthn) + - `web/templates/login.html` +- [ ] 8.3: Notebook list page + - `web/templates/notebooks.html` +- [ ] 8.4: Notebook view page (page grid with SVG thumbnails) + - `web/templates/notebook.html` +- [ ] 8.5: Page viewer (embedded SVG, export buttons) + - `web/templates/page.html` +- [ ] 8.6: Shared notebook/page views (same templates, no auth chrome) +- [ ] 8.7: Web server setup + embed + - `internal/webserver/`, `web/embed.go` +- **Verify:** manual browser test + +## Phase 9: FIDO2/U2F (WebAuthn) + +- [ ] 9.1: WebAuthn integration with `go-webauthn/webauthn` + - `internal/auth/webauthn.go` +- [ ] 9.2: Registration endpoints (begin/finish) +- [ ] 9.3: Login endpoints (begin/finish) +- [ ] 9.4: Key management UI (list keys, add key, remove key) +- [ ] 9.5: Unit tests +- **Verify:** manual test with security key + +## Phase 10: Deployment + +- [ ] 10.1: Dockerfile (multi-stage, non-root) +- [ ] 10.2: systemd units (service, backup timer) + - `deploy/systemd/` +- [ ] 10.3: Install script + - `deploy/scripts/install.sh` +- [ ] 10.4: Graceful shutdown (SIGINT/SIGTERM) +- **Verify:** `make docker && docker run` + +## Phase 11: Android App Sync Integration + +_(Implemented in the eng-pad repo, not here)_ + +- [ ] 11.1: gRPC client dependency (protobuf-lite) +- [ ] 11.2: SyncClient.kt — gRPC channel + stub +- [ ] 11.3: SyncManager.kt — serialize notebook to proto, call sync +- [ ] 11.4: Sync settings screen (server URL, username, password) +- [ ] 11.5: Notebook overflow menu — "Sync to server" +- [ ] 11.6: Library — "Sync all" button +- [ ] 11.7: Sync status indicator on notebook cards diff --git a/README.md b/README.md new file mode 100644 index 0000000..50762dc --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +eng-pad-server +============== + +Read-only sync and web viewer for [eng-pad](https://git.wntrmute.dev/kyle/eng-pad) +engineering notebooks. + +The Android app pushes complete notebooks to this server via gRPC. The +server stores them and serves read-only views through a web UI with +SVG rendering. Shareable links allow unauthenticated access to specific +notebooks. + +## Features + +- **gRPC sync**: receive notebook data from the Android app over TLS +- **Web viewer**: browse notebooks, view pages as SVG, export JPG/PDF +- **Authentication**: password (Argon2id) + FIDO2/U2F security keys +- **Shareable links**: token-based URLs with optional expiry + +## Quick Start + +```bash +# Build +make eng-pad-server + +# Generate example config +cp eng-pad-server.toml.example /srv/eng-pad-server/eng-pad-server.toml +# Edit configuration (TLS certs, database path, etc.) + +# Initialize (creates database, prompts for admin user) +./eng-pad-server init + +# Run +./eng-pad-server server +``` + +## Build + +```bash +make all # vet → lint → test → build +make test # run tests +make lint # golangci-lint +make proto # regenerate gRPC code from .proto files +make proto-lint # buf lint + breaking change detection +``` + +## Documentation + +- [ARCHITECTURE.md](ARCHITECTURE.md) — full system specification +- [CLAUDE.md](CLAUDE.md) — AI development context + +## License + +Private. All rights reserved.