diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 17fa919..eeafd61 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -12,7 +12,24 @@ on: workflow_dispatch: jobs: + preflight: + runs-on: ubuntu-latest + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: Check Vercel secrets + id: check + run: | + if [ -n "${{ secrets.VERCEL_TOKEN }}" ] && [ -n "${{ secrets.VERCEL_ORG_ID }}" ] && [ -n "${{ secrets.VERCEL_PROJECT_ID }}" ]; then + echo "has-secrets=true" >> "$GITHUB_OUTPUT" + else + echo "has-secrets=false" >> "$GITHUB_OUTPUT" + echo "Vercel secrets not configured; skipping deploy." + fi + deploy: + needs: preflight + if: needs.preflight.outputs.has-secrets == 'true' runs-on: ubuntu-latest permissions: contents: read diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 792cc3f..39566f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,13 @@ Before submitting a PR: - [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) — Vercel deployment - [docs/TESTING.md](docs/TESTING.md) — Testing checklist +When you change behavior (storage, sharing, limits, encryption), update the relevant docs and README sections so the system remains self-explanatory. + +## Deployment Notes + +- This is a **static-only** app. Do not add Edge Functions or serverless code. +- GitHub Actions deploys to Vercel only when repository secrets are configured. + ## Questions? Open an issue for bugs, feature requests, or questions. diff --git a/README.md b/README.md index 4325f4a..f02b7d0 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ python -m http.server 8000 - **Block editor** — Notion-style blocks: `/` for slash commands, Tab/Shift+Tab to indent, drag handles to reorder. Supports headings, lists, todos, code, quotes, callouts, dividers, toggles. - **URL as storage** — Content is compressed (binary + deflate/brotli + base85) and stored in the URL hash. ~25–40% more capacity than before. No backend, no accounts. -- **Share by link** — Copy the URL to share; recipients see the same content when they open it. +- **Share by link** — Copy the URL to share; recipients see the same content when they open it. Share/QR links are computed from the current saved hash. - **QR code** — Use `/qr` for a QR code of the current page; a small QR is also shown in the bottom-right panel. - **Local version history** — Git-like branching stored in `localStorage`: each version has a hash, timestamp, and parent. You can undo/redo across versions and create branches by editing an older version and saving. - **History graph** — In the **History** menu: SVG graph (time left→right, branches stacked) and a vertical list. Click a version to **preview** it (read-only with a banner); from there you can **Restore this version** or **Back to current**. - **Version undo/redo** — Buttons in the History menu step to parent (undo) or forward along the redo stack (redo). Editor undo/redo remains `Ctrl+Z` / `Ctrl+Y`. - **Reset** — Clears version history and keeps only the current document as a single version. - **Export** — Share menu: **Copy link**, **Export TXT**, **Export HTML**, **Export MD**. -- **Lock** — Share menu: **Lock with password** encrypts content (AES-GCM). Share the password separately with recipients. -- **URL size limit** — Status bar shows URL size; at 8 KB input is blocked (typing disabled, paste truncated). Delete content to continue. +- **Lock** — Share menu: **Lock with password** encrypts content (AES-GCM). Requires HTTPS or localhost. Share the password separately with recipients. +- **URL size limit** — Status bar shows the computed share URL size with thresholds (safe/warning/danger/critical). At 8 KB input is blocked (typing disabled, paste truncated). Delete content to continue. - **Theme** — Light/dark via `?theme=light` or `?theme=dark`, or system preference. - **PWA** — Installable; manifest and service worker for offline support. @@ -72,17 +72,49 @@ kheMessage/ --- +## Deployment (static-only) + +This project is a **static PWA**: there are no serverless or Edge Functions. Deploy the files as-is on any static host (Vercel, Netlify, GitHub Pages). Vercel configuration lives in `vercel.json` and only defines rewrites/headers/caching. + ## How it works (overview) 1. You type in a **block editor** (Notion-style): each line is a block. Use `/` for slash commands, Tab/Shift+Tab to indent, drag handles to reorder. Content is Markdown-friendly with inline formatting. -2. Changes are **debounced** (e.g. 1200 ms); then **save** runs: content + optional inline styles are compressed, turned into a hash, and written to the URL and `localStorage`. A **version entry** `{ hash, t, parents }` is appended to local history. -3. **Version undo** moves to the parent of the current head and pushes the current head onto a redo stack; **version redo** pops from that stack. Any new save clears the redo stack and can create a new branch. -4. **Preview** loads a chosen version into the editor (read-only), with a banner to restore that version or return to the current head. +2. Changes are **debounced** (e.g. 1200 ms); then **save** runs: blocks → serialize → compress → hash. The hash is written to the URL and `localStorage`. A **version entry** `{ hash, t, parents }` is appended to local history. +3. **Share/QR** uses the computed share URL (origin + path + optional theme + hash) so the status bar and QR match what you can copy. +4. **Version undo** moves to the parent of the current head and pushes the current head onto a redo stack; **version redo** pops from that stack. Any new save clears the redo stack and can create a new branch. +5. **Preview** loads a chosen version into the editor (read-only), with a banner to restore that version or return to the current head. For a deeper dive, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). --- +## Storage, limits, and sharing (details) + +### URL storage pipeline + +Blocks are serialized into a compact binary format, compressed (brotli when available, otherwise deflate-raw), then encoded into a URL-safe base85 or base64url hash. The hash is stored in the URL fragment (`#...`) so there is **no backend** and nothing is sent to a server when you type. + +### URL size thresholds + +The status bar shows the byte size of the computed share URL. Thresholds are conservative: + +- **safe**: 2 KB +- **warning**: 4 KB +- **danger**: 8 KB (input blocked) +- **critical**: 16 KB (most platforms reject these URLs) + +These limits are heuristics; browsers and platforms vary in their actual max URL length. The app errs on the safe side. + +### Share link and QR + +The **Share → Copy link** action saves first (when possible) and copies the computed share URL. QR codes use the same computed URL so QR density matches the actual link length. + +### Encryption (password lock) + +When you lock a note, the serialized blocks are encrypted with **AES‑GCM** using a key derived from the password (PBKDF2, SHA‑256). Encryption requires **HTTPS or localhost** because WebCrypto is only available in secure contexts. + +--- + ## Architecture ```mermaid diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ab0c51b..8439080 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -119,6 +119,27 @@ flowchart TB - `hash` — Current head hash (compressed content) - `kheMessage_localVersions` — Array of `{ hash, t, parents }` entries (max 50) +## Share URL + Size Metrics + +The UI (status bar, QR, and copy link) is driven by a computed **share URL** rather than raw `location.href`: + +1. **Compute hash** from current blocks (`getShortestHash`). +2. **Build share URL**: `origin + path + ?theme + #hash`. +3. **Cache size** in bytes to avoid expensive recomputation on every keystroke. + +This ensures the size indicator and QR code reflect the **actual shareable link** (not a stale hash). + +### Size Thresholds + +The status bar uses conservative thresholds to warn early: + +- **safe**: 2 KB +- **warning**: 4 KB +- **danger**: 8 KB (input blocked) +- **critical**: 16 KB (link likely fails in many contexts) + +These are **heuristics**. Real platform limits vary, so the app prevents writes past the danger threshold to avoid broken links. + ## Version History kheMessage implements a Git-like version history with branching and merge support. @@ -198,3 +219,7 @@ flowchart TB ``` Cache version is defined in `sw.js` as `CACHE_VERSION`; updating it invalidates old caches. + +## Encryption Context + +Password lock uses WebCrypto (AES‑GCM + PBKDF2). WebCrypto only works in secure contexts, so encryption is available on **HTTPS or localhost**. diff --git a/docs/CODE_STRUCTURE.md b/docs/CODE_STRUCTURE.md index 38dcc4f..e4bb5a0 100644 --- a/docs/CODE_STRUCTURE.md +++ b/docs/CODE_STRUCTURE.md @@ -20,18 +20,18 @@ A line-by-line map of the main application file and key functions. | ~2310–2459 | Binary serialization | `serializeBlocks`, `deserializeBlocks` | | ~2459–2525 | Base85 encoding | `encodeBase85`, `decodeBase85`, `looksLikeBase85` | | ~2525–2585 | Compression | `compressBlocks`, `decompressToBlocks` | -| ~2585–2781 | Encryption | `encryptBlocks`, `decryptBlocks`, AES-GCM, PBKDF2 | +| ~2585–2781 | Encryption | `encryptBlocks`, `decryptToBlocks`, AES-GCM, PBKDF2 | | ~2781–3030 | Block rendering | `renderBlocks`, `blocksToDOM`, `domToBlocks` | | ~3030–3382 | Block operations | `insertBlock`, `deleteBlock`, `moveBlock`, `setBlockType` | | ~3382–3471 | Slash menu | Slash command handling, type selection | | ~3471–3501 | Multi-select | Shift+click range selection | | ~3501–3597 | Drag and drop | Block reordering, indent level on drop | -| ~3597–3624 | URL limit | 8KB enforcement, typing disabled at limit | -| ~3624–3731 | Status bar | URL size display, size warnings | -| ~3731–3908 | Save/load | `save`, `set`, `setFromHash`, `get`, `blocksToHash`, `hashToBlocks` | -| ~3908–3962 | Theme | Light/dark toggle, URL param, system preference | -| ~3962–4520 | Version history | `pushLocalVersion`, `versionUndo`, `versionRedo`, `enterPreview`, `exitPreview` | -| ~4520–4588 | UI helpers | `setSaveStatus`, `notify`, `debounce`, `updateQR` | +| ~3597–3720 | URL metrics + limit | `buildShareUrl`, URL size caching, 8KB enforcement | +| ~3720–3840 | Status bar | URL size display, size warnings | +| ~3840–4030 | Save/load | `save`, `load`, `setFromHash`, URL updates | +| ~4030–4125 | Theme | Light/dark toggle, URL param, system preference | +| ~4125–4685 | Version history | `pushLocalVersion`, `versionUndo`, `versionRedo`, `enterPreview`, `exitPreview` | +| ~4685–4760 | UI helpers | `setSaveStatus`, `notify`, `updateQR` | | ~4588–4727 | Export | `downloadTXT`, `downloadHTML`, `downloadMD` | | ~4727–5100 | Initialization | DOM ready, hash load, event listeners, Editor setup | @@ -42,11 +42,10 @@ A line-by-line map of the main application file and key functions. | Function | Description | |----------|-------------| | `save()` | Gets blocks → compresses → updates URL hash + localStorage, pushes version | -| `set(hash)` | Decompresses hash → sets blocks → renders DOM | -| `setFromHash(hash)` | Async version of `set`; handles encrypted content | -| `get()` | Returns string representation of content + style | -| `blocksToHash(blocks)` | Serializes and compresses blocks to hash string | -| `hashToBlocks(hashStr)` | Decompresses and deserializes hash to blocks | +| `load()` | Resolves hash from URL/localStorage → renders blocks | +| `setFromHash(hash)` | Async load; handles encrypted content | +| `getShortestHash(blocks)` | Computes best hash encoding (binary vs compact) | +| `buildShareUrl(hash)` | Builds share URL for size/QR/copy | ### Version History @@ -59,6 +58,14 @@ A line-by-line map of the main application file and key functions. | `exitPreview()` | Returns to current head, editable | | `clearVersionHistory()` | Clears all history (Reset) | +### URL Size + Share Metrics + +| Function | Description | +|----------|-------------| +| `refreshUrlMetrics()` | Computes cached share URL + byte size | +| `syncUrlMetricsToLocation()` | Syncs metrics to `location.href` (preview/lock) | +| `isAtInputLimit()` | Enforces danger threshold (8 KB) based on share URL size | + ### Block Types (BLOCK_TYPES) | Value | Type | Description | diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 3c79ee9..0fcb3e2 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -44,6 +44,7 @@ The repository includes `.github/workflows/deploy.yaml` that: - **Production**: Automatic deploy on push to main branches - **Preview**: Automatic deploy on pull requests (URL commented on PR) - **Action**: Uses `amondnet/vercel-action@v25` +- **Secrets guard**: Deploy steps are skipped when required secrets are missing (common for forked PRs). ### Required GitHub Secrets @@ -75,6 +76,10 @@ The project includes `vercel.json` with: - `qrcode.js`: `max-age=31536000, immutable` (1 year) - Cursor files: `max-age=31536000, immutable` (1 year) +## No Edge Functions + +This project is **static-only** and does not use Vercel Edge Functions or serverless functions. All functionality is client-side in `index.html`. + ## Custom Domain 1. Add your domain in Vercel project settings diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d12440f..bd4d858 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -23,6 +23,8 @@ php -S localhost:8000 Then visit `http://localhost:8000`. +> **Tip:** Encryption (password lock) requires HTTPS or localhost because WebCrypto is only available in secure contexts. + ### Option 3: VS Code Live Server Use the Live Server extension to serve the project. @@ -69,6 +71,12 @@ See [docs/TESTING.md](TESTING.md) for the full testing checklist. Before submitt 4. Test keyboard navigation 5. Check console for errors +## URL Storage + Limits (Developer Notes) + +- **Share URL computation**: The app computes the share URL from the current saved hash (origin + path + optional theme + hash). Status bar and QR use this computed URL so the displayed size matches the actual share link. +- **Limits are conservative**: The 8 KB “danger” threshold is a safety limit; real platform maximums vary. The app blocks input at the danger threshold to prevent broken links. +- **Local files**: When running from `file://`, share URLs reuse the current file path + hash because `location.origin` is `null`. + ## Cursor IDE The project includes `.cursor/` with: diff --git a/docs/TESTING.md b/docs/TESTING.md index 1e8cd07..4183561 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -61,8 +61,9 @@ Before deployment, ensure these secrets are configured in GitHub repository sett - [ ] URL updates after typing (debounced) - [ ] Refreshing page preserves content - [ ] Sharing URL works - recipient sees same content -- [ ] URL size indicator shows in status bar -- [ ] 8KB limit enforced (typing disabled at limit) +- [ ] URL size indicator shows in status bar (matches share URL) +- [ ] 8KB danger limit enforced (typing disabled at limit; paste truncated) +- [ ] Size warnings appear at 4KB/8KB/16KB thresholds #### Version History @@ -82,6 +83,7 @@ Before deployment, ensure these secrets are configured in GitHub repository sett - [ ] Export TXT downloads correctly - [ ] Export HTML downloads correctly - [ ] Export MD downloads correctly +- [ ] Lock/unlock works (AES-GCM) on HTTPS/localhost #### QR Code @@ -188,6 +190,7 @@ Verify cache headers: - [ ] URL with invalid base64 - [ ] localStorage full/disabled - [ ] Service worker registration fails +- [ ] Password lock unavailable on insecure origins (shows message) ## Performance Metrics diff --git a/index.html b/index.html index 812ccd9..d3b3218 100644 --- a/index.html +++ b/index.html @@ -2170,7 +2170,7 @@