diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d1ab8331..bc420783e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Docs: https://docs.clawd.bot - Plugins: require manifest-embedded config schemas, validate configs without loading plugin code, and surface plugin config warnings. (#1272) — thanks @thewilloftheshadow. - Plugins: move channel catalog metadata into plugin manifests; align Nextcloud Talk policy helpers with core patterns. (#1290) — thanks @NicholaiVogel. - Docs: refresh bird skill install metadata and usage notes. (#1302) — thanks @odysseus0. +- Matrix: migrate to matrix-bot-sdk with E2EE support, location handling, and group allowlist upgrades. (#1298) — thanks @sibbl. ### Fixes - Web search: infer Perplexity base URL from API key source (direct vs OpenRouter). - TUI: keep thinking blocks ordered before content during streaming and isolate per-run assembly. (#1202) — thanks @aaronveklabs. diff --git a/docs/channels/location.md b/docs/channels/location.md index f38031fb6..c3742c63a 100644 --- a/docs/channels/location.md +++ b/docs/channels/location.md @@ -14,6 +14,7 @@ Clawdbot normalizes shared locations from chat channels into: Currently supported: - **Telegram** (location pins + venues + live locations) - **WhatsApp** (locationMessage + liveLocationMessage) +- **Matrix** (`m.location` with `geo_uri`) ## Text formatting Locations are rendered as friendly lines without brackets: @@ -44,3 +45,4 @@ When a location is present, these fields are added to `ctx`: ## Channel notes - **Telegram**: venues map to `LocationName/LocationAddress`; live locations use `live_period`. - **WhatsApp**: `locationMessage.comment` and `liveLocationMessage.caption` are appended as the caption line. +- **Matrix**: `geo_uri` is parsed as a pin location; altitude is ignored and `LocationIsLive` is always false. diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index 02764e2e6..6870eab41 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -5,17 +5,26 @@ read_when: --- # Matrix (plugin) -Status: supported via plugin (matrix-js-sdk). Direct messages, rooms, threads, media, reactions, and polls. +Matrix is an open, decentralized messaging protocol. Clawdbot connects as a Matrix **user** +on any homeserver, so you need a Matrix account for the bot. Once it is logged in, you can DM +the bot directly or invite it to rooms (Matrix "groups"). Beeper is a valid client option too, +but it requires E2EE to be enabled. + +Status: supported via plugin (matrix-bot-sdk). Direct messages, rooms, threads, media, reactions, +polls (send + poll-start as text), location, and E2EE (with crypto support). ## Plugin required + Matrix ships as a plugin and is not bundled with the core install. Install via CLI (npm registry): + ```bash clawdbot plugins install @clawdbot/matrix ``` Local checkout (when running from a git repo): + ```bash clawdbot plugins install ./extensions/matrix ``` @@ -25,27 +34,54 @@ Clawdbot will offer the local install path automatically. Details: [Plugins](/plugin) -## Quick setup (beginner) +## Setup + 1) Install the Matrix plugin: - From npm: `clawdbot plugins install @clawdbot/matrix` - From a local checkout: `clawdbot plugins install ./extensions/matrix` -2) Configure credentials: - - Env: `MATRIX_HOMESERVER`, `MATRIX_USER_ID`, `MATRIX_ACCESS_TOKEN` (or `MATRIX_PASSWORD`) +2) Create a Matrix account on a homeserver: + - Browse hosting options at [https://matrix.org/ecosystem/hosting/](https://matrix.org/ecosystem/hosting/) + - Or host it yourself. +3) Get an access token for the bot account: + - Use the Matrix login API with `curl` at your home server: + + ```bash + curl --request POST \ + --url https://matrix.example.org/_matrix/client/v3/login \ + --header 'Content-Type: application/json' \ + --data '{ + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": "your-user-name" + }, + "password": "your-password" + }' + ``` + + - Replace `matrix.example.org` with your homeserver URL. + - Or set `channels.matrix.userId` + `channels.matrix.password`: Clawdbot calls the same + login endpoint, stores the access token in `~/.clawdbot/credentials/matrix/credentials.json`, + and reuses it on next start. +4) Configure credentials: + - Env: `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN` (or `MATRIX_USER_ID` + `MATRIX_PASSWORD`) - Or config: `channels.matrix.*` - If both are set, config takes precedence. -3) Restart the gateway (or finish onboarding). -4) DM access defaults to pairing; approve the pairing code on first contact. + - With access token: user ID is fetched automatically via `/whoami`. + - When set, `channels.matrix.userId` should be the full Matrix ID (example: `@bot:example.org`). +5) Restart the gateway (or finish onboarding). +6) Start a DM with the bot or invite it to a room from any Matrix client + (Element, Beeper, etc.; see https://matrix.org/ecosystem/clients/). Beeper requires E2EE, + so set `channels.matrix.encryption: true` and verify the device. -Runtime note: Matrix requires Node.js (Bun is not supported). +Minimal config (access token, user ID auto-fetched): -Minimal config: ```json5 { channels: { matrix: { enabled: true, homeserver: "https://matrix.example.org", - userId: "@clawdbot:example.org", accessToken: "syt_***", dm: { policy: "pairing" } } @@ -53,18 +89,53 @@ Minimal config: } ``` -## Encryption (E2EE) -End-to-end encrypted rooms are **not** supported. -- Use unencrypted rooms or disable encryption when creating the room. -- If a room is E2EE, the bot will receive encrypted events and won’t reply. +E2EE config (end to end encryption enabled): -## What it is -Matrix is an open messaging protocol. Clawdbot connects as a Matrix user and listens to DMs and rooms. -- A Matrix user account owned by the Gateway. -- Deterministic routing: replies go back to Matrix. +```json5 +{ + channels: { + matrix: { + enabled: true, + homeserver: "https://matrix.example.org", + accessToken: "syt_***", + encryption: true, + dm: { policy: "pairing" } + } + } +} +``` + +## Encryption (E2EE) + +End-to-end encryption is **supported** via the Rust crypto SDK. + +Enable with `channels.matrix.encryption: true`: + +- If the crypto module loads, encrypted rooms are decrypted automatically. +- Outbound media is encrypted when sending to encrypted rooms. +- On first connection, Clawdbot requests device verification from your other sessions. +- Verify the device in another Matrix client (Element, etc.) to enable key sharing. +- If the crypto module cannot be loaded, E2EE is disabled and encrypted rooms will not decrypt; + Clawdbot logs a warning. + +Crypto state is stored per account + access token in +`~/.clawdbot/matrix/accounts//__//crypto/` +(SQLite database). Sync state lives alongside it in `bot-storage.json`. +If the access token (device) changes, a new store is created and the bot must be +re-verified for encrypted rooms. + +**Device verification:** +When E2EE is enabled, the bot will request verification from your other sessions on startup. +Open Element (or another client) and approve the verification request to establish trust. +Once verified, the bot can decrypt messages in encrypted rooms. + +## Routing model + +- Replies always go back to Matrix. - DMs share the agent's main session; rooms map to group sessions. ## Access control (DMs) + - Default: `channels.matrix.dm.policy = "pairing"`. Unknown senders get a pairing code. - Approve via: - `clawdbot pairing list matrix` @@ -73,58 +144,80 @@ Matrix is an open messaging protocol. Clawdbot connects as a Matrix user and lis - `channels.matrix.dm.allowFrom` accepts user IDs or display names. The wizard resolves display names to user IDs when directory search is available. ## Rooms (groups) + - Default: `channels.matrix.groupPolicy = "allowlist"` (mention-gated). Use `channels.defaults.groupPolicy` to override the default when unset. -- Allowlist rooms with `channels.matrix.rooms`: +- Allowlist rooms with `channels.matrix.groups` (room IDs, aliases, or names): + ```json5 { channels: { matrix: { - rooms: { - "!roomId:example.org": { requireMention: true } - } + groupPolicy: "allowlist", + groups: { + "!roomId:example.org": { allow: true }, + "#alias:example.org": { allow: true } + }, + groupAllowFrom: ["@owner:example.org"] } } } ``` + - `requireMention: false` enables auto-reply in that room. +- `groups."*"` can set defaults for mention gating across rooms. +- `groupAllowFrom` restricts which senders can trigger the bot in rooms (optional). +- Per-room `users` allowlists can further restrict senders inside a specific room. - The configure wizard prompts for room allowlists (room IDs, aliases, or names) and resolves names when possible. - On startup, Clawdbot resolves room/user names in allowlists to IDs and logs the mapping; unresolved entries are kept as typed. +- Invites are auto-joined by default; control with `channels.matrix.autoJoin` and `channels.matrix.autoJoinAllowlist`. - To allow **no rooms**, set `channels.matrix.groupPolicy: "disabled"` (or keep an empty allowlist). +- Legacy key: `channels.matrix.rooms` (same shape as `groups`). ## Threads + - Reply threading is supported. -- `channels.matrix.replyToMode` controls replies when tagged: +- `channels.matrix.threadReplies` controls whether replies stay in threads: + - `off`, `inbound` (default), `always` +- `channels.matrix.replyToMode` controls reply-to metadata when not replying in a thread: - `off` (default), `first`, `all` ## Capabilities + | Feature | Status | |---------|--------| | Direct messages | ✅ Supported | | Rooms | ✅ Supported | | Threads | ✅ Supported | | Media | ✅ Supported | -| Reactions | ✅ Supported | -| Polls | ✅ Supported | +| E2EE | ✅ Supported (crypto module required) | +| Reactions | ✅ Supported (send/read via tools) | +| Polls | ✅ Send supported; inbound poll starts are converted to text (responses/ends ignored) | +| Location | ✅ Supported (geo URI; altitude ignored) | | Native commands | ✅ Supported | ## Configuration reference (Matrix) + Full configuration: [Configuration](/gateway/configuration) Provider options: + - `channels.matrix.enabled`: enable/disable channel startup. - `channels.matrix.homeserver`: homeserver URL. -- `channels.matrix.userId`: Matrix user ID. +- `channels.matrix.userId`: Matrix user ID (optional with access token). - `channels.matrix.accessToken`: access token. - `channels.matrix.password`: password for login (token stored). - `channels.matrix.deviceName`: device display name. +- `channels.matrix.encryption`: enable E2EE (default: false). - `channels.matrix.initialSyncLimit`: initial sync limit. - `channels.matrix.threadReplies`: `off | inbound | always` (default: inbound). - `channels.matrix.textChunkLimit`: outbound text chunk size (chars). - `channels.matrix.dm.policy`: `pairing | allowlist | open | disabled` (default: pairing). - `channels.matrix.dm.allowFrom`: DM allowlist (user IDs or display names). `open` requires `"*"`. The wizard resolves names to IDs when possible. - `channels.matrix.groupPolicy`: `allowlist | open | disabled` (default: allowlist). +- `channels.matrix.groupAllowFrom`: allowlisted senders for group messages. - `channels.matrix.allowlistOnly`: force allowlist rules for DMs + rooms. -- `channels.matrix.rooms`: per-room settings and allowlist. +- `channels.matrix.groups`: group allowlist + per-room settings map. +- `channels.matrix.rooms`: legacy group allowlist/config. - `channels.matrix.replyToMode`: reply-to mode for threads/tags. - `channels.matrix.mediaMaxMb`: inbound/outbound media cap (MB). - `channels.matrix.autoJoin`: invite handling (`always | allowlist | off`, default: always). diff --git a/docs/concepts/groups.md b/docs/concepts/groups.md index f64b5bd8a..d6e72aac8 100644 --- a/docs/concepts/groups.md +++ b/docs/concepts/groups.md @@ -149,6 +149,14 @@ Control how group/room messages are handled per channel: slack: { groupPolicy: "allowlist", channels: { "#general": { allow: true } } + }, + matrix: { + groupPolicy: "allowlist", + groupAllowFrom: ["@owner:example.org"], + groups: { + "!roomId:example.org": { allow: true }, + "#alias:example.org": { allow: true } + } } } } @@ -165,6 +173,7 @@ Notes: - WhatsApp/Telegram/Signal/iMessage/Microsoft Teams: use `groupAllowFrom` (fallback: explicit `allowFrom`). - Discord: allowlist uses `channels.discord.guilds..channels`. - Slack: allowlist uses `channels.slack.channels`. +- Matrix: allowlist uses `channels.matrix.groups` (room IDs, aliases, or names). Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists are also supported. - Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`). - Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive. - Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked. diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json index 7cb454ff7..e0e3fdf17 100644 --- a/extensions/matrix/package.json +++ b/extensions/matrix/package.json @@ -24,8 +24,10 @@ } }, "dependencies": { + "@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0", "clawdbot": "workspace:*", "markdown-it": "14.1.0", - "matrix-js-sdk": "40.0.0" + "matrix-bot-sdk": "0.8.0", + "music-metadata": "^11.10.6" } } diff --git a/extensions/matrix/src/channel.directory.test.ts b/extensions/matrix/src/channel.directory.test.ts index 4d004134f..c83f7121b 100644 --- a/extensions/matrix/src/channel.directory.test.ts +++ b/extensions/matrix/src/channel.directory.test.ts @@ -1,4 +1,3 @@ -import os from "node:os"; import { beforeEach, describe, expect, it } from "vitest"; import type { PluginRuntime } from "clawdbot/plugin-sdk"; @@ -11,7 +10,7 @@ describe("matrix directory", () => { beforeEach(() => { setMatrixRuntime({ state: { - resolveStateDir: () => os.tmpdir(), + resolveStateDir: (_env, homeDir) => homeDir(), }, } as PluginRuntime); }); @@ -21,7 +20,8 @@ describe("matrix directory", () => { channels: { matrix: { dm: { allowFrom: ["matrix:@alice:example.org", "bob"] }, - rooms: { + groupAllowFrom: ["@dana:example.org"], + groups: { "!room1:example.org": { users: ["@carol:example.org"] }, "#alias:example.org": { users: [] }, }, @@ -40,6 +40,7 @@ describe("matrix directory", () => { { kind: "user", id: "user:@alice:example.org" }, { kind: "user", id: "bob", name: "incomplete id; expected @user:server" }, { kind: "user", id: "user:@carol:example.org" }, + { kind: "user", id: "user:@dana:example.org" }, ]), ); diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index 3d65152ee..3e17f009b 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -46,10 +46,12 @@ const meta = { function normalizeMatrixMessagingTarget(raw: string): string | undefined { let normalized = raw.trim(); if (!normalized) return undefined; - if (normalized.toLowerCase().startsWith("matrix:")) { + const lowered = normalized.toLowerCase(); + if (lowered.startsWith("matrix:")) { normalized = normalized.slice("matrix:".length).trim(); } - return normalized ? normalized.toLowerCase() : undefined; + const stripped = normalized.replace(/^(room|channel|user):/i, "").trim(); + return stripped || undefined; } function buildMatrixConfigUpdate( @@ -155,10 +157,11 @@ export const matrixPlugin: ChannelPlugin = { }), collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = (cfg as CoreConfig).channels?.defaults?.groupPolicy; - const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; + const groupPolicy = + account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; if (groupPolicy !== "open") return []; return [ - "- Matrix rooms: groupPolicy=\"open\" allows any room to trigger (mention-gated). Set channels.matrix.groupPolicy=\"allowlist\" + channels.matrix.rooms to restrict rooms.", + "- Matrix rooms: groupPolicy=\"open\" allows any room to trigger (mention-gated). Set channels.matrix.groupPolicy=\"allowlist\" + channels.matrix.groups (and optionally channels.matrix.groupAllowFrom) to restrict rooms.", ]; }, }, @@ -168,6 +171,17 @@ export const matrixPlugin: ChannelPlugin = { threading: { resolveReplyToMode: ({ cfg }) => (cfg as CoreConfig).channels?.matrix?.replyToMode ?? "off", + buildToolContext: ({ context, hasRepliedRef }) => { + const currentTarget = context.To; + return { + currentChannelId: currentTarget?.trim() || undefined, + currentThreadTs: + context.MessageThreadId != null + ? String(context.MessageThreadId) + : context.ReplyToId, + hasRepliedRef, + }; + }, }, messaging: { normalizeTarget: normalizeMatrixMessagingTarget, @@ -194,7 +208,14 @@ export const matrixPlugin: ChannelPlugin = { ids.add(raw.replace(/^matrix:/i, "")); } - for (const room of Object.values(account.config.rooms ?? {})) { + for (const entry of account.config.groupAllowFrom ?? []) { + const raw = String(entry).trim(); + if (!raw || raw === "*") continue; + ids.add(raw.replace(/^matrix:/i, "")); + } + + const groups = account.config.groups ?? account.config.rooms ?? {}; + for (const room of Object.values(groups)) { for (const entry of room.users ?? []) { const raw = String(entry).trim(); if (!raw || raw === "*") continue; @@ -226,7 +247,8 @@ export const matrixPlugin: ChannelPlugin = { listGroups: async ({ cfg, accountId, query, limit }) => { const account = resolveMatrixAccount({ cfg: cfg as CoreConfig, accountId }); const q = query?.trim().toLowerCase() || ""; - const ids = Object.keys(account.config.rooms ?? {}) + const groups = account.config.groups ?? account.config.rooms ?? {}; + const ids = Object.keys(groups) .map((raw) => raw.trim()) .filter((raw) => Boolean(raw) && raw !== "*") .map((raw) => raw.replace(/^matrix:/i, "")) @@ -263,10 +285,16 @@ export const matrixPlugin: ChannelPlugin = { validateInput: ({ input }) => { if (input.useEnv) return null; if (!input.homeserver?.trim()) return "Matrix requires --homeserver"; - if (!input.userId?.trim()) return "Matrix requires --user-id"; - if (!input.accessToken?.trim() && !input.password?.trim()) { + const accessToken = input.accessToken?.trim(); + const password = input.password?.trim(); + const userId = input.userId?.trim(); + if (!accessToken && !password) { return "Matrix requires --access-token or --password"; } + if (!accessToken) { + if (!userId) return "Matrix requires --user-id when using --password"; + if (!password) return "Matrix requires --password when using --user-id"; + } return null; }, applyAccountConfig: ({ cfg, input }) => { @@ -381,6 +409,7 @@ export const matrixPlugin: ChannelPlugin = { mediaMaxMb: account.config.mediaMaxMb, initialSyncLimit: account.config.initialSyncLimit, replyToMode: account.config.replyToMode, + accountId: account.accountId, }); }, }, diff --git a/extensions/matrix/src/config-schema.ts b/extensions/matrix/src/config-schema.ts index 04966621d..3cb396883 100644 --- a/extensions/matrix/src/config-schema.ts +++ b/extensions/matrix/src/config-schema.ts @@ -41,6 +41,7 @@ export const MatrixConfigSchema = z.object({ password: z.string().optional(), deviceName: z.string().optional(), initialSyncLimit: z.number().optional(), + encryption: z.boolean().optional(), allowlistOnly: z.boolean().optional(), groupPolicy: z.enum(["open", "disabled", "allowlist"]).optional(), replyToMode: z.enum(["off", "first", "all"]).optional(), @@ -49,7 +50,9 @@ export const MatrixConfigSchema = z.object({ mediaMaxMb: z.number().optional(), autoJoin: z.enum(["always", "allowlist", "off"]).optional(), autoJoinAllowlist: z.array(allowFromEntry).optional(), + groupAllowFrom: z.array(allowFromEntry).optional(), dm: matrixDmSchema, + groups: z.object({}).catchall(matrixRoomSchema).optional(), rooms: z.object({}).catchall(matrixRoomSchema).optional(), actions: matrixActionSchema, }); diff --git a/extensions/matrix/src/group-mentions.ts b/extensions/matrix/src/group-mentions.ts index ee16b713c..5c6aecb5b 100644 --- a/extensions/matrix/src/group-mentions.ts +++ b/extensions/matrix/src/group-mentions.ts @@ -20,7 +20,7 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b const aliases = groupChannel ? [groupChannel] : []; const cfg = params.cfg as CoreConfig; const resolved = resolveMatrixRoomConfig({ - rooms: cfg.channels?.matrix?.rooms, + rooms: cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms, roomId, aliases, name: groupChannel || undefined, diff --git a/extensions/matrix/src/matrix/accounts.test.ts b/extensions/matrix/src/matrix/accounts.test.ts new file mode 100644 index 000000000..2f1cfdb10 --- /dev/null +++ b/extensions/matrix/src/matrix/accounts.test.ts @@ -0,0 +1,83 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { CoreConfig } from "../types.js"; +import { resolveMatrixAccount } from "./accounts.js"; + +vi.mock("./credentials.js", () => ({ + loadMatrixCredentials: () => null, + credentialsMatchConfig: () => false, +})); + +const envKeys = [ + "MATRIX_HOMESERVER", + "MATRIX_USER_ID", + "MATRIX_ACCESS_TOKEN", + "MATRIX_PASSWORD", + "MATRIX_DEVICE_NAME", +]; + +describe("resolveMatrixAccount", () => { + let prevEnv: Record = {}; + + beforeEach(() => { + prevEnv = {}; + for (const key of envKeys) { + prevEnv[key] = process.env[key]; + delete process.env[key]; + } + }); + + afterEach(() => { + for (const key of envKeys) { + const value = prevEnv[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + }); + + it("treats access-token-only config as configured", () => { + const cfg: CoreConfig = { + channels: { + matrix: { + homeserver: "https://matrix.example.org", + accessToken: "tok-access", + }, + }, + }; + + const account = resolveMatrixAccount({ cfg }); + expect(account.configured).toBe(true); + }); + + it("requires userId + password when no access token is set", () => { + const cfg: CoreConfig = { + channels: { + matrix: { + homeserver: "https://matrix.example.org", + userId: "@bot:example.org", + }, + }, + }; + + const account = resolveMatrixAccount({ cfg }); + expect(account.configured).toBe(false); + }); + + it("marks password auth as configured when userId is present", () => { + const cfg: CoreConfig = { + channels: { + matrix: { + homeserver: "https://matrix.example.org", + userId: "@bot:example.org", + password: "secret", + }, + }, + }; + + const account = resolveMatrixAccount({ cfg }); + expect(account.configured).toBe(true); + }); +}); diff --git a/extensions/matrix/src/matrix/accounts.ts b/extensions/matrix/src/matrix/accounts.ts index d451a58d7..8c95c3f1a 100644 --- a/extensions/matrix/src/matrix/accounts.ts +++ b/extensions/matrix/src/matrix/accounts.ts @@ -31,18 +31,20 @@ export function resolveMatrixAccount(params: { const base = (params.cfg.channels?.matrix ?? {}) as MatrixConfig; const enabled = base.enabled !== false; const resolved = resolveMatrixConfig(params.cfg, process.env); - const hasCore = Boolean(resolved.homeserver && resolved.userId); - const hasToken = Boolean(resolved.accessToken || resolved.password); + const hasHomeserver = Boolean(resolved.homeserver); + const hasUserId = Boolean(resolved.userId); + const hasAccessToken = Boolean(resolved.accessToken); + const hasPassword = Boolean(resolved.password); + const hasPasswordAuth = hasUserId && hasPassword; const stored = loadMatrixCredentials(process.env); const hasStored = - stored && - resolved.homeserver && - resolved.userId && - credentialsMatchConfig(stored, { - homeserver: resolved.homeserver, - userId: resolved.userId, - }); - const configured = hasCore && (hasToken || Boolean(hasStored)); + stored && resolved.homeserver + ? credentialsMatchConfig(stored, { + homeserver: resolved.homeserver, + userId: resolved.userId || "", + }) + : false; + const configured = hasHomeserver && (hasAccessToken || hasPasswordAuth || Boolean(hasStored)); return { accountId, enabled, diff --git a/extensions/matrix/src/matrix/actions.ts b/extensions/matrix/src/matrix/actions.ts index 4c95b936e..81c6b7330 100644 --- a/extensions/matrix/src/matrix/actions.ts +++ b/extensions/matrix/src/matrix/actions.ts @@ -1,19 +1,4 @@ -import type { MatrixClient, MatrixEvent } from "matrix-js-sdk"; -import { - Direction, - EventType, - MatrixError, - MsgType, - RelationType, -} from "matrix-js-sdk"; -import type { - ReactionEventContent, - RoomMessageEventContent, -} from "matrix-js-sdk/lib/@types/events.js"; -import type { - RoomPinnedEventsEventContent, - RoomTopicEventContent, -} from "matrix-js-sdk/lib/@types/state_events.js"; +import type { MatrixClient } from "matrix-bot-sdk"; import { getMatrixRuntime } from "../runtime.js"; import type { CoreConfig } from "../types.js"; @@ -23,7 +8,6 @@ import { isBunRuntime, resolveMatrixAuth, resolveSharedMatrixClient, - waitForMatrixSync, } from "./client.js"; import { reactMatrixMessage, @@ -31,6 +15,62 @@ import { sendMessageMatrix, } from "./send.js"; +// Constants that were previously from matrix-js-sdk +const MsgType = { + Text: "m.text", +} as const; + +const RelationType = { + Replace: "m.replace", + Annotation: "m.annotation", +} as const; + +const EventType = { + RoomMessage: "m.room.message", + RoomPinnedEvents: "m.room.pinned_events", + RoomTopic: "m.room.topic", + Reaction: "m.reaction", +} as const; + +// Type definitions for matrix-bot-sdk event content +type RoomMessageEventContent = { + msgtype: string; + body: string; + "m.new_content"?: RoomMessageEventContent; + "m.relates_to"?: { + rel_type?: string; + event_id?: string; + "m.in_reply_to"?: { event_id?: string }; + }; +}; + +type ReactionEventContent = { + "m.relates_to": { + rel_type: string; + event_id: string; + key: string; + }; +}; + +type RoomPinnedEventsEventContent = { + pinned: string[]; +}; + +type RoomTopicEventContent = { + topic?: string; +}; + +type MatrixRawEvent = { + event_id: string; + sender: string; + type: string; + origin_server_ts: number; + content: Record; + unsigned?: { + redacted_because?: unknown; + }; +}; + export type MatrixActionClientOpts = { client?: MatrixClient; timeoutMs?: number; @@ -86,19 +126,23 @@ async function resolveActionClient(opts: MatrixActionClientOpts = {}): Promise(); +function summarizeMatrixRawEvent(event: MatrixRawEvent): MatrixMessageSummary { + const content = event.content as RoomMessageEventContent; const relates = content["m.relates_to"]; let relType: string | undefined; let eventId: string | undefined; @@ -118,27 +162,28 @@ function summarizeMatrixEvent(event: MatrixEvent): MatrixMessageSummary { } : undefined; return { - eventId: event.getId() ?? undefined, - sender: event.getSender() ?? undefined, + eventId: event.event_id, + sender: event.sender, body: content.body, msgtype: content.msgtype, - timestamp: event.getTs() ?? undefined, + timestamp: event.origin_server_ts, relatesTo, }; } async function readPinnedEvents(client: MatrixClient, roomId: string): Promise { try { - const content = (await client.getStateEvent( + const content = (await client.getRoomStateEvent( roomId, EventType.RoomPinnedEvents, "", )) as RoomPinnedEventsEventContent; const pinned = content.pinned; return pinned.filter((id) => id.trim().length > 0); - } catch (err) { - const httpStatus = err instanceof MatrixError ? err.httpStatus : undefined; - const errcode = err instanceof MatrixError ? err.errcode : undefined; + } catch (err: unknown) { + const errObj = err as { statusCode?: number; body?: { errcode?: string } }; + const httpStatus = errObj.statusCode; + const errcode = errObj.body?.errcode; if (httpStatus === 404 || errcode === "M_NOT_FOUND") { return []; } @@ -151,11 +196,14 @@ async function fetchEventSummary( roomId: string, eventId: string, ): Promise { - const raw = await client.fetchRoomEvent(roomId, eventId); - const mapper = client.getEventMapper(); - const event = mapper(raw); - if (event.isRedacted()) return null; - return summarizeMatrixEvent(event); + try { + const raw = await client.getEvent(roomId, eventId) as MatrixRawEvent; + if (raw.unsigned?.redacted_because) return null; + return summarizeMatrixRawEvent(raw); + } catch (err) { + // Event not found, redacted, or inaccessible - return null + return null; + } } export async function sendMatrixMessage( @@ -200,10 +248,10 @@ export async function editMatrixMessage( event_id: messageId, }, }; - const response = await client.sendMessage(resolvedRoom, payload); - return { eventId: response.event_id ?? null }; + const eventId = await client.sendMessage(resolvedRoom, payload); + return { eventId: eventId ?? null }; } finally { - if (stopOnDone) client.stopClient(); + if (stopOnDone) client.stop(); } } @@ -215,11 +263,9 @@ export async function deleteMatrixMessage( const { client, stopOnDone } = await resolveActionClient(opts); try { const resolvedRoom = await resolveMatrixRoomId(client, roomId); - await client.redactEvent(resolvedRoom, messageId, undefined, { - reason: opts.reason, - }); + await client.redactEvent(resolvedRoom, messageId, opts.reason); } finally { - if (stopOnDone) client.stopClient(); + if (stopOnDone) client.stop(); } } @@ -242,22 +288,25 @@ export async function readMatrixMessages( typeof opts.limit === "number" && Number.isFinite(opts.limit) ? Math.max(1, Math.floor(opts.limit)) : 20; - const token = opts.before?.trim() || opts.after?.trim() || null; - const dir = opts.after ? Direction.Forward : Direction.Backward; - const res = await client.createMessagesRequest(resolvedRoom, token, limit, dir); - const mapper = client.getEventMapper(); - const events = res.chunk.map(mapper); - const messages = events - .filter((event) => event.getType() === EventType.RoomMessage) - .filter((event) => !event.isRedacted()) - .map(summarizeMatrixEvent); + const token = opts.before?.trim() || opts.after?.trim() || undefined; + const dir = opts.after ? "f" : "b"; + // matrix-bot-sdk uses doRequest for room messages + const res = await client.doRequest("GET", `/_matrix/client/v3/rooms/${encodeURIComponent(resolvedRoom)}/messages`, { + dir, + limit, + from: token, + }) as { chunk: MatrixRawEvent[]; start?: string; end?: string }; + const messages = res.chunk + .filter((event) => event.type === EventType.RoomMessage) + .filter((event) => !event.unsigned?.redacted_because) + .map(summarizeMatrixRawEvent); return { messages, nextBatch: res.end ?? null, prevBatch: res.start ?? null, }; } finally { - if (stopOnDone) client.stopClient(); + if (stopOnDone) client.stop(); } } @@ -273,19 +322,18 @@ export async function listMatrixReactions( typeof opts.limit === "number" && Number.isFinite(opts.limit) ? Math.max(1, Math.floor(opts.limit)) : 100; - const res = await client.relations( - resolvedRoom, - messageId, - RelationType.Annotation, - EventType.Reaction, - { dir: Direction.Backward, limit }, - ); + // matrix-bot-sdk uses doRequest for relations + const res = await client.doRequest( + "GET", + `/_matrix/client/v1/rooms/${encodeURIComponent(resolvedRoom)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`, + { dir: "b", limit }, + ) as { chunk: MatrixRawEvent[] }; const summaries = new Map(); - for (const event of res.events) { - const content = event.getContent(); - const key = content["m.relates_to"].key; + for (const event of res.chunk) { + const content = event.content as ReactionEventContent; + const key = content["m.relates_to"]?.key; if (!key) continue; - const sender = event.getSender() ?? ""; + const sender = event.sender ?? ""; const entry: MatrixReactionSummary = summaries.get(key) ?? { key, count: 0, @@ -299,7 +347,7 @@ export async function listMatrixReactions( } return Array.from(summaries.values()); } finally { - if (stopOnDone) client.stopClient(); + if (stopOnDone) client.stop(); } } @@ -311,30 +359,28 @@ export async function removeMatrixReactions( const { client, stopOnDone } = await resolveActionClient(opts); try { const resolvedRoom = await resolveMatrixRoomId(client, roomId); - const res = await client.relations( - resolvedRoom, - messageId, - RelationType.Annotation, - EventType.Reaction, - { dir: Direction.Backward, limit: 200 }, - ); - const userId = client.getUserId(); + const res = await client.doRequest( + "GET", + `/_matrix/client/v1/rooms/${encodeURIComponent(resolvedRoom)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`, + { dir: "b", limit: 200 }, + ) as { chunk: MatrixRawEvent[] }; + const userId = await client.getUserId(); if (!userId) return { removed: 0 }; const targetEmoji = opts.emoji?.trim(); - const toRemove = res.events - .filter((event) => event.getSender() === userId) + const toRemove = res.chunk + .filter((event) => event.sender === userId) .filter((event) => { if (!targetEmoji) return true; - const content = event.getContent(); - return content["m.relates_to"].key === targetEmoji; + const content = event.content as ReactionEventContent; + return content["m.relates_to"]?.key === targetEmoji; }) - .map((event) => event.getId()) + .map((event) => event.event_id) .filter((id): id is string => Boolean(id)); if (toRemove.length === 0) return { removed: 0 }; await Promise.all(toRemove.map((id) => client.redactEvent(resolvedRoom, id))); return { removed: toRemove.length }; } finally { - if (stopOnDone) client.stopClient(); + if (stopOnDone) client.stop(); } } @@ -349,10 +395,10 @@ export async function pinMatrixMessage( const current = await readPinnedEvents(client, resolvedRoom); const next = current.includes(messageId) ? current : [...current, messageId]; const payload: RoomPinnedEventsEventContent = { pinned: next }; - await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, payload); + await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, "", payload); return { pinned: next }; } finally { - if (stopOnDone) client.stopClient(); + if (stopOnDone) client.stop(); } } @@ -367,10 +413,10 @@ export async function unpinMatrixMessage( const current = await readPinnedEvents(client, resolvedRoom); const next = current.filter((id) => id !== messageId); const payload: RoomPinnedEventsEventContent = { pinned: next }; - await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, payload); + await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, "", payload); return { pinned: next }; } finally { - if (stopOnDone) client.stopClient(); + if (stopOnDone) client.stop(); } } @@ -395,7 +441,7 @@ export async function listMatrixPins( ).filter((event): event is MatrixMessageSummary => Boolean(event)); return { pinned, events }; } finally { - if (stopOnDone) client.stopClient(); + if (stopOnDone) client.stop(); } } @@ -406,20 +452,23 @@ export async function getMatrixMemberInfo( const { client, stopOnDone } = await resolveActionClient(opts); try { const roomId = opts.roomId ? await resolveMatrixRoomId(client, opts.roomId) : undefined; - const profile = await client.getProfileInfo(userId); - const member = roomId ? client.getRoom(roomId)?.getMember(userId) : undefined; + // matrix-bot-sdk uses getUserProfile + const profile = await client.getUserProfile(userId); + // Note: matrix-bot-sdk doesn't have getRoom().getMember() like matrix-js-sdk + // We'd need to fetch room state separately if needed return { userId, profile: { displayName: profile?.displayname ?? null, avatarUrl: profile?.avatar_url ?? null, }, - membership: member?.membership ?? null, - powerLevel: member?.powerLevel ?? null, - displayName: member?.name ?? null, + membership: null, // Would need separate room state query + powerLevel: null, // Would need separate power levels state query + displayName: profile?.displayname ?? null, + roomId: roomId ?? null, }; } finally { - if (stopOnDone) client.stopClient(); + if (stopOnDone) client.stop(); } } @@ -427,20 +476,42 @@ export async function getMatrixRoomInfo(roomId: string, opts: MatrixActionClient const { client, stopOnDone } = await resolveActionClient(opts); try { const resolvedRoom = await resolveMatrixRoomId(client, roomId); - const room = client.getRoom(resolvedRoom); - const topicEvent = room?.currentState.getStateEvents(EventType.RoomTopic, ""); - const topicContent = topicEvent?.getContent(); - const topic = typeof topicContent?.topic === "string" ? topicContent.topic : undefined; + // matrix-bot-sdk uses getRoomState for state events + let name: string | null = null; + let topic: string | null = null; + let canonicalAlias: string | null = null; + let memberCount: number | null = null; + + try { + const nameState = await client.getRoomStateEvent(resolvedRoom, "m.room.name", ""); + name = nameState?.name ?? null; + } catch { /* ignore */ } + + try { + const topicState = await client.getRoomStateEvent(resolvedRoom, EventType.RoomTopic, ""); + topic = topicState?.topic ?? null; + } catch { /* ignore */ } + + try { + const aliasState = await client.getRoomStateEvent(resolvedRoom, "m.room.canonical_alias", ""); + canonicalAlias = aliasState?.alias ?? null; + } catch { /* ignore */ } + + try { + const members = await client.getJoinedRoomMembers(resolvedRoom); + memberCount = members.length; + } catch { /* ignore */ } + return { roomId: resolvedRoom, - name: room?.name ?? null, - topic: topic ?? null, - canonicalAlias: room?.getCanonicalAlias?.() ?? null, - altAliases: room?.getAltAliases?.() ?? [], - memberCount: room?.getJoinedMemberCount?.() ?? null, + name, + topic, + canonicalAlias, + altAliases: [], // Would need separate query + memberCount, }; } finally { - if (stopOnDone) client.stopClient(); + if (stopOnDone) client.stop(); } } diff --git a/extensions/matrix/src/matrix/active-client.ts b/extensions/matrix/src/matrix/active-client.ts index 2befe15b6..9aa0ffdde 100644 --- a/extensions/matrix/src/matrix/active-client.ts +++ b/extensions/matrix/src/matrix/active-client.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-js-sdk"; +import type { MatrixClient } from "matrix-bot-sdk"; let activeClient: MatrixClient | null = null; diff --git a/extensions/matrix/src/matrix/client.test.ts b/extensions/matrix/src/matrix/client.test.ts index 547d0d981..f806f9c81 100644 --- a/extensions/matrix/src/matrix/client.test.ts +++ b/extensions/matrix/src/matrix/client.test.ts @@ -32,6 +32,7 @@ describe("resolveMatrixConfig", () => { password: "cfg-pass", deviceName: "CfgDevice", initialSyncLimit: 5, + encryption: false, }); }); @@ -51,5 +52,6 @@ describe("resolveMatrixConfig", () => { expect(resolved.password).toBe("env-pass"); expect(resolved.deviceName).toBe("EnvDevice"); expect(resolved.initialSyncLimit).toBeUndefined(); + expect(resolved.encryption).toBe(false); }); }); diff --git a/extensions/matrix/src/matrix/client.ts b/extensions/matrix/src/matrix/client.ts index 6493d0a82..02a086970 100644 --- a/extensions/matrix/src/matrix/client.ts +++ b/extensions/matrix/src/matrix/client.ts @@ -1,4 +1,16 @@ -import { ClientEvent, type MatrixClient, SyncState } from "matrix-js-sdk"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { + ConsoleLogger, + LogService, + MatrixClient, + SimpleFsStorageProvider, + RustSdkCryptoStorageProvider, +} from "matrix-bot-sdk"; +import type { IStorageProvider, ICryptoStorageProvider } from "matrix-bot-sdk"; import type { CoreConfig } from "../types.js"; import { getMatrixRuntime } from "../runtime.js"; @@ -10,22 +22,30 @@ export type MatrixResolvedConfig = { password?: string; deviceName?: string; initialSyncLimit?: number; + encryption?: boolean; }; +/** + * Authenticated Matrix configuration. + * Note: deviceId is NOT included here because it's implicit in the accessToken. + * The crypto storage assumes the device ID (and thus access token) does not change + * between restarts. If the access token becomes invalid or crypto storage is lost, + * both will need to be recreated together. + */ export type MatrixAuth = { homeserver: string; userId: string; accessToken: string; deviceName?: string; initialSyncLimit?: number; + encryption?: boolean; }; -type MatrixSdk = typeof import("matrix-js-sdk"); - type SharedMatrixClientState = { client: MatrixClient; key: string; started: boolean; + cryptoReady: boolean; }; let sharedClientState: SharedMatrixClientState | null = null; @@ -37,14 +57,198 @@ export function isBunRuntime(): boolean { return typeof versions.bun === "string"; } -async function loadMatrixSdk(): Promise { - return (await import("matrix-js-sdk")) as MatrixSdk; +let matrixSdkLoggingConfigured = false; +const matrixSdkBaseLogger = new ConsoleLogger(); + +function shouldSuppressMatrixHttpNotFound( + module: string, + messageOrObject: unknown[], +): boolean { + if (module !== "MatrixHttpClient") return false; + return messageOrObject.some((entry) => { + if (!entry || typeof entry !== "object") return false; + return (entry as { errcode?: string }).errcode === "M_NOT_FOUND"; + }); +} + +function ensureMatrixSdkLoggingConfigured(): void { + if (matrixSdkLoggingConfigured) return; + matrixSdkLoggingConfigured = true; + + LogService.setLogger({ + trace: (module, ...messageOrObject) => + matrixSdkBaseLogger.trace(module, ...messageOrObject), + debug: (module, ...messageOrObject) => + matrixSdkBaseLogger.debug(module, ...messageOrObject), + info: (module, ...messageOrObject) => + matrixSdkBaseLogger.info(module, ...messageOrObject), + warn: (module, ...messageOrObject) => + matrixSdkBaseLogger.warn(module, ...messageOrObject), + error: (module, ...messageOrObject) => { + if (shouldSuppressMatrixHttpNotFound(module, messageOrObject)) return; + matrixSdkBaseLogger.error(module, ...messageOrObject); + }, + }); } function clean(value?: string): string { return value?.trim() ?? ""; } +const DEFAULT_ACCOUNT_KEY = "default"; +const STORAGE_META_FILENAME = "storage-meta.json"; + +type MatrixStoragePaths = { + rootDir: string; + storagePath: string; + cryptoPath: string; + metaPath: string; + accountKey: string; + tokenHash: string; +}; + +function sanitizePathSegment(value: string): string { + const cleaned = value + .trim() + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, "_") + .replace(/^_+|_+$/g, ""); + return cleaned || "unknown"; +} + +function resolveHomeserverKey(homeserver: string): string { + try { + const url = new URL(homeserver); + if (url.host) return sanitizePathSegment(url.host); + } catch { + // fall through + } + return sanitizePathSegment(homeserver); +} + +function hashAccessToken(accessToken: string): string { + return crypto.createHash("sha256").update(accessToken).digest("hex").slice(0, 16); +} + +function resolveLegacyStoragePaths(env: NodeJS.ProcessEnv = process.env): { + storagePath: string; + cryptoPath: string; +} { + const stateDir = getMatrixRuntime().state.resolveStateDir(env, os.homedir); + return { + storagePath: path.join(stateDir, "matrix", "bot-storage.json"), + cryptoPath: path.join(stateDir, "matrix", "crypto"), + }; +} + +function resolveMatrixStoragePaths(params: { + homeserver: string; + userId: string; + accessToken: string; + accountId?: string | null; + env?: NodeJS.ProcessEnv; +}): MatrixStoragePaths { + const env = params.env ?? process.env; + const stateDir = getMatrixRuntime().state.resolveStateDir(env, os.homedir); + const accountKey = sanitizePathSegment(params.accountId ?? DEFAULT_ACCOUNT_KEY); + const userKey = sanitizePathSegment(params.userId); + const serverKey = resolveHomeserverKey(params.homeserver); + const tokenHash = hashAccessToken(params.accessToken); + const rootDir = path.join( + stateDir, + "matrix", + "accounts", + accountKey, + `${serverKey}__${userKey}`, + tokenHash, + ); + return { + rootDir, + storagePath: path.join(rootDir, "bot-storage.json"), + cryptoPath: path.join(rootDir, "crypto"), + metaPath: path.join(rootDir, STORAGE_META_FILENAME), + accountKey, + tokenHash, + }; +} + +function maybeMigrateLegacyStorage(params: { + storagePaths: MatrixStoragePaths; + env?: NodeJS.ProcessEnv; +}): void { + const legacy = resolveLegacyStoragePaths(params.env); + const hasLegacyStorage = fs.existsSync(legacy.storagePath); + const hasLegacyCrypto = fs.existsSync(legacy.cryptoPath); + const hasNewStorage = + fs.existsSync(params.storagePaths.storagePath) || + fs.existsSync(params.storagePaths.cryptoPath); + + if (!hasLegacyStorage && !hasLegacyCrypto) return; + if (hasNewStorage) return; + + fs.mkdirSync(params.storagePaths.rootDir, { recursive: true }); + if (hasLegacyStorage) { + try { + fs.renameSync(legacy.storagePath, params.storagePaths.storagePath); + } catch { + // Ignore migration failures; new store will be created. + } + } + if (hasLegacyCrypto) { + try { + fs.renameSync(legacy.cryptoPath, params.storagePaths.cryptoPath); + } catch { + // Ignore migration failures; new store will be created. + } + } +} + +function writeStorageMeta(params: { + storagePaths: MatrixStoragePaths; + homeserver: string; + userId: string; + accountId?: string | null; +}): void { + try { + const payload = { + homeserver: params.homeserver, + userId: params.userId, + accountId: params.accountId ?? DEFAULT_ACCOUNT_KEY, + accessTokenHash: params.storagePaths.tokenHash, + createdAt: new Date().toISOString(), + }; + fs.mkdirSync(params.storagePaths.rootDir, { recursive: true }); + fs.writeFileSync( + params.storagePaths.metaPath, + JSON.stringify(payload, null, 2), + "utf-8", + ); + } catch { + // ignore meta write failures + } +} + +function sanitizeUserIdList(input: unknown, label: string): string[] { + if (input == null) return []; + if (!Array.isArray(input)) { + LogService.warn( + "MatrixClientLite", + `Expected ${label} list to be an array, got ${typeof input}`, + ); + return []; + } + const filtered = input.filter( + (entry): entry is string => typeof entry === "string" && entry.trim().length > 0, + ); + if (filtered.length !== input.length) { + LogService.warn( + "MatrixClientLite", + `Dropping ${input.length - filtered.length} invalid ${label} entries from sync payload`, + ); + } + return filtered; +} + export function resolveMatrixConfig( cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig, env: NodeJS.ProcessEnv = process.env, @@ -61,6 +265,7 @@ export function resolveMatrixConfig( typeof matrix.initialSyncLimit === "number" ? Math.max(0, Math.floor(matrix.initialSyncLimit)) : undefined; + const encryption = matrix.encryption ?? false; return { homeserver, userId, @@ -68,6 +273,7 @@ export function resolveMatrixConfig( password, deviceName, initialSyncLimit, + encryption, }; } @@ -81,9 +287,6 @@ export async function resolveMatrixAuth(params?: { if (!resolved.homeserver) { throw new Error("Matrix homeserver is required (matrix.homeserver)"); } - if (!resolved.userId) { - throw new Error("Matrix userId is required (matrix.userId)"); - } const { loadMatrixCredentials, @@ -97,21 +300,36 @@ export async function resolveMatrixAuth(params?: { cached && credentialsMatchConfig(cached, { homeserver: resolved.homeserver, - userId: resolved.userId, + userId: resolved.userId || "", }) ? cached : null; + // If we have an access token, we can fetch userId via whoami if not provided if (resolved.accessToken) { - if (cachedCredentials && cachedCredentials.accessToken === resolved.accessToken) { + let userId = resolved.userId; + if (!userId) { + // Fetch userId from access token via whoami + ensureMatrixSdkLoggingConfigured(); + const tempClient = new MatrixClient(resolved.homeserver, resolved.accessToken); + const whoami = await tempClient.getUserId(); + userId = whoami; + // Save the credentials with the fetched userId + saveMatrixCredentials({ + homeserver: resolved.homeserver, + userId, + accessToken: resolved.accessToken, + }); + } else if (cachedCredentials && cachedCredentials.accessToken === resolved.accessToken) { touchMatrixCredentials(env); } return { homeserver: resolved.homeserver, - userId: resolved.userId, + userId, accessToken: resolved.accessToken, deviceName: resolved.deviceName, initialSyncLimit: resolved.initialSyncLimit, + encryption: resolved.encryption, }; } @@ -123,25 +341,45 @@ export async function resolveMatrixAuth(params?: { accessToken: cachedCredentials.accessToken, deviceName: resolved.deviceName, initialSyncLimit: resolved.initialSyncLimit, + encryption: resolved.encryption, }; } + if (!resolved.userId) { + throw new Error( + "Matrix userId is required when no access token is configured (matrix.userId)", + ); + } + if (!resolved.password) { throw new Error( - "Matrix access token or password is required (matrix.accessToken or matrix.password)", + "Matrix password is required when no access token is configured (matrix.password)", ); } - const sdk = await loadMatrixSdk(); - const loginClient = sdk.createClient({ - baseUrl: resolved.homeserver, - }); - const login = await loginClient.loginRequest({ - type: "m.login.password", - identifier: { type: "m.id.user", user: resolved.userId }, - password: resolved.password, - initial_device_display_name: resolved.deviceName ?? "Clawdbot Gateway", + // Login with password using HTTP API + const loginResponse = await fetch(`${resolved.homeserver}/_matrix/client/v3/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + type: "m.login.password", + identifier: { type: "m.id.user", user: resolved.userId }, + password: resolved.password, + initial_device_display_name: resolved.deviceName ?? "Clawdbot Gateway", + }), }); + + if (!loginResponse.ok) { + const errorText = await loginResponse.text(); + throw new Error(`Matrix login failed: ${errorText}`); + } + + const login = (await loginResponse.json()) as { + access_token?: string; + user_id?: string; + device_id?: string; + }; + const accessToken = login.access_token?.trim(); if (!accessToken) { throw new Error("Matrix login did not return an access token"); @@ -153,12 +391,14 @@ export async function resolveMatrixAuth(params?: { accessToken, deviceName: resolved.deviceName, initialSyncLimit: resolved.initialSyncLimit, + encryption: resolved.encryption, }; saveMatrixCredentials({ homeserver: auth.homeserver, userId: auth.userId, accessToken: auth.accessToken, + deviceId: login.device_id, }); return auth; @@ -168,40 +408,128 @@ export async function createMatrixClient(params: { homeserver: string; userId: string; accessToken: string; + encryption?: boolean; localTimeoutMs?: number; + accountId?: string | null; }): Promise { - const sdk = await loadMatrixSdk(); - const store = new sdk.MemoryStore(); - return sdk.createClient({ - baseUrl: params.homeserver, + ensureMatrixSdkLoggingConfigured(); + const env = process.env; + + // Create storage provider + const storagePaths = resolveMatrixStoragePaths({ + homeserver: params.homeserver, userId: params.userId, accessToken: params.accessToken, - localTimeoutMs: params.localTimeoutMs, - store, + accountId: params.accountId, + env, }); + maybeMigrateLegacyStorage({ storagePaths, env }); + fs.mkdirSync(storagePaths.rootDir, { recursive: true }); + const storage: IStorageProvider = new SimpleFsStorageProvider(storagePaths.storagePath); + + // Create crypto storage if encryption is enabled + let cryptoStorage: ICryptoStorageProvider | undefined; + if (params.encryption) { + fs.mkdirSync(storagePaths.cryptoPath, { recursive: true }); + + try { + const { StoreType } = await import("@matrix-org/matrix-sdk-crypto-nodejs"); + cryptoStorage = new RustSdkCryptoStorageProvider( + storagePaths.cryptoPath, + StoreType.Sqlite, + ); + } catch (err) { + LogService.warn("MatrixClientLite", "Failed to initialize crypto storage, E2EE disabled:", err); + } + } + + writeStorageMeta({ + storagePaths, + homeserver: params.homeserver, + userId: params.userId, + accountId: params.accountId, + }); + + const client = new MatrixClient( + params.homeserver, + params.accessToken, + storage, + cryptoStorage, + ); + + if (client.crypto) { + const originalUpdateSyncData = client.crypto.updateSyncData.bind(client.crypto); + client.crypto.updateSyncData = async ( + toDeviceMessages, + otkCounts, + unusedFallbackKeyAlgs, + changedDeviceLists, + leftDeviceLists, + ) => { + const safeChanged = sanitizeUserIdList(changedDeviceLists, "changed device list"); + const safeLeft = sanitizeUserIdList(leftDeviceLists, "left device list"); + try { + return await originalUpdateSyncData( + toDeviceMessages, + otkCounts, + unusedFallbackKeyAlgs, + safeChanged, + safeLeft, + ); + } catch (err) { + const message = typeof err === "string" ? err : err instanceof Error ? err.message : ""; + if (message.includes("Expect value to be String")) { + LogService.warn( + "MatrixClientLite", + "Ignoring malformed device list entries during crypto sync", + message, + ); + return; + } + throw err; + } + }; + } + + return client; } -function buildSharedClientKey(auth: MatrixAuth): string { - return [auth.homeserver, auth.userId, auth.accessToken].join("|"); +function buildSharedClientKey(auth: MatrixAuth, accountId?: string | null): string { + return [ + auth.homeserver, + auth.userId, + auth.accessToken, + auth.encryption ? "e2ee" : "plain", + accountId ?? DEFAULT_ACCOUNT_KEY, + ].join("|"); } async function createSharedMatrixClient(params: { auth: MatrixAuth; timeoutMs?: number; + accountId?: string | null; }): Promise { const client = await createMatrixClient({ homeserver: params.auth.homeserver, userId: params.auth.userId, accessToken: params.auth.accessToken, + encryption: params.auth.encryption, localTimeoutMs: params.timeoutMs, + accountId: params.accountId, }); - return { client, key: buildSharedClientKey(params.auth), started: false }; + return { + client, + key: buildSharedClientKey(params.auth, params.accountId), + started: false, + cryptoReady: false, + }; } async function ensureSharedClientStarted(params: { state: SharedMatrixClientState; timeoutMs?: number; initialSyncLimit?: number; + encryption?: boolean; }): Promise { if (params.state.started) return; if (sharedClientStartPromise) { @@ -209,18 +537,22 @@ async function ensureSharedClientStarted(params: { return; } sharedClientStartPromise = (async () => { - const startOpts: Parameters[0] = { - lazyLoadMembers: true, - threadSupport: true, - }; - if (typeof params.initialSyncLimit === "number") { - startOpts.initialSyncLimit = params.initialSyncLimit; + const client = params.state.client; + + // Initialize crypto if enabled + if (params.encryption && !params.state.cryptoReady) { + try { + const joinedRooms = await client.getJoinedRooms(); + if (client.crypto) { + await client.crypto.prepare(joinedRooms); + params.state.cryptoReady = true; + } + } catch (err) { + LogService.warn("MatrixClientLite", "Failed to prepare crypto:", err); + } } - await params.state.client.startClient(startOpts); - await waitForMatrixSync({ - client: params.state.client, - timeoutMs: params.timeoutMs, - }); + + await client.start(); params.state.started = true; })(); try { @@ -237,10 +569,11 @@ export async function resolveSharedMatrixClient( timeoutMs?: number; auth?: MatrixAuth; startClient?: boolean; + accountId?: string | null; } = {}, ): Promise { const auth = params.auth ?? (await resolveMatrixAuth({ cfg: params.cfg, env: params.env })); - const key = buildSharedClientKey(auth); + const key = buildSharedClientKey(auth, params.accountId); const shouldStart = params.startClient !== false; if (sharedClientState?.key === key) { @@ -249,6 +582,7 @@ export async function resolveSharedMatrixClient( state: sharedClientState, timeoutMs: params.timeoutMs, initialSyncLimit: auth.initialSyncLimit, + encryption: auth.encryption, }); } return sharedClientState.client; @@ -262,11 +596,12 @@ export async function resolveSharedMatrixClient( state: pending, timeoutMs: params.timeoutMs, initialSyncLimit: auth.initialSyncLimit, + encryption: auth.encryption, }); } return pending.client; } - pending.client.stopClient(); + pending.client.stop(); sharedClientState = null; sharedClientPromise = null; } @@ -274,6 +609,7 @@ export async function resolveSharedMatrixClient( sharedClientPromise = createSharedMatrixClient({ auth, timeoutMs: params.timeoutMs, + accountId: params.accountId, }); try { const created = await sharedClientPromise; @@ -283,6 +619,7 @@ export async function resolveSharedMatrixClient( state: created, timeoutMs: params.timeoutMs, initialSyncLimit: auth.initialSyncLimit, + encryption: auth.encryption, }); } return created.client; @@ -291,48 +628,18 @@ export async function resolveSharedMatrixClient( } } -export async function waitForMatrixSync(params: { +export async function waitForMatrixSync(_params: { client: MatrixClient; timeoutMs?: number; abortSignal?: AbortSignal; }): Promise { - const timeoutMs = Math.max(1000, params.timeoutMs ?? 15_000); - if (params.client.getSyncState() === SyncState.Syncing) return; - await new Promise((resolve, reject) => { - let done = false; - let timer: NodeJS.Timeout | undefined; - const cleanup = () => { - if (done) return; - done = true; - params.client.removeListener(ClientEvent.Sync, onSync); - if (params.abortSignal) { - params.abortSignal.removeEventListener("abort", onAbort); - } - if (timer) { - clearTimeout(timer); - timer = undefined; - } - }; - const onSync = (state: SyncState) => { - if (done) return; - if (state === SyncState.Prepared || state === SyncState.Syncing) { - cleanup(); - resolve(); - } - if (state === SyncState.Error) { - cleanup(); - reject(new Error("Matrix sync failed")); - } - }; - const onAbort = () => { - cleanup(); - reject(new Error("Matrix sync aborted")); - }; - params.client.on(ClientEvent.Sync, onSync); - params.abortSignal?.addEventListener("abort", onAbort, { once: true }); - timer = setTimeout(() => { - cleanup(); - reject(new Error("Matrix sync timed out")); - }, timeoutMs); - }); + // matrix-bot-sdk handles sync internally in start() + // This is kept for API compatibility but is essentially a no-op now +} + +export function stopSharedClient(): void { + if (sharedClientState) { + sharedClientState.client.stop(); + sharedClientState = null; + } } diff --git a/extensions/matrix/src/matrix/credentials.ts b/extensions/matrix/src/matrix/credentials.ts index 4784a6f9f..45388462d 100644 --- a/extensions/matrix/src/matrix/credentials.ts +++ b/extensions/matrix/src/matrix/credentials.ts @@ -8,6 +8,7 @@ export type MatrixStoredCredentials = { homeserver: string; userId: string; accessToken: string; + deviceId?: string; createdAt: string; lastUsedAt?: string; }; @@ -94,5 +95,9 @@ export function credentialsMatchConfig( stored: MatrixStoredCredentials, config: { homeserver: string; userId: string }, ): boolean { + // If userId is empty (token-based auth), only match homeserver + if (!config.userId) { + return stored.homeserver === config.homeserver; + } return stored.homeserver === config.homeserver && stored.userId === config.userId; } diff --git a/extensions/matrix/src/matrix/deps.ts b/extensions/matrix/src/matrix/deps.ts index fdcf66fe4..df2f58706 100644 --- a/extensions/matrix/src/matrix/deps.ts +++ b/extensions/matrix/src/matrix/deps.ts @@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url"; import type { RuntimeEnv } from "clawdbot/plugin-sdk"; import { getMatrixRuntime } from "../runtime.js"; -const MATRIX_SDK_PACKAGE = "matrix-js-sdk"; +const MATRIX_SDK_PACKAGE = "matrix-bot-sdk"; export function isMatrixSdkAvailable(): boolean { try { @@ -30,9 +30,9 @@ export async function ensureMatrixSdkInstalled(params: { if (isMatrixSdkAvailable()) return; const confirm = params.confirm; if (confirm) { - const ok = await confirm("Matrix requires matrix-js-sdk. Install now?"); + const ok = await confirm("Matrix requires matrix-bot-sdk. Install now?"); if (!ok) { - throw new Error("Matrix requires matrix-js-sdk (install dependencies first)."); + throw new Error("Matrix requires matrix-bot-sdk (install dependencies first)."); } } @@ -52,6 +52,6 @@ export async function ensureMatrixSdkInstalled(params: { ); } if (!isMatrixSdkAvailable()) { - throw new Error("Matrix dependency install completed but matrix-js-sdk is still missing."); + throw new Error("Matrix dependency install completed but matrix-bot-sdk is still missing."); } } diff --git a/extensions/matrix/src/matrix/index.ts b/extensions/matrix/src/matrix/index.ts index 8729ebc6e..7cd75d8a1 100644 --- a/extensions/matrix/src/matrix/index.ts +++ b/extensions/matrix/src/matrix/index.ts @@ -3,6 +3,7 @@ export { probeMatrix } from "./probe.js"; export { reactMatrixMessage, resolveMatrixRoomId, + sendReadReceiptMatrix, sendMessageMatrix, sendPollMatrix, sendTypingMatrix, diff --git a/extensions/matrix/src/matrix/monitor/auto-join.ts b/extensions/matrix/src/matrix/monitor/auto-join.ts index da3d43ff8..90b05202a 100644 --- a/extensions/matrix/src/matrix/monitor/auto-join.ts +++ b/extensions/matrix/src/matrix/monitor/auto-join.ts @@ -1,5 +1,5 @@ -import type { MatrixClient, MatrixEvent, RoomMember } from "matrix-js-sdk"; -import { RoomMemberEvent } from "matrix-js-sdk"; +import type { MatrixClient } from "matrix-bot-sdk"; +import { AutojoinRoomsMixin } from "matrix-bot-sdk"; import type { RuntimeEnv } from "clawdbot/plugin-sdk"; import type { CoreConfig } from "../../types.js"; @@ -19,25 +19,45 @@ export function registerMatrixAutoJoin(params: { const autoJoin = cfg.channels?.matrix?.autoJoin ?? "always"; const autoJoinAllowlist = cfg.channels?.matrix?.autoJoinAllowlist ?? []; - client.on(RoomMemberEvent.Membership, async (_event: MatrixEvent, member: RoomMember) => { - if (member.userId !== client.getUserId()) return; - if (member.membership !== "invite") return; - const roomId = member.roomId; - if (autoJoin === "off") return; - if (autoJoin === "allowlist") { - const invitedRoom = client.getRoom(roomId); - const alias = invitedRoom?.getCanonicalAlias?.() ?? ""; - const altAliases = invitedRoom?.getAltAliases?.() ?? []; - const allowed = - autoJoinAllowlist.includes("*") || - autoJoinAllowlist.includes(roomId) || - (alias ? autoJoinAllowlist.includes(alias) : false) || - altAliases.some((value) => autoJoinAllowlist.includes(value)); - if (!allowed) { - logVerbose(`matrix: invite ignored (not in allowlist) room=${roomId}`); - return; - } + if (autoJoin === "off") { + return; + } + + if (autoJoin === "always") { + // Use the built-in autojoin mixin for "always" mode + AutojoinRoomsMixin.setupOnClient(client); + logVerbose("matrix: auto-join enabled for all invites"); + return; + } + + // For "allowlist" mode, handle invites manually + client.on("room.invite", async (roomId: string, _inviteEvent: unknown) => { + if (autoJoin !== "allowlist") return; + + // Get room alias if available + let alias: string | undefined; + let altAliases: string[] = []; + try { + const aliasState = await client + .getRoomStateEvent(roomId, "m.room.canonical_alias", "") + .catch(() => null); + alias = aliasState?.alias; + altAliases = Array.isArray(aliasState?.alt_aliases) ? aliasState.alt_aliases : []; + } catch { + // Ignore errors } + + const allowed = + autoJoinAllowlist.includes("*") || + autoJoinAllowlist.includes(roomId) || + (alias ? autoJoinAllowlist.includes(alias) : false) || + altAliases.some((value) => autoJoinAllowlist.includes(value)); + + if (!allowed) { + logVerbose(`matrix: invite ignored (not in allowlist) room=${roomId}`); + return; + } + try { await client.joinRoom(roomId); logVerbose(`matrix: joined room ${roomId}`); diff --git a/extensions/matrix/src/matrix/monitor/direct.ts b/extensions/matrix/src/matrix/monitor/direct.ts index 9f64384f8..fff8383ca 100644 --- a/extensions/matrix/src/matrix/monitor/direct.ts +++ b/extensions/matrix/src/matrix/monitor/direct.ts @@ -1,80 +1,105 @@ -import type { - AccountDataEvents, - MatrixClient, - MatrixEvent, - Room, - RoomMember, -} from "matrix-js-sdk"; -import { ClientEvent, EventType } from "matrix-js-sdk"; +import type { MatrixClient } from "matrix-bot-sdk"; -function hasDirectFlag(member?: RoomMember | null): boolean { - if (!member?.events.member) return false; - const content = member.events.member.getContent() as { is_direct?: boolean } | undefined; - if (content?.is_direct === true) return true; - const prev = member.events.member.getPrevContent() as { is_direct?: boolean } | undefined; - return prev?.is_direct === true; -} +type DirectMessageCheck = { + roomId: string; + senderId?: string; + selfUserId?: string; +}; -export function isLikelyDirectRoom(params: { - room: Room; - senderId: string; - selfId?: string | null; -}): boolean { - if (!params.selfId) return false; - const memberCount = params.room.getJoinedMemberCount?.(); - if (typeof memberCount !== "number" || memberCount !== 2) return false; - return true; -} +type DirectRoomTrackerOptions = { + log?: (message: string) => void; +}; -export function isDirectRoomByFlag(params: { - room: Room; - senderId: string; - selfId?: string | null; -}): boolean { - if (!params.selfId) return false; - const selfMember = params.room.getMember(params.selfId); - const senderMember = params.room.getMember(params.senderId); - if (hasDirectFlag(selfMember) || hasDirectFlag(senderMember)) return true; - const inviter = selfMember?.getDMInviter() ?? senderMember?.getDMInviter(); - return Boolean(inviter); -} +const DM_CACHE_TTL_MS = 30_000; -type MatrixDirectAccountData = AccountDataEvents[EventType.Direct]; +export function createDirectRoomTracker( + client: MatrixClient, + opts: DirectRoomTrackerOptions = {}, +) { + const log = opts.log ?? (() => {}); + let lastDmUpdateMs = 0; + let cachedSelfUserId: string | null = null; + const memberCountCache = new Map(); -export function createDirectRoomTracker(client: MatrixClient) { - const directMap = new Map>(); + const ensureSelfUserId = async (): Promise => { + if (cachedSelfUserId) return cachedSelfUserId; + try { + cachedSelfUserId = await client.getUserId(); + } catch { + cachedSelfUserId = null; + } + return cachedSelfUserId; + }; - const updateDirectMap = (content: MatrixDirectAccountData) => { - directMap.clear(); - for (const [userId, rooms] of Object.entries(content)) { - if (!Array.isArray(rooms)) continue; - const ids = rooms.map((roomId) => String(roomId).trim()).filter(Boolean); - if (ids.length === 0) continue; - directMap.set(userId, new Set(ids)); + const refreshDmCache = async (): Promise => { + const now = Date.now(); + if (now - lastDmUpdateMs < DM_CACHE_TTL_MS) return; + lastDmUpdateMs = now; + try { + await client.dms.update(); + } catch (err) { + log(`matrix: dm cache refresh failed (${String(err)})`); } }; - const initialDirect = client.getAccountData(EventType.Direct); - if (initialDirect) { - updateDirectMap(initialDirect.getContent() ?? {}); - } + const resolveMemberCount = async (roomId: string): Promise => { + const cached = memberCountCache.get(roomId); + const now = Date.now(); + if (cached && now - cached.ts < DM_CACHE_TTL_MS) { + return cached.count; + } + try { + const members = await client.getJoinedRoomMembers(roomId); + const count = members.length; + memberCountCache.set(roomId, { count, ts: now }); + return count; + } catch (err) { + log(`matrix: dm member count failed room=${roomId} (${String(err)})`); + return null; + } + }; - client.on(ClientEvent.AccountData, (event: MatrixEvent) => { - if (event.getType() !== EventType.Direct) return; - updateDirectMap(event.getContent() ?? {}); - }); + const hasDirectFlag = async (roomId: string, userId?: string): Promise => { + const target = userId?.trim(); + if (!target) return false; + try { + const state = await client.getRoomStateEvent(roomId, "m.room.member", target); + return state?.is_direct === true; + } catch { + return false; + } + }; return { - isDirectMessage: (room: Room, senderId: string) => { - const roomId = room.roomId; - const directRooms = directMap.get(senderId); - const selfId = client.getUserId(); - const isDirectByFlag = isDirectRoomByFlag({ room, senderId, selfId }); - return ( - Boolean(directRooms?.has(roomId)) || - isDirectByFlag || - isLikelyDirectRoom({ room, senderId, selfId }) + isDirectMessage: async (params: DirectMessageCheck): Promise => { + const { roomId, senderId } = params; + await refreshDmCache(); + + if (client.dms.isDm(roomId)) { + log(`matrix: dm detected via m.direct room=${roomId}`); + return true; + } + + const memberCount = await resolveMemberCount(roomId); + if (memberCount === 2) { + log(`matrix: dm detected via member count room=${roomId} members=${memberCount}`); + return true; + } + + const selfUserId = params.selfUserId ?? (await ensureSelfUserId()); + const directViaState = + (await hasDirectFlag(roomId, senderId)) || (await hasDirectFlag(roomId, selfUserId ?? "")); + if (directViaState) { + log(`matrix: dm detected via member state room=${roomId}`); + return true; + } + + log( + `matrix: dm check room=${roomId} result=group members=${ + memberCount ?? "unknown" + }`, ); + return false; }, }; } diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index 4b8ee51fd..5c4387c02 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -1,11 +1,18 @@ -import type { MatrixEvent, Room } from "matrix-js-sdk"; -import { EventType, RelationType, RoomEvent } from "matrix-js-sdk"; -import type { RoomMessageEventContent } from "matrix-js-sdk/lib/@types/events.js"; +import type { + EncryptedFile, + LocationMessageEventContent, + MatrixClient, + MessageEventContent, +} from "matrix-bot-sdk"; +import { format } from "node:util"; import { formatAllowlistMatchMeta, + formatLocationText, mergeAllowlist, summarizeMapping, + toLocationContext, + type NormalizedLocation, type ReplyPayload, type RuntimeEnv, } from "clawdbot/plugin-sdk"; @@ -15,6 +22,7 @@ import { isBunRuntime, resolveMatrixAuth, resolveSharedMatrixClient, + stopSharedClient, } from "../client.js"; import { formatPollAsText, @@ -22,7 +30,12 @@ import { type PollStartContent, parsePollStartContent, } from "../poll-types.js"; -import { reactMatrixMessage, sendMessageMatrix, sendTypingMatrix } from "../send.js"; +import { + reactMatrixMessage, + sendMessageMatrix, + sendReadReceiptMatrix, + sendTypingMatrix, +} from "../send.js"; import { resolveMatrixAllowListMatch, resolveMatrixAllowListMatches, @@ -38,12 +51,126 @@ import { resolveMatrixThreadRootId, resolveMatrixThreadTarget } from "./threads. import { resolveMatrixTargets } from "../../resolve-targets.js"; import { getMatrixRuntime } from "../../runtime.js"; +// Constants that were previously from matrix-js-sdk +const EventType = { + RoomMessage: "m.room.message", + RoomMessageEncrypted: "m.room.encrypted", + RoomMember: "m.room.member", + Location: "m.location", +} as const; + +const RelationType = { + Replace: "m.replace", +} as const; + +// Type for raw Matrix events from matrix-bot-sdk +type MatrixRawEvent = { + event_id: string; + sender: string; + type: string; + origin_server_ts: number; + content: Record; + unsigned?: { + age?: number; + redacted_because?: unknown; + }; +}; + +type RoomMessageEventContent = MessageEventContent & { + url?: string; + file?: EncryptedFile; + info?: { + mimetype?: string; + }; + "m.relates_to"?: { + rel_type?: string; + event_id?: string; + "m.in_reply_to"?: { event_id?: string }; + }; +}; + +type MatrixLocationPayload = { + text: string; + context: ReturnType; +}; + +type GeoUriParams = { + latitude: number; + longitude: number; + accuracy?: number; +}; + +function parseGeoUri(value: string): GeoUriParams | null { + const trimmed = value.trim(); + if (!trimmed) return null; + if (!trimmed.toLowerCase().startsWith("geo:")) return null; + const payload = trimmed.slice(4); + const [coordsPart, ...paramParts] = payload.split(";"); + const coords = coordsPart.split(","); + if (coords.length < 2) return null; + const latitude = Number.parseFloat(coords[0] ?? ""); + const longitude = Number.parseFloat(coords[1] ?? ""); + if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null; + + const params = new Map(); + for (const part of paramParts) { + const segment = part.trim(); + if (!segment) continue; + const eqIndex = segment.indexOf("="); + const rawKey = eqIndex === -1 ? segment : segment.slice(0, eqIndex); + const rawValue = eqIndex === -1 ? "" : segment.slice(eqIndex + 1); + const key = rawKey.trim().toLowerCase(); + if (!key) continue; + const valuePart = rawValue.trim(); + params.set(key, valuePart ? decodeURIComponent(valuePart) : ""); + } + + const accuracyRaw = params.get("u"); + const accuracy = accuracyRaw ? Number.parseFloat(accuracyRaw) : undefined; + + return { + latitude, + longitude, + accuracy: Number.isFinite(accuracy) ? accuracy : undefined, + }; +} + +function resolveMatrixLocation(params: { + eventType: string; + content: LocationMessageEventContent; +}): MatrixLocationPayload | null { + const { eventType, content } = params; + const isLocation = + eventType === EventType.Location || + (eventType === EventType.RoomMessage && content.msgtype === EventType.Location); + if (!isLocation) return null; + const geoUri = typeof content.geo_uri === "string" ? content.geo_uri.trim() : ""; + if (!geoUri) return null; + const parsed = parseGeoUri(geoUri); + if (!parsed) return null; + const caption = typeof content.body === "string" ? content.body.trim() : ""; + const location: NormalizedLocation = { + latitude: parsed.latitude, + longitude: parsed.longitude, + accuracy: parsed.accuracy, + caption: caption || undefined, + source: "pin", + isLive: false, + }; + + return { + text: formatLocationText(location), + context: toLocationContext(location), + }; +} + export type MonitorMatrixOpts = { runtime?: RuntimeEnv; abortSignal?: AbortSignal; mediaMaxMb?: number; initialSyncLimit?: number; replyToMode?: ReplyToMode; + accountId?: string | null; }; const DEFAULT_MEDIA_MAX_MB = 20; @@ -56,13 +183,23 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi let cfg = core.config.loadConfig() as CoreConfig; if (cfg.channels?.matrix?.enabled === false) return; + const logger = core.logging.getChildLogger({ module: "matrix-auto-reply" }); + const formatRuntimeMessage = (...args: Parameters) => format(...args); const runtime: RuntimeEnv = opts.runtime ?? { - log: console.log, - error: console.error, + log: (...args) => { + logger.info(formatRuntimeMessage(...args)); + }, + error: (...args) => { + logger.error(formatRuntimeMessage(...args)); + }, exit: (code: number): never => { throw new Error(`exit ${code}`); }, }; + const logVerboseMessage = (message: string) => { + if (!core.logging.shouldLogVerbose()) return; + logger.debug(message); + }; const normalizeUserEntry = (raw: string) => raw.replace(/^matrix:/i, "").replace(/^user:/i, "").trim(); @@ -70,8 +207,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi raw.replace(/^matrix:/i, "").replace(/^(room|channel):/i, "").trim(); const isMatrixUserId = (value: string) => value.startsWith("@") && value.includes(":"); + const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true; let allowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? []; - let roomsConfig = cfg.channels?.matrix?.rooms; + let roomsConfig = cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms; if (allowFrom.length > 0) { const entries = allowFrom @@ -163,7 +301,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi ...cfg.channels?.matrix?.dm, allowFrom, }, - rooms: roomsConfig, + ...(roomsConfig ? { groups: roomsConfig } : {}), }, }, }; @@ -181,17 +319,11 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi cfg, auth: authWithLimit, startClient: false, + accountId: opts.accountId, }); setActiveMatrixClient(client); const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg); - const logger = core.logging.getChildLogger({ module: "matrix-auto-reply" }); - const logVerboseMessage = (message: string) => { - if (core.logging.shouldLogVerbose()) { - logger.debug(message); - } - }; - const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const groupPolicyRaw = cfg.channels?.matrix?.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; const groupPolicy = allowlistOnly && groupPolicyRaw === "open" ? "allowlist" : groupPolicyRaw; @@ -206,30 +338,75 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi const mediaMaxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024; const startupMs = Date.now(); const startupGraceMs = 0; - const directTracker = createDirectRoomTracker(client); + const directTracker = createDirectRoomTracker(client, { log: logVerboseMessage }); registerMatrixAutoJoin({ client, cfg, runtime }); + const warnedEncryptedRooms = new Set(); + const warnedCryptoMissingRooms = new Set(); - const handleTimeline = async ( - event: MatrixEvent, - room: Room | undefined, - toStartOfTimeline?: boolean, + const roomInfoCache = new Map< + string, + { name?: string; canonicalAlias?: string; altAliases: string[] } + >(); + + // Helper to get room info + const getRoomInfo = async (roomId: string) => { + const cached = roomInfoCache.get(roomId); + if (cached) return cached; + let name: string | undefined; + let canonicalAlias: string | undefined; + let altAliases: string[] = []; + try { + const nameState = await client.getRoomStateEvent(roomId, "m.room.name", "").catch(() => null); + name = nameState?.name; + } catch { /* ignore */ } + try { + const aliasState = await client.getRoomStateEvent(roomId, "m.room.canonical_alias", "").catch(() => null); + canonicalAlias = aliasState?.alias; + altAliases = aliasState?.alt_aliases ?? []; + } catch { /* ignore */ } + const info = { name, canonicalAlias, altAliases }; + roomInfoCache.set(roomId, info); + return info; + }; + + // Helper to get member display name + const getMemberDisplayName = async (roomId: string, userId: string): Promise => { + try { + const memberState = await client.getRoomStateEvent(roomId, "m.room.member", userId).catch(() => null); + return memberState?.displayname ?? userId; + } catch { + return userId; + } + }; + + const handleRoomMessage = async ( + roomId: string, + event: MatrixRawEvent, ) => { try { - if (!room) return; - if (toStartOfTimeline) return; - if (event.getType() === EventType.RoomMessageEncrypted || event.isDecryptionFailure()) { + const eventType = event.type; + if (eventType === EventType.RoomMessageEncrypted) { + // Encrypted messages are decrypted automatically by matrix-bot-sdk with crypto enabled return; } - const eventType = event.getType(); const isPollEvent = isPollStartType(eventType); - if (eventType !== EventType.RoomMessage && !isPollEvent) return; - if (event.isRedacted()) return; - const senderId = event.getSender(); + const locationContent = event.content as LocationMessageEventContent; + const isLocationEvent = + eventType === EventType.Location || + (eventType === EventType.RoomMessage && + locationContent.msgtype === EventType.Location); + if (eventType !== EventType.RoomMessage && !isPollEvent && !isLocationEvent) return; + logVerboseMessage( + `matrix: room.message recv room=${roomId} type=${eventType} id=${event.event_id ?? "unknown"}`, + ); + if (event.unsigned?.redacted_because) return; + const senderId = event.sender; if (!senderId) return; - if (senderId === client.getUserId()) return; - const eventTs = event.getTs(); - const eventAge = event.getAge(); + const selfUserId = await client.getUserId(); + if (senderId === selfUserId) return; + const eventTs = event.origin_server_ts; + const eventAge = event.unsigned?.age; if (typeof eventTs === "number" && eventTs < startupMs - startupGraceMs) { return; } @@ -241,15 +418,23 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi return; } - let content = event.getContent(); + const roomInfo = await getRoomInfo(roomId); + const roomName = roomInfo.name; + const roomAliases = [ + roomInfo.canonicalAlias ?? "", + ...roomInfo.altAliases, + ].filter(Boolean); + + let content = event.content as RoomMessageEventContent; if (isPollEvent) { - const pollStartContent = event.getContent(); + const pollStartContent = event.content as PollStartContent; const pollSummary = parsePollStartContent(pollStartContent); if (pollSummary) { - pollSummary.eventId = event.getId() ?? ""; - pollSummary.roomId = room.roomId; + pollSummary.eventId = event.event_id ?? ""; + pollSummary.roomId = roomId; pollSummary.sender = senderId; - pollSummary.senderName = room.getMember(senderId)?.name ?? senderId; + const senderDisplayName = await getMemberDisplayName(roomId, senderId); + pollSummary.senderName = senderDisplayName; const pollText = formatPollAsText(pollSummary); content = { msgtype: "m.text", @@ -260,50 +445,64 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi } } + const locationPayload = resolveMatrixLocation({ + eventType, + content: content as LocationMessageEventContent, + }); + const relates = content["m.relates_to"]; if (relates && "rel_type" in relates) { if (relates.rel_type === RelationType.Replace) return; } - const roomId = room.roomId; - const isDirectMessage = directTracker.isDirectMessage(room, senderId); + const isDirectMessage = await directTracker.isDirectMessage({ + roomId, + senderId, + selfUserId, + }); const isRoom = !isDirectMessage; - if (!isDirectMessage && groupPolicy === "disabled") return; + if (isRoom && groupPolicy === "disabled") return; - const roomAliases = [ - room.getCanonicalAlias?.() ?? "", - ...(room.getAltAliases?.() ?? []), - ].filter(Boolean); - const roomName = room.name ?? undefined; - const roomConfigInfo = resolveMatrixRoomConfig({ - rooms: cfg.channels?.matrix?.rooms, - roomId, - aliases: roomAliases, - name: roomName, - }); - const roomMatchMeta = `matchKey=${roomConfigInfo.matchKey ?? "none"} matchSource=${ - roomConfigInfo.matchSource ?? "none" - }`; + const roomConfigInfo = isRoom + ? resolveMatrixRoomConfig({ + rooms: roomsConfig, + roomId, + aliases: roomAliases, + name: roomName, + }) + : undefined; + const roomConfig = roomConfigInfo?.config; + const roomMatchMeta = roomConfigInfo + ? `matchKey=${roomConfigInfo.matchKey ?? "none"} matchSource=${ + roomConfigInfo.matchSource ?? "none" + }` + : "matchKey=none matchSource=none"; - if (roomConfigInfo.config && !roomConfigInfo.allowed) { + if (isRoom && roomConfig && !roomConfigInfo?.allowed) { logVerboseMessage(`matrix: room disabled room=${roomId} (${roomMatchMeta})`); return; } - if (groupPolicy === "allowlist") { - if (!roomConfigInfo.allowlistConfigured) { + if (isRoom && groupPolicy === "allowlist") { + if (!roomConfigInfo?.allowlistConfigured) { logVerboseMessage(`matrix: drop room message (no allowlist, ${roomMatchMeta})`); return; } - if (!roomConfigInfo.config) { + if (!roomConfig) { logVerboseMessage(`matrix: drop room message (not in allowlist, ${roomMatchMeta})`); return; } } - const senderName = room.getMember(senderId)?.name ?? senderId; + const senderName = await getMemberDisplayName(roomId, senderId); const storeAllowFrom = await core.channel.pairing.readAllowFromStore("matrix").catch(() => []); const effectiveAllowFrom = normalizeAllowListLower([...allowFrom, ...storeAllowFrom]); + const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? []; + const effectiveGroupAllowFrom = normalizeAllowListLower([ + ...groupAllowFrom, + ...storeAllowFrom, + ]); + const groupAllowConfigured = effectiveGroupAllowFrom.length > 0; if (isDirectMessage) { if (!dmEnabled || dmPolicy === "disabled") return; @@ -353,9 +552,10 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi } } - if (isRoom && roomConfigInfo.config?.users?.length) { + const roomUsers = roomConfig?.users ?? []; + if (isRoom && roomUsers.length > 0) { const userMatch = resolveMatrixAllowListMatch({ - allowList: normalizeAllowListLower(roomConfigInfo.config.users), + allowList: normalizeAllowListLower(roomUsers), userId: senderId, userName: senderName, }); @@ -368,11 +568,27 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi return; } } + if (isRoom && groupPolicy === "allowlist" && roomUsers.length === 0 && groupAllowConfigured) { + const groupAllowMatch = resolveMatrixAllowListMatch({ + allowList: effectiveGroupAllowFrom, + userId: senderId, + userName: senderName, + }); + if (!groupAllowMatch.allowed) { + logVerboseMessage( + `matrix: blocked sender ${senderId} (groupAllowFrom, ${roomMatchMeta}, ${formatAllowlistMatchMeta( + groupAllowMatch, + )})`, + ); + return; + } + } if (isRoom) { logVerboseMessage(`matrix: allow room ${roomId} (${roomMatchMeta})`); } - const rawBody = content.body.trim(); + const rawBody = locationPayload?.text + ?? (typeof content.body === "string" ? content.body.trim() : ""); let media: { path: string; contentType?: string; @@ -380,7 +596,12 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi } | null = null; const contentUrl = "url" in content && typeof content.url === "string" ? content.url : undefined; - if (!rawBody && !contentUrl) { + const contentFile = + "file" in content && content.file && typeof content.file === "object" + ? (content.file as EncryptedFile) + : undefined; + const mediaUrl = contentUrl ?? contentFile?.url; + if (!rawBody && !mediaUrl) { return; } @@ -388,13 +609,14 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi "info" in content && content.info && "mimetype" in content.info ? (content.info as { mimetype?: string }).mimetype : undefined; - if (contentUrl?.startsWith("mxc://")) { + if (mediaUrl?.startsWith("mxc://")) { try { media = await downloadMatrixMedia({ client, - mxcUrl: contentUrl, + mxcUrl: mediaUrl, contentType, maxBytes: mediaMaxBytes, + file: contentFile, }); } catch (err) { logVerboseMessage(`matrix: media download failed: ${String(err)}`); @@ -406,7 +628,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi const { wasMentioned, hasExplicitMention } = resolveMentions({ content, - userId: client.getUserId(), + userId: selfUserId, text: bodyText, mentionRegexes, }); @@ -420,10 +642,27 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi userId: senderId, userName: senderName, }); + const senderAllowedForGroup = groupAllowConfigured + ? resolveMatrixAllowListMatches({ + allowList: effectiveGroupAllowFrom, + userId: senderId, + userName: senderName, + }) + : false; + const senderAllowedForRoomUsers = + isRoom && roomUsers.length > 0 + ? resolveMatrixAllowListMatches({ + allowList: normalizeAllowListLower(roomUsers), + userId: senderId, + userName: senderName, + }) + : false; const commandAuthorized = core.channel.commands.resolveCommandAuthorizedFromAuthorizers({ useAccessGroups, authorizers: [ { configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands }, + { configured: roomUsers.length > 0, allowed: senderAllowedForRoomUsers }, + { configured: groupAllowConfigured, allowed: senderAllowedForGroup }, ], }); if ( @@ -436,12 +675,12 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi return; } const shouldRequireMention = isRoom - ? roomConfigInfo.config?.autoReply === true + ? roomConfig?.autoReply === true ? false - : roomConfigInfo.config?.autoReply === false + : roomConfig?.autoReply === false ? true - : typeof roomConfigInfo.config?.requireMention === "boolean" - ? roomConfigInfo.config.requireMention + : typeof roomConfig?.requireMention === "boolean" + ? roomConfig?.requireMention : true : false; const shouldBypassMention = @@ -457,13 +696,14 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi return; } - const messageId = event.getId() ?? ""; + const messageId = event.event_id ?? ""; + const replyToEventId = content["m.relates_to"]?.["m.in_reply_to"]?.event_id; const threadRootId = resolveMatrixThreadRootId({ event, content }); const threadTarget = resolveMatrixThreadTarget({ threadReplies, messageId, threadRootId, - isThreadRoot: event.isThreadRoot, + isThreadRoot: false, // matrix-bot-sdk doesn't have this info readily available }); const route = core.channel.routing.resolveAgentRoute({ @@ -484,16 +724,16 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi storePath, sessionKey: route.sessionKey, }); - const body = core.channel.reply.formatAgentEnvelope({ - channel: "Matrix", - from: envelopeFrom, - timestamp: event.getTs() ?? undefined, - previousTimestamp, - envelope: envelopeOptions, - body: textWithId, - }); + const body = core.channel.reply.formatAgentEnvelope({ + channel: "Matrix", + from: envelopeFrom, + timestamp: eventTs ?? undefined, + previousTimestamp, + envelope: envelopeOptions, + body: textWithId, + }); - const groupSystemPrompt = roomConfigInfo.config?.systemPrompt?.trim() || undefined; + const groupSystemPrompt = roomConfig?.systemPrompt?.trim() || undefined; const ctxPayload = core.channel.reply.finalizeInboundContext({ Body: body, RawBody: bodyText, @@ -508,18 +748,19 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi SenderId: senderId, SenderUsername: senderId.split(":")[0]?.replace(/^@/, ""), GroupSubject: isRoom ? (roomName ?? roomId) : undefined, - GroupChannel: isRoom ? (room.getCanonicalAlias?.() ?? roomId) : undefined, + GroupChannel: isRoom ? (roomInfo.canonicalAlias ?? roomId) : undefined, GroupSystemPrompt: isRoom ? groupSystemPrompt : undefined, Provider: "matrix" as const, Surface: "matrix" as const, WasMentioned: isRoom ? wasMentioned : undefined, MessageSid: messageId, - ReplyToId: threadTarget ? undefined : (event.replyEventId ?? undefined), + ReplyToId: threadTarget ? undefined : (replyToEventId ?? undefined), MessageThreadId: threadTarget, - Timestamp: event.getTs() ?? undefined, + Timestamp: eventTs ?? undefined, MediaPath: media?.path, MediaType: media?.contentType, MediaUrl: media?.path, + ...(locationPayload?.context ?? {}), CommandAuthorized: commandAuthorized, CommandSource: "text" as const, OriginatingChannel: "matrix" as const, @@ -577,6 +818,14 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi return; } + if (messageId) { + sendReadReceiptMatrix(roomId, messageId, client).catch((err) => { + logVerboseMessage( + `matrix: read receipt failed room=${roomId} id=${messageId}: ${String(err)}`, + ); + }); + } + let didSendReply = false; const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, @@ -606,7 +855,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi dispatcher, replyOptions: { ...replyOptions, - skillFilter: roomConfigInfo.config?.skills, + skillFilter: roomConfig?.skills, }, }); markDispatchIdle(); @@ -628,15 +877,101 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi } }; - client.on(RoomEvent.Timeline, handleTimeline); + // matrix-bot-sdk uses on("room.message", handler) + client.on("room.message", handleRoomMessage); - await resolveSharedMatrixClient({ cfg, auth: authWithLimit, startClient: true }); - runtime.log?.(`matrix: logged in as ${auth.userId}`); + client.on("room.encrypted_event", (roomId: string, event: MatrixRawEvent) => { + const eventId = event?.event_id ?? "unknown"; + const eventType = event?.type ?? "unknown"; + logVerboseMessage(`matrix: encrypted event room=${roomId} type=${eventType} id=${eventId}`); + }); + + client.on("room.decrypted_event", (roomId: string, event: MatrixRawEvent) => { + const eventId = event?.event_id ?? "unknown"; + const eventType = event?.type ?? "unknown"; + logVerboseMessage(`matrix: decrypted event room=${roomId} type=${eventType} id=${eventId}`); + }); + + // Handle failed E2EE decryption + client.on("room.failed_decryption", async (roomId: string, event: MatrixRawEvent, error: Error) => { + logger.warn({ roomId, eventId: event.event_id, error: error.message }, "Failed to decrypt message"); + logVerboseMessage( + `matrix: failed decrypt room=${roomId} id=${event.event_id ?? "unknown"} error=${error.message}`, + ); + }); + + client.on("room.invite", (roomId: string, event: MatrixRawEvent) => { + const eventId = event?.event_id ?? "unknown"; + const sender = event?.sender ?? "unknown"; + const isDirect = (event?.content as { is_direct?: boolean } | undefined)?.is_direct === true; + logVerboseMessage( + `matrix: invite room=${roomId} sender=${sender} direct=${String(isDirect)} id=${eventId}`, + ); + }); + + client.on("room.join", (roomId: string, event: MatrixRawEvent) => { + const eventId = event?.event_id ?? "unknown"; + logVerboseMessage(`matrix: join room=${roomId} id=${eventId}`); + }); + + client.on("room.event", (roomId: string, event: MatrixRawEvent) => { + const eventType = event?.type ?? "unknown"; + if (eventType === EventType.RoomMessageEncrypted) { + logVerboseMessage( + `matrix: encrypted raw event room=${roomId} id=${event?.event_id ?? "unknown"}`, + ); + if (auth.encryption !== true && !warnedEncryptedRooms.has(roomId)) { + warnedEncryptedRooms.add(roomId); + const warning = + "matrix: encrypted event received without encryption enabled; set channels.matrix.encryption=true and verify the device to decrypt"; + logger.warn({ roomId }, warning); + } + if (auth.encryption === true && !client.crypto && !warnedCryptoMissingRooms.has(roomId)) { + warnedCryptoMissingRooms.add(roomId); + const warning = + "matrix: encryption enabled but crypto is unavailable; install @matrix-org/matrix-sdk-crypto-nodejs and restart"; + logger.warn({ roomId }, warning); + } + return; + } + if (eventType === EventType.RoomMember) { + const membership = (event?.content as { membership?: string } | undefined)?.membership; + const stateKey = (event as { state_key?: string }).state_key ?? ""; + logVerboseMessage( + `matrix: member event room=${roomId} stateKey=${stateKey} membership=${membership ?? "unknown"}`, + ); + } + }); + + logVerboseMessage("matrix: starting client"); + await resolveSharedMatrixClient({ + cfg, + auth: authWithLimit, + accountId: opts.accountId, + }); + logVerboseMessage("matrix: client started"); + + // matrix-bot-sdk client is already started via resolveSharedMatrixClient + logger.info(`matrix: logged in as ${auth.userId}`); + + // If E2EE is enabled, trigger device verification + if (auth.encryption && client.crypto) { + try { + // Request verification from other sessions + const verificationRequest = await client.crypto.requestOwnUserVerification(); + if (verificationRequest) { + logger.info("matrix: device verification requested - please verify in another client"); + } + } catch (err) { + logger.debug({ error: String(err) }, "Device verification request failed (may already be verified)"); + } + } await new Promise((resolve) => { const onAbort = () => { try { - client.stopClient(); + logVerboseMessage("matrix: stopping client"); + stopSharedClient(); } finally { setActiveMatrixClient(null); resolve(); diff --git a/extensions/matrix/src/matrix/monitor/media.test.ts b/extensions/matrix/src/matrix/monitor/media.test.ts new file mode 100644 index 000000000..d8fd51888 --- /dev/null +++ b/extensions/matrix/src/matrix/monitor/media.test.ts @@ -0,0 +1,67 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { PluginRuntime } from "clawdbot/plugin-sdk"; +import { setMatrixRuntime } from "../../runtime.js"; +import { downloadMatrixMedia } from "./media.js"; + +describe("downloadMatrixMedia", () => { + const saveMediaBuffer = vi.fn().mockResolvedValue({ + path: "/tmp/media", + contentType: "image/png", + }); + + const runtimeStub = { + channel: { + media: { + saveMediaBuffer: (...args: unknown[]) => saveMediaBuffer(...args), + }, + }, + } as unknown as PluginRuntime; + + beforeEach(() => { + vi.clearAllMocks(); + setMatrixRuntime(runtimeStub); + }); + + it("decrypts encrypted media when file payloads are present", async () => { + const decryptMedia = vi.fn().mockResolvedValue(Buffer.from("decrypted")); + const downloadContent = vi.fn().mockResolvedValue(Buffer.from("encrypted")); + + const client = { + downloadContent, + crypto: { decryptMedia }, + mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"), + } as unknown as import("matrix-bot-sdk").MatrixClient; + + const file = { + url: "mxc://example/file", + key: { + kty: "oct", + key_ops: ["encrypt", "decrypt"], + alg: "A256CTR", + k: "secret", + ext: true, + }, + iv: "iv", + hashes: { sha256: "hash" }, + v: "v2", + }; + + const result = await downloadMatrixMedia({ + client, + mxcUrl: "mxc://example/file", + contentType: "image/png", + maxBytes: 1024, + file, + }); + + expect(decryptMedia).toHaveBeenCalled(); + expect(saveMediaBuffer).toHaveBeenCalledWith( + Buffer.from("decrypted"), + "image/png", + "inbound", + 1024, + ); + expect(result?.path).toBe("/tmp/media"); + }); +}); diff --git a/extensions/matrix/src/matrix/monitor/media.ts b/extensions/matrix/src/matrix/monitor/media.ts index 4a2405937..dc49e7c45 100644 --- a/extensions/matrix/src/matrix/monitor/media.ts +++ b/extensions/matrix/src/matrix/monitor/media.ts @@ -1,35 +1,68 @@ -import type { MatrixClient } from "matrix-js-sdk"; +import type { MatrixClient } from "matrix-bot-sdk"; import { getMatrixRuntime } from "../../runtime.js"; +// Type for encrypted file info +type EncryptedFile = { + url: string; + key: { + kty: string; + key_ops: string[]; + alg: string; + k: string; + ext: boolean; + }; + iv: string; + hashes: Record; + v: string; +}; + async function fetchMatrixMediaBuffer(params: { client: MatrixClient; mxcUrl: string; maxBytes: number; }): Promise<{ buffer: Buffer; headerType?: string } | null> { - const url = params.client.mxcUrlToHttp( - params.mxcUrl, - undefined, - undefined, - undefined, - false, - true, - true, - ); + // matrix-bot-sdk provides mxcToHttp helper + const url = params.client.mxcToHttp(params.mxcUrl); if (!url) return null; - const token = params.client.getAccessToken(); - const res = await fetch(url, { - headers: token ? { Authorization: `Bearer ${token}` } : undefined, - }); - if (!res.ok) { - throw new Error(`Matrix media download failed: HTTP ${res.status}`); + + // Use the client's download method which handles auth + try { + const buffer = await params.client.downloadContent(params.mxcUrl); + if (buffer.byteLength > params.maxBytes) { + throw new Error("Matrix media exceeds configured size limit"); + } + return { buffer: Buffer.from(buffer) }; + } catch (err) { + throw new Error(`Matrix media download failed: ${String(err)}`); } - const buffer = Buffer.from(await res.arrayBuffer()); - if (buffer.byteLength > params.maxBytes) { +} + +/** + * Download and decrypt encrypted media from a Matrix room. + */ +async function fetchEncryptedMediaBuffer(params: { + client: MatrixClient; + file: EncryptedFile; + maxBytes: number; +}): Promise<{ buffer: Buffer } | null> { + if (!params.client.crypto) { + throw new Error("Cannot decrypt media: crypto not enabled"); + } + + // Download the encrypted content + const encryptedBuffer = await params.client.downloadContent(params.file.url); + if (encryptedBuffer.byteLength > params.maxBytes) { throw new Error("Matrix media exceeds configured size limit"); } - const headerType = res.headers.get("content-type") ?? undefined; - return { buffer, headerType }; + + // Decrypt using matrix-bot-sdk crypto + const decrypted = await params.client.crypto.decryptMedia( + Buffer.from(encryptedBuffer), + params.file, + ); + + return { buffer: decrypted }; } export async function downloadMatrixMedia(params: { @@ -37,16 +70,30 @@ export async function downloadMatrixMedia(params: { mxcUrl: string; contentType?: string; maxBytes: number; + file?: EncryptedFile; }): Promise<{ path: string; contentType?: string; placeholder: string; } | null> { - const fetched = await fetchMatrixMediaBuffer({ - client: params.client, - mxcUrl: params.mxcUrl, - maxBytes: params.maxBytes, - }); + let fetched: { buffer: Buffer; headerType?: string } | null; + + if (params.file) { + // Encrypted media + fetched = await fetchEncryptedMediaBuffer({ + client: params.client, + file: params.file, + maxBytes: params.maxBytes, + }); + } else { + // Unencrypted media + fetched = await fetchMatrixMediaBuffer({ + client: params.client, + mxcUrl: params.mxcUrl, + maxBytes: params.maxBytes, + }); + } + if (!fetched) return null; const headerType = fetched.headerType ?? params.contentType ?? undefined; const saved = await getMatrixRuntime().channel.media.saveMediaBuffer( diff --git a/extensions/matrix/src/matrix/monitor/mentions.ts b/extensions/matrix/src/matrix/monitor/mentions.ts index 3a10fdda1..1053b3fa1 100644 --- a/extensions/matrix/src/matrix/monitor/mentions.ts +++ b/extensions/matrix/src/matrix/monitor/mentions.ts @@ -1,16 +1,22 @@ -import type { RoomMessageEventContent } from "matrix-js-sdk/lib/@types/events.js"; - import { getMatrixRuntime } from "../../runtime.js"; +// Type for room message content with mentions +type MessageContentWithMentions = { + msgtype: string; + body: string; + "m.mentions"?: { + user_ids?: string[]; + room?: boolean; + }; +}; + export function resolveMentions(params: { - content: RoomMessageEventContent; + content: MessageContentWithMentions; userId?: string | null; text?: string; mentionRegexes: RegExp[]; }) { - const mentions = params.content["m.mentions"] as - | { user_ids?: string[]; room?: boolean } - | undefined; + const mentions = params.content["m.mentions"]; const mentionedUsers = Array.isArray(mentions?.user_ids) ? new Set(mentions.user_ids) : new Set(); diff --git a/extensions/matrix/src/matrix/monitor/replies.ts b/extensions/matrix/src/matrix/monitor/replies.ts index 26233de08..7a9cc06aa 100644 --- a/extensions/matrix/src/matrix/monitor/replies.ts +++ b/extensions/matrix/src/matrix/monitor/replies.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-js-sdk"; +import type { MatrixClient } from "matrix-bot-sdk"; import type { ReplyPayload, RuntimeEnv } from "clawdbot/plugin-sdk"; import { sendMessageMatrix } from "../send.js"; diff --git a/extensions/matrix/src/matrix/monitor/rooms.ts b/extensions/matrix/src/matrix/monitor/rooms.ts index fd9df6fad..f45f54cf4 100644 --- a/extensions/matrix/src/matrix/monitor/rooms.ts +++ b/extensions/matrix/src/matrix/monitor/rooms.ts @@ -1,4 +1,4 @@ -import type { MatrixConfig, MatrixRoomConfig } from "../../types.js"; +import type { MatrixRoomConfig } from "../../types.js"; import { buildChannelKeyCandidates, resolveChannelEntryMatch } from "clawdbot/plugin-sdk"; export type MatrixRoomConfigResolved = { @@ -10,7 +10,7 @@ export type MatrixRoomConfigResolved = { }; export function resolveMatrixRoomConfig(params: { - rooms?: MatrixConfig["rooms"]; + rooms?: Record; roomId: string; aliases: string[]; name?: string | null; diff --git a/extensions/matrix/src/matrix/monitor/threads.ts b/extensions/matrix/src/matrix/monitor/threads.ts index 50a42ac67..3378d3b2b 100644 --- a/extensions/matrix/src/matrix/monitor/threads.ts +++ b/extensions/matrix/src/matrix/monitor/threads.ts @@ -1,6 +1,25 @@ -import type { MatrixEvent } from "matrix-js-sdk"; -import { RelationType } from "matrix-js-sdk"; -import type { RoomMessageEventContent } from "matrix-js-sdk/lib/@types/events.js"; +// Type for raw Matrix event from matrix-bot-sdk +type MatrixRawEvent = { + event_id: string; + sender: string; + type: string; + origin_server_ts: number; + content: Record; +}; + +type RoomMessageEventContent = { + msgtype: string; + body: string; + "m.relates_to"?: { + rel_type?: string; + event_id?: string; + "m.in_reply_to"?: { event_id?: string }; + }; +}; + +const RelationType = { + Thread: "m.thread", +} as const; export function resolveMatrixThreadTarget(params: { threadReplies: "off" | "inbound" | "always"; @@ -22,13 +41,9 @@ export function resolveMatrixThreadTarget(params: { } export function resolveMatrixThreadRootId(params: { - event: MatrixEvent; + event: MatrixRawEvent; content: RoomMessageEventContent; }): string | undefined { - const fromThread = params.event.getThread?.()?.id; - if (fromThread) return fromThread; - const direct = params.event.threadRootId ?? undefined; - if (direct) return direct; const relates = params.content["m.relates_to"]; if (!relates || typeof relates !== "object") return undefined; if ("rel_type" in relates && relates.rel_type === RelationType.Thread) { diff --git a/extensions/matrix/src/matrix/poll-types.ts b/extensions/matrix/src/matrix/poll-types.ts index 38a465cfb..28b36d42d 100644 --- a/extensions/matrix/src/matrix/poll-types.ts +++ b/extensions/matrix/src/matrix/poll-types.ts @@ -7,8 +7,6 @@ * - m.poll.end - Closes a poll */ -import type { TimelineEvents } from "matrix-js-sdk/lib/@types/event.js"; -import type { ExtensibleAnyMessageEventContent } from "matrix-js-sdk/lib/@types/extensible_events.js"; import type { PollInput } from "clawdbot/plugin-sdk"; export const M_POLL_START = "m.poll.start" as const; @@ -34,7 +32,9 @@ export const POLL_END_TYPES = [M_POLL_END, ORG_POLL_END]; export type PollKind = "m.poll.disclosed" | "m.poll.undisclosed"; -export type TextContent = ExtensibleAnyMessageEventContent & { +export type TextContent = { + "m.text"?: string; + "org.matrix.msc1767.text"?: string; body?: string; }; @@ -53,7 +53,13 @@ export type LegacyPollStartContent = { "m.poll"?: PollStartSubtype; }; -export type PollStartContent = TimelineEvents[typeof M_POLL_START] | LegacyPollStartContent; +export type PollStartContent = { + [M_POLL_START]?: PollStartSubtype; + [ORG_POLL_START]?: PollStartSubtype; + "m.poll"?: PollStartSubtype; + "m.text"?: string; + "org.matrix.msc1767.text"?: string; +}; export type PollSummary = { eventId: string; diff --git a/extensions/matrix/src/matrix/probe.ts b/extensions/matrix/src/matrix/probe.ts index baf3c502c..3bfdd1728 100644 --- a/extensions/matrix/src/matrix/probe.ts +++ b/extensions/matrix/src/matrix/probe.ts @@ -49,9 +49,10 @@ export async function probeMatrix(params: { accessToken: params.accessToken, localTimeoutMs: params.timeoutMs, }); - const res = await client.whoami(); + // matrix-bot-sdk uses getUserId() which calls whoami internally + const userId = await client.getUserId(); result.ok = true; - result.userId = res.user_id ?? null; + result.userId = userId ?? null; result.elapsedMs = Date.now() - started; return result; @@ -59,8 +60,8 @@ export async function probeMatrix(params: { return { ...result, status: - typeof err === "object" && err && "httpStatus" in err - ? Number((err as { httpStatus?: number }).httpStatus) + typeof err === "object" && err && "statusCode" in err + ? Number((err as { statusCode?: number }).statusCode) : result.status, error: err instanceof Error ? err.message : String(err), elapsedMs: Date.now() - started, diff --git a/extensions/matrix/src/matrix/send.test.ts b/extensions/matrix/src/matrix/send.test.ts index 1cae12f32..5520d126e 100644 --- a/extensions/matrix/src/matrix/send.test.ts +++ b/extensions/matrix/src/matrix/send.test.ts @@ -3,22 +3,20 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { PluginRuntime } from "clawdbot/plugin-sdk"; import { setMatrixRuntime } from "../runtime.js"; -vi.mock("matrix-js-sdk", () => ({ - EventType: { - Direct: "m.direct", - RoomMessage: "m.room.message", - Reaction: "m.reaction", +vi.mock("matrix-bot-sdk", () => ({ + ConsoleLogger: class { + trace = vi.fn(); + debug = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + error = vi.fn(); }, - MsgType: { - Text: "m.text", - File: "m.file", - Image: "m.image", - Audio: "m.audio", - Video: "m.video", - }, - RelationType: { - Annotation: "m.annotation", + LogService: { + setLogger: vi.fn(), }, + MatrixClient: vi.fn(), + SimpleFsStorageProvider: vi.fn(), + RustSdkCryptoStorageProvider: vi.fn(), })); const loadWebMediaMock = vi.fn().mockResolvedValue({ @@ -52,14 +50,13 @@ const runtimeStub = { let sendMessageMatrix: typeof import("./send.js").sendMessageMatrix; const makeClient = () => { - const sendMessage = vi.fn().mockResolvedValue({ event_id: "evt1" }); - const uploadContent = vi.fn().mockResolvedValue({ - content_uri: "mxc://example/file", - }); + const sendMessage = vi.fn().mockResolvedValue("evt1"); + const uploadContent = vi.fn().mockResolvedValue("mxc://example/file"); const client = { sendMessage, uploadContent, - } as unknown as import("matrix-js-sdk").MatrixClient; + getUserId: vi.fn().mockResolvedValue("@bot:example.org"), + } as unknown as import("matrix-bot-sdk").MatrixClient; return { client, sendMessage, uploadContent }; }; @@ -96,4 +93,76 @@ describe("sendMessageMatrix media", () => { expect(content.formatted_body).toContain("caption"); expect(content.url).toBe("mxc://example/file"); }); + + it("uploads encrypted media with file payloads", async () => { + const { client, sendMessage, uploadContent } = makeClient(); + (client as { crypto?: object }).crypto = { + isRoomEncrypted: vi.fn().mockResolvedValue(true), + encryptMedia: vi.fn().mockResolvedValue({ + buffer: Buffer.from("encrypted"), + file: { + key: { + kty: "oct", + key_ops: ["encrypt", "decrypt"], + alg: "A256CTR", + k: "secret", + ext: true, + }, + iv: "iv", + hashes: { sha256: "hash" }, + v: "v2", + }, + }), + }; + + await sendMessageMatrix("room:!room:example", "caption", { + client, + mediaUrl: "file:///tmp/photo.png", + }); + + const uploadArg = uploadContent.mock.calls[0]?.[0] as Buffer | undefined; + expect(uploadArg?.toString()).toBe("encrypted"); + + const content = sendMessage.mock.calls[0]?.[1] as { + url?: string; + file?: { url?: string }; + }; + expect(content.url).toBeUndefined(); + expect(content.file?.url).toBe("mxc://example/file"); + }); +}); + +describe("sendMessageMatrix threads", () => { + beforeAll(async () => { + setMatrixRuntime(runtimeStub); + ({ sendMessageMatrix } = await import("./send.js")); + }); + + beforeEach(() => { + vi.clearAllMocks(); + setMatrixRuntime(runtimeStub); + }); + + it("includes thread relation metadata when threadId is set", async () => { + const { client, sendMessage } = makeClient(); + + await sendMessageMatrix("room:!room:example", "hello thread", { + client, + threadId: "$thread", + }); + + const content = sendMessage.mock.calls[0]?.[1] as { + "m.relates_to"?: { + rel_type?: string; + event_id?: string; + "m.in_reply_to"?: { event_id?: string }; + }; + }; + + expect(content["m.relates_to"]).toMatchObject({ + rel_type: "m.thread", + event_id: "$thread", + "m.in_reply_to": { event_id: "$thread" }, + }); + }); }); diff --git a/extensions/matrix/src/matrix/send.ts b/extensions/matrix/src/matrix/send.ts index 9e4499594..dfc358515 100644 --- a/extensions/matrix/src/matrix/send.ts +++ b/extensions/matrix/src/matrix/send.ts @@ -1,9 +1,14 @@ -import type { AccountDataEvents, MatrixClient } from "matrix-js-sdk"; -import { EventType, MsgType, RelationType } from "matrix-js-sdk"; import type { - RoomMessageEventContent, - ReactionEventContent, -} from "matrix-js-sdk/lib/@types/events.js"; + DimensionalFileInfo, + EncryptedFile, + FileWithThumbnailInfo, + MessageEventContent, + TextualMessageEventContent, + TimedFileInfo, + VideoFileInfo, + MatrixClient, +} from "matrix-bot-sdk"; +import { parseBuffer, type IFileInfo } from "music-metadata"; import type { PollInput } from "clawdbot/plugin-sdk"; import { getMatrixRuntime } from "../runtime.js"; @@ -13,7 +18,6 @@ import { isBunRuntime, resolveMatrixAuth, resolveSharedMatrixClient, - waitForMatrixSync, } from "./client.js"; import { markdownToMatrixHtml } from "./format.js"; import { buildPollStartContent, M_POLL_START } from "./poll-types.js"; @@ -22,18 +26,72 @@ import type { CoreConfig } from "../types.js"; const MATRIX_TEXT_LIMIT = 4000; const getCore = () => getMatrixRuntime(); -type MatrixDirectAccountData = AccountDataEvents[EventType.Direct]; +// Message types +const MsgType = { + Text: "m.text", + Image: "m.image", + Audio: "m.audio", + Video: "m.video", + File: "m.file", + Notice: "m.notice", +} as const; + +// Relation types +const RelationType = { + Annotation: "m.annotation", + Replace: "m.replace", + Thread: "m.thread", +} as const; + +// Event types +const EventType = { + Direct: "m.direct", + Reaction: "m.reaction", + RoomMessage: "m.room.message", +} as const; + +type MatrixDirectAccountData = Record; type MatrixReplyRelation = { "m.in_reply_to": { event_id: string }; }; -type MatrixMessageContent = Record & { - msgtype: MsgType; - body: string; +type MatrixThreadRelation = { + rel_type: typeof RelationType.Thread; + event_id: string; + is_falling_back?: boolean; + "m.in_reply_to"?: { event_id: string }; }; -type MatrixUploadContent = Parameters[0]; +type MatrixRelation = MatrixReplyRelation | MatrixThreadRelation; + +type MatrixReplyMeta = { + "m.relates_to"?: MatrixRelation; +}; + +type MatrixMediaInfo = FileWithThumbnailInfo | DimensionalFileInfo | TimedFileInfo | VideoFileInfo; + +type MatrixTextContent = TextualMessageEventContent & MatrixReplyMeta; + +type MatrixMediaContent = MessageEventContent & + MatrixReplyMeta & { + info?: MatrixMediaInfo; + url?: string; + file?: EncryptedFile; + filename?: string; + "org.matrix.msc3245.voice"?: Record; + "org.matrix.msc1767.audio"?: { duration: number }; + }; + +type MatrixOutboundContent = MatrixTextContent | MatrixMediaContent; + +type ReactionEventContent = { + "m.relates_to": { + rel_type: typeof RelationType.Annotation; + event_id: string; + key: string; + }; +}; export type MatrixSendResult = { messageId: string; @@ -83,13 +141,14 @@ async function resolveDirectRoomId(client: MatrixClient, userId: string): Promis if (!trimmed.startsWith("@")) { throw new Error(`Matrix user IDs must be fully qualified (got "${trimmed}")`); } - const directEvent = client.getAccountData(EventType.Direct); - const directContent = directEvent?.getContent(); - const list = Array.isArray(directContent?.[trimmed]) ? directContent[trimmed] : []; - if (list.length > 0) return list[0]; - const server = await client.getAccountDataFromServer(EventType.Direct); - const serverList = Array.isArray(server?.[trimmed]) ? server[trimmed] : []; - if (serverList.length > 0) return serverList[0]; + // matrix-bot-sdk: use getAccountData to retrieve m.direct + try { + const directContent = await client.getAccountData(EventType.Direct) as MatrixDirectAccountData | null; + const list = Array.isArray(directContent?.[trimmed]) ? directContent[trimmed] : []; + if (list.length > 0) return list[0]; + } catch { + // Ignore errors, try fetching from server + } throw new Error( `No m.direct room found for ${trimmed}. Open a DM first so Matrix can set m.direct.`, ); @@ -117,75 +176,116 @@ export async function resolveMatrixRoomId( return await resolveDirectRoomId(client, target); } if (target.startsWith("#")) { - const resolved = await client.getRoomIdForAlias(target); - if (!resolved?.room_id) { + const resolved = await client.resolveRoom(target); + if (!resolved) { throw new Error(`Matrix alias ${target} could not be resolved`); } - return resolved.room_id; + return resolved; } return target; } -type MatrixImageInfo = { - w?: number; - h?: number; - thumbnail_url?: string; - thumbnail_info?: { - w: number; - h: number; - mimetype: string; - size: number; - }; +type MatrixMediaMsgType = + | typeof MsgType.Image + | typeof MsgType.Audio + | typeof MsgType.Video + | typeof MsgType.File; + +type MediaKind = "image" | "audio" | "video" | "document" | "unknown"; + +function buildMatrixMediaInfo(params: { + size: number; + mimetype?: string; + durationMs?: number; + imageInfo?: DimensionalFileInfo; +}): MatrixMediaInfo | undefined { + const base: FileWithThumbnailInfo = {}; + if (Number.isFinite(params.size)) { + base.size = params.size; + } + if (params.mimetype) { + base.mimetype = params.mimetype; + } + if (params.imageInfo) { + const dimensional: DimensionalFileInfo = { + ...base, + ...params.imageInfo, + }; + if (typeof params.durationMs === "number") { + const videoInfo: VideoFileInfo = { + ...dimensional, + duration: params.durationMs, + }; + return videoInfo; + } + return dimensional; + } + if (typeof params.durationMs === "number") { + const timedInfo: TimedFileInfo = { + ...base, + duration: params.durationMs, + }; + return timedInfo; + } + if (Object.keys(base).length === 0) return undefined; + return base; +} + +type MatrixFormattedContent = MessageEventContent & { + format?: string; + formatted_body?: string; }; function buildMediaContent(params: { - msgtype: MsgType.Image | MsgType.Audio | MsgType.Video | MsgType.File; + msgtype: MatrixMediaMsgType; body: string; - url: string; + url?: string; filename?: string; mimetype?: string; size: number; - relation?: MatrixReplyRelation; + relation?: MatrixRelation; isVoice?: boolean; durationMs?: number; - imageInfo?: MatrixImageInfo; -}): RoomMessageEventContent { - const info: Record = { mimetype: params.mimetype, size: params.size }; - if (params.durationMs !== undefined) { - info.duration = params.durationMs; - } - if (params.imageInfo) { - if (params.imageInfo.w) info.w = params.imageInfo.w; - if (params.imageInfo.h) info.h = params.imageInfo.h; - if (params.imageInfo.thumbnail_url) { - info.thumbnail_url = params.imageInfo.thumbnail_url; - if (params.imageInfo.thumbnail_info) { - info.thumbnail_info = params.imageInfo.thumbnail_info; - } - } - } - const base: MatrixMessageContent = { + imageInfo?: DimensionalFileInfo; + file?: EncryptedFile; // For encrypted media +}): MatrixMediaContent { + const info = buildMatrixMediaInfo({ + size: params.size, + mimetype: params.mimetype, + durationMs: params.durationMs, + imageInfo: params.imageInfo, + }); + const base: MatrixMediaContent = { msgtype: params.msgtype, body: params.body, filename: params.filename, - info, - url: params.url, + info: info ?? undefined, }; + // Encrypted media should only include the "file" payload, not top-level "url". + if (!params.file && params.url) { + base.url = params.url; + } + // For encrypted files, add the file object + if (params.file) { + base.file = params.file; + } if (params.isVoice) { base["org.matrix.msc3245.voice"] = {}; - base["org.matrix.msc1767.audio"] = { - duration: params.durationMs, - }; + if (typeof params.durationMs === "number") { + base["org.matrix.msc1767.audio"] = { + duration: params.durationMs, + }; + } } if (params.relation) { base["m.relates_to"] = params.relation; } applyMatrixFormatting(base, params.body); - return base as RoomMessageEventContent; + return base; } -function buildTextContent(body: string, relation?: MatrixReplyRelation): RoomMessageEventContent { - const content: MatrixMessageContent = relation +function buildTextContent(body: string, relation?: MatrixRelation): MatrixTextContent { + const content: MatrixTextContent = relation ? { msgtype: MsgType.Text, body, @@ -196,10 +296,10 @@ function buildTextContent(body: string, relation?: MatrixReplyRelation): RoomMes body, }; applyMatrixFormatting(content, body); - return content as RoomMessageEventContent; + return content; } -function applyMatrixFormatting(content: MatrixMessageContent, body: string): void { +function applyMatrixFormatting(content: MatrixFormattedContent, body: string): void { const formatted = markdownToMatrixHtml(body ?? ""); if (!formatted) return; content.format = "org.matrix.custom.html"; @@ -212,10 +312,20 @@ function buildReplyRelation(replyToId?: string): MatrixReplyRelation | undefined return { "m.in_reply_to": { event_id: trimmed } }; } +function buildThreadRelation(threadId: string, replyToId?: string): MatrixThreadRelation { + const trimmed = threadId.trim(); + return { + rel_type: RelationType.Thread, + event_id: trimmed, + is_falling_back: true, + "m.in_reply_to": { event_id: (replyToId?.trim() || trimmed) }, + }; +} + function resolveMatrixMsgType( contentType?: string, fileName?: string, -): MsgType.Image | MsgType.Audio | MsgType.Video | MsgType.File { +): MatrixMediaMsgType { const kind = getCore().media.mediaKindFromMime(contentType ?? ""); switch (kind) { case "image": @@ -247,10 +357,10 @@ const THUMBNAIL_QUALITY = 80; async function prepareImageInfo(params: { buffer: Buffer; client: MatrixClient; -}): Promise { +}): Promise { const meta = await getCore().media.getImageMetadata(params.buffer).catch(() => null); if (!meta) return undefined; - const imageInfo: MatrixImageInfo = { w: meta.width, h: meta.height }; + const imageInfo: DimensionalFileInfo = { w: meta.width, h: meta.height }; const maxDim = Math.max(meta.width, meta.height); if (maxDim > THUMBNAIL_MAX_SIDE) { try { @@ -261,11 +371,12 @@ async function prepareImageInfo(params: { withoutEnlargement: true, }); const thumbMeta = await getCore().media.getImageMetadata(thumbBuffer).catch(() => null); - const thumbUri = await params.client.uploadContent(thumbBuffer as MatrixUploadContent, { - type: "image/jpeg", - name: "thumbnail.jpg", - }); - imageInfo.thumbnail_url = thumbUri.content_uri; + const thumbUri = await params.client.uploadContent( + thumbBuffer, + "image/jpeg", + "thumbnail.jpg", + ); + imageInfo.thumbnail_url = thumbUri; if (thumbMeta) { imageInfo.thumbnail_info = { w: thumbMeta.width, @@ -281,21 +392,76 @@ async function prepareImageInfo(params: { return imageInfo; } +async function resolveMediaDurationMs(params: { + buffer: Buffer; + contentType?: string; + fileName?: string; + kind: MediaKind; +}): Promise { + if (params.kind !== "audio" && params.kind !== "video") return undefined; + try { + const fileInfo: IFileInfo | string | undefined = + params.contentType || params.fileName + ? { + mimeType: params.contentType, + size: params.buffer.byteLength, + path: params.fileName, + } + : undefined; + const metadata = await parseBuffer(params.buffer, fileInfo, { + duration: true, + skipCovers: true, + }); + const durationSeconds = metadata.format.duration; + if (typeof durationSeconds === "number" && Number.isFinite(durationSeconds)) { + return Math.max(0, Math.round(durationSeconds * 1000)); + } + } catch { + // Duration is optional; ignore parse failures. + } + return undefined; +} + async function uploadFile( client: MatrixClient, - file: MatrixUploadContent | Buffer, + file: Buffer, params: { contentType?: string; filename?: string; - includeFilename?: boolean; }, ): Promise { - const upload = await client.uploadContent(file as MatrixUploadContent, { - type: params.contentType, - name: params.filename, - includeFilename: params.includeFilename, - }); - return upload.content_uri; + return await client.uploadContent(file, params.contentType, params.filename); +} + +/** + * Upload media with optional encryption for E2EE rooms. + */ +async function uploadMediaMaybeEncrypted( + client: MatrixClient, + roomId: string, + buffer: Buffer, + params: { + contentType?: string; + filename?: string; + }, +): Promise<{ url: string; file?: EncryptedFile }> { + // Check if room is encrypted and crypto is available + const isEncrypted = client.crypto && await client.crypto.isRoomEncrypted(roomId); + + if (isEncrypted && client.crypto) { + // Encrypt the media before uploading + const encrypted = await client.crypto.encryptMedia(buffer); + const mxc = await client.uploadContent(encrypted.buffer, params.contentType, params.filename); + const file: EncryptedFile = { url: mxc, ...encrypted.file }; + return { + url: mxc, + file, + }; + } + + // Upload unencrypted + const mxc = await uploadFile(client, buffer, params); + return { url: mxc }; } async function resolveMatrixClient(opts: { @@ -318,14 +484,19 @@ async function resolveMatrixClient(opts: { homeserver: auth.homeserver, userId: auth.userId, accessToken: auth.accessToken, + encryption: auth.encryption, localTimeoutMs: opts.timeoutMs, }); - await client.startClient({ - initialSyncLimit: 0, - lazyLoadMembers: true, - threadSupport: true, - }); - await waitForMatrixSync({ client, timeoutMs: opts.timeoutMs }); + if (auth.encryption && client.crypto) { + try { + const joinedRooms = await client.getJoinedRooms(); + await client.crypto.prepare(joinedRooms); + } catch { + // Ignore crypto prep failures for one-off sends; normal sync will retry. + } + } + // matrix-bot-sdk uses start() instead of startClient() + await client.start(); return { client, stopOnDone: true }; } @@ -349,18 +520,29 @@ export async function sendMessageMatrix( const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT); const chunks = getCore().channel.text.chunkMarkdownText(trimmedMessage, chunkLimit); const threadId = normalizeThreadId(opts.threadId); - const relation = threadId ? undefined : buildReplyRelation(opts.replyToId); - const sendContent = (content: RoomMessageEventContent) => - threadId ? client.sendMessage(roomId, threadId, content) : client.sendMessage(roomId, content); + const relation = threadId + ? buildThreadRelation(threadId, opts.replyToId) + : buildReplyRelation(opts.replyToId); + const sendContent = async (content: MatrixOutboundContent) => { + // matrix-bot-sdk uses sendMessage differently + const eventId = await client.sendMessage(roomId, content); + return eventId; + }; let lastMessageId = ""; if (opts.mediaUrl) { const maxBytes = resolveMediaMaxBytes(); const media = await getCore().media.loadWebMedia(opts.mediaUrl, maxBytes); - const contentUri = await uploadFile(client, media.buffer, { + const uploaded = await uploadMediaMaybeEncrypted(client, roomId, media.buffer, { contentType: media.contentType, filename: media.fileName, }); + const durationMs = await resolveMediaDurationMs({ + buffer: media.buffer, + contentType: media.contentType, + fileName: media.fileName, + kind: media.kind, + }); const baseMsgType = resolveMatrixMsgType(media.contentType, media.fileName); const { useVoice } = resolveMatrixVoiceDecision({ wantsVoice: opts.audioAsVoice === true, @@ -375,31 +557,34 @@ export async function sendMessageMatrix( const content = buildMediaContent({ msgtype, body, - url: contentUri, + url: uploaded.url, + file: uploaded.file, filename: media.fileName, mimetype: media.contentType, size: media.buffer.byteLength, + durationMs, relation, isVoice: useVoice, imageInfo, }); - const response = await sendContent(content); - lastMessageId = response.event_id ?? lastMessageId; + const eventId = await sendContent(content); + lastMessageId = eventId ?? lastMessageId; const textChunks = useVoice ? chunks : rest; + const followupRelation = threadId ? relation : undefined; for (const chunk of textChunks) { const text = chunk.trim(); if (!text) continue; - const followup = buildTextContent(text); - const followupRes = await sendContent(followup); - lastMessageId = followupRes.event_id ?? lastMessageId; + const followup = buildTextContent(text, followupRelation); + const followupEventId = await sendContent(followup); + lastMessageId = followupEventId ?? lastMessageId; } } else { for (const chunk of chunks.length ? chunks : [""]) { const text = chunk.trim(); if (!text) continue; const content = buildTextContent(text, relation); - const response = await sendContent(content); - lastMessageId = response.event_id ?? lastMessageId; + const eventId = await sendContent(content); + lastMessageId = eventId ?? lastMessageId; } } @@ -409,7 +594,7 @@ export async function sendMessageMatrix( }; } finally { if (stopOnDone) { - client.stopClient(); + client.stop(); } } } @@ -434,26 +619,19 @@ export async function sendPollMatrix( const roomId = await resolveMatrixRoomId(client, to); const pollContent = buildPollStartContent(poll); const threadId = normalizeThreadId(opts.threadId); - const response = threadId - ? await client.sendEvent( - roomId, - threadId, - M_POLL_START, - pollContent, - ) - : await client.sendEvent( - roomId, - M_POLL_START, - pollContent, - ); + const pollPayload = threadId + ? { ...pollContent, "m.relates_to": buildThreadRelation(threadId) } + : pollContent; + // matrix-bot-sdk sendEvent returns eventId string directly + const eventId = await client.sendEvent(roomId, M_POLL_START, pollPayload); return { - eventId: response.event_id ?? "unknown", + eventId: eventId ?? "unknown", roomId, }; } finally { if (stopOnDone) { - client.stopClient(); + client.stop(); } } } @@ -470,10 +648,29 @@ export async function sendTypingMatrix( }); try { const resolvedTimeoutMs = typeof timeoutMs === "number" ? timeoutMs : 30_000; - await resolved.sendTyping(roomId, typing, resolvedTimeoutMs); + await resolved.setTyping(roomId, typing, resolvedTimeoutMs); } finally { if (stopOnDone) { - resolved.stopClient(); + resolved.stop(); + } + } +} + +export async function sendReadReceiptMatrix( + roomId: string, + eventId: string, + client?: MatrixClient, +): Promise { + if (!eventId?.trim()) return; + const { client: resolved, stopOnDone } = await resolveMatrixClient({ + client, + }); + try { + const resolvedRoom = await resolveMatrixRoomId(resolved, roomId); + await resolved.sendReadReceipt(resolvedRoom, eventId.trim()); + } finally { + if (stopOnDone) { + resolved.stop(); } } } @@ -502,7 +699,7 @@ export async function reactMatrixMessage( await resolved.sendEvent(resolvedRoom, EventType.Reaction, reaction); } finally { if (stopOnDone) { - resolved.stopClient(); + resolved.stop(); } } } diff --git a/extensions/matrix/src/onboarding.ts b/extensions/matrix/src/onboarding.ts index 5dba54238..28f24b788 100644 --- a/extensions/matrix/src/onboarding.ts +++ b/extensions/matrix/src/onboarding.ts @@ -35,8 +35,9 @@ function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy) { async function noteMatrixAuthHelp(prompter: WizardPrompter): Promise { await prompter.note( [ - "Matrix requires a homeserver URL + user ID.", - "Use an access token or a password (password logs in and stores a token).", + "Matrix requires a homeserver URL.", + "Use an access token (recommended) or a password (logs in and stores a token).", + "With access token: user ID is fetched automatically.", "Env vars supported: MATRIX_HOMESERVER, MATRIX_USER_ID, MATRIX_ACCESS_TOKEN, MATRIX_PASSWORD.", `Docs: ${formatDocsLink("/channels/matrix", "channels/matrix")}`, ].join("\n"), @@ -146,8 +147,8 @@ function setMatrixGroupPolicy(cfg: CoreConfig, groupPolicy: "open" | "allowlist" }; } -function setMatrixRoomAllowlist(cfg: CoreConfig, roomKeys: string[]) { - const rooms = Object.fromEntries(roomKeys.map((key) => [key, { allow: true }])); +function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[]) { + const groups = Object.fromEntries(roomKeys.map((key) => [key, { allow: true }])); return { ...cfg, channels: { @@ -155,7 +156,7 @@ function setMatrixRoomAllowlist(cfg: CoreConfig, roomKeys: string[]) { matrix: { ...cfg.channels?.matrix, enabled: true, - rooms, + groups, }, }, }; @@ -180,9 +181,11 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { return { channel, configured, - statusLines: [`Matrix: ${configured ? "configured" : "needs homeserver + user id"}`], + statusLines: [ + `Matrix: ${configured ? "configured" : "needs homeserver + access token or password"}`, + ], selectionHint: !sdkReady - ? "install matrix-js-sdk" + ? "install matrix-bot-sdk" : configured ? "configured" : "needs auth", @@ -208,7 +211,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { const envUserId = process.env.MATRIX_USER_ID?.trim(); const envAccessToken = process.env.MATRIX_ACCESS_TOKEN?.trim(); const envPassword = process.env.MATRIX_PASSWORD?.trim(); - const envReady = Boolean(envHomeserver && envUserId && (envAccessToken || envPassword)); + const envReady = Boolean(envHomeserver && (envAccessToken || (envUserId && envPassword))); if ( envReady && @@ -252,22 +255,9 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { }), ).trim(); - const userId = String( - await prompter.text({ - message: "Matrix user ID", - initialValue: existing.userId ?? envUserId, - validate: (value) => { - const raw = String(value ?? "").trim(); - if (!raw) return "Required"; - if (!raw.startsWith("@")) return "Matrix user IDs should start with @"; - if (!raw.includes(":")) return "Matrix user IDs should include a server (:@server)"; - return undefined; - }, - }), - ).trim(); - let accessToken = existing.accessToken ?? ""; let password = existing.password ?? ""; + let userId = existing.userId ?? ""; if (accessToken || password) { const keep = await prompter.confirm({ @@ -277,15 +267,17 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { if (!keep) { accessToken = ""; password = ""; + userId = ""; } } if (!accessToken && !password) { + // Ask auth method FIRST before asking for user ID const authMode = (await prompter.select({ message: "Matrix auth method", options: [ - { value: "token", label: "Access token" }, - { value: "password", label: "Password (stores token)" }, + { value: "token", label: "Access token (user ID fetched automatically)" }, + { value: "password", label: "Password (requires user ID)" }, ], })) as "token" | "password"; @@ -296,7 +288,24 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { validate: (value) => (value?.trim() ? undefined : "Required"), }), ).trim(); + // With access token, we can fetch the userId automatically - don't prompt for it + // The client.ts will use whoami() to get it + userId = ""; } else { + // Password auth requires user ID upfront + userId = String( + await prompter.text({ + message: "Matrix user ID", + initialValue: existing.userId ?? envUserId, + validate: (value) => { + const raw = String(value ?? "").trim(); + if (!raw) return "Required"; + if (!raw.startsWith("@")) return "Matrix user IDs should start with @"; + if (!raw.includes(":")) return "Matrix user IDs should include a server (:server)"; + return undefined; + }, + }), + ).trim(); password = String( await prompter.text({ message: "Matrix password", @@ -313,6 +322,12 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { }), ).trim(); + // Ask about E2EE encryption + const enableEncryption = await prompter.confirm({ + message: "Enable end-to-end encryption (E2EE)?", + initialValue: existing.encryption ?? false, + }); + next = { ...next, channels: { @@ -321,10 +336,11 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { ...next.channels?.matrix, enabled: true, homeserver, - userId, + userId: userId || undefined, accessToken: accessToken || undefined, password: password || undefined, deviceName: deviceName || undefined, + encryption: enableEncryption || undefined, }, }, }; @@ -333,13 +349,14 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { next = await promptMatrixAllowFrom({ cfg: next, prompter }); } + const existingGroups = next.channels?.matrix?.groups ?? next.channels?.matrix?.rooms; const accessConfig = await promptChannelAccessConfig({ prompter, label: "Matrix rooms", currentPolicy: next.channels?.matrix?.groupPolicy ?? "allowlist", - currentEntries: Object.keys(next.channels?.matrix?.rooms ?? {}), + currentEntries: Object.keys(existingGroups ?? {}), placeholder: "!roomId:server, #alias:server, Project Room", - updatePrompt: Boolean(next.channels?.matrix?.rooms), + updatePrompt: Boolean(existingGroups), }); if (accessConfig) { if (accessConfig.policy !== "allowlist") { @@ -398,7 +415,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { } } next = setMatrixGroupPolicy(next, "allowlist"); - next = setMatrixRoomAllowlist(next, roomKeys); + next = setMatrixGroupRooms(next, roomKeys); } } diff --git a/extensions/matrix/src/types.ts b/extensions/matrix/src/types.ts index e5c91e2e2..8b2e96d34 100644 --- a/extensions/matrix/src/types.ts +++ b/extensions/matrix/src/types.ts @@ -51,12 +51,16 @@ export type MatrixConfig = { password?: string; /** Optional device name when logging in via password. */ deviceName?: string; - /** Initial sync limit for startup (default: matrix-js-sdk default). */ + /** Initial sync limit for startup (default: matrix-bot-sdk default). */ initialSyncLimit?: number; + /** Enable end-to-end encryption (E2EE). Default: false. */ + encryption?: boolean; /** If true, enforce allowlists for groups + DMs regardless of policy. */ allowlistOnly?: boolean; /** Group message policy (default: allowlist). */ groupPolicy?: GroupPolicy; + /** Allowlist for group senders (user IDs or localparts). */ + groupAllowFrom?: Array; /** Control reply threading when reply tags are present (off|first|all). */ replyToMode?: ReplyToMode; /** How to handle thread replies (off|inbound|always). */ @@ -72,6 +76,8 @@ export type MatrixConfig = { /** Direct message policy + allowlist overrides. */ dm?: MatrixDmConfig; /** Room config allowlist keyed by room ID, alias, or name. */ + groups?: Record; + /** Room config allowlist keyed by room ID, alias, or name. Legacy; use groups. */ rooms?: Record; /** Per-action tool gating (default: true for all). */ actions?: MatrixActionConfig; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23e901215..d918ac61f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -264,15 +264,21 @@ importers: extensions/matrix: dependencies: + '@matrix-org/matrix-sdk-crypto-nodejs': + specifier: ^0.4.0 + version: 0.4.0 clawdbot: specifier: workspace:* version: link:../.. markdown-it: specifier: 14.1.0 version: 14.1.0 - matrix-js-sdk: - specifier: 40.0.0 - version: 40.0.0 + matrix-bot-sdk: + specifier: 0.8.0 + version: 0.8.0 + music-metadata: + specifier: ^11.10.6 + version: 11.10.6 extensions/memory-core: dependencies: @@ -313,6 +319,8 @@ importers: specifier: ^4.1.2 version: 4.1.2 + extensions/nextcloud-talk: {} + extensions/signal: {} extensions/slack: {} @@ -1184,9 +1192,9 @@ packages: resolution: {integrity: sha512-1Qa2+bVXD2OuMXOlfUx8AYJzW/rx/RnAVwVVCHc8AMpU+DqPGD/QAu1xAlySxONV2KGr/FC9d0126Dzt05xVgw==} engines: {node: '>=20.0.0'} - '@matrix-org/matrix-sdk-crypto-wasm@16.0.0': - resolution: {integrity: sha512-c+0eu/ckG+ik62CaOFvHAulJlspw2CBKcLrWbiEQsXv4J3PC4xaaDI5VHFAl7FDU+U9Ww2DDNTieszCh4Lk0/Q==} - engines: {node: '>= 18'} + '@matrix-org/matrix-sdk-crypto-nodejs@0.4.0': + resolution: {integrity: sha512-+qqgpn39XFSbsD0dFjssGO9vHEP7sTyfs8yTpt8vuqWpUpF20QMwpCZi0jpYw7GxjErNTsMshopuo8677DfGEA==} + engines: {node: '>= 22'} '@microsoft/agents-activity@1.2.2': resolution: {integrity: sha512-PYl2NAJ3shpAfWcDF35FhoeAIf8HTiYclwkjg+vlOLDtNDpVRchfLb7BikToD31jBuKSP8PwbNiCwdVLaXkAYg==} @@ -1894,6 +1902,9 @@ packages: cpu: [x64] os: [win32] + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@silvia-odwyer/photon-node@0.3.4': resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} @@ -2173,12 +2184,15 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/events@3.0.3': - resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} '@types/express-serve-static-core@5.1.1': resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} @@ -2203,6 +2217,9 @@ packages: '@types/mime-types@2.1.4': resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -2242,9 +2259,15 @@ packages: '@types/retry@0.12.5': resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==} + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} @@ -2346,6 +2369,10 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -2366,6 +2393,9 @@ packages: ajv: optional: true + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -2425,6 +2455,16 @@ packages: resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} engines: {node: '>=12.17'} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2432,6 +2472,9 @@ packages: ast-v8-to-istanbul@0.3.10: resolution: {integrity: sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==} + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} @@ -2455,6 +2498,12 @@ packages: resolution: {integrity: sha512-En9AY6EG1qYqEy5L/quryzbA4akBpJrnBZNxeKTqGHC2xT9Qc4aZ8b7CcbOMFTTc/MGdoNyp+SN4zInZNKxMYA==} engines: {node: '>=14'} + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} @@ -2465,12 +2514,16 @@ packages: resolution: {integrity: sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==} engines: {node: '>= 16'} - base-x@5.0.1: - resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} @@ -2481,6 +2534,13 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -2508,9 +2568,6 @@ packages: browser-or-node@3.0.0: resolution: {integrity: sha512-iczIdVJzGEYhP5DqQxYM9Hh7Ztpqqi+CXZpSmX8ALFs9ecXkQIeqRyM6TfxEfMVpwhl3dSuDvxdzzo9sUOIVBQ==} - bs58@6.0.0: - resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} - buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -2538,6 +2595,9 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -2654,6 +2714,10 @@ packages: console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + content-disposition@1.0.1: resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} engines: {node: '>=18'} @@ -2662,6 +2726,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -2670,6 +2737,9 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2697,10 +2767,22 @@ packages: curve25519-js@0.0.4: resolution: {integrity: sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==} + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2714,6 +2796,10 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2725,6 +2811,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2772,6 +2862,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -2834,6 +2927,10 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -2859,6 +2956,10 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} @@ -2866,6 +2967,10 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} @@ -2876,6 +2981,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -2915,6 +3023,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -2939,9 +3051,16 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + form-data-encoder@1.7.2: resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -2958,6 +3077,10 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -3011,10 +3134,16 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} hasBin: true @@ -3047,6 +3176,15 @@ packages: resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} engines: {node: '>=18'} + har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + + har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3062,6 +3200,9 @@ packages: has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + hashery@1.4.0: resolution: {integrity: sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==} engines: {node: '>=20'} @@ -3093,9 +3234,19 @@ packages: html-parse-string@0.0.9: resolution: {integrity: sha512-wyGnsOolHbNrcb8N6bdJF4EHyzd3zVGCb9/mBxeNjAYBDOZqD7YkqLBz7kXtdgHwNnV8lN/BpSDpsI1zm8Sd8g==} + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + htmlencode@0.0.4: + resolution: {integrity: sha512-0uDvNVpzj/E2TfvLLyyXhKBRvF1y84aZsyRxRXFsQobnHaL4pcaXk+Y9cnFlvnxrBLeXDNq/VJBD+ngdBgQG1w==} + htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -3104,6 +3255,10 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -3111,6 +3266,10 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -3167,14 +3326,17 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} - is-network-error@1.3.0: - resolution: {integrity: sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==} - engines: {node: '>=16'} - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -3182,6 +3344,9 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} @@ -3203,6 +3368,9 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -3235,6 +3403,9 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} @@ -3246,9 +3417,18 @@ packages: resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} engines: {node: '>=16'} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -3264,6 +3444,10 @@ packages: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} + jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -3277,10 +3461,6 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - jwt-decode@4.0.0: - resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} - engines: {node: '>=18'} - katex@0.16.27: resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} hasBin: true @@ -3288,6 +3468,9 @@ packages: keyv@5.5.5: resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==} + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} @@ -3432,16 +3615,16 @@ packages: resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} engines: {node: '>=18'} - loglevel@1.9.2: - resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} - engines: {node: '>= 0.6.0'} - long@4.0.0: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lowdb@1.0.0: + resolution: {integrity: sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==} + engines: {node: '>=4'} + lowdb@7.0.1: resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==} engines: {node: '>=18'} @@ -3499,19 +3682,17 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - matrix-events-sdk@0.0.1: - resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - - matrix-js-sdk@40.0.0: - resolution: {integrity: sha512-KQzMJQ7ZzQlgCIYgUOOVT0+dhlyKZS6bq0Gdf1WCt/tIGc4m56aD2h+EqZKv9aNZ3AtPbPUMDMEpHDva0ggYQg==} + matrix-bot-sdk@0.8.0: + resolution: {integrity: sha512-sCY5UvZfsZhJdCjSc8wZhGhIHOe5cSFSILxx9Zp5a/NEXtmQ6W/bIhefIk4zFAZXetFwXsgvKh1960k1hG5WDw==} engines: {node: '>=22.0.0'} - matrix-widget-api@1.16.0: - resolution: {integrity: sha512-OCsCzEN54jWamvWkBa7PqcKdlOhLA+nJbUyqsATHvzb4/NMcjdUZWSDurZxyNE5eYlNwxClA6Hw20mzJEKJbvg==} - mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -3519,6 +3700,9 @@ packages: memory-stream@1.0.0: resolution: {integrity: sha512-Wm13VcsPIMdG96dzILfij09PvuS3APtcKNh7M28FsCA/w6+1mjR7hhPmfFNoilX9xU7wTdhsH5lJAm6XNzdtww==} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -3527,6 +3711,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3547,10 +3735,18 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} @@ -3573,6 +3769,15 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + morgan@1.10.1: + resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} + engines: {node: '>= 0.8.0'} + mpg123-decoder@1.0.3: resolution: {integrity: sha512-+fjxnWigodWJm3+4pndi+KUg9TBojgn31DPk85zEsim7C6s0X5Ztc/hQYdytXkwuGXH+aB0/aEkG40Emukv6oQ==} @@ -3580,6 +3785,9 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3600,6 +3808,10 @@ packages: engines: {node: ^18 || >=20} hasBin: true + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -3616,6 +3828,11 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-downloader-helper@2.1.10: + resolution: {integrity: sha512-8LdieUd4Bqw/CzfZLf30h+1xSAq3riWSDfWKsPJYz8EULoWxjS1vw6BGLYFZDxQgXjDR7UmC9UpQ0oV93U98Fg==} + engines: {node: '>=14.18'} + hasBin: true + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3655,6 +3872,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3677,10 +3897,6 @@ packages: ogg-opus-decoder@1.7.3: resolution: {integrity: sha512-w47tiZpkLgdkpa+34VzYD8mHUj8I9kfWVZa82mBbNwDvB1byfLXSSzW/HxA4fI3e9kVlICSpXGFwMLV1LPdjwg==} - oidc-client-ts@3.4.1: - resolution: {integrity: sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==} - engines: {node: '>=18'} - ollama@0.6.3: resolution: {integrity: sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==} @@ -3688,10 +3904,18 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3769,10 +3993,6 @@ packages: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} - p-retry@7.1.1: - resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} - engines: {node: '>=20'} - p-timeout@3.2.0: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} engines: {node: '>=8'} @@ -3798,6 +4018,9 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + parse5-htmlparser2-tree-adapter@6.0.1: resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} @@ -3807,6 +4030,9 @@ packages: parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -3826,6 +4052,9 @@ packages: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -3836,6 +4065,12 @@ packages: resolution: {integrity: sha512-r1hWsSIGGmyYUAHR26zSXkxYWLXLMd6AwqcaFYG9YUZ0GBf5GvcjJSeo512tabM4GYFhxhl5pMCmPr7Q72Rq2Q==} engines: {node: '>=20.16.0 || >=22.3.0'} + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3847,6 +4082,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -3883,6 +4122,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postgres@3.4.8: + resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} + engines: {node: '>=12'} + pretty-bytes@6.1.1: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} @@ -3940,10 +4183,17 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + qified@0.6.0: resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==} engines: {node: '>=20'} @@ -3959,6 +4209,10 @@ 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==} @@ -3972,6 +4226,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} @@ -4006,6 +4264,24 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + request-promise-core@1.1.4: + resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==} + engines: {node: '>=0.10.0'} + peerDependencies: + request: ^2.34 + + request-promise@4.2.6: + resolution: {integrity: sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==} + engines: {node: '>=0.10.0'} + deprecated: request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142 + peerDependencies: + request: ^2.34 + + request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4067,19 +4343,29 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sdp-transform@3.0.0: - resolution: {integrity: sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==} - hasBin: true + sanitize-html@2.17.0: + resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} + + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -4205,6 +4491,11 @@ packages: sqlite-vec@0.1.7-alpha.2: resolution: {integrity: sha512-rNgRCv+4V4Ed3yc33Qr+nNmjhtrMnnHzXfLVPeGb28Dx5mmDL3Ngw/Wk8vhCGjj76+oC6gnkmMG8y73BZWGBwQ==} + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -4223,6 +4514,13 @@ packages: resolution: {integrity: sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ==} engines: {node: '>=16.0.0'} + stealthy-require@1.1.1: + resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} + engines: {node: '>=0.10.0'} + + steno@0.4.4: + resolution: {integrity: sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w==} + steno@4.0.2: resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} engines: {node: '>=18'} @@ -4344,6 +4642,10 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -4366,6 +4668,16 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -4409,9 +4721,6 @@ packages: resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} engines: {node: '>=20.18.1'} - unhomoglyph@1.0.6: - resolution: {integrity: sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==} - unicode-properties@1.4.1: resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} @@ -4432,6 +4741,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urijs@1.19.11: resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} @@ -4441,12 +4753,17 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true - uuid@13.0.0: - resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. hasBin: true uuid@8.3.2: @@ -4461,6 +4778,10 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5706,7 +6027,12 @@ snapshots: marked: 15.0.12 mime-types: 3.0.2 - '@matrix-org/matrix-sdk-crypto-wasm@16.0.0': {} + '@matrix-org/matrix-sdk-crypto-nodejs@0.4.0': + dependencies: + https-proxy-agent: 7.0.6 + node-downloader-helper: 2.1.10 + transitivePeerDependencies: + - supports-color '@microsoft/agents-activity@1.2.2': dependencies: @@ -6282,6 +6608,11 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.55.1': optional: true + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + '@silvia-odwyer/photon-node@0.3.4': {} '@sinclair/typebox@0.34.47': {} @@ -6716,7 +7047,12 @@ snapshots: '@types/estree@1.0.8': {} - '@types/events@3.0.3': {} + '@types/express-serve-static-core@4.19.8': + dependencies: + '@types/node': 25.0.9 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 '@types/express-serve-static-core@5.1.1': dependencies: @@ -6725,6 +7061,13 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 1.2.1 + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.8 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 @@ -6751,6 +7094,8 @@ snapshots: '@types/mime-types@2.1.4': {} + '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} '@types/node-fetch@2.6.13': @@ -6790,10 +7135,21 @@ snapshots: '@types/retry@0.12.5': {} + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 25.0.9 + '@types/send@1.2.1': dependencies: '@types/node': 25.0.9 + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 25.0.9 + '@types/send': 0.17.6 + '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 @@ -6954,6 +7310,11 @@ snapshots: dependencies: event-target-shim: 5.0.1 + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -6969,6 +7330,13 @@ snapshots: optionalDependencies: ajv: 8.17.1 + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -7027,6 +7395,14 @@ snapshots: array-back@6.2.2: {} + array-flatten@1.1.1: {} + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-plus@1.0.0: {} + assertion-error@2.0.1: {} ast-v8-to-istanbul@0.3.10: @@ -7035,6 +7411,8 @@ snapshots: estree-walker: 3.0.3 js-tokens: 9.0.1 + async-lock@1.4.1: {} + async-mutex@0.5.0: dependencies: tslib: 2.8.1 @@ -7066,6 +7444,10 @@ snapshots: audio-type@2.2.1: optional: true + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + axios@1.13.2(debug@4.4.3): dependencies: follow-redirects: 1.15.11(debug@4.4.3) @@ -7078,10 +7460,16 @@ snapshots: balanced-match@3.0.1: {} - base-x@5.0.1: {} - base64-js@1.5.1: {} + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + before-after-hook@4.0.0: optional: true @@ -7089,6 +7477,25 @@ snapshots: binary-extensions@2.3.0: {} + bluebird@3.7.2: {} + + body-parser@1.20.4: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -7123,10 +7530,6 @@ snapshots: browser-or-node@3.0.0: {} - bs58@6.0.0: - dependencies: - base-x: 5.0.1 - buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -7161,6 +7564,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + caseless@0.12.0: {} + chai@6.2.2: {} chalk-template@0.4.0: @@ -7300,14 +7705,22 @@ snapshots: console-control-strings@1.1.0: optional: true + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + content-disposition@1.0.1: {} content-type@1.0.5: {} + cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} + core-util-is@1.0.2: {} + core-util-is@1.0.3: {} croner@9.1.0: {} @@ -7338,8 +7751,16 @@ snapshots: curve25519-js@0.0.4: {} + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + data-uri-to-buffer@4.0.1: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -7347,6 +7768,8 @@ snapshots: deep-extend@0.6.0: optional: true + deepmerge@4.3.1: {} + delayed-stream@1.0.0: {} delegates@1.0.0: @@ -7354,6 +7777,8 @@ snapshots: depd@2.0.0: {} + destroy@1.2.0: {} + detect-libc@2.1.2: {} devtools-protocol@0.0.1561482: {} @@ -7400,6 +7825,11 @@ snapshots: eastasianwidth@0.2.0: {} + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -7472,6 +7902,8 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -7488,6 +7920,42 @@ snapshots: expect-type@1.3.0: {} + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + express@5.2.1: dependencies: accepts: 2.0.0 @@ -7523,6 +7991,8 @@ snapshots: extend@3.0.2: {} + extsprintf@1.3.0: {} + fast-content-type-parse@3.0.0: optional: true @@ -7536,6 +8006,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + fast-uri@3.1.0: {} fast-xml-parser@5.2.5: @@ -7576,6 +8048,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -7602,8 +8086,16 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forever-agent@0.6.1: {} + form-data-encoder@1.7.2: {} + form-data@2.3.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -7623,6 +8115,8 @@ snapshots: forwarded@0.2.0: {} + fresh@0.5.2: {} + fresh@2.0.0: {} fs-extra@11.3.3: @@ -7695,10 +8189,16 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 + glob-to-regexp@0.4.1: {} + glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -7752,6 +8252,13 @@ snapshots: transitivePeerDependencies: - supports-color + har-schema@2.0.0: {} + + har-validator@5.1.5: + dependencies: + ajv: 6.12.6 + har-schema: 2.0.0 + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -7763,6 +8270,11 @@ snapshots: has-unicode@2.0.1: optional: true + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + hashery@1.4.0: dependencies: hookified: 1.15.0 @@ -7785,6 +8297,16 @@ snapshots: html-parse-string@0.0.9: {} + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + + htmlencode@0.0.4: {} + htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 @@ -7792,6 +8314,13 @@ snapshots: domutils: 3.2.2 entities: 6.0.1 + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -7807,6 +8336,12 @@ snapshots: transitivePeerDependencies: - supports-color + http-signature@1.2.0: + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.18.0 + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -7818,6 +8353,10 @@ snapshots: dependencies: ms: 2.1.3 + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -7883,14 +8422,18 @@ snapshots: is-interactive@2.0.0: optional: true - is-network-error@1.3.0: {} - is-number@7.0.0: {} + is-plain-object@5.0.0: {} + + is-promise@2.2.2: {} + is-promise@4.0.0: {} is-stream@2.0.1: {} + is-typedarray@1.0.0: {} + is-unicode-supported@1.3.0: optional: true @@ -7906,6 +8449,8 @@ snapshots: isexe@3.1.1: optional: true + isstream@0.1.2: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -7937,6 +8482,8 @@ snapshots: js-tokens@9.0.1: {} + jsbn@0.1.1: {} + json-bigint@1.0.0: dependencies: bignumber.js: 9.3.1 @@ -7948,8 +8495,14 @@ snapshots: '@babel/runtime': 7.28.6 ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + + json-stringify-safe@5.0.1: {} + json5@2.2.3: {} jsonc-parser@3.3.1: {} @@ -7974,6 +8527,13 @@ snapshots: ms: 2.1.3 semver: 7.7.3 + jsprim@1.4.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + jszip@3.10.1: dependencies: lie: 3.3.0 @@ -8002,8 +8562,6 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 - jwt-decode@4.0.0: {} - katex@0.16.27: dependencies: commander: 8.3.0 @@ -8012,6 +8570,8 @@ snapshots: dependencies: '@keyv/serialize': 1.1.1 + leac@0.6.0: {} + lie@3.3.0: dependencies: immediate: 3.0.6 @@ -8137,12 +8697,18 @@ snapshots: yoctocolors: 2.1.2 optional: true - loglevel@1.9.2: {} - long@4.0.0: {} long@5.3.2: {} + lowdb@1.0.0: + dependencies: + graceful-fs: 4.2.11 + is-promise: 2.2.2 + lodash: 4.17.21 + pify: 3.0.0 + steno: 0.4.4 + lowdb@7.0.1: dependencies: steno: 4.0.2 @@ -8196,32 +8762,33 @@ snapshots: math-intrinsics@1.1.0: {} - matrix-events-sdk@0.0.1: {} - - matrix-js-sdk@40.0.0: + matrix-bot-sdk@0.8.0: dependencies: - '@babel/runtime': 7.28.6 - '@matrix-org/matrix-sdk-crypto-wasm': 16.0.0 + '@matrix-org/matrix-sdk-crypto-nodejs': 0.4.0 + '@types/express': 4.17.25 another-json: 0.2.0 - bs58: 6.0.0 - content-type: 1.0.5 - jwt-decode: 4.0.0 - loglevel: 1.9.2 - matrix-events-sdk: 0.0.1 - matrix-widget-api: 1.16.0 - oidc-client-ts: 3.4.1 - p-retry: 7.1.1 - sdp-transform: 3.0.0 - unhomoglyph: 1.0.6 - uuid: 13.0.0 - - matrix-widget-api@1.16.0: - dependencies: - '@types/events': 3.0.3 - events: 3.3.0 + async-lock: 1.4.1 + chalk: 4.1.2 + express: 4.22.1 + glob-to-regexp: 0.4.1 + hash.js: 1.1.7 + html-to-text: 9.0.5 + htmlencode: 0.0.4 + lowdb: 1.0.0 + lru-cache: 10.4.3 + mkdirp: 3.0.1 + morgan: 1.10.1 + postgres: 3.4.8 + request: 2.88.2 + request-promise: 4.2.6(request@2.88.2) + sanitize-html: 2.17.0 + transitivePeerDependencies: + - supports-color mdurl@2.0.0: {} + media-typer@0.3.0: {} + media-typer@1.1.0: {} memory-stream@1.0.0: @@ -8229,10 +8796,14 @@ snapshots: readable-stream: 3.6.2 optional: true + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} merge2@1.4.1: {} + methods@1.1.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -8250,9 +8821,13 @@ snapshots: dependencies: mime-db: 1.54.0 + mime@1.6.0: {} + mimic-function@5.0.1: optional: true + minimalistic-assert@1.0.1: {} + minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -8272,6 +8847,18 @@ snapshots: mitt@3.0.1: {} + mkdirp@3.0.1: {} + + morgan@1.10.1: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.1.0 + transitivePeerDependencies: + - supports-color + mpg123-decoder@1.0.3: dependencies: '@wasm-audio-decoders/common': 9.0.7 @@ -8279,6 +8866,8 @@ snapshots: mrmime@2.0.1: {} + ms@2.0.0: {} + ms@2.1.3: {} music-metadata@11.10.6: @@ -8307,6 +8896,8 @@ snapshots: nanoid@5.1.6: optional: true + negotiator@0.6.3: {} + negotiator@1.0.0: {} node-addon-api@8.5.0: @@ -8317,6 +8908,8 @@ snapshots: node-domexception@1.0.0: {} + node-downloader-helper@2.1.10: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -8394,6 +8987,8 @@ snapshots: dependencies: boolbase: 1.0.0 + oauth-sign@0.9.0: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -8425,20 +9020,22 @@ snapshots: opus-decoder: 0.7.11 optional: true - oidc-client-ts@3.4.1: - dependencies: - jwt-decode: 4.0.0 - ollama@0.6.3: dependencies: whatwg-fetch: 3.6.20 on-exit-leak-free@2.1.2: {} + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + on-finished@2.4.1: dependencies: ee-first: 1.1.1 + on-headers@1.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -8539,10 +9136,6 @@ snapshots: '@types/retry': 0.12.0 retry: 0.13.1 - p-retry@7.1.1: - dependencies: - is-network-error: 1.3.0 - p-timeout@3.2.0: dependencies: p-finally: 1.0.0 @@ -8561,6 +9154,8 @@ snapshots: parse-ms@4.0.0: optional: true + parse-srcset@1.0.2: {} + parse5-htmlparser2-tree-adapter@6.0.1: dependencies: parse5: 6.0.1 @@ -8569,6 +9164,11 @@ snapshots: parse5@6.0.1: {} + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + parseurl@1.3.3: {} partial-json@0.1.7: {} @@ -8585,6 +9185,8 @@ snapshots: lru-cache: 11.2.4 minipass: 7.1.2 + path-to-regexp@0.1.12: {} + path-to-regexp@8.3.0: {} pathe@2.0.3: {} @@ -8593,12 +9195,18 @@ snapshots: optionalDependencies: '@napi-rs/canvas': 0.1.88 + peberminta@0.9.0: {} + + performance-now@2.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} picomatch@4.0.3: {} + pify@3.0.0: {} + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -8641,6 +9249,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres@3.4.8: {} + pretty-bytes@6.1.1: optional: true @@ -8707,8 +9317,14 @@ snapshots: proxy-from-env@1.1.0: {} + psl@1.15.0: + dependencies: + punycode: 2.3.1 + punycode.js@2.3.1: {} + punycode@2.3.1: {} + qified@0.6.0: dependencies: hookified: 1.15.0 @@ -8724,6 +9340,8 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.5.3: {} + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -8749,6 +9367,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -8799,6 +9424,42 @@ snapshots: reflect-metadata@0.2.2: {} + request-promise-core@1.1.4(request@2.88.2): + dependencies: + lodash: 4.17.21 + request: 2.88.2 + + request-promise@4.2.6(request@2.88.2): + dependencies: + bluebird: 3.7.2 + request: 2.88.2 + request-promise-core: 1.1.4(request@2.88.2) + stealthy-require: 1.1.1 + tough-cookie: 2.5.0 + + request@2.88.2: + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.3 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -8893,10 +9554,39 @@ snapshots: safer-buffer@2.1.2: {} - sdp-transform@3.0.0: {} + sanitize-html@2.17.0: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.5.6 + + selderee@0.11.0: + dependencies: + parseley: 0.12.1 semver@7.7.3: {} + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + send@1.2.1: dependencies: debug: 4.4.3 @@ -8913,6 +9603,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -9073,6 +9772,18 @@ snapshots: sqlite-vec-linux-x64: 0.1.7-alpha.2 sqlite-vec-windows-x64: 0.1.7-alpha.2 + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + stackback@0.0.2: {} statuses@2.0.2: {} @@ -9090,6 +9801,12 @@ snapshots: strip-ansi: 7.1.2 optional: true + stealthy-require@1.1.1: {} + + steno@0.4.4: + dependencies: + graceful-fs: 4.2.11 + steno@4.0.2: optional: true @@ -9208,6 +9925,11 @@ snapshots: totalist@3.0.1: {} + tough-cookie@2.5.0: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + tr46@0.0.3: {} ts-algebra@2.0.0: {} @@ -9225,6 +9947,17 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tweetnacl@0.14.5: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + type-is@2.0.1: dependencies: content-type: 1.0.5 @@ -9255,8 +9988,6 @@ snapshots: undici@7.18.2: {} - unhomoglyph@1.0.6: {} - unicode-properties@1.4.1: dependencies: base64-js: 1.5.1 @@ -9278,6 +10009,10 @@ snapshots: unpipe@1.0.0: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + urijs@1.19.11: {} url-join@4.0.1: @@ -9285,9 +10020,11 @@ snapshots: util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} + uuid@11.1.0: {} - uuid@13.0.0: {} + uuid@3.4.0: {} uuid@8.3.2: {} @@ -9296,6 +10033,12 @@ snapshots: vary@1.1.2: {} + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 diff --git a/src/agents/shell-utils.ts b/src/agents/shell-utils.ts index 6974d9a2a..6d4efac59 100644 --- a/src/agents/shell-utils.ts +++ b/src/agents/shell-utils.ts @@ -1,6 +1,6 @@ +import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; -import { spawn } from "node:child_process"; function resolvePowerShellPath(): string { const systemRoot = process.env.SystemRoot || process.env.WINDIR; diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 0956f31d7..c184db128 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -104,6 +104,8 @@ export { resolveMentionGatingWithBypass, } from "../channels/mention-gating.js"; export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js"; +export type { NormalizedLocation } from "../channels/location.js"; +export { formatLocationText, toLocationContext } from "../channels/location.js"; export { resolveDiscordGroupRequireMention, resolveIMessageGroupRequireMention, diff --git a/src/process/command-queue.ts b/src/process/command-queue.ts index 8e4e7377d..564ec5213 100644 --- a/src/process/command-queue.ts +++ b/src/process/command-queue.ts @@ -112,7 +112,8 @@ export function enqueueCommand( } export function getQueueSize(lane: string = CommandLane.Main) { - const state = lanes.get(lane); + const resolved = lane.trim() || CommandLane.Main; + const state = lanes.get(resolved); if (!state) return 0; return state.queue.length + state.active; }