Revision 4 of 16
Makes the server authoritative on every write, moves the edit and public routes to catch-all alias segments, and folds the reorder and test lists into prose.
+24 −45 lines of text vs revision 3 (this site's diff)
| Old | New | Change | Line |
|---|---|---|---|
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | # Build The Urlist | |
| 2 | 2 | ||
| 3 | − | Build the complete application in this repository. Work autonomously from start to finish: plan internally, implement all requirements, test them, fix defects, and stop only when the app is complete. | |
| 3 | + | Build the complete application in this repository. Work autonomously from start to finish and stop only when the app is complete. | |
| 4 | 4 | ||
| 5 | 5 | If a minor detail is not specified, choose a reasonable solution and document it. Do not copy an existing implementation of The Urlist. | |
| 6 | 6 | ||
| @@ -34,19 +34,19 @@ | |||
| 34 | 34 | ||
| 35 | 35 | Anyone can view an active list. | |
| 36 | 36 | ||
| 37 | + | The server is authoritative: revalidate input, ownership, alias availability, and immutability on every write; ignore client-supplied owner IDs. | |
| 38 | + | ||
| 37 | 39 | ## Routes | |
| 38 | 40 | ||
| 39 | 41 | | Route | Behavior | | |
| 40 | 42 | |---|---| | |
| 41 | 43 | | `/` | Home page and first-link entry | | |
| 42 | 44 | | `/s/new` | Create or resume the one local draft | | |
| 43 | − | | `/s/edit/{alias}` | Owner-only, reloadable edit page | | |
| 45 | + | | `/s/edit/{...alias}` | Owner-only, reloadable edit page | | |
| 44 | 46 | | `/s/mylists` | Current user's active and deleted lists | | |
| 45 | − | | `/{alias}` | Active public list, deleted tombstone, or unused-alias state | | |
| 46 | − | ||
| 47 | − | Aliases can have multiple segments. Use catch-all App Router routes where needed. Static app routes take priority. | |
| 47 | + | | `/{...alias}` | Active public list, deleted tombstone, or unused-alias state | | |
| 48 | 48 | ||
| 49 | − | Reserve first segments `s`, `api`, `auth`, `__test`, and `.well-known`. | |
| 49 | + | Aliases may contain multiple `/`-separated segments. Static routes take priority; reserve first segments `s`, `api`, `auth`, `__test`, and `.well-known`. | |
| 50 | 50 | ||
| 51 | 51 | Global navigation must include Home, New, My Lists when signed in, Login or the current user with Log out, and Light/Dark/System theme controls. Confirm before New clears a non-empty draft. | |
| 52 | 52 | ||
| @@ -58,7 +58,6 @@ | |||
| 58 | 58 | ||
| 59 | 59 | A list: | |
| 60 | 60 | ||
| 61 | − | - Requires at least one link to publish or save | |
| 62 | 61 | - Has no product-level link limit | |
| 63 | 62 | - Allows duplicate destination URLs as separate entries | |
| 64 | 63 | - Has an optional description and alias | |
| @@ -71,23 +70,13 @@ | |||
| 71 | 70 | ||
| 72 | 71 | ### Reordering | |
| 73 | 72 | ||
| 74 | − | Drag-and-drop is the primary reorder interaction. | |
| 75 | − | ||
| 76 | − | - Show a visible drag handle on each editable link. | |
| 77 | − | - Support pointer and touch dragging to any position. | |
| 78 | − | - Show clear picked-up, dragging, drop-position, and dropped states. | |
| 79 | − | - Update draft order immediately. | |
| 80 | − | - Save account-owned order to SQLite. | |
| 81 | − | - Show saved order on the public page. | |
| 82 | − | - Support desktop and mobile widths. | |
| 83 | − | ||
| 84 | − | Also provide accessible Move up and Move down controls. Drag-and-drop alone is not sufficient. | |
| 73 | + | Drag-and-drop is the primary reorder interaction: provide a visible handle per editable link, pointer and touch dragging to any position, and clear picked-up, dragging, drop-target, and dropped states. Also provide accessible Move up and Move down controls. Persist order in the draft and SQLite. | |
| 85 | 74 | ||
| 86 | 75 | ## Live metadata | |
| 87 | 76 | ||
| 88 | − | Fetch and parse the live destination page on the server after a link is added. Show Pending, Succeeded, or Failed per link. Block publish and save while any link is Pending. | |
| 77 | + | Fetch and parse the live destination page on the server after a link is added. Show Pending, Succeeded, or Failed per link. | |
| 89 | 78 | ||
| 90 | − | Stop the network operation after 10 seconds and show a terminal state within one more second. Failure keeps the link and permits manual title/description editing and publication. | |
| 79 | + | Abort the fetch after 10 seconds and settle the link into a terminal state. Failure keeps the link and permits manual title/description editing and publication. | |
| 91 | 80 | ||
| 92 | 81 | Metadata precedence: | |
| 93 | 82 | ||
| @@ -97,7 +86,7 @@ | |||
| 97 | 86 | ||
| 98 | 87 | Resolve relative images against the final URL after redirects. Empty metadata is a successful result. Preserve manual title and description edits. Show a neutral image placeholder when needed. | |
| 99 | 88 | ||
| 100 | − | The fetcher must allow only HTTP/HTTPS; reject credentials and non-public addresses, including loopback, private, link-local, reserved, and cloud metadata ranges; revalidate DNS and every redirect; prevent DNS rebinding; follow at most five redirects; limit content to 2 MiB; accept only HTML/XHTML; and never forward app credentials. | |
| 89 | + | The fetcher must be SSRF-safe: HTTP/HTTPS only, no credentials, reject non-public/internal addresses, revalidate DNS on every redirect to prevent rebinding, follow at most five redirects, limit content to 2 MiB, accept only HTML/XHTML, and never forward app credentials. | |
| 101 | 90 | ||
| 102 | 91 | ## Aliases and publication | |
| 103 | 92 | ||
| @@ -107,15 +96,15 @@ | |||
| 107 | 96 | - Is 1–50 characters | |
| 108 | 97 | - Does not start or end with a hyphen | |
| 109 | 98 | ||
| 110 | − | The full alias is at most 200 characters. Reject empty, `.`/`..`, reserved, query/fragment, backslash, and encoded-separator forms. | |
| 99 | + | The full alias is at most 200 characters. | |
| 111 | 100 | ||
| 112 | − | Match the exact full path, so `burke` and `burke/links` can both exist. Active, deleted, and anonymous aliases are all reserved. Enforce uniqueness in SQLite. | |
| 101 | + | Match the exact full path, so `burke` and `burke/links` can both exist. Once published, aliases are permanent and globally unique in SQLite across active, deleted, and anonymous lists; they never change or become available again. | |
| 113 | 102 | ||
| 114 | − | If blank, generate an available seven-character lowercase alphanumeric alias. Aliases never change after publication. | |
| 103 | + | If blank, generate an available seven-character lowercase alphanumeric alias. | |
| 115 | 104 | ||
| 116 | − | Block publish/save when the list is empty, metadata is Pending, or the alias is invalid/unavailable. Repeat validation on the server. On failure, keep all user input. | |
| 105 | + | Block publish/save when the list is empty, metadata is Pending, or the alias is invalid/unavailable. On failure, keep all user input. | |
| 117 | 106 | ||
| 118 | − | For anonymous publication, require a blocking confirmation that says the list cannot be edited or deleted and its alias stays reserved permanently. Use **Publish permanently** and **Cancel**. Enforce immutability on the server. Anonymous lists never appear in My Lists. | |
| 107 | + | For anonymous publication, require a blocking confirmation that says the list cannot be edited or deleted after publication. Use **Publish permanently** and **Cancel**. Anonymous lists never appear in My Lists. | |
| 119 | 108 | ||
| 120 | 109 | ## Mock login and ownership | |
| 121 | 110 | ||
| @@ -123,15 +112,15 @@ | |||
| 123 | 112 | ||
| 124 | 113 | Use a server-verifiable HTTP-only session cookie with an appropriate SameSite policy. Use Secure in production HTTPS while allowing local HTTP development. Login survives reload; logout ends the session. Use stable user IDs, not display names, for ownership. | |
| 125 | 114 | ||
| 126 | − | Only an owner can load, save, delete, or restore an account list. Enforce every check on the server and ignore client-supplied owner IDs. | |
| 115 | + | Only an owner can load, save, delete, or restore an account list. | |
| 127 | 116 | ||
| 128 | − | The edit page loads persistent data directly, keeps alias and destinations read-only, and permits description, membership, order, title, and description changes. Show save progress and errors without losing input. | |
| 117 | + | The edit page loads persistent data directly and permits list-description, link-membership, order, and per-link title/description changes. Show save progress and errors without losing input. | |
| 129 | 118 | ||
| 130 | − | My Lists requires login and shows only the current user's lists in separate Active and Deleted sections. Active cards show alias, optional description, and link count. Provide Create new list, Restore, and clear empty/loading/error states. | |
| 119 | + | My Lists requires login and shows only the current user's lists in separate Active and Deleted sections. Active cards show alias, optional description, and link count. Provide Create new list and Restore. | |
| 131 | 120 | ||
| 132 | 121 | ## Delete and restore | |
| 133 | 122 | ||
| 134 | − | Delete requires confirmation and performs a soft delete. Keep content, ownership, and alias forever. There is no permanent-delete action. | |
| 123 | + | Delete requires confirmation and performs a soft delete that retains content and ownership. There is no permanent-delete action. | |
| 135 | 124 | ||
| 136 | 125 | The public tombstone shows only: | |
| 137 | 126 | ||
| @@ -158,15 +147,9 @@ | |||
| 158 | 147 | ||
| 159 | 148 | Provide persisted Light, Dark, and System themes. System follows OS preference. Keep accessible contrast and avoid an initial wrong-theme flash when practical. | |
| 160 | 149 | ||
| 161 | − | Support current Chromium from desktop down to 320 CSS pixels. Keep navigation, editor fields, drag controls, link actions, sharing, QR, lists, and modals usable without page-level two-dimensional scrolling. | |
| 162 | − | ||
| 163 | − | Meet WCAG 2.2 AA: | |
| 150 | + | Support current Chromium from desktop down to 320 CSS pixels with no page-level horizontal scrolling; modals, drag controls, and the editor must remain usable. | |
| 164 | 151 | ||
| 165 | − | - Semantic headings, labels, names, errors, and status announcements | |
| 166 | − | - Full keyboard use, visible focus, logical focus order, and keyboard reordering | |
| 167 | − | - No color-only meaning; usable at 200% zoom; reduced-motion support | |
| 168 | − | - Modal focus entry, trap, Escape cancellation, and focus return | |
| 169 | − | - Logical focus after deletion or reordering | |
| 152 | + | Meet WCAG 2.2 AA, including full keyboard operation, reduced-motion support, sensible focus after deletion or reordering, and announced status/error changes. | |
| 170 | 153 | ||
| 171 | 154 | Show truthful loading, empty, success, blocked, and error states. Errors must explain recovery. Never present failure as success. | |
| 172 | 155 | ||
| @@ -176,7 +159,7 @@ | |||
| 176 | 159 | ||
| 177 | 160 | Persist users, lists, ownership/state/timestamps, links, and positions. Published and deleted data must survive a complete restart. Configure the database path by environment variable with a safe local default; do not commit database files. | |
| 178 | 161 | ||
| 179 | − | Validate all untrusted input on the server, use parameterized SQL, escape user and remote content, prevent stored/reflected script execution, encode share parameters, apply suitable CSRF protection, and do not expose secrets or internals in errors. | |
| 162 | + | Treat user text and fetched metadata as untrusted when rendering, prevent stored/reflected script execution, encode share parameters, apply CSRF protection, and keep internals out of errors. | |
| 180 | 163 | ||
| 181 | 164 | Provide a development/test-only deterministic reset, preferably `POST /__test/reset` returning `204`. It clears lists and sessions and restores the two users. Disable or protect it in production. | |
| 182 | 165 | ||
| @@ -196,13 +179,9 @@ | |||
| 196 | 179 | db:reset | |
| 197 | 180 | ``` | |
| 198 | 181 | ||
| 199 | − | Use Vitest for validation, aliases, metadata parsing/network policy, ownership, SQLite transactions, and share URLs. | |
| 200 | − | ||
| 201 | − | Use Playwright for anonymous and account publication, both users, My Lists, owner/non-owner access, drag-and-drop plus persisted order, keyboard reordering, delete/restore/tombstone, public/QR/share views, themes, accessibility basics, and mobile layout. | |
| 202 | − | ||
| 203 | − | Tests use a separate temporary database, reset state, avoid order dependence, and clean up processes. | |
| 182 | + | Use Vitest for URL/alias validation, metadata parsing and network policy, ownership, SQLite transactions, and share URLs. Use Playwright for anonymous/account publication, owner/non-owner access, drag-and-drop and keyboard reordering with persisted order, delete/restore/tombstone, and public/QR/share views. Tests use a separate temporary database and must not depend on order. | |
| 204 | 183 | ||
| 205 | − | The README must cover prerequisites, setup, environment, database, development and production commands, mock login, reset, tests, metadata behavior, troubleshooting, and assumptions. | |
| 184 | + | Ship a README covering setup, environment variables, database, commands, mock login, reset, tests, and assumptions. | |
| 206 | 185 | ||
| 207 | 186 | ## Completion | |
| 208 | 187 | ||