mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 12:38:38 +00:00
Matrix: fix secrets scan false positives
This commit is contained in:
@@ -205,7 +205,7 @@
|
|||||||
"filename": "apps/macos/Sources/OpenClawProtocol/GatewayModels.swift",
|
"filename": "apps/macos/Sources/OpenClawProtocol/GatewayModels.swift",
|
||||||
"hashed_secret": "7990585255d25249fb1e6eac3d2bd6c37429b2cd",
|
"hashed_secret": "7990585255d25249fb1e6eac3d2bd6c37429b2cd",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 1859
|
"line_number": 1763
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"apps/macos/Tests/OpenClawIPCTests/AnthropicAuthResolverTests.swift": [
|
"apps/macos/Tests/OpenClawIPCTests/AnthropicAuthResolverTests.swift": [
|
||||||
@@ -266,7 +266,7 @@
|
|||||||
"filename": "apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
|
"filename": "apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
|
||||||
"hashed_secret": "7990585255d25249fb1e6eac3d2bd6c37429b2cd",
|
"hashed_secret": "7990585255d25249fb1e6eac3d2bd6c37429b2cd",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 1859
|
"line_number": 1763
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"docs/.i18n/zh-CN.tm.jsonl": [
|
"docs/.i18n/zh-CN.tm.jsonl": [
|
||||||
@@ -9626,15 +9626,6 @@
|
|||||||
"line_number": 65
|
"line_number": 65
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"docs/channels/matrix.md": [
|
|
||||||
{
|
|
||||||
"type": "Secret Keyword",
|
|
||||||
"filename": "docs/channels/matrix.md",
|
|
||||||
"hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6",
|
|
||||||
"is_verified": false,
|
|
||||||
"line_number": 60
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"docs/channels/nextcloud-talk.md": [
|
"docs/channels/nextcloud-talk.md": [
|
||||||
{
|
{
|
||||||
"type": "Secret Keyword",
|
"type": "Secret Keyword",
|
||||||
@@ -9774,63 +9765,63 @@
|
|||||||
"filename": "docs/gateway/configuration-reference.md",
|
"filename": "docs/gateway/configuration-reference.md",
|
||||||
"hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e",
|
"hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 1614
|
"line_number": 1615
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Secret Keyword",
|
"type": "Secret Keyword",
|
||||||
"filename": "docs/gateway/configuration-reference.md",
|
"filename": "docs/gateway/configuration-reference.md",
|
||||||
"hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770",
|
"hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 1630
|
"line_number": 1631
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Secret Keyword",
|
"type": "Secret Keyword",
|
||||||
"filename": "docs/gateway/configuration-reference.md",
|
"filename": "docs/gateway/configuration-reference.md",
|
||||||
"hashed_secret": "7f8aaf142ce0552c260f2e546dda43ddd7c9aef3",
|
"hashed_secret": "7f8aaf142ce0552c260f2e546dda43ddd7c9aef3",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 1817
|
"line_number": 1818
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Secret Keyword",
|
"type": "Secret Keyword",
|
||||||
"filename": "docs/gateway/configuration-reference.md",
|
"filename": "docs/gateway/configuration-reference.md",
|
||||||
"hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27",
|
"hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 1990
|
"line_number": 1991
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Secret Keyword",
|
"type": "Secret Keyword",
|
||||||
"filename": "docs/gateway/configuration-reference.md",
|
"filename": "docs/gateway/configuration-reference.md",
|
||||||
"hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0",
|
"hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 2046
|
"line_number": 2047
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Secret Keyword",
|
"type": "Secret Keyword",
|
||||||
"filename": "docs/gateway/configuration-reference.md",
|
"filename": "docs/gateway/configuration-reference.md",
|
||||||
"hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd",
|
"hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 2278
|
"line_number": 2279
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Secret Keyword",
|
"type": "Secret Keyword",
|
||||||
"filename": "docs/gateway/configuration-reference.md",
|
"filename": "docs/gateway/configuration-reference.md",
|
||||||
"hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6",
|
"hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 2408
|
"line_number": 2409
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Secret Keyword",
|
"type": "Secret Keyword",
|
||||||
"filename": "docs/gateway/configuration-reference.md",
|
"filename": "docs/gateway/configuration-reference.md",
|
||||||
"hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281",
|
"hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 2661
|
"line_number": 2662
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Secret Keyword",
|
"type": "Secret Keyword",
|
||||||
"filename": "docs/gateway/configuration-reference.md",
|
"filename": "docs/gateway/configuration-reference.md",
|
||||||
"hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25",
|
"hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 2663
|
"line_number": 2664
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"docs/gateway/configuration.md": [
|
"docs/gateway/configuration.md": [
|
||||||
@@ -11048,7 +11039,7 @@
|
|||||||
"filename": "extensions/matrix/src/matrix/accounts.test.ts",
|
"filename": "extensions/matrix/src/matrix/accounts.test.ts",
|
||||||
"hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4",
|
"hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 74
|
"line_number": 78
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"extensions/matrix/src/matrix/client.test.ts": [
|
"extensions/matrix/src/matrix/client.test.ts": [
|
||||||
@@ -11057,14 +11048,14 @@
|
|||||||
"filename": "extensions/matrix/src/matrix/client.test.ts",
|
"filename": "extensions/matrix/src/matrix/client.test.ts",
|
||||||
"hashed_secret": "fe7fcdaea49ece14677acd32374d2f1225819d5c",
|
"hashed_secret": "fe7fcdaea49ece14677acd32374d2f1225819d5c",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 13
|
"line_number": 24
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Secret Keyword",
|
"type": "Secret Keyword",
|
||||||
"filename": "extensions/matrix/src/matrix/client.test.ts",
|
"filename": "extensions/matrix/src/matrix/client.test.ts",
|
||||||
"hashed_secret": "3dc927d80543dc0f643940b70d066bd4b4c4b78e",
|
"hashed_secret": "3dc927d80543dc0f643940b70d066bd4b4c4b78e",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 23
|
"line_number": 34
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"extensions/matrix/src/matrix/client/storage.ts": [
|
"extensions/matrix/src/matrix/client/storage.ts": [
|
||||||
@@ -11073,7 +11064,7 @@
|
|||||||
"filename": "extensions/matrix/src/matrix/client/storage.ts",
|
"filename": "extensions/matrix/src/matrix/client/storage.ts",
|
||||||
"hashed_secret": "7505d64a54e061b7acd54ccd58b49dc43500b635",
|
"hashed_secret": "7505d64a54e061b7acd54ccd58b49dc43500b635",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 8
|
"line_number": 11
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"extensions/memory-lancedb/config.ts": [
|
"extensions/memory-lancedb/config.ts": [
|
||||||
@@ -11659,7 +11650,7 @@
|
|||||||
"filename": "src/agents/tools/web-search.ts",
|
"filename": "src/agents/tools/web-search.ts",
|
||||||
"hashed_secret": "dfba7aade0868074c2861c98e2a9a92f3178a51b",
|
"hashed_secret": "dfba7aade0868074c2861c98e2a9a92f3178a51b",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 291
|
"line_number": 292
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"src/agents/tools/web-tools.enabled-defaults.e2e.test.ts": [
|
"src/agents/tools/web-tools.enabled-defaults.e2e.test.ts": [
|
||||||
@@ -13013,5 +13004,5 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"generated_at": "2026-03-10T03:11:06Z"
|
"generated_at": "2026-03-09T12:14:50Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ Password-based setup (token is cached after login):
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
homeserver: "https://matrix.example.org",
|
homeserver: "https://matrix.example.org",
|
||||||
userId: "@bot:example.org",
|
userId: "@bot:example.org",
|
||||||
password: "replace-me",
|
password: "replace-me", // pragma: allowlist secret
|
||||||
deviceName: "OpenClaw Gateway",
|
deviceName: "OpenClaw Gateway",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "Replace the legacy Matrix plugin with the new Matrix implementation while preserving the public matrix surface and providing automatic migration for current users."
|
|
||||||
owner: "gumadeiras"
|
|
||||||
status: "implemented"
|
|
||||||
last_updated: "2026-03-08"
|
|
||||||
title: "Matrix Supersession Migration"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Matrix Supersession Migration
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This plan replaces the current public `matrix` plugin with the newer Matrix implementation that currently lives under `matrix-js`.
|
|
||||||
|
|
||||||
The external result should feel like an in-place upgrade for existing Matrix users:
|
|
||||||
|
|
||||||
- package stays `@openclaw/matrix`
|
|
||||||
- plugin/channel/binding id stays `matrix`
|
|
||||||
- config stays under `channels.matrix`
|
|
||||||
- existing public Matrix state stays canonical
|
|
||||||
- automatic migration handles everything deterministic
|
|
||||||
- non-deterministic cases surface clear, exact next steps
|
|
||||||
|
|
||||||
This plan is also the working implementation tracker. Update the checklist statuses as tasks land.
|
|
||||||
|
|
||||||
## Progress tracker
|
|
||||||
|
|
||||||
### Current status
|
|
||||||
|
|
||||||
- [x] Migration plan written and tracked in repo
|
|
||||||
- [x] Replace `extensions/matrix` with the new implementation
|
|
||||||
- [x] Remove shipped `matrix-js` public/runtime/package surfaces
|
|
||||||
- [x] Preserve legacy `matrix` config compatibility
|
|
||||||
- [x] Preserve legacy `matrix` state compatibility
|
|
||||||
- [x] Add startup and doctor migration/repair UX
|
|
||||||
- [x] Rewrite docs/help/tests to use `matrix` only
|
|
||||||
- [x] Verify update flow end to end
|
|
||||||
|
|
||||||
### Change log
|
|
||||||
|
|
||||||
- 2026-03-08: Initial supersession plan written and added to docs as a live checklist.
|
|
||||||
- 2026-03-08: Replaced `extensions/matrix` with the new Matrix implementation, removed shipped `matrix-js` surfaces, added startup/doctor Matrix migration UX, and deleted `extensions/matrix-js`.
|
|
||||||
- 2026-03-08: Added encrypted-state migration prep for legacy Matrix rust crypto stores, automatic backup-key extraction, startup room-key restore, and explicit warnings for local-only keys that cannot be exported automatically.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- Replace the current `extensions/matrix` implementation with the current Matrix-js implementation, then delete `extensions/matrix-js`.
|
|
||||||
- Ship the new implementation only as `matrix`: same npm package (`@openclaw/matrix`), same plugin id (`matrix`), same channel id (`matrix`), same config key (`channels.matrix`), same docs path (`/channels/matrix`), same local install path (`extensions/matrix`).
|
|
||||||
- Do not ship any `matrix-js` runtime compatibility aliases, config aliases, CLI aliases, gateway-method aliases, or package aliases.
|
|
||||||
- Preserve existing public `matrix` user configs and state as the source of truth. The migration should feel like an in-place upgrade, not a channel rename.
|
|
||||||
- Use automatic repair on startup and in doctor/update. If repair cannot be done safely, emit explicit, actionable messaging.
|
|
||||||
|
|
||||||
## Public surface after cutover
|
|
||||||
|
|
||||||
- [x] Canonical package/install surface stays `@openclaw/matrix` and `openclaw plugins install @openclaw/matrix`.
|
|
||||||
- [x] Canonical channel/plugin/binding id is `matrix`.
|
|
||||||
- [x] Canonical config namespace is `channels.matrix`.
|
|
||||||
- [x] Canonical CLI surface is `openclaw matrix ...`, including the verification/account commands currently only exposed under Matrix-js.
|
|
||||||
- [x] Canonical gateway methods become `matrix.verify.status`, `matrix.verify.bootstrap`, and `matrix.verify.recoveryKey`.
|
|
||||||
- [x] Canonical ACP/subagent binding channel is `matrix`.
|
|
||||||
- [x] Canonical plugin SDK subpath is `openclaw/plugin-sdk/matrix`.
|
|
||||||
- [x] Remove all shipped/public `matrix-js` references from docs, config help, tests, install catalog metadata, and package exports.
|
|
||||||
|
|
||||||
## Migration flow and UX
|
|
||||||
|
|
||||||
### Standard npm users
|
|
||||||
|
|
||||||
- [x] No config key change required.
|
|
||||||
- [x] No plugin install record rewrite required because the package remains `@openclaw/matrix`.
|
|
||||||
- [x] Updating OpenClaw or running `openclaw plugins update` replaces the plugin in place.
|
|
||||||
- [x] Startup and doctor automatically repair any legacy Matrix config/state that the new implementation cannot consume directly.
|
|
||||||
|
|
||||||
### Startup behavior
|
|
||||||
|
|
||||||
- [x] Keep the existing startup auto-migration model.
|
|
||||||
- [x] On first startup after upgrade, detect legacy Matrix config/state mismatches and repair them automatically when the repair is deterministic and local.
|
|
||||||
- [x] Log a concise one-time summary of what was migrated and only show next steps when user action is still required.
|
|
||||||
|
|
||||||
### Doctor and update behavior
|
|
||||||
|
|
||||||
- [x] `openclaw doctor --fix` and update-triggered doctor run the same Matrix migration logic, but with richer user-facing output.
|
|
||||||
- [x] Doctor shows exactly which Matrix paths/keys were changed and why.
|
|
||||||
- [x] Doctor validates the installed Matrix plugin source and surfaces manual repair steps for custom path installs.
|
|
||||||
|
|
||||||
### Custom or local-path installs
|
|
||||||
|
|
||||||
- [x] Do not auto-rewrite arbitrary custom plugin paths.
|
|
||||||
- [x] If the legacy Matrix plugin was installed from a custom path and that path is now stale or missing, warn clearly and print the exact replacement command or path to use.
|
|
||||||
- [x] If the custom path is valid and already points at the replacement plugin, leave it alone.
|
|
||||||
|
|
||||||
### Unsupported scope
|
|
||||||
|
|
||||||
- [x] No backward compatibility for internal `matrix-js` adopters.
|
|
||||||
- [x] Do not auto-migrate `channels.matrix-js`, `@openclaw/matrix-js`, `openclaw matrix-js`, or `plugins.entries["matrix-js"]`.
|
|
||||||
|
|
||||||
## Implementation changes
|
|
||||||
|
|
||||||
### 1. Replace identity at the package and plugin layer
|
|
||||||
|
|
||||||
- [x] Overwrite `extensions/matrix` with the Matrix-js implementation instead of renaming user-facing config.
|
|
||||||
- [x] Delete `extensions/matrix-js` after the port is complete.
|
|
||||||
- [x] Update `extensions/matrix/package.json`, `extensions/matrix/openclaw.plugin.json`, and `extensions/matrix/index.ts` so the package remains `@openclaw/matrix` but exposes the new feature set.
|
|
||||||
- [x] Port the Matrix-js CLI and gateway-method registration into `extensions/matrix/index.ts` and register it under `matrix`, not `matrix-js`.
|
|
||||||
- [x] Replace all internal `openclaw/plugin-sdk/matrix-js` imports with `openclaw/plugin-sdk/matrix`.
|
|
||||||
- [x] Replace the plugin SDK implementation behind `src/plugin-sdk/matrix.ts` with the Matrix-js helper surface superset, then remove the `matrix-js` plugin-sdk export from `package.json`, `scripts/check-plugin-sdk-exports.mjs`, `scripts/write-plugin-sdk-entry-dts.ts`, and related release/build checks.
|
|
||||||
|
|
||||||
### 2. Preserve legacy `matrix` config compatibility
|
|
||||||
|
|
||||||
- [x] Make the new `matrix` plugin accept the current public legacy `channels.matrix` schema as-is.
|
|
||||||
- [x] Keep support for top-level single-account `channels.matrix.*`.
|
|
||||||
- [x] Keep support for `channels.matrix.accounts.*`.
|
|
||||||
- [x] Keep support for `channels.matrix.defaultAccount`.
|
|
||||||
- [x] Keep support for the legacy `rooms` alias.
|
|
||||||
- [x] Keep support for existing DM and group policy keys.
|
|
||||||
- [x] Keep support for existing bindings that use `match.channel: "matrix"`.
|
|
||||||
- [x] Preserve SecretRef password inputs used by the legacy plugin.
|
|
||||||
- [x] Do not require rewriting normal single-account configs into `accounts.default`.
|
|
||||||
- [x] Add or keep doctor and startup migrations only for keys that are genuinely obsolete or ignored by the new implementation.
|
|
||||||
- [x] Ensure config help, schema labels, and reference docs all describe `channels.matrix`, never `channels.matrix-js`.
|
|
||||||
|
|
||||||
### 3. Preserve legacy `matrix` state and runtime behavior
|
|
||||||
|
|
||||||
- [x] Keep `credentials/matrix/credentials*.json` as the credential root.
|
|
||||||
- [x] Keep `matrix/accounts/<account>/<homeserver>__<user>/<token-hash>/...` as the canonical runtime and crypto root.
|
|
||||||
- [x] Add explicit migration support in the new plugin for direct upgrades from the oldest legacy flat store:
|
|
||||||
- [x] `~/.openclaw/matrix/bot-storage.json`
|
|
||||||
- [x] `~/.openclaw/matrix/crypto/`
|
|
||||||
- [x] Do not retain `matrix-js`-path migration logic in the shipped plugin.
|
|
||||||
- [x] Preserve multi-account isolation and default-account behavior exactly on the `matrix` channel.
|
|
||||||
- [x] Preserve legacy secrets integration by continuing to use the existing `channels.matrix.*` secret collectors and credential surface definitions.
|
|
||||||
- [x] Keep route/session binding, ACP binding, thread binding, and outbound message routing keyed to `matrix`, with the current new Matrix functionality carried over.
|
|
||||||
|
|
||||||
### 4. Migrate internal Matrix-js-only surfaces to `matrix`
|
|
||||||
|
|
||||||
- [x] Replace every internal channel string and binding string `matrix-js` with `matrix` across ACP binding schemas and runtime.
|
|
||||||
- [x] Replace every internal channel string and binding string `matrix-js` with `matrix` across thread binding policy and commands.
|
|
||||||
- [x] Replace every internal channel string and binding string `matrix-js` with `matrix` across auto-reply and session context surfaces.
|
|
||||||
- [x] Replace every internal channel string and binding string `matrix-js` with `matrix` across agent binding commands and tests.
|
|
||||||
- [x] Replace all CLI help, onboarding text, runtime warnings, and verification prompts from `matrix-js` to `matrix`.
|
|
||||||
- [x] Rewrite `docs/channels/matrix.md` to describe the new implementation and new verification, ACP, and thread features.
|
|
||||||
- [x] Remove `docs/channels/matrix-js.md`.
|
|
||||||
- [x] Update shared docs that still reference Matrix-js, including `docs/tools/acp-agents.md`, `docs/tools/subagents.md`, `docs/tools/plugin.md`, and `docs/gateway/configuration-reference.md`.
|
|
||||||
- [x] Leave `docs/zh-CN/**` untouched in this pass.
|
|
||||||
|
|
||||||
### 5. Automatic messaging and failure handling
|
|
||||||
|
|
||||||
- [x] When startup or doctor rewrites Matrix config, emit a short summary such as:
|
|
||||||
- [x] Matrix plugin upgraded in place
|
|
||||||
- [x] migrated deprecated Matrix config keys
|
|
||||||
- [x] migrated legacy Matrix crypto store
|
|
||||||
- [x] no user action required
|
|
||||||
- [x] When automatic repair is not safe, emit exact commands, not generic warnings.
|
|
||||||
- [x] For custom or stale local plugin paths, point users to the concrete replacement command or path.
|
|
||||||
- [x] Never log secrets or token values in migration output.
|
|
||||||
- [x] If a legacy state migration fails, continue with clear non-fatal messaging and tell the user what functionality may be degraded until they re-verify.
|
|
||||||
|
|
||||||
## Test plan and acceptance criteria
|
|
||||||
|
|
||||||
### Config compatibility
|
|
||||||
|
|
||||||
- [x] Existing `channels.matrix` single-account config loads unchanged.
|
|
||||||
- [x] Existing `channels.matrix.accounts.*` config loads unchanged.
|
|
||||||
- [x] Existing `channels.matrix.defaultAccount` behavior is preserved.
|
|
||||||
- [x] Existing SecretRef password config continues to validate and resolve.
|
|
||||||
- [x] Deprecated Matrix-only keys are auto-repaired by startup and doctor with clear change reporting.
|
|
||||||
|
|
||||||
### State compatibility
|
|
||||||
|
|
||||||
- [x] Current canonical `credentials/matrix/*` credentials are reused with no prompt.
|
|
||||||
- [x] Current canonical `matrix/accounts/*` runtime state is reused with no prompt.
|
|
||||||
- [x] Oldest flat legacy Matrix crypto and sync store is migrated automatically to account-scoped storage.
|
|
||||||
- [x] Legacy Matrix encrypted backup material is imported automatically when it can be resolved safely.
|
|
||||||
- [x] Backed-up Matrix room keys are restored automatically on startup after encrypted-state prep.
|
|
||||||
- [x] Multi-account state remains isolated after migration.
|
|
||||||
|
|
||||||
### Plugin and install compatibility
|
|
||||||
|
|
||||||
- [x] Existing npm-installed `@openclaw/matrix` updates in place and remains enabled.
|
|
||||||
- [x] `plugins.installs.matrix` continues to update correctly after the cutover.
|
|
||||||
- [x] Stale custom path installs are detected and produce exact repair messaging.
|
|
||||||
|
|
||||||
### Public surface
|
|
||||||
|
|
||||||
- [x] `openclaw matrix ...` exposes the verification and account commands that the new Matrix implementation owns.
|
|
||||||
- [x] `matrix.verify.*` gateway methods work.
|
|
||||||
- [x] All bindings, ACP, thread, and session flows use `matrix`, not `matrix-js`.
|
|
||||||
- [x] No shipped docs, help, schema output, or package exports reference `matrix-js`.
|
|
||||||
|
|
||||||
### Regression coverage
|
|
||||||
|
|
||||||
- [x] Startup auto-migration path.
|
|
||||||
- [x] Doctor `--fix` Matrix migration path.
|
|
||||||
- [x] Legacy encrypted-state prep and startup restore path.
|
|
||||||
- [x] Update-triggered doctor path.
|
|
||||||
- [x] Route bindings and ACP bindings with `match.channel: "matrix"`.
|
|
||||||
- [x] Thread binding spawn gating and routing on `matrix`.
|
|
||||||
- [x] Plugin install and update records for `matrix`.
|
|
||||||
|
|
||||||
### Acceptance criteria
|
|
||||||
|
|
||||||
- [x] A current public Matrix user can update and keep using `channels.matrix` without editing config.
|
|
||||||
- [x] Automatic migration covers every deterministic case.
|
|
||||||
- [x] Every non-deterministic case produces explicit next steps.
|
|
||||||
- [x] No public `matrix-js` surface remains in the shipped product.
|
|
||||||
|
|
||||||
## Assumptions and defaults
|
|
||||||
|
|
||||||
- Automatic repair policy: startup plus doctor and update.
|
|
||||||
- Custom plugin installs: warn and explain; do not mutate arbitrary custom paths automatically.
|
|
||||||
- No backward compatibility for internal `matrix-js` users or `matrix-js` config, package, CLI, or docs surfaces.
|
|
||||||
- Canonical external identity after release is `matrix` everywhere.
|
|
||||||
- The replacement preserves current public `matrix` behavior first, then layers in the newer Matrix features without requiring users to opt into a new namespace.
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { sendMatrixMessage } from "../src/matrix/actions.js";
|
|
||||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
|
||||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const base = resolveLiveHarnessConfig();
|
|
||||||
const pluginCfg = installLiveHarnessRuntime(base);
|
|
||||||
|
|
||||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
|
||||||
const client = await createMatrixClient({
|
|
||||||
homeserver: auth.homeserver,
|
|
||||||
userId: auth.userId,
|
|
||||||
accessToken: auth.accessToken,
|
|
||||||
password: auth.password,
|
|
||||||
deviceId: auth.deviceId,
|
|
||||||
encryption: false,
|
|
||||||
accountId: auth.accountId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const targetUserId = process.argv[2]?.trim() || "@user:example.org";
|
|
||||||
const stamp = new Date().toISOString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const dmRoomCreate = (await client.doRequest(
|
|
||||||
"POST",
|
|
||||||
"/_matrix/client/v3/createRoom",
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
is_direct: true,
|
|
||||||
invite: [targetUserId],
|
|
||||||
preset: "trusted_private_chat",
|
|
||||||
name: `OpenClaw DM Test ${stamp}`,
|
|
||||||
topic: "matrix basic DM messaging test",
|
|
||||||
},
|
|
||||||
)) as { room_id?: string };
|
|
||||||
|
|
||||||
const dmRoomId = dmRoomCreate.room_id?.trim() ?? "";
|
|
||||||
if (!dmRoomId) {
|
|
||||||
throw new Error("Failed to create DM room");
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentDirect = ((await client.getAccountData("m.direct").catch(() => ({}))) ??
|
|
||||||
{}) as Record<string, string[]>;
|
|
||||||
const existing = Array.isArray(currentDirect[targetUserId]) ? currentDirect[targetUserId] : [];
|
|
||||||
await client.setAccountData("m.direct", {
|
|
||||||
...currentDirect,
|
|
||||||
[targetUserId]: [dmRoomId, ...existing.filter((id) => id !== dmRoomId)],
|
|
||||||
});
|
|
||||||
|
|
||||||
const dmByUserTarget = await sendMatrixMessage(
|
|
||||||
targetUserId,
|
|
||||||
`Matrix basic DM test (user target) ${stamp}`,
|
|
||||||
{ client },
|
|
||||||
);
|
|
||||||
const dmByRoomTarget = await sendMatrixMessage(
|
|
||||||
dmRoomId,
|
|
||||||
`Matrix basic DM test (room target) ${stamp}`,
|
|
||||||
{ client },
|
|
||||||
);
|
|
||||||
|
|
||||||
const roomCreate = (await client.doRequest("POST", "/_matrix/client/v3/createRoom", undefined, {
|
|
||||||
invite: [targetUserId],
|
|
||||||
preset: "private_chat",
|
|
||||||
name: `OpenClaw Room Test ${stamp}`,
|
|
||||||
topic: "matrix basic room messaging test",
|
|
||||||
})) as { room_id?: string };
|
|
||||||
|
|
||||||
const roomId = roomCreate.room_id?.trim() ?? "";
|
|
||||||
if (!roomId) {
|
|
||||||
throw new Error("Failed to create room chat room");
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomSend = await sendMatrixMessage(roomId, `Matrix basic room test ${stamp}`, {
|
|
||||||
client,
|
|
||||||
});
|
|
||||||
|
|
||||||
process.stdout.write(
|
|
||||||
`${JSON.stringify(
|
|
||||||
{
|
|
||||||
homeserver: base.homeserver,
|
|
||||||
senderUserId: base.userId,
|
|
||||||
targetUserId,
|
|
||||||
dm: {
|
|
||||||
roomId: dmRoomId,
|
|
||||||
userTargetMessageId: dmByUserTarget.messageId,
|
|
||||||
roomTargetMessageId: dmByRoomTarget.messageId,
|
|
||||||
},
|
|
||||||
room: {
|
|
||||||
roomId,
|
|
||||||
messageId: roomSend.messageId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}\n`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
process.stderr.write(`BASIC_SEND_ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import { setMatrixRuntime } from "../src/runtime.js";
|
|
||||||
|
|
||||||
type EnvMap = Record<string, string>;
|
|
||||||
|
|
||||||
function loadEnvFile(filePath: string): EnvMap {
|
|
||||||
const out: EnvMap = {};
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
const raw = fs.readFileSync(filePath, "utf8");
|
|
||||||
for (const lineRaw of raw.split(/\r?\n/)) {
|
|
||||||
const line = lineRaw.trim();
|
|
||||||
if (!line || line.startsWith("#")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const idx = line.indexOf("=");
|
|
||||||
if (idx <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const key = line.slice(0, idx).trim();
|
|
||||||
let value = line.slice(idx + 1).trim();
|
|
||||||
if (
|
|
||||||
(value.startsWith('"') && value.endsWith('"')) ||
|
|
||||||
(value.startsWith("'") && value.endsWith("'"))
|
|
||||||
) {
|
|
||||||
value = value.slice(1, -1);
|
|
||||||
}
|
|
||||||
out[key] = value;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeHomeserver(raw: string): string {
|
|
||||||
const trimmed = raw.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function chunkText(text: string, limit: number): string[] {
|
|
||||||
if (!text) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (text.length <= limit) {
|
|
||||||
return [text];
|
|
||||||
}
|
|
||||||
const out: string[] = [];
|
|
||||||
for (let i = 0; i < text.length; i += limit) {
|
|
||||||
out.push(text.slice(i, i + limit));
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LiveHarnessConfig = {
|
|
||||||
homeserver: string;
|
|
||||||
userId: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function resolveLiveHarnessConfig(): LiveHarnessConfig {
|
|
||||||
const envFromFile = loadEnvFile(path.join(os.homedir(), ".openclaw", ".env"));
|
|
||||||
const homeserver = normalizeHomeserver(
|
|
||||||
process.env.MATRIX_HOMESERVER ?? envFromFile.MATRIX_HOMESERVER ?? "",
|
|
||||||
);
|
|
||||||
const userId = process.env.MATRIX_USER_ID ?? envFromFile.MATRIX_USER_ID ?? "";
|
|
||||||
const password = process.env.MATRIX_PASSWORD ?? envFromFile.MATRIX_PASSWORD ?? "";
|
|
||||||
|
|
||||||
if (!homeserver || !userId || !password) {
|
|
||||||
throw new Error("Missing MATRIX_HOMESERVER / MATRIX_USER_ID / MATRIX_PASSWORD");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
homeserver,
|
|
||||||
userId,
|
|
||||||
password,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function installLiveHarnessRuntime(cfg: LiveHarnessConfig): {
|
|
||||||
channels: {
|
|
||||||
matrix: {
|
|
||||||
homeserver: string;
|
|
||||||
userId: string;
|
|
||||||
password: string;
|
|
||||||
encryption: false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
} {
|
|
||||||
const pluginCfg = {
|
|
||||||
channels: {
|
|
||||||
matrix: {
|
|
||||||
homeserver: cfg.homeserver,
|
|
||||||
userId: cfg.userId,
|
|
||||||
password: cfg.password,
|
|
||||||
encryption: false as const,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
setMatrixRuntime({
|
|
||||||
config: {
|
|
||||||
loadConfig: () => pluginCfg,
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
resolveStateDir: () => path.join(os.homedir(), ".openclaw", "matrix-live-harness-state"),
|
|
||||||
},
|
|
||||||
channel: {
|
|
||||||
text: {
|
|
||||||
resolveMarkdownTableMode: () => "off",
|
|
||||||
convertMarkdownTables: (text: string) => text,
|
|
||||||
resolveTextChunkLimit: () => 4000,
|
|
||||||
resolveChunkMode: () => "off",
|
|
||||||
chunkMarkdownTextWithMode: (text: string, limit: number) => chunkText(text, limit),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
media: {
|
|
||||||
mediaKindFromMime: (mime: string) => {
|
|
||||||
const value = (mime || "").toLowerCase();
|
|
||||||
if (value.startsWith("image/")) {
|
|
||||||
return "image";
|
|
||||||
}
|
|
||||||
if (value.startsWith("audio/")) {
|
|
||||||
return "audio";
|
|
||||||
}
|
|
||||||
if (value.startsWith("video/")) {
|
|
||||||
return "video";
|
|
||||||
}
|
|
||||||
return "document";
|
|
||||||
},
|
|
||||||
isVoiceCompatibleAudio: () => false,
|
|
||||||
loadWebMedia: async () => ({
|
|
||||||
buffer: Buffer.from("matrix harness media payload\n", "utf8"),
|
|
||||||
contentType: "text/plain",
|
|
||||||
fileName: "matrix-harness.txt",
|
|
||||||
kind: "document" as const,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
} as never);
|
|
||||||
|
|
||||||
return pluginCfg;
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
|
||||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
|
||||||
|
|
||||||
type MatrixCryptoProbe = {
|
|
||||||
isCrossSigningReady?: () => Promise<boolean>;
|
|
||||||
userHasCrossSigningKeys?: (userId?: string, downloadUncached?: boolean) => Promise<boolean>;
|
|
||||||
bootstrapCrossSigning?: (opts: {
|
|
||||||
setupNewCrossSigning?: boolean;
|
|
||||||
authUploadDeviceSigningKeys?: <T>(
|
|
||||||
makeRequest: (authData: Record<string, unknown> | null) => Promise<T>,
|
|
||||||
) => Promise<T>;
|
|
||||||
}) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const base = resolveLiveHarnessConfig();
|
|
||||||
const cfg = installLiveHarnessRuntime(base);
|
|
||||||
(cfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
|
||||||
|
|
||||||
const auth = await resolveMatrixAuth({ cfg: cfg as never });
|
|
||||||
const client = await createMatrixClient({
|
|
||||||
homeserver: auth.homeserver,
|
|
||||||
userId: auth.userId,
|
|
||||||
accessToken: auth.accessToken,
|
|
||||||
password: auth.password,
|
|
||||||
deviceId: auth.deviceId,
|
|
||||||
encryption: true,
|
|
||||||
accountId: auth.accountId,
|
|
||||||
});
|
|
||||||
const initCrypto = (client as unknown as { initializeCryptoIfNeeded?: () => Promise<void> })
|
|
||||||
.initializeCryptoIfNeeded;
|
|
||||||
if (typeof initCrypto === "function") {
|
|
||||||
await initCrypto.call(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
const inner = (client as unknown as { client?: { getCrypto?: () => unknown } }).client;
|
|
||||||
const crypto = (inner?.getCrypto?.() ?? null) as MatrixCryptoProbe | null;
|
|
||||||
const userId = auth.userId;
|
|
||||||
const password = auth.password;
|
|
||||||
|
|
||||||
const out: Record<string, unknown> = {
|
|
||||||
userId,
|
|
||||||
hasCrypto: Boolean(crypto),
|
|
||||||
readyBefore: null,
|
|
||||||
hasKeysBefore: null,
|
|
||||||
bootstrap: "skipped",
|
|
||||||
readyAfter: null,
|
|
||||||
hasKeysAfter: null,
|
|
||||||
queryHasMaster: null,
|
|
||||||
queryHasSelfSigning: null,
|
|
||||||
queryHasUserSigning: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!crypto || !crypto.bootstrapCrossSigning) {
|
|
||||||
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof crypto.isCrossSigningReady === "function") {
|
|
||||||
out.readyBefore = await crypto.isCrossSigningReady().catch((err) => `error:${String(err)}`);
|
|
||||||
}
|
|
||||||
if (typeof crypto.userHasCrossSigningKeys === "function") {
|
|
||||||
out.hasKeysBefore = await crypto
|
|
||||||
.userHasCrossSigningKeys(userId, true)
|
|
||||||
.catch((err) => `error:${String(err)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const authUploadDeviceSigningKeys = async <T>(
|
|
||||||
makeRequest: (authData: Record<string, unknown> | null) => Promise<T>,
|
|
||||||
): Promise<T> => {
|
|
||||||
try {
|
|
||||||
return await makeRequest(null);
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
return await makeRequest({ type: "m.login.dummy" });
|
|
||||||
} catch {
|
|
||||||
if (!password?.trim()) {
|
|
||||||
throw new Error("Missing password for m.login.password fallback");
|
|
||||||
}
|
|
||||||
return await makeRequest({
|
|
||||||
type: "m.login.password",
|
|
||||||
identifier: { type: "m.id.user", user: userId },
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await crypto.bootstrapCrossSigning({ authUploadDeviceSigningKeys });
|
|
||||||
out.bootstrap = "ok";
|
|
||||||
} catch (err) {
|
|
||||||
out.bootstrap = "error";
|
|
||||||
out.bootstrapError = err instanceof Error ? err.message : String(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof crypto.isCrossSigningReady === "function") {
|
|
||||||
out.readyAfter = await crypto.isCrossSigningReady().catch((err) => `error:${String(err)}`);
|
|
||||||
}
|
|
||||||
if (typeof crypto.userHasCrossSigningKeys === "function") {
|
|
||||||
out.hasKeysAfter = await crypto
|
|
||||||
.userHasCrossSigningKeys(userId, true)
|
|
||||||
.catch((err) => `error:${String(err)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = (await client.doRequest("POST", "/_matrix/client/v3/keys/query", undefined, {
|
|
||||||
device_keys: { [userId]: [] },
|
|
||||||
})) as {
|
|
||||||
master_keys?: Record<string, unknown>;
|
|
||||||
self_signing_keys?: Record<string, unknown>;
|
|
||||||
user_signing_keys?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
out.queryHasMaster = Boolean(query.master_keys?.[userId]);
|
|
||||||
out.queryHasSelfSigning = Boolean(query.self_signing_keys?.[userId]);
|
|
||||||
out.queryHasUserSigning = Boolean(query.user_signing_keys?.[userId]);
|
|
||||||
|
|
||||||
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
process.stderr.write(
|
|
||||||
`CROSS_SIGNING_PROBE_ERROR: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { bootstrapMatrixVerification } from "../src/matrix/actions/verification.js";
|
|
||||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const recoveryKeyArg = process.argv[2];
|
|
||||||
const forceResetCrossSigning = process.argv.includes("--force-reset-cross-signing");
|
|
||||||
|
|
||||||
const base = resolveLiveHarnessConfig();
|
|
||||||
const pluginCfg = installLiveHarnessRuntime(base);
|
|
||||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
|
||||||
|
|
||||||
const result = await bootstrapMatrixVerification({
|
|
||||||
recoveryKey: recoveryKeyArg?.trim() || undefined,
|
|
||||||
forceResetCrossSigning,
|
|
||||||
});
|
|
||||||
|
|
||||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
||||||
if (!result.success) {
|
|
||||||
process.exitCode = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
process.stderr.write(
|
|
||||||
`E2EE_BOOTSTRAP_ERROR: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
|
||||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const roomId = process.argv[2]?.trim();
|
|
||||||
const eventId = process.argv[3]?.trim();
|
|
||||||
|
|
||||||
if (!roomId) {
|
|
||||||
throw new Error(
|
|
||||||
"Usage: node --import tsx extensions/matrix/scripts/live-e2ee-room-state.ts <roomId> [eventId]",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = resolveLiveHarnessConfig();
|
|
||||||
const pluginCfg = installLiveHarnessRuntime(base);
|
|
||||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
|
||||||
|
|
||||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
|
||||||
const client = await createMatrixClient({
|
|
||||||
homeserver: auth.homeserver,
|
|
||||||
userId: auth.userId,
|
|
||||||
accessToken: auth.accessToken,
|
|
||||||
password: auth.password,
|
|
||||||
deviceId: auth.deviceId,
|
|
||||||
encryption: false,
|
|
||||||
accountId: auth.accountId,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encryptionState = (await client.doRequest(
|
|
||||||
"GET",
|
|
||||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.encryption/`,
|
|
||||||
)) as { algorithm?: string; rotation_period_ms?: number; rotation_period_msgs?: number };
|
|
||||||
|
|
||||||
let eventType: string | null = null;
|
|
||||||
if (eventId) {
|
|
||||||
const event = (await client.doRequest(
|
|
||||||
"GET",
|
|
||||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}`,
|
|
||||||
)) as { type?: string };
|
|
||||||
eventType = event.type ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdout.write(
|
|
||||||
`${JSON.stringify(
|
|
||||||
{
|
|
||||||
roomId,
|
|
||||||
encryptionState,
|
|
||||||
eventId: eventId ?? null,
|
|
||||||
eventType,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}\n`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
process.stderr.write(
|
|
||||||
`E2EE_ROOM_STATE_ERROR: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { sendMatrixMessage } from "../src/matrix/actions.js";
|
|
||||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
|
||||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
|
||||||
|
|
||||||
async function delay(ms: number): Promise<void> {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const roomId = process.argv[2]?.trim();
|
|
||||||
const useFullBootstrap = process.argv.includes("--full-bootstrap");
|
|
||||||
const startupTimeoutMs = 45_000;
|
|
||||||
const settleMsRaw = Number.parseInt(process.argv[3] ?? "4000", 10);
|
|
||||||
const settleMs = Number.isFinite(settleMsRaw) && settleMsRaw >= 0 ? settleMsRaw : 4000;
|
|
||||||
|
|
||||||
if (!roomId) {
|
|
||||||
throw new Error(
|
|
||||||
"Usage: node --import tsx extensions/matrix/scripts/live-e2ee-send-room.ts <roomId> [settleMs] [--full-bootstrap]",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = resolveLiveHarnessConfig();
|
|
||||||
const pluginCfg = installLiveHarnessRuntime(base);
|
|
||||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
|
||||||
|
|
||||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
|
||||||
const client = await createMatrixClient({
|
|
||||||
homeserver: auth.homeserver,
|
|
||||||
userId: auth.userId,
|
|
||||||
accessToken: auth.accessToken,
|
|
||||||
password: auth.password,
|
|
||||||
deviceId: auth.deviceId,
|
|
||||||
encryption: true,
|
|
||||||
accountId: auth.accountId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const stamp = new Date().toISOString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!useFullBootstrap) {
|
|
||||||
const bootstrapper = (
|
|
||||||
client as unknown as { cryptoBootstrapper?: { bootstrap?: () => Promise<void> } }
|
|
||||||
).cryptoBootstrapper;
|
|
||||||
if (bootstrapper?.bootstrap) {
|
|
||||||
bootstrapper.bootstrap = async () => {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.race([
|
|
||||||
client.start(),
|
|
||||||
new Promise<never>((_, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
reject(
|
|
||||||
new Error(
|
|
||||||
`Matrix client start timed out after ${startupTimeoutMs}ms (fullBootstrap=${useFullBootstrap})`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, startupTimeoutMs);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (settleMs > 0) {
|
|
||||||
await delay(settleMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sent = await sendMatrixMessage(
|
|
||||||
roomId,
|
|
||||||
`Matrix E2EE existing-room test ${stamp} (settleMs=${settleMs})`,
|
|
||||||
{ client },
|
|
||||||
);
|
|
||||||
|
|
||||||
const event = (await client.doRequest(
|
|
||||||
"GET",
|
|
||||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(sent.messageId)}`,
|
|
||||||
)) as { type?: string };
|
|
||||||
|
|
||||||
process.stdout.write(
|
|
||||||
`${JSON.stringify(
|
|
||||||
{
|
|
||||||
roomId,
|
|
||||||
messageId: sent.messageId,
|
|
||||||
storedEventType: event.type ?? null,
|
|
||||||
fullBootstrap: useFullBootstrap,
|
|
||||||
settleMs,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}\n`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
process.stderr.write(
|
|
||||||
`E2EE_SEND_ROOM_ERROR: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import { sendMatrixMessage } from "../src/matrix/actions.js";
|
|
||||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
|
||||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
|
||||||
|
|
||||||
const MEGOLM_ALG = "m.megolm.v1.aes-sha2";
|
|
||||||
|
|
||||||
type MatrixEventLike = {
|
|
||||||
type?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const targetUserId = process.argv[2]?.trim() || "@user:example.org";
|
|
||||||
const useFullBootstrap = process.argv.includes("--full-bootstrap");
|
|
||||||
const startupTimeoutMs = 45_000;
|
|
||||||
const base = resolveLiveHarnessConfig();
|
|
||||||
const pluginCfg = installLiveHarnessRuntime(base);
|
|
||||||
|
|
||||||
// Enable encryption for this run only.
|
|
||||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
|
||||||
|
|
||||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
|
||||||
const client = await createMatrixClient({
|
|
||||||
homeserver: auth.homeserver,
|
|
||||||
userId: auth.userId,
|
|
||||||
accessToken: auth.accessToken,
|
|
||||||
password: auth.password,
|
|
||||||
deviceId: auth.deviceId,
|
|
||||||
encryption: true,
|
|
||||||
accountId: auth.accountId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const stamp = new Date().toISOString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!useFullBootstrap) {
|
|
||||||
const bootstrapper = (
|
|
||||||
client as unknown as { cryptoBootstrapper?: { bootstrap?: () => Promise<void> } }
|
|
||||||
).cryptoBootstrapper;
|
|
||||||
if (bootstrapper?.bootstrap) {
|
|
||||||
bootstrapper.bootstrap = async () => {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.race([
|
|
||||||
client.start(),
|
|
||||||
new Promise<never>((_, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
reject(
|
|
||||||
new Error(
|
|
||||||
`Matrix client start timed out after ${startupTimeoutMs}ms (fullBootstrap=${useFullBootstrap})`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, startupTimeoutMs);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const dmRoomCreate = (await client.doRequest(
|
|
||||||
"POST",
|
|
||||||
"/_matrix/client/v3/createRoom",
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
is_direct: true,
|
|
||||||
invite: [targetUserId],
|
|
||||||
preset: "trusted_private_chat",
|
|
||||||
name: `OpenClaw E2EE DM ${stamp}`,
|
|
||||||
topic: "matrix E2EE DM test",
|
|
||||||
initial_state: [
|
|
||||||
{
|
|
||||||
type: "m.room.encryption",
|
|
||||||
state_key: "",
|
|
||||||
content: {
|
|
||||||
algorithm: MEGOLM_ALG,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)) as { room_id?: string };
|
|
||||||
|
|
||||||
const dmRoomId = dmRoomCreate.room_id?.trim() ?? "";
|
|
||||||
if (!dmRoomId) {
|
|
||||||
throw new Error("Failed to create encrypted DM room");
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentDirect = ((await client.getAccountData("m.direct").catch(() => ({}))) ??
|
|
||||||
{}) as Record<string, string[]>;
|
|
||||||
const existing = Array.isArray(currentDirect[targetUserId]) ? currentDirect[targetUserId] : [];
|
|
||||||
await client.setAccountData("m.direct", {
|
|
||||||
...currentDirect,
|
|
||||||
[targetUserId]: [dmRoomId, ...existing.filter((id) => id !== dmRoomId)],
|
|
||||||
});
|
|
||||||
|
|
||||||
const dmSend = await sendMatrixMessage(
|
|
||||||
dmRoomId,
|
|
||||||
`Matrix E2EE DM test ${stamp}\nPlease reply here so I can validate decrypt/read.`,
|
|
||||||
{
|
|
||||||
client,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const roomCreate = (await client.doRequest("POST", "/_matrix/client/v3/createRoom", undefined, {
|
|
||||||
invite: [targetUserId],
|
|
||||||
preset: "private_chat",
|
|
||||||
name: `OpenClaw E2EE Room ${stamp}`,
|
|
||||||
topic: "matrix E2EE room test",
|
|
||||||
initial_state: [
|
|
||||||
{
|
|
||||||
type: "m.room.encryption",
|
|
||||||
state_key: "",
|
|
||||||
content: {
|
|
||||||
algorithm: MEGOLM_ALG,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})) as { room_id?: string };
|
|
||||||
|
|
||||||
const roomId = roomCreate.room_id?.trim() ?? "";
|
|
||||||
if (!roomId) {
|
|
||||||
throw new Error("Failed to create encrypted room chat");
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomSend = await sendMatrixMessage(
|
|
||||||
roomId,
|
|
||||||
`Matrix E2EE room test ${stamp}\nPlease reply here too.`,
|
|
||||||
{
|
|
||||||
client,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const dmRaw = (await client.doRequest(
|
|
||||||
"GET",
|
|
||||||
`/_matrix/client/v3/rooms/${encodeURIComponent(dmRoomId)}/event/${encodeURIComponent(dmSend.messageId)}`,
|
|
||||||
)) as MatrixEventLike;
|
|
||||||
|
|
||||||
const roomRaw = (await client.doRequest(
|
|
||||||
"GET",
|
|
||||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(roomSend.messageId)}`,
|
|
||||||
)) as MatrixEventLike;
|
|
||||||
|
|
||||||
process.stdout.write(
|
|
||||||
`${JSON.stringify(
|
|
||||||
{
|
|
||||||
homeserver: base.homeserver,
|
|
||||||
senderUserId: base.userId,
|
|
||||||
targetUserId,
|
|
||||||
encryptionAlgorithm: MEGOLM_ALG,
|
|
||||||
fullBootstrap: useFullBootstrap,
|
|
||||||
dm: {
|
|
||||||
roomId: dmRoomId,
|
|
||||||
messageId: dmSend.messageId,
|
|
||||||
storedEventType: dmRaw.type ?? null,
|
|
||||||
},
|
|
||||||
room: {
|
|
||||||
roomId,
|
|
||||||
messageId: roomSend.messageId,
|
|
||||||
storedEventType: roomRaw.type ?? null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}\n`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
process.stderr.write(`E2EE_SEND_ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import {
|
|
||||||
getMatrixEncryptionStatus,
|
|
||||||
getMatrixVerificationStatus,
|
|
||||||
verifyMatrixRecoveryKey,
|
|
||||||
} from "../src/matrix/actions.js";
|
|
||||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const includeRecoveryKey = process.argv.includes("--include-recovery-key");
|
|
||||||
const verifyStoredRecoveryKey = process.argv.includes("--verify-stored-recovery-key");
|
|
||||||
|
|
||||||
const base = resolveLiveHarnessConfig();
|
|
||||||
const pluginCfg = installLiveHarnessRuntime(base);
|
|
||||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
|
||||||
|
|
||||||
const verification = await getMatrixVerificationStatus({
|
|
||||||
includeRecoveryKey,
|
|
||||||
});
|
|
||||||
const encryption = await getMatrixEncryptionStatus({
|
|
||||||
includeRecoveryKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
let recoveryVerificationResult: unknown = null;
|
|
||||||
if (verifyStoredRecoveryKey) {
|
|
||||||
const key =
|
|
||||||
verification && typeof verification === "object" && "recoveryKey" in verification
|
|
||||||
? (verification as { recoveryKey?: string | null }).recoveryKey
|
|
||||||
: null;
|
|
||||||
if (key?.trim()) {
|
|
||||||
recoveryVerificationResult = await verifyMatrixRecoveryKey(key);
|
|
||||||
} else {
|
|
||||||
recoveryVerificationResult = {
|
|
||||||
success: false,
|
|
||||||
error: "No stored recovery key returned (use --include-recovery-key)",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdout.write(
|
|
||||||
`${JSON.stringify(
|
|
||||||
{
|
|
||||||
homeserver: base.homeserver,
|
|
||||||
userId: base.userId,
|
|
||||||
verification,
|
|
||||||
encryption,
|
|
||||||
recoveryVerificationResult,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}\n`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
process.stderr.write(`E2EE_STATUS_ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
|
||||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
|
||||||
|
|
||||||
type MatrixRawEvent = {
|
|
||||||
event_id?: string;
|
|
||||||
type?: string;
|
|
||||||
sender?: string;
|
|
||||||
room_id?: string;
|
|
||||||
origin_server_ts?: number;
|
|
||||||
content?: {
|
|
||||||
body?: string;
|
|
||||||
msgtype?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const roomId = process.argv[2]?.trim();
|
|
||||||
const targetUserId = process.argv[3]?.trim() || "@user:example.org";
|
|
||||||
const timeoutSecRaw = Number.parseInt(process.argv[4] ?? "120", 10);
|
|
||||||
const timeoutMs =
|
|
||||||
(Number.isFinite(timeoutSecRaw) && timeoutSecRaw > 0 ? timeoutSecRaw : 120) * 1000;
|
|
||||||
const useFullBootstrap = process.argv.includes("--full-bootstrap");
|
|
||||||
const startupTimeoutMs = 45_000;
|
|
||||||
|
|
||||||
if (!roomId) {
|
|
||||||
throw new Error(
|
|
||||||
"Usage: node --import tsx extensions/matrix/scripts/live-e2ee-wait-reply.ts <roomId> [targetUserId] [timeoutSec] [--full-bootstrap]",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = resolveLiveHarnessConfig();
|
|
||||||
const pluginCfg = installLiveHarnessRuntime(base);
|
|
||||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
|
||||||
|
|
||||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
|
||||||
const client = await createMatrixClient({
|
|
||||||
homeserver: auth.homeserver,
|
|
||||||
userId: auth.userId,
|
|
||||||
accessToken: auth.accessToken,
|
|
||||||
password: auth.password,
|
|
||||||
deviceId: auth.deviceId,
|
|
||||||
encryption: true,
|
|
||||||
accountId: auth.accountId,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!useFullBootstrap) {
|
|
||||||
const bootstrapper = (
|
|
||||||
client as unknown as { cryptoBootstrapper?: { bootstrap?: () => Promise<void> } }
|
|
||||||
).cryptoBootstrapper;
|
|
||||||
if (bootstrapper?.bootstrap) {
|
|
||||||
bootstrapper.bootstrap = async () => {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.race([
|
|
||||||
client.start(),
|
|
||||||
new Promise<never>((_, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
reject(
|
|
||||||
new Error(
|
|
||||||
`Matrix client start timed out after ${startupTimeoutMs}ms (fullBootstrap=${useFullBootstrap})`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, startupTimeoutMs);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const found = await new Promise<MatrixRawEvent | null>((resolve) => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
resolve(null);
|
|
||||||
}, timeoutMs);
|
|
||||||
|
|
||||||
client.on("room.message", (eventRoomId, event) => {
|
|
||||||
const rid = String(eventRoomId || "");
|
|
||||||
const raw = event as MatrixRawEvent;
|
|
||||||
if (rid !== roomId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((raw.sender ?? "").trim() !== targetUserId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((raw.type ?? "").trim() !== "m.room.message") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearTimeout(timer);
|
|
||||||
resolve(raw);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.stdout.write(
|
|
||||||
`${JSON.stringify(
|
|
||||||
{
|
|
||||||
roomId,
|
|
||||||
targetUserId,
|
|
||||||
timeoutMs,
|
|
||||||
found: Boolean(found),
|
|
||||||
message: found
|
|
||||||
? {
|
|
||||||
eventId: found.event_id ?? null,
|
|
||||||
type: found.type ?? null,
|
|
||||||
sender: found.sender ?? null,
|
|
||||||
timestamp: found.origin_server_ts ?? null,
|
|
||||||
text: found.content?.body ?? null,
|
|
||||||
msgtype: found.content?.msgtype ?? null,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}\n`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
process.stderr.write(
|
|
||||||
`E2EE_WAIT_REPLY_ERROR: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { readMatrixMessages } from "../src/matrix/actions.js";
|
|
||||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
|
||||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const roomId = process.argv[2]?.trim();
|
|
||||||
if (!roomId) {
|
|
||||||
throw new Error("Usage: bun extensions/matrix/scripts/live-read-room.ts <roomId> [limit]");
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestedLimit = Number.parseInt(process.argv[3] ?? "30", 10);
|
|
||||||
const limit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? requestedLimit : 30;
|
|
||||||
|
|
||||||
const base = resolveLiveHarnessConfig();
|
|
||||||
const pluginCfg = installLiveHarnessRuntime(base);
|
|
||||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
|
||||||
const client = await createMatrixClient({
|
|
||||||
homeserver: auth.homeserver,
|
|
||||||
userId: auth.userId,
|
|
||||||
accessToken: auth.accessToken,
|
|
||||||
password: auth.password,
|
|
||||||
deviceId: auth.deviceId,
|
|
||||||
encryption: false,
|
|
||||||
accountId: auth.accountId,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await readMatrixMessages(roomId, { client, limit });
|
|
||||||
const compact = result.messages.map((msg) => ({
|
|
||||||
id: msg.eventId,
|
|
||||||
sender: msg.sender,
|
|
||||||
ts: msg.timestamp,
|
|
||||||
text: msg.body ?? "",
|
|
||||||
}));
|
|
||||||
|
|
||||||
process.stdout.write(
|
|
||||||
`${JSON.stringify(
|
|
||||||
{
|
|
||||||
roomId,
|
|
||||||
count: compact.length,
|
|
||||||
messages: compact,
|
|
||||||
nextBatch: result.nextBatch ?? null,
|
|
||||||
prevBatch: result.prevBatch ?? null,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}\n`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
process.stderr.write(`READ_ROOM_ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -476,7 +476,7 @@ describe("matrix directory", () => {
|
|||||||
input: {
|
input: {
|
||||||
homeserver: "https://matrix.example.org",
|
homeserver: "https://matrix.example.org",
|
||||||
userId: "@bot:example.org",
|
userId: "@bot:example.org",
|
||||||
password: "new-password",
|
password: "new-password", // pragma: allowlist secret
|
||||||
},
|
},
|
||||||
}) as CoreConfig;
|
}) as CoreConfig;
|
||||||
|
|
||||||
@@ -492,7 +492,7 @@ describe("matrix directory", () => {
|
|||||||
default: {
|
default: {
|
||||||
homeserver: "https://matrix.example.org",
|
homeserver: "https://matrix.example.org",
|
||||||
userId: "@bot:example.org",
|
userId: "@bot:example.org",
|
||||||
password: "old-password",
|
password: "old-password", // pragma: allowlist secret
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ describe("matrix CLI verification commands", () => {
|
|||||||
input: expect.objectContaining({
|
input: expect.objectContaining({
|
||||||
homeserver: "https://matrix.example.org",
|
homeserver: "https://matrix.example.org",
|
||||||
userId: "@ops:example.org",
|
userId: "@ops:example.org",
|
||||||
password: "secret",
|
password: "secret", // pragma: allowlist secret
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ describe("resolveMatrixAuth", () => {
|
|||||||
matrix: {
|
matrix: {
|
||||||
homeserver: "https://matrix.example.org",
|
homeserver: "https://matrix.example.org",
|
||||||
userId: "@bot:example.org",
|
userId: "@bot:example.org",
|
||||||
password: "secret",
|
password: "secret", // pragma: allowlist secret
|
||||||
encryption: true,
|
encryption: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -158,7 +158,7 @@ describe("resolveMatrixAuth", () => {
|
|||||||
matrix: {
|
matrix: {
|
||||||
homeserver: "https://matrix.example.org",
|
homeserver: "https://matrix.example.org",
|
||||||
userId: "@bot:example.org",
|
userId: "@bot:example.org",
|
||||||
password: "secret",
|
password: "secret", // pragma: allowlist secret
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as CoreConfig;
|
||||||
@@ -196,7 +196,7 @@ describe("resolveMatrixAuth", () => {
|
|||||||
matrix: {
|
matrix: {
|
||||||
homeserver: "https://matrix.example.org",
|
homeserver: "https://matrix.example.org",
|
||||||
userId: "@bot:example.org",
|
userId: "@bot:example.org",
|
||||||
password: "secret",
|
password: "secret", // pragma: allowlist secret
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as CoreConfig;
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function getMatrixScopedEnvVarNames(accountId: string): {
|
|||||||
homeserver: `MATRIX_${token}_HOMESERVER`,
|
homeserver: `MATRIX_${token}_HOMESERVER`,
|
||||||
userId: `MATRIX_${token}_USER_ID`,
|
userId: `MATRIX_${token}_USER_ID`,
|
||||||
accessToken: `MATRIX_${token}_ACCESS_TOKEN`,
|
accessToken: `MATRIX_${token}_ACCESS_TOKEN`,
|
||||||
password: `MATRIX_${token}_PASSWORD`,
|
password: `MATRIX_${token}_PASSWORD`, // pragma: allowlist secret
|
||||||
deviceId: `MATRIX_${token}_DEVICE_ID`,
|
deviceId: `MATRIX_${token}_DEVICE_ID`,
|
||||||
deviceName: `MATRIX_${token}_DEVICE_NAME`,
|
deviceName: `MATRIX_${token}_DEVICE_NAME`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function authFor(accountId: string): MatrixAuth {
|
|||||||
homeserver: "https://matrix.example.org",
|
homeserver: "https://matrix.example.org",
|
||||||
userId: `@${accountId}:example.org`,
|
userId: `@${accountId}:example.org`,
|
||||||
accessToken: `token-${accountId}`,
|
accessToken: `token-${accountId}`,
|
||||||
password: "secret",
|
password: "secret", // pragma: allowlist secret
|
||||||
deviceId: `${accountId.toUpperCase()}-DEVICE`,
|
deviceId: `${accountId.toUpperCase()}-DEVICE`,
|
||||||
deviceName: `${accountId} device`,
|
deviceName: `${accountId} device`,
|
||||||
initialSyncLimit: undefined,
|
initialSyncLimit: undefined,
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ describe("updateMatrixAccountConfig", () => {
|
|||||||
default: {
|
default: {
|
||||||
homeserver: "https://matrix.example.org",
|
homeserver: "https://matrix.example.org",
|
||||||
userId: "@bot:example.org",
|
userId: "@bot:example.org",
|
||||||
accessToken: "old-token",
|
accessToken: "old-token", // pragma: allowlist secret
|
||||||
password: "old-password",
|
password: "old-password", // pragma: allowlist secret
|
||||||
encryption: true,
|
encryption: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -813,7 +813,7 @@ describe("MatrixClient crypto bootstrapping", () => {
|
|||||||
matrixJsClient.getCrypto = vi.fn(() => ({ on: vi.fn() }));
|
matrixJsClient.getCrypto = vi.fn(() => ({ on: vi.fn() }));
|
||||||
const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, {
|
const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, {
|
||||||
encryption: true,
|
encryption: true,
|
||||||
password: "secret-password",
|
password: "secret-password", // pragma: allowlist secret
|
||||||
});
|
});
|
||||||
const bootstrapSpy = vi
|
const bootstrapSpy = vi
|
||||||
.fn()
|
.fn()
|
||||||
@@ -846,7 +846,7 @@ describe("MatrixClient crypto bootstrapping", () => {
|
|||||||
matrixJsClient.getCrypto = vi.fn(() => ({ on: vi.fn() }));
|
matrixJsClient.getCrypto = vi.fn(() => ({ on: vi.fn() }));
|
||||||
const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, {
|
const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, {
|
||||||
encryption: true,
|
encryption: true,
|
||||||
password: "secret-password",
|
password: "secret-password", // pragma: allowlist secret
|
||||||
});
|
});
|
||||||
const bootstrapSpy = vi.fn().mockResolvedValue({
|
const bootstrapSpy = vi.fn().mockResolvedValue({
|
||||||
crossSigningReady: false,
|
crossSigningReady: false,
|
||||||
|
|||||||
@@ -679,10 +679,13 @@ export class MatrixClient {
|
|||||||
let keyLoadAttempted = false;
|
let keyLoadAttempted = false;
|
||||||
let keyLoadError: string | null = null;
|
let keyLoadError: string | null = null;
|
||||||
if (serverVersion && decryptionKeyCached === false) {
|
if (serverVersion && decryptionKeyCached === false) {
|
||||||
if (typeof crypto.loadSessionBackupPrivateKeyFromSecretStorage === "function") {
|
if (
|
||||||
|
typeof crypto.loadSessionBackupPrivateKeyFromSecretStorage ===
|
||||||
|
"function" /* pragma: allowlist secret */
|
||||||
|
) {
|
||||||
keyLoadAttempted = true;
|
keyLoadAttempted = true;
|
||||||
try {
|
try {
|
||||||
await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
await crypto.loadSessionBackupPrivateKeyFromSecretStorage(); // pragma: allowlist secret
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
keyLoadError = err instanceof Error ? err.message : String(err);
|
keyLoadError = err instanceof Error ? err.message : String(err);
|
||||||
}
|
}
|
||||||
@@ -777,8 +780,9 @@ export class MatrixClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let defaultKeyId: string | null | undefined = undefined;
|
let defaultKeyId: string | null | undefined = undefined;
|
||||||
if (typeof crypto.getSecretStorageStatus === "function") {
|
const canReadSecretStorageStatus = typeof crypto.getSecretStorageStatus === "function"; // pragma: allowlist secret
|
||||||
const status = await crypto.getSecretStorageStatus().catch(() => null);
|
if (canReadSecretStorageStatus) {
|
||||||
|
const status = await crypto.getSecretStorageStatus().catch(() => null); // pragma: allowlist secret
|
||||||
defaultKeyId = status?.defaultKeyId;
|
defaultKeyId = status?.defaultKeyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,8 +848,9 @@ export class MatrixClient {
|
|||||||
const rawRecoveryKey = params.recoveryKey?.trim();
|
const rawRecoveryKey = params.recoveryKey?.trim();
|
||||||
if (rawRecoveryKey) {
|
if (rawRecoveryKey) {
|
||||||
let defaultKeyId: string | null | undefined = undefined;
|
let defaultKeyId: string | null | undefined = undefined;
|
||||||
if (typeof crypto.getSecretStorageStatus === "function") {
|
const canReadSecretStorageStatus = typeof crypto.getSecretStorageStatus === "function"; // pragma: allowlist secret
|
||||||
const status = await crypto.getSecretStorageStatus().catch(() => null);
|
if (canReadSecretStorageStatus) {
|
||||||
|
const status = await crypto.getSecretStorageStatus().catch(() => null); // pragma: allowlist secret
|
||||||
defaultKeyId = status?.defaultKeyId;
|
defaultKeyId = status?.defaultKeyId;
|
||||||
}
|
}
|
||||||
this.recoveryKeyStore.storeEncodedRecoveryKey({
|
this.recoveryKeyStore.storeEncodedRecoveryKey({
|
||||||
@@ -856,12 +861,15 @@ export class MatrixClient {
|
|||||||
|
|
||||||
let activeVersion = await this.resolveActiveRoomKeyBackupVersion(crypto);
|
let activeVersion = await this.resolveActiveRoomKeyBackupVersion(crypto);
|
||||||
if (!activeVersion) {
|
if (!activeVersion) {
|
||||||
if (typeof crypto.loadSessionBackupPrivateKeyFromSecretStorage !== "function") {
|
if (
|
||||||
|
typeof crypto.loadSessionBackupPrivateKeyFromSecretStorage !==
|
||||||
|
"function" /* pragma: allowlist secret */
|
||||||
|
) {
|
||||||
return await fail(
|
return await fail(
|
||||||
"Matrix crypto backend cannot load backup keys from secret storage. Verify this device with 'openclaw matrix verify device <key>' first.",
|
"Matrix crypto backend cannot load backup keys from secret storage. Verify this device with 'openclaw matrix verify device <key>' first.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
await crypto.loadSessionBackupPrivateKeyFromSecretStorage(); // pragma: allowlist secret
|
||||||
loadedFromSecretStorage = true;
|
loadedFromSecretStorage = true;
|
||||||
activeVersion = await this.resolveActiveRoomKeyBackupVersion(crypto);
|
activeVersion = await this.resolveActiveRoomKeyBackupVersion(crypto);
|
||||||
}
|
}
|
||||||
@@ -960,8 +968,9 @@ export class MatrixClient {
|
|||||||
const rawRecoveryKey = params?.recoveryKey?.trim();
|
const rawRecoveryKey = params?.recoveryKey?.trim();
|
||||||
if (rawRecoveryKey) {
|
if (rawRecoveryKey) {
|
||||||
let defaultKeyId: string | null | undefined = undefined;
|
let defaultKeyId: string | null | undefined = undefined;
|
||||||
if (typeof crypto.getSecretStorageStatus === "function") {
|
const canReadSecretStorageStatus = typeof crypto.getSecretStorageStatus === "function"; // pragma: allowlist secret
|
||||||
const status = await crypto.getSecretStorageStatus().catch(() => null);
|
if (canReadSecretStorageStatus) {
|
||||||
|
const status = await crypto.getSecretStorageStatus().catch(() => null); // pragma: allowlist secret
|
||||||
defaultKeyId = status?.defaultKeyId;
|
defaultKeyId = status?.defaultKeyId;
|
||||||
}
|
}
|
||||||
this.recoveryKeyStore.storeEncodedRecoveryKey({
|
this.recoveryKeyStore.storeEncodedRecoveryKey({
|
||||||
@@ -1072,10 +1081,11 @@ export class MatrixClient {
|
|||||||
private async resolveCachedRoomKeyBackupDecryptionKey(
|
private async resolveCachedRoomKeyBackupDecryptionKey(
|
||||||
crypto: MatrixCryptoBootstrapApi,
|
crypto: MatrixCryptoBootstrapApi,
|
||||||
): Promise<boolean | null> {
|
): Promise<boolean | null> {
|
||||||
if (typeof crypto.getSessionBackupPrivateKey !== "function") {
|
const canGetSessionBackupPrivateKey = typeof crypto.getSessionBackupPrivateKey === "function"; // pragma: allowlist secret
|
||||||
|
if (!canGetSessionBackupPrivateKey) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const key = await crypto.getSessionBackupPrivateKey().catch(() => null);
|
const key = await crypto.getSessionBackupPrivateKey().catch(() => null); // pragma: allowlist secret
|
||||||
return key ? key.length > 0 : false;
|
return key ? key.length > 0 : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ describe("MatrixCryptoBootstrapper", () => {
|
|||||||
{
|
{
|
||||||
type: "m.login.password",
|
type: "m.login.password",
|
||||||
identifier: { type: "m.id.user", user: "@bot:example.org" },
|
identifier: { type: "m.id.user", user: "@bot:example.org" },
|
||||||
password: "super-secret-password",
|
password: "super-secret-password", // pragma: allowlist secret
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ describe("MatrixRecoveryKeyStore", () => {
|
|||||||
keyId: "GENERATED",
|
keyId: "GENERATED",
|
||||||
keyInfo: { name: "generated" },
|
keyInfo: { name: "generated" },
|
||||||
privateKey: new Uint8Array([5, 6, 7, 8]),
|
privateKey: new Uint8Array([5, 6, 7, 8]),
|
||||||
encodedPrivateKey: "encoded-generated-key",
|
encodedPrivateKey: "encoded-generated-key", // pragma: allowlist secret
|
||||||
};
|
};
|
||||||
const createRecoveryKeyFromPassphrase = vi.fn(async () => generated);
|
const createRecoveryKeyFromPassphrase = vi.fn(async () => generated);
|
||||||
const bootstrapSecretStorage = vi.fn(
|
const bootstrapSecretStorage = vi.fn(
|
||||||
@@ -98,7 +98,7 @@ describe("MatrixRecoveryKeyStore", () => {
|
|||||||
);
|
);
|
||||||
expect(store.getRecoveryKeySummary()).toMatchObject({
|
expect(store.getRecoveryKeySummary()).toMatchObject({
|
||||||
keyId: "GENERATED",
|
keyId: "GENERATED",
|
||||||
encodedPrivateKey: "encoded-generated-key",
|
encodedPrivateKey: "encoded-generated-key", // pragma: allowlist secret
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ describe("MatrixRecoveryKeyStore", () => {
|
|||||||
keyId: "RECOVERED",
|
keyId: "RECOVERED",
|
||||||
keyInfo: { name: "recovered" },
|
keyInfo: { name: "recovered" },
|
||||||
privateKey: new Uint8Array([1, 1, 2, 3]),
|
privateKey: new Uint8Array([1, 1, 2, 3]),
|
||||||
encodedPrivateKey: "encoded-recovered-key",
|
encodedPrivateKey: "encoded-recovered-key", // pragma: allowlist secret
|
||||||
};
|
};
|
||||||
const createRecoveryKeyFromPassphrase = vi.fn(async () => generated);
|
const createRecoveryKeyFromPassphrase = vi.fn(async () => generated);
|
||||||
const bootstrapSecretStorage = vi.fn(
|
const bootstrapSecretStorage = vi.fn(
|
||||||
@@ -171,7 +171,7 @@ describe("MatrixRecoveryKeyStore", () => {
|
|||||||
);
|
);
|
||||||
expect(store.getRecoveryKeySummary()).toMatchObject({
|
expect(store.getRecoveryKeySummary()).toMatchObject({
|
||||||
keyId: "RECOVERED",
|
keyId: "RECOVERED",
|
||||||
encodedPrivateKey: "encoded-recovered-key",
|
encodedPrivateKey: "encoded-recovered-key", // pragma: allowlist secret
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ describe("MatrixRecoveryKeyStore", () => {
|
|||||||
keyId: "REPAIRED",
|
keyId: "REPAIRED",
|
||||||
keyInfo: { name: "repaired" },
|
keyInfo: { name: "repaired" },
|
||||||
privateKey: new Uint8Array([7, 7, 8, 9]),
|
privateKey: new Uint8Array([7, 7, 8, 9]),
|
||||||
encodedPrivateKey: "encoded-repaired-key",
|
encodedPrivateKey: "encoded-repaired-key", // pragma: allowlist secret
|
||||||
};
|
};
|
||||||
const createRecoveryKeyFromPassphrase = vi.fn(async () => generated);
|
const createRecoveryKeyFromPassphrase = vi.fn(async () => generated);
|
||||||
const bootstrapSecretStorage = vi.fn(
|
const bootstrapSecretStorage = vi.fn(
|
||||||
@@ -223,7 +223,7 @@ describe("MatrixRecoveryKeyStore", () => {
|
|||||||
);
|
);
|
||||||
expect(store.getRecoveryKeySummary()).toMatchObject({
|
expect(store.getRecoveryKeySummary()).toMatchObject({
|
||||||
keyId: "REPAIRED",
|
keyId: "REPAIRED",
|
||||||
encodedPrivateKey: "encoded-repaired-key",
|
encodedPrivateKey: "encoded-repaired-key", // pragma: allowlist secret
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ export class MatrixRecoveryKeyStore {
|
|||||||
} = {},
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let status: MatrixSecretStorageStatus | null = null;
|
let status: MatrixSecretStorageStatus | null = null;
|
||||||
if (typeof crypto.getSecretStorageStatus === "function") {
|
const canReadSecretStorageStatus = typeof crypto.getSecretStorageStatus === "function"; // pragma: allowlist secret
|
||||||
|
if (canReadSecretStorageStatus) {
|
||||||
try {
|
try {
|
||||||
status = await crypto.getSecretStorageStatus();
|
status = await crypto.getSecretStorageStatus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -268,7 +269,7 @@ export class MatrixRecoveryKeyStore {
|
|||||||
if (
|
if (
|
||||||
parsed.version !== 1 ||
|
parsed.version !== 1 ||
|
||||||
typeof parsed.createdAt !== "string" ||
|
typeof parsed.createdAt !== "string" ||
|
||||||
typeof parsed.privateKeyBase64 !== "string" ||
|
typeof parsed.privateKeyBase64 !== "string" || // pragma: allowlist secret
|
||||||
!parsed.privateKeyBase64.trim()
|
!parsed.privateKeyBase64.trim()
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe("matrix onboarding", () => {
|
|||||||
|
|
||||||
process.env.MATRIX_HOMESERVER = "https://matrix.env.example.org";
|
process.env.MATRIX_HOMESERVER = "https://matrix.env.example.org";
|
||||||
process.env.MATRIX_USER_ID = "@env:example.org";
|
process.env.MATRIX_USER_ID = "@env:example.org";
|
||||||
process.env.MATRIX_PASSWORD = "env-password";
|
process.env.MATRIX_PASSWORD = "env-password"; // pragma: allowlist secret
|
||||||
process.env.MATRIX_ACCESS_TOKEN = "";
|
process.env.MATRIX_ACCESS_TOKEN = "";
|
||||||
process.env.MATRIX_OPS_HOMESERVER = "https://matrix.ops.env.example.org";
|
process.env.MATRIX_OPS_HOMESERVER = "https://matrix.ops.env.example.org";
|
||||||
process.env.MATRIX_OPS_ACCESS_TOKEN = "ops-env-token";
|
process.env.MATRIX_OPS_ACCESS_TOKEN = "ops-env-token";
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe("matrix legacy state migration", () => {
|
|||||||
matrix: {
|
matrix: {
|
||||||
homeserver: "https://matrix.example.org",
|
homeserver: "https://matrix.example.org",
|
||||||
userId: "@bot:example.org",
|
userId: "@bot:example.org",
|
||||||
password: "secret",
|
password: "secret", // pragma: allowlist secret
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user