security: add timing-safe comparisons and fix dependency CVEs

- Create shared safeEqual() utility using timingSafeEqual (src/security/safe-equal.ts)
- Fix hook token comparison in server-http.ts to use safeEqual()
- Fix node pairing token in node-pairing.ts to use safeEqual()
- Fix audit token in audit-extra.ts to use safeEqual()
- Refactor gateway auth.ts to import from shared utility
- Bump tar 7.5.4→7.5.7 (CVE GHSA-34x7-hfp2-rc4v: hardlink path traversal)
- Bump hono 4.11.4→4.11.7 (XSS via ErrorBoundary + static middleware key read)
- Add pnpm.overrides for matrix transitive deps (form-data→2.5.5, qs→6.14.1)
This commit is contained in:
issuemakerable 2026-01-29 19:28:33 +09:00
parent 01e0d3a320
commit 19823c5498
8 changed files with 116 additions and 165 deletions

View File

@ -23,7 +23,7 @@
- When Peter asks for links, reply with full `https://docs.molt.bot/...` URLs (not root-relative).
- When you touch docs, end the reply with the `https://docs.molt.bot/...` URLs you referenced.
- README (GitHub): keep absolute docs URLs (`https://docs.molt.bot/...`) so links work on GitHub.
- Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and “gateway host”.
- Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and "gateway host".
## exe.dev VM ops (general)
- Access: stable path is `ssh exe.dev` then `ssh vm-name` (assume SSH key already set).
@ -45,32 +45,69 @@
- Node remains supported for running built output (`dist/*`) and production installs.
- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch. Release checklist: `docs/platforms/mac/release.md`.
- Type-check/build: `pnpm build` (tsc)
- Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt)
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
- Lint: `pnpm lint` (oxlint with `--type-aware`). Fix: `pnpm lint:fix`.
- Format check: `pnpm format` (oxfmt `--check`). Fix: `pnpm format:fix` (oxfmt `--write`).
- Tests: `pnpm test` (vitest via parallel runner); coverage: `pnpm test:coverage`.
- Run a **single test file**: `npx vitest run src/path/to/file.test.ts` (or `bunx vitest run ...`).
- Run tests **matching a name**: `npx vitest run -t "test name pattern"`.
- Watch mode: `pnpm test:watch`.
- E2E tests: `pnpm test:e2e` (vitest with `vitest.e2e.config.ts`).
- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live`.
- Full local gate (run before landing PRs): `pnpm lint && pnpm build && pnpm test`.
- CI runs: lint, format check, build (tsc), tests (node + bun), protocol check — all on every push/PR.
## Coding Style & Naming Conventions
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
- Formatting/linting via Oxlint and Oxfmt; run `pnpm lint` before commits.
- Language: TypeScript (ESM, `"type": "module"`). `strict: true` in tsconfig.
- Target: ES2022, NodeNext module resolution.
- Formatting/linting via **oxlint** and **oxfmt** (not eslint/prettier); run `pnpm lint` before commits.
- oxlint config: `.oxlintrc.json` — plugins: `unicorn`, `typescript`, `oxc`; correctness category = error.
- Add brief code comments for tricky or non-obvious logic.
- Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`.
- Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability.
- Keep files concise; extract helpers instead of "V2" copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`.
- Aim to keep files under ~500 LOC (guideline, not hard guardrail). `pnpm check:loc` enforces `--max 500`.
- Naming: use **Moltbot** for product/app/docs headings; use `moltbot` for CLI command, package/binary, paths, and config keys.
### Import Conventions
- Node built-ins: use `node:` prefix (e.g. `import fs from "node:fs"`, `import path from "node:path"`).
- Order: node built-ins → external packages → local modules (separated by blank lines).
- Use `import type { Foo } from "..."` for type-only imports; inline `type` in mixed imports when only some are types: `import { type Foo, bar } from "..."`.
- Local imports use `.js` extension (ESM requirement): `import { foo } from "./bar.js"`.
- Barrel re-exports allowed in module index files (e.g. `src/config/config.ts` re-exports from submodules).
### TypeScript Patterns
- Strict mode with `noEmitOnError`. No `any`, `@ts-ignore`, or `@ts-expect-error`.
- Validation: Zod (`zod` v4) for config/schema validation. TypeBox (`@sinclair/typebox`) for tool input schemas.
- Use `as const` for constant arrays/objects; derive types with `typeof X[number]`.
- Avoid `Type.Union` in tool schemas; use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists.
- Avoid raw `format` property names in tool schemas (reserved keyword in some validators).
- Prefer `Type.Optional(...)` over `... | null` in tool schemas.
### Naming
- Files: `kebab-case.ts` (e.g. `session-utils.ts`, `parse-log-line.ts`).
- Functions/variables: `camelCase` (e.g. `loadConfig`, `resolveMainSessionKey`).
- Types/interfaces: `PascalCase` (e.g. `MoltbotConfig`, `SessionEntry`, `ChannelMeta`).
- Constants: `UPPER_SNAKE_CASE` for true constants (e.g. `DEFAULT_LOG_DIR`, `MAX_LOG_AGE_MS`).
- Test files: `*.test.ts` colocated with source. E2E: `*.e2e.test.ts`. Live: `*.live.test.ts`.
### Error Handling
- Prefer explicit error types; avoid empty `catch {}` blocks.
- Logging errors: use `tslog` via `src/logging/logger.ts`; never hand-roll loggers.
- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don't hand-roll spinners/bars.
- Status output: keep tables + ANSI-safe wrapping (`src/terminal/table.ts`).
- Colors: use shared CLI palette in `src/terminal/palette.ts` (no hardcoded ANSI colors).
### Testing Patterns
- Framework: Vitest (`describe`/`it`/`expect`). Test timeout: 120s. Pool: `forks`.
- Mocking: `vi.fn()`, `vi.mock()`. Tests use a global setup file (`test/setup.ts`) that installs an isolated test home and plugin registry.
- Assertions: `expect(x).toBe(y)`, `expect(fn).toHaveBeenCalledTimes(n)`, `expect(promise).rejects.toThrow("msg")`.
- Coverage: V8 provider, thresholds 70% lines/functions/statements, 55% branches.
- Do not set test workers above 16.
- Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior.
## Release Channels (Naming)
- stable: tagged releases only (e.g. `vYYYY.M.D`), npm dist-tag `latest`.
- beta: prerelease tags `vYYYY.M.D-beta.N`, npm dist-tag `beta` (may ship without macOS app).
- dev: moving head on `main` (no tag; git checkout main).
## Testing Guidelines
- Framework: Vitest with V8 coverage thresholds (70% lines/branches/functions/statements).
- Naming: match source names with `*.test.ts`; e2e in `*.e2e.test.ts`.
- Run `pnpm test` (or `pnpm test:coverage`) before pushing when you touch logic.
- Do not set test workers above 16; tried already.
- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (Moltbot-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
- Full kit + whats covered: `docs/testing.md`.
- Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior or the user asks for one.
- Mobile: before using a simulator, check for connected real devices (iOS + Android) and prefer them when available.
## Commit & Pull Request Guidelines
- Create commits with `scripts/committer "<msg>" <file...>`; avoid manual `git add`/`git commit` so staging stays scoped.
- Follow concise, action-oriented commit messages (e.g., `CLI: add verbose flag to send`).
@ -81,12 +118,12 @@
- PR review calls: prefer a single `gh pr view --json ...` to batch metadata/comments; run `gh pr diff` only when needed.
- Before starting a review when a GH Issue/PR is pasted: run `git pull`; if there are local changes or unpushed commits, stop and alert the user before reviewing.
- Goal: merge PRs. Prefer **rebase** when commits are clean; **squash** when history is messy.
- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless its truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`.
- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless it's truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`.
- If you review a PR and later do work on it, land via merge/squash (no direct-main commits) and always add the PR author as a co-contributor.
- When working on a PR: add a changelog entry with the PR number and thank the contributor.
- When working on an issue: reference the issue in the changelog entry.
- When merging a PR: leave a PR comment that explains exactly what we did and include the SHA hashes.
- When merging a PR from a new contributor: add their avatar to the README “Thanks to all clawtributors” thumbnail list.
- When merging a PR from a new contributor: add their avatar to the README "Thanks to all clawtributors" thumbnail list.
- After merging a PR: run `bun scripts/update-clawtributors.ts` if the contributor is missing, then commit the regenerated README.
## Shorthand Commands
@ -115,15 +152,15 @@
- Never update the Carbon dependency.
- Any dependency with `pnpm.patchedDependencies` must use an exact version (no `^`/`~`).
- Patching dependencies (pnpm patches, overrides, or vendored changes) requires explicit approval; do not do this by default.
- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); dont hand-roll spinners/bars.
- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don't hand-roll spinners/bars.
- Status output: keep tables + ANSI-safe wrapping (`src/terminal/table.ts`); `status --all` = read-only/pasteable, `status --deep` = probes.
- Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the Moltbot Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep moltbot` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
- macOS logs: use `./scripts/clawlog.sh` to query unified logs for the Moltbot subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
- If shared guardrails are available locally, review them; otherwise follow this repo's guidance.
- SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; dont introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code.
- SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; don't introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code.
- Connection providers: when adding a new connection, update every UI surface and docs (macOS app, web UI, mobile if applicable, onboarding/overview docs) and add matching status + configuration forms so provider lists and settings stay in sync.
- Version locations: `package.json` (CLI), `apps/android/app/build.gradle.kts` (versionName/versionCode), `apps/ios/Sources/Info.plist` + `apps/ios/Tests/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `apps/macos/Sources/Moltbot/Resources/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `docs/install/updating.md` (pinned npm version), `docs/platforms/mac/release.md` (APP_VERSION/APP_BUILD examples), Peekaboo Xcode projects/Info.plists (MARKETING_VERSION/CURRENT_PROJECT_VERSION).
- **Restart apps:** “restart iOS/Android apps” means rebuild (recompile/install) and relaunch, not just kill/launch.
- **Restart apps:** "restart iOS/Android apps" means rebuild (recompile/install) and relaunch, not just kill/launch.
- **Device checks:** before testing, verify connected real devices (iOS/Android) before reaching for simulators/emulators.
- iOS Team ID lookup: `security find-identity -p codesigning -v` → use Apple Development (…) TEAMID. Fallback: `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`.
- A2UI bundle hash: `src/canvas-host/a2ui/.bundle.hash` is auto-generated; ignore unexpected changes, and only regenerate via `pnpm canvas:a2ui:bundle` (or `scripts/bundle-a2ui.sh`) when needed. Commit the hash as a separate commit.
@ -140,19 +177,19 @@
- If commit/push already requested, auto-stage and include formatting-only follow-ups in the same commit (or a tiny follow-up commit if needed), no extra confirmation.
- Only ask when changes are semantic (logic/data/behavior).
- Lobster seam: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed.
- **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief “other files present” note only if relevant.
- **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief "other files present" note only if relevant.
- Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause.
- Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed).
- Tool schema guardrails (google-antigravity): avoid `Type.Union` in tool input schemas; no `anyOf`/`oneOf`/`allOf`. Use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists, and `Type.Optional(...)` instead of `... | null`. Keep top-level tool schema as `type: "object"` with `properties`.
- Tool schema guardrails: avoid raw `format` property names in tool schemas; some validators treat `format` as a reserved keyword and reject the schema.
- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/agents/<agentId>/sessions/*.jsonl` (use the `agent=<id>` value in the Runtime line of the system prompt; newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there.
- When asked to open a "session" file, open the Pi session logs under `~/.clawdbot/agents/<agentId>/sessions/*.jsonl` (use the `agent=<id>` value in the Runtime line of the system prompt; newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there.
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac.
- Never send streaming/partial replies to external messaging surfaces (WhatsApp, Telegram); only final replies should be delivered there. Streaming/tool events may still go to internal UIs/control channel.
- Voice wake forwarding tips:
- Command template should stay `moltbot-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Dont add extra quotes.
- launchd PATH is minimal; ensure the apps launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`moltbot` binaries resolve when invoked via `moltbot-mac`.
- For manual `moltbot message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tools escaping.
- Release guardrails: do not change version numbers without operators explicit consent; always ask permission before running any npm publish/release step.
- Command template should stay `moltbot-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don't add extra quotes.
- launchd PATH is minimal; ensure the app's launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`moltbot` binaries resolve when invoked via `moltbot-mac`.
- For manual `moltbot message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool's escaping.
- Release guardrails: do not change version numbers without operator's explicit consent; always ask permission before running any npm publish/release step.
## NPM + 1Password (publish/verify)
- Use the 1password skill; all `op` commands must run inside a fresh tmux session.

View File

@ -186,7 +186,7 @@
"express": "^5.2.1",
"file-type": "^21.3.0",
"grammy": "^1.39.3",
"hono": "4.11.4",
"hono": "4.11.7",
"jiti": "^2.6.1",
"json5": "^2.2.3",
"jszip": "^3.10.1",
@ -201,7 +201,7 @@
"qrcode-terminal": "^0.12.0",
"sharp": "^0.34.5",
"sqlite-vec": "0.1.7-alpha.2",
"tar": "7.5.4",
"tar": "7.5.7",
"tslog": "^4.10.2",
"undici": "^7.19.0",
"ws": "^8.19.0",
@ -242,14 +242,16 @@
"wireit": "^0.14.12"
},
"overrides": {
"tar": "7.5.4"
"tar": "7.5.7"
},
"pnpm": {
"minimumReleaseAge": 2880,
"overrides": {
"@sinclair/typebox": "0.34.47",
"hono": "4.11.4",
"tar": "7.5.4"
"hono": "4.11.7",
"tar": "7.5.7",
"request>form-data": "2.5.5",
"request>qs": "6.14.1"
}
},
"vitest": {

149
pnpm-lock.yaml generated
View File

@ -6,8 +6,10 @@ settings:
overrides:
'@sinclair/typebox': 0.34.47
hono: 4.11.4
tar: 7.5.4
hono: 4.11.7
tar: 7.5.7
request>form-data: 2.5.5
request>qs: 6.14.1
importers:
@ -21,7 +23,7 @@ importers:
version: 3.975.0
'@buape/carbon':
specifier: 0.14.0
version: 0.14.0(hono@4.11.4)
version: 0.14.0(hono@4.11.7)
'@clack/prompts':
specifier: ^0.11.0
version: 0.11.0
@ -110,8 +112,8 @@ importers:
specifier: ^1.39.3
version: 1.39.3
hono:
specifier: 4.11.4
version: 4.11.4
specifier: 4.11.7
version: 4.11.7
jiti:
specifier: ^2.6.1
version: 2.6.1
@ -155,8 +157,8 @@ importers:
specifier: 0.1.7-alpha.2
version: 0.1.7-alpha.2
tar:
specifier: 7.5.4
version: 7.5.4
specifier: 7.5.7
version: 7.5.7
tslog:
specifier: ^4.10.2
version: 4.10.2
@ -383,12 +385,12 @@ importers:
'@microsoft/agents-hosting-extensions-teams':
specifier: ^1.2.2
version: 1.2.2
moltbot:
specifier: workspace:*
version: link:../..
express:
specifier: ^5.2.1
version: 5.2.1
moltbot:
specifier: workspace:*
version: link:../..
proper-lockfile:
specifier: ^4.1.2
version: 4.1.2
@ -1090,7 +1092,7 @@ packages:
resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
engines: {node: '>=18.14.1'}
peerDependencies:
hono: 4.11.4
hono: 4.11.7
'@huggingface/jinja@0.5.3':
resolution: {integrity: sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA==}
@ -3214,11 +3216,6 @@ packages:
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
clawdbot@2026.1.24-3:
resolution: {integrity: sha512-zt9BzhWXduq8ZZR4rfzQDurQWAgmijTTyPZCQGrn5ew6wCEwhxxEr2/NHG7IlCwcfRsKymsY4se9KMhoNz0JtQ==}
engines: {node: '>=22.12.0'}
hasBin: true
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
@ -3632,10 +3629,6 @@ packages:
forever-agent@0.6.1:
resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
form-data@2.3.3:
resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
engines: {node: '>= 0.12'}
form-data@2.5.5:
resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==}
engines: {node: '>= 0.12'}
@ -3793,8 +3786,8 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
hono@4.11.4:
resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==}
hono@4.11.7:
resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==}
engines: {node: '>=16.9.0'}
hookified@1.15.0:
@ -4809,10 +4802,6 @@ packages:
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
engines: {node: '>=0.6'}
qs@6.5.3:
resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
engines: {node: '>=0.6'}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -5190,8 +5179,8 @@ packages:
tailwindcss@4.1.17:
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
tar@7.5.4:
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==}
tar@7.5.7:
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
engines: {node: '>=18'}
thenify-all@1.6.0:
@ -6433,14 +6422,14 @@ snapshots:
'@borewit/text-codec@0.2.1': {}
'@buape/carbon@0.14.0(hono@4.11.4)':
'@buape/carbon@0.14.0(hono@4.11.7)':
dependencies:
'@types/node': 25.0.10
discord-api-types: 0.38.37
optionalDependencies:
'@cloudflare/workers-types': 4.20260120.0
'@discordjs/voice': 0.19.0
'@hono/node-server': 1.19.9(hono@4.11.4)
'@hono/node-server': 1.19.9(hono@4.11.7)
'@types/bun': 1.3.6
'@types/ws': 8.18.1
ws: 8.19.0
@ -6696,9 +6685,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@hono/node-server@1.19.9(hono@4.11.4)':
'@hono/node-server@1.19.9(hono@4.11.7)':
dependencies:
hono: 4.11.4
hono: 4.11.7
optional: true
'@huggingface/jinja@0.5.3':
@ -9098,84 +9087,6 @@ snapshots:
dependencies:
clsx: 2.1.1
clawdbot@2026.1.24-3(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3):
dependencies:
'@agentclientprotocol/sdk': 0.13.1(zod@4.3.6)
'@aws-sdk/client-bedrock': 3.975.0
'@buape/carbon': 0.14.0(hono@4.11.4)
'@clack/prompts': 0.11.0
'@grammyjs/runner': 2.0.3(grammy@1.39.3)
'@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3)
'@homebridge/ciao': 1.3.4
'@line/bot-sdk': 10.6.0
'@lydell/node-pty': 1.2.0-beta.3
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-tui': 0.49.3
'@mozilla/readability': 0.6.0
'@sinclair/typebox': 0.34.47
'@slack/bolt': 4.6.0(@types/express@5.0.6)
'@slack/web-api': 7.13.0
'@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5)
ajv: 8.17.1
body-parser: 2.2.2
chalk: 5.6.2
chokidar: 5.0.0
chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482)
cli-highlight: 2.1.11
commander: 14.0.2
croner: 9.1.0
detect-libc: 2.1.2
discord-api-types: 0.38.37
dotenv: 17.2.3
express: 5.2.1
file-type: 21.3.0
grammy: 1.39.3
hono: 4.11.4
jiti: 2.6.1
json5: 2.2.3
jszip: 3.10.1
linkedom: 0.18.12
long: 5.3.2
markdown-it: 14.1.0
node-edge-tts: 1.2.9
osc-progress: 0.3.0
pdfjs-dist: 5.4.530
playwright-core: 1.58.0
proper-lockfile: 4.1.2
qrcode-terminal: 0.12.0
sharp: 0.34.5
sqlite-vec: 0.1.7-alpha.2
tar: 7.5.4
tslog: 4.10.2
undici: 7.19.0
ws: 8.19.0
yaml: 2.8.2
zod: 4.3.6
optionalDependencies:
'@napi-rs/canvas': 0.1.88
node-llama-cpp: 3.15.0(typescript@5.9.3)
transitivePeerDependencies:
- '@discordjs/opus'
- '@modelcontextprotocol/sdk'
- '@types/express'
- audio-decode
- aws-crt
- bufferutil
- canvas
- debug
- devtools-protocol
- encoding
- ffmpeg-static
- jimp
- link-preview-js
- node-opus
- opusscript
- supports-color
- typescript
- utf-8-validate
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
@ -9217,7 +9128,7 @@ snapshots:
npmlog: 6.0.2
rc: 1.2.8
semver: 7.7.3
tar: 7.5.4
tar: 7.5.7
url-join: 4.0.1
which: 2.0.2
yargs: 17.7.2
@ -9650,12 +9561,6 @@ snapshots:
forever-agent@0.6.1: {}
form-data@2.3.3:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
form-data@2.5.5:
dependencies:
asynckit: 0.4.0
@ -9851,7 +9756,7 @@ snapshots:
highlight.js@11.11.1: {}
hono@4.11.4: {}
hono@4.11.7: {}
hookified@1.15.0: {}
@ -10930,8 +10835,6 @@ snapshots:
dependencies:
side-channel: 1.1.0
qs@6.5.3: {}
queue-microtask@1.2.3: {}
quick-format-unescaped@4.0.4: {}
@ -11035,7 +10938,7 @@ snapshots:
combined-stream: 1.0.8
extend: 3.0.2
forever-agent: 0.6.1
form-data: 2.3.3
form-data: 2.5.5
har-validator: 5.1.5
http-signature: 1.2.0
is-typedarray: 1.0.0
@ -11044,7 +10947,7 @@ snapshots:
mime-types: 2.1.35
oauth-sign: 0.9.0
performance-now: 2.1.0
qs: 6.5.3
qs: 6.14.1
safe-buffer: 5.2.1
tough-cookie: 2.5.0
tunnel-agent: 0.6.0
@ -11470,7 +11373,7 @@ snapshots:
tailwindcss@4.1.17: {}
tar@7.5.4:
tar@7.5.7:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0

