diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cc2511..3028730 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,20 +17,25 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.30.3 + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20 - cache: npm + node-version: 22 + cache: pnpm - name: Install dependencies - run: npm ci + run: pnpm install --frozen-lockfile - name: Typecheck - run: npm run typecheck + run: pnpm typecheck - name: Build - run: npm run build + run: pnpm build - name: Test - run: npm test + run: pnpm test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b56ef7a..8cfc18d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,24 +20,29 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.30.3 + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20 - cache: npm + node-version: 22 + cache: pnpm registry-url: https://registry.npmjs.org - name: Install dependencies - run: npm ci + run: pnpm install --frozen-lockfile - name: Typecheck - run: npm run typecheck + run: pnpm typecheck - name: Build - run: npm run build + run: pnpm build - name: Test - run: npm test + run: pnpm test - name: Verify tag matches package version if: startsWith(github.ref, 'refs/tags/v') @@ -50,6 +55,6 @@ jobs: fi - name: Publish to npm - run: npm publish --provenance --access public + run: pnpm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 69ac51b..f6fb0d9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ coverage **/.env **/.env.* !**/.env.example +.claude/settings.local.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 621a0db..3d53aa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.2.0 + +### New + +- `openDM(userId)` for DM reuse/create +- `fetchMessage(threadId, messageId)` for single-message fetch +- `fetchChannelMessages(channelId, options)` for top-level channel history + +### Changes + +- `fetchMessages` and `listThreads` use server-backed pagination +- Bump `chat` SDK to 4.15.0 +- Clean up event listeners on shutdown +- Guard `decodeRecoveryKey` against malformed keys + ## 0.1.0 - Initial release. diff --git a/README.md b/README.md index 48b4cd1..a27cd02 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,247 @@ # @beeper/chat-adapter-matrix -Matrix adapter for [Chat SDK](https://chat-sdk.dev/docs). Uses Matrix sync (no webhook server required). - -If you are using Beeper, you can use Chat SDK with your Beeper Cloud accounts and Matrix chats. This lets you use Chat SDK with WhatsApp, Telegram, Instagram, Signal, X Chat, and more. For bridged chats, we recommend personal usage, since some networks may limit automated activity. +Matrix adapter for [Chat SDK](https://chat-sdk.dev/docs). It runs over Matrix sync instead of webhooks and works with Beeper conversations and bridged networks such as WhatsApp, Telegram, Instagram, and Signal. ## Installation +Requires Node.js `>=22`. + ```bash -npm install chat @beeper/chat-adapter-matrix matrix-js-sdk +pnpm add chat @beeper/chat-adapter-matrix ``` ## Usage +`createMatrixAdapter()` reads its config from environment variables when called without arguments. + ```ts import { Chat } from "chat"; import { createMemoryState } from "@chat-adapter/state-memory"; import { createMatrixAdapter } from "@beeper/chat-adapter-matrix"; +const matrix = createMatrixAdapter(); + const bot = new Chat({ - userName: "beeper-bot", + userName: process.env.MATRIX_BOT_USERNAME ?? "beeper-bot", state: createMemoryState(), - adapters: { - matrix: createMatrixAdapter({ - baseURL: process.env.MATRIX_BASE_URL!, - auth: { - type: "accessToken", - accessToken: process.env.MATRIX_ACCESS_TOKEN!, - }, - recoveryKey: process.env.MATRIX_RECOVERY_KEY, - }), - }, + adapters: { matrix }, }); bot.onNewMention(async (thread, message) => { await thread.subscribe(); - await thread.post(`Hi ${message.author.userName}`); + await thread.post(`Hi ${message.author.userName}. Mention me or run /ping.`); }); + +bot.onSlashCommand("/ping", async (event) => { + await event.channel.post("pong"); +}); + +await bot.initialize(); ``` -## Auth +Chat SDK concepts such as `Chat`, `Thread`, `Message`, subscriptions, and handlers work the same here. See the upstream docs for the core API: + +- [Getting Started](https://chat-sdk.dev/docs/getting-started) +- [Usage](https://chat-sdk.dev/docs/usage) +- [Threads, Messages, and Channels](https://chat-sdk.dev/docs/threads-messages-channels) +- [Direct Messages](https://chat-sdk.dev/docs/direct-messages) -Access token: +## Authentication + +### Access token ```ts -auth: { type: "accessToken", accessToken: process.env.MATRIX_ACCESS_TOKEN! }; +createMatrixAdapter({ + baseURL: process.env.MATRIX_BASE_URL!, + auth: { + type: "accessToken", + accessToken: process.env.MATRIX_ACCESS_TOKEN!, + userID: process.env.MATRIX_USER_ID, + }, +}); ``` -Username/password: +### Username/password ```ts -auth: { - type: "password", - username: process.env.MATRIX_USERNAME!, - password: process.env.MATRIX_PASSWORD!, -}; +createMatrixAdapter({ + baseURL: process.env.MATRIX_BASE_URL!, + auth: { + type: "password", + username: process.env.MATRIX_USERNAME!, + password: process.env.MATRIX_PASSWORD!, + userID: process.env.MATRIX_USER_ID, + }, +}); ``` -## Environment +## Defaults -Required: +- Persistence behavior is active whenever Chat provides a `state` adapter. +- Redis or another durable state adapter is recommended for restart durability. +- `deviceID` is inferred from auth when possible, then reused from state, and only generated as a last resort. +- `recoveryKey` enables E2EE. +- `inviteAutoJoin: {}` enables invite auto-join. -- `MATRIX_BASE_URL` -- Access token mode: `MATRIX_ACCESS_TOKEN` -- Password mode: `MATRIX_USERNAME`, `MATRIX_PASSWORD` +## Common Options -Common optional: - -- `MATRIX_USER_ID` -- `MATRIX_DEVICE_ID` -- `MATRIX_RECOVERY_KEY` +```ts +createMatrixAdapter({ + baseURL: process.env.MATRIX_BASE_URL!, + auth: { + type: "accessToken", + accessToken: process.env.MATRIX_ACCESS_TOKEN!, + userID: process.env.MATRIX_USER_ID, + }, + recoveryKey: process.env.MATRIX_RECOVERY_KEY, + commandPrefix: "/", + roomAllowlist: ["!room:beeper.com"], + inviteAutoJoin: { + inviterAllowlist: ["@alice:beeper.com", "@ops:beeper.com"], + }, + matrixSDKLogLevel: "error", +}); +``` -Advanced optional (only if needed): device ID persistence keys, E2EE storage settings, session settings, and `MATRIX_SDK_LOG_LEVEL`. +Advanced tuning stays in code config: -## Examples +```ts +createMatrixAdapter({ + baseURL: process.env.MATRIX_BASE_URL!, + auth: { + type: "accessToken", + accessToken: process.env.MATRIX_ACCESS_TOKEN!, + userID: process.env.MATRIX_USER_ID, + }, + e2ee: { + useIndexedDB: false, + cryptoDatabasePrefix: "beeper-matrix-bot", + }, + persistence: { + keyPrefix: "my-bot", + session: { + ttlMs: 86_400_000, + }, + sync: { + persistIntervalMs: 10_000, + }, + }, +}); +``` -Copy [`examples/.env.example`](./examples/.env.example) to `examples/.env`, then run: +## Environment Variables + +`createMatrixAdapter()` with no arguments uses only these env vars: + +| Variable | Required | Description | +|----------|----------|-------------| +| `MATRIX_BASE_URL` | Yes | Matrix homeserver base URL | +| `MATRIX_ACCESS_TOKEN` | Yes* | Access token for access-token auth | +| `MATRIX_USERNAME` | Yes* | Username for password auth | +| `MATRIX_PASSWORD` | Yes* | Password for password auth | +| `MATRIX_USER_ID` | No | User ID hint | +| `MATRIX_DEVICE_ID` | No | Explicit device ID override | +| `MATRIX_RECOVERY_KEY` | No | Enables E2EE and key-backup bootstrap | +| `MATRIX_BOT_USERNAME` | No | Mention-detection username | +| `MATRIX_COMMAND_PREFIX` | No | Slash command prefix. Defaults to `/` | +| `MATRIX_INVITE_AUTOJOIN` | No | Enable invite auto-join | +| `MATRIX_INVITE_AUTOJOIN_ALLOWLIST` | No | Comma-separated Matrix user IDs allowed to invite the bot | +| `MATRIX_SDK_LOG_LEVEL` | No | Matrix SDK log level | + +\*Use either `MATRIX_ACCESS_TOKEN`, or `MATRIX_USERNAME` plus `MATRIX_PASSWORD`. + +## Thread Model + +- A Matrix room is a Chat SDK channel. +- Top-level room messages belong to the channel timeline. +- Matrix threaded replies map to Chat SDK threads using `roomID + rootEventID`. +- `openDM(userId)` reuses existing direct rooms when possible and creates one when needed. + +## Features + +| Feature | Supported | +|---------|-----------| +| Mentions | Yes | +| Rich text | Yes, via Matrix `formatted_body` and `m.mentions` | +| Thread replies | Yes | +| Reactions (add/remove) | Yes | +| Message edits | Yes | +| Message deletes | Yes | +| Typing indicator | Yes | +| Direct messages | Yes | +| File uploads | Yes | +| Message history | Yes | +| Channel and thread metadata | Yes | +| E2EE | Yes | +| Invite auto-join | Yes | +| Slash commands | Prefix-parsed from message text | +| Webhooks | No, this adapter uses sync polling | +| Cards | No | +| Modals | No | +| Ephemeral messages | No | +| Native streaming | No | + +## Persistence + +For production, pair the adapter with a durable Chat state adapter such as Redis. -```bash -npm run example:bun -``` +```ts +import { Chat } from "chat"; +import { createRedisState } from "@chat-adapter/state-redis"; +import { createMatrixAdapter } from "@beeper/chat-adapter-matrix"; -Generate a Beeper access token interactively: +const matrix = createMatrixAdapter({ + baseURL: process.env.MATRIX_BASE_URL!, + auth: { + type: "accessToken", + accessToken: process.env.MATRIX_ACCESS_TOKEN!, + userID: process.env.MATRIX_USER_ID, + }, + recoveryKey: process.env.MATRIX_RECOVERY_KEY, +}); -```bash -npm run token:bun +const bot = new Chat({ + userName: process.env.MATRIX_BOT_USERNAME ?? "beeper-bot", + state: createRedisState({ url: process.env.REDIS_URL! }), + adapters: { matrix }, +}); ``` -## Get a Beeper Access Token +Persistence covers: -Use the interactive helper: +- generated or inferred device IDs +- password login sessions +- DM room mappings +- Matrix sync snapshots +- E2EE secrets bundles when E2EE is enabled -```bash -npm run token:bun -``` +## Message and History APIs -It prints: +The adapter supports: -- `MATRIX_BASE_URL` -- `MATRIX_ACCESS_TOKEN` -- `MATRIX_USER_ID` -- `MATRIX_DEVICE_ID` +- `fetchMessage(threadId, messageId)` +- `fetchMessages(threadId, options)` +- `fetchChannelMessages(channelId, options)` +- `fetchThread(threadId)` +- `fetchChannelInfo(channelId)` +- `listThreads(channelId, options)` +- `openDM(userId)` -Paste those values into `examples/.env` or your deployment secrets. +## Limitations -or: +- `handleWebhook()` returns `501` by design because Matrix uses sync polling here. +- Cards, modals, and ephemeral messages are not implemented. +- Native streaming is not implemented at the adapter layer. +- Slash commands are parsed from plain text messages; Matrix does not provide native slash command events. -```bash -bun --env-file=examples/.env run examples/bot.ts -``` +## Examples -## Notes +- [`examples/bot.ts`](./examples/bot.ts) uses in-memory state for local development. +- [`examples/bot.redis.ts`](./examples/bot.redis.ts) uses Redis-backed state for restart durability. +- [`examples/.env.example`](./examples/.env.example) lists the supported env vars for the examples. +- [`scripts/get-access-token.ts`](./scripts/get-access-token.ts) helps generate Beeper credentials interactively. -- `handleWebhook()` returns `501` by design. -- Access-token auth resolves identity with `whoami`. -- Password auth sends configured `device_id` during login. -- Use Redis state in production for stable sessions and device IDs. +For release-specific changes, see [CHANGELOG.md](./CHANGELOG.md). ## License diff --git a/e2e/.env.example b/e2e/.env.example new file mode 100644 index 0000000..e740b90 --- /dev/null +++ b/e2e/.env.example @@ -0,0 +1,13 @@ +# Both required +E2E_BASE_URL=https://matrix.example.com + +# Bot account bootstrap for "new device" login +E2E_BOT_LOGIN_TOKEN=... +E2E_BOT_RECOVERY_KEY=EsTc... + +# Sender account bootstrap for "new device" login +E2E_SENDER_LOGIN_TOKEN=... +E2E_SENDER_RECOVERY_KEY=EsTc... + +# Optional: use pre-existing room instead of creating one +# E2E_ROOM_ID=!abc:example.com diff --git a/e2e/e2e.test.ts b/e2e/e2e.test.ts new file mode 100644 index 0000000..da5670a --- /dev/null +++ b/e2e/e2e.test.ts @@ -0,0 +1,801 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { stringifyMarkdown, type Message } from "chat"; +import { EventType, RoomEvent, RoomMemberEvent, type MatrixEvent } from "matrix-js-sdk"; +import { + createIsolatedRoom, + createParticipant, + createParticipantFromSession, + env, + type E2EParticipant, + getOrCreateRoom, + nonce, + shutdownParticipant, + sleep, + waitForEvent, + waitForEncryptedRoom, + waitForFetchedMessage, + waitForJoinedMemberCount, + waitForMatchingMessage, + waitForRoom, +} from "./helpers"; + +const hasCoreCredentials = Boolean( + process.env.E2E_BASE_URL && + process.env.E2E_BOT_LOGIN_TOKEN && + process.env.E2E_SENDER_LOGIN_TOKEN +); + +const hasRecoveryCredentials = Boolean( + process.env.E2E_BOT_RECOVERY_KEY && process.env.E2E_SENDER_RECOVERY_KEY +); + +describe.skipIf(!hasCoreCredentials)("E2E Matrix Adapter", () => { + let bot: E2EParticipant; + let sender: E2EParticipant; + let roomID: string; + + beforeAll(async () => { + [bot, sender] = await Promise.all([ + createParticipant({ + name: "e2e-bot", + loginToken: env.botLoginToken, + recoveryKey: env.botRecoveryKey, + }), + createParticipant({ + name: "e2e-sender", + loginToken: env.senderLoginToken, + recoveryKey: env.senderRecoveryKey, + }), + ]); + + roomID = await getOrCreateRoom(bot.matrixClient, sender.userID); + await Promise.all([ + waitForEncryptedRoom(bot.matrixClient, roomID, 30_000), + waitForEncryptedRoom(sender.matrixClient, roomID, 30_000), + waitForJoinedMemberCount(bot.matrixClient, roomID, 2, 30_000), + waitForJoinedMemberCount(sender.matrixClient, roomID, 2, 30_000), + ]); + + const sharedThreadId = bot.adapter.encodeThreadId({ roomID }); + const botWarmupTag = `e2e-warmup-bot-${nonce()}`; + const botWarmup = await bot.adapter.postMessage(sharedThreadId, botWarmupTag); + await waitForFetchedMessage( + sender.adapter, + sender.adapter.encodeThreadId({ roomID }), + botWarmup.id, + (message) => message.text.includes(botWarmupTag), + 45_000 + ); + + const senderWarmupTag = `e2e-warmup-sender-${nonce()}`; + const senderWarmup = await sender.adapter.postMessage( + sender.adapter.encodeThreadId({ roomID }), + senderWarmupTag + ); + await waitForFetchedMessage( + bot.adapter, + sharedThreadId, + senderWarmup.id, + (message) => message.text.includes(senderWarmupTag), + 45_000 + ); + + await sleep(1_000); + }); + + afterAll(async () => { + const shutdowns = [bot ? shutdownParticipant(bot) : undefined, sender ? shutdownParticipant(sender) : undefined].filter( + (value): value is Promise => Boolean(value) + ); + await Promise.all(shutdowns); + }); + + it("bot receives text message from sender", async () => { + const tag = `e2e-text-${nonce()}`; + const threadId = sender.adapter.encodeThreadId({ roomID }); + const posted = await sender.adapter.postMessage(threadId, `hello ${tag}`); + const message = await waitForFetchedMessage( + bot.adapter, + bot.adapter.encodeThreadId({ roomID }), + posted.id, + (candidate) => candidate.text.includes(tag) + ); + + expect(message.text).toContain(tag); + expect(message.author.userId).toBe(sender.userID); + expect(message.raw.isEncrypted()).toBe(true); + expect(message.raw.getWireType()).toBe(EventType.RoomMessageEncrypted); + expect(message.raw.getRoomId()).toBe(roomID); + }); + + it("bot posts a message visible to sender", async () => { + const tag = `e2e-post-${nonce()}`; + const threadId = bot.adapter.encodeThreadId({ roomID }); + const posted = await bot.adapter.postMessage(threadId, `bot says ${tag}`); + const message = await waitForFetchedMessage( + sender.adapter, + sender.adapter.encodeThreadId({ roomID }), + posted.id, + (candidate) => candidate.text.includes(tag) + ); + expect(message.text).toContain(tag); + expect(message.author.userId).toBe(bot.userID); + }); + + it("preserves rich text and mention semantics across encrypted delivery", async () => { + const tag = `e2e-format-${nonce()}`; + const localpart = bot.userID.slice(1).split(":")[0]; + const threadId = sender.adapter.encodeThreadId({ roomID }); + const posted = await sender.adapter.postMessage(threadId, { + markdown: `Hello **${tag}** <@${bot.userID}>`, + }); + const message = await waitForFetchedMessage( + bot.adapter, + bot.adapter.encodeThreadId({ roomID }), + posted.id, + (candidate) => candidate.text.includes(tag) && candidate.isMention === true, + 45_000 + ); + + expect(message.text).toContain(`Hello ${tag} @${localpart}`); + expect(message.isMention).toBe(true); + expect(stringifyMarkdown(message.formatted).trim()).toContain(`**${tag}**`); + expect(stringifyMarkdown(message.formatted)).toContain(`@${localpart}`); + }); + + it("thread round-trip: sender creates thread, bot replies in it", async () => { + const rootTag = `e2e-thread-root-${nonce()}`; + const replyTag = `e2e-thread-reply-${nonce()}`; + const threadId = sender.adapter.encodeThreadId({ roomID }); + + // Sender sends root message + const rootPosted = await sender.adapter.postMessage( + threadId, + `Thread root ${rootTag}` + ); + const rootEventId = rootPosted.id; + await waitForFetchedMessage( + bot.adapter, + bot.adapter.encodeThreadId({ roomID }), + rootEventId, + (message) => message.text.includes(rootTag) + ); + + // Sender sends a threaded reply + const threadReplyTag = `e2e-thread-child-${nonce()}`; + const senderThreadId = sender.adapter.encodeThreadId({ + roomID, + rootEventID: rootEventId, + }); + await sender.adapter.postMessage( + senderThreadId, + `Thread reply ${threadReplyTag}` + ); + + const childThreadID = bot.adapter.encodeThreadId({ + roomID, + rootEventID: rootEventId, + }); + await waitForMatchingMessage( + bot.adapter, + childThreadID, + (message) => message.text.includes(threadReplyTag) + ); + const decoded = bot.adapter.decodeThreadId(childThreadID); + expect(decoded.roomID).toBe(roomID); + expect(decoded.rootEventID).toBe(rootEventId); + + // Bot replies in the same thread + const replyPosted = await bot.adapter.postMessage( + childThreadID, + `Bot thread reply ${replyTag}` + ); + const replyMessage = await waitForFetchedMessage( + sender.adapter, + senderThreadId, + replyPosted.id, + (message) => message.text.includes(replyTag) + ); + expect(replyMessage.text).toContain(replyTag); + }); + + it("reaction round-trip", async () => { + const tag = `e2e-react-${nonce()}`; + const threadId = bot.adapter.encodeThreadId({ roomID }); + + // Bot sends a message for both sides to react to + const posted = await bot.adapter.postMessage(threadId, `React target ${tag}`); + const messageId = posted.id; + + await sleep(500); + + // Bot adds reaction — sender should see it + const senderSeesReaction = waitForEvent<{ rawEmoji: string; added: boolean }>( + (cb) => { + sender.onReaction((data) => { + if (data.messageId === messageId && data.added) { + cb({ rawEmoji: data.rawEmoji, added: data.added }); + } + }); + return () => sender.onReaction(null); + } + ); + + await bot.adapter.addReaction(threadId, messageId, "👍"); + + const senderReaction = await senderSeesReaction; + expect(senderReaction.rawEmoji).toBe("👍"); + expect(senderReaction.added).toBe(true); + + // Sender adds reaction — bot should receive via onReaction + const botSeesReaction = waitForEvent<{ rawEmoji: string; added: boolean }>( + (cb) => { + bot.onReaction((data) => { + if (data.messageId === messageId && data.added) { + cb({ rawEmoji: data.rawEmoji, added: data.added }); + } + }); + return () => bot.onReaction(null); + } + ); + + const senderThreadId = sender.adapter.encodeThreadId({ roomID }); + await sender.adapter.addReaction(senderThreadId, messageId, "🎉"); + + const botReaction = await botSeesReaction; + expect(botReaction.rawEmoji).toBe("🎉"); + expect(botReaction.added).toBe(true); + }); + + it("reaction removal round-trip", async () => { + const tag = `e2e-unreact-${nonce()}`; + const threadId = bot.adapter.encodeThreadId({ roomID }); + const posted = await bot.adapter.postMessage( + threadId, + `Reaction remove target ${tag}` + ); + const messageId = posted.id; + + const senderSawAdd = waitForEvent<{ rawEmoji: string; added: boolean }>((cb) => { + sender.onReaction((data) => { + if (data.messageId === messageId && data.rawEmoji === "🔥" && data.added) { + cb({ rawEmoji: data.rawEmoji, added: data.added }); + } + }); + return () => sender.onReaction(null); + }); + + await bot.adapter.addReaction(threadId, messageId, "🔥"); + await senderSawAdd; + + const senderSawRemoval = waitForEvent<{ rawEmoji: string; added: boolean }>((cb) => { + sender.onReaction((data) => { + if (data.messageId === messageId && data.rawEmoji === "🔥" && !data.added) { + cb({ rawEmoji: data.rawEmoji, added: data.added }); + } + }); + return () => sender.onReaction(null); + }); + + await bot.adapter.removeReaction(threadId, messageId, "🔥"); + + const removal = await senderSawRemoval; + expect(removal.rawEmoji).toBe("🔥"); + expect(removal.added).toBe(false); + }); + + it("edit round-trip: bot sends and edits, sender sees edited content", async () => { + const tag = `e2e-edit-${nonce()}`; + const editedTag = `e2e-edited-${nonce()}`; + const threadId = bot.adapter.encodeThreadId({ roomID }); + // Bot sends original + const posted = await bot.adapter.postMessage(threadId, `Original ${tag}`); + const messageId = posted.id; + await waitForFetchedMessage( + sender.adapter, + sender.adapter.encodeThreadId({ roomID }), + messageId, + (message) => message.text.includes(tag) + ); + + // Bot edits the message + await bot.adapter.editMessage(threadId, messageId, { + markdown: `Edited **${editedTag}**`, + }); + const editedMessage = await waitForMatchingMessage( + sender.adapter, + sender.adapter.encodeThreadId({ roomID }), + (message) => + message.id === messageId && + message.text.includes(editedTag) && + !message.text.includes(tag), + 45_000 + ); + + expect(editedMessage.text).toContain(editedTag); + expect(editedMessage.text).not.toContain(tag); + expect(stringifyMarkdown(editedMessage.formatted).trim()).toContain( + `**${editedTag}**` + ); + + const fetched = await waitForFetchedMessage( + sender.adapter, + sender.adapter.encodeThreadId({ roomID }), + messageId, + (message) => + message.text.includes(editedTag) && + !message.text.includes(tag), + 45_000 + ); + expect(fetched.text).toContain(editedTag); + expect(fetched.text).not.toContain(tag); + expect(stringifyMarkdown(fetched.formatted).trim()).toContain(`**${editedTag}**`); + }); + + it("strips Matrix reply fallback from fetched reply messages", async () => { + const rootTag = `e2e-reply-root-${nonce()}`; + const replyTag = `e2e-reply-visible-${nonce()}`; + const threadId = sender.adapter.encodeThreadId({ roomID }); + const rootPosted = await sender.adapter.postMessage(threadId, `Reply root ${rootTag}`); + await waitForFetchedMessage( + bot.adapter, + bot.adapter.encodeThreadId({ roomID }), + rootPosted.id, + (message) => message.text.includes(rootTag) + ); + + const replyResponse = await sender.matrixClient.sendEvent(roomID, EventType.RoomMessage, { + msgtype: "m.text", + body: `> <${bot.userID}> Reply root ${rootTag}\n> quoted\n\nVisible ${replyTag}`, + format: "org.matrix.custom.html", + formatted_body: + `
In reply to` + + ` ${bot.userID}
Reply root ${rootTag}
` + + `

Visible ${replyTag}

