[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6JZ4CTFMSCYWDG9K32P0WA9":3,"skill-same:01M6JZ4CTFMSCYWDG9K32P0WA9":64},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":43,"commitSha":44,"skillDirectory":45,"forksCount":13,"repositoryHtmlUrl":46,"repositoryHomepageUrl":47,"ownerHtmlUrl":48,"ownerBlogUrl":47,"ownerDescription":6,"updatedAt":49,"install":50,"resources":63},"01M6JZ4CTFMSCYWDG9K32P0WA9","desktop-app",null,"Build and maintain the Tauri v2 desktop app. Covers scaffolding, Turborepo wiring, CI build matrix, auto-updater with Ed25519 signature verification, and native OS integrations (menu, tray, notifications, window-state).","MUDdown\u002FMUDdown","MUDdown","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F271809841?v=4","01M6JZ2ZZ19Z0NRQS2YHZ57AHZ","01M6JZ303FSH41JFNGAAJSQGDN",2,"MIT","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fdesktop-app\u002FSKILL.md",[],[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"app","desktop app","desktop","maintain","wiring","window","matrix","with","native","updater","os","scaffolding","tauri","auto","integrations","ed25519","turborepo","ci","notifications","verification","2026-11-10 10:12:30",false,"---\nname: desktop-app\ndescription: Build and maintain the Tauri v2 desktop app. Covers scaffolding, Turborepo wiring, CI build matrix, auto-updater with Ed25519 signature verification, and native OS integrations (menu, tray, notifications, window-state).\n---\n\n# Desktop App Skill\n\nYou are working on the MUDdown Tauri v2 desktop app (`apps\u002Fdesktop`). This skill covers the scaffolding layout, workspace wiring, CI\u002FCD build matrix, auto-updater signature verification, and native OS integrations.\n\n## Scaffold Layout\n\n```\napps\u002Fdesktop\u002F\n├── src\u002F\n│   ├── main.ts              # Game client (auth, characters, WebSocket, rendering)\n│   ├── index.html           # Single-page app shell\n│   └── styles.css           # Dark terminal theme (CSS custom properties)\n├── src-tauri\u002F\n│   ├── src\u002Flib.rs           # Rust backend (menu, tray, Tauri commands)\n│   ├── Cargo.toml           # Rust dependencies\n│   ├── tauri.conf.json      # App config, CSP, updater settings\n│   ├── build.rs             # Tauri build script\n│   └── icons\u002F               # App icons (PNG, ICO, ICNS for each platform)\n├── vite.config.ts           # Vite bundler (port 1420, strictPort)\n├── package.json             # @muddown\u002Fdesktop\n├── tsconfig.json            # TypeScript (noEmit: true)\n└── UPDATER_KEYS.md          # Ed25519 key rotation procedure\n```\n\n## Turborepo Wiring\n\nThe desktop app is a workspace package at `apps\u002Fdesktop`. Add it to the root `package.json` workspaces array if not present.\n\n### Workspace Dependencies\n\nIn `apps\u002Fdesktop\u002Fpackage.json`:\n```json\n{\n  \"dependencies\": {\n    \"@muddown\u002Fclient\": \"*\",\n    \"@muddown\u002Fshared\": \"*\",\n    \"@tauri-apps\u002Fapi\": \"^2.0.0\",\n    \"@tauri-apps\u002Fplugin-dialog\": \"^2.0.0\",\n    \"@tauri-apps\u002Fplugin-notification\": \"^2.0.0\",\n    \"@tauri-apps\u002Fplugin-store\": \"^2.0.0\",\n    \"@tauri-apps\u002Fplugin-updater\": \"^2.0.0\"\n  }\n}\n```\n\n### Vite Aliases\n\nIn `vite.config.ts`, resolve workspace packages to their source directories:\n```ts\nresolve: {\n  alias: {\n    \"@muddown\u002Fclient\": path.resolve(__dirname, \"..\u002F..\u002Fpackages\u002Fclient\u002Fsrc\"),\n    \"@muddown\u002Fshared\": path.resolve(__dirname, \"..\u002F..\u002Fpackages\u002Fshared\u002Fsrc\"),\n  },\n}\n```\n\n### Build Order\n\nBuild workspace deps before the Tauri app:\n```bash\nnpx turbo run build --filter=@muddown\u002Fclient... --filter=@muddown\u002Fshared...\n```\n\n## Rust Backend (`src-tauri\u002Fsrc\u002Flib.rs`)\n\n### Tauri Commands\n\nExpose custom commands to the JS frontend with `#[tauri::command]`:\n\n| Command | Purpose | Error handling |\n|---------|---------|----------------|\n| `set_window_title` | Update native title bar | `if let Err(e)` → `eprintln!` |\n| `send_notification` | Send OS notification | Best-effort `let _` (permission denial expected) |\n\nRegister commands in the builder:\n```rust\n.invoke_handler(tauri::generate_handler![set_window_title, send_notification])\n```\n\n### Tauri Plugins\n\nRegister plugins in `tauri::Builder::default()`:\n```rust\n.plugin(tauri_plugin_dialog::init())\n.plugin(tauri_plugin_notification::init())\n.plugin(tauri_plugin_window_state::Builder::default().build())\n.plugin(tauri_plugin_store::Builder::default().build())\n.plugin(tauri_plugin_updater::Builder::default().build())\n```\n\n### Cargo Dependencies\n\nIn `apps\u002Fdesktop\u002Fsrc-tauri\u002FCargo.toml`:\n```toml\n[dependencies]\ntauri = { version = \"2\", features = [\"tray-icon\", \"image-png\"] }\ntauri-plugin-dialog = \"2\"\ntauri-plugin-notification = \"2\"\ntauri-plugin-window-state = \"2\"\ntauri-plugin-updater = \"2\"\ntauri-plugin-store = \"2\"\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n```\n\n### Error Handling Conventions\n\n- Tauri commands: use `if let Err(e) = ... { eprintln!(\"[context] message: {e}\"); }` for recoverable errors.\n- Menu event emitting: check `Err` and log with `eprintln!`.\n- Tray show\u002Ffocus: check `Err` on `w.show()` and `w.set_focus()` and log.\n- Notifications: `let _` is acceptable (permission denial is expected, not an error).\n- App startup: use `.unwrap_or_else(|e| panic!(\"...: {}\", e))` on `.run()` to preserve the error message.\n\n## Native OS Integrations\n\n### Menu Bar\n\nBuild menus in the `.setup()` closure using `SubmenuBuilder` and `MenuItemBuilder`:\n- **File**: Connect, Disconnect, Quit\n- **View**: Clear Output (Cmd\u002FCtrl+L), Focus Input (Cmd\u002FCtrl+K), Toggle Inventory, Toggle Hints\n- **Help**: Game Commands, About MUDdown\n\nForward menu actions to JS via `handle.emit(\"menu-action\", id)`. In JS, listen with `listen(\"menu-action\", ...)`.\n\n### System Tray\n\nUse `TrayIconBuilder` with a PNG icon and a context menu:\n- **Show MUDdown** — `w.show()` + `w.set_focus()`\n- **Quit** — `handle.exit(0)`\n\nUpdate the tray tooltip from JS to reflect connection status (e.g., \"MUDdown — Connected\" \u002F \"MUDdown — Disconnected\").\n\n### Notifications\n\nUse `tauri-plugin-notification` via the Rust `NotificationExt` trait or the JS `@tauri-apps\u002Fplugin-notification` API. Trigger notifications for:\n- Player mentions (another player mentions you in chat)\n- Combat events (damage, defeats)\n- NPC contact (lifecycle `onContact` triggers)\n\n### Window State\n\nThe `tauri-plugin-window-state` plugin automatically persists and restores window size\u002Fposition. No custom code needed — just register the plugin.\n\n### Keyboard Shortcuts\n\nDefine accelerators on menu items:\n- `CmdOrCtrl+L` → Clear Output\n- `CmdOrCtrl+K` → Focus Input\n\nHandle via the menu-action event flow (Rust emits → JS listens).\n\n### Discord Rich Presence (Opt-In)\n\nThe desktop app integrates Discord Rich Presence via direct local IPC to the Discord desktop client. **It is off by default** and gated behind a per-user preference (`discord_rich_presence` in `prefs.json`). No OAuth, no MUDdown-server involvement — the integration uses Discord's \"Rich Presence Without Authentication\" mode (`SetApplicationId` + `SET_ACTIVITY` only).\n\n#### Crate\n\n```toml\n# Cargo.toml\ndiscord-rich-presence = \"0.2\"   # vionya\u002Fdiscord-rich-presence, MIT\n```\n\n#### Rust state pattern (`src-tauri\u002Fsrc\u002Fdiscord_presence.rs`)\n\n```rust\npub struct PresenceState {\n    pub enabled: bool,\n    pub client: Option\u003CDiscordIpcClient>,\n    pub session_start: Option\u003Ci64>,\n}\npub fn initial_state() -> Mutex\u003CPresenceState> { \u002F* enabled: false *\u002F }\n```\n\nRegister in `lib.rs`:\n```rust\n.manage(discord_presence::initial_state())\n.invoke_handler(tauri::generate_handler![\n    discord_presence_set_enabled,\n    discord_presence_update,\n    discord_presence_clear,\n    \u002F* ... *\u002F\n])\n```\n\n#### Command contract\n\n`discord_presence_update` and `discord_presence_clear` early-return when `state.enabled == false`. `discord_presence_set_enabled` is the toggle itself, so it always runs and is idempotent against the cached `enabled` value. `discord_presence_update` lazily calls `ensure_connected()` — connection failures (Discord not running) are silent no-ops, not errors. On `set_activity` failure, drop the client so the next call retries a fresh connection. Toggling off via `discord_presence_set_enabled(false)` calls `ClearActivity` then closes the IPC client.\n\n#### JS scheduler pattern (`src\u002Fdiscord-presence.ts`)\n\nThe presence updater runs through `createPresenceScheduler({ invoke, debounceMs: 15000, ... })` which exposes `{ schedule, clear, flushForTesting }`. Key invariants:\n\n- **Leading-edge fire**: First call after `clear()` (or first call ever) invokes immediately. Critical: initialize `lastSentAt = Number.NEGATIVE_INFINITY` (not `0`) so the leading-edge check `(now - lastSentAt) >= debounceMs` is true at `t=0`. Reset to `NEGATIVE_INFINITY` inside `clear()`.\n- **Trailing flush**: Calls inside the 15-second window are coalesced into a single trailing invocation with the latest payload.\n- **Non-room messages ignored**: `parseRoomPresence` returns `null` for envelopes without `:::room` + H1 title; the scheduler drops those silently.\n- **15s floor**: Self-imposed throttle. Discord's published RPC docs recommend ≤1 update per 15s.\n\n#### Wiring into game flow\n\nIn `main.ts`:\n- Call `discord_presence_set_enabled` at startup with the persisted pref.\n- In the room-message handler (`appendMessage` where `className === \"room\"`), call `presenceScheduler.schedule(muddown)`.\n- On `onDisplaced` and on `onClose(willReconnect=false)`, call `presenceScheduler.clear()`.\n- The Settings panel toggle persists the pref *and* invokes `discord_presence_set_enabled` *and* calls `presenceScheduler.clear()` when disabling.\n\n#### What is shared \u002F not shared\n\nShared: region name, room title, session start timestamp, MUDdown logo + tooltip, two profile buttons. Deliberately **not** shared: character names, inventory, equipment, combat status, room IDs, account identifiers. Document any change to this list in `MUDdown.wiki\u002FDesktop-App.md` and `apps\u002Fwebsite\u002Fsrc\u002Fpages\u002Fprivacy.astro`.\n\n#### Multi-account caveat\n\nDiscord exposes IPC slots `discord-ipc-0` … `discord-ipc-9`; first-accept-wins. With multiple Discord accounts logged into the desktop client, presence appears on whichever account opened the slot first. Document, don't fight it.\n\n#### Application ID\n\n`APPLICATION_ID` in `discord_presence.rs` must match a Discord developer-portal application registered to MUDdown. Asset keys (`large_image: \"muddown-logo\"`) must be uploaded under that app's Rich Presence asset list.\n\n#### Testing\n\nTests live in `apps\u002Fdesktop\u002Ftests\u002Fdiscord-presence.test.ts` (vitest) and `apps\u002Fdesktop\u002Fsrc-tauri\u002Fsrc\u002Fdiscord_presence.rs` (`#[cfg(test)] mod tests`). Run with `npm test` and `cargo test --lib` respectively.\n\nJS unit coverage:\n- `parseRoomPresence`: room envelope yields `{ region, title }`; non-room envelopes (`:::system`, etc.) return `null`; rooms missing the H1 title return `null`; missing `region` attribute defaults to `\"Unknown\"`.\n- `createPresenceScheduler`: leading-edge fire at `t=0` (relies on `lastSentAt = Number.NEGATIVE_INFINITY`); trailing-flush coalescing of multiple `schedule()` calls inside the 15 s window into one `discord_presence_update`; non-room envelopes ignored; `clear()` invokes `discord_presence_clear`, cancels any pending timer, and resets state so the next `schedule()` fires immediately again; `onError(\"update\", err)` and `onError(\"clear\", err)` paths; failure does not advance `lastSentAt` (next schedule still leading-edges); `flushForTesting()` drains a queued trailing flush without advancing the clock.\n\nUse a fake clock (`makeFakeClock()` in the test file) to drive `now`, `setTimeout`, and `clearTimeout` deterministically. Microtask flushing for rejected `invoke` mocks needs two `await Promise.resolve()` hops.\n\nRust unit coverage (`cargo test --lib`):\n- `update_is_noop_when_disabled`: `discord_presence_update` returns immediately when `enabled == false`; no client is constructed.\n- `disable_clears_session_start`: toggling `enabled` from `true` → `false` clears `session_start` and `last_connect_failure`.\n- `ensure_connected_respects_cooldown`: a stamped `last_connect_failure` within `RECONNECT_COOLDOWN_SECS` causes `ensure_connected` to short-circuit so no IPC connect is attempted.\n\nManual smoke checklist before shipping any presence change:\n- Toggle on with Discord **not** running → no error log, scheduler still fires `schedule()` quietly, presence absent on profile.\n- Toggle on with Discord running → \"Playing MUDdown \u002F Exploring &lt;region&gt; \u002F &lt;Room Title&gt;\" appears within ~1 s, elapsed timer counts up, MUDdown logo + tooltip render on the profile expanded view.\n- Move between rooms rapidly → at most one update per 15 s window; final room title is what appears after the trailing flush.\n- Toggle off → activity disappears from profile within a couple of seconds.\n- Quit Discord while presence is live → integration silently no-ops, no retry-spam in the log; reconnect attempts cool down for 30 s.\n- Verify the Application ID (`1490036207340748960`) and asset key (`muddown-logo`) match the Discord developer portal — a typo in either field renders presence with a blank logo.\n\n## ARIA Accessibility\n\nApply ARIA roles per the MUDdown spec (§8) when rendering messages:\n\n| Block type | ARIA attributes |\n|------------|-----------------|\n| `room` | `role=\"main\"` |\n| `system` | `role=\"alert\"` |\n| `combat` | `role=\"log\"` `aria-live=\"polite\"` |\n| `dialogue` | `role=\"group\"` `aria-label=\"NPC dialogue\"` |\n\n## CSP Configuration\n\nIn `tauri.conf.json` → `app.security.csp`:\n- `connect-src`: Allow WebSocket to game server (`ws:\u002F\u002Flocalhost:3300`, `wss:\u002F\u002Fmuddown.com`) and HTTPS for OAuth\n- `style-src`: `'self' 'unsafe-inline'` + Google Fonts\n- `font-src`: Google Fonts CDN\n- `img-src`: `'self' data:`\n\nThe `unsafe-inline` for styles is required by the Tauri webview. This is a desktop app, not a web page, so the risk profile is different from a public website.\n\n## CI Build Matrix\n\nThe GitHub Actions workflow lives at `.github\u002Fworkflows\u002Fdesktop-build.yml`.\n\n### Triggers\n\n- Push to `main` touching `apps\u002Fdesktop\u002F**`, `packages\u002Fclient\u002F**`, `packages\u002Fshared\u002F**`, or the workflow file\n- PRs to `main` with the same path filters\n- Manual dispatch (`workflow_dispatch`)\n\n### Build Matrix\n\n| Platform | Target | Artifact |\n|----------|--------|----------|\n| `macos-latest` | `aarch64-apple-darwin` | `.dmg` |\n| `macos-latest` | `x86_64-apple-darwin` | `.dmg` |\n| `ubuntu-22.04` | `x86_64-unknown-linux-gnu` | `.AppImage`, `.deb` |\n| `windows-latest` | `x86_64-pc-windows-msvc` | `.msi` |\n\n### Key Steps\n\n1. Checkout, setup Node 22, install Rust stable with target\n2. Cache Rust deps with `Swatinem\u002Frust-cache@v2` (key by target, workspaces: `apps\u002Fdesktop\u002Fsrc-tauri`)\n3. Install Linux system deps (webkit2gtk, appindicator, rsvg, patchelf) — Ubuntu only\n4. `npm ci` → `npx turbo run build --filter=@muddown\u002Fclient... --filter=@muddown\u002Fshared...`\n5. `tauri-apps\u002Ftauri-action@v0` with signing secrets and Apple notarization secrets\n6. Upload artifacts via `actions\u002Fupload-artifact@v4`\n\n### Code Signing Secrets\n\n| Secret | Platform | Purpose |\n|--------|----------|---------|\n| `TAURI_SIGNING_PRIVATE_KEY` | All | Ed25519 key for update signing |\n| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | All | Optional password for the signing key |\n| `APPLE_CERTIFICATE` | macOS | Base64-encoded `.p12` Developer ID Application certificate |\n| `APPLE_CERTIFICATE_PASSWORD` | macOS | Password for the `.p12` file |\n| `APPLE_SIGNING_IDENTITY` | macOS | e.g. `Developer ID Application: Your Name (TEAMID)` |\n| `APPLE_ID` | macOS | Apple ID email for notarization submission |\n| `APPLE_PASSWORD` | macOS | App-specific password (not Apple ID password) |\n| `APPLE_TEAM_ID` | macOS | 10-character Apple Developer Team ID |\n\n## Apple Notarization (macOS)\n\nApple notarization is required for macOS distribution outside the App Store. Without it, Gatekeeper blocks the app. When the Apple secrets above are configured, `tauri-apps\u002Ftauri-action` automatically handles code signing, notarization submission, and stapling.\n\n### Entitlements\n\nThe entitlements plist at `src-tauri\u002FEntitlements.plist` grants hardened-runtime permissions required by the WebView:\n- `com.apple.security.cs.allow-jit` — WebKit JIT\n- `com.apple.security.cs.allow-unsigned-executable-memory` — JavaScriptCore executable memory allocation (required alongside allow-jit under the hardened runtime)\n\n> **Note:** `com.apple.security.network.client` is an App Sandbox entitlement, not a hardened-runtime entitlement. It is unnecessary for Developer ID distribution since the hardened runtime does not restrict outbound networking.\n\n### Bundle Configuration\n\nIn `tauri.conf.json` → `bundle.macOS`:\n- `minimumSystemVersion`: `\"10.13\"` — macOS High Sierra minimum\n- `entitlements`: `\"Entitlements.plist\"` — path relative to `src-tauri\u002F`\n- `dmg` — DMG window layout (app icon position, Applications folder shortcut)\n\n### CI Verification\n\nThe CI pipeline verifies notarization via `xcrun stapler validate` on the `.dmg`. This step runs only on macOS runners and only when Apple secrets are configured. Full setup instructions are in `apps\u002Fdesktop\u002FUPDATER_KEYS.md`.\n\n## Auto-Updater & Signature Verification\n\n### Configuration (`tauri.conf.json`)\n\n```json\n{\n  \"plugins\": {\n    \"updater\": {\n      \"active\": true,\n      \"pubkey\": \"\u003CEd25519 public key>\",\n      \"endpoints\": [\n        \"https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Freleases\u002Flatest\u002Fdownload\u002Flatest.json\"\n      ]\n    }\n  }\n}\n```\n\n- The endpoint points to the `latest.json` manifest published with each GitHub Release.\n\n### Runtime Validation Points\n\n1. **Rust layer** (`tauri-plugin-updater`): Automatically validates every downloaded artifact against `pubkey` before applying. Built into the plugin — no custom Rust code needed.\n2. **JS layer** (`@tauri-apps\u002Fplugin-updater`): The `check()` and `downloadAndInstall()` APIs surface the plugin's result. Use them to show update prompts and handle validation failures.\n\nIf a signature does not match (tampering, forged release, key mismatch), the update is **rejected** and the app stays on its current version.\n\n### Key Generation\n\n```bash\nnpx @tauri-apps\u002Fcli signer generate -w ~\u002F.tauri\u002Fmuddown.key\n```\n\nProduces:\n- `~\u002F.tauri\u002Fmuddown.key` — private key (store as `TAURI_SIGNING_PRIVATE_KEY` secret)\n- `~\u002F.tauri\u002Fmuddown.key.pub` — public key (put in `tauri.conf.json` → `plugins.updater.pubkey`)\n\n### Key Rotation Procedure\n\nFull procedure in `apps\u002Fdesktop\u002FUPDATER_KEYS.md`:\n\n1. Generate a new Ed25519 key pair.\n2. Update `tauri.conf.json` → `plugins.updater.pubkey`.\n3. Update `TAURI_SIGNING_PRIVATE_KEY` GitHub Actions secret.\n4. Update `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` if password-protected.\n5. Create a new signed release. Clients running the new version will reject old-key artifacts.\n\n> **Note:** The CI signature verification step reads the public key dynamically from `tauri.conf.json`, so no workflow file update is needed during rotation.\n\n### Integration Test Plan\n\nThe CI workflow includes a signature verification test (`apps\u002Fdesktop\u002Ftests\u002Fverify-signature.sh`) that:\n1. Locates all `.sig` files produced by `tauri build`.\n2. Verifies each signature against the project's public key using `minisign`.\n3. Tampers with each artifact and verifies the updater **rejects** the invalid signature.\n\nThe test runs on macOS and Linux CI targets after the build step. It is skipped when `TAURI_SIGNING_PRIVATE_KEY` is not configured (e.g., on forks).\n\n## Icons\n\n- Must be **8-bit\u002Fchannel RGBA** PNG (Tauri crashes on 16-bit\u002Fchannel).\n- Convert with ImageMagick if needed: `magick icon.png -depth 8 icon.png`\n- Required formats: `icon.png` (512×512), `icon.ico` (Windows), `icon.icns` (macOS)\n- Store in `apps\u002Fdesktop\u002Fsrc-tauri\u002Ficons\u002F`\n\n## Acceptance Criteria\n\nBefore considering desktop app work complete:\n\n- [ ] `npm run dev` in `apps\u002Fdesktop` launches the Tauri dev window\n- [ ] `npm run tauri build` produces platform-appropriate artifacts\n- [ ] TypeScript compiles clean: `cd apps\u002Fdesktop && npx tsc --noEmit`\n- [ ] All workspace tests pass: `npx turbo run test`\n- [ ] ARIA roles match the spec (room=main, system=alert, combat=log, dialogue=group)\n- [ ] Menu bar actions work (File, View, Help submenus)\n- [ ] System tray shows\u002Fhides the window\n- [ ] Keyboard shortcuts (Cmd\u002FCtrl+L, Cmd\u002FCtrl+K) work\n- [ ] Window position persists across restarts\n- [ ] Notifications fire for mentions, combat, NPC contact\n- [ ] CSP allows WebSocket to game server and OAuth endpoints\n- [ ] CI workflow builds all 4 targets without errors\n- [ ] `tauri.conf.json` updater has `pubkey` populated (when active)\n- [ ] CI signature verification test passes (valid accepted, tampered rejected)\n- [ ] `UPDATER_KEYS.md` documents key rotation\n\n## Traceability\n\nThis skill covers the Phase 5 Tauri desktop app checklist in `PROJECT_PLAN.md` (lines ~225–243). See that document for the full roadmap context and remaining items.\n","# Desktop App Skill\n\nYou are working on the MUDdown Tauri v2 desktop app (`apps\u002Fdesktop`). This skill covers the scaffolding layout, workspace wiring, CI\u002FCD build matrix, auto-updater signature verification, and native OS integrations.\n\n## Scaffold Layout\n\n```\napps\u002Fdesktop\u002F\n├── src\u002F\n│   ├── main.ts              # Game client (auth, characters, WebSocket, rendering)\n│   ├── index.html           # Single-page app shell\n│   └── styles.css           # Dark terminal theme (CSS custom properties)\n├── src-tauri\u002F\n│   ├── src\u002Flib.rs           # Rust backend (menu, tray, Tauri commands)\n│   ├── Cargo.toml           # Rust dependencies\n│   ├── tauri.conf.json      # App config, CSP, updater settings\n│   ├── build.rs             # Tauri build script\n│   └── icons\u002F               # App icons (PNG, ICO, ICNS for each platform)\n├── vite.config.ts           # Vite bundler (port 1420, strictPort)\n├── package.json             # @muddown\u002Fdesktop\n├── tsconfig.json            # TypeScript (noEmit: true)\n└── UPDATER_KEYS.md          # Ed25519 key rotation procedure\n```\n\n## Turborepo Wiring\n\nThe desktop app is a workspace package at `apps\u002Fdesktop`. Add it to the root `package.json` workspaces array if not present.\n\n### Workspace Dependencies\n\nIn `apps\u002Fdesktop\u002Fpackage.json`:\n```json\n{\n  \"dependencies\": {\n    \"@muddown\u002Fclient\": \"*\",\n    \"@muddown\u002Fshared\": \"*\",\n    \"@tauri-apps\u002Fapi\": \"^2.0.0\",\n    \"@tauri-apps\u002Fplugin-dialog\": \"^2.0.0\",\n    \"@tauri-apps\u002Fplugin-notification\": \"^2.0.0\",\n    \"@tauri-apps\u002Fplugin-store\": \"^2.0.0\",\n    \"@tauri-apps\u002Fplugin-updater\": \"^2.0.0\"\n  }\n}\n```\n\n### Vite Aliases\n\nIn `vite.config.ts`, resolve workspace packages to their source directories:\n```ts\nresolve: {\n  alias: {\n    \"@muddown\u002Fclient\": path.resolve(__dirname, \"..\u002F..\u002Fpackages\u002Fclient\u002Fsrc\"),\n    \"@muddown\u002Fshared\": path.resolve(__dirname, \"..\u002F..\u002Fpackages\u002Fshared\u002Fsrc\"),\n  },\n}\n```\n\n### Build Order\n\nBuild workspace deps before the Tauri app:\n```bash\nnpx turbo run build --filter=@muddown\u002Fclient... --filter=@muddown\u002Fshared...\n```\n\n## Rust Backend (`src-tauri\u002Fsrc\u002Flib.rs`)\n\n### Tauri Commands\n\nExpose custom commands to the JS frontend with `#[tauri::command]`:\n\n| Command | Purpose | Error handling |\n|---------|---------|----------------|\n| `set_window_title` | Update native title bar | `if let Err(e)` → `eprintln!` |\n| `send_notification` | Send OS notification | Best-effort `let _` (permission denial expected) |\n\nRegister commands in the builder:\n```rust\n.invoke_handler(tauri::generate_handler![set_window_title, send_notification])\n```\n\n### Tauri Plugins\n\nRegister plugins in `tauri::Builder::default()`:\n```rust\n.plugin(tauri_plugin_dialog::init())\n.plugin(tauri_plugin_notification::init())\n.plugin(tauri_plugin_window_state::Builder::default().build())\n.plugin(tauri_plugin_store::Builder::default().build())\n.plugin(tauri_plugin_updater::Builder::default().build())\n```\n\n### Cargo Dependencies\n\nIn `apps\u002Fdesktop\u002Fsrc-tauri\u002FCargo.toml`:\n```toml\n[dependencies]\ntauri = { version = \"2\", features = [\"tray-icon\", \"image-png\"] }\ntauri-plugin-dialog = \"2\"\ntauri-plugin-notification = \"2\"\ntauri-plugin-window-state = \"2\"\ntauri-plugin-updater = \"2\"\ntauri-plugin-store = \"2\"\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n```\n\n### Error Handling Conventions\n\n- Tauri commands: use `if let Err(e) = ... { eprintln!(\"[context] message: {e}\"); }` for recoverable errors.\n- Menu event emitting: check `Err` and log with `eprintln!`.\n- Tray show\u002Ffocus: check `Err` on `w.show()` and `w.set_focus()` and log.\n- Notifications: `let _` is acceptable (permission denial is expected, not an error).\n- App startup: use `.unwrap_or_else(|e| panic!(\"...: {}\", e))` on `.run()` to preserve the error message.\n\n## Native OS Integrations\n\n### Menu Bar\n\nBuild menus in the `.setup()` closure using `SubmenuBuilder` and `MenuItemBuilder`:\n- **File**: Connect, Disconnect, Quit\n- **View**: Clear Output (Cmd\u002FCtrl+L), Focus Input (Cmd\u002FCtrl+K), Toggle Inventory, Toggle Hints\n- **Help**: Game Commands, About MUDdown\n\nForward menu actions to JS via `handle.emit(\"menu-action\", id)`. In JS, listen with `listen(\"menu-action\", ...)`.\n\n### System Tray\n\nUse `TrayIconBuilder` with a PNG icon and a context menu:\n- **Show MUDdown** — `w.show()` + `w.set_focus()`\n- **Quit** — `handle.exit(0)`\n\nUpdate the tray tooltip from JS to reflect connection status (e.g., \"MUDdown — Connected\" \u002F \"MUDdown — Disconnected\").\n\n### Notifications\n\nUse `tauri-plugin-notification` via the Rust `NotificationExt` trait or the JS `@tauri-apps\u002Fplugin-notification` API. Trigger notifications for:\n- Player mentions (another player mentions you in chat)\n- Combat events (damage, defeats)\n- NPC contact (lifecycle `onContact` triggers)\n\n### Window State\n\nThe `tauri-plugin-window-state` plugin automatically persists and restores window size\u002Fposition. No custom code needed — just register the plugin.\n\n### Keyboard Shortcuts\n\nDefine accelerators on menu items:\n- `CmdOrCtrl+L` → Clear Output\n- `CmdOrCtrl+K` → Focus Input\n\nHandle via the menu-action event flow (Rust emits → JS listens).\n\n### Discord Rich Presence (Opt-In)\n\nThe desktop app integrates Discord Rich Presence via direct local IPC to the Discord desktop client. **It is off by default** and gated behind a per-user preference (`discord_rich_presence` in `prefs.json`). No OAuth, no MUDdown-server involvement — the integration uses Discord's \"Rich Presence Without Authentication\" mode (`SetApplicationId` + `SET_ACTIVITY` only).\n\n#### Crate\n\n```toml\n# Cargo.toml\ndiscord-rich-presence = \"0.2\"   # vionya\u002Fdiscord-rich-presence, MIT\n```\n\n#### Rust state pattern (`src-tauri\u002Fsrc\u002Fdiscord_presence.rs`)\n\n```rust\npub struct PresenceState {\n    pub enabled: bool,\n    pub client: Option\u003CDiscordIpcClient>,\n    pub session_start: Option\u003Ci64>,\n}\npub fn initial_state() -> Mutex\u003CPresenceState> { \u002F* enabled: false *\u002F }\n```\n\nRegister in `lib.rs`:\n```rust\n.manage(discord_presence::initial_state())\n.invoke_handler(tauri::generate_handler![\n    discord_presence_set_enabled,\n    discord_presence_update,\n    discord_presence_clear,\n    \u002F* ... *\u002F\n])\n```\n\n#### Command contract\n\n`discord_presence_update` and `discord_presence_clear` early-return when `state.enabled == false`. `discord_presence_set_enabled` is the toggle itself, so it always runs and is idempotent against the cached `enabled` value. `discord_presence_update` lazily calls `ensure_connected()` — connection failures (Discord not running) are silent no-ops, not errors. On `set_activity` failure, drop the client so the next call retries a fresh connection. Toggling off via `discord_presence_set_enabled(false)` calls `ClearActivity` then closes the IPC client.\n\n#### JS scheduler pattern (`src\u002Fdiscord-presence.ts`)\n\nThe presence updater runs through `createPresenceScheduler({ invoke, debounceMs: 15000, ... })` which exposes `{ schedule, clear, flushForTesting }`. Key invariants:\n\n- **Leading-edge fire**: First call after `clear()` (or first call ever) invokes immediately. Critical: initialize `lastSentAt = Number.NEGATIVE_INFINITY` (not `0`) so the leading-edge check `(now - lastSentAt) >= debounceMs` is true at `t=0`. Reset to `NEGATIVE_INFINITY` inside `clear()`.\n- **Trailing flush**: Calls inside the 15-second window are coalesced into a single trailing invocation with the latest payload.\n- **Non-room messages ignored**: `parseRoomPresence` returns `null` for envelopes without `:::room` + H1 title; the scheduler drops those silently.\n- **15s floor**: Self-imposed throttle. Discord's published RPC docs recommend ≤1 update per 15s.\n\n#### Wiring into game flow\n\nIn `main.ts`:\n- Call `discord_presence_set_enabled` at startup with the persisted pref.\n- In the room-message handler (`appendMessage` where `className === \"room\"`), call `presenceScheduler.schedule(muddown)`.\n- On `onDisplaced` and on `onClose(willReconnect=false)`, call `presenceScheduler.clear()`.\n- The Settings panel toggle persists the pref *and* invokes `discord_presence_set_enabled` *and* calls `presenceScheduler.clear()` when disabling.\n\n#### What is shared \u002F not shared\n\nShared: region name, room title, session start timestamp, MUDdown logo + tooltip, two profile buttons. Deliberately **not** shared: character names, inventory, equipment, combat status, room IDs, account identifiers. Document any change to this list in `MUDdown.wiki\u002FDesktop-App.md` and `apps\u002Fwebsite\u002Fsrc\u002Fpages\u002Fprivacy.astro`.\n\n#### Multi-account caveat\n\nDiscord exposes IPC slots `discord-ipc-0` … `discord-ipc-9`; first-accept-wins. With multiple Discord accounts logged into the desktop client, presence appears on whichever account opened the slot first. Document, don't fight it.\n\n#### Application ID\n\n`APPLICATION_ID` in `discord_presence.rs` must match a Discord developer-portal application registered to MUDdown. Asset keys (`large_image: \"muddown-logo\"`) must be uploaded under that app's Rich Presence asset list.\n\n#### Testing\n\nTests live in `apps\u002Fdesktop\u002Ftests\u002Fdiscord-presence.test.ts` (vitest) and `apps\u002Fdesktop\u002Fsrc-tauri\u002Fsrc\u002Fdiscord_presence.rs` (`#[cfg(test)] mod tests`). Run with `npm test` and `cargo test --lib` respectively.\n\nJS unit coverage:\n- `parseRoomPresence`: room envelope yields `{ region, title }`; non-room envelopes (`:::system`, etc.) return `null`; rooms missing the H1 title return `null`; missing `region` attribute defaults to `\"Unknown\"`.\n- `createPresenceScheduler`: leading-edge fire at `t=0` (relies on `lastSentAt = Number.NEGATIVE_INFINITY`); trailing-flush coalescing of multiple `schedule()` calls inside the 15 s window into one `discord_presence_update`; non-room envelopes ignored; `clear()` invokes `discord_presence_clear`, cancels any pending timer, and resets state so the next `schedule()` fires immediately again; `onError(\"update\", err)` and `onError(\"clear\", err)` paths; failure does not advance `lastSentAt` (next schedule still leading-edges); `flushForTesting()` drains a queued trailing flush without advancing the clock.\n\nUse a fake clock (`makeFakeClock()` in the test file) to drive `now`, `setTimeout`, and `clearTimeout` deterministically. Microtask flushing for rejected `invoke` mocks needs two `await Promise.resolve()` hops.\n\nRust unit coverage (`cargo test --lib`):\n- `update_is_noop_when_disabled`: `discord_presence_update` returns immediately when `enabled == false`; no client is constructed.\n- `disable_clears_session_start`: toggling `enabled` from `true` → `false` clears `session_start` and `last_connect_failure`.\n- `ensure_connected_respects_cooldown`: a stamped `last_connect_failure` within `RECONNECT_COOLDOWN_SECS` causes `ensure_connected` to short-circuit so no IPC connect is attempted.\n\nManual smoke checklist before shipping any presence change:\n- Toggle on with Discord **not** running → no error log, scheduler still fires `schedule()` quietly, presence absent on profile.\n- Toggle on with Discord running → \"Playing MUDdown \u002F Exploring &lt;region&gt; \u002F &lt;Room Title&gt;\" appears within ~1 s, elapsed timer counts up, MUDdown logo + tooltip render on the profile expanded view.\n- Move between rooms rapidly → at most one update per 15 s window; final room title is what appears after the trailing flush.\n- Toggle off → activity disappears from profile within a couple of seconds.\n- Quit Discord while presence is live → integration silently no-ops, no retry-spam in the log; reconnect attempts cool down for 30 s.\n- Verify the Application ID (`1490036207340748960`) and asset key (`muddown-logo`) match the Discord developer portal — a typo in either field renders presence with a blank logo.\n\n## ARIA Accessibility\n\nApply ARIA roles per the MUDdown spec (§8) when rendering messages:\n\n| Block type | ARIA attributes |\n|------------|-----------------|\n| `room` | `role=\"main\"` |\n| `system` | `role=\"alert\"` |\n| `combat` | `role=\"log\"` `aria-live=\"polite\"` |\n| `dialogue` | `role=\"group\"` `aria-label=\"NPC dialogue\"` |\n\n## CSP Configuration\n\nIn `tauri.conf.json` → `app.security.csp`:\n- `connect-src`: Allow WebSocket to game server (`ws:\u002F\u002Flocalhost:3300`, `wss:\u002F\u002Fmuddown.com`) and HTTPS for OAuth\n- `style-src`: `'self' 'unsafe-inline'` + Google Fonts\n- `font-src`: Google Fonts CDN\n- `img-src`: `'self' data:`\n\nThe `unsafe-inline` for styles is required by the Tauri webview. This is a desktop app, not a web page, so the risk profile is different from a public website.\n\n## CI Build Matrix\n\nThe GitHub Actions workflow lives at `.github\u002Fworkflows\u002Fdesktop-build.yml`.\n\n### Triggers\n\n- Push to `main` touching `apps\u002Fdesktop\u002F**`, `packages\u002Fclient\u002F**`, `packages\u002Fshared\u002F**`, or the workflow file\n- PRs to `main` with the same path filters\n- Manual dispatch (`workflow_dispatch`)\n\n### Build Matrix\n\n| Platform | Target | Artifact |\n|----------|--------|----------|\n| `macos-latest` | `aarch64-apple-darwin` | `.dmg` |\n| `macos-latest` | `x86_64-apple-darwin` | `.dmg` |\n| `ubuntu-22.04` | `x86_64-unknown-linux-gnu` | `.AppImage`, `.deb` |\n| `windows-latest` | `x86_64-pc-windows-msvc` | `.msi` |\n\n### Key Steps\n\n1. Checkout, setup Node 22, install Rust stable with target\n2. Cache Rust deps with `Swatinem\u002Frust-cache@v2` (key by target, workspaces: `apps\u002Fdesktop\u002Fsrc-tauri`)\n3. Install Linux system deps (webkit2gtk, appindicator, rsvg, patchelf) — Ubuntu only\n4. `npm ci` → `npx turbo run build --filter=@muddown\u002Fclient... --filter=@muddown\u002Fshared...`\n5. `tauri-apps\u002Ftauri-action@v0` with signing secrets and Apple notarization secrets\n6. Upload artifacts via `actions\u002Fupload-artifact@v4`\n\n### Code Signing Secrets\n\n| Secret | Platform | Purpose |\n|--------|----------|---------|\n| `TAURI_SIGNING_PRIVATE_KEY` | All | Ed25519 key for update signing |\n| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | All | Optional password for the signing key |\n| `APPLE_CERTIFICATE` | macOS | Base64-encoded `.p12` Developer ID Application certificate |\n| `APPLE_CERTIFICATE_PASSWORD` | macOS | Password for the `.p12` file |\n| `APPLE_SIGNING_IDENTITY` | macOS | e.g. `Developer ID Application: Your Name (TEAMID)` |\n| `APPLE_ID` | macOS | Apple ID email for notarization submission |\n| `APPLE_PASSWORD` | macOS | App-specific password (not Apple ID password) |\n| `APPLE_TEAM_ID` | macOS | 10-character Apple Developer Team ID |\n\n## Apple Notarization (macOS)\n\nApple notarization is required for macOS distribution outside the App Store. Without it, Gatekeeper blocks the app. When the Apple secrets above are configured, `tauri-apps\u002Ftauri-action` automatically handles code signing, notarization submission, and stapling.\n\n### Entitlements\n\nThe entitlements plist at `src-tauri\u002FEntitlements.plist` grants hardened-runtime permissions required by the WebView:\n- `com.apple.security.cs.allow-jit` — WebKit JIT\n- `com.apple.security.cs.allow-unsigned-executable-memory` — JavaScriptCore executable memory allocation (required alongside allow-jit under the hardened runtime)\n\n> **Note:** `com.apple.security.network.client` is an App Sandbox entitlement, not a hardened-runtime entitlement. It is unnecessary for Developer ID distribution since the hardened runtime does not restrict outbound networking.\n\n### Bundle Configuration\n\nIn `tauri.conf.json` → `bundle.macOS`:\n- `minimumSystemVersion`: `\"10.13\"` — macOS High Sierra minimum\n- `entitlements`: `\"Entitlements.plist\"` — path relative to `src-tauri\u002F`\n- `dmg` — DMG window layout (app icon position, Applications folder shortcut)\n\n### CI Verification\n\nThe CI pipeline verifies notarization via `xcrun stapler validate` on the `.dmg`. This step runs only on macOS runners and only when Apple secrets are configured. Full setup instructions are in `apps\u002Fdesktop\u002FUPDATER_KEYS.md`.\n\n## Auto-Updater & Signature Verification\n\n### Configuration (`tauri.conf.json`)\n\n```json\n{\n  \"plugins\": {\n    \"updater\": {\n      \"active\": true,\n      \"pubkey\": \"\u003CEd25519 public key>\",\n      \"endpoints\": [\n        \"https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Freleases\u002Flatest\u002Fdownload\u002Flatest.json\"\n      ]\n    }\n  }\n}\n```\n\n- The endpoint points to the `latest.json` manifest published with each GitHub Release.\n\n### Runtime Validation Points\n\n1. **Rust layer** (`tauri-plugin-updater`): Automatically validates every downloaded artifact against `pubkey` before applying. Built into the plugin — no custom Rust code needed.\n2. **JS layer** (`@tauri-apps\u002Fplugin-updater`): The `check()` and `downloadAndInstall()` APIs surface the plugin's result. Use them to show update prompts and handle validation failures.\n\nIf a signature does not match (tampering, forged release, key mismatch), the update is **rejected** and the app stays on its current version.\n\n### Key Generation\n\n```bash\nnpx @tauri-apps\u002Fcli signer generate -w ~\u002F.tauri\u002Fmuddown.key\n```\n\nProduces:\n- `~\u002F.tauri\u002Fmuddown.key` — private key (store as `TAURI_SIGNING_PRIVATE_KEY` secret)\n- `~\u002F.tauri\u002Fmuddown.key.pub` — public key (put in `tauri.conf.json` → `plugins.updater.pubkey`)\n\n### Key Rotation Procedure\n\nFull procedure in `apps\u002Fdesktop\u002FUPDATER_KEYS.md`:\n\n1. Generate a new Ed25519 key pair.\n2. Update `tauri.conf.json` → `plugins.updater.pubkey`.\n3. Update `TAURI_SIGNING_PRIVATE_KEY` GitHub Actions secret.\n4. Update `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` if password-protected.\n5. Create a new signed release. Clients running the new version will reject old-key artifacts.\n\n> **Note:** The CI signature verification step reads the public key dynamically from `tauri.conf.json`, so no workflow file update is needed during rotation.\n\n### Integration Test Plan\n\nThe CI workflow includes a signature verification test (`apps\u002Fdesktop\u002Ftests\u002Fverify-signature.sh`) that:\n1. Locates all `.sig` files produced by `tauri build`.\n2. Verifies each signature against the project's public key using `minisign`.\n3. Tampers with each artifact and verifies the updater **rejects** the invalid signature.\n\nThe test runs on macOS and Linux CI targets after the build step. It is skipped when `TAURI_SIGNING_PRIVATE_KEY` is not configured (e.g., on forks).\n\n## Icons\n\n- Must be **8-bit\u002Fchannel RGBA** PNG (Tauri crashes on 16-bit\u002Fchannel).\n- Convert with ImageMagick if needed: `magick icon.png -depth 8 icon.png`\n- Required formats: `icon.png` (512×512), `icon.ico` (Windows), `icon.icns` (macOS)\n- Store in `apps\u002Fdesktop\u002Fsrc-tauri\u002Ficons\u002F`\n\n## Acceptance Criteria\n\nBefore considering desktop app work complete:\n\n- [ ] `npm run dev` in `apps\u002Fdesktop` launches the Tauri dev window\n- [ ] `npm run tauri build` produces platform-appropriate artifacts\n- [ ] TypeScript compiles clean: `cd apps\u002Fdesktop && npx tsc --noEmit`\n- [ ] All workspace tests pass: `npx turbo run test`\n- [ ] ARIA roles match the spec (room=main, system=alert, combat=log, dialogue=group)\n- [ ] Menu bar actions work (File, View, Help submenus)\n- [ ] System tray shows\u002Fhides the window\n- [ ] Keyboard shortcuts (Cmd\u002FCtrl+L, Cmd\u002FCtrl+K) work\n- [ ] Window position persists across restarts\n- [ ] Notifications fire for mentions, combat, NPC contact\n- [ ] CSP allows WebSocket to game server and OAuth endpoints\n- [ ] CI workflow builds all 4 targets without errors\n- [ ] `tauri.conf.json` updater has `pubkey` populated (when active)\n- [ ] CI signature verification test passes (valid accepted, tampered rejected)\n- [ ] `UPDATER_KEYS.md` documents key rotation\n\n## Traceability\n\nThis skill covers the Phase 5 Tauri desktop app checklist in `PROJECT_PLAN.md` (lines ~225–243). See that document for the full roadmap context and remaining items.\n",{"name":5,"description":7},"main","003024ca8f1bd677ce1cf158084703aa98867375",".github\u002Fskills\u002Fdesktop-app","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown","https:\u002F\u002Fwww.muddown.com","https:\u002F\u002Fgithub.com\u002FMUDdown","2026-08-06 08:37:36",{"source":8,"skillName":5,"commands":51,"prompt":61,"zipAvailable":62},[52,55,58],{"tool":53,"command":54},"npx","npx skills add MUDdown\u002FMUDdown --skill desktop-app",{"tool":56,"command":57},"bunx","bunx skills add MUDdown\u002FMUDdown --skill desktop-app",{"tool":59,"command":60},"pnpm","pnpm dlx skills add MUDdown\u002FMUDdown --skill desktop-app","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fdesktop-app\u002FSKILL.md",true,[],[65,92,112,133,155,182],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":79,"publishedAt":38},"01M6JZ5NT4RRHMMEDQTJE0BQFK","testing","Write and maintain tests for MUDdown packages. Covers vitest for the server and parser, fixture helpers, world integrity tests, and testing conventions.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Ftesting\u002FSKILL.md",[71,76],{"categoryNo":72,"categoryName":73,"taxonomyType":74,"taxonomyTypeDesc":75},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":77,"categoryName":78,"taxonomyType":74,"taxonomyTypeDesc":75},"01JBSEEDCAT000000000000010","后端与接口",[67,80,81,82,83,84,85,86,21,87,88,89,90,91],"integrity","for","tests","testing conventions","fixture","conventions","world","write","parser","vitest","server","packages",{"skillNo":93,"name":94,"nameZh":6,"description":95,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":98,"publishedAt":38},"01M6JZ5JM4ZZHTZ2S93S00GCKY","room-creation","Create and modify MUDdown room files. Covers the Markdown+YAML structure, frontmatter fields, container blocks, bidirectional exits, item\u002FNPC references, lighting, and region organization.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Froom-creation\u002FSKILL.md",[],[99,100,101,102,103,104,105,106,107,108,109,110,111],"room creation","creation","create","yaml","markdown","container","frontmatter","blocks","lighting","structure","organization","references","files",{"skillNo":113,"name":114,"nameZh":6,"description":115,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":116,"categories":117,"keywords":123,"publishedAt":38},"01M6JZ5FHRMJ7DFRWS3H3TQY6Y","privacy","Audit and maintain privacy compliance for MUDdown. Covers the privacy policy, data collection practices, cookie\u002Fstorage usage, and compliance testing.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fprivacy\u002FSKILL.md",[118],{"categoryNo":119,"categoryName":120,"taxonomyType":121,"taxonomyTypeDesc":122},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[114,124,125,81,126,127,21,128,129,130,131,132,67],"privacy policy","collection","usage","data collection","data","compliance","audit","storage","policy",{"skillNo":134,"name":135,"nameZh":6,"description":136,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":137,"categories":138,"keywords":140,"publishedAt":38},"01M6JZ5C6GZ0H6AD33FJY399M3","persistence","Work with the database abstraction layer and account\u002Fcharacter\u002Fworld persistence. Covers the GameDatabase interface, SQLite adapter, save\u002Fload lifecycle, auth sessions, and testing patterns.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fpersistence\u002FSKILL.md",[139],{"categoryNo":72,"categoryName":73,"taxonomyType":74,"taxonomyTypeDesc":75},[135,141,142,143,144,145,146,86,147,148,149,150,151,152,153,25,154,67],"interface","layer","load","patterns","auth","adapter","character","work","save","sqlite","lifecycle","testing patterns","database","sessions",{"skillNo":156,"name":157,"nameZh":6,"description":158,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":159,"categories":160,"keywords":165,"publishedAt":38},"01M6JZ5836SXKR67RCMC83VRDJ","osc8-bridge","Add or modify OSC 8 hyperlink capabilities in the telnet bridge — NEW-ENVIRON (RFC 1572) negotiation, Mudlet\u002FFado\u002FMudForge `send:` URIs, and per-scheme tooltip\u002Fmenu metadata. Covers the capability pipeline from telnet negotiation through renderer output, the sanitization contract, and the word-wrap invariant.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fosc8-bridge\u002FSKILL.md",[161,164],{"categoryNo":162,"categoryName":163,"taxonomyType":74,"taxonomyTypeDesc":75},"01JBSEEDCAT000000000000002","文档处理",{"categoryNo":119,"categoryName":120,"taxonomyType":121,"taxonomyTypeDesc":122},[166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181],"osc8 bridge","bridge","Word","RFC","pipeline","negotiation","metadata","capabilities","wrap","renderer","sanitization","scheme","output","add","osc","contract",{"skillNo":183,"name":184,"nameZh":6,"description":185,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":186,"categories":187,"keywords":188,"publishedAt":38},"01M6JZ548282CBBAQBAJ8HNPG2","oauth-provider","Add a new OAuth\u002FOIDC identity provider to MUDdown. Covers the shared type, auth switch statements, server config, login page button, env vars, tests, and local verification.","https:\u002F\u002Fgithub.com\u002FMUDdown\u002FMUDdown\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Foauth-provider\u002FSKILL.md",[],[189,190,191,82,192,179,193,194,195,196,197,198,199,200,90,37,201,202,145],"provider","oauth provider","OAuth","login","env","type","shared","local","switch","page","oidc","env vars","identity","config"]