diff --git a/PROJECTS/advanced/rveng/challenges/06-stripped-gate/challenge.json b/PROJECTS/advanced/rveng/challenges/06-stripped-gate/challenge.json new file mode 100644 index 00000000..ac6c9c4a --- /dev/null +++ b/PROJECTS/advanced/rveng/challenges/06-stripped-gate/challenge.json @@ -0,0 +1,7 @@ +{ + "id": "06-stripped-gate", + "module": "disassembly", + "title": "Gate in the dark", + "mission": "This binary is stripped: no symbol table, so there are no function names to look up. Discover the functions by their prologue, disassemble the one that checks a number, and read the constant it compares against. Answer in decimal or hex.", + "answer": { "category": "found_value", "expected": 1337 } +} diff --git a/PROJECTS/advanced/rveng/challenges/06-stripped-gate/source.c b/PROJECTS/advanced/rveng/challenges/06-stripped-gate/source.c new file mode 100644 index 00000000..1c42d31b --- /dev/null +++ b/PROJECTS/advanced/rveng/challenges/06-stripped-gate/source.c @@ -0,0 +1,23 @@ +#include +#include + +int check(int n) { + if (n == 1337) { + return 1; + } + return 0; +} + +int main(int argc, char **argv) { + char *secret = "the_flag_is_here"; + int n = 0; + if (argc > 1) { + n = atoi(argv[1]); + } + if (check(n)) { + printf("unlocked: %s\n", secret); + } else { + printf("wrong number\n"); + } + return 0; +} diff --git a/PROJECTS/advanced/rveng/challenges/06-stripped-gate/target b/PROJECTS/advanced/rveng/challenges/06-stripped-gate/target new file mode 100755 index 00000000..60281096 Binary files /dev/null and b/PROJECTS/advanced/rveng/challenges/06-stripped-gate/target differ diff --git a/PROJECTS/advanced/rveng/docs/research/05-static-analysis.md b/PROJECTS/advanced/rveng/docs/research/05-static-analysis.md new file mode 100644 index 00000000..ca1915bf --- /dev/null +++ b/PROJECTS/advanced/rveng/docs/research/05-static-analysis.md @@ -0,0 +1,191 @@ + + + +# Static Analysis Depth Research + +The M3.5 depth features that lift rveng from a hex-and-disasm viewer to a real +static reverse-engineering workbench: import resolution through the PLT, cross +references with RIP-relative operand math, control-flow-graph reconstruction, +and function discovery in stripped binaries. Every offset, opcode, and address +below is traced to the sample `gate` binary (`gcc -no-pie -fno-stack-protector +-O0`) and its stripped copy `gate_stripped` (`strip gate`), captured with +`readelf` and `objdump -M intel`. Nothing here executes a binary: PLT, GOT, and +relocation tables are inspected as data, exactly like the header and sections. + +## Import resolution through the PLT + +A dynamically linked call does not jump straight to libc. It jumps to a small +stub in `.plt` that reads a pointer from `.got.plt`, which the dynamic linker +fills in. To name a call the way objdump prints `call 401030 `, the +engine resolves the stub back to the imported symbol. + +Sections that carry the mapping, from `readelf -SW gate`: + +- `.plt` PROGBITS at `0x401020`, entry size `0x10` (16 bytes). +- `.got.plt` PROGBITS at `0x403fe8`, entry size 8. +- `.rela.plt` RELA at `0x400520`, entry size `0x18` (24 bytes), 3 entries. +- `.dynsym` DYNSYM at `0x4003d0`, entry size `0x18`, its linked string table is + `.dynstr`. + +The `.plt` layout from `objdump -d -j .plt`: + +``` +401020 : push/jmp/nop the PLT0 resolver stub (not an import) +401030 : ff 25 ca 2f 00 00 jmp QWORD PTR [rip+0x2fca] -> 404000 +401040 : ff 25 c2 2f 00 00 jmp QWORD PTR [rip+0x2fc2] -> 404008 +401050 : ff 25 ba 2f 00 00 jmp QWORD PTR [rip+0x2fba] -> 404010 +``` + +The first `.plt` slot (`0x401020`) is the resolver trampoline (PLT0), not an +import. Real entries begin at `0x401030` and step by 16. + +Resolution algorithm, robust across PIE and non-PIE: + +1. For a PLT entry, decode its leading `jmp QWORD PTR [rip+disp32]` (opcode + `ff 25`). The GOT slot it reads is `entry_addr + 6 + disp32`. For `0x401030`: + `0x401036 + 0x2fca = 0x404000`. +2. `.rela.plt` maps each GOT slot to a dynamic symbol. Each `Elf64_Rela` is + `{ r_offset: u64, r_info: u64, r_addend: i64 }`. The symbol index is + `r_info >> 32`; the type is `r_info & 0xffffffff` (7 = `R_X86_64_JUMP_SLOT`). + From `readelf -rW`: + + ``` + r_offset 0x404000 sym 2 JUMP_SLOT puts + r_offset 0x404008 sym 3 JUMP_SLOT printf + r_offset 0x404010 sym 5 JUMP_SLOT atoi + ``` +3. Look the symbol index up in `.dynsym` (same 24-byte `Elf64_Sym` layout as + `.symtab`, names via `.dynstr`) to get `puts`, `printf`, `atoi`. + +So `0x401030 -> 0x404000 -> dynsym[2] -> puts`. A `call` whose target is a `.plt` +entry is then annotated with that name. + +KAT (call-site to import, from `main` in `objdump -d`): + +- `401199: call 401050` resolves to `atoi`. +- `4011a6: call 401146` is `check`, a local function, not a PLT entry. +- `4011c5: call 401040` resolves to `printf`. +- `4011d6: call 401030` resolves to `puts`. + +VERIFIED, and load-bearing for the stripped module: `.plt`, `.got.plt`, +`.rela.plt`, and `.dynsym` all SURVIVE `strip`. `readelf -SW gate_stripped` +still shows them; `readelf -rW gate_stripped` still lists the three JUMP_SLOT +relocations to `puts`, `printf`, `atoi`. Stripping removes `.symtab` and +`.strtab` (local names like `check`, `main`), never the dynamic-linking tables. +A stripped binary can still name its library calls. This is a real teaching +point, not an accident. + +Robustness note an adversarial check flagged: binaries built with +`-fcf-protection` (default on many modern toolchains) grow an `endbr64` and a +`.plt.sec` section, so the `jmp [rip+x]` may live in `.plt.sec` rather than +`.plt`. Resolving by decoding the stub's `jmp [rip+disp]` wherever it sits (and +matching the GOT slot against `.rela.plt`) handles both layouts; resolving by +the PLT entry's pushed relocation index does not. The engine decodes the jmp. +Our curated `-no-pie -O0` samples use the classic single `.plt`. + +## RIP-relative cross references + +x86-64 addresses data PC-relative. `lea rax,[rip+disp]` computes an absolute +address as `address_of_next_instruction + disp`, where the next-instruction +address is `insn.address + insn.size`. This is how a function points at a string +without a relocation in a non-PIE binary. + +KAT (from `main`, `objdump -d -j .text`): + +- `401173: lea rax,[rip+0xe8a]` with size 7 resolves to `0x40117a + 0xe8a = + 0x402004`, the `.rodata` string `the_flag_is_here`. +- `4011b6: lea rax,[rip+0xe58]` (size 7) resolves to `0x4011bd + 0xe58 = + 0x402015`, the `unlocked: %s` format string. +- `4011cc: lea rax,[rip+0xe50]` (size 7) resolves to `0x4011d3 + 0xe50 = + 0x402023`, the `wrong number` string. + +capstone exposes the displacement on the memory operand whose base register is +RIP. The engine computes `insn.address + insn.size + disp` and, when that +address falls inside a known section, labels the reference (a `.rodata` hit is a +string; a `.text` hit is code). + +Cross references are the inverse index: for a target address, the list of +instructions that reach it. Two reference kinds cover these challenges: + +- control-flow refs, from `call`/`jmp`/`jcc` branch targets. Example: `check` at + `0x401146` is referenced by `4011a6: call 401146` in `main`. +- data refs, from resolved RIP-relative operands. Example: the flag string at + `0x402004` is referenced by `401173: lea` in `main`. + +"What calls this function" and "what reads this string" are the two questions a +learner asks constantly; both are this same reverse map. + +## Control-flow graph reconstruction + +A basic block is a straight run of instructions with one entry and one exit: no +branch lands in the middle, no branch leaves except at the end. The standard +leader algorithm: + +1. The first instruction of the function is a leader. +2. Any branch target inside the function is a leader. +3. The instruction following any conditional or unconditional jump is a leader. + +A block runs from a leader up to the instruction before the next leader. `call` +does NOT end a block: it returns, so control falls through. Blocks end at +conditional jumps, unconditional jumps, and `ret`. This matches how IDA and +Ghidra draw function graphs. + +KAT: `check` (`0x401146`..`0x401163`), from `objdump -d`: + +``` +401146 push rbp | B0 (entry) +401147 mov rbp,rsp | +40114a mov [rbp-4],edi | +40114d cmp [rbp-4],0x539 | +401154 jne 40115d | -> ends B0 +401156 mov eax,1 | B1 (fallthrough of jne) +40115b jmp 401162 | -> ends B1 +40115d mov eax,0 | B2 (jne taken target) +401162 pop rbp | B3 (jmp target, also B2 fallthrough) +401163 ret | -> ends B3 +``` + +Leaders: `0x401146` (start), `0x401156` (after the `jne`), `0x40115d` (the `jne` +target), `0x401162` (the `jmp` target). Four basic blocks. Edges: + +- B0 ends in `jne`: to B2 (taken, `0x40115d`) and to B1 (fallthrough, `0x401156`). +- B1 ends in `jmp`: to B3 (`0x401162`). +- B2 ends by fallthrough (its successor `0x401162` is a leader): to B3. +- B3 ends in `ret`: no successors. + +A clean diamond, which is exactly the shape that teaches "the gate splits the +flow and both sides rejoin". Edge kinds worth labeling for the learner: taken, +fallthrough, unconditional. + +## Function discovery in stripped binaries + +`strip` removes `.symtab`, so `check` and `main` have no names and the symbol +table is empty. Disassembly must start from something other than a symbol. + +For the curated `-O0` challenges, every C function opens with the frame-pointer +prologue `push rbp; mov rbp,rsp`, bytes `55 48 89 e5`. Scanning executable +sections for that pattern recovers function entry points. Verified in +`gate_stripped`: `0x401146` (was `check`) and `0x401164` (was `main`) both begin +`55 48 89 e5`; `_start` at `0x401060` does not (it has a different prologue), so +the scan cleanly separates user functions from the runtime start-up code. + +Honest limits an adversarial check must record: this heuristic only finds +functions that keep a frame pointer. Anything built with `-O2` +(frame-pointer-omitted), or hand-written assembly, will be missed; real tools +add call-target recovery, symbol hints, and unwind-table (`.eh_frame`) parsing. +For rveng's curated, frame-pointer-preserving samples the prologue scan is +sufficient and honest, and it is the right first lesson in "how do you even find +a function when the names are gone". The stripped challenge disassembles a +discovered region by address, never by name, and grades a found-value answer, so +no symbol is ever leaked. + +## Facts pulled forward for the KATs + +- PLT entries: `0x401030 -> puts`, `0x401040 -> printf`, `0x401050 -> atoi`; + GOT slots `0x404000/8/10`; `.rela.plt` symbol index is `r_info >> 32`. +- RIP-relative: `target = insn.address + insn.size + disp`; `0x401173 lea -> + 0x402004` (the flag string). +- `check` CFG: 4 basic blocks, leaders `0x401146/0x401156/0x40115d/0x401162`, + diamond edges as above. +- Stripped discovery: prologue `55 48 89 e5` finds `check` and `main`, not + `_start`; `.dynsym`/`.rela.plt`/`.plt` survive `strip` so imports stay named. diff --git a/PROJECTS/advanced/rveng/docs/research/INDEX.md b/PROJECTS/advanced/rveng/docs/research/INDEX.md index 098d099c..52c3c015 100644 --- a/PROJECTS/advanced/rveng/docs/research/INDEX.md +++ b/PROJECTS/advanced/rveng/docs/research/INDEX.md @@ -23,6 +23,7 @@ binaries safe, and it is a hard constraint, not a preference. | `02-x86-64-capstone.md` | implementing `engine/disasm.py`: the minimal x86-64 instruction set these challenges use, the cmp/jcc gate pattern, capstone 5.0.9 API, the objdump-vs-capstone byte-for-byte agreement KAT, the Intel-syntax constant correction | | `03-pedagogy.md` | implementing `engine/challenge.py` and the runner: the solve-then-reveal loop, the three answer-spec categories (found-value, identified-symbol, patched-bytes), module-to-category mapping, grading rules | | `04-no-execution-posture.md` | any API or engine work touching binaries: the enumeration proving every operation is read-only, why patch grading is a static diff, the API input constraints, the standing no-execution rule | +| `05-static-analysis.md` | implementing the M3.5 depth engine (`plt.py`, `xref.py`, `cfg.py`, stripped discovery): PLT/GOT/.rela.plt/.dynsym import resolution, RIP-relative operand math, the basic-block leader algorithm, prologue-scan function discovery, all KAT-traced to `gate` and `gate_stripped` | Key verified facts pulled forward: - ELF header cross-checks exactly to raw bytes: entry `0x401060`, e_shoff diff --git a/PROJECTS/advanced/rveng/frontend/.dockerignore b/PROJECTS/advanced/rveng/frontend/.dockerignore new file mode 100644 index 00000000..a0256ec7 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/.dockerignore @@ -0,0 +1,15 @@ +node_modules +build +dist +.git +.gitignore +*.md +.env* +.vscode +.idea +*.log +npm-debug.log* +pnpm-debug.log* +.DS_Store +coverage +.nyc_output diff --git a/PROJECTS/advanced/rveng/frontend/.env.example b/PROJECTS/advanced/rveng/frontend/.env.example new file mode 100644 index 00000000..f2f18850 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/.env.example @@ -0,0 +1,6 @@ +# ©AngelaMos | 2026 +# .env.example + +# Backend the Vite dev server proxies /api to. +# Run the API with: uv run uvicorn rveng.api.server:app --port 8000 +VITE_API_TARGET=http://localhost:8000 diff --git a/PROJECTS/advanced/rveng/frontend/.gitignore b/PROJECTS/advanced/rveng/frontend/.gitignore new file mode 100644 index 00000000..61cb0c2e --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.vite + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/PROJECTS/advanced/rveng/frontend/.stylelintignore b/PROJECTS/advanced/rveng/frontend/.stylelintignore new file mode 100644 index 00000000..37da03e1 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/.stylelintignore @@ -0,0 +1,22 @@ +# ©AngelaMos | 2025 +# .stylelintignore + +# Dependencies +node_modules/ + +# Production builds +dist/ +build/ +out/ + +# JS/TS files +**/*.js +**/*.jsx +**/*.ts +**/*.tsx + +# Generated files +*.min.css + +# Error system styles - ignore from linting +src/core/app/_toastStyles.scss diff --git a/PROJECTS/advanced/rveng/frontend/biome.json b/PROJECTS/advanced/rveng/frontend/biome.json new file mode 100644 index 00000000..7f29029a --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/biome.json @@ -0,0 +1,94 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 82, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "semicolons": "asNeeded", + "trailingCommas": "es5", + "arrowParentheses": "always" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noExcessiveCognitiveComplexity": { + "level": "error", + "options": { "maxAllowedComplexity": 25 } + }, + "noForEach": "off", + "useLiteralKeys": "off" + }, + "correctness": { + "noUnusedVariables": "error", + "noUnusedImports": "error", + "useExhaustiveDependencies": "warn", + "useHookAtTopLevel": "error", + "noUndeclaredVariables": "error" + }, + "style": { + "useImportType": "error", + "useConst": "error", + "useTemplate": "error", + "useSelfClosingElements": "error", + "useFragmentSyntax": "error", + "noNonNullAssertion": "error", + "useConsistentArrayType": { + "level": "error", + "options": { "syntax": "shorthand" } + }, + "useNamingConvention": "off" + }, + "suspicious": { + "noExplicitAny": "error", + "noDebugger": "error", + "noConsole": "warn", + "noArrayIndexKey": "warn", + "noAssignInExpressions": "error", + "noDoubleEquals": "error", + "noRedeclare": "error", + "noVar": "error" + }, + "security": { + "noDangerouslySetInnerHtml": "error" + }, + "a11y": { + "useAltText": "error", + "useAnchorContent": "error", + "useKeyWithClickEvents": "error", + "noStaticElementInteractions": "error", + "useButtonType": "error", + "useValidAnchor": "error" + } + } + }, + "overrides": [ + { + "includes": ["src/main.tsx"], + "linter": { + "rules": { + "style": { + "noNonNullAssertion": "off" + } + } + } + } + ] +} diff --git a/PROJECTS/advanced/rveng/frontend/index.html b/PROJECTS/advanced/rveng/frontend/index.html new file mode 100644 index 00000000..840bb5f5 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/index.html @@ -0,0 +1,48 @@ + + + + + + + + + + + rveng - reverse-engineering lab + + + + +
+ + + diff --git a/PROJECTS/advanced/rveng/frontend/package.json b/PROJECTS/advanced/rveng/frontend/package.json new file mode 100644 index 00000000..b7d1721b --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "rveng-web", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", + "typecheck": "tsc -b", + "lint:scss": "stylelint '**/*.scss'", + "lint:scss:fix": "stylelint '**/*.scss' --fix" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.12", + "axios": "^1.13.0", + "react": "^19.2.1", + "react-dom": "^19.2.0", + "react-error-boundary": "^6.0.0", + "react-icon": "^1.0.0", + "react-icons": "^5.5.0", + "react-router-dom": "^7.1.1", + "sonner": "^2.0.7", + "zod": "^4.1.13", + "zustand": "^5.0.9" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.8", + "@tanstack/react-query-devtools": "^5.91.1", + "@types/node": "^24.10.2", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "sass": "^1.95.0", + "stylelint": "^16.26.1", + "stylelint-config-prettier-scss": "^1.0.0", + "stylelint-config-standard-scss": "^16.0.0", + "typescript": "~5.9.3", + "vite": "npm:rolldown-vite@7.2.5", + "vite-tsconfig-paths": "^5.1.0" + }, + "pnpm": { + "overrides": { + "vite": "npm:rolldown-vite@7.2.5" + } + } +} diff --git a/PROJECTS/advanced/rveng/frontend/pnpm-lock.yaml b/PROJECTS/advanced/rveng/frontend/pnpm-lock.yaml new file mode 100644 index 00000000..d04a7163 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/pnpm-lock.yaml @@ -0,0 +1,2603 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + vite: npm:rolldown-vite@7.2.5 + +importers: + + .: + dependencies: + '@tanstack/react-query': + specifier: ^5.90.12 + version: 5.90.12(react@19.2.1) + axios: + specifier: ^1.13.0 + version: 1.13.2 + react: + specifier: ^19.2.1 + version: 19.2.1 + react-dom: + specifier: ^19.2.0 + version: 19.2.1(react@19.2.1) + react-error-boundary: + specifier: ^6.0.0 + version: 6.0.0(react@19.2.1) + react-icon: + specifier: ^1.0.0 + version: 1.0.0(babel-runtime@5.8.38)(react@19.2.1) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.2.1) + react-router-dom: + specifier: ^7.1.1 + version: 7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + zod: + specifier: ^4.1.13 + version: 4.1.13 + zustand: + specifier: ^5.0.9 + version: 5.0.9(@types/react@19.2.7)(react@19.2.1) + devDependencies: + '@biomejs/biome': + specifier: ^2.3.8 + version: 2.3.8 + '@tanstack/react-query-devtools': + specifier: ^5.91.1 + version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1) + '@types/node': + specifier: ^24.10.2 + version: 24.10.2 + '@types/react': + specifier: ^19.2.7 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.7) + '@vitejs/plugin-react': + specifier: ^5.1.1 + version: 5.1.2(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0)) + sass: + specifier: ^1.95.0 + version: 1.95.0 + stylelint: + specifier: ^16.26.1 + version: 16.26.1(typescript@5.9.3) + stylelint-config-prettier-scss: + specifier: ^1.0.0 + version: 1.0.0(stylelint@16.26.1(typescript@5.9.3)) + stylelint-config-standard-scss: + specifier: ^16.0.0 + version: 16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)) + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: npm:rolldown-vite@7.2.5 + version: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0) + vite-tsconfig-paths: + specifier: ^5.1.0 + version: 5.1.4(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))(typescript@5.9.3) + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.3.8': + resolution: {integrity: sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.3.8': + resolution: {integrity: sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.3.8': + resolution: {integrity: sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.3.8': + resolution: {integrity: sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.3.8': + resolution: {integrity: sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.3.8': + resolution: {integrity: sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.3.8': + resolution: {integrity: sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.3.8': + resolution: {integrity: sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.3.8': + resolution: {integrity: sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@cacheable/memory@2.0.6': + resolution: {integrity: sha512-7e8SScMocHxcAb8YhtkbMhGG+EKLRIficb1F5sjvhSYsWTZGxvg4KIDp8kgxnV2PUJ3ddPe6J9QESjKvBWRDkg==} + + '@cacheable/utils@2.3.2': + resolution: {integrity: sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==} + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.20': + resolution: {integrity: sha512-8BHsjXfSciZxjmHQOuVdW2b8WLUPts9a+mfL13/PzEviufUEW2xnvQuOlKs9dRBHgRqJ53SF/DUoK9+MZk72oQ==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@dual-bundle/import-meta-resolve@4.2.1': + resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} + + '@emnapi/core@1.7.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.0': + resolution: {integrity: sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.5.4 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@napi-rs/wasm-runtime@1.1.0': + resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/runtime@0.97.0': + resolution: {integrity: sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.97.0': + resolution: {integrity: sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@rolldown/binding-android-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.50': + resolution: {integrity: sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.50': + resolution: {integrity: sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50': + resolution: {integrity: sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50': + resolution: {integrity: sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50': + resolution: {integrity: sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50': + resolution: {integrity: sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.50': + resolution: {integrity: sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.50': + resolution: {integrity: sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.50': + resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==} + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + + '@tanstack/query-core@5.90.12': + resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} + + '@tanstack/query-devtools@5.91.1': + resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==} + + '@tanstack/react-query-devtools@5.91.1': + resolution: {integrity: sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==} + peerDependencies: + '@tanstack/react-query': ^5.90.10 + react: ^18 || ^19 + + '@tanstack/react-query@5.90.12': + resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==} + peerDependencies: + react: ^18 || ^19 + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/node@24.10.2': + resolution: {integrity: sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + babel-runtime@5.8.38: + resolution: {integrity: sha512-KpgoA8VE/pMmNCrnEeeXqFG24TIH11Z3ZaimIhJWsin8EbfZy3WzFKUTIan10ZIDgRVvi9EkLbruJElJC9dRlg==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + baseline-browser-mapping@2.9.5: + resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==} + hasBin: true + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cacheable@2.3.0: + resolution: {integrity: sha512-HHiAvOBmlcR2f3SQ7kdlYD8+AUJG+wlFZ/Ze8tl1Vzvz0MdOh8IYA/EFU4ve8t1/sZ0j4MGi7ST5MoTwHessQA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001759: + resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + core-js@1.2.7: + resolution: {integrity: sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@11.1.1: + resolution: {integrity: sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + flat-cache@6.1.19: + resolution: {integrity: sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hashery@1.3.0: + resolution: {integrity: sha512-fWltioiy5zsSAs9ouEnvhsVJeAXRybGCNNv0lvzpzNOSDbULXRy7ivFWwCCv4I5Am6kSo75hmbsCduOoc2/K4w==} + engines: {node: '>=20'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookified@1.14.0: + resolution: {integrity: sha512-pi1ynXIMFx/uIIwpWJ/5CEtOHLGtnUB0WhGeeYT+fKcQ+WCQbm3/rrkAXnpfph++PgepNqPdTC2WTj8A6k6zoQ==} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@5.5.5: + resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + mdn-data@2.25.0: + resolution: {integrity: sha512-T2LPsjgUE/tgMmRXREVmwsux89DwWfNjiynOeXuLd2mX6jphGQ2YE3Ukz7LQ2VOFKiVZU/Ee1GqzHiipZCjymw==} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + qified@0.5.3: + resolution: {integrity: sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ==} + engines: {node: '>=20'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.2.1: + resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==} + peerDependencies: + react: ^19.2.1 + + react-error-boundary@6.0.0: + resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==} + peerDependencies: + react: '>=16.13.1' + + react-icon@1.0.0: + resolution: {integrity: sha512-VzSlpBHnLanVw79mOxyq98hWDi6DlxK9qPiZ1bAK6bLurMBCaxO/jjyYUrRx9+JGLc/NbnwOmyE/W5Qglbb2QA==} + peerDependencies: + babel-runtime: ^5.3.3 + react: '>=0.12.0' + + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.10.1: + resolution: {integrity: sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.10.1: + resolution: {integrity: sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.1: + resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown-vite@7.2.5: + resolution: {integrity: sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + esbuild: ^0.25.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + rolldown@1.0.0-beta.50: + resolution: {integrity: sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sass@1.95.0: + resolution: {integrity: sha512-9QMjhLq+UkOg/4bb8Lt8A+hJZvY3t+9xeZMKSBtBEgxrXA3ed5Ts4NDreUkYgJP1BTmrscQE/xYhf7iShow6lw==} + engines: {node: '>=14.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + stylelint-config-prettier-scss@1.0.0: + resolution: {integrity: sha512-Gr2qLiyvJGKeDk0E/+awNTrZB/UtNVPLqCDOr07na/sLekZwm26Br6yYIeBYz3ulsEcQgs5j+2IIMXCC+wsaQA==} + engines: {node: 14.* || 16.* || >= 18} + hasBin: true + peerDependencies: + stylelint: '>=15.0.0' + + stylelint-config-recommended-scss@16.0.2: + resolution: {integrity: sha512-aUTHhPPWCvFyWaxtckJlCPaXTDFsp4pKO8evXNCsW9OwsaUWyMd6jvcUhSmfGWPrTddvzNqK4rS/UuSLcbVGdQ==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.24.0 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-recommended@17.0.0: + resolution: {integrity: sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-config-standard-scss@16.0.0: + resolution: {integrity: sha512-/FHECLUu+med/e6OaPFpprG86ShC4SYT7Tzb2PTVdDjJsehhFBOioSlWqYFqJxmGPIwO3AMBxNo+kY3dxrbczA==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.23.1 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-standard@39.0.1: + resolution: {integrity: sha512-b7Fja59EYHRNOTa3aXiuWnhUWXFU2Nfg6h61bLfAb5GS5fX3LMUD0U5t4S8N/4tpHQg3Acs2UVPR9jy2l1g/3A==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-scss@6.13.0: + resolution: {integrity: sha512-kZPwFUJkfup2gP1enlrS2h9U5+T5wFoqzJ1n/56AlpwSj28kmFe7ww/QFydvPsg5gLjWchAwWWBLtterynZrOw==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.8.2 + + stylelint@16.26.1: + resolution: {integrity: sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==} + engines: {node: '>=18.12.0'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + update-browserslist-db@1.2.2: + resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zod@4.1.13: + resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + + zustand@5.0.9: + resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/runtime@7.28.4': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@biomejs/biome@2.3.8': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.3.8 + '@biomejs/cli-darwin-x64': 2.3.8 + '@biomejs/cli-linux-arm64': 2.3.8 + '@biomejs/cli-linux-arm64-musl': 2.3.8 + '@biomejs/cli-linux-x64': 2.3.8 + '@biomejs/cli-linux-x64-musl': 2.3.8 + '@biomejs/cli-win32-arm64': 2.3.8 + '@biomejs/cli-win32-x64': 2.3.8 + + '@biomejs/cli-darwin-arm64@2.3.8': + optional: true + + '@biomejs/cli-darwin-x64@2.3.8': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.3.8': + optional: true + + '@biomejs/cli-linux-arm64@2.3.8': + optional: true + + '@biomejs/cli-linux-x64-musl@2.3.8': + optional: true + + '@biomejs/cli-linux-x64@2.3.8': + optional: true + + '@biomejs/cli-win32-arm64@2.3.8': + optional: true + + '@biomejs/cli-win32-x64@2.3.8': + optional: true + + '@cacheable/memory@2.0.6': + dependencies: + '@cacheable/utils': 2.3.2 + '@keyv/bigmap': 1.3.0(keyv@5.5.5) + hookified: 1.14.0 + keyv: 5.5.5 + + '@cacheable/utils@2.3.2': + dependencies: + hashery: 1.3.0 + keyv: 5.5.5 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.20': {} + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@dual-bundle/import-meta-resolve@4.2.1': {} + + '@emnapi/core@1.7.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@keyv/bigmap@1.3.0(keyv@5.5.5)': + dependencies: + hashery: 1.3.0 + hookified: 1.14.0 + keyv: 5.5.5 + + '@keyv/serialize@1.1.1': {} + + '@napi-rs/wasm-runtime@1.1.0': + dependencies: + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@oxc-project/runtime@0.97.0': {} + + '@oxc-project/types@0.97.0': {} + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@rolldown/binding-android-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.50': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.50': + dependencies: + '@napi-rs/wasm-runtime': 1.1.0 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.50': {} + + '@rolldown/pluginutils@1.0.0-beta.53': {} + + '@tanstack/query-core@5.90.12': {} + + '@tanstack/query-devtools@5.91.1': {} + + '@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1)': + dependencies: + '@tanstack/query-devtools': 5.91.1 + '@tanstack/react-query': 5.90.12(react@19.2.1) + react: 19.2.1 + + '@tanstack/react-query@5.90.12(react@19.2.1)': + dependencies: + '@tanstack/query-core': 5.90.12 + react: 19.2.1 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/node@24.10.2': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.7)': + dependencies: + '@types/react': 19.2.7 + + '@types/react@19.2.7': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@5.1.2(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0) + transitivePeerDependencies: + - supports-color + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + astral-regex@2.0.0: {} + + asynckit@0.4.0: {} + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-runtime@5.8.38: + dependencies: + core-js: 1.2.7 + + balanced-match@2.0.0: {} + + baseline-browser-mapping@2.9.5: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.5 + caniuse-lite: 1.0.30001759 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.2(browserslist@4.28.1) + + cacheable@2.3.0: + dependencies: + '@cacheable/memory': 2.0.6 + '@cacheable/utils': 2.3.2 + hookified: 1.14.0 + keyv: 5.5.5 + qified: 0.5.3 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001759: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + core-js@1.2.7: {} + + cosmiconfig@9.0.0(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + css-functions-list@3.2.3: {} + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + delayed-stream@1.0.0: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.1.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.267: {} + + emoji-regex@8.0.0: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + escalade@3.2.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.0: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@11.1.1: + dependencies: + flat-cache: 6.1.19 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + flat-cache@6.1.19: + dependencies: + cacheable: 2.3.0 + flatted: 3.3.3 + hookified: 1.14.0 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globjoin@0.1.4: {} + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hashery@1.3.0: + dependencies: + hookified: 1.14.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookified@1.14.0: {} + + html-tags@3.3.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immutable@5.1.4: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + ini@1.3.8: {} + + is-arrayish@0.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-plain-object@5.0.0: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + keyv@5.5.5: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@6.0.3: {} + + known-css-properties@0.37.0: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lines-and-columns@1.2.4: {} + + lodash.truncate@4.4.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + mdn-data@2.12.2: {} + + mdn-data@2.25.0: {} + + meow@13.2.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + postcss-media-query-parser@0.2.3: {} + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-from-env@1.1.0: {} + + qified@0.5.3: + dependencies: + hookified: 1.14.0 + + queue-microtask@1.2.3: {} + + react-dom@19.2.1(react@19.2.1): + dependencies: + react: 19.2.1 + scheduler: 0.27.0 + + react-error-boundary@6.0.0(react@19.2.1): + dependencies: + '@babel/runtime': 7.28.4 + react: 19.2.1 + + react-icon@1.0.0(babel-runtime@5.8.38)(react@19.2.1): + dependencies: + babel-runtime: 5.8.38 + react: 19.2.1 + + react-icons@5.5.0(react@19.2.1): + dependencies: + react: 19.2.1 + + react-refresh@0.18.0: {} + + react-router-dom@7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + react-router: 7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + + react-router@7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + cookie: 1.1.1 + react: 19.2.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.1(react@19.2.1) + + react@19.2.1: {} + + readdirp@4.1.2: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0): + dependencies: + '@oxc-project/runtime': 0.97.0 + fdir: 6.5.0(picomatch@4.0.3) + lightningcss: 1.30.2 + picomatch: 4.0.3 + postcss: 8.5.6 + rolldown: 1.0.0-beta.50 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.2 + fsevents: 2.3.3 + sass: 1.95.0 + + rolldown@1.0.0-beta.50: + dependencies: + '@oxc-project/types': 0.97.0 + '@rolldown/pluginutils': 1.0.0-beta.50 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.50 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.50 + '@rolldown/binding-darwin-x64': 1.0.0-beta.50 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.50 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.50 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.50 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.50 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.50 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.50 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.50 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.50 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.50 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.50 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.50 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sass@1.95.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + set-cookie-parser@2.7.2: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + sonner@2.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + + source-map-js@1.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + stylelint-config-prettier-scss@1.0.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + + stylelint-config-recommended-scss@16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + postcss-scss: 4.0.9(postcss@8.5.6) + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3)) + stylelint-scss: 6.13.0(stylelint@16.26.1(typescript@5.9.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-recommended@17.0.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + + stylelint-config-standard-scss@16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended-scss: 16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)) + stylelint-config-standard: 39.0.1(stylelint@16.26.1(typescript@5.9.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-standard@39.0.1(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3)) + + stylelint-scss@6.13.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + css-tree: 3.1.0 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mdn-data: 2.25.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + stylelint: 16.26.1(typescript@5.9.3) + + stylelint@16.26.1(typescript@5.9.3): + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-syntax-patches-for-csstree': 1.0.20 + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1) + '@dual-bundle/import-meta-resolve': 4.2.1 + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 9.0.0(typescript@5.9.3) + css-functions-list: 3.2.3 + css-tree: 3.1.0 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 11.1.1 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 7.0.5 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mathml-tag-names: 2.1.3 + meow: 13.2.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.5.6) + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + svg-tags@1.0.0: {} + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + update-browserslist-db@1.2.2(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vite-tsconfig-paths@5.1.4(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))(typescript@5.9.3): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0) + transitivePeerDependencies: + - supports-color + - typescript + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + yallist@3.1.1: {} + + zod@4.1.13: {} + + zustand@5.0.9(@types/react@19.2.7)(react@19.2.1): + optionalDependencies: + '@types/react': 19.2.7 + react: 19.2.1 diff --git a/PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-192x192.png b/PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-192x192.png new file mode 100644 index 00000000..188b1255 Binary files /dev/null and b/PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-192x192.png differ diff --git a/PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-512x512.png b/PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-512x512.png new file mode 100644 index 00000000..063c71c3 Binary files /dev/null and b/PROJECTS/advanced/rveng/frontend/public/assets/android-chrome-512x512.png differ diff --git a/PROJECTS/advanced/rveng/frontend/public/assets/apple-touch-icon.png b/PROJECTS/advanced/rveng/frontend/public/assets/apple-touch-icon.png new file mode 100644 index 00000000..54915be7 Binary files /dev/null and b/PROJECTS/advanced/rveng/frontend/public/assets/apple-touch-icon.png differ diff --git a/PROJECTS/advanced/rveng/frontend/public/assets/favicon-16x16.png b/PROJECTS/advanced/rveng/frontend/public/assets/favicon-16x16.png new file mode 100644 index 00000000..434cf58c Binary files /dev/null and b/PROJECTS/advanced/rveng/frontend/public/assets/favicon-16x16.png differ diff --git a/PROJECTS/advanced/rveng/frontend/public/assets/favicon-32x32.png b/PROJECTS/advanced/rveng/frontend/public/assets/favicon-32x32.png new file mode 100644 index 00000000..cd1b7406 Binary files /dev/null and b/PROJECTS/advanced/rveng/frontend/public/assets/favicon-32x32.png differ diff --git a/PROJECTS/advanced/rveng/frontend/public/assets/favicon.ico b/PROJECTS/advanced/rveng/frontend/public/assets/favicon.ico new file mode 100644 index 00000000..2485b796 Binary files /dev/null and b/PROJECTS/advanced/rveng/frontend/public/assets/favicon.ico differ diff --git a/PROJECTS/advanced/rveng/frontend/public/assets/site.webmanifest b/PROJECTS/advanced/rveng/frontend/public/assets/site.webmanifest new file mode 100644 index 00000000..1dd91123 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/public/assets/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} diff --git a/PROJECTS/advanced/rveng/frontend/src/App.tsx b/PROJECTS/advanced/rveng/frontend/src/App.tsx new file mode 100644 index 00000000..a7d82d6a --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/App.tsx @@ -0,0 +1,36 @@ +// =========================== +// ©AngelaMos | 2026 +// App.tsx +// =========================== + +import { QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { RouterProvider } from 'react-router-dom' +import { Toaster } from 'sonner' + +import { queryClient } from '@/core/api' +import { router } from '@/core/app/routers' +import '@/core/app/toast.module.scss' + +export default function App(): React.ReactElement { + return ( + +
+ + +
+ +
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/api/client.ts b/PROJECTS/advanced/rveng/frontend/src/api/client.ts new file mode 100644 index 00000000..9657d80d --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/api/client.ts @@ -0,0 +1,107 @@ +// =================== +// © AngelaMos | 2026 +// client.ts +// =================== + +import { API_ENDPOINTS } from '@/config' +import { apiClient } from '@/core/api' +import type { + CfgView, + ChallengeDetail, + ChallengeSummary, + DisasmView, + ElfView, + HexView, + ProgressView, + StringsView, + SubmitRequest, + SubmitResult, + Target, + XrefsView, +} from './types' + +export async function fetchChallenges(): Promise { + const { data } = await apiClient.get( + API_ENDPOINTS.CHALLENGES + ) + return data +} + +export async function fetchChallenge(cid: string): Promise { + const { data } = await apiClient.get( + API_ENDPOINTS.CHALLENGE(cid) + ) + return data +} + +export async function fetchHex( + cid: string, + offset: number, + length: number +): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.HEX(cid), { + params: { offset, length }, + }) + return data +} + +export async function fetchElf(cid: string): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.ELF(cid)) + return data +} + +export async function fetchDisasm( + cid: string, + target: Target, + session: string +): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.DISASM(cid), { + params: { symbol: target.symbol, address: target.address, session }, + }) + return data +} + +export async function fetchCfg(cid: string, target: Target): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.CFG(cid), { + params: { symbol: target.symbol, address: target.address }, + }) + return data +} + +export async function fetchXrefs( + cid: string, + target: number +): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.XREFS(cid), { + params: { target }, + }) + return data +} + +export async function fetchStrings( + cid: string, + minLength: number +): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.STRINGS(cid), { + params: { min_length: minLength }, + }) + return data +} + +export async function fetchProgress(session: string): Promise { + const { data } = await apiClient.get(API_ENDPOINTS.PROGRESS, { + params: { session }, + }) + return data +} + +export async function submitAnswer( + cid: string, + body: SubmitRequest +): Promise { + const { data } = await apiClient.post( + API_ENDPOINTS.SUBMIT(cid), + body + ) + return data +} diff --git a/PROJECTS/advanced/rveng/frontend/src/api/hooks/index.ts b/PROJECTS/advanced/rveng/frontend/src/api/hooks/index.ts new file mode 100644 index 00000000..990f7fb1 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/api/hooks/index.ts @@ -0,0 +1,119 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { QUERY_KEYS } from '@/config' +import { QUERY_STRATEGIES } from '@/core/api' +import { getSessionId } from '@/lib/session' +import { + fetchCfg, + fetchChallenge, + fetchChallenges, + fetchDisasm, + fetchElf, + fetchHex, + fetchProgress, + fetchStrings, + fetchXrefs, + submitAnswer, +} from '../client' +import type { SubmitResult, Target } from '../types' + +export function useChallenges() { + return useQuery({ + queryKey: QUERY_KEYS.CHALLENGES, + queryFn: fetchChallenges, + ...QUERY_STRATEGIES.static, + }) +} + +export function useChallenge(cid: string) { + return useQuery({ + queryKey: QUERY_KEYS.CHALLENGE(cid), + queryFn: () => fetchChallenge(cid), + enabled: cid.length > 0, + ...QUERY_STRATEGIES.static, + }) +} + +export function useHex(cid: string, offset: number, length: number) { + return useQuery({ + queryKey: QUERY_KEYS.HEX(cid, offset, length), + queryFn: () => fetchHex(cid, offset, length), + enabled: cid.length > 0, + ...QUERY_STRATEGIES.static, + }) +} + +export function useElf(cid: string) { + return useQuery({ + queryKey: QUERY_KEYS.ELF(cid), + queryFn: () => fetchElf(cid), + enabled: cid.length > 0, + ...QUERY_STRATEGIES.static, + }) +} + +export function useDisasm(cid: string, target: Target | null) { + const session = getSessionId() + return useQuery({ + queryKey: QUERY_KEYS.DISASM(cid, target?.label ?? '', session), + queryFn: () => fetchDisasm(cid, target as Target, session), + enabled: cid.length > 0 && target !== null, + ...QUERY_STRATEGIES.standard, + }) +} + +export function useCfg(cid: string, target: Target | null) { + return useQuery({ + queryKey: QUERY_KEYS.CFG(cid, target?.label ?? ''), + queryFn: () => fetchCfg(cid, target as Target), + enabled: cid.length > 0 && target !== null, + ...QUERY_STRATEGIES.static, + }) +} + +export function useXrefs(cid: string, target: number | null) { + return useQuery({ + queryKey: QUERY_KEYS.XREFS(cid, target ?? -1), + queryFn: () => fetchXrefs(cid, target as number), + enabled: cid.length > 0 && target !== null, + ...QUERY_STRATEGIES.static, + }) +} + +export function useStrings(cid: string, minLength: number) { + return useQuery({ + queryKey: QUERY_KEYS.STRINGS(cid, minLength), + queryFn: () => fetchStrings(cid, minLength), + enabled: cid.length > 0, + ...QUERY_STRATEGIES.static, + }) +} + +export function useProgress() { + const session = getSessionId() + return useQuery({ + queryKey: QUERY_KEYS.PROGRESS(session), + queryFn: () => fetchProgress(session), + ...QUERY_STRATEGIES.standard, + }) +} + +export function useSubmit(cid: string) { + const queryClient = useQueryClient() + const session = getSessionId() + return useMutation({ + mutationFn: (answer: string) => submitAnswer(cid, { answer, session }), + onSuccess: (result) => { + if (result.correct) { + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.PROGRESS(session) }) + queryClient.invalidateQueries({ + queryKey: ['challenges', cid, 'disasm'], + }) + } + }, + }) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/api/index.ts b/PROJECTS/advanced/rveng/frontend/src/api/index.ts new file mode 100644 index 00000000..0840a7c1 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/api/index.ts @@ -0,0 +1,7 @@ +// =================== +// © AngelaMos | 2025 +// index.ts +// =================== + +export * from './hooks' +export * from './types' diff --git a/PROJECTS/advanced/rveng/frontend/src/api/types/index.ts b/PROJECTS/advanced/rveng/frontend/src/api/types/index.ts new file mode 100644 index 00000000..2d8dede3 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/api/types/index.ts @@ -0,0 +1,136 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +import type { Category } from '@/config' + +export interface ChallengeSummary { + id: string + module: string + title: string +} + +export interface ChallengeDetail { + id: string + module: string + title: string + mission: string + category: Category + size: number +} + +export interface HexView { + base: number + length: number + lines: string[] +} + +export interface FunctionView { + name: string + value: number + size: number +} + +export interface DiscoveredFunction { + address: number + label: string +} + +export interface SectionView { + index: number + name: string + type: string + addr: number + offset: number + size: number + flags: string +} + +export interface ElfView { + type: number + machine: number + entry: number + sections: SectionView[] + functions: FunctionView[] + discovered: DiscoveredFunction[] +} + +export interface InstructionView { + address: number + mnemonic: string + op_str: string + bytes: string + immediate: number | null + branch_target: number | null + rip_target: number | null + call_name: string | null + is_gate: boolean +} + +export interface DisasmView { + symbol: string + instructions: InstructionView[] + gate_address: number | null +} + +export interface CfgBlock { + start: number + end: number + instructions: number[] +} + +export interface CfgEdge { + src: number + dst: number + kind: string +} + +export interface CfgView { + symbol: string + blocks: CfgBlock[] + edges: CfgEdge[] +} + +export interface XrefView { + from_addr: number + to_addr: number + kind: string +} + +export interface XrefsView { + target: number + references: XrefView[] +} + +export interface Target { + label: string + address: number + symbol?: string +} + +export interface StringView { + offset: number + text: string +} + +export interface StringsView { + strings: StringView[] +} + +export interface SubmitRequest { + answer: string + session: string +} + +export interface SubmitResult { + correct: boolean + message: string + revealed_source: string | null +} + +export interface ProgressView { + session: string + solved: string[] + total: number +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.module.scss new file mode 100644 index 00000000..67f76d08 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.module.scss @@ -0,0 +1,77 @@ +// =================== +// © AngelaMos | 2026 +// AnswerRunner.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.runner { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.field { + display: flex; + flex-direction: column; + gap: $space-2; +} + +.hint { + font-size: $font-size-sm; + color: $color-text-muted; +} + +.inputRow { + display: flex; + gap: $space-2; +} + +.input { + flex: 1; + padding: $space-2 $space-3; + font-family: $font-mono; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; +} + +.inputRow button { + padding: $space-2 $space-4; + font-weight: $font-weight-medium; + color: $color-white; + background: $color-gray-900; + border: none; + border-radius: $radius-sm; + cursor: pointer; + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } +} + +.correct, +.incorrect { + padding: $space-2 $space-3; + font-size: $font-size-sm; + border-radius: $radius-sm; +} + +.correct { + color: $color-gray-900; + background: $color-gray-200; + border: 1px solid $color-gray-400; +} + +.incorrect { + color: $color-gray-700; + background: $color-gray-100; + border: 1px solid $color-border; +} + +.solvedNote { + font-size: $font-size-sm; + color: $color-text-muted; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.tsx b/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.tsx new file mode 100644 index 00000000..12faff6d --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/AnswerRunner.tsx @@ -0,0 +1,75 @@ +// =================== +// © AngelaMos | 2026 +// AnswerRunner.tsx +// =================== + +import { type FormEvent, useState } from 'react' +import { useSubmit } from '@/api/hooks' +import type { SubmitResult } from '@/api/types' +import { CATEGORY_HINT, type Category } from '@/config' +import styles from './AnswerRunner.module.scss' +import { SourceReveal } from './SourceReveal' + +interface AnswerRunnerProps { + cid: string + category: Category + solved: boolean +} + +export function AnswerRunner({ + cid, + category, + solved, +}: AnswerRunnerProps): React.ReactElement { + const [answer, setAnswer] = useState('') + const [result, setResult] = useState(null) + const submit = useSubmit(cid) + + const onSubmit = (event: FormEvent): void => { + event.preventDefault() + if (answer.trim().length === 0) { + return + } + submit.mutate(answer, { onSuccess: (data) => setResult(data) }) + } + + const pending = submit.isPending + const empty = answer.trim().length === 0 + + return ( +
+ + + {result && ( +
+ {result.message} +
+ )} + + {result?.correct && result.revealed_source && ( + + )} + + {solved && result === null && ( +

+ Already solved. Submit again to reveal the source. +

+ )} + + ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.module.scss new file mode 100644 index 00000000..0a368472 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.module.scss @@ -0,0 +1,95 @@ +// =================== +// © AngelaMos | 2026 +// CfgPane.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.cfg { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.summary { + font-size: $font-size-sm; + color: $color-text-muted; +} + +.blocks { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.block { + display: flex; + flex-direction: column; + gap: $space-2; + padding: $space-3 $space-4; + background: $color-surface; + border: 1px solid $color-border; + border-left: 3px solid $color-gray-700; + border-radius: $radius-sm; +} + +.blockHead { + display: flex; + align-items: baseline; + gap: $space-3; +} + +.blockName { + font-weight: $font-weight-semibold; +} + +.blockRange { + font-family: $font-mono; + font-size: $font-size-sm; + color: $color-text-muted; +} + +.blockBody { + font-size: $font-size-sm; + color: $color-text-muted; +} + +.edgeRow { + display: flex; + flex-wrap: wrap; + gap: $space-2; +} + +.edge { + padding: $space-1 $space-2; + font-family: $font-mono; + font-size: $font-size-xs; + border: 1px solid $color-border; + border-radius: $radius-full; + color: $color-text; +} + +.taken { + border-color: $color-gray-600; + background: $color-gray-100; +} + +.fallthrough { + color: $color-text-muted; +} + +.jump { + border-color: $color-gray-500; +} + +.terminal { + font-size: $font-size-xs; + color: $color-text-muted; +} + +.state { + padding: $space-4; + color: $color-text-muted; + font-size: $font-size-sm; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.tsx b/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.tsx new file mode 100644 index 00000000..cfd48960 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/CfgPane.tsx @@ -0,0 +1,79 @@ +// =================== +// © AngelaMos | 2026 +// CfgPane.tsx +// =================== + +import { useMemo } from 'react' +import { useCfg } from '@/api/hooks' +import type { Target } from '@/api/types' +import { formatAddr } from '@/lib/format' +import styles from './CfgPane.module.scss' + +interface CfgPaneProps { + cid: string + target: Target | null +} + +export function CfgPane({ cid, target }: CfgPaneProps): React.ReactElement { + const { data, isLoading, isError } = useCfg(cid, target) + + const outEdges = useMemo(() => { + const map = new Map() + for (const edge of data?.edges ?? []) { + const list = map.get(edge.src) ?? [] + list.push({ dst: edge.dst, kind: edge.kind }) + map.set(edge.src, list) + } + return map + }, [data]) + + if (target === null) { + return
No function selected
+ } + if (isLoading) { + return
Building graph...
+ } + if (isError || !data) { + return
Failed to build graph
+ } + + return ( +
+

+ {data.blocks.length} basic blocks, {data.edges.length} edges +

+
+ {data.blocks.map((block, index) => { + const edges = outEdges.get(block.start) ?? [] + return ( +
+
+ block {index} + + {formatAddr(block.start)} .. {formatAddr(block.end)} + +
+
+ {block.instructions.length} instructions +
+
+ {edges.length === 0 ? ( + terminal + ) : ( + edges.map((edge) => ( + + {edge.kind} to {formatAddr(edge.dst)} + + )) + )} +
+
+ ) + })} +
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.module.scss new file mode 100644 index 00000000..50fef78a --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.module.scss @@ -0,0 +1,60 @@ +// =================== +// © AngelaMos | 2026 +// ChallengeCard.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.card { + display: flex; + flex-direction: column; + gap: $space-2; + padding: $space-4; + background: $color-bg; + border: 1px solid $color-border; + border-radius: $radius-md; + text-decoration: none; + color: inherit; + transition: + border-color $duration-fast $ease, + transform $duration-fast $ease; + + &:hover { + border-color: $color-gray-400; + transform: translateY(-2px); + } +} + +.top { + display: flex; + align-items: center; + justify-content: space-between; +} + +.module { + font-size: $font-size-xs; + text-transform: uppercase; + letter-spacing: 0.08em; + color: $color-text-muted; +} + +.solved { + padding: $space-1 $space-2; + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + color: $color-gray-900; + background: $color-gray-200; + border-radius: $radius-full; +} + +.title { + font-size: $font-size-lg; + font-weight: $font-weight-semibold; +} + +.id { + font-family: $font-mono; + font-size: $font-size-xs; + color: $color-text-muted; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.tsx b/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.tsx new file mode 100644 index 00000000..10f751b1 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/ChallengeCard.tsx @@ -0,0 +1,30 @@ +// =================== +// © AngelaMos | 2026 +// ChallengeCard.tsx +// =================== + +import { Link } from 'react-router-dom' +import type { ChallengeSummary } from '@/api/types' +import { ROUTES } from '@/config' +import styles from './ChallengeCard.module.scss' + +interface ChallengeCardProps { + challenge: ChallengeSummary + solved: boolean +} + +export function ChallengeCard({ + challenge, + solved, +}: ChallengeCardProps): React.ReactElement { + return ( + +
+ {challenge.module} + {solved && Solved} +
+

{challenge.title}

+ {challenge.id} + + ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.module.scss new file mode 100644 index 00000000..d8e8bbea --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.module.scss @@ -0,0 +1,133 @@ +// =================== +// © AngelaMos | 2026 +// DisasmPane.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.disasm { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: $space-4; +} + +.picker { + display: flex; + align-items: center; + gap: $space-2; + font-size: $font-size-sm; + color: $color-text-muted; + + select { + padding: $space-1 $space-2; + font-family: $font-mono; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; + background: $color-bg; + } +} + +.gateBanner { + padding: $space-1 $space-3; + font-family: $font-mono; + font-size: $font-size-sm; + color: $color-gray-900; + background: $color-gray-200; + border: 1px solid $color-gray-400; + border-radius: $radius-sm; + cursor: pointer; +} + +.listing { + overflow-x: auto; + padding: $space-2; + background: $color-gray-900; + border-radius: $radius-md; + font-family: $font-mono; + font-size: $font-size-sm; +} + +.row { + display: grid; + grid-template-columns: 6rem 9rem 4rem 1fr auto; + gap: $space-3; + align-items: center; + padding: $space-1 $space-2; + white-space: pre; + border-radius: $radius-sm; +} + +.gate { + background: $color-gray-700; + outline: 1px solid $color-gray-400; +} + +.addr { + color: $color-gray-500; +} + +.raw { + color: $color-gray-600; + overflow: hidden; + text-overflow: ellipsis; +} + +.mnemonic { + color: $color-white; + font-weight: $font-weight-semibold; +} + +.ops { + color: $color-gray-300; +} + +.callName { + color: $color-gray-100; + font-weight: $font-weight-semibold; +} + +.dataRef { + color: $color-gray-500; +} + +.callers { + font-family: $font-mono; + font-size: $font-size-sm; + color: $color-text-muted; +} + +.target { + justify-self: end; + padding: 0 $space-2; + font-family: $font-mono; + font-size: $font-size-xs; + color: $color-gray-400; + background: transparent; + border: 1px solid transparent; + border-radius: $radius-sm; +} + +.linkable { + color: $color-gray-100; + border-color: $color-gray-600; + cursor: pointer; + + &:hover { + background: $color-gray-800; + } +} + +.state { + padding: $space-4; + color: $color-text-muted; + font-size: $font-size-sm; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.tsx b/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.tsx new file mode 100644 index 00000000..a0dac146 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/DisasmPane.tsx @@ -0,0 +1,112 @@ +// =================== +// © AngelaMos | 2026 +// DisasmPane.tsx +// =================== + +import { useMemo, useRef } from 'react' +import { useDisasm, useXrefs } from '@/api/hooks' +import type { Target } from '@/api/types' +import { formatAddr } from '@/lib/format' +import styles from './DisasmPane.module.scss' + +interface DisasmPaneProps { + cid: string + target: Target | null +} + +export function DisasmPane({ cid, target }: DisasmPaneProps): React.ReactElement { + const { data, isLoading, isError } = useDisasm(cid, target) + const xrefs = useXrefs(cid, target?.address ?? null) + const rowRefs = useRef>(new Map()) + + const addresses = useMemo( + () => new Set((data?.instructions ?? []).map((i) => i.address)), + [data] + ) + + if (target === null) { + return
No function selected
+ } + + const scrollTo = (address: number): void => { + rowRefs.current.get(address)?.scrollIntoView({ + block: 'center', + behavior: 'smooth', + }) + } + + const callers = (xrefs.data?.references ?? []).filter((r) => r.kind === 'call') + + return ( +
+
+ {data?.gate_address != null && ( + + )} + {callers.length > 0 && ( + + called by {callers.map((c) => formatAddr(c.from_addr)).join(', ')} + + )} +
+ + {isLoading &&
Disassembling...
} + {isError &&
Failed to disassemble
} + + {data && ( +
+ {data.instructions.map((ins) => { + const linkable = + ins.branch_target != null && addresses.has(ins.branch_target) + return ( +
{ + rowRefs.current.set(ins.address, el) + }} + className={`${styles.row} ${ins.is_gate ? styles.gate : ''}`} + > + {formatAddr(ins.address)} + {ins.bytes} + {ins.mnemonic} + + {ins.op_str} + {ins.call_name && ( + + {' '} + <{ins.call_name}> + + )} + {ins.rip_target != null && ( + + {' '} + # {formatAddr(ins.rip_target)} + + )} + + {ins.branch_target != null && ( + + )} +
+ ) + })} +
+ )} +
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.module.scss new file mode 100644 index 00000000..e009ee96 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.module.scss @@ -0,0 +1,108 @@ +// =================== +// © AngelaMos | 2026 +// HexViewer.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.hex { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: $space-4; +} + +.pager { + display: flex; + gap: $space-2; + + button { + padding: $space-1 $space-3; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; + background: $color-surface; + color: $color-text; + cursor: pointer; + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } + } +} + +.range { + font-family: $font-mono; + font-size: $font-size-sm; + color: $color-text; +} + +.total { + color: $color-text-muted; +} + +.jump { + display: flex; + gap: $space-2; + margin-left: auto; + + input { + width: 12rem; + padding: $space-1 $space-2; + font-family: $font-mono; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; + } + + button { + padding: $space-1 $space-3; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; + background: $color-surface; + cursor: pointer; + } +} + +.dump { + overflow-x: auto; + padding: $space-3; + background: $color-gray-900; + border-radius: $radius-md; + font-family: $font-mono; + font-size: $font-size-sm; + line-height: $line-height-normal; +} + +.row { + display: flex; + gap: $space-4; + white-space: pre; +} + +.gutter { + color: $color-gray-500; +} + +.bytes { + color: $color-gray-100; +} + +.ascii { + color: $color-gray-400; +} + +.state { + padding: $space-4; + color: $color-text-muted; + font-size: $font-size-sm; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.tsx b/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.tsx new file mode 100644 index 00000000..6b3ef5ae --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/HexViewer.tsx @@ -0,0 +1,122 @@ +// =================== +// © AngelaMos | 2026 +// HexViewer.tsx +// =================== + +import { type FormEvent, useState } from 'react' +import { useHex } from '@/api/hooks' +import { HEX } from '@/config' +import { formatOffset } from '@/lib/format' +import styles from './HexViewer.module.scss' + +interface HexRow { + offset: string + bytes: string + ascii: string + raw: string +} + +const LINE_RE = /^([0-9a-f]{8}) {2}(.+?) {2}\|(.*)\|$/ + +function parseLine(line: string): HexRow { + const match = LINE_RE.exec(line) + if (match === null) { + return { offset: '', bytes: '', ascii: '', raw: line } + } + return { offset: match[1], bytes: match[2], ascii: match[3], raw: line } +} + +function parseTarget(text: string): number | null { + const token = text.trim().toLowerCase() + if (token.length === 0) { + return null + } + const value = token.startsWith('0x') + ? Number.parseInt(token.slice(2), 16) + : Number.parseInt(token, 10) + return Number.isNaN(value) || value < 0 ? null : value +} + +interface HexViewerProps { + cid: string + size: number +} + +export function HexViewer({ cid, size }: HexViewerProps): React.ReactElement { + const [offset, setOffset] = useState(0) + const [jump, setJump] = useState('') + const length = HEX.PAGE_LENGTH + const { data, isLoading, isError } = useHex(cid, offset, length) + + const canPrev = offset > 0 + const canNext = offset + length < size + + const gotoPrev = (): void => setOffset(Math.max(0, offset - length)) + const gotoNext = (): void => { + if (canNext) { + setOffset(offset + length) + } + } + + const onJump = (event: FormEvent): void => { + event.preventDefault() + const target = parseTarget(jump) + if (target === null) { + return + } + const aligned = Math.min(target, Math.max(0, size - 1)) + setOffset(aligned - (aligned % HEX.BYTES_PER_LINE)) + } + + return ( +
+
+
+ + +
+ + {data + ? `${formatOffset(data.base)} .. ${formatOffset(data.base + data.length)}` + : formatOffset(offset)} + of {size} bytes + +
+ setJump(e.target.value)} + placeholder="offset e.g. 0x2000" + aria-label="Jump to offset" + /> + +
+
+ + {isLoading &&
Loading hex...
} + {isError &&
Failed to load hex
} + + {data && ( +
+ {data.lines.map(parseLine).map((row) => ( +
+ {row.offset ? ( + <> + {row.offset} + {row.bytes} + {row.ascii} + + ) : ( + {row.raw} + )} +
+ ))} +
+ )} +
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.module.scss new file mode 100644 index 00000000..42a666be --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.module.scss @@ -0,0 +1,65 @@ +// =================== +// © AngelaMos | 2026 +// MissionPanel.module.scss +// =================== + +@use '../styles/tokens' as *; + +.panel { + display: flex; + flex-direction: column; + gap: $space-3; + padding: $space-5; + background: $color-surface; + border: 1px solid $color-border; + border-radius: $radius-md; +} + +.head { + display: flex; + align-items: center; + justify-content: space-between; +} + +.module { + font-size: $font-size-xs; + text-transform: uppercase; + letter-spacing: 0.08em; + color: $color-text-muted; +} + +.solved { + padding: $space-1 $space-2; + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + color: $color-gray-900; + background: $color-gray-200; + border-radius: $radius-full; +} + +.title { + font-size: $font-size-xl; + font-weight: $font-weight-semibold; +} + +.mission { + color: $color-text; + line-height: $line-height-relaxed; +} + +.meta { + display: flex; + gap: $space-3; + font-size: $font-size-sm; +} + +.category { + padding: $space-1 $space-2; + color: $color-text; + background: $color-gray-100; + border-radius: $radius-sm; +} + +.size { + color: $color-text-muted; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.tsx b/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.tsx new file mode 100644 index 00000000..27b39680 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/MissionPanel.tsx @@ -0,0 +1,36 @@ +// =================== +// © AngelaMos | 2026 +// MissionPanel.tsx +// =================== + +import type { ChallengeDetail } from '@/api/types' +import { CATEGORY_LABEL } from '@/config' +import { formatBytes } from '@/lib/format' +import styles from './MissionPanel.module.scss' + +interface MissionPanelProps { + challenge: ChallengeDetail + solved: boolean +} + +export function MissionPanel({ + challenge, + solved, +}: MissionPanelProps): React.ReactElement { + return ( +
+
+ {challenge.module} + {solved && Solved} +
+

{challenge.title}

+

{challenge.mission}

+
+ + {CATEGORY_LABEL[challenge.category]} + + {formatBytes(challenge.size)} +
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.module.scss new file mode 100644 index 00000000..51a2fe34 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.module.scss @@ -0,0 +1,33 @@ +// =================== +// © AngelaMos | 2026 +// ProgressBar.module.scss +// =================== + +@use '../styles/tokens' as *; + +.wrap { + display: flex; + flex-direction: column; + gap: $space-2; +} + +.label { + display: flex; + justify-content: space-between; + font-size: $font-size-sm; + color: $color-text-muted; +} + +.track { + height: 0.5rem; + background: $color-gray-200; + border-radius: $radius-full; + overflow: hidden; +} + +.fill { + height: 100%; + background: $color-gray-700; + border-radius: $radius-full; + transition: width $duration-normal $ease; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.tsx b/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.tsx new file mode 100644 index 00000000..eb7d2fc1 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/ProgressBar.tsx @@ -0,0 +1,31 @@ +// =================== +// © AngelaMos | 2026 +// ProgressBar.tsx +// =================== + +import styles from './ProgressBar.module.scss' + +interface ProgressBarProps { + solved: number + total: number +} + +export function ProgressBar({ + solved, + total, +}: ProgressBarProps): React.ReactElement { + const pct = total > 0 ? Math.round((solved / total) * 100) : 0 + return ( +
+
+ Progress + + {solved} / {total} + +
+
+
+
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.module.scss new file mode 100644 index 00000000..05043f74 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.module.scss @@ -0,0 +1,67 @@ +// =================== +// © AngelaMos | 2026 +// SectionMap.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.wrap { + overflow-x: auto; + border: 1px solid $color-border; + border-radius: $radius-md; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: $font-size-sm; + + th, + td { + padding: $space-2 $space-3; + text-align: left; + border-bottom: 1px solid $color-border; + } + + th { + font-weight: $font-weight-semibold; + color: $color-text-muted; + background: $color-surface; + } + + tbody tr:last-child td { + border-bottom: none; + } +} + +.exec { + background: $color-gray-50; + + .name { + font-weight: $font-weight-semibold; + } +} + +.name { + font-family: $font-mono; +} + +.mono { + font-family: $font-mono; + color: $color-text-muted; +} + +.num { + text-align: right; + font-variant-numeric: tabular-nums; +} + +.dim { + color: $color-text-muted; +} + +.flags { + font-family: $font-mono; + letter-spacing: 0.1em; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.tsx b/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.tsx new file mode 100644 index 00000000..64fb86c3 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/SectionMap.tsx @@ -0,0 +1,48 @@ +// =================== +// © AngelaMos | 2026 +// SectionMap.tsx +// =================== + +import type { SectionView } from '@/api/types' +import { formatAddr, formatOffset } from '@/lib/format' +import styles from './SectionMap.module.scss' + +export function SectionMap({ + sections, +}: { + sections: SectionView[] +}): React.ReactElement { + return ( +
+ + + + + + + + + + + + + + {sections.map((section) => ( + + + + + + + + + + ))} + +
#NameTypeAddrOffsetSizeFlags
{section.index}{section.name || '(null)'}{section.type}{formatAddr(section.addr)}{formatOffset(section.offset)}{section.size}{section.flags || '-'}
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.module.scss new file mode 100644 index 00000000..7f3ea36f --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.module.scss @@ -0,0 +1,39 @@ +// =================== +// © AngelaMos | 2026 +// SourceReveal.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.reveal { + display: flex; + flex-direction: column; + gap: $space-2; + padding: $space-4; + background: $color-gray-900; + border-radius: $radius-md; +} + +.heading { + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $color-gray-50; +} + +.note { + font-size: $font-size-sm; + color: $color-gray-400; +} + +.code { + overflow-x: auto; + padding: $space-3; + background: $color-black; + border-radius: $radius-sm; + font-family: $font-mono; + font-size: $font-size-sm; + line-height: $line-height-normal; + color: $color-gray-100; + white-space: pre; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.tsx b/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.tsx new file mode 100644 index 00000000..fb555399 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/SourceReveal.tsx @@ -0,0 +1,18 @@ +// =================== +// © AngelaMos | 2026 +// SourceReveal.tsx +// =================== + +import styles from './SourceReveal.module.scss' + +export function SourceReveal({ source }: { source: string }): React.ReactElement { + return ( +
+

Source revealed

+

+ You reached the answer from the binary. Here is the C that produced it. +

+
{source}
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.module.scss new file mode 100644 index 00000000..d46e7175 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.module.scss @@ -0,0 +1,74 @@ +// =================== +// © AngelaMos | 2026 +// StringsPane.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/fonts' as *; + +.strings { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.toolbar { + display: flex; + align-items: center; + gap: $space-4; +} + +.control { + display: flex; + align-items: center; + gap: $space-3; + font-size: $font-size-sm; + color: $color-text-muted; + + input { + width: 12rem; + } +} + +.count { + margin-left: auto; + font-size: $font-size-sm; + color: $color-text-muted; +} + +.list { + max-height: 28rem; + overflow-y: auto; + border: 1px solid $color-border; + border-radius: $radius-md; + font-family: $font-mono; + font-size: $font-size-sm; +} + +.row { + display: grid; + grid-template-columns: 8rem 1fr; + gap: $space-4; + padding: $space-1 $space-3; + border-bottom: 1px solid $color-border; + + &:last-child { + border-bottom: none; + } +} + +.offset { + color: $color-text-muted; +} + +.text { + color: $color-text; + white-space: pre-wrap; + word-break: break-all; +} + +.state { + padding: $space-4; + color: $color-text-muted; + font-size: $font-size-sm; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.tsx b/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.tsx new file mode 100644 index 00000000..407fae2b --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/StringsPane.tsx @@ -0,0 +1,49 @@ +// =================== +// © AngelaMos | 2026 +// StringsPane.tsx +// =================== + +import { useState } from 'react' +import { useStrings } from '@/api/hooks' +import { STRINGS } from '@/config' +import { formatOffset } from '@/lib/format' +import styles from './StringsPane.module.scss' + +export function StringsPane({ cid }: { cid: string }): React.ReactElement { + const [minLength, setMinLength] = useState(STRINGS.DEFAULT_MIN_LENGTH) + const { data, isLoading, isError } = useStrings(cid, minLength) + + return ( +
+
+ + {data && ( + {data.strings.length} strings + )} +
+ + {isLoading &&
Loading strings...
} + {isError &&
Failed to load strings
} + + {data && ( +
+ {data.strings.map((entry) => ( +
+ {formatOffset(entry.offset)} + {entry.text} +
+ ))} +
+ )} +
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/Workspace.module.scss b/PROJECTS/advanced/rveng/frontend/src/components/Workspace.module.scss new file mode 100644 index 00000000..ed1e7ab3 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/Workspace.module.scss @@ -0,0 +1,104 @@ +// =================== +// © AngelaMos | 2026 +// Workspace.module.scss +// =================== + +@use '../styles/tokens' as *; +@use '../styles/mixins' as *; + +.workspace { + display: grid; + grid-template-columns: minmax(20rem, 24rem) 1fr; + gap: $space-6; + align-items: start; + + @include breakpoint-down('lg') { + grid-template-columns: 1fr; + } +} + +.side { + display: flex; + flex-direction: column; + gap: $space-4; + position: sticky; + top: $space-4; + + @include breakpoint-down('lg') { + position: static; + } +} + +.runnerCard { + display: flex; + flex-direction: column; + gap: $space-3; + padding: $space-5; + background: $color-bg; + border: 1px solid $color-border; + border-radius: $radius-md; +} + +.runnerTitle { + font-size: $font-size-base; + font-weight: $font-weight-semibold; +} + +.analysis { + display: flex; + flex-direction: column; + gap: $space-4; + min-width: 0; +} + +.tabs { + display: flex; + gap: $space-1; + border-bottom: 1px solid $color-border; +} + +.tab { + padding: $space-2 $space-4; + font-size: $font-size-sm; + color: $color-text-muted; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + + &:hover { + color: $color-text; + } +} + +.activeTab { + color: $color-text; + font-weight: $font-weight-medium; + border-bottom-color: $color-gray-900; +} + +.picker { + display: flex; + align-items: center; + gap: $space-2; + font-size: $font-size-sm; + color: $color-text-muted; + + select { + padding: $space-1 $space-2; + font-size: $font-size-sm; + border: 1px solid $color-border; + border-radius: $radius-sm; + background: $color-bg; + } +} + +.pane { + min-width: 0; +} + +.state { + padding: $space-6; + color: $color-text-muted; + font-size: $font-size-sm; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/Workspace.tsx b/PROJECTS/advanced/rveng/frontend/src/components/Workspace.tsx new file mode 100644 index 00000000..48aa37b7 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/Workspace.tsx @@ -0,0 +1,139 @@ +// =================== +// © AngelaMos | 2026 +// Workspace.tsx +// =================== + +import { useMemo, useState } from 'react' +import { useChallenge, useElf, useProgress } from '@/api/hooks' +import type { Target } from '@/api/types' +import { AnswerRunner } from './AnswerRunner' +import { CfgPane } from './CfgPane' +import { DisasmPane } from './DisasmPane' +import { HexViewer } from './HexViewer' +import { MissionPanel } from './MissionPanel' +import { SectionMap } from './SectionMap' +import { StringsPane } from './StringsPane' +import styles from './Workspace.module.scss' + +const TABS = ['hex', 'disasm', 'graph', 'sections', 'strings'] as const +type Tab = (typeof TABS)[number] + +const TAB_LABEL: Record = { + hex: 'Hex', + disasm: 'Disassembly', + graph: 'Graph', + sections: 'Sections', + strings: 'Strings', +} + +const PREFERRED = ['check', 'main', '_start'] + +function pickDefault(targets: Target[]): Target | null { + for (const name of PREFERRED) { + const hit = targets.find((t) => t.label === name) + if (hit) { + return hit + } + } + return targets[0] ?? null +} + +export function Workspace({ cid }: { cid: string }): React.ReactElement { + const [tab, setTab] = useState('hex') + const [targetKey, setTargetKey] = useState('') + const challenge = useChallenge(cid) + const elf = useElf(cid) + const progress = useProgress() + + const targets = useMemo(() => { + const named = (elf.data?.functions ?? []) + .filter((f) => f.size > 0 && f.value > 0) + .map((f) => ({ label: f.name, address: f.value, symbol: f.name })) + if (named.length > 0) { + return named + } + return (elf.data?.discovered ?? []).map((d) => ({ + label: d.label, + address: d.address, + })) + }, [elf.data]) + + if (challenge.isLoading) { + return
Loading challenge...
+ } + if (challenge.isError || !challenge.data) { + return
Challenge not found
+ } + + const detail = challenge.data + const solved = progress.data?.solved.includes(cid) ?? false + const selected = + targets.find((t) => t.label === targetKey) ?? pickDefault(targets) + const needsTarget = tab === 'disasm' || tab === 'graph' + + return ( +
+ + +
+
+ {TABS.map((item) => ( + + ))} +
+ + {needsTarget && targets.length > 0 && ( + + )} + +
+ {tab === 'hex' && } + {tab === 'disasm' && + (elf.isLoading ? ( +
Loading ELF...
+ ) : ( + + ))} + {tab === 'graph' && + (elf.isLoading ? ( +
Loading ELF...
+ ) : ( + + ))} + {tab === 'sections' && + (elf.isLoading ? ( +
Loading ELF...
+ ) : ( + + ))} + {tab === 'strings' && } +
+
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/components/index.tsx b/PROJECTS/advanced/rveng/frontend/src/components/index.tsx new file mode 100644 index 00000000..6b55e178 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/components/index.tsx @@ -0,0 +1,16 @@ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +export * from './AnswerRunner' +export * from './CfgPane' +export * from './ChallengeCard' +export * from './DisasmPane' +export * from './HexViewer' +export * from './MissionPanel' +export * from './ProgressBar' +export * from './SectionMap' +export * from './SourceReveal' +export * from './StringsPane' +export * from './Workspace' diff --git a/PROJECTS/advanced/rveng/frontend/src/config.ts b/PROJECTS/advanced/rveng/frontend/src/config.ts new file mode 100644 index 00000000..abc77f62 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/config.ts @@ -0,0 +1,102 @@ +// =================== +// © AngelaMos | 2026 +// config.ts +// =================== + +export const API_ENDPOINTS = { + CHALLENGES: '/challenges', + CHALLENGE: (cid: string) => `/challenges/${cid}`, + HEX: (cid: string) => `/challenges/${cid}/hex`, + ELF: (cid: string) => `/challenges/${cid}/elf`, + DISASM: (cid: string) => `/challenges/${cid}/disasm`, + CFG: (cid: string) => `/challenges/${cid}/cfg`, + XREFS: (cid: string) => `/challenges/${cid}/xrefs`, + STRINGS: (cid: string) => `/challenges/${cid}/strings`, + SUBMIT: (cid: string) => `/challenges/${cid}/submit`, + PROGRESS: '/progress', +} as const + +export const QUERY_KEYS = { + CHALLENGES: ['challenges'] as const, + CHALLENGE: (cid: string) => ['challenges', cid] as const, + HEX: (cid: string, offset: number, length: number) => + ['challenges', cid, 'hex', offset, length] as const, + ELF: (cid: string) => ['challenges', cid, 'elf'] as const, + DISASM: (cid: string, label: string, session: string) => + ['challenges', cid, 'disasm', label, session] as const, + CFG: (cid: string, label: string) => ['challenges', cid, 'cfg', label] as const, + XREFS: (cid: string, target: number) => + ['challenges', cid, 'xrefs', target] as const, + STRINGS: (cid: string, minLength: number) => + ['challenges', cid, 'strings', minLength] as const, + PROGRESS: (session: string) => ['progress', session] as const, +} as const + +export const ROUTES = { + HOME: '/', + CHALLENGE: '/c/:cid', + challenge: (cid: string) => `/c/${cid}`, +} as const + +export const STORAGE_KEYS = { + UI: 'rveng-ui', + SESSION: 'rveng-session', +} as const + +export const CATEGORY = { + FOUND_VALUE: 'found_value', + IDENTIFIED_SYMBOL: 'identified_symbol', + PATCHED_BYTES: 'patched_bytes', +} as const + +export type Category = (typeof CATEGORY)[keyof typeof CATEGORY] + +export const CATEGORY_LABEL: Record = { + [CATEGORY.FOUND_VALUE]: 'Find a value', + [CATEGORY.IDENTIFIED_SYMBOL]: 'Name a symbol', + [CATEGORY.PATCHED_BYTES]: 'Patch bytes', +} + +export const CATEGORY_HINT: Record = { + [CATEGORY.FOUND_VALUE]: 'Submit the value in decimal or hex (0x...)', + [CATEGORY.IDENTIFIED_SYMBOL]: 'Submit the exact symbol name', + [CATEGORY.PATCHED_BYTES]: 'Submit the replacement bytes as hex (e.g. 9090)', +} + +export const HEX = { + DEFAULT_LENGTH: 256, + PAGE_LENGTH: 256, + MAX_LENGTH: 4096, + BYTES_PER_LINE: 16, +} as const + +export const STRINGS = { + DEFAULT_MIN_LENGTH: 4, + MIN_MIN_LENGTH: 1, + MAX_MIN_LENGTH: 64, +} as const + +export const QUERY_CONFIG = { + STALE_TIME: { + STATIC: Number.POSITIVE_INFINITY, + DEFAULT: 1000 * 60 * 5, + FREQUENT: 1000 * 30, + }, + GC_TIME: { + DEFAULT: 1000 * 60 * 30, + LONG: 1000 * 60 * 60, + }, + RETRY: { + DEFAULT: 2, + NONE: 0, + }, +} as const + +export const HTTP_STATUS = { + OK: 200, + BAD_REQUEST: 400, + NOT_FOUND: 404, + PAYLOAD_TOO_LARGE: 413, + UNPROCESSABLE: 422, + INTERNAL_SERVER: 500, +} as const diff --git a/PROJECTS/advanced/rveng/frontend/src/core/api/api.config.ts b/PROJECTS/advanced/rveng/frontend/src/core/api/api.config.ts new file mode 100644 index 00000000..9f8bc734 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/api/api.config.ts @@ -0,0 +1,22 @@ +// =================== +// © AngelaMos | 2026 +// api.config.ts +// =================== + +import axios, { type AxiosInstance } from 'axios' +import { transformAxiosError } from './errors' + +const getBaseURL = (): string => { + return import.meta.env.VITE_API_URL ?? '/api' +} + +export const apiClient: AxiosInstance = axios.create({ + baseURL: getBaseURL(), + timeout: 15000, + headers: { 'Content-Type': 'application/json' }, +}) + +apiClient.interceptors.response.use( + (response) => response, + (error) => Promise.reject(transformAxiosError(error)) +) diff --git a/PROJECTS/advanced/rveng/frontend/src/core/api/errors.ts b/PROJECTS/advanced/rveng/frontend/src/core/api/errors.ts new file mode 100644 index 00000000..26982793 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/api/errors.ts @@ -0,0 +1,116 @@ +/** + * ©AngelaMos | 2025 + * errors.ts + */ + +import type { AxiosError } from 'axios' + +export const ApiErrorCode = { + NETWORK_ERROR: 'NETWORK_ERROR', + VALIDATION_ERROR: 'VALIDATION_ERROR', + AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR', + AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR', + NOT_FOUND: 'NOT_FOUND', + CONFLICT: 'CONFLICT', + RATE_LIMITED: 'RATE_LIMITED', + SERVER_ERROR: 'SERVER_ERROR', + UNKNOWN_ERROR: 'UNKNOWN_ERROR', +} as const + +export type ApiErrorCode = (typeof ApiErrorCode)[keyof typeof ApiErrorCode] + +export class ApiError extends Error { + readonly code: ApiErrorCode + readonly statusCode: number + readonly details?: Record + + constructor( + message: string, + code: ApiErrorCode, + statusCode: number, + details?: Record + ) { + super(message) + this.name = 'ApiError' + this.code = code + this.statusCode = statusCode + this.details = details + } + + getUserMessage(): string { + const messages: Record = { + [ApiErrorCode.NETWORK_ERROR]: + 'Unable to connect. Please check your internet connection.', + [ApiErrorCode.VALIDATION_ERROR]: 'Please check your input and try again.', + [ApiErrorCode.AUTHENTICATION_ERROR]: + 'Your session has expired. Please log in again.', + [ApiErrorCode.AUTHORIZATION_ERROR]: + 'You do not have permission to perform this action.', + [ApiErrorCode.NOT_FOUND]: 'The requested resource was not found.', + [ApiErrorCode.CONFLICT]: + 'This operation conflicts with an existing resource.', + [ApiErrorCode.RATE_LIMITED]: + 'Too many requests. Please wait a moment and try again.', + [ApiErrorCode.SERVER_ERROR]: + 'Something went wrong on our end. Please try again later.', + [ApiErrorCode.UNKNOWN_ERROR]: + 'An unexpected error occurred. Please try again.', + } + return messages[this.code] + } +} + +interface ApiErrorResponse { + detail?: string | { msg: string; type: string }[] + message?: string +} + +export function transformAxiosError(error: AxiosError): ApiError { + if (!error.response) { + return new ApiError('Network error', ApiErrorCode.NETWORK_ERROR, 0) + } + + const { status } = error.response + const data = error.response.data as ApiErrorResponse | undefined + let message = 'An error occurred' + let details: Record | undefined + + if (data?.detail) { + if (typeof data.detail === 'string') { + message = data.detail + } else if (Array.isArray(data.detail)) { + details = { validation: [] } + data.detail.forEach((err) => { + details?.validation.push(err.msg) + }) + message = 'Validation error' + } + } else if (data?.message) { + message = data.message + } + + const codeMap: Record = { + 400: ApiErrorCode.VALIDATION_ERROR, + 401: ApiErrorCode.AUTHENTICATION_ERROR, + 403: ApiErrorCode.AUTHORIZATION_ERROR, + 404: ApiErrorCode.NOT_FOUND, + 409: ApiErrorCode.CONFLICT, + 413: ApiErrorCode.VALIDATION_ERROR, + 422: ApiErrorCode.VALIDATION_ERROR, + 429: ApiErrorCode.RATE_LIMITED, + 500: ApiErrorCode.SERVER_ERROR, + 502: ApiErrorCode.SERVER_ERROR, + 503: ApiErrorCode.SERVER_ERROR, + 504: ApiErrorCode.SERVER_ERROR, + } + + const code = codeMap[status] || ApiErrorCode.UNKNOWN_ERROR + + return new ApiError(message, code, status, details) +} + +declare module '@tanstack/react-query' { + interface Register { + defaultError: ApiError + } +} diff --git a/PROJECTS/advanced/rveng/frontend/src/core/api/index.ts b/PROJECTS/advanced/rveng/frontend/src/core/api/index.ts new file mode 100644 index 00000000..1818cd7e --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/api/index.ts @@ -0,0 +1,8 @@ +// =================== +// © AngelaMos | 2025 +// index.ts +// =================== + +export * from './api.config' +export * from './errors' +export * from './query.config' diff --git a/PROJECTS/advanced/rveng/frontend/src/core/api/query.config.ts b/PROJECTS/advanced/rveng/frontend/src/core/api/query.config.ts new file mode 100644 index 00000000..d6590232 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/api/query.config.ts @@ -0,0 +1,93 @@ +// =================== +// © AngelaMos | 2026 +// query.config.ts +// =================== + +import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' +import { QUERY_CONFIG } from '@/config' +import { ApiError, ApiErrorCode } from './errors' + +const NO_RETRY_ERROR_CODES: readonly ApiErrorCode[] = [ + ApiErrorCode.NOT_FOUND, + ApiErrorCode.VALIDATION_ERROR, +] as const + +const shouldRetryQuery = (failureCount: number, error: Error): boolean => { + if (error instanceof ApiError) { + if (NO_RETRY_ERROR_CODES.includes(error.code)) { + return false + } + } + return failureCount < QUERY_CONFIG.RETRY.DEFAULT +} + +const calculateRetryDelay = (attemptIndex: number): number => { + const baseDelay = 500 + const maxDelay = 5000 + return Math.min(baseDelay * 2 ** attemptIndex, maxDelay) +} + +const handleQueryCacheError = ( + error: Error, + query: { state: { data: unknown } } +): void => { + if (query.state.data !== undefined) { + const message = + error instanceof ApiError + ? error.getUserMessage() + : 'Background update failed' + toast.error(message) + } +} + +const handleMutationCacheError = ( + error: Error, + _variables: unknown, + _context: unknown, + mutation: { options: { onError?: unknown } } +): void => { + if (mutation.options.onError === undefined) { + const message = + error instanceof ApiError ? error.getUserMessage() : 'Operation failed' + toast.error(message) + } +} + +export const QUERY_STRATEGIES = { + standard: { + staleTime: QUERY_CONFIG.STALE_TIME.DEFAULT, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + }, + static: { + staleTime: QUERY_CONFIG.STALE_TIME.STATIC, + gcTime: QUERY_CONFIG.GC_TIME.LONG, + refetchOnMount: false, + refetchOnWindowFocus: false, + }, +} as const + +export type QueryStrategy = keyof typeof QUERY_STRATEGIES + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: QUERY_CONFIG.STALE_TIME.DEFAULT, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + retry: shouldRetryQuery, + retryDelay: calculateRetryDelay, + refetchOnWindowFocus: false, + refetchOnMount: true, + refetchOnReconnect: true, + }, + mutations: { + retry: QUERY_CONFIG.RETRY.NONE, + }, + }, + queryCache: new QueryCache({ + onError: handleQueryCacheError, + }), + mutationCache: new MutationCache({ + onError: handleMutationCacheError, + }), +}) diff --git a/PROJECTS/advanced/rveng/frontend/src/core/app/routers.tsx b/PROJECTS/advanced/rveng/frontend/src/core/app/routers.tsx new file mode 100644 index 00000000..e8b12abb --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/app/routers.tsx @@ -0,0 +1,28 @@ +// =================== +// © AngelaMos | 2026 +// routers.tsx +// =================== + +import { createBrowserRouter } from 'react-router-dom' +import { ROUTES } from '@/config' +import { Shell } from './shell' + +export const router = createBrowserRouter([ + { + element: , + children: [ + { + path: ROUTES.HOME, + lazy: () => import('@/pages/browser'), + }, + { + path: ROUTES.CHALLENGE, + lazy: () => import('@/pages/workspace'), + }, + { + path: '*', + lazy: () => import('@/pages/browser'), + }, + ], + }, +]) diff --git a/PROJECTS/advanced/rveng/frontend/src/core/app/shell.module.scss b/PROJECTS/advanced/rveng/frontend/src/core/app/shell.module.scss new file mode 100644 index 00000000..a58244fe --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/app/shell.module.scss @@ -0,0 +1,62 @@ +// =================== +// © AngelaMos | 2026 +// shell.module.scss +// =================== + +@use '../../styles/tokens' as *; +@use '../../styles/fonts' as *; + +.shell { + display: flex; + flex-direction: column; + min-height: 100vh; + min-height: 100dvh; +} + +.header { + display: flex; + align-items: baseline; + gap: $space-3; + padding: $space-4 $space-6; + border-bottom: 1px solid $color-border; + background: $color-bg; +} + +.brand { + font-family: $font-mono; + font-size: $font-size-xl; + font-weight: $font-weight-bold; + color: $color-text; + text-decoration: none; +} + +.tag { + font-size: $font-size-sm; + color: $color-text-muted; +} + +.content { + flex: 1; + padding: $space-6; +} + +.loading { + padding: $space-8; + color: $color-text-muted; +} + +.error { + display: flex; + flex-direction: column; + gap: $space-3; + padding: $space-6; + + pre { + padding: $space-3; + background: $color-gray-100; + border-radius: $radius-sm; + font-family: $font-mono; + font-size: $font-size-sm; + overflow-x: auto; + } +} diff --git a/PROJECTS/advanced/rveng/frontend/src/core/app/shell.tsx b/PROJECTS/advanced/rveng/frontend/src/core/app/shell.tsx new file mode 100644 index 00000000..8d2ac468 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/app/shell.tsx @@ -0,0 +1,44 @@ +/** + * ©AngelaMos | 2026 + * shell.tsx + */ + +import { Suspense } from 'react' +import { ErrorBoundary } from 'react-error-boundary' +import { Link, Outlet } from 'react-router-dom' +import { ROUTES } from '@/config' +import styles from './shell.module.scss' + +function ShellErrorFallback({ error }: { error: Error }): React.ReactElement { + return ( +
+

Something went wrong

+
{error.message}
+
+ ) +} + +function ShellLoading(): React.ReactElement { + return
Loading...
+} + +export function Shell(): React.ReactElement { + return ( +
+
+ + rveng + + reverse-engineering lab +
+ +
+ + }> + + + +
+
+ ) +} diff --git a/PROJECTS/advanced/rveng/frontend/src/core/app/toast.module.scss b/PROJECTS/advanced/rveng/frontend/src/core/app/toast.module.scss new file mode 100644 index 00000000..62a8ee32 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/app/toast.module.scss @@ -0,0 +1,4 @@ +// =================== +// © AngelaMos | 2026 +// toast.module.scss +// =================== diff --git a/PROJECTS/advanced/rveng/frontend/src/core/lib/index.ts b/PROJECTS/advanced/rveng/frontend/src/core/lib/index.ts new file mode 100644 index 00000000..cdee46e2 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/lib/index.ts @@ -0,0 +1,6 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +export * from './shell.ui.store' diff --git a/PROJECTS/advanced/rveng/frontend/src/core/lib/shell.ui.store.ts b/PROJECTS/advanced/rveng/frontend/src/core/lib/shell.ui.store.ts new file mode 100644 index 00000000..d601a53b --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/core/lib/shell.ui.store.ts @@ -0,0 +1,63 @@ +/** + * ©AngelaMos | 2025 + * ui.store.ts + */ + +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +type Theme = 'light' | 'dark' | 'system' + +interface UIState { + theme: Theme + sidebarOpen: boolean + sidebarCollapsed: boolean + setTheme: (theme: Theme) => void + toggleSidebar: () => void + setSidebarOpen: (open: boolean) => void + toggleSidebarCollapsed: () => void +} + +export const useUIStore = create()( + devtools( + persist( + (set) => ({ + theme: 'dark', + sidebarOpen: false, + sidebarCollapsed: false, + + setTheme: (theme) => set({ theme }, false, 'ui/setTheme'), + + toggleSidebar: () => + set( + (state) => ({ sidebarOpen: !state.sidebarOpen }), + false, + 'ui/toggleSidebar' + ), + + setSidebarOpen: (open) => + set({ sidebarOpen: open }, false, 'ui/setSidebarOpen'), + + toggleSidebarCollapsed: () => + set( + (state) => ({ sidebarCollapsed: !state.sidebarCollapsed }), + false, + 'ui/toggleSidebarCollapsed' + ), + }), + { + name: 'ui-storage', + partialize: (state) => ({ + theme: state.theme, + sidebarCollapsed: state.sidebarCollapsed, + }), + } + ), + { name: 'UIStore' } + ) +) + +export const useTheme = (): Theme => useUIStore((s) => s.theme) +export const useSidebarOpen = (): boolean => useUIStore((s) => s.sidebarOpen) +export const useSidebarCollapsed = (): boolean => + useUIStore((s) => s.sidebarCollapsed) diff --git a/PROJECTS/advanced/rveng/frontend/src/lib/format.ts b/PROJECTS/advanced/rveng/frontend/src/lib/format.ts new file mode 100644 index 00000000..3f60dde0 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/lib/format.ts @@ -0,0 +1,48 @@ +// =================== +// © AngelaMos | 2026 +// format.ts +// =================== + +export function toHex(value: number, pad = 0): string { + const body = value.toString(16).padStart(pad, '0') + return `0x${body}` +} + +export function formatAddr(value: number): string { + return toHex(value, 6) +} + +export function formatOffset(value: number): string { + return toHex(value, 8) +} + +const ELF_TYPES: Record = { + 0: 'NONE', + 1: 'REL', + 2: 'EXEC', + 3: 'DYN', + 4: 'CORE', +} + +const ELF_MACHINES: Record = { + 3: 'x86', + 40: 'ARM', + 62: 'x86-64', + 183: 'AArch64', + 243: 'RISC-V', +} + +export function elfTypeName(value: number): string { + return ELF_TYPES[value] ?? toHex(value) +} + +export function elfMachineName(value: number): string { + return ELF_MACHINES[value] ?? toHex(value) +} + +export function formatBytes(count: number): string { + if (count < 1024) { + return `${count} B` + } + return `${(count / 1024).toFixed(1)} KiB` +} diff --git a/PROJECTS/advanced/rveng/frontend/src/lib/session.ts b/PROJECTS/advanced/rveng/frontend/src/lib/session.ts new file mode 100644 index 00000000..2ff1da9c --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/lib/session.ts @@ -0,0 +1,35 @@ +// =================== +// © AngelaMos | 2026 +// session.ts +// =================== + +import { STORAGE_KEYS } from '@/config' + +function createId(): string { + if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) { + return crypto.randomUUID() + } + return `s-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}` +} + +let cached: string | null = null + +export function getSessionId(): string { + if (cached !== null) { + return cached + } + try { + const existing = localStorage.getItem(STORAGE_KEYS.SESSION) + if (existing) { + cached = existing + return existing + } + const fresh = createId() + localStorage.setItem(STORAGE_KEYS.SESSION, fresh) + cached = fresh + return fresh + } catch { + cached = createId() + return cached + } +} diff --git a/PROJECTS/advanced/rveng/frontend/src/main.tsx b/PROJECTS/advanced/rveng/frontend/src/main.tsx new file mode 100644 index 00000000..1b1f2c8a --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/main.tsx @@ -0,0 +1,15 @@ +// =========================== +// ©AngelaMos | 2025 +// main.tsx +// =========================== + +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import './styles.scss' + +createRoot(document.getElementById('root')!).render( + + + +) diff --git a/PROJECTS/advanced/rveng/frontend/src/pages/browser/browser.module.scss b/PROJECTS/advanced/rveng/frontend/src/pages/browser/browser.module.scss new file mode 100644 index 00000000..175a40d2 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/pages/browser/browser.module.scss @@ -0,0 +1,44 @@ +// =================== +// © AngelaMos | 2026 +// browser.module.scss +// =================== + +@use '../../styles/tokens' as *; +@use '../../styles/mixins' as *; + +.page { + display: flex; + flex-direction: column; + gap: $space-6; + max-width: $breakpoint-xl; + margin: 0 auto; +} + +.header { + display: flex; + flex-direction: column; + gap: $space-2; +} + +.title { + font-size: $font-size-3xl; + font-weight: $font-weight-bold; +} + +.subtitle { + color: $color-text-muted; +} + +.progress { + max-width: 24rem; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + gap: $space-4; +} + +.state { + color: $color-text-muted; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/pages/browser/index.tsx b/PROJECTS/advanced/rveng/frontend/src/pages/browser/index.tsx new file mode 100644 index 00000000..19d4b023 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/pages/browser/index.tsx @@ -0,0 +1,53 @@ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +import { useChallenges, useProgress } from '@/api/hooks' +import { ChallengeCard, ProgressBar } from '@/components' +import styles from './browser.module.scss' + +export function Component(): React.ReactElement { + const challenges = useChallenges() + const progress = useProgress() + const solvedSet = new Set(progress.data?.solved ?? []) + + return ( +
+
+