`, + "m.relates_to": { + "m.in_reply_to": { + event_id: rootPosted.id, + }, + }, + }); + + const replyMessage = await waitForFetchedMessage( + bot.adapter, + bot.adapter.encodeThreadId({ roomID }), + replyResponse.event_id, + (message) => message.text.includes(replyTag), + 45_000 + ); + + expect(replyMessage.text).toBe(`Visible ${replyTag}`); + expect(stringifyMarkdown(replyMessage.formatted).trim()).toBe(`Visible ${replyTag}`); + }); + + it("fetchMessages pagination", async () => { + const tag = `e2e-page-${nonce()}`; + const paginationRoomID = await createIsolatedRoom( + bot.matrixClient, + sender.matrixClient, + sender.userID, + `pagination-${tag}`, + 45_000 + ); + const threadId = bot.adapter.encodeThreadId({ roomID: paginationRoomID }); + const count = 5; + + // Sender sends N messages + const senderThreadId = sender.adapter.encodeThreadId({ roomID: paginationRoomID }); + for (let i = 0; i < count; i++) { + await sender.adapter.postMessage(senderThreadId, `${tag} msg-${i}`); + await sleep(200); + } + + // Let sync propagate + await sleep(2_000); + + // Fetch with a small page size + const page1 = await bot.adapter.fetchMessages(threadId, { + direction: "backward", + limit: 3, + }); + + expect(page1.messages.length).toBeGreaterThanOrEqual(1); + expect(page1.messages.length).toBeLessThanOrEqual(3); + + // If there's a next cursor, fetch more + if (page1.nextCursor) { + const page2 = await bot.adapter.fetchMessages(threadId, { + direction: "backward", + limit: 3, + cursor: page1.nextCursor, + }); + + expect(page2.messages.length).toBeGreaterThanOrEqual(1); + + // No overlap between pages + const page1Ids = new Set(page1.messages.map((m) => m.id)); + for (const m of page2.messages) { + expect(page1Ids.has(m.id)).toBe(false); + } + } + }); + + it("restarts on the same session and catches up paginated room history", async () => { + const offlineCount = 6; + const restartTag = `e2e-restart-${nonce()}`; + const restartRoomID = await createIsolatedRoom( + bot.matrixClient, + sender.matrixClient, + sender.userID, + `restart-${restartTag}`, + 45_000 + ); + const roomThreadId = sender.adapter.encodeThreadId({ roomID: restartRoomID }); + const botSession = bot.session; + const botState = bot.state; + + const baseline = await sender.adapter.postMessage( + roomThreadId, + `Restart baseline ${restartTag}` + ); + await waitForFetchedMessage( + bot.adapter, + bot.adapter.encodeThreadId({ roomID: restartRoomID }), + baseline.id, + (message) => message.text.includes(restartTag) + ); + + await shutdownParticipant(bot); + + const offlinePosts = []; + for (let index = 0; index < offlineCount; index += 1) { + offlinePosts.push( + await sender.adapter.postMessage( + roomThreadId, + `Restart offline ${restartTag}-${index.toString().padStart(2, "0")}` + ) + ); + } + + bot = await createParticipantFromSession({ + name: "e2e-bot-restarted", + recoveryKey: env.botRecoveryKey, + session: botSession, + state: botState, + }); + + await Promise.all([ + waitForEncryptedRoom(bot.matrixClient, restartRoomID, 45_000), + waitForJoinedMemberCount(bot.matrixClient, restartRoomID, 2, 45_000), + ]); + + const latestOffline = offlinePosts[offlinePosts.length - 1]; + const caughtUpMessage = await waitForFetchedMessage( + bot.adapter, + bot.adapter.encodeThreadId({ roomID: restartRoomID }), + latestOffline.id, + (message) => message.text.includes(restartTag), + 60_000 + ); + expect(caughtUpMessage.text).toContain(restartTag); + + const liveTag = `Restart live ${restartTag}`; + const botSawLiveMessage = waitForEvent>((cb) => { + bot.onMessage((incomingThreadId, message) => { + if ( + incomingThreadId === bot.adapter.encodeThreadId({ roomID: restartRoomID }) && + message.text.includes(liveTag) + ) { + cb(message); + } + }); + return () => bot.onMessage(null); + }, 45_000); + + const livePosted = await sender.adapter.postMessage(roomThreadId, liveTag); + const liveMessage = await botSawLiveMessage; + expect(liveMessage.id).toBe(livePosted.id); + expect(liveMessage.text).toContain(liveTag); + + const fetchedIds = new Set(); + const offlinePostIds = new Set(offlinePosts.map((post) => post.id)); + let cursor: string | undefined; + + while (true) { + const page = await bot.adapter.fetchMessages( + bot.adapter.encodeThreadId({ roomID: restartRoomID }), + { + direction: "backward", + limit: 5, + cursor, + } + ); + for (const message of page.messages) { + fetchedIds.add(message.id); + } + + if ([...offlinePostIds].every((id) => fetchedIds.has(id))) { + break; + } + + cursor = page.nextCursor; + if (!cursor) { + break; + } + } + + expect(fetchedIds.has(livePosted.id)).toBe(true); + expect(offlinePosts.every((post) => fetchedIds.has(post.id))).toBe(true); + }); + + it("fetches a single message and sees deletion via redaction", async () => { + const tag = `e2e-delete-${nonce()}`; + const threadId = bot.adapter.encodeThreadId({ roomID }); + const posted = await bot.adapter.postMessage(threadId, `Delete target ${tag}`); + + const initial = await sender.adapter.fetchMessage(threadId, posted.id); + expect(initial).toBeTruthy(); + const initialMessage = await waitForFetchedMessage( + sender.adapter, + threadId, + posted.id, + (message) => message.text.includes(tag) + ); + expect(initialMessage.text).toContain(tag); + + const senderRoom = await waitForRoom(sender.matrixClient, roomID); + const sawRedaction = waitForEvent((cb) => { + const handler = (event: MatrixEvent, room: unknown) => { + if (room !== senderRoom) { + return; + } + if (event.getAssociatedId() === posted.id) { + cb(event); + } + }; + + senderRoom.on(RoomEvent.Redaction, handler); + return () => senderRoom.off(RoomEvent.Redaction, handler); + }); + + await bot.adapter.deleteMessage(threadId, posted.id); + await sawRedaction; + + const deleted = await sender.adapter.fetchMessage(threadId, posted.id); + expect(deleted).toBeNull(); + }); + + it("creates a DM, reuses it, and posts through postChannelMessage", async () => { + const dmThreadId = await bot.adapter.openDM(sender.userID); + const dmThreadIdAgain = await bot.adapter.openDM(sender.userID); + expect(dmThreadIdAgain).toBe(dmThreadId); + + const decoded = bot.adapter.decodeThreadId(dmThreadId); + await waitForRoom(bot.matrixClient, decoded.roomID); + await waitForRoom(sender.matrixClient, decoded.roomID); + await waitForJoinedMemberCount(bot.matrixClient, decoded.roomID, 2, 30_000); + await waitForJoinedMemberCount(sender.matrixClient, decoded.roomID, 2, 30_000); + + const dmChannelId = bot.adapter.channelIdFromThreadId(dmThreadId); + const channelInfo = await bot.adapter.fetchChannelInfo(dmChannelId); + expect(channelInfo.id).toBe(dmChannelId); + expect(channelInfo.isDM).toBe(true); + expect(channelInfo.metadata?.roomID).toBe(decoded.roomID); + expect((channelInfo.memberCount ?? 0) >= 2).toBe(true); + + const tag = `e2e-dm-${nonce()}`; + const posted = await bot.adapter.postChannelMessage(dmChannelId, `DM hello ${tag}`); + const message = await waitForFetchedMessage( + sender.adapter, + sender.adapter.encodeThreadId({ roomID: decoded.roomID }), + posted.id, + (candidate) => candidate.text.includes(tag) + ); + expect(message.author.userId).toBe(bot.userID); + expect(message.text).toContain(tag); + }); + + it("fetches channel info, channel messages, thread info, and thread lists", async () => { + const rootTag = `e2e-thread-list-root-${nonce()}`; + const replyTag = `e2e-thread-list-reply-${nonce()}`; + const threadListRoomID = await createIsolatedRoom( + bot.matrixClient, + sender.matrixClient, + sender.userID, + `thread-list-${rootTag}`, + 45_000 + ); + const channelId = bot.adapter.channelIdFromThreadId( + bot.adapter.encodeThreadId({ roomID: threadListRoomID }) + ); + + const roomInfo = await bot.adapter.fetchChannelInfo(channelId); + expect(roomInfo.id).toBe(channelId); + expect(roomInfo.isDM).toBe(false); + expect((roomInfo.memberCount ?? 0) >= 2).toBe(true); + expect(roomInfo.metadata?.roomID).toBe(threadListRoomID); + + const rootPosted = await sender.adapter.postMessage( + sender.adapter.encodeThreadId({ roomID: threadListRoomID }), + `Thread root ${rootTag}` + ); + + const threadId = sender.adapter.encodeThreadId({ + roomID: threadListRoomID, + rootEventID: rootPosted.id, + }); + await sender.adapter.postMessage(threadId, `Thread reply ${replyTag}`); + + await waitForFetchedMessage( + bot.adapter, + bot.adapter.encodeThreadId({ roomID: threadListRoomID }), + rootPosted.id, + (message) => message.text.includes(rootTag) + ); + await waitForMatchingMessage( + bot.adapter, + bot.adapter.encodeThreadId({ roomID: threadListRoomID, rootEventID: rootPosted.id }), + (message) => message.text.includes(replyTag) + ); + + const channelMessages = await bot.adapter.fetchChannelMessages(channelId, { + direction: "backward", + limit: 20, + }); + expect(channelMessages.messages.some((message) => message.id === rootPosted.id)).toBe(true); + expect( + channelMessages.messages.some((message) => message.text.includes(replyTag)) + ).toBe(false); + + const threadInfo = await bot.adapter.fetchThread(threadId); + expect(threadInfo.id).toBe(threadId); + expect(threadInfo.channelId).toBe(channelId); + expect(threadInfo.isDM).toBe(false); + expect(threadInfo.metadata?.roomID).toBe(threadListRoomID); + + const threads = await bot.adapter.listThreads(channelId, { limit: 20 }); + const summary = threads.threads.find((thread) => thread.id === threadId); + expect(summary).toBeTruthy(); + expect(summary?.rootMessage.id).toBe(rootPosted.id); + expect(summary?.rootMessage.text).toContain(rootTag); + expect((summary?.replyCount ?? 0) >= 1).toBe(true); + }); + + it("includes live room metadata in channel and thread info", async () => { + const topic = `Adapter metadata ${nonce()}`; + await bot.matrixClient.sendStateEvent( + roomID, + EventType.RoomTopic, + { topic }, + "" + ); + await sleep(1_000); + + const channelId = bot.adapter.channelIdFromThreadId(bot.adapter.encodeThreadId({ roomID })); + const rootPosted = await bot.adapter.postMessage( + bot.adapter.encodeThreadId({ roomID }), + `Metadata root ${nonce()}` + ); + const threadId = bot.adapter.encodeThreadId({ + roomID, + rootEventID: rootPosted.id, + }); + + const [channelInfo, threadInfo] = await Promise.all([ + bot.adapter.fetchChannelInfo(channelId), + bot.adapter.fetchThread(threadId), + ]); + + expect(channelInfo.metadata?.roomID).toBe(roomID); + expect(channelInfo.metadata?.topic).toBe(topic); + expect(channelInfo.metadata?.encrypted).toBe(true); + expect(threadInfo.metadata?.roomID).toBe(roomID); + expect(threadInfo.metadata?.topic).toBe(topic); + expect(threadInfo.metadata?.encrypted).toBe(true); + }); + + it("uploads a file attachment and fetches it back", async () => { + const tag = `e2e-file-${nonce()}`; + const threadId = bot.adapter.encodeThreadId({ roomID }); + const expectedContents = `attachment payload ${tag}`; + const posted = await bot.adapter.postMessage(threadId, { + files: [ + { + filename: `${tag}.txt`, + mimeType: "text/plain", + data: Buffer.from(expectedContents, "utf8"), + }, + ], + }); + + const attachmentMessage = await waitForMatchingMessage( + sender.adapter, + sender.adapter.encodeThreadId({ roomID }), + (message) => + message.id === posted.id && + message.author.userId === bot.userID && + message.attachments.some((attachment) => attachment.name === `${tag}.txt`), + 45_000 + ); + expect(attachmentMessage.attachments).toHaveLength(1); + expect(attachmentMessage.attachments[0]?.url.startsWith("mxc://")).toBe(true); + expect(attachmentMessage.raw.isEncrypted()).toBe(true); + + const liveAttachment = attachmentMessage.attachments[0]; + expect(typeof liveAttachment?.fetchData).toBe("function"); + const liveAttachmentData = await liveAttachment?.fetchData?.(); + expect(liveAttachmentData?.toString("utf8")).toBe(expectedContents); + + const fetched = await sender.adapter.fetchMessage(threadId, attachmentMessage.id); + expect(fetched).toBeTruthy(); + expect(fetched?.attachments).toHaveLength(1); + expect(fetched?.attachments[0]?.url.startsWith("mxc://")).toBe(true); + expect(fetched?.raw.isEncrypted()).toBe(true); + + const fetchedAttachment = fetched?.attachments[0]; + expect(typeof fetchedAttachment?.fetchData).toBe("function"); + const fetchedAttachmentData = await fetchedAttachment?.fetchData?.(); + expect(fetchedAttachmentData?.toString("utf8")).toBe(expectedContents); + }); + + it.skipIf(!hasRecoveryCredentials)( + "restores historical encrypted messages on a fresh device using recovery key", + async () => { + await shutdownParticipant(sender); + + const tag = `e2e-recovery-${nonce()}`; + const threadId = bot.adapter.encodeThreadId({ roomID }); + const posted = await bot.adapter.postMessage(threadId, `Historical ${tag}`); + + const restoredSender = await createParticipant({ + name: "e2e-sender-restored", + loginToken: env.senderLoginToken, + recoveryKey: env.senderRecoveryKey, + }); + sender = restoredSender; + + await Promise.all([ + waitForEncryptedRoom(sender.matrixClient, roomID, 45_000), + waitForJoinedMemberCount(sender.matrixClient, roomID, 2, 45_000), + ]); + + const restoredMessage = await waitForFetchedMessage( + sender.adapter, + sender.adapter.encodeThreadId({ roomID }), + posted.id, + (message) => message.text.includes(tag), + 60_000 + ); + + expect(restoredMessage.text).toContain(tag); + expect(restoredMessage.author.userId).toBe(bot.userID); + expect(restoredMessage.raw.isEncrypted()).toBe(true); + } + ); + + it("emits typing notifications", async () => { + const threadId = bot.adapter.encodeThreadId({ roomID }); + const senderRoom = await waitForRoom(sender.matrixClient, roomID); + const botMember = senderRoom.getMember(bot.userID); + expect(botMember).toBeTruthy(); + + const senderSawTyping = waitForEvent((cb) => { + const member = senderRoom.getMember(bot.userID); + if (!member) { + throw new Error(`Missing room member ${bot.userID}`); + } + + const handler = (_event: MatrixEvent, updatedMember: typeof member) => { + if (updatedMember.userId === bot.userID && updatedMember.typing) { + cb(); + } + }; + + member.on(RoomMemberEvent.Typing, handler); + return () => member.off(RoomMemberEvent.Typing, handler); + }, 20_000); + + await bot.adapter.startTyping(threadId); + await senderSawTyping; + }); +}); diff --git a/e2e/helpers.ts b/e2e/helpers.ts new file mode 100644 index 0000000..08f258d --- /dev/null +++ b/e2e/helpers.ts @@ -0,0 +1,453 @@ +import { randomBytes } from "node:crypto"; +import { Chat, type Message, type ReactionEvent, type StateAdapter } from "chat"; +import { createMemoryState } from "@chat-adapter/state-memory"; +import { createRedisState } from "@chat-adapter/state-redis"; +import { EventType } from "matrix-js-sdk"; +import type { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk"; +import { MatrixAdapter } from "../src/index"; + +export const env = { + get baseURL(): string { + return process.env.E2E_BASE_URL!; + }, + get botLoginToken(): string { + return process.env.E2E_BOT_LOGIN_TOKEN!; + }, + get botRecoveryKey(): string | undefined { + return process.env.E2E_BOT_RECOVERY_KEY || undefined; + }, + get senderLoginToken(): string { + return process.env.E2E_SENDER_LOGIN_TOKEN!; + }, + get senderRecoveryKey(): string | undefined { + return process.env.E2E_SENDER_RECOVERY_KEY || undefined; + }, + get roomID(): string | undefined { + return process.env.E2E_ROOM_ID || undefined; + }, + get redisURL(): string | undefined { + return process.env.E2E_REDIS_URL || undefined; + }, +}; + +export interface E2EParticipant { + adapter: MatrixAdapter; + chat: Chat; + matrixClient: MatrixClient; + session: MatrixLoginResponse; + state: StateAdapter; + userID: string; + onMessage: (cb: ((threadID: string, message: Message) => void) | null) => void; + onReaction: (cb: ((data: ReactionEvent) => void) | null) => void; +} + +export async function createParticipant(opts: { + loginToken: string; + name: string; + recoveryKey?: string; +}): Promise { + const login = await loginToMatrix(opts.loginToken, opts.name); + return createParticipantFromSession({ + name: opts.name, + recoveryKey: opts.recoveryKey, + session: login, + }); +} + +export async function createParticipantFromSession(opts: { + name: string; + recoveryKey?: string; + session: MatrixLoginResponse; + state?: StateAdapter; +}): Promise { + const state = opts.state ?? createE2EState(opts.name); + const adapter = new MatrixAdapter({ + baseURL: env.baseURL, + auth: { + type: "accessToken", + accessToken: opts.session.accessToken, + userID: opts.session.userID, + }, + deviceID: opts.session.deviceID, + inviteAutoJoin: {}, + e2ee: { + useIndexedDB: false, + }, + recoveryKey: opts.recoveryKey, + }); + + let messageCallback: ((threadID: string, message: Message) => void) | null = null; + let reactionCallback: ((data: ReactionEvent) => void) | null = null; + + const chat = new Chat({ + userName: opts.name, + state, + adapters: { matrix: adapter }, + }); + + chat.onNewMessage(/[\s\S]*/u, async (thread, message, context) => { + messageCallback?.(context?.threadId ?? thread.id, message); + }); + + chat.onSubscribedMessage(async (thread, message, context) => { + messageCallback?.(context?.threadId ?? thread.id, message); + }); + + chat.onReaction(async (event: ReactionEvent) => { + reactionCallback?.(event); + }); + + await chat.initialize(); + + const matrixClient = getInitializedClient(adapter); + + return { + adapter, + chat, + matrixClient, + session: opts.session, + state, + userID: opts.session.userID, + onMessage: (cb) => { messageCallback = cb; }, + onReaction: (cb) => { reactionCallback = cb; }, + }; +} + +function createE2EState(name: string): StateAdapter { + if (env.redisURL) { + return createRedisState({ + url: env.redisURL, + keyPrefix: `matrix-chat-adapter-e2e:${name}`, + }); + } + + return createMemoryState(); +} + +export async function shutdownParticipant(participant: E2EParticipant): Promise { + participant.onMessage(null); + participant.onReaction(null); + await participant.adapter.shutdown(); +} + +type MatrixLoginResponse = { + accessToken: string; + deviceID: string; + userID: string; +}; + +function generateDeviceID(): string { + return `E2E_${randomBytes(8).toString("hex").toUpperCase()}`; +} + +async function loginToMatrix( + loginToken: string, + participantName: string +): Promise { + const requestedDeviceID = generateDeviceID(); + const response = await fetch(`${env.baseURL}/_matrix/client/v3/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: "org.matrix.login.jwt", + token: loginToken, + device_id: requestedDeviceID, + initial_device_display_name: `matrix-chat-adapter-${participantName}`, + }), + }); + + if (!response.ok) { + throw new Error( + `Matrix login failed with ${response.status}: ${await response.text()}` + ); + } + + const payload = (await response.json()) as { + access_token?: unknown; + device_id?: unknown; + user_id?: unknown; + }; + const accessToken = readStringProperty(payload.access_token, "access_token"); + const userID = readStringProperty(payload.user_id, "user_id"); + const deviceID = + typeof payload.device_id === "string" && payload.device_id.length > 0 + ? payload.device_id + : requestedDeviceID; + + return { accessToken, deviceID, userID }; +} + +function readStringProperty(value: unknown, key: string): string { + if (typeof value === "string" && value.length > 0) { + return value; + } + throw new Error(`Matrix login response is missing ${key}`); +} + +export async function getOrCreateRoom( + botClient: MatrixClient, + senderUserID: string, +): Promise { + if (env.roomID) { + return env.roomID; + } + + return createEncryptedRoom(botClient, senderUserID); +} + +export async function createIsolatedRoom( + botClient: MatrixClient, + senderClient: MatrixClient, + senderUserID: string, + roomName = `matrix-chat-adapter-e2e-${nonce()}`, + timeoutMs = 30_000 +): Promise { + const roomID = await createEncryptedRoom(botClient, senderUserID, roomName); + + await Promise.all([ + waitForEncryptedRoom(botClient, roomID, timeoutMs), + waitForEncryptedRoom(senderClient, roomID, timeoutMs), + waitForJoinedMemberCount(botClient, roomID, 2, timeoutMs), + waitForJoinedMemberCount(senderClient, roomID, 2, timeoutMs), + ]); + + return roomID; +} + +async function createEncryptedRoom( + botClient: MatrixClient, + senderUserID: string, + roomName?: string +): Promise { + const { room_id } = await botClient.createRoom({ + name: roomName, + preset: "private_chat", + invite: [senderUserID], + initial_state: [ + { + type: EventType.RoomEncryption, + state_key: "", + content: { algorithm: "m.megolm.v1.aes-sha2" }, + }, + ], + }); + + if (typeof room_id !== "string" || room_id.length === 0) { + throw new Error("Matrix createRoom did not return room_id"); + } + + return room_id; +} + +export function waitForEvent( + subscribe: (callback: (value: T) => void) => (() => void) | void, + timeoutMs = 10_000 +): Promise { + return new Promise((resolve, reject) => { + let cleanup: (() => void) | void; + let settled = false; + let shouldCleanupAfterSubscribe = false; + + const settle = (finish: () => void) => { + if (settled) { + return; + } + + settled = true; + clearTimeout(timer); + if (cleanup) { + cleanup(); + } else { + shouldCleanupAfterSubscribe = true; + } + finish(); + }; + + const timer = setTimeout(() => { + settle(() => reject(new Error(`waitForEvent timed out after ${timeoutMs}ms`))); + }, timeoutMs); + + try { + cleanup = subscribe((value) => { + settle(() => resolve(value)); + }); + if (shouldCleanupAfterSubscribe) { + cleanup?.(); + } + } catch (error) { + settle(() => reject(error)); + } + }); +} + +export async function waitForCondition( + condition: () => boolean, + timeoutMs = 10_000, + intervalMs = 250 +): Promise { + const startedAt = Date.now(); + + while (true) { + if (condition()) { + return; + } + + if (Date.now() - startedAt >= timeoutMs) { + throw new Error(`waitForCondition timed out after ${timeoutMs}ms`); + } + + await sleep(intervalMs); + } +} + +export async function waitForRoom( + client: MatrixClient, + roomID: string, + timeoutMs = 10_000 +): Promise { + await waitForCondition(() => Boolean(client.getRoom(roomID)), timeoutMs); + const room = client.getRoom(roomID); + if (!room) { + throw new Error(`Room ${roomID} was not found after waiting`); + } + return room; +} + +export async function waitForEncryptedRoom( + client: MatrixClient, + roomID: string, + timeoutMs = 20_000 +): Promise { + const room = await waitForRoom(client, roomID, timeoutMs); + await waitForCondition(() => client.isRoomEncrypted(roomID), timeoutMs); + return room; +} + +export async function waitForJoinedMemberCount( + client: MatrixClient, + roomID: string, + expectedCount: number, + timeoutMs = 20_000 +): Promise { + const room = await waitForRoom(client, roomID, timeoutMs); + await waitForCondition( + () => (client.getRoom(roomID)?.getJoinedMembers().length ?? 0) >= expectedCount, + timeoutMs + ); + return room; +} + +export async function waitForFetchedMessage( + adapter: MatrixAdapter, + threadId: string, + messageId: string, + predicate: (message: Message) => boolean = () => true, + timeoutMs = 30_000, + intervalMs = 1_000 +): Promise> { + const startedAt = Date.now(); + + while (Date.now() - startedAt < timeoutMs) { + let message: Message | null = null; + try { + message = await adapter.fetchMessage(threadId, messageId); + } catch (error) { + if (!isTransientMatrixError(error)) { + throw error; + } + } + + if (message && isDecryptedMessage(message) && predicate(message)) { + return message; + } + await sleep(intervalMs); + } + + throw new Error(`waitForFetchedMessage timed out after ${timeoutMs}ms`); +} + +export async function waitForMatchingMessage( + adapter: MatrixAdapter, + threadId: string, + predicate: (message: Message) => boolean, + timeoutMs = 30_000, + intervalMs = 1_000, + limit = 20 +): Promise> { + const startedAt = Date.now(); + + while (Date.now() - startedAt < timeoutMs) { + let page: + | Awaited> + | null = null; + try { + page = await adapter.fetchMessages(threadId, { + direction: "backward", + limit, + }); + } catch (error) { + if (!isTransientMatrixError(error)) { + throw error; + } + } + + if (!page) { + await sleep(intervalMs); + continue; + } + + const match = page.messages.find( + (message) => isDecryptedMessage(message) && predicate(message) + ); + if (match) { + return match; + } + await sleep(intervalMs); + } + + throw new Error(`waitForMatchingMessage timed out after ${timeoutMs}ms`); +} + +function getInitializedClient(adapter: MatrixAdapter): MatrixClient { + const candidate: unknown = Reflect.get(adapter, "client"); + if (!isMatrixClient(candidate)) { + throw new Error("Matrix client was not initialized"); + } + return candidate; +} + +function isMatrixClient(value: unknown): value is MatrixClient { + return ( + typeof value === "object" && + value !== null && + typeof Reflect.get(value, "startClient") === "function" && + typeof Reflect.get(value, "stopClient") === "function" + ); +} + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function nonce(): string { + return Math.random().toString(36).slice(2, 10); +} + +function isDecryptedMessage(message: Message): boolean { + return !message.text.startsWith("** Unable to decrypt:"); +} + +function isTransientMatrixError(error: unknown): boolean { + if (!(error instanceof Error)) { + return false; + } + + return ( + error.message.includes("429") || + error.message.includes("503") || + error.message.includes("M_LIMIT_EXCEEDED") || + error.message.includes("Server returned 503 error") + ); +} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..0d075d8 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,28 @@ +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["coverage/**", "dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { parser: tseslint.parser }, + plugins: { "@typescript-eslint": tseslint.plugin }, + rules: { + "@typescript-eslint/no-explicit-any": "error", + "no-restricted-syntax": [ + "error", + { + selector: "TSAsExpression > TSAnyKeyword", + message: + "Do not assert to `any`. Prefer inference, `unknown`, or a narrower type.", + }, + { + selector: "TSTypeAssertion > TSAnyKeyword", + message: + "Do not assert to `any`. Prefer inference, `unknown`, or a narrower type.", + }, + ], + }, + } +); diff --git a/examples/.env.example b/examples/.env.example index 76247bd..e21ca62 100644 --- a/examples/.env.example +++ b/examples/.env.example @@ -12,17 +12,9 @@ MATRIX_USER_ID=@bot:beeper.com # Optional MATRIX_DEVICE_ID= -MATRIX_DEVICE_ID_PERSIST_ENABLED=true -MATRIX_DEVICE_ID_PERSIST_KEY= MATRIX_RECOVERY_KEY= -MATRIX_E2EE_ENABLED= -MATRIX_E2EE_USE_INDEXEDDB=false -MATRIX_E2EE_DB_PREFIX=beeper-chat-adapter-matrix -MATRIX_E2EE_STORAGE_PASSWORD= -MATRIX_E2EE_STORAGE_KEY_BASE64= -MATRIX_SESSION_ENABLED=true -MATRIX_SESSION_KEY= -MATRIX_SESSION_TTL_MS= +MATRIX_INVITE_AUTOJOIN=false +MATRIX_INVITE_AUTOJOIN_ALLOWLIST= MATRIX_SDK_LOG_LEVEL=error -BOT_USER_NAME=beeper-bot +MATRIX_BOT_USERNAME=beeper-bot REDIS_URL=redis://localhost:6379 diff --git a/examples/bot.redis.ts b/examples/bot.redis.ts index 122f725..b2e2b88 100644 --- a/examples/bot.redis.ts +++ b/examples/bot.redis.ts @@ -15,7 +15,7 @@ const logger = new ConsoleLogger( const matrix = createMatrixAdapter(); const bot = new Chat({ - userName: process.env.BOT_USER_NAME ?? "beeper-bot", + userName: process.env.MATRIX_BOT_USERNAME ?? "beeper-bot", logger, state: createRedisState({ url: redisURL }), adapters: { diff --git a/examples/bot.ts b/examples/bot.ts index e222710..a4e251c 100644 --- a/examples/bot.ts +++ b/examples/bot.ts @@ -10,7 +10,7 @@ const logger = new ConsoleLogger( const matrix = createMatrixAdapter(); const bot = new Chat({ - userName: process.env.BOT_USER_NAME ?? "beeper-bot", + userName: process.env.MATRIX_BOT_USERNAME ?? "beeper-bot", logger, state: createMemoryState(), adapters: { diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 21e103f..0000000 --- a/package-lock.json +++ /dev/null @@ -1,4631 +0,0 @@ -{ - "name": "@beeper/chat-adapter-matrix", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@beeper/chat-adapter-matrix", - "version": "0.1.0", - "license": "MIT", - "dependencies": { - "@chat-adapter/state-memory": "^4.13.4", - "@chat-adapter/state-redis": "^4.13.4", - "chat": "^4.13.4", - "matrix-js-sdk": "^41.0.0" - }, - "devDependencies": { - "@types/node": "^22.10.2", - "@vitest/coverage-v8": "^2.1.8", - "tsup": "^8.3.5", - "typescript": "^5.7.2", - "vitest": "^2.1.8" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@chat-adapter/state-memory": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@chat-adapter/state-memory/-/state-memory-4.14.0.tgz", - "integrity": "sha512-6YWMok5/1tS81lXspXhObmXVN+Zaopdd4ZMMh3r2WVeCCLCvbRYz9X9tqA+QuI2XCWxUcAPuWavFATgn+PdkLw==", - "license": "MIT", - "dependencies": { - "chat": "4.14.0" - } - }, - "node_modules/@chat-adapter/state-redis": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@chat-adapter/state-redis/-/state-redis-4.14.0.tgz", - "integrity": "sha512-JammJeUWsh14Lk6egFI05aVx9FHvcAmvOwpNw6HRh56vff54kfCeBt+S07FnTkd2FSNUy7Jvo4bnVGHjF+cUWg==", - "license": "MIT", - "dependencies": { - "chat": "4.14.0", - "redis": "^4.7.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@matrix-org/matrix-sdk-crypto-wasm": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-17.1.0.tgz", - "integrity": "sha512-yKPqBvKlHSqkt/UJh+Z+zLKQP8bd19OxokXYXh3VkKbW0+C44nPHsidSwd3SH+RxT+Ck2PDRwVcVXEnUft+/2g==", - "license": "Apache-2.0", - "engines": { - "node": ">= 18" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@redis/bloom": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", - "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/client": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", - "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", - "license": "MIT", - "dependencies": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@redis/graph": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", - "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/json": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", - "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/search": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", - "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/time-series": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", - "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/events": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", - "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@vitest/coverage-v8": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", - "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.7", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.12", - "magicast": "^0.3.5", - "std-env": "^3.8.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "2.1.9", - "vitest": "2.1.9" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@workflow/serde": { - "version": "4.1.0-beta.2", - "resolved": "https://registry.npmjs.org/@workflow/serde/-/serde-4.1.0-beta.2.tgz", - "integrity": "sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==", - "license": "Apache-2.0" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/another-json": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", - "integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==", - "license": "Apache-2.0" - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", - "license": "MIT", - "dependencies": { - "base-x": "^5.0.0" - } - }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chat": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/chat/-/chat-4.14.0.tgz", - "integrity": "sha512-gVod4FBptr4ewPEdY2zo1GnegI8/O0aC5F+IvFMIjimqmQjW0txhQbgI/DgmD48pwXJhxUGpvNrqzMrChx8GWw==", - "license": "MIT", - "dependencies": { - "@workflow/serde": "4.1.0-beta.2", - "mdast-util-to-string": "^4.0.0", - "remark-gfm": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.5" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", - "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-network-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", - "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/loglevel": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", - "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" - } - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/matrix-events-sdk": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", - "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==", - "license": "Apache-2.0" - }, - "node_modules/matrix-js-sdk": { - "version": "41.0.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-41.0.0.tgz", - "integrity": "sha512-58j4WuOwXvT6bmTgc48b3bITVG/rWv5+N//yA5mvr6Rc04EUSpwTPhgMOTjUVnJsrhLGzdkgal4Xtt2R+xNdwg==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^17.1.0", - "another-json": "^0.2.0", - "bs58": "^6.0.0", - "content-type": "^1.0.4", - "jwt-decode": "^4.0.0", - "loglevel": "^1.9.2", - "matrix-events-sdk": "0.0.1", - "matrix-widget-api": "^1.16.1", - "oidc-client-ts": "^3.0.1", - "p-retry": "7", - "sdp-transform": "^3.0.0", - "unhomoglyph": "^1.0.6", - "uuid": "13" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/matrix-widget-api": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.17.0.tgz", - "integrity": "sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==", - "license": "Apache-2.0", - "dependencies": { - "@types/events": "^3.0.0", - "events": "^3.2.0" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/oidc-client-ts": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.4.1.tgz", - "integrity": "sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==", - "license": "Apache-2.0", - "dependencies": { - "jwt-decode": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/p-retry": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", - "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", - "license": "MIT", - "dependencies": { - "is-network-error": "^1.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/redis": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", - "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", - "license": "MIT", - "workspaces": [ - "./packages/*" - ], - "dependencies": { - "@redis/bloom": "1.2.0", - "@redis/client": "1.6.1", - "@redis/graph": "1.1.1", - "@redis/json": "1.0.7", - "@redis/search": "1.2.0", - "@redis/time-series": "1.1.0" - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/sdp-transform": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-3.0.0.tgz", - "integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==", - "license": "MIT", - "bin": { - "sdp-verify": "checker.js" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" - }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unhomoglyph": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", - "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==", - "license": "MIT" - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/package.json b/package.json index 6b92565..ea1b884 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,10 @@ { "name": "@beeper/chat-adapter-matrix", - "version": "0.1.0", + "version": "0.2.0", "description": "Matrix adapter for chat", + "engines": { + "node": ">=22" + }, "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", @@ -18,26 +21,33 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "example:bun": "bun --env-file=examples/.env run examples/bot.ts", - "example:bun:redis": "bun --env-file=examples/.env run examples/bot.redis.ts", - "token:bun": "bun run scripts/get-access-token.ts", + "example": "node --experimental-strip-types --env-file=examples/.env examples/bot.ts", + "example:redis": "node --experimental-strip-types --env-file=examples/.env examples/bot.redis.ts", + "matrix-token": "node --experimental-strip-types scripts/get-access-token.ts", "clean:repo": "rm -rf dist coverage", + "lint:types": "eslint .", "test": "vitest run --coverage", + "test:e2e": "vitest run --config vitest.config.e2e.ts", "test:watch": "vitest", - "typecheck": "tsc --noEmit", + "typecheck": "pnpm lint:types && tsc --noEmit", "clean": "rm -rf dist" }, "dependencies": { - "@chat-adapter/state-memory": "^4.13.4", - "@chat-adapter/state-redis": "^4.13.4", - "chat": "^4.13.4", - "matrix-js-sdk": "^41.0.0" + "@chat-adapter/state-memory": "^4.17.0", + "@chat-adapter/state-redis": "^4.17.0", + "chat": "^4.17.0", + "marked": "^15.0.12", + "matrix-js-sdk": "^41.0.0", + "node-html-parser": "^7.1.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^22.10.2", "@vitest/coverage-v8": "^2.1.8", + "eslint": "^10.0.2", "tsup": "^8.3.5", "typescript": "^5.7.2", + "typescript-eslint": "^8.56.1", "vitest": "^2.1.8" }, "repository": { @@ -57,5 +67,6 @@ "bot", "adapter" ], - "license": "MIT" + "license": "MIT", + "packageManager": "pnpm@10.30.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..30910b8 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3544 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@chat-adapter/state-memory': + specifier: ^4.17.0 + version: 4.17.0 + '@chat-adapter/state-redis': + specifier: ^4.17.0 + version: 4.17.0 + chat: + specifier: ^4.17.0 + version: 4.17.0 + marked: + specifier: ^15.0.12 + version: 15.0.12 + matrix-js-sdk: + specifier: ^41.0.0 + version: 41.0.0 + node-html-parser: + specifier: ^7.1.0 + version: 7.1.0 + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.0.3) + '@types/node': + specifier: ^22.10.2 + version: 22.19.15 + '@vitest/coverage-v8': + specifier: ^2.1.8 + version: 2.1.9(vitest@2.1.9(@types/node@22.19.15)) + eslint: + specifier: ^10.0.2 + version: 10.0.3 + tsup: + specifier: ^8.3.5 + version: 8.5.1(postcss@8.5.8)(typescript@5.9.3) + typescript: + specifier: ^5.7.2 + version: 5.9.3 + typescript-eslint: + specifier: ^8.56.1 + version: 8.56.1(eslint@10.0.3)(typescript@5.9.3) + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.19.15) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@chat-adapter/state-memory@4.17.0': + resolution: {integrity: sha512-7LewkFY6gQVEXxAycaWUXax9Pt7Prmn2mBdoIBbTEAzV5bSQBaXqKyrd6VWCd9gEyxNlBUainyQul4iGNPw1sQ==} + + '@chat-adapter/state-redis@4.17.0': + resolution: {integrity: sha512-sJ2V/pZESZKrGkuafMqg4gqyvHwz8d0JDUvqeZ+FVfEgT/h0g1kYfSc++qfEayzmPpSJxC6gzGpBGTiL/5utCw==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.3': + resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.3': + resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.1.1': + resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.3': + resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.6.1': + resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@matrix-org/matrix-sdk-crypto-wasm@17.1.0': + resolution: {integrity: sha512-yKPqBvKlHSqkt/UJh+Z+zLKQP8bd19OxokXYXh3VkKbW0+C44nPHsidSwd3SH+RxT+Ck2PDRwVcVXEnUft+/2g==} + engines: {node: '>= 18'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@redis/bloom@5.11.0': + resolution: {integrity: sha512-KYiVilAhAFN3057afUb/tfYJpsEyTkQB+tQcn5gVVA7DgcNOAj8lLxe4j8ov8BF6I9C1Fe/kwlbuAICcTMX8Lw==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.11.0 + + '@redis/client@5.11.0': + resolution: {integrity: sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ==} + engines: {node: '>= 18'} + peerDependencies: + '@node-rs/xxhash': ^1.1.0 + peerDependenciesMeta: + '@node-rs/xxhash': + optional: true + + '@redis/json@5.11.0': + resolution: {integrity: sha512-1iAy9kAtcD0quB21RbPTbUqqy+T2Uu2JxucwE+B4A+VaDbIRvpZR6DMqV8Iqaws2YxJYB3GC5JVNzPYio2ErUg==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.11.0 + + '@redis/search@5.11.0': + resolution: {integrity: sha512-g1l7f3Rnyk/xI99oGHIgWHSKFl45Re5YTIcO8j/JE8olz389yUFyz2+A6nqVy/Zi031VgPDWscbbgOk8hlhZ3g==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.11.0 + + '@redis/time-series@5.11.0': + resolution: {integrity: sha512-TWFeOcU4xkj0DkndnOyhtxvX1KWD+78UHT3XX3x3XRBUGWeQrKo3jqzDsZwxbggUgf9yLJr/akFHXru66X5UQA==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.11.0 + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/events@3.0.3': + resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@typescript-eslint/eslint-plugin@8.56.1': + resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.56.1': + resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.56.1': + resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/coverage-v8@2.1.9': + resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} + peerDependencies: + '@vitest/browser': 2.1.9 + vitest: 2.1.9 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + '@workflow/serde@4.1.0-beta.2': + resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + another-json@0.2.0: + resolution: {integrity: sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + chat@4.17.0: + resolution: {integrity: sha512-kX9jIXmEU2ksnF1YshM+qfI/6pBy6k8pELkuRaE2AJyF/0nt4GUc0McBhn2DNpXLDI5sVwRGB3PmI+q5wUtYFg==} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.0.3: + resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.4: + resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-network-error@1.3.1: + resolution: {integrity: sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==} + engines: {node: '>=16'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + loglevel@1.9.2: + resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} + engines: {node: '>= 0.6.0'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + + matrix-events-sdk@0.0.1: + resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} + + matrix-js-sdk@41.0.0: + resolution: {integrity: sha512-58j4WuOwXvT6bmTgc48b3bITVG/rWv5+N//yA5mvr6Rc04EUSpwTPhgMOTjUVnJsrhLGzdkgal4Xtt2R+xNdwg==} + engines: {node: '>=22.0.0'} + + matrix-widget-api@1.17.0: + resolution: {integrity: sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.1: + resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-html-parser@7.1.0: + resolution: {integrity: sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + oidc-client-ts@3.4.1: + resolution: {integrity: sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-retry@7.1.1: + resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} + engines: {node: '>=20'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + redis@5.11.0: + resolution: {integrity: sha512-YwXjATVDT+AuxcyfOwZn046aml9jMlQPvU1VXIlLDVAExe0u93aTfPYSeRgG4p9Q/Jlkj+LXJ1XEoFV+j2JKcQ==} + engines: {node: '>= 18'} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remend@1.2.2: + resolution: {integrity: sha512-4ZJgIB9EG9fQE41mOJCRHMmnxDTKHWawQoJWZyUbZuj680wVyogu2ihnj8Edqm7vh2mo/TWHyEZpn2kqeDvS7w==} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + sdp-transform@3.0.0: + resolution: {integrity: sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.56.1: + resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unhomoglyph@1.0.6: + resolution: {integrity: sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.28.6': {} + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + + '@chat-adapter/state-memory@4.17.0': + dependencies: + chat: 4.17.0 + transitivePeerDependencies: + - supports-color + + '@chat-adapter/state-redis@4.17.0': + dependencies: + chat: 4.17.0 + redis: 5.11.0 + transitivePeerDependencies: + - '@node-rs/xxhash' + - supports-color + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3)': + dependencies: + eslint: 10.0.3 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.3': + dependencies: + '@eslint/object-schema': 3.0.3 + debug: 4.4.3 + minimatch: 10.2.4 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.3': + dependencies: + '@eslint/core': 1.1.1 + + '@eslint/core@1.1.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.0.3)': + optionalDependencies: + eslint: 10.0.3 + + '@eslint/object-schema@3.0.3': {} + + '@eslint/plugin-kit@0.6.1': + dependencies: + '@eslint/core': 1.1.1 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@matrix-org/matrix-sdk-crypto-wasm@17.1.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@redis/bloom@5.11.0(@redis/client@5.11.0)': + dependencies: + '@redis/client': 5.11.0 + + '@redis/client@5.11.0': + dependencies: + cluster-key-slot: 1.1.2 + + '@redis/json@5.11.0(@redis/client@5.11.0)': + dependencies: + '@redis/client': 5.11.0 + + '@redis/search@5.11.0(@redis/client@5.11.0)': + dependencies: + '@redis/client': 5.11.0 + + '@redis/time-series@5.11.0(@redis/client@5.11.0)': + dependencies: + '@redis/client': 5.11.0 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.8': {} + + '@types/events@3.0.3': {} + + '@types/json-schema@7.0.15': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + + '@types/unist@3.0.3': {} + + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/type-utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + eslint: 10.0.3 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + eslint: 10.0.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.56.1(eslint@10.0.3)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.0.3 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.56.1': {} + + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.56.1(eslint@10.0.3)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + eslint: 10.0.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 + + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.19.15))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@22.19.15) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.15))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.19.15) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@workflow/serde@4.1.0-beta.2': {} + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.14.0: + 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 + + another-json@0.2.0: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + assertion-error@2.0.1: {} + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base-x@5.0.1: {} + + boolbase@1.0.0: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bundle-require@5.1.0(esbuild@0.27.3): + dependencies: + esbuild: 0.27.3 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + ccount@2.0.1: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + character-entities@2.0.2: {} + + chat@4.17.0: + dependencies: + '@workflow/serde': 4.1.0-beta.2 + mdast-util-to-string: 4.0.0 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + remend: 1.2.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cluster-key-slot@1.1.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + content-type@1.0.5: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@4.5.0: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.0.3: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.3 + '@eslint/config-helpers': 0.5.3 + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.4 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + events@3.3.0: {} + + expect-type@1.3.0: {} + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.1 + rollup: 4.59.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.4 + keyv: 4.5.4 + + flatted@3.3.4: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + has-flag@4.0.0: {} + + he@1.2.0: {} + + html-escaper@2.0.2: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-network-error@1.3.1: {} + + is-plain-obj@4.1.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + joycon@3.1.1: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jwt-decode@4.0.0: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + loglevel@1.9.2: {} + + longest-streak@3.1.0: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + + markdown-table@3.0.4: {} + + marked@15.0.12: {} + + matrix-events-sdk@0.0.1: {} + + matrix-js-sdk@41.0.0: + dependencies: + '@babel/runtime': 7.28.6 + '@matrix-org/matrix-sdk-crypto-wasm': 17.1.0 + 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.17.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.17.0: + dependencies: + '@types/events': 3.0.3 + events: 3.3.0 + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.3: {} + + mlly@1.8.1: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-html-parser@7.1.0: + dependencies: + css-select: 5.2.2 + he: 1.2.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-assign@4.1.1: {} + + oidc-client-ts@3.4.1: + dependencies: + jwt-decode: 4.0.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-retry@7.1.1: + dependencies: + is-network-error: 1.3.1 + + package-json-from-dist@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.1 + pathe: 2.0.3 + + postcss-load-config@6.0.1(postcss@8.5.8): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.8 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + readdirp@4.1.2: {} + + redis@5.11.0: + dependencies: + '@redis/bloom': 5.11.0(@redis/client@5.11.0) + '@redis/client': 5.11.0 + '@redis/json': 5.11.0(@redis/client@5.11.0) + '@redis/search': 5.11.0(@redis/client@5.11.0) + '@redis/time-series': 5.11.0(@redis/client@5.11.0) + transitivePeerDependencies: + - '@node-rs/xxhash' + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remend@1.2.2: {} + + resolve-from@5.0.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + sdp-transform@3.0.0: {} + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.5.0 + minimatch: 10.2.4 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tree-kill@1.2.2: {} + + trough@2.2.0: {} + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + + tsup@8.5.1(postcss@8.5.8)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.3) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.3 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.8) + resolve-from: 5.0.0 + rollup: 4.59.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.8 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.56.1(eslint@10.0.3)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + eslint: 10.0.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + undici-types@6.21.0: {} + + unhomoglyph@1.0.6: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + uuid@13.0.0: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite-node@2.1.9(@types/node@22.19.15): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.19.15) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@22.19.15): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.8 + rollup: 4.59.0 + optionalDependencies: + '@types/node': 22.19.15 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@22.19.15): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.15)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.19.15) + vite-node: 2.1.9(@types/node@22.19.15) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.15 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + yocto-queue@0.1.0: {} + + zwitch@2.0.4: {} diff --git a/scripts/get-access-token.ts b/scripts/get-access-token.ts index 4d0a76e..20a19f8 100644 --- a/scripts/get-access-token.ts +++ b/scripts/get-access-token.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env bun +#!/usr/bin/env -S node --experimental-strip-types import { createInterface } from "node:readline/promises"; import readline from "node:readline"; @@ -299,7 +299,7 @@ async function whoami(baseDomain: string, accessToken: string): Promise { - output.write("Beeper Access Token Helper (Bun)\n\n"); + output.write("Beeper Access Token Helper\n\n"); const env = await promptChoice( "Environment:", diff --git a/src/index.test.ts b/src/index.test.ts index 87258bb..4f98493 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,18 +1,86 @@ import { describe, expect, it, vi } from "vitest"; -import { getEmoji } from "chat"; -import { EventType, RelationType } from "matrix-js-sdk"; +import { Chat, getEmoji, stringifyMarkdown } from "chat"; +import type { ChatInstance, Logger, StateAdapter } from "chat"; +import { createMemoryState } from "@chat-adapter/state-memory"; +import { EventType, MsgType, RelationType, type MatrixClient } from "matrix-js-sdk"; +import { MatrixError } from "matrix-js-sdk/lib/http-api/errors"; import { encodeRecoveryKey } from "matrix-js-sdk/lib/crypto-api/recovery-key"; import { createMatrixAdapter, MatrixAdapter } from "./index"; +type RawEventLike = { + content?: Record; + event_id?: string; + isThreadRoot?: boolean; + origin_server_ts?: number; + room_id?: string; + sender?: string; + threadRootId?: string; + type?: string; + unsigned?: Record; + [key: string]: unknown; +}; + +type MessagesResponseLike = { + chunk: RawEventLike[]; + end?: string; +}; + +type RelationsResponseLike = { + originalEvent: ReturnType | null; + events: Array>; + nextBatch: string | null; + prevBatch: string | null; +}; + +type MemberLike = { + name?: string; + rawDisplayName?: string; +}; + +type StateEventLike = { + getContent: () => T; +}; + +type RoomStateLike = { + getStateEvents: (eventType: string, stateKey: string) => StateEventLike | null; +}; + +type RoomLike = { + currentState: RoomStateLike; + getMember: (userId: string) => MemberLike | null; + roomId: string; + name: string; + timeline: unknown[]; + hasEncryptionStateEvent: () => boolean; + getJoinedMembers: () => Array>; + getMyMembership: () => string; + findEventById: (eventID?: string) => ReturnType | null; +}; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function readString(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} + +function readNumber(value: unknown): number | undefined { + return typeof value === "number" ? value : undefined; +} + function makeEvent(overrides: Record = {}) { const event = { getId: () => "$event", getRoomId: () => "!room:beeper.com", getTs: () => 1_700_000_000_000, getSender: () => "@alice:beeper.com", + getStateKey: () => undefined, getType: () => EventType.RoomMessage, getContent: () => ({ body: "hello" }), getRelation: () => null, + isRelation: () => false, + getServerAggregatedRelation: () => undefined, threadRootId: undefined, isThreadRoot: false, isRedaction: () => false, @@ -23,8 +91,110 @@ function makeEvent(overrides: Record = {}) { return event; } +function makeStateEvent(content: Record): StateEventLike { + return { + getContent: () => content as T, + }; +} + +function makeRawEvent(overrides: Record = {}) { + return { + event_id: "$raw", + room_id: "!room:beeper.com", + origin_server_ts: 1_700_000_000_000, + sender: "@alice:beeper.com", + type: EventType.RoomMessage, + content: { body: "hello" }, + unsigned: {}, + ...overrides, + }; +} + +function mapRawToEvent(raw: RawEventLike) { + const content = raw.content ?? {}; + const relatesTo = isRecord(content["m.relates_to"]) + ? content["m.relates_to"] + : undefined; + const relationType = readString(relatesTo?.rel_type); + const relationEventID = readString(relatesTo?.event_id); + const mappedThreadRootId = + typeof raw.threadRootId === "string" + ? raw.threadRootId + : relationType === "m.thread" && typeof relationEventID === "string" + ? relationEventID + : undefined; + const isThreadRoot = raw.isThreadRoot === true; + const unsignedRelations = isRecord(raw.unsigned?.["m.relations"]) + ? raw.unsigned["m.relations"] + : undefined; + + return makeEvent({ + getId: () => raw.event_id ?? "$raw", + getRoomId: () => raw.room_id ?? "!room:beeper.com", + getTs: () => raw.origin_server_ts ?? 1_700_000_000_000, + getSender: () => raw.sender ?? "@alice:beeper.com", + getType: () => raw.type ?? EventType.RoomMessage, + getContent: () => content, + getRelation: () => + relationType + ? { + rel_type: relationType, + } + : null, + isRelation: (expectedRelType: string) => relationType === expectedRelType, + getServerAggregatedRelation: (expectedRelType: string) => + unsignedRelations?.[expectedRelType], + threadRootId: mappedThreadRootId, + isThreadRoot, + }); +} + +type AdapterInternals = { + deviceID?: string; + e2eeConfig?: Record; + e2eeEnabled?: boolean; + getSecretStorageKeyFromRecoveryKey: (opts: { + keys: Record; + }) => [string, Uint8Array] | null; + loadPersistedSession: () => Promise<{ + accessToken: string; + userID: string; + deviceID?: string; + } | null>; + persistSession: (session: { + accessToken: string; + deviceID?: string; + userID: string; + }) => Promise; + resolveAuth: () => Promise<{ + accessToken: string; + userID: string; + deviceID?: string; + }>; + resolveDeviceID: (...candidates: Array) => Promise; + stateAdapter: StateAdapter | null; +}; + +function asMatrixClient(client: ReturnType): MatrixClient { + if (!isMatrixClient(client)) { + throw new Error("Fake client does not satisfy the MatrixClient contract used in tests"); + } + return client; +} + +function getInternals(adapter: MatrixAdapter): AdapterInternals { + return adapter as unknown as AdapterInternals; +} + function makeClient() { const handlers = new Map void>(); + const store = { + save: vi.fn(async () => undefined), + }; + const crypto = { + exportSecretsBundle: vi.fn(async () => ({ bundle: "secret" })), + importSecretsBundle: vi.fn(async () => undefined), + }; const client = { on: (eventName: string, cb: (...args: unknown[]) => void) => { @@ -32,43 +202,136 @@ function makeClient() { }, startClient: vi.fn(async () => undefined), stopClient: vi.fn(() => undefined), + removeAllListeners: vi.fn(() => undefined), sendMessage: vi.fn(async () => ({ event_id: "$sent" })), sendEvent: vi.fn(async () => ({ event_id: "$reaction" })), + uploadContent: vi.fn(async () => ({ content_uri: "mxc://beeper.com/uploaded" })), redactEvent: vi.fn(async () => ({ event_id: "$redaction" })), sendTyping: vi.fn(async () => ({})), + createRoom: vi.fn(async () => ({ room_id: "!new-dm:beeper.com" })), + joinRoom: vi.fn(async () => ({ room_id: "!joined:beeper.com" })), + createMessagesRequest: vi.fn( + async (): Promise => ({ chunk: [], end: undefined }) + ), + createThreadListMessagesRequest: vi.fn( + async (): Promise => ({ chunk: [], end: undefined }) + ), + fetchRoomEvent: vi.fn(async (): Promise => null), + getAccountDataFromServer: vi.fn( + async (): Promise | null> => null + ), + getAccessToken: vi.fn(() => "token"), + getCrypto: vi.fn(() => crypto), + getEventMapper: vi.fn(() => (raw: Record) => mapRawToEvent(raw)), initRustCrypto: vi.fn(async () => undefined), + mxcUrlToHttp: vi.fn((url: string) => url), + relations: vi.fn( + async (): Promise => ({ + originalEvent: null, + events: [], + nextBatch: null, + prevBatch: null, + }) + ), + setAccountData: vi.fn(async (): Promise> => ({})), decryptEventIfNeeded: vi.fn(async () => undefined), - getRoom: vi.fn(() => ({ - roomId: "!room:beeper.com", - name: "Example Room", - timeline: [], - getJoinedMembers: () => [{}, {}], - findEventById: () => makeEvent({ getId: () => "$sent" }), + getRoom: vi.fn((roomID?: string): RoomLike | null => makeRoom({ + roomId: roomID ?? "!room:beeper.com", })), + store, + __crypto: crypto, __handlers: handlers, }; return client; } -function makeStateAdapter(initial: Record = {}) { - const store = new Map(Object.entries(initial)); +function isMatrixClient(value: unknown): value is MatrixClient { + if (!isRecord(value)) { + return false; + } + + return [ + "createMessagesRequest", + "createRoom", + "getEventMapper", + "getRoom", + "relations", + "sendEvent", + "startClient", + "stopClient", + ].every((key) => typeof Reflect.get(value, key) === "function"); +} + +function makeRoom(overrides: Partial = {}): RoomLike { + return { + roomId: "!room:beeper.com", + name: "Example Room", + timeline: [], + currentState: { + getStateEvents: () => null, + }, + getMember: () => null, + hasEncryptionStateEvent: () => false, + getJoinedMembers: () => [{}, {}], + getMyMembership: () => "join", + findEventById: () => makeEvent({ getId: () => "$sent" }), + ...overrides, + }; +} + +function makeRoomState(events: Record>): RoomStateLike { + return { + getStateEvents: (eventType: string, stateKey: string) => + stateKey === "" && events[eventType] ? makeStateEvent(events[eventType]) : null, + }; +} + +function makeRoomMembers( + members: Record +): RoomLike["getMember"] { + return (userId: string) => members[userId] ?? null; +} + +function makeStateAdapter(initial: Record = {}): StateAdapter { + const base = createMemoryState(); + let ready: Promise | null = null; + const connect: StateAdapter["connect"] = async () => { + ready ??= (async () => { + await base.connect(); + for (const [key, value] of Object.entries(initial)) { + await base.set(key, value); + } + })(); + await ready; + }; + const afterReady = async (run: () => Promise): Promise => { + await (ready ?? connect()); + return run(); + }; + const get: StateAdapter["get"] = (key) => afterReady(() => base.get(key)); + const set: StateAdapter["set"] = (key, value, ttlMs) => + afterReady(() => base.set(key, value, ttlMs)); + const setIfNotExists: StateAdapter["setIfNotExists"] = (key, value, ttlMs) => + afterReady(() => base.setIfNotExists(key, value, ttlMs)); + return { - acquireLock: vi.fn(async () => null), - connect: vi.fn(async () => undefined), - delete: vi.fn(async (key: string) => { - store.delete(key); - }), - disconnect: vi.fn(async () => undefined), - extendLock: vi.fn(async () => false), - get: vi.fn(async (key: string) => (store.has(key) ? store.get(key) : null)), - isSubscribed: vi.fn(async () => false), - releaseLock: vi.fn(async () => undefined), - set: vi.fn(async (key: string, value: unknown) => { - store.set(key, value); - }), - subscribe: vi.fn(async () => undefined), - unsubscribe: vi.fn(async () => undefined), + acquireLock: vi.fn((threadId, ttlMs) => + afterReady(() => base.acquireLock(threadId, ttlMs)) + ), + connect: vi.fn(connect), + delete: vi.fn((key) => afterReady(() => base.delete(key))), + disconnect: vi.fn(() => afterReady(() => base.disconnect())), + extendLock: vi.fn((lock, ttlMs) => + afterReady(() => base.extendLock(lock, ttlMs)) + ), + get, + isSubscribed: vi.fn((threadId) => afterReady(() => base.isSubscribed(threadId))), + releaseLock: vi.fn((lock) => afterReady(() => base.releaseLock(lock))), + set: vi.fn(set), + setIfNotExists: vi.fn(setIfNotExists), + subscribe: vi.fn((threadId) => afterReady(() => base.subscribe(threadId))), + unsubscribe: vi.fn((threadId) => afterReady(() => base.unsubscribe(threadId))), }; } @@ -77,32 +340,78 @@ function markSyncReady(client: ReturnType) { syncHandler?.("PREPARED"); } -function makeChatInstance(overrides: Record = {}) { +function decodeCursorToken(cursor: string): { + dir: "forward" | "backward"; + kind: string; + roomID: string; + rootEventID?: string; + token: string; +} { + if (!cursor.startsWith("mxv1:")) { + throw new Error(`Expected mxv1 cursor, got: ${cursor}`); + } + return JSON.parse(Buffer.from(cursor.slice(5), "base64url").toString("utf8")); +} + +function requireValue(value: T | null | undefined, label: string): T { + if (value === null || value === undefined) { + throw new Error(`Expected ${label} to be present`); + } + return value; +} + +function makeChatInstance( + overrides: Partial & { state?: StateAdapter } = {} +): ChatInstance { + const { state = makeStateAdapter(), ...chatOverrides } = overrides; + const chat = new Chat({ + userName: "test-bot", + adapters: {}, + state, + }); + + Object.assign(chat, chatOverrides); + return chat; +} + +function makeTestLogger() { + const child = vi.fn<(prefix: string) => Logger>(); + const debug = vi.fn<(message: string, ...args: unknown[]) => void>(); + const info = vi.fn<(message: string, ...args: unknown[]) => void>(); + const warn = vi.fn<(message: string, ...args: unknown[]) => void>(); + const error = vi.fn<(message: string, ...args: unknown[]) => void>(); + + const logger: Logger = { + child(prefix) { + child(prefix); + return logger; + }, + debug, + info, + warn, + error, + }; + return { - getLogger: () => - ({ - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - child: () => ({}) as never, - }) as never, - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processAction: vi.fn(), - processAppHomeOpened: vi.fn(), - processAssistantContextChanged: vi.fn(), - processAssistantThreadStarted: vi.fn(), - processMessage: vi.fn(), - processModalClose: vi.fn(), - processModalSubmit: vi.fn(), - processReaction: vi.fn(), - processSlashCommand: vi.fn(), - ...overrides, + logger, + child, + debug, + info, + warn, + error, }; } +async function makeInitializedAdapter(fakeClient: ReturnType) { + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { type: "accessToken", accessToken: "token", userID: "@bot:beeper.com" }, + createClient: () => asMatrixClient(fakeClient), + }); + await adapter.initialize(makeChatInstance()); + return adapter; +} + describe("MatrixAdapter", () => { it("encodes and decodes thread IDs", () => { const adapter = new MatrixAdapter({ @@ -136,13 +445,13 @@ describe("MatrixAdapter", () => { accessToken: "token", userID: "@bot:beeper.com", }, - createClient: () => fakeClient as never, + createClient: () => asMatrixClient(fakeClient), }); const processMessage = vi.fn(); const processSlashCommand = vi.fn(); - await adapter.initialize(makeChatInstance({ processMessage, processSlashCommand }) as never); + await adapter.initialize(makeChatInstance({ processMessage, processSlashCommand })); const timelineHandler = fakeClient.__handlers.get("Room.timeline"); expect(timelineHandler).toBeTruthy(); @@ -166,6 +475,176 @@ describe("MatrixAdapter", () => { }); }); + it("logs and handles unexpected timeline processing failures", async () => { + const fakeClient = makeClient(); + const logger = makeTestLogger(); + const processMessage = vi.fn(() => { + throw new Error("boom"); + }); + + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + logger: logger.logger, + createClient: () => asMatrixClient(fakeClient), + }); + + await adapter.initialize(makeChatInstance({ processMessage })); + + const timelineHandler = fakeClient.__handlers.get("Room.timeline"); + expect(timelineHandler).toBeTruthy(); + markSyncReady(fakeClient); + + timelineHandler?.(makeEvent(), { roomId: "!room:beeper.com" }, false); + await Promise.resolve(); + await Promise.resolve(); + + expect(logger.error).toHaveBeenCalledWith( + "Unhandled Matrix timeline event failure", + expect.objectContaining({ + eventId: "$event", + eventType: EventType.RoomMessage, + roomId: "!room:beeper.com", + error: expect.any(Error), + }) + ); + }); + + it("auto-joins invite events from allowlisted inviters", async () => { + const fakeClient = makeClient(); + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + inviteAutoJoin: { + inviterAllowlist: ["@alice:beeper.com"], + }, + createClient: () => asMatrixClient(fakeClient), + }); + + await adapter.initialize(makeChatInstance()); + const timelineHandler = fakeClient.__handlers.get("Room.timeline"); + + timelineHandler?.( + makeEvent({ + getType: () => EventType.RoomMember, + getSender: () => "@alice:beeper.com", + getStateKey: () => "@bot:beeper.com", + getContent: () => ({ membership: "invite" }), + }), + { roomId: "!invited:beeper.com" }, + false + ); + await Promise.resolve(); + + expect(fakeClient.joinRoom).toHaveBeenCalledWith("!invited:beeper.com"); + }); + + it("retries invite auto-join when the homeserver rate limits the join", async () => { + const fakeClient = makeClient(); + const logger = makeTestLogger(); + fakeClient.joinRoom = vi + .fn() + .mockRejectedValueOnce( + new MatrixError( + { + errcode: "M_LIMIT_EXCEEDED", + error: "Too Many Requests", + retry_after_ms: 0, + }, + 429 + ) + ) + .mockResolvedValueOnce({ room_id: "!invited:beeper.com" }); + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + inviteAutoJoin: { + inviterAllowlist: ["@alice:beeper.com"], + }, + logger: logger.logger, + createClient: () => asMatrixClient(fakeClient), + }); + + await adapter.initialize(makeChatInstance()); + const timelineHandler = fakeClient.__handlers.get("Room.timeline"); + vi.useFakeTimers(); + + try { + timelineHandler?.( + makeEvent({ + getType: () => EventType.RoomMember, + getSender: () => "@alice:beeper.com", + getStateKey: () => "@bot:beeper.com", + getContent: () => ({ membership: "invite" }), + }), + { roomId: "!invited:beeper.com" }, + false + ); + await vi.runAllTimersAsync(); + } finally { + vi.useRealTimers(); + } + + expect(fakeClient.joinRoom).toHaveBeenCalledTimes(2); + expect(fakeClient.joinRoom).toHaveBeenNthCalledWith(1, "!invited:beeper.com"); + expect(fakeClient.joinRoom).toHaveBeenNthCalledWith(2, "!invited:beeper.com"); + expect(logger.warn).toHaveBeenCalledWith( + "Matrix invite auto-join rate limited, retrying", + expect.objectContaining({ + roomId: "!invited:beeper.com", + attempt: 1, + maxAttempts: 3, + retryDelayMs: 0, + error: expect.any(MatrixError), + }) + ); + }); + + it("does not auto-join invite events from non-allowlisted inviters", async () => { + const fakeClient = makeClient(); + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + inviteAutoJoin: { + inviterAllowlist: ["@trusted:beeper.com"], + }, + createClient: () => asMatrixClient(fakeClient), + }); + + await adapter.initialize(makeChatInstance()); + const timelineHandler = fakeClient.__handlers.get("Room.timeline"); + + timelineHandler?.( + makeEvent({ + getType: () => EventType.RoomMember, + getSender: () => "@alice:beeper.com", + getStateKey: () => "@bot:beeper.com", + getContent: () => ({ membership: "invite" }), + }), + { roomId: "!blocked:beeper.com" }, + false + ); + await Promise.resolve(); + + expect(fakeClient.joinRoom).not.toHaveBeenCalled(); + }); + it("maps reaction add and redaction remove events", async () => { const fakeClient = makeClient(); const processReaction = vi.fn(); @@ -177,10 +656,10 @@ describe("MatrixAdapter", () => { accessToken: "token", userID: "@bot:beeper.com", }, - createClient: () => fakeClient as never, + createClient: () => asMatrixClient(fakeClient), }); - await adapter.initialize(makeChatInstance({ processReaction }) as never); + await adapter.initialize(makeChatInstance({ processReaction })); const timelineHandler = fakeClient.__handlers.get("Room.timeline"); markSyncReady(fakeClient); @@ -225,6 +704,61 @@ describe("MatrixAdapter", () => { }); }); + it("routes reactions to thread context when target event belongs to a thread", async () => { + const fakeClient = makeClient(); + fakeClient.getRoom.mockReturnValue( + makeRoom({ + findEventById: (eventId?: string) => + eventId === "$target" + ? makeEvent({ + getId: () => "$target", + getRoomId: () => "!room:beeper.com", + threadRootId: "$root", + }) + : null, + }) + ); + + const processReaction = vi.fn(); + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + createClient: () => asMatrixClient(fakeClient), + }); + await adapter.initialize(makeChatInstance({ processReaction })); + + const timelineHandler = fakeClient.__handlers.get("Room.timeline"); + markSyncReady(fakeClient); + timelineHandler?.( + makeEvent({ + getId: () => "$reaction2", + getType: () => EventType.Reaction, + getContent: () => ({ + "m.relates_to": { + rel_type: RelationType.Annotation, + event_id: "$target", + key: "🔥", + }, + }), + }), + { roomId: "!room:beeper.com" }, + false + ); + await Promise.resolve(); + + expect(processReaction).toHaveBeenCalledOnce(); + expect(processReaction.mock.calls[0][0]).toMatchObject({ + threadId: "matrix:!room%3Abeeper.com:%24root", + messageId: "$target", + emoji: getEmoji("🔥"), + added: true, + }); + }); + it("initializes rust crypto and decrypts encrypted events when e2ee is enabled", async () => { const fakeClient = makeClient(); const processMessage = vi.fn(); @@ -237,11 +771,11 @@ describe("MatrixAdapter", () => { userID: "@bot:beeper.com", }, deviceID: "DEVICE1", - createClient: () => fakeClient as never, - e2ee: { enabled: true }, + createClient: () => asMatrixClient(fakeClient), + e2ee: {}, }); - await adapter.initialize(makeChatInstance({ processMessage }) as never); + await adapter.initialize(makeChatInstance({ processMessage })); expect(fakeClient.initRustCrypto).toHaveBeenCalledOnce(); @@ -261,36 +795,53 @@ describe("MatrixAdapter", () => { }); it("enables e2ee when recovery key is provided", () => { - const adapter = createMatrixAdapter({ - baseURL: "https://hs.beeper.com", - auth: { - type: "accessToken", - accessToken: "token", - userID: "@bot:beeper.com", - }, - recoveryKey: "s3cr3t-recovery-key", - }) as unknown as { e2eeConfig?: { enabled?: boolean } }; + const adapter = getInternals( + createMatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + recoveryKey: "s3cr3t-recovery-key", + }) + ); + + expect(adapter.e2eeEnabled).toBe(true); + }); + + it("enables e2ee when explicit e2ee config is provided", () => { + const adapter = getInternals( + createMatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + e2ee: {}, + }) + ); - expect(adapter.e2eeConfig?.enabled).toBe(true); + expect(adapter.e2eeEnabled).toBe(true); }); it("decodes recovery key for secret storage callback", () => { const recoveryKey = encodeRecoveryKey(new Uint8Array(32).fill(7)); expect(recoveryKey).toBeDefined(); - - const adapter = createMatrixAdapter({ - baseURL: "https://hs.beeper.com", - auth: { - type: "accessToken", - accessToken: "token", - userID: "@bot:beeper.com", - }, - recoveryKey: recoveryKey!, - }) as unknown as { - getSecretStorageKeyFromRecoveryKey: (opts: { - keys: Record; - }) => [string, Uint8Array] | null; - }; + const validatedRecoveryKey = requireValue(recoveryKey, "recoveryKey"); + + const adapter = getInternals( + createMatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + recoveryKey: validatedRecoveryKey, + }) + ); const result = adapter.getSecretStorageKeyFromRecoveryKey({ keys: { @@ -303,70 +854,357 @@ describe("MatrixAdapter", () => { expect(result?.[1]).toBeInstanceOf(Uint8Array); }); - it("generates and persists a device id when one is not provided", async () => { - const adapter = new MatrixAdapter({ - baseURL: "https://hs.beeper.com", - auth: { - type: "accessToken", - accessToken: "token", - userID: "@bot:beeper.com", - }, - }) as unknown as { - stateAdapter: unknown; - resolveDeviceID: () => Promise; - deviceID?: string; - }; - const state = makeStateAdapter(); - adapter.stateAdapter = state as unknown; + it("sends Matrix edit payload with dont_render_edited context", async () => { + const fakeClient = makeClient(); + const adapter = await makeInitializedAdapter(fakeClient); - await adapter.resolveDeviceID(); + await adapter.editMessage( + "matrix:!room%3Abeeper.com", + "$original", + "updated body" + ); - expect(adapter.deviceID).toMatch(/^chatsdk_[A-Z0-9]{8}$/); - expect(state.set).toHaveBeenCalled(); + expect(fakeClient.sendEvent).toHaveBeenCalledWith( + "!room:beeper.com", + EventType.RoomMessage, + expect.objectContaining({ + "com.beeper.dont_render_edited": true, + "m.new_content": { + "com.beeper.dont_render_edited": true, + msgtype: "m.text", + body: "updated body", + }, + "m.relates_to": { + rel_type: RelationType.Replace, + event_id: "$original", + }, + }) + ); }); - it("reuses persisted device id when available", async () => { - const persistedDeviceID = "chatsdk_ABCDEFGH"; - const state = makeStateAdapter({ - "matrix:device:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com": - persistedDeviceID, + it("parses Matrix formatted_body, strips reply fallback, and maps author metadata", async () => { + const fakeClient = makeClient(); + fakeClient.getRoom = vi.fn(() => + makeRoom({ + name: "Product Sync", + getMember: makeRoomMembers({ + "@alice:beeper.com": { + name: "Alice Example", + rawDisplayName: "Alice Example", + }, + }), + }) + ); + fakeClient.fetchRoomEvent = vi.fn(async () => + makeRawEvent({ + event_id: "$formatted", + sender: "@alice:beeper.com", + content: { + body: "> <@alice:beeper.com> replied\n> quoted\n\nHello @bot there", + msgtype: MsgType.Text, + format: "org.matrix.custom.html", + formatted_body: + '
quoted

Hello @bot there

', + "m.mentions": { + user_ids: ["@bot:beeper.com"], + }, + }, + }) + ); + + const adapter = await makeInitializedAdapter(fakeClient); + const message = await adapter.fetchMessage( + "matrix:!room%3Abeeper.com", + "$formatted" + ); + + expect(message).toBeTruthy(); + expect(message?.text).toBe("Hello @bot there"); + expect( + stringifyMarkdown(requireValue(message, "formatted message").formatted).trim() + ).toBe( + "Hello @bot **there**" + ); + expect(message?.isMention).toBe(true); + expect(message?.author).toMatchObject({ + userId: "@alice:beeper.com", + userName: "alice", + fullName: "Alice Example", + isBot: "unknown", + isMe: false, }); + }); - const adapter = new MatrixAdapter({ - baseURL: "https://hs.beeper.com", - auth: { - type: "accessToken", - accessToken: "token", - userID: "@bot:beeper.com", - }, - deviceID: " ", - }) as unknown as { - stateAdapter: unknown; - resolveDeviceID: () => Promise; - deviceID?: string; - }; - adapter.stateAdapter = state as unknown; + it("strips Matrix reply fallback from plain body text", async () => { + const fakeClient = makeClient(); + fakeClient.fetchRoomEvent = vi.fn(async () => + makeRawEvent({ + event_id: "$reply-body", + content: { + body: "> <@alice:beeper.com> replied\n> quoted line\n\nVisible reply body", + msgtype: MsgType.Text, + }, + }) + ); - await adapter.resolveDeviceID(); + const adapter = await makeInitializedAdapter(fakeClient); + const message = await adapter.fetchMessage( + "matrix:!room%3Abeeper.com", + "$reply-body" + ); - expect(adapter.deviceID).toBe(persistedDeviceID); + expect(message?.text).toBe("Visible reply body"); + expect( + stringifyMarkdown(requireValue(message, "reply body message").formatted).trim() + ).toBe( + "Visible reply body" + ); }); - it("supports typed username/password auth config", () => { + it("surfaces editedAt from aggregated replacement metadata", async () => { + const fakeClient = makeClient(); + const editedAt = 1_700_000_123_000; + fakeClient.createMessagesRequest = vi.fn(async () => ({ + chunk: [ + makeRawEvent({ + event_id: "$edited", + content: { + body: "Original body", + msgtype: MsgType.Text, + }, + unsigned: { + "m.relations": { + "m.replace": { + content: { + "m.new_content": { + body: "Edited body", + msgtype: MsgType.Text, + }, + }, + origin_server_ts: editedAt, + }, + }, + }, + }), + ], + })); + + const adapter = await makeInitializedAdapter(fakeClient); + const result = await adapter.fetchMessages("matrix:!room%3Abeeper.com"); + + expect(result.messages).toHaveLength(1); + expect(result.messages[0]?.text).toBe("Edited body"); + expect(result.messages[0]?.metadata.edited).toBe(true); + expect(result.messages[0]?.metadata.editedAt).toEqual(new Date(editedAt)); + }); + + it("renders outbound markdown and mention placeholders as Matrix rich text", async () => { + const fakeClient = makeClient(); + const adapter = await makeInitializedAdapter(fakeClient); + + await adapter.postMessage("matrix:!room%3Abeeper.com", { + markdown: "Hello **team** <@@alice:beeper.com>", + }); + + expect(fakeClient.sendEvent).toHaveBeenCalledWith( + "!room:beeper.com", + EventType.RoomMessage, + expect.objectContaining({ + body: "Hello team @alice", + msgtype: MsgType.Text, + format: "org.matrix.custom.html", + formatted_body: expect.stringContaining("team"), + "m.mentions": { + user_ids: ["@alice:beeper.com"], + }, + }) + ); + expect(fakeClient.sendEvent).toHaveBeenCalledWith( + "!room:beeper.com", + EventType.RoomMessage, + expect.objectContaining({ + formatted_body: expect.stringContaining( + "https://matrix.to/#/%40alice%3Abeeper.com" + ), + }) + ); + }); + + it("enriches thread and channel metadata from Matrix room state", async () => { + const fakeClient = makeClient(); + fakeClient.getRoom = vi.fn(() => + makeRoom({ + name: "Adapter QA", + currentState: makeRoomState({ + "m.room.avatar": { url: "mxc://beeper.com/avatar" }, + "m.room.canonical_alias": { alias: "#adapter:beeper.com" }, + "m.room.encryption": { algorithm: "m.megolm.v1.aes-sha2" }, + "m.room.topic": { topic: "Adapter verification" }, + }), + hasEncryptionStateEvent: () => true, + }) + ); + + const adapter = await makeInitializedAdapter(fakeClient); + const thread = await adapter.fetchThread( + adapter.encodeThreadId({ + roomID: "!room:beeper.com", + rootEventID: "$root", + }) + ); + const channel = await adapter.fetchChannelInfo("matrix:!room%3Abeeper.com"); + + expect(thread.channelName).toBe("Adapter QA"); + expect(thread.metadata).toMatchObject({ + roomID: "!room:beeper.com", + canonicalAlias: "#adapter:beeper.com", + topic: "Adapter verification", + avatarURL: "mxc://beeper.com/avatar", + encrypted: true, + encryptionAlgorithm: "m.megolm.v1.aes-sha2", + isDM: false, + name: "Adapter QA", + }); + expect(channel.name).toBe("Adapter QA"); + expect(channel.metadata).toMatchObject(thread.metadata ?? {}); + }); + + it("uploads file payloads and posts Matrix media events", async () => { + const fakeClient = makeClient(); + fakeClient.sendEvent = vi + .fn() + .mockResolvedValueOnce({ event_id: "$text" }) + .mockResolvedValueOnce({ event_id: "$file" }); + fakeClient.uploadContent = vi + .fn() + .mockResolvedValueOnce({ content_uri: "mxc://beeper.com/file-1" }); + + const adapter = await makeInitializedAdapter(fakeClient); + + await adapter.postMessage("matrix:!room%3Abeeper.com", { + markdown: "File incoming", + files: [ + { + data: new Uint8Array([1, 2, 3]).buffer, + filename: "report.png", + mimeType: "image/png", + }, + ], + }); + + expect(fakeClient.uploadContent).toHaveBeenCalledWith( + expect.any(Blob), + expect.objectContaining({ + name: "report.png", + type: "image/png", + }) + ); + expect(fakeClient.sendEvent).toHaveBeenNthCalledWith( + 1, + "!room:beeper.com", + EventType.RoomMessage, + expect.objectContaining({ + body: "File incoming", + msgtype: "m.text", + }) + ); + expect(fakeClient.sendEvent).toHaveBeenNthCalledWith( + 2, + "!room:beeper.com", + EventType.RoomMessage, + expect.objectContaining({ + body: "report.png", + msgtype: "m.image", + url: "mxc://beeper.com/file-1", + }) + ); + }); + + it("skips invalid file uploads instead of passing malformed entries downstream", async () => { + const fakeClient = makeClient(); + const logger = makeTestLogger(); + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { type: "accessToken", accessToken: "token", userID: "@bot:beeper.com" }, + logger: logger.logger, + createClient: () => asMatrixClient(fakeClient), + }); + await adapter.initialize(makeChatInstance()); + + await adapter.postMessage("matrix:!room%3Abeeper.com", { + files: [ + { data: new Uint8Array([1, 2, 3]), filename: "valid.bin" }, + { data: new Uint8Array([4, 5, 6]), filename: " " }, + { data: "not-binary", filename: "invalid.txt" }, + ], + } as never); + + expect(fakeClient.uploadContent).toHaveBeenCalledTimes(1); + expect(fakeClient.uploadContent).toHaveBeenCalledWith( + expect.any(Blob), + expect.objectContaining({ name: "valid.bin" }) + ); + expect(logger.warn).toHaveBeenCalledWith("Skipping invalid Matrix file upload", { + filename: "invalid.txt", + }); + }); + + it("falls back to a synthetic MatrixEvent when a sent event is not yet in the timeline", async () => { + const fakeClient = makeClient(); + fakeClient.getRoom.mockReturnValue( + makeRoom({ + findEventById: () => null, + }) + ); + fakeClient.sendEvent.mockResolvedValue({ event_id: "$missing" }); + + const adapter = await makeInitializedAdapter(fakeClient); + const sent = await adapter.postMessage("matrix:!room%3Abeeper.com", "hello"); + + expect(sent.id).toBe("$missing"); + expect(sent.raw.getId()).toBe("$missing"); + expect(sent.raw.getRoomId()).toBe("!room:beeper.com"); + expect(sent.raw.getContent()).toMatchObject({ body: "hello", msgtype: "m.text" }); + }); + + it("logs and rethrows Matrix send failures", async () => { + const fakeClient = makeClient(); + const logger = makeTestLogger(); + const sendError = new Error("send failed"); + fakeClient.sendEvent = vi.fn().mockRejectedValue(sendError); + const adapter = new MatrixAdapter({ baseURL: "https://hs.beeper.com", auth: { - type: "password", - username: "bot", - password: "secret", + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", }, + logger: logger.logger, + createClient: () => asMatrixClient(fakeClient), }); + await adapter.initialize(makeChatInstance()); - expect(adapter).toBeInstanceOf(MatrixAdapter); + await expect( + adapter.postMessage("matrix:!room%3Abeeper.com", "hello") + ).rejects.toThrow("send failed"); + expect(logger.error).toHaveBeenCalledWith( + "Matrix send message failed", + expect.objectContaining({ + roomId: "!room:beeper.com", + eventType: EventType.RoomMessage, + msgtype: "m.text", + error: sendError, + }) + ); }); - it("shuts down matrix client cleanly", async () => { + it("logs and rethrows Matrix upload failures", async () => { const fakeClient = makeClient(); + const logger = makeTestLogger(); + const uploadError = new Error("upload failed"); + fakeClient.uploadContent = vi.fn().mockRejectedValue(uploadError); + const adapter = new MatrixAdapter({ baseURL: "https://hs.beeper.com", auth: { @@ -374,54 +1212,283 @@ describe("MatrixAdapter", () => { accessToken: "token", userID: "@bot:beeper.com", }, - createClient: () => fakeClient as never, + logger: logger.logger, + createClient: () => asMatrixClient(fakeClient), + }); + await adapter.initialize(makeChatInstance()); + + await expect( + adapter.postMessage("matrix:!room%3Abeeper.com", { + markdown: "File incoming", + files: [ + { + data: new Uint8Array([1, 2, 3]).buffer, + filename: "report.png", + mimeType: "image/png", + }, + ], + }) + ).rejects.toThrow("upload failed"); + expect(logger.error).toHaveBeenCalledWith( + "Matrix upload content failed", + expect.objectContaining({ + fileName: "report.png", + mimeType: "image/png", + msgtype: "m.image", + error: uploadError, + }) + ); + }); + + it("rejects empty outbound messages instead of posting blank content", async () => { + const fakeClient = makeClient(); + const adapter = await makeInitializedAdapter(fakeClient); + + await expect( + adapter.postMessage("matrix:!room%3Abeeper.com", "") + ).rejects.toThrow("Cannot post an empty Matrix message."); + expect(fakeClient.sendEvent).not.toHaveBeenCalled(); + }); + + it("appends URL-only attachments to message body", async () => { + const fakeClient = makeClient(); + fakeClient.sendEvent = vi.fn(async () => ({ event_id: "$text" })); + const adapter = await makeInitializedAdapter(fakeClient); + + await adapter.postMessage("matrix:!room%3Abeeper.com", { + raw: "See attachment", + attachments: [ + { + type: "file", + name: "spec", + url: "https://example.com/spec.pdf", + }, + ], + }); + + expect(fakeClient.uploadContent).not.toHaveBeenCalled(); + expect(fakeClient.sendEvent).toHaveBeenCalledWith( + "!room:beeper.com", + EventType.RoomMessage, + expect.objectContaining({ + msgtype: "m.text", + body: "See attachment\n\nspec: https://example.com/spec.pdf", + }) + ); + }); + + it("generates and persists a device id when one is not provided", async () => { + const adapter = getInternals( + new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + }) + ); + const state = makeStateAdapter(); + adapter.stateAdapter = state; + + await adapter.resolveDeviceID(); + + expect(adapter.deviceID).toMatch(/^chatsdk_[A-Z0-9]{8}$/); + expect(state.set).toHaveBeenCalled(); + }); + + it("reuses persisted device id when available", async () => { + const persistedDeviceID = "chatsdk_ABCDEFGH"; + const state = makeStateAdapter({ + "matrix:device:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com": + persistedDeviceID, + }); + + const adapter = getInternals( + new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + deviceID: " ", + }) + ); + adapter.stateAdapter = state; + + await adapter.resolveDeviceID(); + + expect(adapter.deviceID).toBe(persistedDeviceID); + }); + + it("supports typed username/password auth config", () => { + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "password", + username: "bot", + password: "secret", + }, }); - await adapter.initialize(makeChatInstance({ getState: vi.fn(() => makeStateAdapter() as never) }) as never); + expect(adapter).toBeInstanceOf(MatrixAdapter); + }); + + it("shuts down matrix client cleanly", async () => { + const fakeClient = makeClient(); + const adapter = await makeInitializedAdapter(fakeClient); await adapter.shutdown(); + expect(fakeClient.store.save).toHaveBeenCalledWith(true); expect(fakeClient.stopClient).toHaveBeenCalledOnce(); }); + it("passes a chat-backed matrix store into custom client creation when state is available", async () => { + const fakeClient = makeClient(); + const createStore = vi.fn(() => ({ + save: vi.fn(async () => undefined), + startup: vi.fn(async () => undefined), + getSyncToken: vi.fn(() => null), + setSyncToken: vi.fn(), + storeRoom: vi.fn(), + setUserCreator: vi.fn(), + getRoom: vi.fn(() => null), + getRooms: vi.fn(() => []), + removeRoom: vi.fn(), + getRoomSummaries: vi.fn(() => []), + storeUser: vi.fn(), + getUser: vi.fn(() => null), + getUsers: vi.fn(() => []), + scrollback: vi.fn(() => []), + storeEvents: vi.fn(), + storeFilter: vi.fn(), + getFilter: vi.fn(() => null), + getFilterIdByName: vi.fn(() => null), + setFilterIdByName: vi.fn(), + storeAccountDataEvents: vi.fn(), + getAccountData: vi.fn(() => undefined), + setSyncData: vi.fn(async () => undefined), + wantsSave: vi.fn(() => false), + getSavedSync: vi.fn(async () => null), + getSavedSyncToken: vi.fn(async () => null), + deleteAllData: vi.fn(async () => undefined), + getOutOfBandMembers: vi.fn(async () => null), + setOutOfBandMembers: vi.fn(async () => undefined), + clearOutOfBandMembers: vi.fn(async () => undefined), + getClientOptions: vi.fn(async () => undefined), + storeClientOptions: vi.fn(async () => undefined), + getPendingEvents: vi.fn(async () => []), + setPendingEvents: vi.fn(async () => undefined), + saveToDeviceBatches: vi.fn(async () => undefined), + getOldestToDeviceBatch: vi.fn(async () => null), + removeToDeviceBatch: vi.fn(async () => undefined), + destroy: vi.fn(async () => undefined), + isNewlyCreated: vi.fn(async () => false), + accountData: new Map(), + })); + const createClient = vi.fn(() => asMatrixClient(fakeClient)); + const state = makeStateAdapter(); + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { type: "accessToken", accessToken: "token", userID: "@bot:beeper.com" }, + createStore, + createClient, + }); + + await adapter.initialize(makeChatInstance({ state })); + + expect(createStore).toHaveBeenCalledWith( + expect.objectContaining({ + scopeKey: expect.stringMatching( + /^matrix:store:https%3A%2F%2Fhs\.beeper\.com:%40bot%3Abeeper\.com:/ + ), + }) + ); + expect(createClient).toHaveBeenCalledWith( + expect.objectContaining({ + baseUrl: "https://hs.beeper.com", + store: createStore.mock.results[0]?.value, + }) + ); + }); + + it("imports and persists secrets bundles when enabled", async () => { + const fakeClient = makeClient(); + fakeClient.__crypto.importSecretsBundle.mockResolvedValue(undefined); + const state = makeStateAdapter({ + "matrix:store:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com:DEVICE1:secrets-bundle": + { bundle: "persisted" }, + }); + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { type: "accessToken", accessToken: "token", userID: "@bot:beeper.com" }, + deviceID: "DEVICE1", + createClient: () => asMatrixClient(fakeClient), + e2ee: {}, + }); + + await adapter.initialize(makeChatInstance({ state })); + await Promise.resolve(); + await Promise.resolve(); + + expect(fakeClient.__crypto.importSecretsBundle).toHaveBeenCalledWith({ + bundle: "persisted", + }); + expect(fakeClient.__crypto.exportSecretsBundle).toHaveBeenCalled(); + expect(state.set).toHaveBeenCalledWith( + "matrix:store:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com:DEVICE1:secrets-bundle", + { bundle: "secret" } + ); + }); + + it("does not fail initialization when secrets bundle import fails", async () => { + const fakeClient = makeClient(); + fakeClient.__crypto.importSecretsBundle.mockRejectedValue(new Error("boom")); + const logger = makeTestLogger(); + const state = makeStateAdapter({ + "matrix:store:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com:DEVICE1:secrets-bundle": + { bundle: "persisted" }, + }); + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { type: "accessToken", accessToken: "token", userID: "@bot:beeper.com" }, + deviceID: "DEVICE1", + createClient: () => asMatrixClient(fakeClient), + e2ee: {}, + logger: logger.logger, + }); + + await expect(adapter.initialize(makeChatInstance({ state }))).resolves.toBeUndefined(); + await Promise.resolve(); + expect(logger.warn).toHaveBeenCalledWith( + "Failed to import persisted Matrix secrets bundle", + expect.objectContaining({ error: expect.any(Error) }) + ); + }); + it("persists and reloads matrix session via chat state", async () => { const baseURL = "https://hs.beeper.com"; const state = makeStateAdapter(); - const adapter = new MatrixAdapter({ + const adapter = getInternals(new MatrixAdapter({ baseURL, auth: { type: "password", username: "bot", password: "secret", }, - }); + })); - (adapter as unknown as { stateAdapter: unknown }).stateAdapter = - state as unknown; + adapter.stateAdapter = state; - await ( - adapter as unknown as { - persistSession: (session: { - accessToken: string; - deviceID?: string; - userID: string; - }) => Promise; - } - ).persistSession({ + await adapter.persistSession({ accessToken: "persisted-token", userID: "@bot:beeper.com", deviceID: "DEVICE1", }); - const restored = await ( - adapter as unknown as { - loadPersistedSession: () => Promise<{ - accessToken: string; - userID: string; - deviceID?: string; - } | null>; - } - ).loadPersistedSession(); + const restored = await adapter.loadPersistedSession(); expect(state.set).toHaveBeenCalled(); expect(restored).toMatchObject({ @@ -432,7 +1499,7 @@ describe("MatrixAdapter", () => { expect(state.set).toHaveBeenCalledWith( "matrix:session:https%3A%2F%2Fhs.beeper.com:username:bot", - expect.any(Object), + expect.objectContaining({}), undefined ); }); @@ -469,20 +1536,12 @@ describe("MatrixAdapter", () => { ({ loginWithPassword, whoami, - }) as never, - }); - - (adapter as unknown as { stateAdapter: unknown }).stateAdapter = - state as unknown; - const resolved = await ( - adapter as unknown as { - resolveAuth: () => Promise<{ - accessToken: string; - userID: string; - deviceID?: string; - }>; - } - ).resolveAuth(); + }), + }); + + const internals = getInternals(adapter); + internals.stateAdapter = state; + const resolved = await internals.resolveAuth(); expect(resolved).toMatchObject({ accessToken: "persisted-token", @@ -529,20 +1588,12 @@ describe("MatrixAdapter", () => { ({ loginWithPassword, whoami, - }) as never, - }); - - (adapter as unknown as { stateAdapter: unknown }).stateAdapter = - state as unknown; - const resolved = await ( - adapter as unknown as { - resolveAuth: () => Promise<{ - accessToken: string; - userID: string; - deviceID?: string; - }>; - } - ).resolveAuth(); + }), + }); + + const internals = getInternals(adapter); + internals.stateAdapter = state; + const resolved = await internals.resolveAuth(); expect(loginWithPassword).toHaveBeenCalledOnce(); expect(resolved).toMatchObject({ @@ -575,20 +1626,12 @@ describe("MatrixAdapter", () => { loginRequest, loginWithPassword, whoami: vi.fn(), - }) as never, - }); - - (adapter as unknown as { stateAdapter: unknown }).stateAdapter = - makeStateAdapter() as unknown; - const resolved = await ( - adapter as unknown as { - resolveAuth: () => Promise<{ - accessToken: string; - userID: string; - deviceID?: string; - }>; - } - ).resolveAuth(); + }), + }); + + const internals = getInternals(adapter); + internals.stateAdapter = makeStateAdapter(); + const resolved = await internals.resolveAuth(); expect(loginRequest).toHaveBeenCalledOnce(); expect(loginRequest).toHaveBeenCalledWith( @@ -604,11 +1647,11 @@ describe("MatrixAdapter", () => { expect(resolved).toMatchObject({ accessToken: "fresh-token", userID: "@bot:beeper.com", - deviceID: "DEVICE2", + deviceID: "DEVICE1", }); }); - it("uses whoami device_id for access token auth", async () => { + it("prefers an explicit deviceID over auth-derived device IDs", async () => { const whoami = vi.fn(async () => ({ user_id: "@bot:beeper.com", device_id: "DEVICE_FROM_WHOAMI", @@ -619,26 +1662,51 @@ describe("MatrixAdapter", () => { auth: { type: "accessToken", accessToken: "token", + userID: "@bot:beeper.com", }, deviceID: "DEVICE_FALLBACK", createBootstrapClient: () => ({ whoami, loginWithPassword: vi.fn(), - }) as never, - }); - - (adapter as unknown as { stateAdapter: unknown }).stateAdapter = - makeStateAdapter() as unknown; - const resolved = await ( - adapter as unknown as { - resolveAuth: () => Promise<{ - accessToken: string; - userID: string; - deviceID?: string; - }>; - } - ).resolveAuth(); + }), + }); + + const internals = getInternals(adapter); + internals.stateAdapter = makeStateAdapter(); + const resolved = await internals.resolveAuth(); + + expect(whoami).toHaveBeenCalledOnce(); + expect(resolved).toMatchObject({ + accessToken: "token", + userID: "@bot:beeper.com", + deviceID: "DEVICE_FALLBACK", + }); + }); + + it("uses whoami device_id for access token auth when no explicit deviceID is provided", async () => { + const whoami = vi.fn(async () => ({ + user_id: "@bot:beeper.com", + device_id: "DEVICE_FROM_WHOAMI", + })); + + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + createBootstrapClient: () => + ({ + whoami, + loginWithPassword: vi.fn(), + }), + }); + + const internals = getInternals(adapter); + internals.stateAdapter = makeStateAdapter(); + const resolved = await internals.resolveAuth(); expect(whoami).toHaveBeenCalledOnce(); expect(resolved).toMatchObject({ @@ -647,4 +1715,640 @@ describe("MatrixAdapter", () => { deviceID: "DEVICE_FROM_WHOAMI", }); }); + + it("falls back to a persisted deviceID when auth does not return one", async () => { + const whoami = vi.fn(async () => ({ + user_id: "@bot:beeper.com", + })); + const state = makeStateAdapter({ + "matrix:device:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com": + "DEVICE_FROM_STATE", + }); + + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { + type: "accessToken", + accessToken: "token", + userID: "@bot:beeper.com", + }, + createBootstrapClient: () => + ({ + whoami, + loginWithPassword: vi.fn(), + }), + }); + + const internals = getInternals(adapter); + internals.stateAdapter = state; + const resolved = await internals.resolveAuth(); + + expect(resolved).toMatchObject({ + accessToken: "token", + userID: "@bot:beeper.com", + deviceID: "DEVICE_FROM_STATE", + }); + }); + + it("rejects legacy cursors for API pagination", async () => { + const fakeClient = makeClient(); + const adapter = await makeInitializedAdapter(fakeClient); + + await expect( + adapter.fetchMessages("matrix:!room%3Abeeper.com", { cursor: "$legacy_cursor" }) + ).rejects.toThrow("Invalid cursor format. Expected mxv1 cursor."); + }); + + it("rejects cursors reused with a different fetch direction", async () => { + const fakeClient = makeClient(); + const adapter = await makeInitializedAdapter(fakeClient); + + const cursor = `mxv1:${Buffer.from( + JSON.stringify({ + dir: "backward", + kind: "room_messages", + roomID: "!room:beeper.com", + token: "room-page-token-1", + }), + "utf8" + ).toString("base64url")}`; + + await expect( + adapter.fetchMessages("matrix:!room%3Abeeper.com", { + direction: "forward", + limit: 10, + cursor, + }) + ).rejects.toThrow("Invalid cursor direction. Expected forward."); + }); + + it("fetches non-thread messages via matrix API with mxv1 cursor", async () => { + const fakeClient = makeClient(); + fakeClient.createMessagesRequest + .mockResolvedValueOnce({ + chunk: [ + makeRawEvent({ + event_id: "$top2", + origin_server_ts: 1_700_000_000_200, + content: { body: "top-2" }, + }), + makeRawEvent({ + event_id: "$reply1", + origin_server_ts: 1_700_000_000_100, + content: { + body: "reply-1", + "m.relates_to": { rel_type: "m.thread", event_id: "$root" }, + }, + }), + makeRawEvent({ + event_id: "$top1", + origin_server_ts: 1_700_000_000_050, + content: { body: "top-1" }, + }), + ], + end: "room-page-token-1", + }) + .mockResolvedValueOnce({ + chunk: [], + end: undefined, + }); + + const adapter = await makeInitializedAdapter(fakeClient); + + const firstPage = await adapter.fetchMessages("matrix:!room%3Abeeper.com", { + direction: "backward", + limit: 10, + }); + + expect(firstPage.messages.map((message) => message.id)).toEqual([ + "$top1", + "$top2", + ]); + expect(firstPage.nextCursor).toBeTruthy(); + const decoded = decodeCursorToken( + requireValue(firstPage.nextCursor, "firstPage.nextCursor") + ); + expect(decoded).toMatchObject({ + kind: "room_messages", + token: "room-page-token-1", + roomID: "!room:beeper.com", + dir: "backward", + }); + + await adapter.fetchMessages("matrix:!room%3Abeeper.com", { + direction: "backward", + limit: 10, + cursor: firstPage.nextCursor, + }); + expect(fakeClient.createMessagesRequest).toHaveBeenNthCalledWith( + 2, + "!room:beeper.com", + "room-page-token-1", + 10, + "b" + ); + }); + + it("fetches thread messages via relations and includes root on first page", async () => { + const fakeClient = makeClient(); + fakeClient.relations.mockResolvedValue({ + originalEvent: null, + events: [ + makeEvent({ + getId: () => "$reply2", + getTs: () => 1_700_000_000_400, + getRoomId: () => "!room:beeper.com", + getContent: () => ({ + body: "reply-2", + "m.relates_to": { rel_type: "m.thread", event_id: "$root" }, + }), + threadRootId: "$root", + isRelation: () => true, + }), + makeEvent({ + getId: () => "$reply1", + getTs: () => 1_700_000_000_300, + getRoomId: () => "!room:beeper.com", + getContent: () => ({ + body: "reply-1", + "m.relates_to": { rel_type: "m.thread", event_id: "$root" }, + }), + threadRootId: "$root", + isRelation: () => true, + }), + ], + nextBatch: "thread-page-token-1", + prevBatch: null, + }); + fakeClient.fetchRoomEvent.mockResolvedValue( + makeRawEvent({ + event_id: "$root", + origin_server_ts: 1_700_000_000_100, + content: { body: "root" }, + }) + ); + + const adapter = await makeInitializedAdapter(fakeClient); + + const page = await adapter.fetchMessages( + "matrix:!room%3Abeeper.com:%24root", + { direction: "forward", limit: 3 } + ); + + expect(page.messages.map((message) => message.id)).toEqual([ + "$root", + "$reply1", + "$reply2", + ]); + expect(page.messages.every((message) => message.threadId.endsWith(":%24root"))).toBe( + true + ); + expect(page.nextCursor).toBeTruthy(); + expect( + decodeCursorToken(requireValue(page.nextCursor, "thread page nextCursor")) + ).toMatchObject({ + kind: "thread_relations", + token: "thread-page-token-1", + roomID: "!room:beeper.com", + rootEventID: "$root", + dir: "forward", + }); + }); + + it("fetches channel-level messages through fetchChannelMessages", async () => { + const fakeClient = makeClient(); + fakeClient.createMessagesRequest.mockResolvedValue({ + chunk: [ + makeRawEvent({ + event_id: "$reply", + origin_server_ts: 1_700_000_000_200, + content: { + body: "reply", + "m.relates_to": { rel_type: "m.thread", event_id: "$root" }, + }, + }), + makeRawEvent({ + event_id: "$top", + origin_server_ts: 1_700_000_000_050, + content: { body: "top" }, + }), + ], + end: "channel-page-token-1", + }); + + const adapter = await makeInitializedAdapter(fakeClient); + + const result = await adapter.fetchChannelMessages?.("matrix:!room%3Abeeper.com", { + direction: "backward", + limit: 20, + }); + + expect(result?.messages.map((message) => message.id)).toEqual(["$top"]); + expect(result?.nextCursor).toBeTruthy(); + const nextCursor = requireValue(result?.nextCursor, "channel nextCursor"); + expect(decodeCursorToken(nextCursor)).toMatchObject({ + kind: "room_messages", + token: "channel-page-token-1", + roomID: "!room:beeper.com", + }); + }); + + it("filters edit relations from room history", async () => { + const fakeClient = makeClient(); + fakeClient.createMessagesRequest.mockResolvedValue({ + chunk: [ + makeRawEvent({ + event_id: "$edit", + origin_server_ts: 1_700_000_000_150, + content: { + body: "edited", + "m.relates_to": { rel_type: RelationType.Replace, event_id: "$top" }, + }, + }), + makeRawEvent({ + event_id: "$top", + origin_server_ts: 1_700_000_000_050, + content: { body: "top" }, + }), + ], + end: undefined, + }); + + const adapter = await makeInitializedAdapter(fakeClient); + + const result = await adapter.fetchMessages("matrix:!room%3Abeeper.com", { + direction: "backward", + limit: 20, + }); + + expect(result.messages.map((message) => message.id)).toEqual(["$top"]); + }); + + it("applies server-aggregated edits to fetched messages", async () => { + const fakeClient = makeClient(); + fakeClient.fetchRoomEvent.mockResolvedValueOnce( + makeRawEvent({ + event_id: "$top", + origin_server_ts: 1_700_000_000_050, + content: { body: "top" }, + unsigned: { + "m.relations": { + [RelationType.Replace]: { + event_id: "$edit", + content: { + body: "* edited", + "m.new_content": { + body: "edited", + msgtype: MsgType.Text, + }, + }, + }, + }, + }, + }) + ); + + const adapter = await makeInitializedAdapter(fakeClient); + + const message = await adapter.fetchMessage?.("matrix:!room%3Abeeper.com", "$top"); + + expect(message?.text).toBe("edited"); + expect(message?.metadata.edited).toBe(true); + }); + + it("fetches a single message in context and returns null for mismatches", async () => { + const fakeClient = makeClient(); + const adapter = await makeInitializedAdapter(fakeClient); + + fakeClient.fetchRoomEvent + .mockResolvedValueOnce( + makeRawEvent({ + event_id: "$root", + origin_server_ts: 1_700_000_000_000, + content: { body: "root" }, + }) + ) + .mockResolvedValueOnce( + makeRawEvent({ + event_id: "$reply-other-thread", + origin_server_ts: 1_700_000_000_000, + content: { + body: "wrong thread", + "m.relates_to": { rel_type: "m.thread", event_id: "$another-root" }, + }, + }) + ); + const message = await adapter.fetchMessage?.("matrix:!room%3Abeeper.com:%24root", "$root"); + expect(message?.id).toBe("$root"); + expect(message?.threadId).toBe("matrix:!room%3Abeeper.com:%24root"); + const mismatch = await adapter.fetchMessage?.( + "matrix:!room%3Abeeper.com:%24root", + "$reply-other-thread" + ); + expect(mismatch).toBeNull(); + }); + + it("fetchMessage returns null when server returns M_NOT_FOUND", async () => { + const fakeClient = makeClient(); + const adapter = await makeInitializedAdapter(fakeClient); + fakeClient.fetchRoomEvent.mockRejectedValueOnce( + new MatrixError({ errcode: "M_NOT_FOUND", error: "Event not found" }, 404) + ); + const result = await adapter.fetchMessage?.( + "matrix:!room%3Abeeper.com:%24root", + "$missing" + ); + expect(result).toBeNull(); + }); + + it("fetchMessage propagates transient server errors", async () => { + const fakeClient = makeClient(); + const adapter = await makeInitializedAdapter(fakeClient); + fakeClient.fetchRoomEvent.mockRejectedValueOnce( + new MatrixError({ errcode: "M_UNKNOWN", error: "Internal server error" }, 500) + ); + await expect( + adapter.fetchMessage?.("matrix:!room%3Abeeper.com:%24root", "$event") + ).rejects.toThrow("Internal server error"); + }); + + it("preserves attachment metadata and fetchData for Matrix media events", async () => { + const fakeClient = makeClient(); + fakeClient.fetchRoomEvent.mockResolvedValueOnce( + makeRawEvent({ + event_id: "$file", + origin_server_ts: 1_700_000_000_000, + content: { + body: "report.txt", + msgtype: MsgType.File, + url: "mxc://beeper.com/file-1", + info: { + mimetype: "text/plain", + size: 7, + }, + }, + }) + ); + fakeClient.mxcUrlToHttp.mockReturnValueOnce( + "https://hs.beeper.com/_matrix/client/v1/media/download/beeper.com/file-1" + ); + + const fetchMock = vi.fn(async () => + new Response(Buffer.from("payload"), { + status: 200, + headers: { + "Content-Type": "text/plain", + }, + }) + ); + vi.stubGlobal("fetch", fetchMock); + + try { + const adapter = await makeInitializedAdapter(fakeClient); + const message = await adapter.fetchMessage?.( + "matrix:!room%3Abeeper.com", + "$file" + ); + + expect(message?.attachments).toHaveLength(1); + expect(message?.attachments[0]).toMatchObject({ + type: "file", + name: "report.txt", + mimeType: "text/plain", + size: 7, + url: "mxc://beeper.com/file-1", + }); + await expect(message?.attachments[0]?.fetchData?.()).resolves.toEqual( + Buffer.from("payload") + ); + expect(fakeClient.mxcUrlToHttp).toHaveBeenCalledWith( + "mxc://beeper.com/file-1", + undefined, + undefined, + undefined, + true, + true, + true + ); + expect(fetchMock).toHaveBeenCalledWith( + "https://hs.beeper.com/_matrix/client/v1/media/download/beeper.com/file-1", + { + headers: { + Authorization: "Bearer token", + }, + } + ); + } finally { + vi.unstubAllGlobals(); + } + }); + + it("openDM reuses a cached mapping", async () => { + const fakeClient = makeClient(); + const cachedState = makeStateAdapter({ + "matrix:dm:%40bob%3Abeeper.com": "!cached-dm:beeper.com", + }); + const adapterFromCache = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { type: "accessToken", accessToken: "token", userID: "@bot:beeper.com" }, + createClient: () => asMatrixClient(fakeClient), + }); + await adapterFromCache.initialize(makeChatInstance({ state: cachedState })); + const cachedThread = await adapterFromCache.openDM("@bob:beeper.com"); + expect(cachedThread).toBe("matrix:!cached-dm%3Abeeper.com"); + expect(fakeClient.createRoom).not.toHaveBeenCalled(); + expect(fakeClient.setAccountData).not.toHaveBeenCalled(); + }); + + it("openDM reuses the m.direct mapping and caches it locally", async () => { + const fakeClient = makeClient(); + const directState = makeStateAdapter(); + fakeClient.getAccountDataFromServer.mockResolvedValue({ + "@bob:beeper.com": ["!from-direct:beeper.com"], + }); + const adapterFromDirect = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { type: "accessToken", accessToken: "token", userID: "@bot:beeper.com" }, + createClient: () => asMatrixClient(fakeClient), + }); + await adapterFromDirect.initialize(makeChatInstance({ state: directState })); + const directThread = await adapterFromDirect.openDM("@bob:beeper.com"); + expect(directThread).toBe("matrix:!from-direct%3Abeeper.com"); + expect(directState.set).toHaveBeenCalledWith( + "matrix:dm:%40bob%3Abeeper.com", + "!from-direct:beeper.com" + ); + expect(fakeClient.createRoom).not.toHaveBeenCalled(); + expect(fakeClient.setAccountData).not.toHaveBeenCalled(); + }); + + it("openDM creates and persists a DM when no mapping exists", async () => { + const fakeClient = makeClient(); + const createState = makeStateAdapter(); + fakeClient.getAccountDataFromServer.mockResolvedValue({}); + fakeClient.createRoom.mockResolvedValue({ room_id: "!created-dm:beeper.com" }); + fakeClient.setAccountData.mockResolvedValue({}); + const adapterCreate = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { type: "accessToken", accessToken: "token", userID: "@bot:beeper.com" }, + createClient: () => asMatrixClient(fakeClient), + }); + await adapterCreate.initialize(makeChatInstance({ state: createState })); + const createdThread = await adapterCreate.openDM("@bob:beeper.com"); + expect(createdThread).toBe("matrix:!created-dm%3Abeeper.com"); + expect(fakeClient.createRoom).toHaveBeenCalledWith({ + invite: ["@bob:beeper.com"], + is_direct: true, + }); + expect(fakeClient.setAccountData).toHaveBeenCalledWith(EventType.Direct, { + "@bob:beeper.com": ["!created-dm:beeper.com"], + }); + expect(createState.set).toHaveBeenCalledWith( + "matrix:dm:%40bob%3Abeeper.com", + "!created-dm:beeper.com" + ); + }); + + it("openDM skips direct mappings for rooms the bot already left", async () => { + const fakeClient = makeClient(); + fakeClient.getAccountDataFromServer.mockResolvedValue({ + "@bob:beeper.com": ["!stale-dm:beeper.com", "!active-dm:beeper.com"], + }); + fakeClient.getRoom.mockImplementation((roomID?: string) => { + if (roomID === "!stale-dm:beeper.com") { + return makeRoom({ + roomId: roomID, + name: "Stale DM", + getMyMembership: () => "leave", + }); + } + if (roomID === "!active-dm:beeper.com") { + return makeRoom({ + roomId: roomID, + name: "Active DM", + getMyMembership: () => "join", + }); + } + return null; + }); + + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { type: "accessToken", accessToken: "token", userID: "@bot:beeper.com" }, + createClient: () => asMatrixClient(fakeClient), + }); + await adapter.initialize(makeChatInstance()); + + const threadID = await adapter.openDM("@bob:beeper.com"); + + expect(threadID).toBe("matrix:!active-dm%3Abeeper.com"); + expect(fakeClient.createRoom).not.toHaveBeenCalled(); + }); + + it("openDM clears stale cached mappings before creating a fresh DM", async () => { + const fakeClient = makeClient(); + fakeClient.getRoom.mockImplementation((roomID?: string) => { + if (roomID === "!stale-dm:beeper.com") { + return null; + } + return makeRoom({ roomId: roomID ?? "!room:beeper.com" }); + }); + fakeClient.getAccountDataFromServer.mockResolvedValue({}); + fakeClient.createRoom.mockResolvedValue({ room_id: "!fresh-dm:beeper.com" }); + + const state = makeStateAdapter({ + "matrix:dm:%40bob%3Abeeper.com": "!stale-dm:beeper.com", + }); + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { type: "accessToken", accessToken: "token", userID: "@bot:beeper.com" }, + createClient: () => asMatrixClient(fakeClient), + }); + + await adapter.initialize(makeChatInstance({ state })); + const threadId = await adapter.openDM("@bob:beeper.com"); + + expect(threadId).toBe("matrix:!fresh-dm%3Abeeper.com"); + expect(state.delete).toHaveBeenCalledWith("matrix:dm:%40bob%3Abeeper.com"); + expect(fakeClient.createRoom).toHaveBeenCalledOnce(); + }); + + it("merges fresh m.direct account data before persisting a newly created DM", async () => { + const fakeClient = makeClient(); + fakeClient.getAccountDataFromServer + .mockResolvedValueOnce({}) + .mockResolvedValueOnce({ + "@bob:beeper.com": ["!existing-dm:beeper.com"], + "@carol:beeper.com": ["!carol-dm:beeper.com"], + }); + fakeClient.createRoom.mockResolvedValue({ room_id: "!new-dm:beeper.com" }); + + const adapter = new MatrixAdapter({ + baseURL: "https://hs.beeper.com", + auth: { type: "accessToken", accessToken: "token", userID: "@bot:beeper.com" }, + createClient: () => asMatrixClient(fakeClient), + }); + + await adapter.initialize(makeChatInstance({ state: makeStateAdapter() })); + await adapter.openDM("@bob:beeper.com"); + + expect(fakeClient.setAccountData).toHaveBeenCalledWith(EventType.Direct, { + "@bob:beeper.com": ["!existing-dm:beeper.com", "!new-dm:beeper.com"], + "@carol:beeper.com": ["!carol-dm:beeper.com"], + }); + }); + + it("lists threads using server-side thread API with mxv1 cursor", async () => { + const fakeClient = makeClient(); + fakeClient.createThreadListMessagesRequest.mockResolvedValue({ + chunk: [ + makeRawEvent({ + event_id: "$root2", + origin_server_ts: 1_700_000_000_200, + content: { body: "Root 2" }, + unsigned: { + "m.relations": { + "m.thread": { + count: 4, + latest_event: { origin_server_ts: 1_700_000_000_900 }, + }, + }, + }, + }), + makeRawEvent({ + event_id: "$root1", + origin_server_ts: 1_700_000_000_100, + content: { body: "Root 1" }, + unsigned: { + "m.relations": { + "m.thread": { + count: 2, + latest_event: { origin_server_ts: 1_700_000_000_500 }, + }, + }, + }, + }), + ], + end: "thread-list-page-token-1", + }); + + const adapter = await makeInitializedAdapter(fakeClient); + + const result = await adapter.listThreads("matrix:!room%3Abeeper.com", { limit: 2 }); + + expect(result.threads.map((thread) => thread.id)).toEqual([ + "matrix:!room%3Abeeper.com:%24root2", + "matrix:!room%3Abeeper.com:%24root1", + ]); + expect(result.threads.map((thread) => thread.replyCount)).toEqual([4, 2]); + expect(result.nextCursor).toBeTruthy(); + expect( + decodeCursorToken(requireValue(result.nextCursor, "thread list nextCursor")) + ).toMatchObject({ + kind: "thread_list", + token: "thread-list-page-token-1", + roomID: "!room:beeper.com", + dir: "backward", + }); + }); }); diff --git a/src/index.ts b/src/index.ts index 3c2614c..f22131d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,11 @@ import { randomBytes } from "node:crypto"; import type { Adapter, AdapterPostableMessage, + Attachment, ChannelInfo, ChatInstance, EmojiValue, + FileUpload, FetchOptions, FetchResult, FormattedContent, @@ -22,41 +24,70 @@ import { getEmoji, isCardElement, Message, + markdownToPlainText, parseMarkdown, stringifyMarkdown, } from "chat"; import sdk, { + Direction, + MatrixEvent, + type ICreateClientOpts, type MatrixClient, - type MatrixEvent, + type IEvent, + type IThreadBundledRelationship, type Room, + type RoomMember, ClientEvent, EventType, MsgType, RelationType, RoomEvent, + SyncState, + ThreadFilterType, + THREAD_RELATION_TYPE, } from "matrix-js-sdk"; +import type { IStore } from "matrix-js-sdk/lib/store"; +import type { + RoomMessageEventContent, + RoomMessageTextEventContent, +} from "matrix-js-sdk/lib/@types/events"; +import type { MediaEventContent } from "matrix-js-sdk/lib/@types/media"; +import { MatrixError } from "matrix-js-sdk/lib/http-api/errors"; import { decodeRecoveryKey } from "matrix-js-sdk/lib/crypto-api/recovery-key"; import { logger as matrixSDKLogger } from "matrix-js-sdk/lib/logger"; +import { marked } from "marked"; +import { + HTMLElement, + NodeType, + parse as parseHTML, + type Node as HTMLNode, +} from "node-html-parser"; +import { ChatStateMatrixStore } from "./store/chat-state-matrix-store"; import type { MatrixAuthBootstrapClient, MatrixAccessTokenAuthConfig, MatrixAdapterConfig, MatrixAuthConfig, + MatrixCreateStoreOptions, + MatrixPersistenceConfig, + MatrixPersistenceSyncConfig, MatrixThreadID, } from "./types"; const MATRIX_PREFIX = "matrix"; -const MATRIX_DEVICE_PREFIX = "matrix:device"; -const MATRIX_SESSION_PREFIX = "matrix:session"; +const MATRIX_CURSOR_PREFIX = "mxv1:"; const DEFAULT_COMMAND_PREFIX = "/"; +const DEFAULT_PERSISTENCE_KEY_PREFIX = "matrix"; const TYPING_TIMEOUT_MS = 30_000; +const DEFAULT_MATRIX_STORE_PERSIST_INTERVAL_MS = 30_000; const FAST_SYNC_DEFAULTS: NonNullable = { initialSyncLimit: 1, lazyLoadMembers: true, disablePresence: true, pollTimeout: 10_000, }; -const MATRIX_SDK_LOG_LEVELS: Record = { +type SDKLogLevel = NonNullable; +const MATRIX_SDK_LOG_LEVELS: Record = { trace: 0, debug: 1, info: 2, @@ -70,9 +101,26 @@ type MatrixMessageContent = { format?: string; formatted_body?: string; msgtype?: string; + "m.mentions"?: { + room?: boolean; + user_ids?: string[]; + }; [key: string]: unknown; }; +type MatrixTextMessageContent = RoomMessageTextEventContent & { + "com.beeper.dont_render_edited"?: boolean; +}; + +type MatrixRoomMessageContent = RoomMessageEventContent & { + "com.beeper.dont_render_edited"?: boolean; + "m.new_content"?: RoomMessageEventContent & { + "com.beeper.dont_render_edited"?: boolean; + }; +}; + +type MatrixOutboundMessageContent = MatrixRoomMessageContent | MediaEventContent; + type StoredReaction = { emoji: EmojiValue; messageID: string; @@ -88,6 +136,13 @@ type ResolvedAuth = { userID: string; }; +type ResolvedPersistenceConfig = { + keyPrefix: string; + session: Pick, "decrypt" | "encrypt" | "ttlMs">; + sync: Required, "persistIntervalMs">> & + Pick, "snapshotTtlMs">; +}; + type StoredSession = { accessToken?: string; authType: MatrixAuthConfig["type"]; @@ -102,11 +157,67 @@ type StoredSession = { username?: string; }; -type DeviceIDPersistenceConfig = { - enabled: boolean; - key?: string; +type CursorKind = "room_messages" | "thread_relations" | "thread_list"; + +type CursorDirection = "forward" | "backward"; + +type CursorV1Payload = { + dir: CursorDirection; + kind: CursorKind; + roomID: string; + rootEventID?: string; + token: string; +}; + +type DirectAccountData = Record; + +type OutboundUpload = { + data: Blob; + fileName: string; + info?: { + h?: number; + mimetype?: string; + size?: number; + w?: number; + }; + msgtype: MatrixMediaMsgType; + type?: string; +}; + +type MatrixMediaMsgType = + | MsgType.Audio + | MsgType.File + | MsgType.Image + | MsgType.Video; + +type ParsedMatrixContent = { + markdown: string; + mentionsRoom: boolean; + mentionedUserIDs: Set; + text: string; +}; + +type RenderedMatrixMessage = { + body: string; + formattedBody?: string; + mentions?: { + room?: boolean; + user_ids?: string[]; + }; +}; + +type MatrixRoomMetadata = { + avatarURL?: string; + canonicalAlias?: string; + encrypted: boolean; + encryptionAlgorithm?: string; + isDM: boolean; + name?: string; + roomID: string; + topic?: string; }; +// Intentionally unsupported in this adapter: postEphemeral, openModal, and native stream. export class MatrixAdapter implements Adapter { readonly name = "matrix"; readonly userName: string; @@ -115,33 +226,32 @@ export class MatrixAdapter implements Adapter { private readonly auth: MatrixAuthConfig; private readonly commandPrefix: string; private readonly roomAllowlist?: Set; + private readonly inviteAutoJoinEnabled: boolean; + private readonly inviteAutoJoinInviterAllowlist?: Set; private readonly syncOptions?: MatrixAdapterConfig["sync"]; private readonly createClientFn?: MatrixAdapterConfig["createClient"]; + private readonly createStoreFn?: MatrixAdapterConfig["createStore"]; private readonly createBootstrapClientFn?: MatrixAdapterConfig["createBootstrapClient"]; private readonly e2eeConfig?: MatrixAdapterConfig["e2ee"]; + private readonly e2eeEnabled: boolean; + private readonly persistenceConfig: ResolvedPersistenceConfig; private readonly recoveryKey?: string; private readonly matrixSDKLogLevel?: MatrixAdapterConfig["matrixSDKLogLevel"]; - private readonly deviceIDPersistence: DeviceIDPersistenceConfig; private readonly loggerProvided: boolean; - private readonly sessionConfig: Required< - Pick, "enabled"> - > & - Pick< - NonNullable, - "decrypt" | "encrypt" | "key" | "ttlMs" - >; private logger: Logger; private chat: ChatInstance | null = null; private stateAdapter: StateAdapter | null = null; private client: MatrixClient | null = null; + private matrixStoreScopeKey: string | null = null; private started = false; private userID: string; private deviceID?: string; - private botUserID?: string; private readonly reactionByEventID = new Map(); private readonly myReactionByKey = new Map(); private readonly processedTimelineEventIDs = new Set(); + private lastSecretsBundlePersistAt = 0; + private secretsBundleUnavailableLogged = false; private liveSyncReady = false; private shuttingDown = false; @@ -149,39 +259,34 @@ export class MatrixAdapter implements Adapter { this.validateConfig(config); this.baseURL = config.baseURL; this.auth = config.auth; - this.userID = - config.auth.type === "accessToken" - ? (config.auth.userID ?? "") - : (config.auth.userID ?? ""); - this.botUserID = this.userID || undefined; + this.userID = config.auth.userID ?? ""; this.deviceID = normalizeOptionalString(config.deviceID); this.userName = config.userName ?? "bot"; this.commandPrefix = config.commandPrefix ?? DEFAULT_COMMAND_PREFIX; this.roomAllowlist = config.roomAllowlist ? new Set(config.roomAllowlist) : undefined; + const inviteAutoJoinInviterAllowlist = normalizeStringList( + config.inviteAutoJoin?.inviterAllowlist + ); + this.inviteAutoJoinEnabled = Boolean(config.inviteAutoJoin); + this.inviteAutoJoinInviterAllowlist = + inviteAutoJoinInviterAllowlist.length > 0 + ? new Set(inviteAutoJoinInviterAllowlist) + : undefined; this.syncOptions = config.sync ?? FAST_SYNC_DEFAULTS; this.createClientFn = config.createClient; + this.createStoreFn = config.createStore; this.createBootstrapClientFn = config.createBootstrapClient; + this.e2eeEnabled = Boolean(config.recoveryKey || config.e2ee); this.e2eeConfig = { ...config.e2ee, - enabled: config.e2ee?.enabled ?? Boolean(config.recoveryKey), storagePassword: config.e2ee?.storagePassword ?? config.recoveryKey, }; + this.persistenceConfig = normalizePersistenceConfig(config.persistence); this.recoveryKey = normalizeOptionalString(config.recoveryKey); this.matrixSDKLogLevel = config.matrixSDKLogLevel; - this.deviceIDPersistence = { - enabled: config.deviceIDPersistence?.enabled ?? true, - key: normalizeOptionalString(config.deviceIDPersistence?.key), - }; - this.sessionConfig = { - decrypt: config.session?.decrypt, - enabled: config.session?.enabled ?? true, - encrypt: config.session?.encrypt, - key: config.session?.key, - ttlMs: config.session?.ttlMs, - }; this.loggerProvided = Boolean(config.logger); this.logger = config.logger ?? new ConsoleLogger("info").child("matrix"); } @@ -197,33 +302,39 @@ export class MatrixAdapter implements Adapter { } this.stateAdapter = chat.getState(); this.configureMatrixSDKLogging(); - await this.resolveDeviceID(); + let resolvedAuth: ResolvedAuth | null = null; if (this.createClientFn) { - this.client = this.createClientFn(); + await this.resolveDeviceID(); + const store = await this.maybeCreateMatrixStore(); + this.matrixStoreScopeKey = this.resolveMatrixStoreContext()?.scopeKey ?? null; + const clientOptions = this.buildCustomClientOptions(store); + this.client = this.createClientFn(clientOptions); } else { - const resolvedAuth = await this.resolveAuth(); + resolvedAuth = await this.resolveAuth(); this.userID = resolvedAuth.userID; - this.botUserID = resolvedAuth.userID; this.deviceID = normalizeOptionalString(resolvedAuth.deviceID) ?? this.deviceID; - this.client = this.buildClient(resolvedAuth); + const store = await this.maybeCreateMatrixStore(resolvedAuth); + this.matrixStoreScopeKey = + this.resolveMatrixStoreContext(resolvedAuth)?.scopeKey ?? null; + this.client = this.buildClient(resolvedAuth, store); } this.client.on(ClientEvent.Sync, (state: string) => { - if (state === "PREPARED" || state === "SYNCING") { + if (state === SyncState.Prepared || state === SyncState.Syncing) { this.liveSyncReady = true; } this.logger.debug("Matrix sync state", { state }); }); this.client.on(RoomEvent.Timeline, (event, room, toStartOfTimeline) => { - void this.onTimelineEvent(event, room, Boolean(toStartOfTimeline)); + this.dispatchTimelineEvent(event, room, Boolean(toStartOfTimeline)); }); this.client.on(ClientEvent.Event, (event) => { if (!event.getRoomId()) { return; } - void this.onTimelineEvent(event, undefined, false); + this.dispatchTimelineEvent(event, undefined, false); }); await this.maybeInitE2EE(); @@ -231,13 +342,13 @@ export class MatrixAdapter implements Adapter { this.started = true; this.logger.info("Matrix adapter initialized", { - userID: this.userID, - baseURL: this.baseURL, + userId: this.userID, + baseUrl: this.baseURL, }); } get botUserId(): string | undefined { - return this.botUserID; + return this.userID || undefined; } async shutdown(): Promise { @@ -247,9 +358,13 @@ export class MatrixAdapter implements Adapter { this.shuttingDown = true; try { + await this.maybePersistSecretsBundle(true); + await this.maybeFlushMatrixStore(); + this.client.removeAllListeners(); this.client.stopClient(); this.reactionByEventID.clear(); this.myReactionByKey.clear(); + this.client = null; this.started = false; this.logger.info("Matrix adapter shutdown complete"); } finally { @@ -289,7 +404,7 @@ export class MatrixAdapter implements Adapter { channelIdFromThreadId(threadId: string): string { const { roomID } = this.decodeThreadId(threadId); - return `${MATRIX_PREFIX}:${encodeURIComponent(roomID)}`; + return this.encodeThreadId({ roomID }); } renderFormatted(content: FormattedContent): string { @@ -297,30 +412,7 @@ export class MatrixAdapter implements Adapter { } parseMessage(raw: MatrixEvent): Message { - const roomID = raw.getRoomId(); - if (!roomID) { - throw new Error("Matrix event missing room ID"); - } - - const threadID = this.threadIDForEvent(raw, roomID); - const content = raw.getContent(); - const text = this.extractText(content); - const sender = raw.getSender() ?? "unknown"; - - return new Message({ - id: raw.getId() ?? `${roomID}:${raw.getTs()}`, - threadId: threadID, - text, - formatted: parseMarkdown(text), - author: this.makeUser(sender), - metadata: { - dateSent: new Date(raw.getTs()), - edited: this.isEdited(raw), - }, - attachments: this.extractAttachments(content), - raw, - isMention: this.isMentioned(content, text), - }); + return this.parseMessageInternal(raw); } async postMessage( @@ -328,14 +420,24 @@ export class MatrixAdapter implements Adapter { message: AdapterPostableMessage ): Promise> { const { roomID, rootEventID } = this.decodeThreadId(threadId); - const content = this.toRoomMessageContent(message); - - const response = await this.sendRoomMessage(roomID, rootEventID, content); + const contents = await this.toRoomMessageContents(message); + const [firstContent, ...extraContents] = contents; + if (!firstContent) { + throw new Error("Cannot post an empty Matrix message."); + } + const response = await this.sendRoomMessage(roomID, rootEventID, firstContent); + for (const content of extraContents) { + await this.sendRoomMessage(roomID, rootEventID, content); + } return { id: response.event_id, threadId, - raw: this.mustGetEventByID(roomID, response.event_id), + raw: this.resolveSentEvent(roomID, response.event_id, { + content: firstContent, + roomID, + sender: this.userID, + }), }; } @@ -343,7 +445,7 @@ export class MatrixAdapter implements Adapter { channelId: string, message: AdapterPostableMessage ): Promise> { - const roomID = this.decodeChannelID(channelId); + const roomID = this.decodeThreadId(channelId).roomID; return this.postMessage(this.encodeThreadId({ roomID }), message); } @@ -354,30 +456,45 @@ export class MatrixAdapter implements Adapter { ): Promise> { const { roomID, rootEventID } = this.decodeThreadId(threadId); const baseContent = this.toRoomMessageContent(message); + const newContent: MatrixTextMessageContent = { + ...baseContent, + "com.beeper.dont_render_edited": true, + }; - const response = await this.sendRoomMessage(roomID, rootEventID, { - "m.new_content": { - msgtype: baseContent.msgtype, - body: baseContent.body, - }, + const editContent: MatrixRoomMessageContent = { + "com.beeper.dont_render_edited": true, + "m.new_content": newContent, "m.relates_to": { rel_type: RelationType.Replace, event_id: messageId, }, - msgtype: baseContent.msgtype, + msgtype: newContent.msgtype, body: `* ${baseContent.body}`, - }); + }; + + const response = await this.sendRoomMessage(roomID, rootEventID, editContent); return { id: response.event_id, threadId, - raw: this.mustGetEventByID(roomID, response.event_id), + raw: this.resolveSentEvent(roomID, response.event_id, { + content: editContent, + roomID, + sender: this.userID, + }), }; } async deleteMessage(threadId: string, messageId: string): Promise { const { roomID } = this.decodeThreadId(threadId); - await this.requireClient().redactEvent(roomID, messageId); + await this.withLoggedMatrixOperation( + "Matrix redact message failed", + { + roomId: roomID, + eventId: messageId, + }, + () => this.requireClient().redactEvent(roomID, messageId) + ); } async addReaction( @@ -386,19 +503,24 @@ export class MatrixAdapter implements Adapter { emoji: EmojiValue | string ): Promise { const { roomID, rootEventID } = this.decodeThreadId(threadId); - const rawEmoji = typeof emoji === "string" ? emoji : emoji.toString(); + const rawEmoji = this.rawEmoji(emoji); - const response = await this.requireClient().sendEvent( - roomID, - rootEventID ?? null, - EventType.Reaction, + const response = await this.withLoggedMatrixOperation( + "Matrix send reaction failed", { - "m.relates_to": { - rel_type: RelationType.Annotation, - event_id: messageId, - key: rawEmoji, - }, - } + roomId: roomID, + rootEventId: rootEventID, + messageId, + emoji: rawEmoji, + }, + () => + this.requireClient().sendEvent(roomID, rootEventID ?? null, EventType.Reaction, { + "m.relates_to": { + rel_type: RelationType.Annotation, + event_id: messageId, + key: rawEmoji, + }, + }) ); const key = this.myReactionKey(threadId, messageId, rawEmoji); @@ -410,7 +532,7 @@ export class MatrixAdapter implements Adapter { messageId: string, emoji: EmojiValue | string ): Promise { - const rawEmoji = typeof emoji === "string" ? emoji : emoji.toString(); + const rawEmoji = this.rawEmoji(emoji); const reactionEventID = this.myReactionByKey.get( this.myReactionKey(threadId, messageId, rawEmoji) ); @@ -420,12 +542,67 @@ export class MatrixAdapter implements Adapter { } const { roomID } = this.decodeThreadId(threadId); - await this.requireClient().redactEvent(roomID, reactionEventID); + await this.withLoggedMatrixOperation( + "Matrix remove reaction failed", + { + roomId: roomID, + reactionEventId: reactionEventID, + messageId, + emoji: rawEmoji, + }, + () => this.requireClient().redactEvent(roomID, reactionEventID) + ); + this.myReactionByKey.delete(this.myReactionKey(threadId, messageId, rawEmoji)); } async startTyping(threadId: string): Promise { const { roomID } = this.decodeThreadId(threadId); - await this.requireClient().sendTyping(roomID, true, TYPING_TIMEOUT_MS); + await this.withLoggedMatrixOperation( + "Matrix typing request failed", + { + roomId: roomID, + timeoutMs: TYPING_TIMEOUT_MS, + }, + () => this.requireClient().sendTyping(roomID, true, TYPING_TIMEOUT_MS) + ); + } + + async openDM(userId: string): Promise { + const cachedRoomID = await this.loadPersistedDMRoomID(userId); + if (cachedRoomID) { + if (this.isUsableDirectRoom(cachedRoomID)) { + return this.encodeThreadId({ roomID: cachedRoomID }); + } + await this.clearPersistedDMRoomID(userId); + } + + const direct = await this.loadDirectAccountData(); + const existingRoomID = this.findExistingDirectRoomID(direct, userId); + if (existingRoomID) { + await this.persistDMRoomID(userId, existingRoomID); + return this.encodeThreadId({ roomID: existingRoomID }); + } + + const response = await this.withLoggedMatrixOperation( + "Matrix create DM room failed", + { + userId, + }, + () => + this.requireClient().createRoom({ + invite: [userId], + is_direct: true, + }) + ); + + const createdRoomID = response.room_id; + if (!createdRoomID) { + throw new Error("Matrix createRoom did not return room_id for DM."); + } + + await this.persistDMRoomID(userId, createdRoomID); + await this.persistDirectAccountDataRoom(userId, createdRoomID, direct); + return this.encodeThreadId({ roomID: createdRoomID }); } async fetchMessages( @@ -433,71 +610,122 @@ export class MatrixAdapter implements Adapter { options: FetchOptions = {} ): Promise> { const { roomID, rootEventID } = this.decodeThreadId(threadId); - const room = this.requireRoom(roomID); - - const messageEvents = room.timeline.filter((event) => - this.isMessageEvent(event, roomID, rootEventID) - ); - const direction = options.direction ?? "backward"; const limit = options.limit ?? 50; + const cursor = options.cursor + ? this.decodeCursorV1( + options.cursor, + rootEventID ? "thread_relations" : "room_messages", + roomID, + rootEventID, + direction + ) + : null; - if (direction === "forward") { - const startIndex = options.cursor - ? messageEvents.findIndex((e) => e.getId() === options.cursor) + 1 - : 0; - - const slice = messageEvents.slice(startIndex, startIndex + limit); - const last = slice.at(-1)?.getId(); - const hasMore = startIndex + limit < messageEvents.length; + if (!rootEventID) { + const response = await this.fetchRoomMessagesPage({ + roomID, + includeThreadReplies: false, + limit, + direction, + fromToken: cursor?.token ?? null, + }); return { - messages: slice.map((event) => this.parseMessage(event)), - nextCursor: hasMore ? last : undefined, + messages: response.events.map((event) => this.parseMessageInternal(event)), + nextCursor: response.nextToken + ? this.encodeCursorV1({ + kind: "room_messages", + dir: direction, + token: response.nextToken, + roomID, + }) + : undefined, }; } - const endIndex = options.cursor - ? messageEvents.findIndex((e) => e.getId() === options.cursor) - : messageEvents.length; - - const boundedEnd = endIndex >= 0 ? endIndex : messageEvents.length; - const start = Math.max(0, boundedEnd - limit); - const slice = messageEvents.slice(start, boundedEnd); + const includeRoot = !cursor; + const response = await this.fetchThreadMessagesPage({ + roomID, + rootEventID, + includeRoot, + limit, + direction, + fromToken: cursor?.token ?? null, + }); return { - messages: slice.map((event) => this.parseMessage(event)), - nextCursor: start > 0 ? messageEvents[start - 1]?.getId() : undefined, + messages: response.events.map((event) => + this.parseMessageInternal(event, this.encodeThreadId({ roomID, rootEventID })) + ), + nextCursor: response.nextToken + ? this.encodeCursorV1({ + kind: "thread_relations", + dir: direction, + token: response.nextToken, + roomID, + rootEventID, + }) + : undefined, }; } + async fetchChannelMessages( + channelId: string, + options: FetchOptions = {} + ): Promise> { + const roomID = this.decodeThreadId(channelId).roomID; + return this.fetchMessages(this.encodeThreadId({ roomID }), options); + } + + async fetchMessage( + threadId: string, + messageId: string + ): Promise | null> { + const { roomID, rootEventID } = this.decodeThreadId(threadId); + const event = await this.fetchRoomEventMapped(roomID, messageId); + if (!event) { + return null; + } + + if (!this.isMessageEventInContext(event, roomID, rootEventID)) { + return null; + } + + const overrideThreadID = rootEventID + ? this.encodeThreadId({ roomID, rootEventID }) + : undefined; + return this.parseMessageInternal(event, overrideThreadID); + } + async fetchThread(threadId: string): Promise { const { roomID } = this.decodeThreadId(threadId); const room = this.requireRoom(roomID); + const isDM = await this.isDirectRoom(roomID); + const metadata = this.readRoomMetadata(room, isDM); return { id: threadId, channelId: this.channelIdFromThreadId(threadId), - channelName: room.name, - isDM: room.getJoinedMembers().length === 2, - metadata: { - roomID, - }, + channelName: metadata.name, + isDM, + metadata, }; } async fetchChannelInfo(channelId: string): Promise { - const roomID = this.decodeChannelID(channelId); + const roomID = this.decodeThreadId(channelId).roomID; const room = this.requireRoom(roomID); + const isDM = await this.isDirectRoom(roomID); + const metadata = this.readRoomMetadata(room, isDM); + const members = room.getJoinedMembers(); return { id: channelId, - name: room.name, - isDM: room.getJoinedMembers().length === 2, - memberCount: room.getJoinedMembers().length, - metadata: { - roomID, - }, + name: metadata.name, + isDM, + memberCount: members.length, + metadata, }; } @@ -505,177 +733,625 @@ export class MatrixAdapter implements Adapter { channelId: string, options: ListThreadsOptions = {} ): Promise> { - const roomID = this.decodeChannelID(channelId); - const room = this.requireRoom(roomID); - - const threadMap = new Map< - string, - { - root?: MatrixEvent; - replyCount: number; - lastTS?: number; - } - >(); - - for (const event of room.timeline) { - if (event.getType() !== EventType.RoomMessage) { - continue; - } + const roomID = this.decodeThreadId(channelId).roomID; + const limit = options.limit ?? 50; + const cursor = options.cursor + ? this.decodeCursorV1(options.cursor, "thread_list", roomID, undefined, "backward") + : null; + const listResponse = await this.requireClient().createThreadListMessagesRequest( + roomID, + cursor?.token ?? null, + limit, + Direction.Backward, + ThreadFilterType.All + ); + const events = await this.mapRawEvents(listResponse.chunk ?? [], roomID); + const summaries: ThreadSummary[] = []; - const rootID = event.threadRootId; - if (!rootID) { + for (const rootEvent of events) { + const rootID = rootEvent.getId(); + if (!rootID || rootEvent.getType() !== EventType.RoomMessage) { continue; } - const entry = threadMap.get(rootID) ?? { replyCount: 0 }; - - if (event.getId() === rootID) { - entry.root = event; - } else { - entry.replyCount += 1; - } + const bundled = rootEvent.getServerAggregatedRelation( + THREAD_RELATION_TYPE.name + ); + const latestTS = bundled?.latest_event?.origin_server_ts; + const threadID = this.encodeThreadId({ roomID, rootEventID: rootID }); - entry.lastTS = Math.max(entry.lastTS ?? 0, event.getTs()); - threadMap.set(rootID, entry); + summaries.push({ + id: threadID, + rootMessage: this.parseMessageInternal(rootEvent, threadID), + replyCount: bundled?.count ?? 0, + lastReplyAt: typeof latestTS === "number" ? new Date(latestTS) : undefined, + }); } - const summaries: ThreadSummary[] = []; - for (const [rootID, entry] of threadMap.entries()) { - if (!entry.root) { - continue; - } + return { + threads: summaries, + nextCursor: listResponse.end + ? this.encodeCursorV1({ + kind: "thread_list", + dir: "backward", + token: listResponse.end, + roomID, + }) + : undefined, + }; + } - summaries.push({ - id: this.encodeThreadId({ roomID, rootEventID: rootID }), - rootMessage: this.parseMessage(entry.root), - replyCount: entry.replyCount, - lastReplyAt: entry.lastTS ? new Date(entry.lastTS) : undefined, - }); + private parseMessageInternal( + raw: MatrixEvent, + overrideThreadID?: string + ): Message { + const roomID = raw.getRoomId(); + if (!roomID) { + throw new Error("Matrix event missing room ID"); } - summaries.sort( - (a, b) => - (b.lastReplyAt?.getTime() ?? 0) - (a.lastReplyAt?.getTime() ?? 0) - ); + const threadID = overrideThreadID ?? this.threadIDForEvent(raw, roomID); + const content = raw.getContent(); + const edited = this.extractEditedContent(raw); + const effectiveContent = edited?.content ?? content; + const parsed = this.parseMatrixContent(effectiveContent); + const sender = raw.getSender() ?? "unknown"; - const limit = options.limit ?? 50; + return new Message({ + id: raw.getId() ?? `${roomID}:${raw.getTs()}`, + threadId: threadID, + text: parsed.text, + formatted: parseMarkdown(parsed.markdown), + author: this.makeUser(sender, roomID), + metadata: { + dateSent: new Date(raw.getTs()), + edited: this.isEdited(raw) || Boolean(edited?.content), + editedAt: edited?.editedAt, + }, + attachments: this.extractAttachments(effectiveContent), + raw, + isMention: this.isMentioned(effectiveContent, parsed), + }); + } - return { - threads: summaries.slice(0, limit), - nextCursor: summaries.length > limit ? String(limit) : undefined, - }; + private encodeCursorV1(payload: CursorV1Payload): string { + return `${MATRIX_CURSOR_PREFIX}${Buffer.from( + JSON.stringify(payload), + "utf8" + ).toString("base64url")}`; } - private async resolveAuth(): Promise { - if (this.auth.type === "accessToken") { - const whoami = await this.lookupWhoAmIFromAccessToken(this.auth.accessToken); - const userID = this.auth.userID ?? whoami.userID; - const resolved: ResolvedAuth = { - accessToken: this.auth.accessToken, - userID, - deviceID: whoami.deviceID ?? this.deviceID, - }; - await this.persistDeviceIDForResolvedUser(userID); - await this.persistSession(resolved); - return resolved; + private decodeCursorV1( + cursor: string, + expectedKind: CursorKind, + expectedRoomID: string, + expectedRootEventID?: string, + expectedDirection?: CursorDirection + ): CursorV1Payload { + if (!cursor.startsWith(MATRIX_CURSOR_PREFIX)) { + throw new Error("Invalid cursor format. Expected mxv1 cursor."); } - const restored = await this.loadPersistedSession(); - if (restored?.accessToken) { - try { - const whoami = await this.lookupWhoAmIFromAccessToken(restored.accessToken); - const resolved: ResolvedAuth = { - accessToken: restored.accessToken, - userID: whoami.userID, - deviceID: this.deviceID ?? whoami.deviceID ?? restored.deviceID, - }; - await this.persistDeviceIDForResolvedUser(resolved.userID, resolved.deviceID); - await this.persistSession(resolved); - this.logger.info("Reused persisted Matrix session", { - userID: resolved.userID, - }); - return resolved; - } catch (error) { - this.logger.warn("Persisted Matrix session is invalid. Falling back to password login.", { - error: String(error), - }); - } + let parsed: unknown; + try { + parsed = JSON.parse( + Buffer.from(cursor.slice(MATRIX_CURSOR_PREFIX.length), "base64url").toString("utf8") + ); + } catch (error) { + throw new Error(`Invalid cursor format. ${String(error)}`); } - const bootstrapClient = this.createBootstrapClient(); - const loginResponse = bootstrapClient.loginRequest - ? await bootstrapClient.loginRequest({ - type: "m.login.password", - password: this.auth.password, - identifier: { - type: "m.id.user", - user: this.auth.username, - }, - user: this.auth.username, - device_id: this.deviceID, - initial_device_display_name: this.auth.initialDeviceDisplayName, - }) - : await bootstrapClient.loginWithPassword( - this.auth.username, - this.auth.password - ); + if (!isRecord(parsed)) { + throw new Error("Invalid cursor format. Cursor payload must be an object."); + } - const userID = this.auth.userID ?? loginResponse.user_id; - if (!userID) { - throw new Error("Password login succeeded but no user ID was returned."); + if (parsed.kind !== expectedKind) { + throw new Error(`Invalid cursor kind. Expected ${expectedKind}.`); + } + if (parsed.roomID !== expectedRoomID) { + throw new Error("Invalid cursor context. Room mismatch."); + } + if (parsed.dir !== "forward" && parsed.dir !== "backward") { + throw new Error("Invalid cursor format. Invalid direction."); + } + if (expectedDirection && parsed.dir !== expectedDirection) { + throw new Error(`Invalid cursor direction. Expected ${expectedDirection}.`); + } + if (typeof parsed.token !== "string" || parsed.token.length === 0) { + throw new Error("Invalid cursor format. Missing token."); } - const resolved: ResolvedAuth = { - accessToken: loginResponse.access_token, - userID, - deviceID: loginResponse.device_id ?? this.deviceID ?? undefined, + const rootEventID = + typeof parsed.rootEventID === "string" ? parsed.rootEventID : undefined; + if (expectedRootEventID) { + if (rootEventID !== expectedRootEventID) { + throw new Error("Invalid cursor context. Thread mismatch."); + } + } else if (rootEventID) { + throw new Error("Invalid cursor context. Unexpected thread scope."); + } + + return { + dir: parsed.dir, + kind: expectedKind, + roomID: expectedRoomID, + rootEventID, + token: parsed.token, }; - await this.persistDeviceIDForResolvedUser(resolved.userID, resolved.deviceID); - await this.persistSession(resolved); - return resolved; } - private async lookupUserIDFromAccessToken(accessToken: string): Promise { - const whoami = await this.lookupWhoAmIFromAccessToken(accessToken); - return whoami.userID; + private toSDKDirection(dir: CursorDirection): Direction { + return dir === "forward" ? Direction.Forward : Direction.Backward; } - private async lookupWhoAmIFromAccessToken( - accessToken: string - ): Promise<{ deviceID?: string; userID: string }> { - const bootstrapClient = this.createBootstrapClient({ - accessToken, - deviceID: this.deviceID, + private async fetchRoomMessagesPage(args: { + roomID: string; + includeThreadReplies: boolean; + limit: number; + direction: CursorDirection; + fromToken: string | null; + }): Promise<{ events: MatrixEvent[]; nextToken: string | null }> { + const response = await this.requireClient().createMessagesRequest( + args.roomID, + args.fromToken, + args.limit, + this.toSDKDirection(args.direction) + ); + const messageChunk = (response.chunk ?? []).filter( + (raw) => + raw.type === EventType.RoomMessage || + raw.type === EventType.RoomMessageEncrypted + ); + const events = await this.mapRawEvents(messageChunk, args.roomID); + const filtered = events.filter((event) => { + if (event.getType() !== EventType.RoomMessage) { + return false; + } + if (event.getRoomId() !== args.roomID) { + return false; + } + if (args.includeThreadReplies) { + return true; + } + return this.isTopLevelMessageEvent(event); }); - const whoami = await bootstrapClient.whoami(); - const userID = whoami.user_id; - const deviceID = - typeof whoami.device_id === "string" ? whoami.device_id : undefined; - if (!userID) { - throw new Error("Access token whoami lookup did not return user_id."); - } - - return { userID, deviceID }; + return { + events: this.sortEventsChronologically(filtered), + nextToken: response.end ?? null, + }; } - private buildClient(auth: ResolvedAuth): MatrixClient { - const cryptoCallbacks = - this.recoveryKey && this.e2eeConfig?.enabled - ? { - getSecretStorageKey: async ( - opts: { keys: Record }, - _name: string - ) => this.getSecretStorageKeyFromRecoveryKey(opts), - } - : undefined; - + private async fetchThreadMessagesPage(args: { + roomID: string; + rootEventID: string; + includeRoot: boolean; + limit: number; + direction: CursorDirection; + fromToken: string | null; + }): Promise<{ events: MatrixEvent[]; nextToken: string | null }> { + // When includeRoot is true, reserve one slot for the root and fetch at most + // limit - 1 replies; the merged result is sliced back to args.limit below. + const relationLimit = args.includeRoot + ? Math.max(args.limit - 1, 1) + : args.limit; + + const relationResponse = await this.requireClient().relations( + args.roomID, + args.rootEventID, + THREAD_RELATION_TYPE.name, + null, + { + dir: this.toSDKDirection(args.direction), + from: args.fromToken ?? undefined, + limit: relationLimit, + } + ); + + const candidateEvents = relationResponse.events.filter( + (event) => + event.getType() === EventType.RoomMessage || + event.getType() === EventType.RoomMessageEncrypted + ); + await Promise.all( + candidateEvents.map((event) => this.tryDecryptEvent(event)) + ); + const replies = this.sortEventsChronologically( + candidateEvents.filter((event) => + this.isMessageEventInContext(event, args.roomID, args.rootEventID) + ) + ); + + if (!args.includeRoot) { + return { + events: replies.slice(0, args.limit), + nextToken: relationResponse.nextBatch ?? null, + }; + } + + const rootEvent = + relationResponse.originalEvent?.getId() === args.rootEventID + ? relationResponse.originalEvent + : await this.fetchRoomEventMapped(args.roomID, args.rootEventID); + const rootArray = + rootEvent && + this.isMessageEventInContext(rootEvent, args.roomID, args.rootEventID) + ? [rootEvent] + : []; + const dedupedReplies = replies.filter( + (event) => event.getId() !== args.rootEventID + ); + + return { + events: [...rootArray, ...dedupedReplies].slice(0, args.limit), + nextToken: relationResponse.nextBatch ?? null, + }; + } + + private async mapRawEvents( + rawEvents: Array>, + roomID: string + ): Promise { + const mapper = this.requireClient().getEventMapper(); + const events = rawEvents.map((event) => { + const withRoomID = event.room_id ? event : { ...event, room_id: roomID }; + return mapper(withRoomID); + }); + await Promise.all(events.map((event) => this.tryDecryptEvent(event))); + return events; + } + + private mapRawEvent(rawEvent: Partial, roomID: string): MatrixEvent { + const mapper = this.requireClient().getEventMapper(); + const withRoomID = rawEvent.room_id + ? rawEvent + : { ...rawEvent, room_id: roomID }; + return mapper(withRoomID); + } + + private async fetchRoomEventMapped( + roomID: string, + eventID: string + ): Promise { + try { + const rawEvent = await this.requireClient().fetchRoomEvent(roomID, eventID); + if (!rawEvent) { + return null; + } + + const mapped = this.mapRawEvent(rawEvent, roomID); + await this.tryDecryptEvent(mapped); + return mapped; + } catch (error) { + const isNotFound = + error instanceof MatrixError && + (error.errcode === "M_NOT_FOUND" || error.httpStatus === 404); + if (isNotFound) { + this.logger.debug("Room event not found", { roomID, eventID }); + return null; + } + throw error; + } + } + + private sortEventsChronologically(events: MatrixEvent[]): MatrixEvent[] { + const deduped = new Map(); + const withoutIDs: MatrixEvent[] = []; + + for (const event of events) { + const eventID = event.getId(); + if (!eventID) { + withoutIDs.push(event); + continue; + } + deduped.set(eventID, event); + } + + return [...deduped.values(), ...withoutIDs].sort((a, b) => { + const tsDiff = a.getTs() - b.getTs(); + if (tsDiff !== 0) { + return tsDiff; + } + return (a.getId() ?? "").localeCompare(b.getId() ?? ""); + }); + } + + private isTopLevelMessageEvent(event: MatrixEvent): boolean { + return !event.threadRootId && !event.getRelation(); + } + + private isMessageEventInContext( + event: MatrixEvent, + roomID: string, + rootEventID?: string + ): boolean { + if (event.getType() !== EventType.RoomMessage || event.getRoomId() !== roomID) { + return false; + } + + if (!rootEventID) { + return this.isTopLevelMessageEvent(event); + } + + return event.threadRootId === rootEventID || event.getId() === rootEventID; + } + + private getDMStorageKey(userID: string): string { + return `${this.persistenceConfig.keyPrefix}:dm:${encodeURIComponent(userID)}`; + } + + private async loadPersistedDMRoomID(userID: string): Promise { + if (!this.stateAdapter) { + return null; + } + + const cached = await this.stateAdapter.get( + this.getDMStorageKey(userID) + ); + const normalized = normalizeOptionalString(cached); + return normalized ?? null; + } + + private async clearPersistedDMRoomID(userID: string): Promise { + if (!this.stateAdapter) { + return; + } + + await this.stateAdapter.delete(this.getDMStorageKey(userID)); + } + + private async persistDMRoomID(userID: string, roomID: string): Promise { + if (!this.stateAdapter) { + return; + } + + await this.stateAdapter.set(this.getDMStorageKey(userID), roomID); + } + + private async loadDirectAccountData(): Promise { + const cached = this.loadCachedDirectAccountData(); + if (Object.keys(cached).length > 0) { + return cached; + } + + const direct = await this.requireClient().getAccountDataFromServer(EventType.Direct); + return this.normalizeDirectAccountData(direct); + } + + private loadCachedDirectAccountData(): DirectAccountData { + const client = this.requireClient(); + const getAccountData = Reflect.get(client, "getAccountData"); + if (typeof getAccountData !== "function") { + return {}; + } + + const event = getAccountData.call(client, EventType.Direct); + const direct = + typeof event === "object" && + event !== null && + typeof Reflect.get(event, "getContent") === "function" + ? Reflect.get(event, "getContent").call(event) + : undefined; + return this.normalizeDirectAccountData(direct); + } + + private normalizeDirectAccountData(value: unknown): DirectAccountData { + if (!value || typeof value !== "object") { + return {}; + } + + const out: DirectAccountData = {}; + for (const [userID, roomIDs] of Object.entries(value)) { + if (!Array.isArray(roomIDs)) { + continue; + } + out[userID] = roomIDs.filter( + (roomID): roomID is string => typeof roomID === "string" && roomID.length > 0 + ); + } + + return out; + } + + private findExistingDirectRoomID( + direct: DirectAccountData, + userID: string + ): string | null { + const client = this.requireClient(); + const candidates = direct[userID] ?? []; + for (const roomID of candidates) { + if (!roomID) { + continue; + } + + const room = client.getRoom(roomID); + if (!room) { + // m.direct is server state; if the room is not in the local sync cache yet, + // prefer the server mapping over creating a duplicate DM. + return roomID; + } + + const membership = room.getMyMembership(); + if (membership === "join" || membership === "invite") { + return roomID; + } + } + return null; + } + + private async isDirectRoom(roomID: string): Promise { + const cached = this.loadCachedDirectAccountData(); + if (this.directAccountDataContainsRoom(cached, roomID)) { + return true; + } + + const direct = await this.loadDirectAccountData(); + return this.directAccountDataContainsRoom(direct, roomID); + } + + private directAccountDataContainsRoom( + direct: DirectAccountData, + roomID: string + ): boolean { + return Object.values(direct).some((roomIDs) => roomIDs.includes(roomID)); + } + + private async persistDirectAccountDataRoom( + userID: string, + roomID: string, + existing: DirectAccountData + ): Promise { + const latest = await this.loadLatestDirectAccountData(existing); + const existingRooms = latest[userID] ?? []; + if (!existingRooms.includes(roomID)) { + const updated: DirectAccountData = { + ...latest, + [userID]: [...existingRooms, roomID], + }; + await this.requireClient().setAccountData(EventType.Direct, updated); + } + } + + private async loadLatestDirectAccountData( + fallback: DirectAccountData + ): Promise { + try { + const direct = await this.requireClient().getAccountDataFromServer(EventType.Direct); + return this.normalizeDirectAccountData(direct); + } catch (error) { + this.logger.debug("Failed to refresh m.direct before writing; using cached snapshot", { + error, + }); + return fallback; + } + } + + private async resolveAuth(): Promise { + if (this.auth.type === "accessToken") { + const whoami = await this.lookupWhoAmIFromAccessToken(this.auth.accessToken); + const userID = this.auth.userID ?? whoami.userID; + const deviceID = await this.resolveDeviceID(whoami.deviceID); + const resolved: ResolvedAuth = { + accessToken: this.auth.accessToken, + userID, + deviceID, + }; + await Promise.all([ + this.persistDeviceIDForResolvedUser(userID, resolved.deviceID), + this.persistSession(resolved), + ]); + return resolved; + } + + const restored = await this.loadPersistedSession(); + if (restored?.accessToken) { + try { + const whoami = await this.lookupWhoAmIFromAccessToken(restored.accessToken); + const deviceID = await this.resolveDeviceID( + whoami.deviceID, + restored.deviceID + ); + const resolved: ResolvedAuth = { + accessToken: restored.accessToken, + userID: whoami.userID, + deviceID, + }; + await Promise.all([ + this.persistDeviceIDForResolvedUser(resolved.userID, resolved.deviceID), + this.persistSession(resolved, restored.createdAt), + ]); + this.logger.info("Reused persisted Matrix session", { + userId: resolved.userID, + }); + return resolved; + } catch (error) { + this.logger.warn( + "Persisted Matrix session is invalid. Falling back to password login.", + { error } + ); + } + } + + const bootstrapClient = this.createBootstrapClient(); + const loginResponse = bootstrapClient.loginRequest + ? await bootstrapClient.loginRequest({ + type: "m.login.password", + password: this.auth.password, + identifier: { + type: "m.id.user", + user: this.auth.username, + }, + user: this.auth.username, + device_id: this.deviceID, + initial_device_display_name: this.auth.initialDeviceDisplayName, + }) + : await bootstrapClient.loginWithPassword( + this.auth.username, + this.auth.password + ); + + let userID = this.auth.userID ?? loginResponse.user_id; + if (!userID) { + throw new Error("Password login succeeded but no user ID was returned."); + } + + let authDeviceID = normalizeOptionalString(loginResponse.device_id); + if (!authDeviceID) { + const whoami = await this.lookupWhoAmIFromAccessToken(loginResponse.access_token); + userID = userID ?? whoami.userID; + authDeviceID = whoami.deviceID; + } + + const deviceID = await this.resolveDeviceID(authDeviceID); + const resolved: ResolvedAuth = { + accessToken: loginResponse.access_token, + userID, + deviceID, + }; + await Promise.all([ + this.persistDeviceIDForResolvedUser(resolved.userID, resolved.deviceID), + this.persistSession(resolved), + ]); + return resolved; + } + + private async lookupWhoAmIFromAccessToken( + accessToken: string + ): Promise<{ deviceID?: string; userID: string }> { + const bootstrapClient = this.createBootstrapClient({ + accessToken, + deviceID: this.deviceID, + }); + const whoami = await bootstrapClient.whoami(); + const userID = whoami.user_id; + const deviceID = + typeof whoami.device_id === "string" ? whoami.device_id : undefined; + + if (!userID) { + throw new Error("Access token whoami lookup did not return user_id."); + } + + return { userID, deviceID }; + } + + private buildClient(auth: ResolvedAuth, store?: IStore): MatrixClient { + const cryptoCallbacks = + this.recoveryKey && this.e2eeEnabled + ? { + getSecretStorageKey: async ( + opts: { keys: Record }, + _name: string + ) => this.getSecretStorageKeyFromRecoveryKey(opts), + } + : undefined; + return sdk.createClient({ baseUrl: this.baseURL, accessToken: auth.accessToken, userId: auth.userID, deviceId: auth.deviceID, cryptoCallbacks, + store, }); } @@ -691,33 +1367,211 @@ export class MatrixAdapter implements Adapter { }); } - return sdk.createClient({ + const client = sdk.createClient({ baseUrl: this.baseURL, accessToken: args?.accessToken, deviceId: args?.deviceID, - }) as MatrixAuthBootstrapClient; + }); + + return { + loginRequest: client.loginRequest?.bind(client), + loginWithPassword: client.loginWithPassword.bind(client), + whoami: client.whoami.bind(client), + }; + } + + private buildCustomClientOptions(store?: IStore): ICreateClientOpts | undefined { + if (!store) { + return undefined; + } + + const customUserID = + normalizeOptionalString(this.auth.userID) ?? + normalizeOptionalString(this.userID); + + return { + baseUrl: this.baseURL, + accessToken: + this.auth.type === "accessToken" ? this.auth.accessToken : undefined, + userId: customUserID, + deviceId: this.deviceID, + store, + }; + } + + private async maybeCreateMatrixStore(resolvedAuth?: ResolvedAuth): Promise { + if (!this.stateAdapter) { + return undefined; + } + + const storeContext = this.resolveMatrixStoreContext(resolvedAuth); + if (!storeContext) { + return undefined; + } + + const { scopeKey, userID, deviceID } = storeContext; + this.matrixStoreScopeKey = scopeKey; + + const createStoreOptions: MatrixCreateStoreOptions = { + baseURL: this.baseURL, + config: { ...this.persistenceConfig.sync }, + deviceID, + logger: this.logger, + scopeKey, + state: this.stateAdapter, + userID, + }; + + if (this.createStoreFn) { + return this.createStoreFn(createStoreOptions); + } + + return new ChatStateMatrixStore({ + state: this.stateAdapter, + scopeKey, + logger: this.logger, + persistIntervalMs: this.persistenceConfig.sync.persistIntervalMs, + snapshotTtlMs: this.persistenceConfig.sync.snapshotTtlMs, + }); + } + + private resolveMatrixStoreContext( + resolvedAuth?: ResolvedAuth + ): { deviceID?: string; scopeKey: string; userID: string } | null { + const userID = normalizeOptionalString(resolvedAuth?.userID) ?? + normalizeOptionalString(this.auth.userID) ?? + normalizeOptionalString(this.userID); + if (!userID) { + this.logger.warn( + "No user ID is available for Matrix persistence scoping. Continuing without persistent sync store." + ); + return null; + } + + const deviceID = normalizeOptionalString(resolvedAuth?.deviceID) ?? this.deviceID; + return { + userID, + deviceID, + scopeKey: this.buildMatrixStoreScopeKey(userID, deviceID), + }; + } + + private buildMatrixStoreScopeKey(userID: string, deviceID?: string): string { + return `${this.persistenceStorePrefix}:${encodeURIComponent(this.baseURL)}:${encodeURIComponent(userID)}:${encodeURIComponent(deviceID ?? "default")}`; + } + + private async maybeFlushMatrixStore(): Promise { + const store = this.client?.store as { save?: (force?: boolean) => Promise } | undefined; + if (!store?.save) { + return; + } + + try { + await store.save(true); + } catch (error) { + this.logger.warn("Failed to flush Matrix sync store during shutdown", { error }); + } + } + + private dispatchTimelineEvent( + event: MatrixEvent, + room: Room | undefined, + toStartOfTimeline: boolean + ): void { + void this.onTimelineEvent(event, room, toStartOfTimeline).catch((error) => { + this.logger.error("Unhandled Matrix timeline event failure", { + eventId: event.getId(), + eventType: event.getType(), + roomId: room?.roomId ?? event.getRoomId(), + error, + }); + }); + } + + private async withLoggedMatrixOperation( + message: string, + context: Record, + operation: () => Promise + ): Promise { + try { + return await operation(); + } catch (error) { + this.logger.error(message, { ...context, error }); + throw error; + } } - private sendRoomMessage( + private async sendRoomMessage( roomID: string, rootEventID: string | undefined, - content: unknown + content: MatrixOutboundMessageContent ) { - const client = this.requireClient(); - if (rootEventID) { - return client.sendEvent( - roomID, - rootEventID, - EventType.RoomMessage, - content as never - ); - } + const response = await this.withLoggedMatrixOperation( + "Matrix send message failed", + { + roomId: roomID, + rootEventId: rootEventID, + eventType: EventType.RoomMessage, + msgtype: content.msgtype, + }, + async () => { + const client = this.requireClient(); + if (rootEventID) { + return client.sendEvent(roomID, rootEventID, EventType.RoomMessage, content); + } + + return client.sendEvent(roomID, EventType.RoomMessage, content); + } + ); + void this.maybePersistSecretsBundle(); + return response; + } - return client.sendEvent(roomID, EventType.RoomMessage, content as never); + private async toRoomMessageContents( + message: AdapterPostableMessage + ): Promise { + const textContent = this.toRoomMessageContent(message); + const attachments = this.extractAttachmentsFromMessage(message); + const uploads = await this.collectUploads(message, attachments); + const linkLines = this.collectLinkOnlyAttachmentLines(attachments); + const textBody = this.mergeTextAndLinks(textContent, linkLines); + const contents: MatrixOutboundMessageContent[] = []; + + if ((normalizeOptionalString(textBody.body) ?? "").length > 0) { + contents.push(textBody); + } + + const uploadedContents = await Promise.all( + uploads.map(async (upload) => { + const uploadResponse = await this.withLoggedMatrixOperation( + "Matrix upload content failed", + { + fileName: upload.fileName, + mimeType: upload.info?.mimetype, + msgtype: upload.msgtype, + }, + () => + this.requireClient().uploadContent(upload.data, { + name: upload.fileName, + type: upload.info?.mimetype, + }) + ); + const content: MediaEventContent = { + body: upload.fileName, + msgtype: upload.msgtype, + url: uploadResponse.content_uri, + info: upload.info, + }; + return content; + }) + ); + contents.push(...uploadedContents); + + return contents; } private async maybeInitE2EE(): Promise { - if (!this.e2eeConfig?.enabled) { + if (!this.e2eeEnabled) { return; } @@ -727,56 +1581,148 @@ export class MatrixAdapter implements Adapter { ); } - const requestedIndexedDB = this.e2eeConfig.useIndexedDB; - const useIndexedDB = - requestedIndexedDB === true && !hasIndexedDB() - ? false - : requestedIndexedDB; + const e2eeConfig = this.e2eeConfig ?? {}; + const requestedIndexedDB = e2eeConfig.useIndexedDB; + const useIndexedDB = + requestedIndexedDB === true && !hasIndexedDB() + ? false + : requestedIndexedDB; + + if (requestedIndexedDB === true && useIndexedDB === false) { + this.logger.warn( + "IndexedDB requested for Matrix E2EE, but indexedDB is unavailable in this runtime. Falling back to non-IndexedDB crypto store." + ); + } + + await this.requireClient().initRustCrypto({ + useIndexedDB, + cryptoDatabasePrefix: e2eeConfig.cryptoDatabasePrefix, + storagePassword: e2eeConfig.storagePassword, + storageKey: e2eeConfig.storageKey, + }); + await this.maybeImportPersistedSecretsBundle(); + void this.maybeLoadKeyBackupFromRecoveryKey(); + void this.maybePersistSecretsBundle(); + + this.logger.info("Matrix E2EE initialized", { + useIndexedDB: useIndexedDB !== false, + cryptoDatabasePrefix: e2eeConfig.cryptoDatabasePrefix, + }); + } + + private async maybeLoadKeyBackupFromRecoveryKey(): Promise { + if (!this.recoveryKey) { + return; + } + + const crypto = this.requireClient().getCrypto(); + if (!crypto) { + return; + } + + try { + await crypto.loadSessionBackupPrivateKeyFromSecretStorage(); + await crypto.checkKeyBackupAndEnable(); + this.logger.info("Loaded Matrix key backup using recovery key"); + } catch (error) { + this.logger.warn( + "Failed to load Matrix key backup from recovery key. E2EE will run, but historical key restore may be unavailable.", + { error } + ); + } + } + + private get persistSecretsBundleEnabled(): boolean { + return Boolean(this.e2eeEnabled && this.stateAdapter); + } + + private getSecretsBundleStorageKey(): string | null { + if (!this.persistSecretsBundleEnabled || !this.matrixStoreScopeKey) { + return null; + } + + return `${this.matrixStoreScopeKey}:secrets-bundle`; + } + + private async maybeImportPersistedSecretsBundle(): Promise { + if (!this.stateAdapter) { + return; + } + + const storageKey = this.getSecretsBundleStorageKey(); + if (!storageKey) { + return; + } + + const crypto = this.requireClient().getCrypto(); + if (!crypto?.importSecretsBundle) { + return; + } + + try { + const bundle = await this.stateAdapter.get(storageKey); + if (!bundle) { + return; + } - if (requestedIndexedDB === true && useIndexedDB === false) { - this.logger.warn( - "IndexedDB requested for Matrix E2EE, but indexedDB is unavailable in this runtime. Falling back to non-IndexedDB crypto store." - ); + await crypto.importSecretsBundle(bundle as Parameters< + NonNullable["importSecretsBundle"]> + >[0]); + this.logger.info("Imported persisted Matrix secrets bundle"); + } catch (error) { + this.logger.warn("Failed to import persisted Matrix secrets bundle", { error }); } + } - await this.requireClient().initRustCrypto({ - useIndexedDB, - cryptoDatabasePrefix: this.e2eeConfig.cryptoDatabasePrefix, - storagePassword: this.e2eeConfig.storagePassword, - storageKey: this.e2eeConfig.storageKey, - }); - void this.maybeLoadKeyBackupFromRecoveryKey(); + private async maybePersistSecretsBundle(force = false): Promise { + if (!this.stateAdapter) { + return; + } - this.logger.info("Matrix E2EE initialized", { - useIndexedDB: useIndexedDB !== false, - cryptoDatabasePrefix: this.e2eeConfig.cryptoDatabasePrefix, - }); - } + const storageKey = this.getSecretsBundleStorageKey(); + if (!storageKey) { + return; + } - private async maybeLoadKeyBackupFromRecoveryKey(): Promise { - if (!this.recoveryKey) { + const crypto = this.client?.getCrypto(); + if (!crypto?.exportSecretsBundle) { return; } - const crypto = this.requireClient().getCrypto(); - if (!crypto) { + const now = Date.now(); + if (!force && now - this.lastSecretsBundlePersistAt < 60_000) { return; } try { - await crypto.loadSessionBackupPrivateKeyFromSecretStorage(); - await crypto.checkKeyBackupAndEnable(); - this.logger.info("Loaded Matrix key backup using recovery key"); + const bundle = await crypto.exportSecretsBundle(); + await this.stateAdapter.set(storageKey, bundle); + this.lastSecretsBundlePersistAt = now; + this.secretsBundleUnavailableLogged = false; + if (force) { + this.logger.debug("Persisted Matrix secrets bundle during shutdown"); + } } catch (error) { - this.logger.warn( - "Failed to load Matrix key backup from recovery key. E2EE will run, but historical key restore may be unavailable.", - { error: String(error) } - ); + if (this.isExpectedSecretsBundleUnavailableError(error)) { + if (!this.secretsBundleUnavailableLogged) { + this.logger.debug( + "Skipping Matrix secrets bundle persistence because cross-signing secrets are not available yet" + ); + this.secretsBundleUnavailableLogged = true; + } + return; + } + this.logger.warn("Failed to persist Matrix secrets bundle", { error }); } } + private isExpectedSecretsBundleUnavailableError(error: unknown): boolean { + return error instanceof Error && + error.message.includes("cross-signing keys"); + } + private async tryDecryptEvent(event: MatrixEvent): Promise { - if (!this.e2eeConfig?.enabled) { + if (!this.e2eeEnabled) { return; } @@ -786,10 +1732,11 @@ export class MatrixAdapter implements Adapter { try { await this.requireClient().decryptEventIfNeeded(event); + void this.maybePersistSecretsBundle(); } catch (error) { this.logger.warn("Failed to decrypt Matrix event", { - eventID: event.getId(), - error: String(error), + eventId: event.getId(), + error, }); } } @@ -816,14 +1763,6 @@ export class MatrixAdapter implements Adapter { return room; } - private decodeChannelID(channelId: string): string { - const parts = channelId.split(":"); - if (parts.length !== 2 || parts[0] !== MATRIX_PREFIX) { - throw new Error(`Invalid Matrix channel ID: ${channelId}`); - } - return decodeURIComponent(parts[1]); - } - private async onTimelineEvent( event: MatrixEvent, room: Room | undefined, @@ -839,9 +1778,7 @@ export class MatrixAdapter implements Adapter { return; } this.processedTimelineEventIDs.add(eventID); - if (this.processedTimelineEventIDs.size > 10_000) { - this.processedTimelineEventIDs.clear(); - } + evictOldestEntries(this.processedTimelineEventIDs); } const roomID = room?.roomId ?? event.getRoomId(); @@ -849,72 +1786,52 @@ export class MatrixAdapter implements Adapter { return; } + if (event.getType() === EventType.RoomMember) { + await this.maybeAutoJoinInvite(event, roomID); + } + if (!this.liveSyncReady) { this.logger.debug("Ignoring pre-live-sync event", { - eventID, + eventId: eventID, eventType: event.getType(), - roomID, + roomId: roomID, }); return; } if (this.roomAllowlist && !this.roomAllowlist.has(roomID)) { - this.logger.debug("Ignoring event outside room allowlist", { roomID }); + this.logger.debug("Ignoring event outside room allowlist", { roomId: roomID }); return; } if (this.userID && event.getSender() === this.userID) { this.logger.debug("Ignoring self-sent event", { - eventID: event.getId(), - userID: this.userID, + eventId: event.getId(), + userId: this.userID, }); return; } await this.tryDecryptEvent(event); + this.logger.debug("Matrix timeline event received", { + eventId: event.getId(), + eventType: event.getType(), + roomId: roomID, + sender: event.getSender(), + }); + const chat = this.requireChat(); if (event.getType() === EventType.Reaction) { - this.logger.debug("Matrix timeline event received", { - eventID: event.getId(), - eventType: event.getType(), - roomID, - }); - this.logger.debug("Processing reaction event", { - eventID: event.getId(), - roomID, - }); this.handleReactionEvent(event, roomID); return; } if (event.isRedaction()) { - this.logger.debug("Matrix timeline event received", { - eventID: event.getId(), - eventType: event.getType(), - roomID, - }); - this.logger.debug("Processing redaction event", { - eventID: event.getId(), - redacts: event.getAssociatedId(), - }); this.handleReactionRedaction(event); return; } - if ( - event.getType() !== EventType.RoomMessage && - event.getType() !== EventType.RoomMessageEncrypted - ) { - return; - } - this.logger.debug("Matrix timeline event received", { - eventID: event.getId(), - eventType: event.getType(), - roomID, - sender: event.getSender(), - }); - if (event.getType() !== EventType.RoomMessage) { return; } @@ -922,8 +1839,8 @@ export class MatrixAdapter implements Adapter { const threadID = this.threadIDForEvent(event, roomID); const message = this.parseMessage(event); this.logger.debug("Dispatching Matrix message to Chat SDK", { - eventID: event.getId(), - threadID, + eventId: event.getId(), + threadId: threadID, isMention: message.isMention, }); @@ -933,7 +1850,7 @@ export class MatrixAdapter implements Adapter { if (slash) { this.logger.debug("Dispatching slash command", { command: slash.command, - threadID, + threadId: threadID, }); chat.processSlashCommand({ adapter: this, @@ -947,6 +1864,125 @@ export class MatrixAdapter implements Adapter { } } + private async maybeAutoJoinInvite( + event: MatrixEvent, + roomID: string + ): Promise { + if (!this.inviteAutoJoinEnabled || event.getType() !== EventType.RoomMember) { + return; + } + + const membership = event.getContent<{ membership?: string }>()?.membership; + if (membership !== "invite") { + return; + } + + const targetUserID = event.getStateKey(); + if (!targetUserID || targetUserID !== this.userID) { + return; + } + + const inviter = event.getSender(); + if (!this.shouldAcceptInvite(roomID, inviter)) { + this.logger.info("Declined Matrix invite due to invite auto-join policy", { + roomId: roomID, + inviter, + }); + return; + } + + try { + await this.joinRoomWithRetry(roomID); + this.logger.info("Accepted Matrix invite", { + roomId: roomID, + inviter, + }); + } catch (error) { + this.logger.warn("Failed to auto-join Matrix invite", { + roomId: roomID, + inviter, + error, + }); + } + } + + private async joinRoomWithRetry(roomID: string, maxAttempts = 3): Promise { + let lastError: unknown; + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + try { + await this.requireClient().joinRoom(roomID); + return; + } catch (error) { + lastError = error; + if (!this.isRetryableJoinError(error) || attempt === maxAttempts) { + throw error; + } + + const retryDelayMs = this.retryDelayMsForJoinError(error); + this.logger.warn("Matrix invite auto-join rate limited, retrying", { + roomId: roomID, + attempt, + maxAttempts, + retryDelayMs, + error, + }); + await new Promise((resolve) => + setTimeout(resolve, retryDelayMs) + ); + } + } + + throw lastError; + } + + private shouldAcceptInvite( + roomID: string, + inviter: string | null | undefined + ): boolean { + if (this.roomAllowlist && !this.roomAllowlist.has(roomID)) { + return false; + } + + if (!this.inviteAutoJoinInviterAllowlist) { + return true; + } + + if (!inviter) { + return false; + } + + return this.inviteAutoJoinInviterAllowlist.has(inviter); + } + + private isRetryableJoinError(error: unknown): error is MatrixError { + return ( + error instanceof MatrixError && + (error.errcode === "M_LIMIT_EXCEEDED" || error.httpStatus === 429) + ); + } + + private retryDelayMsForJoinError(error: MatrixError): number { + const retryAfterMs = + typeof error.data?.retry_after_ms === "number" && error.data.retry_after_ms >= 0 + ? error.data.retry_after_ms + : undefined; + if (retryAfterMs !== undefined) { + return retryAfterMs; + } + + const retryAfterHeader = error.httpHeaders?.get("retry-after"); + const retryAfterSeconds = + typeof retryAfterHeader === "string" + ? Number.parseFloat(retryAfterHeader) + : Number.NaN; + if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0) { + return retryAfterSeconds * 1000; + } + + return 500; + } + private handleReactionEvent(event: MatrixEvent, roomID: string): void { const content = event.getContent<{ "m.relates_to"?: Record }>(); const relatesTo = content["m.relates_to"]; @@ -971,7 +2007,7 @@ export class MatrixAdapter implements Adapter { return; } - const threadID = this.encodeThreadId({ roomID }); + const threadID = this.resolveReactionThreadID(roomID, targetEventID); const emoji = getEmoji(key); const reactionEventID = event.getId(); @@ -984,6 +2020,8 @@ export class MatrixAdapter implements Adapter { rawEmoji: key, userID: sender, }); + + evictOldestEntries(this.reactionByEventID); } this.requireChat().processReaction({ @@ -998,6 +2036,16 @@ export class MatrixAdapter implements Adapter { }); } + private resolveReactionThreadID(roomID: string, relatedEventID: string): string { + const room = this.requireClient().getRoom(roomID); + const relatedEvent = room?.findEventById(relatedEventID); + if (!relatedEvent) { + return this.encodeThreadId({ roomID }); + } + + return this.threadIDForEvent(relatedEvent, roomID); + } + private handleReactionRedaction(event: MatrixEvent): void { const redactedEventID = event.getAssociatedId(); if (!redactedEventID) { @@ -1031,34 +2079,465 @@ export class MatrixAdapter implements Adapter { return this.encodeThreadId({ roomID, rootEventID }); } - private extractText(content: MatrixMessageContent): string { - if (typeof content.body === "string") { - return content.body; - } - if (typeof content.formatted_body === "string") { - return content.formatted_body; + private readRoomMetadata(room: Room, isDM: boolean): MatrixRoomMetadata { + const canonicalAlias = normalizeOptionalString( + this.readStateEventString(room, "m.room.canonical_alias", "alias") + ); + const topic = normalizeOptionalString( + this.readStateEventString(room, "m.room.topic", "topic") + ); + const avatarURL = normalizeOptionalString( + this.readStateEventString(room, "m.room.avatar", "url") + ); + const encryption = this.readStateEventContent(room, "m.room.encryption"); + const encryptionAlgorithm = readStringValue(encryption?.algorithm); + const encrypted = this.roomHasEncryptionStateEvent(room) ?? Boolean(encryptionAlgorithm); + + return { + roomID: room.roomId, + name: normalizeOptionalString(room.name) ?? canonicalAlias, + canonicalAlias, + topic, + avatarURL, + encrypted, + encryptionAlgorithm, + isDM, + }; + } + + private readStateEventContent( + room: Room, + eventType: string + ): Record | undefined { + const event = this.readRoomStateEvent(room, eventType); + if (!event) { + return undefined; + } + const content = event.getContent>(); + return isRecord(content) ? content : undefined; + } + + private readStateEventString( + room: Room, + eventType: string, + key: string + ): string | undefined { + const content = this.readStateEventContent(room, eventType); + return readStringValue(content?.[key]); + } + + private readRoomStateEvent(room: Room, eventType: string): MatrixEvent | undefined { + if (!("currentState" in room) || !room.currentState) { + return undefined; + } + + const { currentState } = room; + if ( + typeof currentState !== "object" || + !("getStateEvents" in currentState) || + typeof currentState.getStateEvents !== "function" + ) { + return undefined; + } + + return currentState.getStateEvents(eventType, "") ?? undefined; + } + + private roomHasEncryptionStateEvent(room: Room): boolean | undefined { + if ( + !("hasEncryptionStateEvent" in room) || + typeof room.hasEncryptionStateEvent !== "function" + ) { + return undefined; + } + + return room.hasEncryptionStateEvent(); + } + + private readRoomMember(room: Room | undefined, userId: string): RoomMember | undefined { + if (!room || !("getMember" in room) || typeof room.getMember !== "function") { + return undefined; + } + + return room.getMember(userId) ?? undefined; + } + + private parseMatrixContent(content: MatrixMessageContent): ParsedMatrixContent { + const mentionedUserIDs = this.extractMentionedUserIDs(content); + const mentionsRoom = this.extractRoomMention(content); + const formattedBody = normalizeOptionalString(content.formatted_body); + if (formattedBody) { + const htmlMarkdown = this.parseMatrixHTML(formattedBody); + for (const mentionedUserID of htmlMarkdown.mentionedUserIDs) { + mentionedUserIDs.add(mentionedUserID); + } + + if (htmlMarkdown.markdown.length > 0) { + return { + text: markdownToPlainText(htmlMarkdown.markdown), + markdown: htmlMarkdown.markdown, + mentionedUserIDs, + mentionsRoom, + }; + } + } + + const body = this.stripReplyFallbackFromBody( + normalizeOptionalString(content.body) ?? "" + ); + return { + text: body, + markdown: this.markdownForPlainText(body, content.msgtype), + mentionedUserIDs, + mentionsRoom, + }; + } + + private parseMatrixHTML( + html: string + ): { markdown: string; mentionedUserIDs: Set } { + const root = parseHTML(this.stripReplyFallbackFromHTML(html)); + const mentionedUserIDs = new Set(); + const markdown = this.normalizeMarkdownSpacing( + this.renderHTMLNodesToMarkdown(root.childNodes, mentionedUserIDs) + ); + return { + markdown, + mentionedUserIDs, + }; + } + + private renderHTMLNodesToMarkdown( + nodes: HTMLNode[], + mentionedUserIDs: Set + ): string { + return nodes + .map((node) => this.renderHTMLNodeToMarkdown(node, mentionedUserIDs)) + .join(""); + } + + private renderHTMLNodeToMarkdown( + node: HTMLNode, + mentionedUserIDs: Set + ): string { + if (node.nodeType === NodeType.TEXT_NODE) { + return node.text; + } + + if (!(node instanceof HTMLElement)) { + return ""; + } + + const tagName = node.tagName.toLowerCase(); + const children = this.renderHTMLNodesToMarkdown(node.childNodes, mentionedUserIDs); + + switch (tagName) { + case "mx-reply": + return ""; + case "html": + case "body": + case "span": + return children; + case "br": + return "\n"; + case "p": + case "div": + return children.trim() ? `${children.trim()}\n\n` : ""; + case "strong": + case "b": + return children ? `**${children}**` : ""; + case "em": + case "i": + return children ? `*${children}*` : ""; + case "del": + case "s": + return children ? `~~${children}~~` : ""; + case "code": + return node.parentNode instanceof HTMLElement && + node.parentNode.tagName.toLowerCase() === "pre" + ? children + : `\`${children}\``; + case "pre": { + const codeContent = children.replace(/\n+$/u, ""); + return codeContent ? `\n\`\`\`\n${codeContent}\n\`\`\`\n\n` : ""; + } + case "blockquote": { + const quoted = children.trim(); + if (!quoted) { + return ""; + } + return `${quoted + .split("\n") + .map((line) => `> ${line}`) + .join("\n")}\n\n`; + } + case "ul": + return `${node.childNodes + .map((child) => this.renderListItemToMarkdown(child, mentionedUserIDs, null)) + .filter(Boolean) + .join("\n")}\n\n`; + case "ol": + return `${node.childNodes + .map((child, index) => + this.renderListItemToMarkdown(child, mentionedUserIDs, index + 1) + ) + .filter(Boolean) + .join("\n")}\n\n`; + case "a": + return this.renderHTMLLinkToMarkdown(node, children, mentionedUserIDs); + case "img": + return normalizeOptionalString(node.getAttribute("alt")) ?? "image"; + default: + return children; + } + } + + private renderListItemToMarkdown( + node: HTMLNode, + mentionedUserIDs: Set, + ordinal: number | null + ): string { + if (!(node instanceof HTMLElement) || node.tagName.toLowerCase() !== "li") { + return ""; + } + const content = this.normalizeMarkdownSpacing( + this.renderHTMLNodesToMarkdown(node.childNodes, mentionedUserIDs) + ); + if (!content) { + return ""; + } + return `${ordinal === null ? "-" : `${ordinal}.`} ${content}`; + } + + private renderHTMLLinkToMarkdown( + node: HTMLElement, + children: string, + mentionedUserIDs: Set + ): string { + const href = normalizeOptionalString(node.getAttribute("href")); + const text = children || node.text; + if (!href) { + return text; + } + + const mentionedUserID = this.parseMatrixToUserID(href); + if (mentionedUserID) { + mentionedUserIDs.add(mentionedUserID); + return text || this.matrixMentionDisplayText(mentionedUserID); + } + + return `[${text || href}](${href})`; + } + + private parseMatrixToUserID(href: string): string | null { + let url: URL; + try { + url = new URL(href); + } catch { + return null; + } + + if (url.hostname !== "matrix.to") { + return null; + } + + const rawPath = url.hash.startsWith("#/") ? url.hash.slice(2) : url.hash; + const firstSegment = rawPath.split("/")[0]; + if (!firstSegment) { + return null; + } + + const identifier = decodeURIComponent(firstSegment); + return identifier.startsWith("@") ? identifier : null; + } + + private extractMentionedUserIDs(content: MatrixMessageContent): Set { + const mentions = new Set(); + const matrixMentions = content["m.mentions"]; + if (!isRecord(matrixMentions) || !Array.isArray(matrixMentions.user_ids)) { + return mentions; + } + + for (const userID of matrixMentions.user_ids) { + if (typeof userID === "string" && userID.length > 0) { + mentions.add(userID); + } + } + + return mentions; + } + + private extractRoomMention(content: MatrixMessageContent): boolean { + const matrixMentions = content["m.mentions"]; + return isRecord(matrixMentions) && matrixMentions.room === true; + } + + private stripReplyFallbackFromBody(body: string): string { + const lines = body.split("\n"); + let index = 0; + while (index < lines.length && lines[index]?.startsWith(">")) { + index += 1; + } + + if (index === 0 || index >= lines.length || lines[index] !== "") { + return body; + } + + return lines.slice(index + 1).join("\n"); + } + + private stripReplyFallbackFromHTML(html: string): string { + const root = parseHTML(html); + for (const child of [...root.childNodes]) { + if (child instanceof HTMLElement && child.tagName.toLowerCase() === "mx-reply") { + child.remove(); + } + } + return root.toString(); + } + + private extractAttachments(content: MatrixMessageContent) { + const url = typeof content.url === "string" ? content.url : undefined; + if (!url) { + return []; + } + + const info = isRecord(content.info) ? content.info : undefined; + const mimeType = typeof info?.mimetype === "string" ? info.mimetype : undefined; + const attachment: Attachment = { + type: this.attachmentTypeForContent(content, mimeType), + url, + name: normalizeOptionalString(content.body), + mimeType, + size: typeof info?.size === "number" ? info.size : undefined, + width: typeof info?.w === "number" ? info.w : undefined, + height: typeof info?.h === "number" ? info.h : undefined, + fetchData: this.createAttachmentFetcher(url), + }; + + return [attachment]; + } + + private extractEditedContent(raw: MatrixEvent): { + content?: MatrixMessageContent; + editedAt?: Date; + } | undefined { + const replacement = raw.getServerAggregatedRelation<{ + content?: MatrixRoomMessageContent; + origin_server_ts?: number; + }>(RelationType.Replace); + const replacementContent = isRecord(replacement?.content) + ? replacement.content + : undefined; + const newContent = isRecord(replacementContent?.["m.new_content"]) + ? replacementContent["m.new_content"] + : undefined; + + if (!newContent) { + return undefined; + } + + return { + content: newContent, + editedAt: + typeof replacement?.origin_server_ts === "number" + ? new Date(replacement.origin_server_ts) + : undefined, + }; + } + + private attachmentTypeForContent( + content: MatrixMessageContent, + mimeType?: string + ): Attachment["type"] { + switch (content.msgtype) { + case MsgType.Image: + return "image"; + case MsgType.Video: + return "video"; + case MsgType.Audio: + return "audio"; + case MsgType.File: + return "file"; + default: { + const mediaType = this.messageTypeForMimeType(mimeType); + switch (mediaType) { + case MsgType.Image: + return "image"; + case MsgType.Video: + return "video"; + case MsgType.Audio: + return "audio"; + default: + return "file"; + } + } + } + } + + private createAttachmentFetcher(url: string): Attachment["fetchData"] | undefined { + if (typeof fetch !== "function") { + return undefined; } - return ""; + + return async () => { + const client = this.requireClient(); + const downloadURL = this.resolveAttachmentDownloadURL(url, client); + if (!downloadURL) { + throw new Error(`Unable to resolve Matrix attachment download URL for ${url}`); + } + + const accessToken = + typeof client.getAccessToken === "function" + ? normalizeOptionalString(client.getAccessToken() ?? undefined) + : undefined; + const response = await fetch(downloadURL, { + headers: accessToken + ? { + Authorization: `Bearer ${accessToken}`, + } + : undefined, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch Matrix attachment (${response.status} ${response.statusText})` + ); + } + + return Buffer.from(await response.arrayBuffer()); + }; } - private extractAttachments(content: MatrixMessageContent) { - const url = typeof content.url === "string" ? content.url : undefined; - if (!url) { - return []; + private resolveAttachmentDownloadURL( + url: string, + client: MatrixClient + ): string | undefined { + if (typeof client.mxcUrlToHttp === "function") { + const authenticatedURL = normalizeOptionalString( + client.mxcUrlToHttp( + url, + undefined, + undefined, + undefined, + true, + true, + true + ) ?? undefined + ); + if (authenticatedURL) { + return authenticatedURL; + } + + const unauthenticatedURL = normalizeOptionalString( + client.mxcUrlToHttp(url, undefined, undefined, undefined, true) ?? undefined + ); + if (unauthenticatedURL) { + return unauthenticatedURL; + } } - return [ - { - type: "file" as const, - url, - mimeType: - typeof content.info === "object" && - content.info !== null && - typeof (content.info as { mimetype?: unknown }).mimetype === "string" - ? (content.info as { mimetype: string }).mimetype - : undefined, - }, - ]; + return url.startsWith("mxc://") ? undefined : url; } private isEdited(event: MatrixEvent): boolean { @@ -1066,12 +2545,19 @@ export class MatrixAdapter implements Adapter { return relation?.rel_type === RelationType.Replace; } - private isMentioned(content: MatrixMessageContent, text: string): boolean { + private isMentioned(content: MatrixMessageContent, parsed: ParsedMatrixContent): boolean { + if (parsed.mentionsRoom) { + return true; + } + if (this.userID && parsed.mentionedUserIDs.has(this.userID)) { + return true; + } + const formatted = typeof content.formatted_body === "string" ? content.formatted_body : ""; const hasUserID = this.userID - ? text.includes(this.userID) || formatted.includes(this.userID) + ? parsed.text.includes(this.userID) || formatted.includes(this.userID) : false; const hasMatrixTo = this.userID ? formatted.includes(`matrix.to/#/${encodeURIComponent(this.userID)}`) @@ -1082,7 +2568,7 @@ export class MatrixAdapter implements Adapter { : `@${this.userName}`; const hasUserName = - text.includes(usernameMention) || formatted.includes(usernameMention); + parsed.text.includes(usernameMention) || formatted.includes(usernameMention); return hasUserID || hasMatrixTo || hasUserName; } @@ -1110,60 +2596,390 @@ export class MatrixAdapter implements Adapter { private toRoomMessageContent( message: AdapterPostableMessage - ): { body: string; msgtype: MsgType } { - const body = this.toText(message); - - return { - body, + ): MatrixTextMessageContent { + const rendered = this.renderTextMessage(message); + const content: MatrixTextMessageContent = { + body: rendered.body, msgtype: MsgType.Text, }; + if (rendered.formattedBody) { + content.format = "org.matrix.custom.html"; + content.formatted_body = rendered.formattedBody; + } + if (rendered.mentions) { + content["m.mentions"] = rendered.mentions; + } + + return content; } - private toText(message: AdapterPostableMessage): string { + private renderTextMessage(message: AdapterPostableMessage): RenderedMatrixMessage { if (typeof message === "string") { - return message; + return this.renderPlainTextMessage(message); } if (isCardElement(message)) { - return "[Card message]"; + return this.renderPlainTextMessage("[Card message]"); } if (typeof message === "object" && message !== null) { if ("raw" in message && typeof message.raw === "string") { - return message.raw; + return this.renderPlainTextMessage(message.raw); } if ("markdown" in message && typeof message.markdown === "string") { - return message.markdown; + return this.renderMarkdownMessage(message.markdown); } if ("ast" in message) { - return stringifyMarkdown(message.ast); + return this.renderMarkdownMessage(stringifyMarkdown(message.ast)); } if ("card" in message) { - return message.fallbackText ?? "[Card message]"; + return this.renderPlainTextMessage(message.fallbackText ?? "[Card message]"); } } - return String(message); + return { body: "" }; } - private isMessageEvent( - event: MatrixEvent, - roomID: string, - rootEventID?: string - ): boolean { - if (event.getType() !== EventType.RoomMessage) { - return false; + private renderPlainTextMessage(text: string): RenderedMatrixMessage { + const rendered = this.replaceMentionPlaceholdersInPlainText(text); + if (rendered.mentionedUserIDs.size === 0) { + return { + body: rendered.body, + }; } - if (event.getRoomId() !== roomID) { - return false; + return { + body: rendered.body, + formattedBody: this.renderMarkdownToMatrixHTML(rendered.markdown), + mentions: this.buildMentionsContent(rendered.mentionedUserIDs), + }; + } + + private renderMarkdownMessage(markdown: string): RenderedMatrixMessage { + const rendered = this.replaceMentionPlaceholdersInMarkdown(markdown); + return { + body: markdownToPlainText(rendered.markdown), + formattedBody: this.renderMarkdownToMatrixHTML(rendered.markdown), + mentions: this.buildMentionsContent(rendered.mentionedUserIDs), + }; + } + + private replaceMentionPlaceholdersInPlainText(text: string): { + body: string; + markdown: string; + mentionedUserIDs: Set; + } { + const mentionedUserIDs = new Set(); + const pattern = /<@(@[^>\s]+:[^>\s]+)>/gu; + let body = ""; + let markdown = ""; + let lastIndex = 0; + + for (const match of text.matchAll(pattern)) { + const [token, userID] = match; + const index = match.index ?? 0; + const plainSegment = text.slice(lastIndex, index); + body += plainSegment; + markdown += escapeMarkdownText(plainSegment); + + const mentionText = this.matrixMentionDisplayText(userID); + body += mentionText; + markdown += `[${escapeMarkdownLinkText(mentionText)}](${this.matrixToUserLink(userID)})`; + mentionedUserIDs.add(userID); + lastIndex = index + token.length; + } + + const trailing = text.slice(lastIndex); + body += trailing; + markdown += escapeMarkdownText(trailing); + + return { body, markdown, mentionedUserIDs }; + } + + private replaceMentionPlaceholdersInMarkdown(markdown: string): { + markdown: string; + mentionedUserIDs: Set; + } { + const mentionedUserIDs = new Set(); + const transformed = markdown.replace( + /<@(@[^>\s]+:[^>\s]+)>/gu, + (_match, userID: string) => { + mentionedUserIDs.add(userID); + return `[${escapeMarkdownLinkText(this.matrixMentionDisplayText(userID))}](${this.matrixToUserLink( + userID + )})`; + } + ); + + return { + markdown: transformed, + mentionedUserIDs, + }; + } + + private renderMarkdownToMatrixHTML(markdown: string): string { + return marked.parse(markdown, { + async: false, + breaks: true, + gfm: true, + }); + } + + private buildMentionsContent( + mentionedUserIDs: Set + ): { room?: boolean; user_ids?: string[] } | undefined { + if (mentionedUserIDs.size === 0) { + return undefined; } - if (!rootEventID) { - return !event.threadRootId; + return { + user_ids: [...mentionedUserIDs], + }; + } + + private matrixToUserLink(userID: string): string { + return `https://matrix.to/#/${encodeURIComponent(userID)}`; + } + + private matrixMentionDisplayText(userID: string): string { + return `@${matrixLocalpart(userID)}`; + } + + private markdownForPlainText(text: string, msgtype?: string): string { + const escaped = escapeMarkdownText(text); + if (msgtype === "m.emote" && escaped.length > 0) { + return `*${escaped}*`; } + return escaped; + } - return event.threadRootId === rootEventID || event.getId() === rootEventID; + private normalizeMarkdownSpacing(markdown: string): string { + return markdown.replace(/\n{3,}/gu, "\n\n").trim(); + } + + private mergeTextAndLinks( + content: MatrixTextMessageContent, + linkLines: string[] + ): MatrixTextMessageContent { + if (linkLines.length === 0) { + return content; + } + + const suffix = linkLines.join("\n"); + const body = content.body ?? ""; + const mergedBody = body ? `${body}\n\n${suffix}` : suffix; + if (!content.formatted_body) { + return { + ...content, + body: mergedBody, + }; + } + + const formattedSuffix = linkLines + .map((line) => `

${escapeHTML(line)}

`) + .join(""); + + return { + ...content, + body: mergedBody, + formatted_body: `${content.formatted_body}${formattedSuffix}`, + }; + } + + private collectLinkOnlyAttachmentLines(attachments: Attachment[]): string[] { + const lines: string[] = []; + for (const attachment of attachments) { + const hasLocalData = + Boolean(attachment.data) || typeof attachment.fetchData === "function"; + if (hasLocalData) { + continue; + } + if (!attachment.url) { + continue; + } + const label = attachment.name ?? attachment.type; + lines.push(`${label}: ${attachment.url}`); + } + return lines; + } + + private async collectUploads( + message: AdapterPostableMessage, + attachments: Attachment[] + ): Promise { + const uploads: OutboundUpload[] = []; + const files = this.extractFilesFromMessage(message); + for (const file of files) { + uploads.push({ + data: this.normalizeUploadData(file.data), + fileName: file.filename, + info: { + mimetype: normalizeOptionalString(file.mimeType), + size: this.binarySize(file.data), + }, + msgtype: this.messageTypeForMimeType(normalizeOptionalString(file.mimeType)), + }); + } + + for (const attachment of attachments) { + const data = await this.readAttachmentData(attachment); + if (!data) { + continue; + } + const fileName = + normalizeOptionalString(attachment.name) ?? + this.defaultAttachmentName(attachment); + uploads.push({ + data: this.normalizeUploadData(data), + fileName, + info: { + h: attachment.height, + mimetype: normalizeOptionalString(attachment.mimeType), + size: attachment.size ?? this.binarySize(data), + w: attachment.width, + }, + msgtype: this.messageTypeForAttachment(attachment), + type: attachment.type, + }); + } + + return uploads; + } + + private extractFilesFromMessage(message: AdapterPostableMessage): FileUpload[] { + if (typeof message !== "object" || message === null) { + return []; + } + if (!("files" in message) || !Array.isArray(message.files)) { + return []; + } + return message.files.flatMap((file): FileUpload[] => { + const normalized = this.normalizeFileUpload(file); + return normalized ? [normalized] : []; + }); + } + + private normalizeFileUpload(file: unknown): FileUpload | null { + if (!isRecord(file)) { + return null; + } + + const filename = normalizeOptionalString( + typeof file.filename === "string" ? file.filename : undefined + ); + if (!filename) { + return null; + } + + const data = this.normalizeFileUploadData(file.data); + if (!data) { + this.logger.warn("Skipping invalid Matrix file upload", { filename }); + return null; + } + + return { + filename, + data, + mimeType: + typeof file.mimeType === "string" ? normalizeOptionalString(file.mimeType) : undefined, + }; + } + + private normalizeFileUploadData(data: unknown): Buffer | Blob | ArrayBuffer | null { + if (Buffer.isBuffer(data) || data instanceof Blob || data instanceof ArrayBuffer) { + return data; + } + + if (data instanceof Uint8Array) { + return Buffer.from(data); + } + + return null; + } + + private extractAttachmentsFromMessage(message: AdapterPostableMessage): Attachment[] { + if (typeof message !== "object" || message === null) { + return []; + } + if (!("attachments" in message) || !Array.isArray(message.attachments)) { + return []; + } + return message.attachments.filter((a): a is Attachment => isRecord(a)); + } + + private async readAttachmentData( + attachment: Attachment + ): Promise { + if (typeof attachment.fetchData === "function") { + return attachment.fetchData(); + } + return attachment.data ?? null; + } + + private normalizeUploadData(data: Buffer | Blob | ArrayBuffer): Blob { + if (data instanceof Blob) { + return data; + } + if (this.isNodeBuffer(data)) { + return new Blob([new Uint8Array(data)]); + } + return new Blob([data]); + } + + private binarySize(data: Buffer | Blob | ArrayBuffer): number { + if (data instanceof ArrayBuffer) { + return data.byteLength; + } + if (this.isNodeBuffer(data)) { + return data.length; + } + return data.size; + } + + private isNodeBuffer(value: unknown): value is Buffer { + return typeof Buffer !== "undefined" && Buffer.isBuffer(value); + } + + private messageTypeForAttachment(attachment: Attachment): MatrixMediaMsgType { + switch (attachment.type) { + case "image": + return MsgType.Image; + case "video": + return MsgType.Video; + case "audio": + return MsgType.Audio; + default: + return this.messageTypeForMimeType(normalizeOptionalString(attachment.mimeType)); + } + } + + private messageTypeForMimeType(mimeType?: string): MatrixMediaMsgType { + if (!mimeType) { + return MsgType.File; + } + if (mimeType.startsWith("image/")) { + return MsgType.Image; + } + if (mimeType.startsWith("video/")) { + return MsgType.Video; + } + if (mimeType.startsWith("audio/")) { + return MsgType.Audio; + } + return MsgType.File; + } + + private defaultAttachmentName(attachment: Attachment): string { + switch (attachment.type) { + case "image": + return "image"; + case "video": + return "video"; + case "audio": + return "audio"; + default: + return "file"; + } } private mustGetEventByID(roomID: string, eventID: string): MatrixEvent { @@ -1175,16 +2991,68 @@ export class MatrixAdapter implements Adapter { return event; } - private makeUser(userId: string) { + private resolveSentEvent( + roomID: string, + eventID: string, + fallback: { + content: MatrixRoomMessageContent; + roomID: string; + sender?: string; + } + ): MatrixEvent { + try { + return this.mustGetEventByID(roomID, eventID); + } catch (error) { + this.logger.warn("Sent Matrix event not found in local timeline; using synthetic fallback", { + error, + eventId: eventID, + roomId: roomID, + }); + + return new MatrixEvent({ + content: fallback.content, + event_id: eventID, + origin_server_ts: Date.now(), + room_id: fallback.roomID, + sender: fallback.sender ?? "", + type: EventType.RoomMessage, + }); + } + } + + private isUsableDirectRoom(roomID: string): boolean { + const room = this.requireClient().getRoom(roomID); + if (!room) { + return false; + } + + const membership = room.getMyMembership(); + return membership === "join" || membership === "invite"; + } + + private makeUser(userId: string, roomId?: string) { + const room = roomId ? this.client?.getRoom(roomId) ?? undefined : undefined; + const member = this.readRoomMember(room, userId); + const localpart = matrixLocalpart(userId); + const displayName = + readStringValue(member?.rawDisplayName) ?? + readStringValue(member?.name) ?? + localpart; + const isBot: boolean | "unknown" = userId === this.userID ? true : "unknown"; + return { userId, - userName: userId, - fullName: userId, - isBot: userId === this.userID, + userName: localpart, + fullName: displayName, + isBot, isMe: userId === this.userID, }; } + private rawEmoji(emoji: EmojiValue | string): string { + return typeof emoji === "string" ? emoji : emoji.toString(); + } + private myReactionKey( threadId: string, messageId: string, @@ -1193,27 +3061,31 @@ export class MatrixAdapter implements Adapter { return `${threadId}::${messageId}::${rawEmoji}`; } + private get persistenceSessionPrefix(): string { + return `${this.persistenceConfig.keyPrefix}:session:${encodeURIComponent(this.baseURL)}`; + } + + private get persistenceStorePrefix(): string { + return `${this.persistenceConfig.keyPrefix}:store`; + } + private get sessionBasePrefix(): string { - return `${MATRIX_SESSION_PREFIX}:${encodeURIComponent(this.baseURL)}`; + return this.persistenceSessionPrefix; } private getSessionStorageKey(userID: string): string { - if (this.sessionConfig.key) { - return this.sessionConfig.key; - } - return `${this.sessionBasePrefix}:user:${encodeURIComponent(userID)}`; } private getSessionUsernameTemporaryKey(): string | null { - if (this.sessionConfig.key || this.auth.type !== "password") { + if (this.auth.type !== "password") { return null; } return `${this.sessionBasePrefix}:username:${encodeURIComponent(this.auth.username)}`; } private async loadPersistedSession(): Promise { - if (!this.sessionConfig.enabled || !this.stateAdapter) { + if (!this.stateAdapter) { return null; } @@ -1239,57 +3111,52 @@ export class MatrixAdapter implements Adapter { return null; } - private async persistSession(auth: ResolvedAuth): Promise { - if (!this.sessionConfig.enabled || !this.stateAdapter) { + private async persistSession(auth: ResolvedAuth, existingCreatedAt?: string): Promise { + if (!this.stateAdapter) { return; } const now = new Date().toISOString(); - const existing = await this.loadPersistedSession(); const session: StoredSession = { accessToken: auth.accessToken, authType: this.auth.type, baseURL: this.baseURL, - createdAt: existing?.createdAt ?? now, + createdAt: existingCreatedAt ?? now, deviceID: auth.deviceID, - e2eeEnabled: Boolean(this.e2eeConfig?.enabled), + e2eeEnabled: this.e2eeEnabled, recoveryKeyPresent: Boolean(this.e2eeConfig?.storagePassword), updatedAt: now, userID: auth.userID, username: this.auth.type === "password" ? this.auth.username : undefined, }; const encodedSession = this.encodeStoredSession(session); + const sessionKey = this.getSessionStorageKey(auth.userID); await this.stateAdapter.set( - this.getSessionStorageKey(auth.userID), + sessionKey, encodedSession, - this.sessionConfig.ttlMs + this.persistenceConfig.session.ttlMs ); const temporaryKey = this.getSessionUsernameTemporaryKey(); - if (temporaryKey && temporaryKey !== this.getSessionStorageKey(auth.userID)) { - await this.stateAdapter.set(temporaryKey, encodedSession, this.sessionConfig.ttlMs); + if (temporaryKey && temporaryKey !== sessionKey) { + await this.stateAdapter.set( + temporaryKey, + encodedSession, + this.persistenceConfig.session.ttlMs + ); } } private encodeStoredSession(session: StoredSession): StoredSession { - if (!this.sessionConfig.encrypt) { + if (!this.persistenceConfig.session.encrypt) { return session; } - const encryptedPayload = this.sessionConfig.encrypt(JSON.stringify(session)); - return { - authType: session.authType, - baseURL: session.baseURL, - createdAt: session.createdAt, - deviceID: session.deviceID, - e2eeEnabled: session.e2eeEnabled, - encryptedPayload, - recoveryKeyPresent: session.recoveryKeyPresent, - updatedAt: session.updatedAt, - userID: session.userID, - username: session.username, - }; + const encryptedPayload = + this.persistenceConfig.session.encrypt(JSON.stringify(session)); + const { accessToken, ...metadata } = session; + return { ...metadata, encryptedPayload }; } private decodeStoredSession( @@ -1299,33 +3166,36 @@ export class MatrixAdapter implements Adapter { return session; } - if (!this.sessionConfig.decrypt) { + if (!this.persistenceConfig.session.decrypt) { return null; } try { - const decryptedJSON = this.sessionConfig.decrypt(session.encryptedPayload); - const parsed = JSON.parse(decryptedJSON) as StoredSession; + const decryptedJSON = + this.persistenceConfig.session.decrypt(session.encryptedPayload); + const parsed: unknown = JSON.parse(decryptedJSON); + if (!this.isValidStoredSession(parsed)) { + return null; + } return parsed; } catch (error) { this.logger.warn("Failed to decrypt persisted Matrix session", { - error: String(error), + error, }); return null; } } private isValidStoredSession(value: unknown): value is StoredSession { - if (!value || typeof value !== "object") { + if (!isRecord(value)) { return false; } - const session = value as Partial; return ( - typeof session.accessToken === "string" && - typeof session.userID === "string" && - typeof session.baseURL === "string" && - session.baseURL === this.baseURL + typeof value.accessToken === "string" && + typeof value.userID === "string" && + typeof value.baseURL === "string" && + value.baseURL === this.baseURL ); } @@ -1342,49 +3212,75 @@ export class MatrixAdapter implements Adapter { return null; } - const privateKey = decodeRecoveryKey(this.recoveryKey); - return [keyID, privateKey]; + try { + const privateKey = decodeRecoveryKey(this.recoveryKey) as Uint8Array; + return [keyID, privateKey]; + } catch { + this.logger.warn("Invalid recovery key format, unable to decode"); + return null; + } } private validateConfig(config: MatrixAdapterConfig): void { if (!config.baseURL?.trim()) { throw new Error("baseURL is required."); } - if (config.session?.ttlMs !== undefined && config.session.ttlMs <= 0) { - throw new Error("session.ttlMs must be a positive number."); + if (config.persistence?.session?.ttlMs !== undefined && config.persistence.session.ttlMs <= 0) { + throw new Error("persistence.session.ttlMs must be a positive number."); + } + if ( + config.persistence?.sync?.persistIntervalMs !== undefined && + config.persistence.sync.persistIntervalMs <= 0 + ) { + throw new Error("persistence.sync.persistIntervalMs must be a positive number."); + } + if ( + config.persistence?.sync?.snapshotTtlMs !== undefined && + config.persistence.sync.snapshotTtlMs <= 0 + ) { + throw new Error("persistence.sync.snapshotTtlMs must be a positive number."); } if ( - (config.session?.encrypt && !config.session?.decrypt) || - (!config.session?.encrypt && config.session?.decrypt) + (config.persistence?.session?.encrypt && !config.persistence?.session?.decrypt) || + (!config.persistence?.session?.encrypt && config.persistence?.session?.decrypt) ) { throw new Error( - "session.encrypt and session.decrypt must be provided together." + "persistence.session.encrypt and persistence.session.decrypt must be provided together." ); } } - private async resolveDeviceID(): Promise { - if (this.deviceID) { - return; + private async resolveDeviceID(...candidates: Array): Promise { + const configuredDeviceID = normalizeOptionalString(this.deviceID); + if (configuredDeviceID) { + this.deviceID = configuredDeviceID; + return configuredDeviceID; + } + + for (const candidate of candidates) { + const normalized = normalizeOptionalString(candidate); + if (normalized) { + this.deviceID = normalized; + await this.persistDeviceID(normalized); + return normalized; + } } const persisted = await this.loadPersistedDeviceID(); if (persisted) { this.deviceID = persisted; - return; + return persisted; } const generated = generateDeviceID(); this.deviceID = generated; await this.persistDeviceID(generated); + return generated; } private getDeviceIDStorageKey(identityHint?: string): string { - if (this.deviceIDPersistence.key) { - return this.deviceIDPersistence.key; - } - - const basePrefix = `${MATRIX_DEVICE_PREFIX}:${encodeURIComponent(this.baseURL)}`; + const basePrefix = + `${this.persistenceConfig.keyPrefix}:device:${encodeURIComponent(this.baseURL)}`; const hint = identityHint ?? this.auth.userID ?? @@ -1393,7 +3289,7 @@ export class MatrixAdapter implements Adapter { } private async loadPersistedDeviceID(): Promise { - if (!this.deviceIDPersistence.enabled || !this.stateAdapter) { + if (!this.stateAdapter) { return null; } @@ -1407,7 +3303,7 @@ export class MatrixAdapter implements Adapter { continue; } const value = await this.stateAdapter.get(key); - const normalized = normalizeOptionalString(value ?? undefined); + const normalized = normalizeOptionalString(value); if (normalized) { return normalized; } @@ -1417,7 +3313,7 @@ export class MatrixAdapter implements Adapter { } private async persistDeviceID(deviceID: string, identityHint?: string): Promise { - if (!this.deviceIDPersistence.enabled || !this.stateAdapter) { + if (!this.stateAdapter) { return; } @@ -1438,12 +3334,7 @@ export class MatrixAdapter implements Adapter { const temporaryKey = this.getDeviceIDStorageKey(); const canonicalKey = this.getDeviceIDStorageKey(userID); - if ( - this.deviceIDPersistence.enabled && - this.stateAdapter && - !this.deviceIDPersistence.key && - temporaryKey !== canonicalKey - ) { + if (this.stateAdapter && temporaryKey !== canonicalKey) { await this.stateAdapter.delete(temporaryKey); } } @@ -1463,10 +3354,10 @@ export class MatrixAdapter implements Adapter { return; } - const loggerWithSetLevel = matrixSDKLogger as unknown as { - setLevel?: (level: number, persist?: boolean) => void; - }; - loggerWithSetLevel.setLevel?.(numericLevel, false); + const setLevel = Reflect.get(matrixSDKLogger, "setLevel"); + if (typeof setLevel === "function") { + setLevel.call(matrixSDKLogger, numericLevel, false); + } matrixSDKLogConfigured = true; } } @@ -1488,45 +3379,60 @@ export function createMatrixAdapter(config?: MatrixAdapterConfig): MatrixAdapter } const recoveryKey = process.env.MATRIX_RECOVERY_KEY; - const e2eeEnabled = - Boolean(recoveryKey) || envBool(process.env.MATRIX_E2EE_ENABLED); + const inviteAutoJoinInviterAllowlist = parseEnvList( + process.env.MATRIX_INVITE_AUTOJOIN_ALLOWLIST + ); + const inviteAutoJoinEnabled = envBool( + process.env.MATRIX_INVITE_AUTOJOIN, + inviteAutoJoinInviterAllowlist.length > 0 + ); const auth = resolveAuthFromEnv(); return new MatrixAdapter({ baseURL, auth, + userName: process.env.MATRIX_BOT_USERNAME ?? "bot", deviceID: normalizeOptionalString(process.env.MATRIX_DEVICE_ID), - deviceIDPersistence: { - enabled: envBool(process.env.MATRIX_DEVICE_ID_PERSIST_ENABLED, true), - key: normalizeOptionalString(process.env.MATRIX_DEVICE_ID_PERSIST_KEY), - }, commandPrefix: process.env.MATRIX_COMMAND_PREFIX, recoveryKey, - e2ee: { - enabled: e2eeEnabled, - useIndexedDB: envBool( - process.env.MATRIX_E2EE_USE_INDEXEDDB, - hasIndexedDB() - ), - cryptoDatabasePrefix: process.env.MATRIX_E2EE_DB_PREFIX, - storagePassword: process.env.MATRIX_E2EE_STORAGE_PASSWORD ?? recoveryKey, - storageKey: decodeBase64( - process.env.MATRIX_E2EE_STORAGE_KEY_BASE64, - "MATRIX_E2EE_STORAGE_KEY_BASE64" - ), - }, - session: { - enabled: envBool(process.env.MATRIX_SESSION_ENABLED, true), - key: process.env.MATRIX_SESSION_KEY, - ttlMs: parseEnvNumber(process.env.MATRIX_SESSION_TTL_MS), - }, + inviteAutoJoin: inviteAutoJoinEnabled + ? { + inviterAllowlist: inviteAutoJoinInviterAllowlist, + } + : undefined, matrixSDKLogLevel: parseSDKLogLevel(process.env.MATRIX_SDK_LOG_LEVEL) ?? "error", }); } -export type { MatrixAdapterConfig, MatrixThreadID } from "./types"; +export type { + MatrixAdapterConfig, + MatrixCreateStoreOptions, + MatrixPersistenceConfig, + MatrixPersistenceSyncConfig, + MatrixThreadID, +} from "./types"; + +function normalizePersistenceConfig( + config?: MatrixPersistenceConfig +): ResolvedPersistenceConfig { + return { + keyPrefix: + normalizeOptionalString(config?.keyPrefix) ?? DEFAULT_PERSISTENCE_KEY_PREFIX, + session: { + decrypt: config?.session?.decrypt, + encrypt: config?.session?.encrypt, + ttlMs: config?.session?.ttlMs, + }, + sync: { + persistIntervalMs: + config?.sync?.persistIntervalMs ?? + DEFAULT_MATRIX_STORE_PERSIST_INTERVAL_MS, + snapshotTtlMs: config?.sync?.snapshotTtlMs, + }, + }; +} function resolveAuthFromEnv(): MatrixAuthConfig { const username = process.env.MATRIX_USERNAME; @@ -1573,33 +3479,15 @@ function envBool(value: string | undefined, fallback = false): boolean { ); } -function decodeBase64( - value: string | undefined, - envName: string -): Uint8Array | undefined { +function parseEnvList(value: string | undefined): string[] { if (!value) { - return undefined; - } - - const bytes = Buffer.from(value, "base64"); - if (bytes.length === 0) { - throw new Error(`${envName} is set but could not be decoded as base64.`); - } - - return bytes; -} - -function parseEnvNumber(value: string | undefined): number | undefined { - if (!value) { - return undefined; - } - - const parsed = Number(value); - if (!Number.isFinite(parsed) || parsed <= 0) { - throw new Error(`Expected a positive number, got: ${value}`); + return []; } - return parsed; + return value + .split(",") + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0); } function generateDeviceID(length = 8): string { @@ -1614,7 +3502,7 @@ function generateDeviceID(length = 8): string { return out; } -function normalizeOptionalString(value: string | undefined): string | undefined { +function normalizeOptionalString(value: string | null | undefined): string | undefined { if (!value) { return undefined; } @@ -1622,25 +3510,74 @@ function normalizeOptionalString(value: string | undefined): string | undefined return trimmed.length > 0 ? trimmed : undefined; } +function readStringValue(value: unknown): string | undefined { + return typeof value === "string" ? normalizeOptionalString(value) : undefined; +} + +function matrixLocalpart(userID: string): string { + return userID.startsWith("@") ? userID.slice(1).split(":")[0] ?? userID : userID; +} + +function escapeMarkdownText(value: string): string { + return value.replace(/([\\`*_{}\[\]()#+\-.!|>])/gu, "\\$1"); +} + +function escapeMarkdownLinkText(value: string): string { + return value.replace(/([\\\]])/gu, "\\$1"); +} + +function escapeHTML(value: string): string { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function normalizeStringList(values: string[] | undefined): string[] { + if (!values || values.length === 0) { + return []; + } + + return values + .map((value) => normalizeOptionalString(value)) + .filter((value): value is string => Boolean(value)); +} + function hasIndexedDB(): boolean { return typeof globalThis.indexedDB !== "undefined" && globalThis.indexedDB !== null; } -function parseSDKLogLevel( - value: string | undefined -): MatrixAdapterConfig["matrixSDKLogLevel"] | undefined { +function isSDKLogLevel(value: string): value is SDKLogLevel { + return value in MATRIX_SDK_LOG_LEVELS; +} + +function parseSDKLogLevel(value: string | undefined): SDKLogLevel | undefined { if (!value) { return undefined; } const normalized = value.trim().toLowerCase(); - if ( - normalized === "trace" || - normalized === "debug" || - normalized === "info" || - normalized === "warn" || - normalized === "error" - ) { - return normalized; + return isSDKLogLevel(normalized) ? normalized : undefined; +} + +function evictOldestEntries( + collection: { size: number; keys(): Iterable; delete(key: string): unknown }, + maxSize = 10_000, + targetSize = 5_000 +): void { + if (collection.size <= maxSize) return; + const toDelete = collection.size - targetSize; + let deleted = 0; + // Map and Set iteration is insertion ordered, so keys() yields the oldest + // entries first for the collections used by this adapter. + for (const key of collection.keys()) { + if (deleted >= toDelete) break; + collection.delete(key); + deleted++; } - return undefined; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/src/store/chat-state-matrix-store.test.ts b/src/store/chat-state-matrix-store.test.ts new file mode 100644 index 0000000..6c713db --- /dev/null +++ b/src/store/chat-state-matrix-store.test.ts @@ -0,0 +1,248 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { Logger, StateAdapter } from "chat"; +import { createMemoryState } from "@chat-adapter/state-memory"; +import type { IStateEventWithRoomId } from "matrix-js-sdk/lib/@types/search"; +import type { IndexedToDeviceBatch } from "matrix-js-sdk/lib/models/ToDeviceMessage"; +import { ChatStateMatrixStore } from "./chat-state-matrix-store"; + +function makeLogger(): Logger { + return { + child: () => makeLogger(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; +} + +function makeSavedSync(nextBatch = "s1") { + return { + nextBatch, + accountData: [ + { + type: "m.direct", + content: { + "@alice:beeper.com": ["!room:beeper.com"], + }, + }, + ], + roomsData: { + join: { + "!room:beeper.com": { + account_data: { events: [] }, + ephemeral: { events: [] }, + state: { events: [] }, + summary: {}, + timeline: { + events: [], + limited: false, + prev_batch: "t0", + }, + unread_notifications: {}, + }, + }, + }, + }; +} + +async function makeState( + initial: Record = {} +): Promise { + const state = createMemoryState(); + await state.connect(); + for (const [key, value] of Object.entries(initial)) { + await state.set(key, value); + } + return state; +} + +async function makeStore( + state: StateAdapter, + options: Partial[0]> = {} +) { + const store = new ChatStateMatrixStore({ + state, + scopeKey: "matrix:store:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com:DEVICE1", + logger: makeLogger(), + persistIntervalMs: 30_000, + ...options, + }); + await store.startup(); + return store; +} + +describe("ChatStateMatrixStore", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-03-07T12:00:00.000Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("loads persisted saved sync during startup", async () => { + const scope = + "matrix:store:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com:DEVICE1"; + const state = await makeState({ + [`${scope}:meta`]: { + version: 1, + updatedAt: "2026-03-07T12:00:00.000Z", + nextToDeviceBatchID: 0, + filterIds: {}, + nextBatch: "s1", + }, + [`${scope}:saved-sync`]: makeSavedSync(), + }); + + const store = await makeStore(state, { scopeKey: scope }); + const saved = await store.getSavedSync(); + + expect(saved).toEqual(makeSavedSync()); + expect(await store.getSavedSyncToken()).toBe("s1"); + expect(await store.isNewlyCreated()).toBe(false); + }); + + it("reads saved sync token from meta without loading the full snapshot", async () => { + const scope = + "matrix:store:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com:DEVICE1"; + const state = await makeState({ + [`${scope}:meta`]: { + version: 1, + updatedAt: "2026-03-07T12:00:00.000Z", + nextToDeviceBatchID: 0, + filterIds: {}, + nextBatch: "s99", + }, + }); + const getSpy = vi.spyOn(state, "get"); + const store = new ChatStateMatrixStore({ + state, + scopeKey: scope, + logger: makeLogger(), + persistIntervalMs: 30_000, + }); + + expect(await store.getSavedSyncToken()).toBe("s99"); + expect(getSpy).toHaveBeenCalledWith(`${scope}:meta`); + expect(getSpy).not.toHaveBeenCalledWith(`${scope}:saved-sync`); + }); + + it("marks sync data dirty and only persists on a forced save before the interval", async () => { + const scope = + "matrix:store:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com:DEVICE1"; + const state = await makeState({ + [`${scope}:meta`]: { + version: 1, + updatedAt: "2026-03-07T12:00:00.000Z", + lastSavedAt: "2026-03-07T12:00:00.000Z", + nextToDeviceBatchID: 0, + filterIds: {}, + nextBatch: "s1", + }, + [`${scope}:saved-sync`]: makeSavedSync(), + }); + const setSpy = vi.spyOn(state, "set"); + const store = await makeStore(state, { scopeKey: scope }); + + await store.setSyncData({ + next_batch: "s2", + rooms: makeSavedSync("s2").roomsData, + account_data: { events: makeSavedSync("s2").accountData }, + } as never); + + expect(store.wantsSave()).toBe(false); + await store.save(); + expect(setSpy).not.toHaveBeenCalledWith( + expect.stringContaining(":saved-sync"), + expect.anything(), + expect.anything() + ); + + await store.save(true); + + expect(setSpy).toHaveBeenCalledWith( + expect.stringContaining(":saved-sync"), + expect.objectContaining({ nextBatch: "s2" }), + undefined + ); + expect(setSpy).toHaveBeenCalledWith( + expect.stringContaining(":meta"), + expect.objectContaining({ nextBatch: "s2" }), + undefined + ); + }); + + it("persists filter IDs, OOB members, pending events, and to-device batches across store instances", async () => { + const state = await makeState(); + const first = await makeStore(state); + const oobMembers: IStateEventWithRoomId[] = [ + { + room_id: "!room:beeper.com", + state_key: "@alice:beeper.com", + type: "m.room.member", + content: { membership: "join" }, + } as IStateEventWithRoomId, + ]; + const pendingEvents = [{ event_id: "$pending" }]; + + first.setFilterIdByName("sync", "filter-1"); + await first.setOutOfBandMembers("!room:beeper.com", oobMembers); + await first.setPendingEvents("!room:beeper.com", pendingEvents); + await first.saveToDeviceBatches([ + { + eventType: "m.room_key", + txnId: "txn-1", + batch: [ + { + userId: "@alice:beeper.com", + deviceId: "DEVICE2", + payload: { + type: "m.room_key", + content: { room_id: "!room:beeper.com" }, + }, + }, + ], + }, + ]); + + const second = await makeStore(state); + const toDevice = await second.getOldestToDeviceBatch(); + + expect(second.getFilterIdByName("sync")).toBe("filter-1"); + expect(await second.getOutOfBandMembers("!room:beeper.com")).toEqual(oobMembers); + expect(await second.getPendingEvents("!room:beeper.com")).toEqual(pendingEvents); + expect(toDevice).toMatchObject>({ + id: 0, + txnId: "txn-1", + eventType: "m.room_key", + }); + }); + + it("deletes all persisted keys", async () => { + const state = await makeState(); + const store = await makeStore(state); + + await store.setSyncData({ + next_batch: "s3", + rooms: makeSavedSync("s3").roomsData, + account_data: { events: makeSavedSync("s3").accountData }, + } as never); + await store.setPendingEvents("!room:beeper.com", [{ event_id: "$pending" }]); + await store.save(true); + + await store.deleteAllData(); + + expect(await state.get("matrix:store:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com:DEVICE1:meta")).toBeNull(); + expect( + await state.get( + "matrix:store:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com:DEVICE1:saved-sync" + ) + ).toBeNull(); + expect( + await state.get( + "matrix:store:https%3A%2F%2Fhs.beeper.com:%40bot%3Abeeper.com:DEVICE1:pending-events:%21room%3Abeeper.com" + ) + ).toBeNull(); + }); +}); diff --git a/src/store/chat-state-matrix-store.ts b/src/store/chat-state-matrix-store.ts new file mode 100644 index 0000000..ad9390d --- /dev/null +++ b/src/store/chat-state-matrix-store.ts @@ -0,0 +1,514 @@ +import type { Logger, StateAdapter } from "chat"; +import { MatrixEvent, type IStartClientOpts } from "matrix-js-sdk"; +import type { IEvent } from "matrix-js-sdk/lib/models/event"; +import type { + IndexedToDeviceBatch, + ToDeviceBatchWithTxnId, +} from "matrix-js-sdk/lib/models/ToDeviceMessage"; +import { SyncAccumulator, type ISyncResponse } from "matrix-js-sdk/lib/sync-accumulator"; +import type { IStateEventWithRoomId } from "matrix-js-sdk/lib/@types/search"; +import { MemoryStore } from "matrix-js-sdk/lib/store/memory"; +import type { ISavedSync } from "matrix-js-sdk/lib/store"; + +const STORE_VERSION = 1; +const DEFAULT_PERSIST_INTERVAL_MS = 30_000; + +type PersistedMeta = { + filterIds: Record; + lastSavedAt?: string; + nextBatch?: string; + nextToDeviceBatchID: number; + updatedAt: string; + version: number; +}; + +type PersistedToDeviceState = { + batches: IndexedToDeviceBatch[]; +}; + +type PersistedIndex = { + roomIDs: string[]; +}; + +function normalizeStringRecord( + value: Record | undefined +): Record { + if (!value) { + return {}; + } + + return Object.fromEntries( + Object.entries(value).filter((entry): entry is [string, string] => { + return typeof entry[0] === "string" && typeof entry[1] === "string"; + }) + ); +} + +function normalizeRoomIndex(value: unknown): string[] { + if (!isRecord(value) || !Array.isArray(value.roomIDs)) { + return []; + } + + return value.roomIDs.filter((roomID): roomID is string => typeof roomID === "string"); +} + +function isPersistedMeta(value: unknown): value is PersistedMeta { + return ( + isRecord(value) && + value.version === STORE_VERSION && + typeof value.updatedAt === "string" && + typeof value.nextToDeviceBatchID === "number" + ); +} + +function isSavedSync(value: unknown): value is ISavedSync { + return ( + isRecord(value) && + typeof value.nextBatch === "string" && + Array.isArray(value.accountData) && + isRecord(value.roomsData) + ); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export class ChatStateMatrixStore extends MemoryStore { + private readonly state: StateAdapter; + private readonly scopeKey: string; + private readonly logger: Logger; + private readonly persistIntervalMs: number; + private readonly snapshotTtlMs?: number; + private readonly syncAccumulator = new SyncAccumulator(); + private latestSavedSync: ISavedSync | null = null; + private filterIDs = new Map(); + private pendingEventsByRoom = new Map[]>(); + private oobMembersByRoom = new Map(); + private persistedToDeviceBatches: IndexedToDeviceBatch[] = []; + private nextToDeviceBatchID = 0; + private dirty = false; + private started = false; + private newlyCreated = true; + private lastSavedAt = 0; + + constructor(options: { + state: StateAdapter; + scopeKey: string; + logger: Logger; + persistIntervalMs?: number; + snapshotTtlMs?: number; + }) { + super(); + this.state = options.state; + this.scopeKey = options.scopeKey; + this.logger = options.logger; + this.persistIntervalMs = + options.persistIntervalMs ?? DEFAULT_PERSIST_INTERVAL_MS; + this.snapshotTtlMs = options.snapshotTtlMs; + } + + override async startup(): Promise { + if (this.started) { + return; + } + + const [meta, savedSync, clientOptions, toDevice, oobRoomIDs, pendingRoomIDs] = + await Promise.all([ + this.state.get(this.metaKey), + this.state.get(this.savedSyncKey), + this.state.get(this.clientOptionsKey), + this.state.get(this.toDeviceKey), + this.state.get(this.oobMembersIndexKey), + this.state.get(this.pendingEventsIndexKey), + ]); + + if (isPersistedMeta(meta)) { + this.filterIDs = new Map(Object.entries(normalizeStringRecord(meta.filterIds))); + this.nextToDeviceBatchID = meta.nextToDeviceBatchID; + this.lastSavedAt = meta.lastSavedAt ? Date.parse(meta.lastSavedAt) || 0 : 0; + this.newlyCreated = false; + } + + if (isSavedSync(savedSync)) { + this.latestSavedSync = savedSync; + this.syncAccumulator.accumulate({ + next_batch: savedSync.nextBatch, + rooms: savedSync.roomsData, + account_data: { events: savedSync.accountData }, + }); + super.setSyncToken(savedSync.nextBatch); + super.storeAccountDataEvents( + savedSync.accountData.map((event) => new MatrixEvent(event)) + ); + this.newlyCreated = false; + } + + if (clientOptions) { + await super.storeClientOptions(clientOptions); + this.newlyCreated = false; + } + + if (Array.isArray(toDevice)) { + this.persistedToDeviceBatches = toDevice; + this.newlyCreated = false; + } else if (isRecord(toDevice) && Array.isArray(toDevice.batches)) { + this.persistedToDeviceBatches = toDevice.batches; + this.newlyCreated = false; + } + + await this.loadIndexedRoomState(oobRoomIDs, pendingRoomIDs); + this.started = true; + } + + override async isNewlyCreated(): Promise { + return this.newlyCreated; + } + + override wantsSave(): boolean { + if (!this.dirty) { + return false; + } + + return Date.now() - this.lastSavedAt >= this.persistIntervalMs; + } + + override async save(force = false): Promise { + if (!this.dirty || (!force && !this.wantsSave())) { + return; + } + + if (!this.latestSavedSync) { + this.dirty = false; + return; + } + + const nowISO = new Date().toISOString(); + const meta: PersistedMeta = { + version: STORE_VERSION, + updatedAt: nowISO, + lastSavedAt: nowISO, + nextBatch: this.latestSavedSync.nextBatch, + filterIds: Object.fromEntries(this.filterIDs), + nextToDeviceBatchID: this.nextToDeviceBatchID, + }; + + await Promise.all([ + this.state.set(this.savedSyncKey, this.latestSavedSync, this.snapshotTtlMs), + this.state.set(this.metaKey, meta, this.snapshotTtlMs), + ]); + + this.lastSavedAt = Date.now(); + this.dirty = false; + } + + override async getSavedSync(): Promise { + if (this.latestSavedSync) { + return this.latestSavedSync; + } + + const stored = await this.state.get(this.savedSyncKey); + if (isSavedSync(stored)) { + this.latestSavedSync = stored; + return stored; + } + + return null; + } + + override async getSavedSyncToken(): Promise { + if (this.latestSavedSync?.nextBatch) { + return this.latestSavedSync.nextBatch; + } + + const meta = await this.state.get(this.metaKey); + if (isPersistedMeta(meta) && typeof meta.nextBatch === "string") { + return meta.nextBatch; + } + + return null; + } + + override async setSyncData(syncData: ISyncResponse): Promise { + this.syncAccumulator.accumulate(syncData); + const accumulated = this.syncAccumulator.getJSON(true); + if (!accumulated.nextBatch) { + return; + } + + this.latestSavedSync = accumulated; + super.setSyncToken(accumulated.nextBatch); + super.storeAccountDataEvents( + accumulated.accountData.map((event) => new MatrixEvent(event)) + ); + this.dirty = true; + } + + override async getClientOptions(): Promise { + const existing = await super.getClientOptions(); + if (existing) { + return existing; + } + + const stored = await this.state.get(this.clientOptionsKey); + if (!stored) { + return undefined; + } + + await super.storeClientOptions(stored); + return stored; + } + + override async storeClientOptions(options: IStartClientOpts): Promise { + await super.storeClientOptions(options); + await this.state.set(this.clientOptionsKey, options, this.snapshotTtlMs); + } + + override async getOutOfBandMembers( + roomId: string + ): Promise { + const cached = this.oobMembersByRoom.get(roomId); + if (cached) { + return cached; + } + + const stored = await this.state.get( + this.oobMembersKey(roomId) + ); + if (!stored) { + return null; + } + + this.oobMembersByRoom.set(roomId, stored); + return stored; + } + + override async setOutOfBandMembers( + roomId: string, + membershipEvents: IStateEventWithRoomId[] + ): Promise { + this.oobMembersByRoom.set(roomId, membershipEvents); + await Promise.all([ + this.state.set(this.oobMembersKey(roomId), membershipEvents, this.snapshotTtlMs), + this.persistRoomIndex(this.oobMembersIndexKey, this.oobMembersByRoom), + ]); + } + + override async clearOutOfBandMembers(roomId: string): Promise { + this.oobMembersByRoom.delete(roomId); + await Promise.all([ + this.state.delete(this.oobMembersKey(roomId)), + this.persistRoomIndex(this.oobMembersIndexKey, this.oobMembersByRoom), + ]); + } + + override async getPendingEvents(roomId: string): Promise[]> { + const cached = this.pendingEventsByRoom.get(roomId); + if (cached) { + return cached; + } + + const stored = await this.state.get[] | null>( + this.pendingEventsKey(roomId) + ); + if (!stored) { + return []; + } + + this.pendingEventsByRoom.set(roomId, stored); + return stored; + } + + override async setPendingEvents( + roomId: string, + events: Partial[] + ): Promise { + if (events.length === 0) { + this.pendingEventsByRoom.delete(roomId); + await Promise.all([ + this.state.delete(this.pendingEventsKey(roomId)), + this.persistRoomIndex(this.pendingEventsIndexKey, this.pendingEventsByRoom), + ]); + return; + } + + this.pendingEventsByRoom.set(roomId, events); + await Promise.all([ + this.state.set(this.pendingEventsKey(roomId), events, this.snapshotTtlMs), + this.persistRoomIndex(this.pendingEventsIndexKey, this.pendingEventsByRoom), + ]); + } + + override async saveToDeviceBatches( + batches: ToDeviceBatchWithTxnId[] + ): Promise { + for (const batch of batches) { + this.persistedToDeviceBatches.push({ + id: this.nextToDeviceBatchID++, + eventType: batch.eventType, + txnId: batch.txnId, + batch: batch.batch, + }); + } + + await Promise.all([ + this.state.set( + this.toDeviceKey, + { batches: this.persistedToDeviceBatches }, + this.snapshotTtlMs + ), + this.persistMeta(), + ]); + } + + override async getOldestToDeviceBatch(): Promise { + return this.persistedToDeviceBatches[0] ?? null; + } + + override async removeToDeviceBatch(id: number): Promise { + this.persistedToDeviceBatches = this.persistedToDeviceBatches.filter( + (batch) => batch.id !== id + ); + + await this.state.set( + this.toDeviceKey, + { batches: this.persistedToDeviceBatches }, + this.snapshotTtlMs + ); + } + + override getFilterIdByName(filterName: string): string | null { + return this.filterIDs.get(filterName) ?? null; + } + + override setFilterIdByName(filterName: string, filterId?: string): void { + if (filterId) { + this.filterIDs.set(filterName, filterId); + } else { + this.filterIDs.delete(filterName); + } + + void this.persistMeta().catch((error) => { + this.logger.warn("Failed to persist Matrix filter IDs", { error }); + }); + } + + override async deleteAllData(): Promise { + await super.deleteAllData(); + + const oobRoomIDs = [...this.oobMembersByRoom.keys()]; + const pendingRoomIDs = [...this.pendingEventsByRoom.keys()]; + + this.latestSavedSync = null; + this.filterIDs.clear(); + this.oobMembersByRoom.clear(); + this.pendingEventsByRoom.clear(); + this.persistedToDeviceBatches = []; + this.nextToDeviceBatchID = 0; + this.dirty = false; + this.newlyCreated = true; + this.lastSavedAt = 0; + + await Promise.all([ + this.state.delete(this.metaKey), + this.state.delete(this.savedSyncKey), + this.state.delete(this.clientOptionsKey), + this.state.delete(this.toDeviceKey), + this.state.delete(this.oobMembersIndexKey), + this.state.delete(this.pendingEventsIndexKey), + ...oobRoomIDs.map((roomID) => this.state.delete(this.oobMembersKey(roomID))), + ...pendingRoomIDs.map((roomID) => this.state.delete(this.pendingEventsKey(roomID))), + ]); + } + + override async destroy(): Promise { + await this.save(true); + } + + private async loadIndexedRoomState( + oobRoomIDsRaw: unknown, + pendingRoomIDsRaw: unknown + ): Promise { + const oobRoomIDs = normalizeRoomIndex(oobRoomIDsRaw); + const pendingRoomIDs = normalizeRoomIndex(pendingRoomIDsRaw); + + await Promise.all([ + ...oobRoomIDs.map(async (roomID) => { + const members = await this.state.get( + this.oobMembersKey(roomID) + ); + if (members) { + this.oobMembersByRoom.set(roomID, members); + } + }), + ...pendingRoomIDs.map(async (roomID) => { + const pending = await this.state.get[] | null>( + this.pendingEventsKey(roomID) + ); + if (pending) { + this.pendingEventsByRoom.set(roomID, pending); + } + }), + ]); + + if (oobRoomIDs.length > 0 || pendingRoomIDs.length > 0) { + this.newlyCreated = false; + } + } + + private async persistMeta(): Promise { + const meta: PersistedMeta = { + version: STORE_VERSION, + updatedAt: new Date().toISOString(), + lastSavedAt: + this.lastSavedAt > 0 ? new Date(this.lastSavedAt).toISOString() : undefined, + nextBatch: this.latestSavedSync?.nextBatch ?? super.getSyncToken() ?? undefined, + filterIds: Object.fromEntries(this.filterIDs), + nextToDeviceBatchID: this.nextToDeviceBatchID, + }; + + await this.state.set(this.metaKey, meta, this.snapshotTtlMs); + } + + private async persistRoomIndex( + key: string, + collection: Map + ): Promise { + const value: PersistedIndex = { + roomIDs: [...collection.keys()], + }; + await this.state.set(key, value, this.snapshotTtlMs); + } + + private get metaKey(): string { + return `${this.scopeKey}:meta`; + } + + private get savedSyncKey(): string { + return `${this.scopeKey}:saved-sync`; + } + + private get clientOptionsKey(): string { + return `${this.scopeKey}:client-options`; + } + + private get toDeviceKey(): string { + return `${this.scopeKey}:to-device`; + } + + private get oobMembersIndexKey(): string { + return `${this.scopeKey}:room-index:oob-members`; + } + + private get pendingEventsIndexKey(): string { + return `${this.scopeKey}:room-index:pending-events`; + } + + private oobMembersKey(roomID: string): string { + return `${this.scopeKey}:oob-members:${encodeURIComponent(roomID)}`; + } + + private pendingEventsKey(roomID: string): string { + return `${this.scopeKey}:pending-events:${encodeURIComponent(roomID)}`; + } +} diff --git a/src/types.ts b/src/types.ts index 6047626..5e818a0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,41 @@ -import type { IStartClientOpts, MatrixClient } from "matrix-js-sdk"; -import type { Logger } from "chat"; +import type { ICreateClientOpts, IStartClientOpts, MatrixClient } from "matrix-js-sdk"; +import type { Logger, StateAdapter } from "chat"; +import type { IStore } from "matrix-js-sdk/lib/store"; export interface MatrixE2EEConfig { cryptoDatabasePrefix?: string; - enabled?: boolean; storageKey?: Uint8Array; storagePassword?: string; useIndexedDB?: boolean; } +export interface MatrixPersistenceSyncConfig { + persistIntervalMs?: number; + snapshotTtlMs?: number; +} + +export interface MatrixPersistenceSessionConfig { + decrypt?: (value: string) => string; + encrypt?: (value: string) => string; + ttlMs?: number; +} + +export interface MatrixPersistenceConfig { + keyPrefix?: string; + session?: MatrixPersistenceSessionConfig; + sync?: MatrixPersistenceSyncConfig; +} + +export interface MatrixCreateStoreOptions { + baseURL: string; + config: MatrixPersistenceSyncConfig; + deviceID?: string; + logger: Logger; + scopeKey: string; + state: StateAdapter; + userID: string; +} + export interface MatrixAccessTokenAuthConfig { accessToken: string; type: "accessToken"; @@ -31,40 +58,32 @@ export interface MatrixAdapterConfig { auth: MatrixAuthConfig; baseURL: string; commandPrefix?: string; + createStore?: (options: MatrixCreateStoreOptions) => IStore; createBootstrapClient?: ( options: { accessToken?: string; baseURL: string; deviceID?: string } ) => MatrixAuthBootstrapClient; - createClient?: () => MatrixClient; - deviceIDPersistence?: MatrixDeviceIDPersistenceConfig; + createClient?: (options?: ICreateClientOpts) => MatrixClient; deviceID?: string; e2ee?: MatrixE2EEConfig; + inviteAutoJoin?: MatrixInviteAutoJoinConfig; logger?: Logger; matrixSDKLogLevel?: "trace" | "debug" | "info" | "warn" | "error"; + persistence?: MatrixPersistenceConfig; recoveryKey?: string; roomAllowlist?: string[]; - session?: MatrixSessionConfig; sync?: IStartClientOpts; userName?: string; } +export interface MatrixInviteAutoJoinConfig { + inviterAllowlist?: string[]; +} + export interface MatrixThreadID { roomID: string; rootEventID?: string; } -export interface MatrixSessionConfig { - enabled?: boolean; - encrypt?: (value: string) => string; - key?: string; - ttlMs?: number; - decrypt?: (value: string) => string; -} - -export interface MatrixDeviceIDPersistenceConfig { - enabled?: boolean; - key?: string; -} - export interface MatrixAuthBootstrapClient { loginRequest?: (data: { type: "m.login.password"; diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts new file mode 100644 index 0000000..c0de215 --- /dev/null +++ b/vitest.config.e2e.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["e2e/**/*.test.ts"], + hookTimeout: 120_000, + testTimeout: 120_000, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts index 5b01228..d5a313d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,6 +4,7 @@ export default defineConfig({ test: { globals: true, environment: "node", + include: ["src/**/*.test.ts"], coverage: { provider: "v8", reporter: ["text", "json-summary"],