View File

@ -1,7 +1,7 @@
import { timingSafeEqual } from "node:crypto";
import type { IncomingMessage } from "node:http";
import type { GatewayAuthConfig, GatewayTailscaleMode } from "../config/config.js";
import { readTailscaleWhoisIdentity, type TailscaleWhoisIdentity } from "../infra/tailscale.js";
import { safeEqual } from "../security/safe-equal.js";
import { isTrustedProxyAddress, parseForwardedForClientIp, resolveGatewayClientIp } from "./net.js";
export type ResolvedGatewayAuthMode = "token" | "password";
@ -32,11 +32,6 @@ type TailscaleUser = {
type TailscaleWhoisLookup = (ip: string) => Promise<TailscaleWhoisIdentity | null>;
function safeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
function normalizeLogin(login: string): string {
return login.trim().toLowerCase();
}

View File

@ -14,6 +14,7 @@ import type { createSubsystemLogger } from "../logging/subsystem.js";
import { handleSlackHttpRequest } from "../slack/http/index.js";
import { resolveAgentAvatar } from "../agents/identity-avatar.js";
import { handleControlUiAvatarRequest, handleControlUiHttpRequest } from "./control-ui.js";
import { safeEqual } from "../security/safe-equal.js";
import {
extractHookToken,
getHookChannelError,
@ -77,7 +78,7 @@ export function createHooksRequestHandler(
}
const { token, fromQuery } = extractHookToken(req, url);
if (!token || token !== hooksConfig.token) {
if (!token || !hooksConfig.token || !safeEqual(token, hooksConfig.token)) {
res.statusCode = 401;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Unauthorized");

View File

@ -1,7 +1,9 @@
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { resolveStateDir } from "../config/paths.js";
import { safeEqual } from "../security/safe-equal.js";
export type NodePairingPendingRequest = {
requestId: string;
@ -268,7 +270,7 @@ export async function verifyNodeToken(
const normalized = normalizeNodeId(nodeId);
const node = state.pairedByNodeId[normalized];
if (!node) return { ok: false };
return node.token === token ? { ok: true, node } : { ok: false };
return safeEqual(node.token, token) ? { ok: true, node } : { ok: false };
}
export async function updatePairedNodeMetadata(

View File

@ -9,6 +9,7 @@ import { resolveNativeSkillsEnabled } from "../config/commands.js";
import { resolveOAuthDir } from "../config/paths.js";
import { formatCliCommand } from "../cli/command-format.js";
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { safeEqual } from "./safe-equal.js";
import type { AgentToolsConfig } from "../config/types.tools.js";
import { resolveBrowserConfig } from "../browser/config.js";
import { isToolAllowedByPolicies } from "../agents/pi-tools.policy.js";
@ -181,7 +182,7 @@ export function collectHooksHardeningFindings(cfg: MoltbotConfig): SecurityAudit
gatewayAuth.token.trim()
? gatewayAuth.token.trim()
: null;
if (token && gatewayToken && token === gatewayToken) {
if (token && gatewayToken && safeEqual(token, gatewayToken)) {
findings.push({
checkId: "hooks.token_reuse_gateway_token",
severity: "warn",

View File

@ -0,0 +1,10 @@
import { timingSafeEqual } from "node:crypto";
/**
* Timing-safe string comparison. Use this for any secret/token comparison
* to prevent timing attacks. Returns false if either string is empty.
*/
export function safeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
}