Repository navigation
UI #1237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UI #1237
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,12 @@ Run the full local check before pushing changes: | |
| npm run check | ||
| ``` | ||
|
|
||
| ### Bundle budget | ||
|
|
||
| Production assets are limited to 3,240,000 JavaScript bytes and 165,000 CSS | ||
| bytes. The JavaScript budget includes the 40,000-byte allowance for local | ||
| game/app management. Run `npm run size` after a fresh build for current totals. | ||
|
|
||
| ### Linux WebHID permissions | ||
|
|
||
| If a device appears in Chromium's picker but OpenMouse reports `Failed to open | ||
|
|
@@ -68,6 +74,28 @@ Grant access to every `hidraw` node for each product. Chromium opens the HID | |
| device before OpenMouse selects its vendor configuration collection, so access | ||
| to only the `0xff02:0x0002` collection's node is insufficient. | ||
|
|
||
| ## Local game and app profiles | ||
|
|
||
| Games & apps separates OpenMouse presets from Your apps. With the updated | ||
| OpenMouse Bridge running, Add game or app lets users choose a running app or | ||
| enter an executable name/full path. Personal entries are saved in Bridge's | ||
|
Comment on lines
+79
to
+81
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔎 Supported by static analysis
Correct the README: the add dialog has no running-app picker and rejects executable names. The README at lines 79-81 says that Add game or app lets users "choose a running app or enter an executable name/full path." The add flow does not provide either option as written:
A full executable path still works. Only the two documented alternatives are unavailable. Either implement the two documented options, or change the README to describe the full-path entry that the dialog supports. The README also says that a filename matches any executable with that name (README lines 84-88). Remove or correct that statement together with the first sentence, because the add flow cannot create a filename-only entry.
|
||
| local config, not sent to OpenMouse for approval. They survive catalog refreshes | ||
| and Bridge restarts. Adding/editing/removing an entry does not require a mouse. | ||
|
|
||
| Choose Game to keep its profile while the process runs, including after | ||
| alt-tabbing. Choose App for foreground-only switching. A full executable path | ||
| matches that installation only; a filename matches any executable with that | ||
| name. Foreground matching on Linux requires X11. Local profiles retain the | ||
| existing native/full-settings limits: Bridge can apply supported DPI/polling | ||
| settings with the browser closed; the control panel must remain open to apply | ||
| and restore full device-settings snapshots. | ||
|
|
||
| Local targets use stable IDs, not display names, so a target can be renamed or | ||
| share a name with an OpenMouse preset without losing or confusing its settings. | ||
| Removing a local target also removes its linked mouse profiles; clearing its | ||
| mouse settings leaves the target available. Older Bridges show update guidance | ||
| rather than falling back to a public game request. | ||
|
|
||
| ## Community | ||
|
|
||
| Questions, device requests, bug reports, or just want to follow along? Come | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| export class BodyTooLarge extends Error {} | ||
|
|
||
| export async function boundedRequest(request, limit) { | ||
| if (Number(request.headers.get("Content-Length")) > limit) throw new BodyTooLarge(); | ||
| if (!request.body) return request; | ||
| const reader = request.body.getReader(); | ||
| const chunks = []; | ||
| let size = 0; | ||
| try { | ||
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
| size += value.byteLength; | ||
| if (size > limit) { | ||
| await reader.cancel().catch(() => {}); | ||
| throw new BodyTooLarge(); | ||
| } | ||
| chunks.push(value); | ||
| } | ||
| } finally { | ||
| reader.releaseLock(); | ||
| } | ||
| const bytes = new Uint8Array(size); | ||
| let offset = 0; | ||
| for (const chunk of chunks) { | ||
| bytes.set(chunk, offset); | ||
| offset += chunk.byteLength; | ||
| } | ||
| const headers = new Headers(request.headers); | ||
| headers.delete("Content-Length"); | ||
| return new Request(request, { headers, body: bytes }); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,48 @@ | ||||||
| function text(value, max) { | ||||||
| if (typeof value !== "string" || value.length > max) throw new Error("Invalid text"); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔎 Supported by static analysis
Reject empty embed-field names and values before the webhook call.
🐛 Suggested fix
|
||||||
| return value; | ||||||
| } | ||||||
|
|
||||||
| export function feedbackPayload(input) { | ||||||
| if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Invalid payload"); | ||||||
| const payload = { allowed_mentions: { parse: [] } }; | ||||||
| if (input.content !== undefined) payload.content = text(input.content, 2000); | ||||||
| if (input.embeds !== undefined) { | ||||||
| if (!Array.isArray(input.embeds) || input.embeds.length > 10) throw new Error("Invalid embeds"); | ||||||
| let total = 0; | ||||||
| payload.embeds = input.embeds.map((embed) => { | ||||||
| if (!embed || typeof embed !== "object" || Array.isArray(embed)) throw new Error("Invalid embed"); | ||||||
| const result = {}; | ||||||
| for (const [key, limit] of [["title", 256], ["description", 4096]]) { | ||||||
| if (embed[key] !== undefined) { result[key] = text(embed[key], limit); total += result[key].length; } | ||||||
| } | ||||||
| if (embed.color !== undefined) { | ||||||
| if (!Number.isInteger(embed.color) || embed.color < 0 || embed.color > 0xffffff) throw new Error("Invalid color"); | ||||||
| result.color = embed.color; | ||||||
| } | ||||||
| if (embed.fields !== undefined) { | ||||||
| if (!Array.isArray(embed.fields) || embed.fields.length > 25) throw new Error("Invalid fields"); | ||||||
| result.fields = embed.fields.map((field) => { | ||||||
| const name = text(field?.name, 256); | ||||||
| const value = text(field?.value, 1024); | ||||||
| total += name.length + value.length; | ||||||
| return { name, value, ...(typeof field.inline === "boolean" ? { inline: field.inline } : {}) }; | ||||||
| }); | ||||||
| } | ||||||
| if (embed.footer !== undefined) { | ||||||
| const value = text(embed.footer?.text, 2048); | ||||||
| total += value.length; | ||||||
| result.footer = { text: value }; | ||||||
| } | ||||||
| if (embed.timestamp !== undefined) { | ||||||
| const value = text(embed.timestamp, 64); | ||||||
| if (!Number.isFinite(Date.parse(value))) throw new Error("Invalid timestamp"); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔎 Supported by static analysis
Validate the embed timestamp format before
🐛 Suggested fix
Committable suggestion
|
||||||
| result.timestamp = value; | ||||||
| } | ||||||
| return result; | ||||||
| }); | ||||||
| if (total > 6000) throw new Error("Embeds too long"); | ||||||
| } | ||||||
| if (!payload.content && !payload.embeds?.length) throw new Error("Empty feedback"); | ||||||
| return payload; | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import { boundedRequest, BodyTooLarge } from "./_lib/body-limit.js"; | ||
|
|
||
| // Site-wide security guard. Runs before every Cloudflare Pages route — static | ||
| // pages, admin, and all /api/* endpoints. Tracks per-IP behavior, flags | ||
| // abuse/exploit attempts, and permanently bans repeat offenders. | ||
|
|
@@ -128,6 +130,9 @@ const ASSET_DESTS = new Set([ | |
| const ASSET_EXT_RE = /\.(?:js|mjs|css|map|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|eot|mp4|webm|mp3|ogg|wav|wasm|webmanifest)$/i; | ||
|
|
||
| function isStaticSubresource(request, url) { | ||
| let pathname; | ||
| try { pathname = decodeURIComponent(url.pathname); } catch { return false; } | ||
| if (/^\/+api(?:\/|$)/i.test(pathname) || !ASSET_EXT_RE.test(pathname)) return false; | ||
| const method = request.method.toUpperCase(); | ||
| if (method !== "GET" && method !== "HEAD") return false; | ||
| const dest = request.headers.get("Sec-Fetch-Dest"); | ||
|
|
@@ -176,15 +181,23 @@ export async function onRequest(context) { | |
| } | ||
|
|
||
| async function handle({ request, env, next }) { | ||
| const pathname = new URL(request.url).pathname; | ||
| const limit = pathname === "/api/comments" ? 16 * 1024 : MAX_BODY_BYTES; | ||
| try { | ||
| request = await boundedRequest(request, limit); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔎 Supported by static analysis
Read request bodies only after the ban, Origin, exploit, and rate-limit checks. Since this change, Declared bodies over the limit are rejected from Move the header-only checks ahead of the body read. Keep the body read before every path that forwards to Ordering to apply
|
||
| } catch (error) { | ||
| return error instanceof BodyTooLarge ? tooLarge() : new Response("Invalid body", { status: 400 }); | ||
|
Comment on lines
+186
to
+189
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔎 Supported by static analysis
Count declared-length oversize POSTs as strikes before returning 413. The The The early return does not change behavior for streamed oversized bodies without Restore the strike for
|
||
| } | ||
| const forward = () => next(request); | ||
| const kv = env.SECURITY_KV; | ||
| if (!kv) return next(); | ||
| if (!kv) return forward(); | ||
|
|
||
| const ip = clientIp(request); | ||
| const url = new URL(request.url); | ||
|
|
||
| // Subresources are the bulk of traffic and the bulk of the KV bill; skip them | ||
| // before any storage work. | ||
| if (isStaticSubresource(request, url)) return next(); | ||
| if (isStaticSubresource(request, url)) return forward(); | ||
|
|
||
| // An admin who is themselves caught by a ban (shared/NAT IP) still has to | ||
| // reach the unban endpoint, so that one path is exempt — and only when the | ||
|
|
@@ -224,5 +237,5 @@ async function handle({ request, env, next }) { | |
| return tooMany(); | ||
| } | ||
|
|
||
| return next(); | ||
| return forward(); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔎 Supported by static analysis
Script executed:
sed -n '1,180p' build/check-bundle-size.tsRepository: OpenMouse-Project/openmouse
Length of output: 2002
Script executed:
Repository: OpenMouse-Project/openmouse
Length of output: 3130
Document the 18,000-byte preview-fixtures allowance in the README.
The README states a flat JavaScript limit of 3,240,000 bytes.
build/check-bundle-size.tsenforces that limit only when nopreview-fixtures-*asset exists indist/assets. When such an asset exists, the checker allows 3,258,000 bytes. A production build can passnpm run sizewith a JavaScript total the README describes as over budget.📝 Suggested README correction
Correct the README in the same PR. Contributors use this text as the build budget, so the documented limit must match the enforced limit.
Committable suggestion