Challenges

+

+ Read the binary. Reach the answer. Reveal the source. +

+
+ + {progress.data && ( +
+ +
+ )} + + {challenges.isLoading &&

Loading...

} + {challenges.isError && ( +

Failed to load challenges

+ )} + + {challenges.data && ( +
+ {challenges.data.map((challenge) => ( + + ))} +
+ )} +
+ ) +} + +Component.displayName = 'Browser' diff --git a/PROJECTS/advanced/rveng/frontend/src/pages/workspace/index.tsx b/PROJECTS/advanced/rveng/frontend/src/pages/workspace/index.tsx new file mode 100644 index 00000000..f17a1e31 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/pages/workspace/index.tsx @@ -0,0 +1,28 @@ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +import { Link, useParams } from 'react-router-dom' +import { Workspace } from '@/components' +import { ROUTES } from '@/config' +import styles from './workspace.module.scss' + +export function Component(): React.ReactElement { + const { cid } = useParams<{ cid: string }>() + + if (!cid) { + return
No challenge selected
+ } + + return ( +
+ + Back to challenges + + +
+ ) +} + +Component.displayName = 'ChallengeWorkspace' diff --git a/PROJECTS/advanced/rveng/frontend/src/pages/workspace/workspace.module.scss b/PROJECTS/advanced/rveng/frontend/src/pages/workspace/workspace.module.scss new file mode 100644 index 00000000..cf482729 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/pages/workspace/workspace.module.scss @@ -0,0 +1,24 @@ +// =================== +// © AngelaMos | 2026 +// workspace.module.scss +// =================== + +@use '../../styles/tokens' as *; + +.page { + display: flex; + flex-direction: column; + gap: $space-4; +} + +.back { + align-self: flex-start; + font-size: $font-size-sm; + color: $color-text-muted; + text-decoration: none; + + &:hover { + color: $color-text; + text-decoration: underline; + } +} diff --git a/PROJECTS/advanced/rveng/frontend/src/styles.scss b/PROJECTS/advanced/rveng/frontend/src/styles.scss new file mode 100644 index 00000000..d3afe02d --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles.scss @@ -0,0 +1,31 @@ +// =================== +// © AngelaMos | 2026 +// styles.scss +// =================== + +@forward 'styles/tokens'; +@forward 'styles/fonts'; +@forward 'styles/mixins'; + +@use 'styles/reset'; +@use 'styles/tokens' as *; +@use 'styles/fonts' as *; + +body { + font-family: $font-sans; + color: $color-text; + background-color: $color-bg; +} + +#root { + min-height: 100vh; + min-height: 100dvh; + display: flex; + flex-direction: column; +} + +.app { + flex: 1; + display: flex; + flex-direction: column; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/styles/_fonts.scss b/PROJECTS/advanced/rveng/frontend/src/styles/_fonts.scss new file mode 100644 index 00000000..049e125a --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles/_fonts.scss @@ -0,0 +1,9 @@ +// =================== +// © AngelaMos | 2026 +// _fonts.scss +// =================== + +$font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, + Arial, sans-serif; +$font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', + monospace; diff --git a/PROJECTS/advanced/rveng/frontend/src/styles/_index.scss b/PROJECTS/advanced/rveng/frontend/src/styles/_index.scss new file mode 100644 index 00000000..9d5d028c --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles/_index.scss @@ -0,0 +1,8 @@ +// =================== +// © AngelaMos | 2026 +// _index.scss +// =================== + +@forward 'tokens'; +@forward 'fonts'; +@forward 'mixins'; diff --git a/PROJECTS/advanced/rveng/frontend/src/styles/_mixins.scss b/PROJECTS/advanced/rveng/frontend/src/styles/_mixins.scss new file mode 100644 index 00000000..aef18b11 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles/_mixins.scss @@ -0,0 +1,42 @@ +// =================== +// © AngelaMos | 2026 +// _mixins.scss +// =================== + +@use 'sass:map'; +@use 'tokens' as *; + +$breakpoints: ( + 'xs': $breakpoint-xs, + 'sm': $breakpoint-sm, + 'md': $breakpoint-md, + 'lg': $breakpoint-lg, + 'xl': $breakpoint-xl, + '2xl': $breakpoint-2xl, +); + +@mixin breakpoint-up($size) { + @media (min-width: map.get($breakpoints, $size)) { @content; } +} + +@mixin breakpoint-down($size) { + @media (max-width: map.get($breakpoints, $size)) { @content; } +} + +@mixin flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +@mixin visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/styles/_reset.scss b/PROJECTS/advanced/rveng/frontend/src/styles/_reset.scss new file mode 100644 index 00000000..71e9d060 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles/_reset.scss @@ -0,0 +1,75 @@ +// =================== +// © AngelaMos | 2026 +// _reset.scss +// =================== + +*, +*::before, +*::after { + box-sizing: border-box; +} + +* { + margin: 0; + padding: 0; + -webkit-tap-highlight-color: transparent; +} + +html { + font-size: 16px; + -webkit-text-size-adjust: 100%; +} + +body { + min-height: 100vh; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +img, +picture, +video, +canvas, +svg { + display: block; + max-width: 100%; +} + +input, +button, +textarea, +select { + font: inherit; + color: inherit; +} + +button { + cursor: pointer; + background: none; + border: none; +} + +a { + color: inherit; + text-decoration: none; +} + +ul, +ol { + list-style: none; +} + +p, +h1, +h2, +h3, +h4, +h5, +h6 { + overflow-wrap: break-word; +} + +#root { + isolation: isolate; +} diff --git a/PROJECTS/advanced/rveng/frontend/src/styles/_tokens.scss b/PROJECTS/advanced/rveng/frontend/src/styles/_tokens.scss new file mode 100644 index 00000000..7880360e --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/src/styles/_tokens.scss @@ -0,0 +1,85 @@ +// =================== +// © AngelaMos | 2026 +// _tokens.scss +// =================== +// +// Neutral baseline. No brand identity -- grayscale palette, system type, plain +// spacing scale. Design on top of these; nothing here implies a look. + +// Spacing (8px base) +$space-0: 0; +$space-1: 0.25rem; +$space-2: 0.5rem; +$space-3: 0.75rem; +$space-4: 1rem; +$space-5: 1.25rem; +$space-6: 1.5rem; +$space-8: 2rem; +$space-10: 2.5rem; +$space-12: 3rem; +$space-16: 4rem; +$space-20: 5rem; +$space-24: 6rem; + +// Font sizes +$font-size-xs: 0.75rem; +$font-size-sm: 0.875rem; +$font-size-base: 1rem; +$font-size-lg: 1.125rem; +$font-size-xl: 1.25rem; +$font-size-2xl: 1.5rem; +$font-size-3xl: 1.875rem; +$font-size-4xl: 2.25rem; + +// Weights / line-height / radius +$font-weight-normal: 400; +$font-weight-medium: 500; +$font-weight-semibold: 600; +$font-weight-bold: 700; +$line-height-tight: 1.25; +$line-height-normal: 1.5; +$line-height-relaxed: 1.75; +$radius-sm: 0.25rem; +$radius-md: 0.5rem; +$radius-lg: 0.75rem; +$radius-full: 9999px; + +// Neutral grayscale +$color-white: #ffffff; +$color-black: #000000; +$color-gray-50: #fafafa; +$color-gray-100: #f4f4f5; +$color-gray-200: #e4e4e7; +$color-gray-300: #d4d4d8; +$color-gray-400: #a1a1aa; +$color-gray-500: #71717a; +$color-gray-600: #52525b; +$color-gray-700: #3f3f46; +$color-gray-800: #27272a; +$color-gray-900: #18181b; + +// Neutral semantic aliases +$color-bg: $color-white; +$color-surface: $color-gray-50; +$color-border: $color-gray-200; +$color-text: $color-gray-900; +$color-text-muted: $color-gray-500; + +// Breakpoints +$breakpoint-xs: 480px; +$breakpoint-sm: 640px; +$breakpoint-md: 768px; +$breakpoint-lg: 1024px; +$breakpoint-xl: 1280px; +$breakpoint-2xl: 1536px; + +// Z-index +$z-base: 1; +$z-dropdown: 1000; +$z-modal: 1300; +$z-toast: 1400; + +// Motion +$duration-fast: 150ms; +$duration-normal: 250ms; +$ease: cubic-bezier(0.4, 0, 0.2, 1); diff --git a/PROJECTS/advanced/rveng/frontend/stylelint.config.js b/PROJECTS/advanced/rveng/frontend/stylelint.config.js new file mode 100644 index 00000000..56ebd7bc --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/stylelint.config.js @@ -0,0 +1,107 @@ +// ©AngelaMos | 2025 +// stylelint.config.js + +/** @type {import('stylelint').Config} */ +export default { + extends: ['stylelint-config-standard-scss', 'stylelint-config-prettier-scss'], + rules: { + 'block-no-empty': true, + 'declaration-no-important': true, + 'color-no-invalid-hex': true, + 'property-no-unknown': true, + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: ['global'], + }, + ], + + 'selector-class-pattern': [ + '^[a-z]([a-z0-9-]+)?(__[a-z0-9]([a-z0-9-]+)?)?(--[a-z0-9]([a-z0-9-]+)?)?$|^[a-z][a-zA-Z0-9]*$', + { + message: + 'Selector should be in BEM format (e.g., .block__element--modifier) or CSS Modules camelCase (e.g., .testButton)', + }, + ], + + 'value-keyword-case': [ + 'lower', + { + camelCaseSvgKeywords: true, + ignoreKeywords: [ + 'BlinkMacSystemFont', + 'SFMono-Regular', + 'Menlo', + 'Monaco', + 'Consolas', + 'Roboto', + 'Arial', + 'Helvetica', + 'Times', + 'Georgia', + 'Verdana', + 'Tahoma', + 'Trebuchet', + 'Impact', + 'Comic', + ], + }, + ], + + 'property-no-vendor-prefix': [ + true, + { + ignoreProperties: ['text-size-adjust', 'appearance', 'backdrop-filter'], + }, + ], + 'value-no-vendor-prefix': true, + 'selector-no-vendor-prefix': true, + + 'property-no-deprecated': [ + true, + { + ignoreProperties: ['clip'], + }, + ], + + 'container-name-pattern': null, + 'layer-name-pattern': null, + + 'scss/at-rule-no-unknown': true, + 'scss/declaration-nested-properties-no-divided-groups': true, + 'scss/dollar-variable-no-missing-interpolation': true, + 'scss/dollar-variable-empty-line-before': null, + + 'declaration-empty-line-before': null, + 'custom-property-empty-line-before': null, + + 'no-descending-specificity': null, + + 'media-feature-name-no-unknown': [ + true, + { + ignoreMediaFeatureNames: ['map'], + }, + ], + + 'color-function-notation': null, + 'hue-degree-notation': null, + }, + ignoreFiles: [ + 'node_modules/**', + 'dist/**', + 'build/**', + '**/*.js', + '**/*.ts', + '**/*.tsx', + ], + overrides: [ + { + files: ['**/styles/_reset.scss', '**/styles/_fonts.scss'], + rules: { + 'declaration-no-important': null, + 'scss/comment-no-empty': null, + }, + }, + ], +} diff --git a/PROJECTS/advanced/rveng/frontend/tsconfig.app.json b/PROJECTS/advanced/rveng/frontend/tsconfig.app.json new file mode 100644 index 00000000..afe17f60 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/tsconfig.app.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/PROJECTS/advanced/rveng/frontend/tsconfig.json b/PROJECTS/advanced/rveng/frontend/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/PROJECTS/advanced/rveng/frontend/tsconfig.node.json b/PROJECTS/advanced/rveng/frontend/tsconfig.node.json new file mode 100644 index 00000000..a96b3e59 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/PROJECTS/advanced/rveng/frontend/vite.config.ts b/PROJECTS/advanced/rveng/frontend/vite.config.ts new file mode 100644 index 00000000..601fec69 --- /dev/null +++ b/PROJECTS/advanced/rveng/frontend/vite.config.ts @@ -0,0 +1,70 @@ +/** + * ©AngelaMos | 2026 + * vite.config.ts + */ + +import path from 'node:path' +import react from '@vitejs/plugin-react' +import { defineConfig, loadEnv } from 'vite' +import tsconfigPaths from 'vite-tsconfig-paths' + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, __dirname, '') + const isDev = mode === 'development' + + return { + plugins: [react(), tsconfigPaths()], + + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + + css: { + preprocessorOptions: { + scss: {}, + }, + }, + + server: { + port: 5173, + host: '0.0.0.0', + proxy: { + '/api': { + target: env.VITE_API_TARGET || 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, + + build: { + target: 'esnext', + cssTarget: 'chrome100', + sourcemap: isDev ? true : 'hidden', + minify: 'oxc', + rollupOptions: { + output: { + manualChunks(id: string): string | undefined { + if (id.includes('node_modules')) { + if (id.includes('react-dom') || id.includes('react-router')) { + return 'vendor-react' + } + if (id.includes('@tanstack/react-query')) { + return 'vendor-query' + } + if (id.includes('zustand')) { + return 'vendor-state' + } + } + return undefined + }, + }, + }, + }, + + preview: { + port: 4173, + }, + } +}) diff --git a/PROJECTS/advanced/rveng/src/rveng/api/app.py b/PROJECTS/advanced/rveng/src/rveng/api/app.py index 041f3911..d312006e 100644 --- a/PROJECTS/advanced/rveng/src/rveng/api/app.py +++ b/PROJECTS/advanced/rveng/src/rveng/api/app.py @@ -6,22 +6,30 @@ app.py from pathlib import Path from fastapi import FastAPI, HTTPException, Query +from fastapi.middleware.cors import CORSMiddleware from rveng.api import schemas from rveng.api.limits import ( DEFAULT_SESSION, + DEV_ORIGIN_REGEX, + MAX_BODY_BYTES, MAX_DISASM_BYTES, MAX_HEX_BYTES, + MAX_SESSION_LEN, ) +from rveng.api.middleware import BodySizeLimitMiddleware from rveng.api.store import ( ChallengeStore, InMemoryProgress, ProgressStore, load_store, ) -from rveng.engine import disasm, hex as hexmod, strings +from rveng.engine import cfg as cfgmod +from rveng.engine import disasm, hex as hexmod, strings, xref from rveng.engine.challenge import Challenge, grade +from rveng.engine.discover import discover_functions from rveng.engine.elf import ElfImage, NotAnElf +from rveng.engine.plt import plt_map CHALLENGES_ROOT = Path(__file__).resolve().parents[3] / "challenges" @@ -35,6 +43,13 @@ def create_app( store = store or load_store(CHALLENGES_ROOT) progress = progress or InMemoryProgress() app = FastAPI(title="rveng") + app.add_middleware(BodySizeLimitMiddleware, max_bytes=MAX_BODY_BYTES) + app.add_middleware( + CORSMiddleware, + allow_origin_regex=DEV_ORIGIN_REGEX, + allow_methods=["GET", "POST"], + allow_headers=["*"], + ) def require(cid: str) -> Challenge: challenge = store.get(cid) @@ -48,6 +63,27 @@ def create_app( except NotAnElf as exc: raise HTTPException(422, f"not an ELF: {exc}") + def instructions_for( + image: ElfImage, + symbol: str | None, + address: int | None) -> tuple[list[disasm.Instruction], str]: + if symbol: + sym = image.symbol(symbol) + if sym is None: + raise HTTPException(404, "symbol not found") + if sym.size > MAX_DISASM_BYTES: + raise HTTPException(413, "symbol too large to disassemble") + try: + return disasm.disassemble_symbol(image, sym), symbol + except ValueError as exc: + raise HTTPException(422, str(exc)) + if address is not None: + try: + return disasm.disassemble_at(image, address), f"sub_{address:x}" + except ValueError as exc: + raise HTTPException(422, str(exc)) + raise HTTPException(422, "provide a symbol or an address") + @app.get("/api/challenges") def list_challenges() -> list[schemas.ChallengeSummary]: return [ @@ -96,26 +132,30 @@ def create_app( schemas.FunctionView( name=f.name, value=f.value, size=f.size) for f in image.functions() if f.name + ], + discovered=[ + schemas.DiscoveredFunctionView( + address=d.address, label=d.label) + for d in discover_functions(image) ]) @app.get("/api/challenges/{cid}/disasm") def disasm_view( - cid: str, symbol: str = Query(...)) -> schemas.DisasmView: + cid: str, + symbol: str | None = Query(None), + address: int | None = Query(None, ge=0), + session: str = Query( + DEFAULT_SESSION, max_length=MAX_SESSION_LEN) + ) -> schemas.DisasmView: c = require(cid) image = elf_of(c) - sym = image.symbol(symbol) - if sym is None: - raise HTTPException(404, "symbol not found") - try: - instructions = disasm.disassemble_symbol(image, sym) - except ValueError as exc: - raise HTTPException(422, str(exc)) - if sum(i.size for i in instructions) > MAX_DISASM_BYTES: - raise HTTPException(413, "symbol too large to disassemble") - gate = disasm.find_gate(instructions) + instructions, label = instructions_for(image, symbol, address) + imports = plt_map(image) + solved = c.id in progress.solved(session) + gate = disasm.find_gate(instructions) if solved else None gate_addr = gate.address if gate else None return schemas.DisasmView( - symbol=symbol, + symbol=label, gate_address=gate_addr, instructions=[ schemas.InstructionView( @@ -123,10 +163,49 @@ def create_app( op_str=i.op_str, bytes=i.raw.hex(), immediate=i.immediate, branch_target=i.branch_target, + rip_target=i.rip_target, + call_name=(imports.get(i.branch_target) + if i.mnemonic == "call" else None), is_gate=(i.address == gate_addr)) for i in instructions ]) + @app.get("/api/challenges/{cid}/cfg") + def cfg_view( + cid: str, + symbol: str | None = Query(None), + address: int | None = Query(None, ge=0)) -> schemas.CfgView: + c = require(cid) + image = elf_of(c) + instructions, label = instructions_for(image, symbol, address) + graph = cfgmod.build_cfg(instructions) + return schemas.CfgView( + symbol=label, + blocks=[ + schemas.CfgBlockView( + start=b.start, end=b.end, + instructions=list(b.instructions)) + for b in graph.blocks + ], + edges=[ + schemas.CfgEdgeView(src=e.src, dst=e.dst, kind=e.kind) + for e in graph.edges + ]) + + @app.get("/api/challenges/{cid}/xrefs") + def xrefs_view( + cid: str, target: int = Query(..., ge=0)) -> schemas.XrefsView: + c = require(cid) + image = elf_of(c) + refs = xref.xrefs_to(disasm.disassemble_text(image), target) + return schemas.XrefsView( + target=target, + references=[ + schemas.XrefView( + from_addr=r.from_addr, to_addr=r.to_addr, kind=r.kind) + for r in refs + ]) + @app.get("/api/challenges/{cid}/strings") def strings_view( cid: str, @@ -154,7 +233,9 @@ def create_app( @app.get("/api/progress") def get_progress( - session: str = Query(DEFAULT_SESSION)) -> schemas.ProgressView: + session: str = Query( + DEFAULT_SESSION, + max_length=MAX_SESSION_LEN)) -> schemas.ProgressView: solved = progress.solved(session) return schemas.ProgressView( session=session, diff --git a/PROJECTS/advanced/rveng/src/rveng/api/limits.py b/PROJECTS/advanced/rveng/src/rveng/api/limits.py index 1bf9f70a..7f927a80 100644 --- a/PROJECTS/advanced/rveng/src/rveng/api/limits.py +++ b/PROJECTS/advanced/rveng/src/rveng/api/limits.py @@ -6,4 +6,8 @@ limits.py MAX_HEX_BYTES = 4096 MAX_DISASM_BYTES = 65536 MAX_ANSWER_LEN = 8192 +MAX_SESSION_LEN = 128 +MAX_BODY_BYTES = 65536 DEFAULT_SESSION = "local" + +DEV_ORIGIN_REGEX = r"^http://(localhost|127\.0\.0\.1)(:\d+)?$" diff --git a/PROJECTS/advanced/rveng/src/rveng/api/middleware.py b/PROJECTS/advanced/rveng/src/rveng/api/middleware.py new file mode 100644 index 00000000..6be5daba --- /dev/null +++ b/PROJECTS/advanced/rveng/src/rveng/api/middleware.py @@ -0,0 +1,63 @@ +""" +©AngelaMos | 2026 +middleware.py +""" + +import json + +CONTENT_LENGTH = b"content-length" +PAYLOAD_TOO_LARGE = 413 + + +class BodySizeLimitMiddleware: + """ + Reject request bodies larger than a byte cap before they are parsed + """ + + def __init__(self, app, max_bytes: int): + self.app = app + self.max_bytes = max_bytes + + async def __call__(self, scope, receive, send): + if scope["type"] != "http": + await self.app(scope, receive, send) + return + if self._declared_length(scope) > self.max_bytes: + await self._reject(send) + return + await self.app(scope, self._bounded(receive), send) + + def _declared_length(self, scope) -> int: + for key, value in scope.get("headers", []): + if key.lower() == CONTENT_LENGTH: + try: + return int(value) + except ValueError: + return 0 + return 0 + + def _bounded(self, receive): + seen = 0 + + async def bounded_receive(): + nonlocal seen + message = await receive() + if message["type"] == "http.request": + seen += len(message.get("body", b"")) + if seen > self.max_bytes: + return {"type": "http.disconnect"} + return message + + return bounded_receive + + async def _reject(self, send) -> None: + body = json.dumps({"detail": "request body too large"}).encode() + await send({ + "type": "http.response.start", + "status": PAYLOAD_TOO_LARGE, + "headers": [ + (b"content-type", b"application/json"), + (b"content-length", str(len(body)).encode()), + ], + }) + await send({"type": "http.response.body", "body": body}) diff --git a/PROJECTS/advanced/rveng/src/rveng/api/schemas.py b/PROJECTS/advanced/rveng/src/rveng/api/schemas.py index 8ed12bff..037e2564 100644 --- a/PROJECTS/advanced/rveng/src/rveng/api/schemas.py +++ b/PROJECTS/advanced/rveng/src/rveng/api/schemas.py @@ -3,9 +3,9 @@ schemas.py """ -from pydantic import BaseModel +from pydantic import BaseModel, Field -from rveng.api.limits import DEFAULT_SESSION, MAX_ANSWER_LEN +from rveng.api.limits import DEFAULT_SESSION, MAX_ANSWER_LEN, MAX_SESSION_LEN class ChallengeSummary(BaseModel): @@ -35,6 +35,11 @@ class FunctionView(BaseModel): size: int +class DiscoveredFunctionView(BaseModel): + address: int + label: str + + class SectionView(BaseModel): index: int name: str @@ -51,6 +56,7 @@ class ElfView(BaseModel): entry: int sections: list[SectionView] functions: list[FunctionView] + discovered: list[DiscoveredFunctionView] class InstructionView(BaseModel): @@ -60,6 +66,8 @@ class InstructionView(BaseModel): bytes: str immediate: int | None branch_target: int | None + rip_target: int | None + call_name: str | None is_gate: bool @@ -69,6 +77,35 @@ class DisasmView(BaseModel): gate_address: int | None +class CfgBlockView(BaseModel): + start: int + end: int + instructions: list[int] + + +class CfgEdgeView(BaseModel): + src: int + dst: int + kind: str + + +class CfgView(BaseModel): + symbol: str + blocks: list[CfgBlockView] + edges: list[CfgEdgeView] + + +class XrefView(BaseModel): + from_addr: int + to_addr: int + kind: str + + +class XrefsView(BaseModel): + target: int + references: list[XrefView] + + class StringView(BaseModel): offset: int text: str @@ -80,7 +117,7 @@ class StringsView(BaseModel): class SubmitRequest(BaseModel): answer: str - session: str = DEFAULT_SESSION + session: str = Field(DEFAULT_SESSION, max_length=MAX_SESSION_LEN) def within_limits(self) -> bool: return len(self.answer) <= MAX_ANSWER_LEN diff --git a/PROJECTS/advanced/rveng/src/rveng/api/store.py b/PROJECTS/advanced/rveng/src/rveng/api/store.py index fb30e5f3..8c316d61 100644 --- a/PROJECTS/advanced/rveng/src/rveng/api/store.py +++ b/PROJECTS/advanced/rveng/src/rveng/api/store.py @@ -4,6 +4,7 @@ store.py """ import json +import logging from pathlib import Path from typing import Protocol @@ -15,6 +16,8 @@ from rveng.engine.challenge import ( PatchedBytes, ) +log = logging.getLogger(__name__) + MANIFEST = "challenge.json" TARGET = "target" SOURCE = "source.c" @@ -32,15 +35,18 @@ class ChallengeError(ValueError): def _build_answer(spec: dict, binary: bytes): category = spec.get("category") - if category == CAT_FOUND_VALUE: - return FoundValue(spec["expected"]) - if category == CAT_IDENTIFIED_SYMBOL: - return IdentifiedSymbol(spec["name"]) - if category == CAT_PATCHED_BYTES: - offset = spec["offset"] - replacement = bytes.fromhex(spec["patch"]) - known_good = patch.apply(binary, offset, replacement) - return PatchedBytes(offset=offset, known_good=known_good) + try: + if category == CAT_FOUND_VALUE: + return FoundValue(spec["expected"]) + if category == CAT_IDENTIFIED_SYMBOL: + return IdentifiedSymbol(spec["name"]) + if category == CAT_PATCHED_BYTES: + offset = spec["offset"] + replacement = bytes.fromhex(spec["patch"]) + known_good = patch.apply(binary, offset, replacement) + return PatchedBytes(offset=offset, known_good=known_good) + except (KeyError, ValueError) as exc: + raise ChallengeError(f"bad {category} answer spec: {exc}") from exc raise ChallengeError(f"unknown answer category: {category}") @@ -51,18 +57,21 @@ def load_challenge(directory: Path) -> Challenge: manifest_path = directory / MANIFEST if not manifest_path.is_file(): raise ChallengeError(f"no manifest in {directory}") - manifest = json.loads(manifest_path.read_text()) - binary = (directory / TARGET).read_bytes() - source = (directory / SOURCE).read_text() - return Challenge( - id=manifest["id"], - module=manifest["module"], - title=manifest["title"], - mission=manifest["mission"], - binary=binary, - source=source, - answer=_build_answer(manifest["answer"], binary), - ) + try: + manifest = json.loads(manifest_path.read_text()) + binary = (directory / TARGET).read_bytes() + source = (directory / SOURCE).read_text() + return Challenge( + id=manifest["id"], + module=manifest["module"], + title=manifest["title"], + mission=manifest["mission"], + binary=binary, + source=source, + answer=_build_answer(manifest["answer"], binary), + ) + except (json.JSONDecodeError, KeyError, OSError) as exc: + raise ChallengeError(f"malformed challenge in {directory}: {exc}") from exc class ChallengeStore: @@ -86,8 +95,12 @@ def load_store(root: Path) -> ChallengeStore: """ challenges = [] for directory in sorted(root.iterdir()): - if (directory / MANIFEST).is_file(): + if not (directory / MANIFEST).is_file(): + continue + try: challenges.append(load_challenge(directory)) + except ChallengeError as exc: + log.warning("skipping challenge: %s", exc) return ChallengeStore(challenges) diff --git a/PROJECTS/advanced/rveng/src/rveng/engine/cfg.py b/PROJECTS/advanced/rveng/src/rveng/engine/cfg.py new file mode 100644 index 00000000..37dd9027 --- /dev/null +++ b/PROJECTS/advanced/rveng/src/rveng/engine/cfg.py @@ -0,0 +1,106 @@ +""" +©AngelaMos | 2026 +cfg.py +""" + +from dataclasses import dataclass + +from rveng.engine.disasm import Instruction + +RET = "ret" +JMP = "jmp" + +EDGE_TAKEN = "taken" +EDGE_FALLTHROUGH = "fallthrough" +EDGE_JUMP = "jump" + + +@dataclass(frozen=True) +class BasicBlock: + """ + A straight run of instructions with a single entry and single exit + """ + + start: int + end: int + instructions: tuple[int, ...] + + +@dataclass(frozen=True) +class Edge: + """ + A control-flow edge between two basic blocks, keyed by their starts + """ + + src: int + dst: int + kind: str + + +@dataclass(frozen=True) +class ControlFlowGraph: + """ + The basic blocks and edges of a single function + """ + + blocks: list[BasicBlock] + edges: list[Edge] + + +def _is_terminator(ins: Instruction) -> bool: + return (ins.is_conditional_branch + or ins.mnemonic == JMP + or ins.mnemonic == RET) + + +def _leaders(instructions: list[Instruction], within: set[int]) -> list[int]: + addrs = [i.address for i in instructions] + leaders = {addrs[0]} + for idx, ins in enumerate(instructions): + if not _is_terminator(ins): + continue + if idx + 1 < len(instructions): + leaders.add(addrs[idx + 1]) + if ins.branch_target is not None and ins.branch_target in within: + leaders.add(ins.branch_target) + return sorted(leaders) + + +def build_cfg(instructions: list[Instruction]) -> ControlFlowGraph: + """ + Split a function's instructions into basic blocks and connect them + """ + if not instructions: + return ControlFlowGraph([], []) + + addrs = [i.address for i in instructions] + by_addr = {i.address: i for i in instructions} + within = set(addrs) + starts = _leaders(instructions, within) + + blocks = [] + for pos, start in enumerate(starts): + stop = starts[pos + 1] if pos + 1 < len(starts) else None + body = tuple( + a for a in addrs if a >= start and (stop is None or a < stop)) + blocks.append(BasicBlock(start=start, end=body[-1], instructions=body)) + + block_starts = [b.start for b in blocks] + edges = [] + for pos, block in enumerate(blocks): + last = by_addr[block.end] + following = block_starts[pos + 1] if pos + 1 < len(blocks) else None + if last.mnemonic == RET: + continue + if last.is_conditional_branch: + if last.branch_target in within: + edges.append(Edge(block.start, last.branch_target, EDGE_TAKEN)) + if following is not None: + edges.append(Edge(block.start, following, EDGE_FALLTHROUGH)) + elif last.mnemonic == JMP: + if last.branch_target in within: + edges.append(Edge(block.start, last.branch_target, EDGE_JUMP)) + elif following is not None: + edges.append(Edge(block.start, following, EDGE_FALLTHROUGH)) + + return ControlFlowGraph(blocks, edges) diff --git a/PROJECTS/advanced/rveng/src/rveng/engine/disasm.py b/PROJECTS/advanced/rveng/src/rveng/engine/disasm.py index 850259c4..6c060dab 100644 --- a/PROJECTS/advanced/rveng/src/rveng/engine/disasm.py +++ b/PROJECTS/advanced/rveng/src/rveng/engine/disasm.py @@ -6,7 +6,7 @@ disasm.py from dataclasses import dataclass from capstone import CS_ARCH_X86, CS_MODE_64, CS_OPT_SYNTAX_INTEL, Cs -from capstone.x86 import X86_OP_IMM +from capstone.x86 import X86_OP_IMM, X86_OP_MEM, X86_REG_RIP from rveng.engine.elf import ElfImage, Symbol @@ -31,6 +31,7 @@ class Instruction: raw: bytes immediate: int | None branch_target: int | None = None + rip_target: int | None = None @property def size(self) -> int: @@ -68,6 +69,13 @@ def _immediate(ins) -> int | None: return value +def _rip_target(ins) -> int | None: + for operand in ins.operands: + if operand.type == X86_OP_MEM and operand.mem.base == X86_REG_RIP: + return ins.address + ins.size + operand.mem.disp + return None + + def disassemble(code: bytes, base: int = 0) -> list[Instruction]: """ Decode a byte range into annotated instructions at virtual base @@ -85,10 +93,54 @@ def disassemble(code: bytes, base: int = 0) -> list[Instruction]: raw=bytes(ins.bytes), immediate=None if flow else imm, branch_target=imm if flow else None, + rip_target=_rip_target(ins), )) return out +def disassemble_text(image: ElfImage) -> list[Instruction]: + """ + Disassemble the whole .text section for whole-binary analysis + """ + section = image.section(".text") + if section is None: + return [] + code = image.data[section.offset:section.offset + section.size] + return disassemble(code, section.addr) + + +RET = "ret" + + +def _section_at(image: ElfImage, vaddr: int): + for section in image.sections: + if section.addr == 0 or section.is_nobits: + continue + if section.addr <= vaddr < section.addr + section.size: + return section + return None + + +def disassemble_at( + image: ElfImage, + vaddr: int, + max_bytes: int = 4096) -> list[Instruction]: + """ + Disassemble from a raw virtual address until the first ret + """ + section = _section_at(image, vaddr) + if section is None: + raise ValueError("address is not in a mapped section") + start = vaddr - section.addr + section.offset + end = min(start + max_bytes, section.offset + section.size) + out = [] + for ins in disassemble(image.data[start:end], vaddr): + out.append(ins) + if ins.mnemonic == RET: + break + return out + + def disassemble_symbol( image: ElfImage, symbol: Symbol) -> list[Instruction]: """ diff --git a/PROJECTS/advanced/rveng/src/rveng/engine/discover.py b/PROJECTS/advanced/rveng/src/rveng/engine/discover.py new file mode 100644 index 00000000..e6cde1df --- /dev/null +++ b/PROJECTS/advanced/rveng/src/rveng/engine/discover.py @@ -0,0 +1,41 @@ +""" +©AngelaMos | 2026 +discover.py +""" + +from dataclasses import dataclass + +from rveng.engine.elf import SHF_EXECINSTR, ElfImage + +PROLOGUE = b"\x55\x48\x89\xe5" + + +@dataclass(frozen=True) +class DiscoveredFunction: + """ + A candidate function entry found without a symbol table + """ + + address: int + section: str + + @property + def label(self) -> str: + return f"sub_{self.address:x}" + + +def discover_functions(image: ElfImage) -> list[DiscoveredFunction]: + """ + Find function entries by scanning executable sections for the prologue + """ + found = [] + for section in image.sections: + if section.is_nobits or not section.flags & SHF_EXECINSTR: + continue + blob = image.data[section.offset:section.offset + section.size] + pos = blob.find(PROLOGUE) + while pos != -1: + found.append(DiscoveredFunction( + address=section.addr + pos, section=section.name)) + pos = blob.find(PROLOGUE, pos + 1) + return sorted(found, key=lambda f: f.address) diff --git a/PROJECTS/advanced/rveng/src/rveng/engine/plt.py b/PROJECTS/advanced/rveng/src/rveng/engine/plt.py new file mode 100644 index 00000000..d8e0e5d3 --- /dev/null +++ b/PROJECTS/advanced/rveng/src/rveng/engine/plt.py @@ -0,0 +1,101 @@ +""" +©AngelaMos | 2026 +plt.py +""" + +import struct + +from rveng.engine.elf import ElfImage + +DYNSYM = ".dynsym" +DYNSTR = ".dynstr" +RELA_PLT = ".rela.plt" +PLT = ".plt" + +SYM_ENTRY = 24 +SYM_NAME = 0x00 + +RELA_ENTRY = 24 +RELA_OFFSET = 0x00 +RELA_INFO = 0x08 + +PLT_ENTRY = 16 +JMP_RIP = b"\xff\x25" +JMP_RIP_LEN = 6 + + +def _cstring(blob: bytes, offset: int) -> str: + end = blob.find(b"\x00", offset) + if end == -1: + end = len(blob) + return blob[offset:end].decode("utf-8", "replace") + + +def _dynamic_names(image: ElfImage) -> list[str]: + dynsym = image.section(DYNSYM) + dynstr = image.section(DYNSTR) + if dynsym is None or dynstr is None or dynsym.entsize == 0: + return [] + names = image.data[dynstr.offset:dynstr.offset + dynstr.size] + body = image.data[dynsym.offset:dynsym.offset + dynsym.size] + en = image.header.endian + out = [] + for base in range(0, len(body) - SYM_ENTRY + 1, SYM_ENTRY): + name_off = struct.unpack_from(en + "I", body, base + SYM_NAME)[0] + out.append(_cstring(names, name_off)) + return out + + +def _got_to_name(image: ElfImage, names: list[str]) -> dict[int, str]: + rela = image.section(RELA_PLT) + if rela is None or rela.entsize == 0: + return {} + body = image.data[rela.offset:rela.offset + rela.size] + en = image.header.endian + mapping = {} + for base in range(0, len(body) - RELA_ENTRY + 1, RELA_ENTRY): + r_offset = struct.unpack_from(en + "Q", body, base + RELA_OFFSET)[0] + r_info = struct.unpack_from(en + "Q", body, base + RELA_INFO)[0] + sym_index = r_info >> 32 + if 0 <= sym_index < len(names): + mapping[r_offset] = names[sym_index] + return mapping + + +def _stub_got_slot(entry: bytes, entry_addr: int, endian: str) -> int | None: + jmp = entry.find(JMP_RIP) + if jmp == -1 or jmp + JMP_RIP_LEN > len(entry): + return None + disp = struct.unpack_from(endian + "i", entry, jmp + 2)[0] + return entry_addr + jmp + JMP_RIP_LEN + disp + + +def plt_map(image: ElfImage) -> dict[int, str]: + """ + Map each PLT stub address to the imported symbol it jumps to + """ + plt = image.section(PLT) + if plt is None: + return {} + got_names = _got_to_name(image, _dynamic_names(image)) + if not got_names: + return {} + body = image.data[plt.offset:plt.offset + plt.size] + en = image.header.endian + result = {} + for base in range(0, len(body) - PLT_ENTRY + 1, PLT_ENTRY): + entry = body[base:base + PLT_ENTRY] + got_slot = _stub_got_slot(entry, plt.addr + base, en) + if got_slot is None: + continue + name = got_names.get(got_slot) + if name is not None: + result[plt.addr + base] = name + return result + + +def resolve_call(image: ElfImage, target: int) -> str | None: + """ + Return the import a call target resolves to, or None when it is local + """ + return plt_map(image).get(target) diff --git a/PROJECTS/advanced/rveng/src/rveng/engine/xref.py b/PROJECTS/advanced/rveng/src/rveng/engine/xref.py new file mode 100644 index 00000000..bb74443d --- /dev/null +++ b/PROJECTS/advanced/rveng/src/rveng/engine/xref.py @@ -0,0 +1,55 @@ +""" +©AngelaMos | 2026 +xref.py +""" + +from dataclasses import dataclass + +from rveng.engine.disasm import Instruction + +CALL = "call" +KIND_CALL = "call" +KIND_BRANCH = "branch" +KIND_DATA = "data" + + +@dataclass(frozen=True) +class Reference: + """ + One cross reference from an instruction to a target address + """ + + from_addr: int + to_addr: int + kind: str + + +def references(instructions: list[Instruction]) -> list[Reference]: + """ + Every control-flow and data reference the instructions emit + """ + out = [] + for ins in instructions: + if ins.branch_target is not None: + kind = KIND_CALL if ins.mnemonic == CALL else KIND_BRANCH + out.append(Reference(ins.address, ins.branch_target, kind)) + if ins.rip_target is not None: + out.append(Reference(ins.address, ins.rip_target, KIND_DATA)) + return out + + +def build_xrefs(instructions: list[Instruction]) -> dict[int, list[Reference]]: + """ + Group every reference by the address it points at + """ + table: dict[int, list[Reference]] = {} + for ref in references(instructions): + table.setdefault(ref.to_addr, []).append(ref) + return table + + +def xrefs_to(instructions: list[Instruction], target: int) -> list[Reference]: + """ + Every reference that points at target + """ + return [ref for ref in references(instructions) if ref.to_addr == target] diff --git a/PROJECTS/advanced/rveng/tests/conftest.py b/PROJECTS/advanced/rveng/tests/conftest.py index ea9b2193..5981788e 100644 --- a/PROJECTS/advanced/rveng/tests/conftest.py +++ b/PROJECTS/advanced/rveng/tests/conftest.py @@ -18,3 +18,13 @@ def gate_path() -> Path: @pytest.fixture(scope="session") def gate_bytes(gate_path: Path) -> bytes: return gate_path.read_bytes() + + +@pytest.fixture(scope="session") +def gate_stripped_path() -> Path: + return FIXTURES / "gate_stripped" + + +@pytest.fixture(scope="session") +def gate_stripped_bytes(gate_stripped_path: Path) -> bytes: + return gate_stripped_path.read_bytes() diff --git a/PROJECTS/advanced/rveng/tests/fixtures/gate_stripped b/PROJECTS/advanced/rveng/tests/fixtures/gate_stripped new file mode 100755 index 00000000..60281096 Binary files /dev/null and b/PROJECTS/advanced/rveng/tests/fixtures/gate_stripped differ diff --git a/PROJECTS/advanced/rveng/tests/test_api.py b/PROJECTS/advanced/rveng/tests/test_api.py index 2c5571fb..2f62143b 100644 --- a/PROJECTS/advanced/rveng/tests/test_api.py +++ b/PROJECTS/advanced/rveng/tests/test_api.py @@ -3,11 +3,17 @@ test_api.py """ +import shutil +from pathlib import Path + import pytest from fastapi.testclient import TestClient from rveng.api.app import create_app -from rveng.api.limits import MAX_HEX_BYTES +from rveng.api.limits import MAX_HEX_BYTES, MAX_SESSION_LEN +from rveng.api.store import load_store + +CHALLENGES = Path(__file__).resolve().parents[1] / "challenges" ELF_MAGIC_LINE = ( "00000000 7f 45 4c 46 02 01 01 00 " @@ -20,11 +26,12 @@ def client() -> TestClient: return TestClient(create_app()) -def test_list_has_three_seed_challenges(client: TestClient): +def test_list_has_the_seed_challenges(client: TestClient): body = client.get("/api/challenges").json() ids = {c["id"] for c in body} assert ids == { - "03-flip-the-gate", "04-name-the-function", "05-find-the-gate"} + "03-flip-the-gate", "04-name-the-function", "05-find-the-gate", + "06-stripped-gate"} def test_detail_does_not_leak_source_or_answer(client: TestClient): @@ -58,9 +65,25 @@ def test_elf_view_reports_entry_and_functions(client: TestClient): assert "check" in names and "main" in names -def test_disasm_marks_the_gate(client: TestClient): +def test_disasm_hides_gate_before_solve(client: TestClient): body = client.get( - "/api/challenges/05-find-the-gate/disasm?symbol=check").json() + "/api/challenges/05-find-the-gate/disasm" + "?symbol=check&session=fresh").json() + assert body["gate_address"] is None + assert all(i["is_gate"] is False for i in body["instructions"]) + cmp_ins = next( + i for i in body["instructions"] if i["mnemonic"] == "cmp") + assert "0x539" in cmp_ins["op_str"] + + +def test_disasm_reveals_gate_after_solve(client: TestClient): + session = "solver" + client.post( + "/api/challenges/05-find-the-gate/submit", + json={"answer": "1337", "session": session}) + body = client.get( + "/api/challenges/05-find-the-gate/disasm" + f"?symbol=check&session={session}").json() assert body["gate_address"] == 0x40114D gate = next(i for i in body["instructions"] if i["is_gate"]) assert gate["mnemonic"] == "cmp" @@ -129,4 +152,95 @@ def test_progress_tracks_solves(client: TestClient): json={"answer": "1337", "session": "s1"}) body = client.get("/api/progress?session=s1").json() assert body["solved"] == ["05-find-the-gate"] - assert body["total"] == 3 + assert body["total"] == 4 + + +def test_disasm_resolves_import_call_names(client: TestClient): + body = client.get( + "/api/challenges/05-find-the-gate/disasm?symbol=main").json() + names = {i["call_name"] for i in body["instructions"] if i["call_name"]} + assert "atoi" in names and "puts" in names + + +def test_disasm_exposes_rip_target(client: TestClient): + body = client.get( + "/api/challenges/05-find-the-gate/disasm?symbol=main").json() + targets = {i["rip_target"] for i in body["instructions"]} + assert 0x402004 in targets + + +def test_elf_lists_discovered_functions(client: TestClient): + body = client.get("/api/challenges/05-find-the-gate/elf").json() + labels = {d["label"] for d in body["discovered"]} + assert "sub_401146" in labels + + +def test_cfg_of_check_is_a_diamond(client: TestClient): + body = client.get( + "/api/challenges/05-find-the-gate/cfg?symbol=check").json() + assert len(body["blocks"]) == 4 + assert len(body["edges"]) == 4 + + +def test_xrefs_find_caller_of_check(client: TestClient): + body = client.get( + "/api/challenges/05-find-the-gate/xrefs?target=4198726").json() + assert any( + r["from_addr"] == 0x4011A6 and r["kind"] == "call" + for r in body["references"]) + + +def test_stripped_challenge_disassembles_by_address(client: TestClient): + elf = client.get("/api/challenges/06-stripped-gate/elf").json() + assert elf["functions"] == [] + assert any(d["address"] == 0x401146 for d in elf["discovered"]) + dis = client.get( + "/api/challenges/06-stripped-gate/disasm?address=4198726").json() + assert dis["instructions"][-1]["mnemonic"] == "ret" + + +def test_stripped_challenge_grades_found_value(client: TestClient): + r = client.post( + "/api/challenges/06-stripped-gate/submit", + json={"answer": "0x539"}).json() + assert r["correct"] is True + + +def test_load_store_skips_malformed_dir(tmp_path: Path): + root = tmp_path / "challenges" + root.mkdir() + shutil.copytree( + CHALLENGES / "04-name-the-function", + root / "04-name-the-function") + broken = root / "99-broken" + broken.mkdir() + (broken / "challenge.json").write_text("{ not valid json") + store = load_store(root) + assert {c.id for c in store.list()} == {"04-name-the-function"} + + +def test_answer_over_answer_len_is_413(client: TestClient): + r = client.post( + "/api/challenges/05-find-the-gate/submit", + json={"answer": "9" * 10000}) + assert r.status_code == 413 + + +def test_body_over_size_cap_is_413(client: TestClient): + r = client.post( + "/api/challenges/05-find-the-gate/submit", + json={"answer": "9" * 100000}) + assert r.status_code == 413 + + +def test_session_over_max_len_is_422(client: TestClient): + long_session = "s" * (MAX_SESSION_LEN + 1) + r = client.get(f"/api/progress?session={long_session}") + assert r.status_code == 422 + + +def test_cors_allows_localhost_dev_origin(client: TestClient): + r = client.get( + "/api/challenges", headers={"origin": "http://localhost:5173"}) + assert r.headers.get("access-control-allow-origin") == ( + "http://localhost:5173") diff --git a/PROJECTS/advanced/rveng/tests/test_cfg.py b/PROJECTS/advanced/rveng/tests/test_cfg.py new file mode 100644 index 00000000..9dbcb3f0 --- /dev/null +++ b/PROJECTS/advanced/rveng/tests/test_cfg.py @@ -0,0 +1,45 @@ +""" +©AngelaMos | 2026 +test_cfg.py +""" + +from rveng.engine.cfg import EDGE_FALLTHROUGH, EDGE_JUMP, EDGE_TAKEN, build_cfg +from rveng.engine.disasm import disassemble_symbol +from rveng.engine.elf import ElfImage + +B0 = 0x401146 +B1 = 0x401156 +B2 = 0x40115D +B3 = 0x401162 + + +def _check_cfg(gate_bytes: bytes): + image = ElfImage(gate_bytes) + symbol = image.symbol("check") + return build_cfg(disassemble_symbol(image, symbol)) + + +def test_check_has_four_basic_blocks(gate_bytes: bytes): + cfg = _check_cfg(gate_bytes) + assert [b.start for b in cfg.blocks] == [B0, B1, B2, B3] + + +def test_check_block_zero_ends_at_the_gate_jne(gate_bytes: bytes): + cfg = _check_cfg(gate_bytes) + assert cfg.blocks[0].end == 0x401154 + + +def test_check_cfg_is_a_diamond(gate_bytes: bytes): + cfg = _check_cfg(gate_bytes) + edges = {(e.src, e.dst, e.kind) for e in cfg.edges} + assert edges == { + (B0, B2, EDGE_TAKEN), + (B0, B1, EDGE_FALLTHROUGH), + (B1, B3, EDGE_JUMP), + (B2, B3, EDGE_FALLTHROUGH), + } + + +def test_terminal_block_has_no_out_edges(gate_bytes: bytes): + cfg = _check_cfg(gate_bytes) + assert all(e.src != B3 for e in cfg.edges) diff --git a/PROJECTS/advanced/rveng/tests/test_discover.py b/PROJECTS/advanced/rveng/tests/test_discover.py new file mode 100644 index 00000000..3eb5e24d --- /dev/null +++ b/PROJECTS/advanced/rveng/tests/test_discover.py @@ -0,0 +1,42 @@ +""" +©AngelaMos | 2026 +test_discover.py +""" + +from rveng.engine.disasm import disassemble_at, find_gate +from rveng.engine.discover import discover_functions +from rveng.engine.elf import ElfImage + +CHECK = 0x401146 +MAIN = 0x401164 +START = 0x401060 + + +def test_prologue_scan_finds_check_and_main(gate_stripped_bytes: bytes): + image = ElfImage(gate_stripped_bytes) + addrs = {f.address for f in discover_functions(image)} + assert CHECK in addrs + assert MAIN in addrs + + +def test_prologue_scan_skips_the_runtime_start(gate_stripped_bytes: bytes): + image = ElfImage(gate_stripped_bytes) + addrs = {f.address for f in discover_functions(image)} + assert START not in addrs + + +def test_discovered_function_has_an_address_label(gate_stripped_bytes: bytes): + image = ElfImage(gate_stripped_bytes) + functions = discover_functions(image) + check = next(f for f in functions if f.address == CHECK) + assert check.label == "sub_401146" + + +def test_disassemble_at_recovers_the_gate_without_symbols( + gate_stripped_bytes: bytes): + image = ElfImage(gate_stripped_bytes) + instructions = disassemble_at(image, CHECK) + assert instructions[-1].mnemonic == "ret" + gate = find_gate(instructions) + assert gate is not None + assert gate.immediate == 1337 diff --git a/PROJECTS/advanced/rveng/tests/test_plt.py b/PROJECTS/advanced/rveng/tests/test_plt.py new file mode 100644 index 00000000..7f897cf0 --- /dev/null +++ b/PROJECTS/advanced/rveng/tests/test_plt.py @@ -0,0 +1,51 @@ +""" +©AngelaMos | 2026 +test_plt.py +""" + +from rveng.engine.elf import ElfImage +from rveng.engine.plt import _stub_got_slot, plt_map, resolve_call + +PUTS = 0x401030 +PRINTF = 0x401040 +ATOI = 0x401050 + + +def test_plt_map_names_the_three_imports(gate_bytes: bytes): + image = ElfImage(gate_bytes) + assert plt_map(image) == {PUTS: "puts", PRINTF: "printf", ATOI: "atoi"} + + +def test_plt0_stub_is_not_treated_as_an_import(gate_bytes: bytes): + image = ElfImage(gate_bytes) + assert 0x401020 not in plt_map(image) + + +def test_resolve_call_maps_target_to_name(gate_bytes: bytes): + image = ElfImage(gate_bytes) + assert resolve_call(image, ATOI) == "atoi" + assert resolve_call(image, PUTS) == "puts" + + +def test_resolve_call_returns_none_for_local_target(gate_bytes: bytes): + image = ElfImage(gate_bytes) + assert resolve_call(image, 0x401146) is None + + +def test_plt_resolution_survives_stripping(gate_stripped_bytes: bytes): + image = ElfImage(gate_stripped_bytes) + assert plt_map(image) == {PUTS: "puts", PRINTF: "printf", ATOI: "atoi"} + + +def test_stub_decode_of_classic_entry(): + entry = bytes.fromhex("ff25ca2f0000") + b"\x00" * 10 + assert _stub_got_slot(entry, 0x401030, "<") == 0x404000 + + +def test_stub_decode_tolerates_truncated_jmp_pattern(): + entry = b"\x90" * 11 + b"\xff\x25\x00\x00\x00" + assert _stub_got_slot(entry, 0x401030, "<") is None + + +def test_stub_decode_returns_none_without_jmp(): + assert _stub_got_slot(b"\x90" * 16, 0x401030, "<") is None diff --git a/PROJECTS/advanced/rveng/tests/test_xref.py b/PROJECTS/advanced/rveng/tests/test_xref.py new file mode 100644 index 00000000..f575b068 --- /dev/null +++ b/PROJECTS/advanced/rveng/tests/test_xref.py @@ -0,0 +1,40 @@ +""" +©AngelaMos | 2026 +test_xref.py +""" + +from rveng.engine.disasm import disassemble_text +from rveng.engine.elf import ElfImage +from rveng.engine.xref import build_xrefs, xrefs_to + +CHECK = 0x401146 +FLAG = 0x402004 +MAIN_CALLS_CHECK = 0x4011A6 +MAIN_LEA_FLAG = 0x401173 + + +def test_rip_relative_resolves_to_flag_string(gate_bytes: bytes): + image = ElfImage(gate_bytes) + by_addr = {i.address: i for i in disassemble_text(image)} + assert by_addr[MAIN_LEA_FLAG].rip_target == FLAG + + +def test_xref_finds_caller_of_check(gate_bytes: bytes): + image = ElfImage(gate_bytes) + refs = xrefs_to(disassemble_text(image), CHECK) + assert any( + r.from_addr == MAIN_CALLS_CHECK and r.kind == "call" for r in refs) + + +def test_xref_finds_data_reference_to_flag(gate_bytes: bytes): + image = ElfImage(gate_bytes) + refs = xrefs_to(disassemble_text(image), FLAG) + assert any( + r.from_addr == MAIN_LEA_FLAG and r.kind == "data" for r in refs) + + +def test_build_xrefs_groups_by_target(gate_bytes: bytes): + image = ElfImage(gate_bytes) + table = build_xrefs(disassemble_text(image)) + assert CHECK in table + assert FLAG in table