feat: slack-notifications

This commit is contained in:
Salihu
2026-03-06 22:36:19 +01:00
parent 9791ff1170
commit 58b150ee04
18 changed files with 941 additions and 55 deletions
+1
View File
@@ -152,6 +152,7 @@ FORCE_HTTPS=true
# DOCS: https://docs.getoutline.com/s/hosting/doc/slack-sgMujR8J9J
SLACK_CLIENT_ID=get_a_key_from_slack
SLACK_CLIENT_SECRET=get_the_secret_of_above_key
SLACK_SIGNING_SECRET=get_the_signing_secret_from_slack
# Google sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/google-hOuvtCmTqQ
+1
View File
@@ -13,6 +13,7 @@ GOOGLE_CLIENT_SECRET=123
SLACK_CLIENT_ID=123
SLACK_CLIENT_SECRET=123
SLACK_VERIFICATION_TOKEN=test-token-123
SLACK_SIGNING_SECRET=test-signing-secret-123
GITHUB_CLIENT_ID=123;
GITHUB_CLIENT_SECRET=123;
+52 -11
View File
@@ -3,6 +3,7 @@ import { computed, action, observable } from "mobx";
import { now } from "mobx-utils";
import { UserPreferenceDefaults } from "@shared/constants";
import {
NotificationChannelType,
NotificationEventDefaults,
type NotificationEventType,
TeamPreference,
@@ -206,36 +207,76 @@ class User extends ParanoidModel implements Searchable {
* Returns the current preference for the given notification event type taking
* into account the default system value.
*
* @param type The type of notification event
* @returns The current preference
* @param type The type of notification event.
* @param channel Optional channel type for channel-specific check.
* @returns The current preference.
*/
public subscribedToEventType = (type: NotificationEventType) =>
this.notificationSettings[type] ?? NotificationEventDefaults[type] ?? false;
public subscribedToEventType = (
type: NotificationEventType,
channel = NotificationChannelType.Email
): boolean => {
const setting = this.notificationSettings[type];
const defaultValue = NotificationEventDefaults[type] ?? false;
if (setting === undefined) {
return defaultValue;
}
if (typeof setting === "boolean") {
return setting;
}
if (typeof setting === "object") {
return setting[channel] ?? defaultValue;
}
return defaultValue;
};
/**
* Sets a preference for the users notification settings on the model and
* saves the change to the server.
*
* @param type The type of notification event
* @param value Set the preference to true/false
* @param type The type of notification event.
* @param value Set the preference to true/false.
* @param channel Optional channel type for channel-specific settings.
*/
@action
setNotificationEventType = async (
eventType: NotificationEventType,
value: boolean
value: boolean | Record<NotificationChannelType, boolean>,
channel?: NotificationChannelType
) => {
this.notificationSettings = {
...this.notificationSettings,
[eventType]: value,
};
if (channel !== undefined) {
// Setting a specific channel preference
const currentSetting = this.notificationSettings[eventType];
const channelSettings =
typeof currentSetting === "object" ? currentSetting : {};
this.notificationSettings = {
...this.notificationSettings,
[eventType]: {
...channelSettings,
[channel]: value,
},
};
} else {
// Setting all channels or simple boolean
this.notificationSettings = {
...this.notificationSettings,
[eventType]: value,
};
}
if (value) {
await client.post(`/users.notificationsSubscribe`, {
eventType,
channel,
});
} else {
await client.post(`/users.notificationsUnsubscribe`, {
eventType,
channel,
});
}
};
+56 -15
View File
@@ -18,16 +18,23 @@ import {
} from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import { NotificationEventType } from "@shared/types";
import {
NotificationEventType,
NotificationChannelType,
IntegrationService,
IntegrationType,
} from "@shared/types";
import Heading from "~/components/Heading";
import Notice from "~/components/Notice";
import Scene from "~/components/Scene";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useCurrentUser from "~/hooks/useCurrentUser";
import { client } from "~/utils/ApiClient";
import isCloudHosted from "~/utils/isCloudHosted";
import { settingsPath } from "~/utils/routeHelpers";
import ChannelSelector from "./components/ChannelSelector";
import SettingRow from "./components/SettingRow";
function Notifications() {
@@ -161,11 +168,29 @@ function Notifications() {
toast.success(t("Notifications saved"));
}, 500);
const handleChange = React.useCallback(
(eventType: NotificationEventType) => async (checked: boolean) => {
await user.setNotificationEventType(eventType, checked);
showSuccessMessage();
},
const handleChannelsChange = React.useCallback(
(eventType: NotificationEventType) =>
async (channels: NotificationChannelType[]) => {
for (const channel of [
NotificationChannelType.Email,
NotificationChannelType.Slack,
]) {
const shouldEnable = channels.includes(channel);
const currentlyEnabled = user.subscribedToEventType(
eventType,
channel
);
if (shouldEnable !== currentlyEnabled) {
await user.setNotificationEventType(
eventType,
shouldEnable,
channel
);
}
}
showSuccessMessage();
},
[user, showSuccessMessage]
);
@@ -206,7 +231,10 @@ function Notifications() {
</Notice>
)}
<Text as="p" type="secondary">
<Trans>Manage when and where you receive email notifications.</Trans>
<Trans>
Manage when and where you receive notifications. Choose to receive
notifications via email, Slack, or both.
</Trans>
</Text>
<SettingRow
@@ -223,7 +251,22 @@ function Notifications() {
</SettingRow>
{options.map((option) => {
const setting = user.subscribedToEventType(option.event);
const emailSetting = user.subscribedToEventType(
option.event,
NotificationChannelType.Email
);
const slackSetting = user.subscribedToEventType(
option.event,
NotificationChannelType.Slack
);
const enabledChannels: NotificationChannelType[] = [];
if (emailSetting) {
enabledChannels.push(NotificationChannelType.Email);
}
if (slackSetting) {
enabledChannels.push(NotificationChannelType.Slack);
}
return (
<SettingRow
@@ -238,12 +281,10 @@ function Notifications() {
}
compact
>
<Switch
key={option.event}
id={option.event}
name={option.event}
checked={!!setting}
onChange={handleChange(option.event)}
<ChannelSelector
value={enabledChannels}
onChange={handleChannelsChange(option.event)}
slackDisabled={!hasSlackLinked}
/>
</SettingRow>
);
@@ -0,0 +1,58 @@
import * as React from "react";
import { EmailIcon } from "outline-icons";
import { useTranslation } from "react-i18next";
import { NotificationChannelType } from "@shared/types";
import { faSlack } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import FilterOptions from "~/components/FilterOptions";
type Props = {
value: NotificationChannelType[];
onChange: (channels: NotificationChannelType[]) => void;
slackDisabled?: boolean;
};
/**
* A dropdown selector for managing notification channel preferences.
* Displays enabled channels and allows toggling via a popover menu.
*/
function ChannelSelector({ value, onChange, slackDisabled = false }: Props) {
const { t } = useTranslation();
const channels = React.useMemo(
() => [
{
key: NotificationChannelType.Email,
label: t("Email"),
icon: <EmailIcon size={16} />,
},
{
key: NotificationChannelType.Slack,
label: t("Slack"),
icon: <FontAwesomeIcon icon={faSlack} size="xs" />,
},
],
[t, slackDisabled]
);
const handleToggle = React.useCallback(
(channelType: NotificationChannelType) => {
const newValue = value.includes(channelType)
? value.filter((c) => c !== channelType)
: [...value, channelType];
onChange(newValue);
},
[value, onChange]
);
return (
<FilterOptions
defaultLabel={t("Select Channels")}
options={channels}
selectedKeys={value}
onSelect={handleToggle}
/>
);
}
export default ChannelSelector;
+21
View File
@@ -0,0 +1,21 @@
diff --git a/node_modules/@chat-adapter/shared/package.json b/node_modules/@chat-adapter/shared/package.json
index 3015551..d624867 100644
--- a/node_modules/@chat-adapter/shared/package.json
+++ b/node_modules/@chat-adapter/shared/package.json
@@ -6,7 +6,8 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
- "import": "./dist/index.js"
+ "import": "./dist/index.js",
+ "default": "./dist/index.js"
}
},
"main": "./dist/index.js",
@@ -46,4 +47,4 @@
"build": "tsup",
"test": "vitest run --coverage"
}
-}
\ No newline at end of file
+}
+21
View File
@@ -0,0 +1,21 @@
diff --git a/node_modules/@chat-adapter/slack/package.json b/node_modules/@chat-adapter/slack/package.json
index 871fe33..a454831 100644
--- a/node_modules/@chat-adapter/slack/package.json
+++ b/node_modules/@chat-adapter/slack/package.json
@@ -9,7 +9,8 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
- "import": "./dist/index.js"
+ "import": "./dist/index.js",
+ "default": "./dist/index.js"
}
},
"files": [
@@ -53,4 +54,4 @@
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
}
-}
\ No newline at end of file
+}
+33
View File
@@ -0,0 +1,33 @@
diff --git a/node_modules/chat/package.json b/node_modules/chat/package.json
index 89e4e44..24b552d 100644
--- a/node_modules/chat/package.json
+++ b/node_modules/chat/package.json
@@ -9,15 +9,18 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
- "import": "./dist/index.js"
+ "import": "./dist/index.js",
+ "default": "./dist/index.js"
},
"./jsx-runtime": {
"types": "./dist/jsx-runtime.d.ts",
- "import": "./dist/jsx-runtime.js"
+ "import": "./dist/jsx-runtime.js",
+ "default": "./dist/jsx-runtime.js"
},
"./jsx-dev-runtime": {
"types": "./dist/jsx-runtime.d.ts",
- "import": "./dist/jsx-runtime.js"
+ "import": "./dist/jsx-runtime.js",
+ "default": "./dist/jsx-runtime.js"
}
},
"files": [
@@ -68,4 +71,4 @@
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist docs"
}
-}
\ No newline at end of file
+}
+31 -10
View File
@@ -254,17 +254,38 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
case IntegrationType.LinkedAccount: {
// validation middleware ensures that code is non-null at this point
const data = await Slack.oauthAccess(code!, SlackUtils.connectUrl());
await Integration.create<Integration<IntegrationType.LinkedAccount>>({
service: IntegrationService.Slack,
type: IntegrationType.LinkedAccount,
userId: user.id,
teamId: user.teamId,
settings: {
slack: {
serviceUserId: data.user_id,
serviceTeamId: data.team_id,
await sequelize.transaction(async (transaction) => {
const authentication = await IntegrationAuthentication.create(
{
service: IntegrationService.Slack,
userId: user.id,
teamId: user.teamId,
token: data.access_token,
scopes: data.scope.split(","),
},
},
{ transaction }
);
await Integration.create<
Integration<IntegrationType.LinkedAccount>
>(
{
service: IntegrationService.Slack,
type: IntegrationType.LinkedAccount,
userId: user.id,
teamId: user.teamId,
// need to add events
authenticationId: authentication.id,
settings: {
slack: {
serviceUserId: data.user_id,
serviceTeamId: data.team_id,
},
},
},
{ transaction }
);
});
break;
}
+6
View File
@@ -37,6 +37,12 @@ class SlackPluginEnvironment extends Environment {
environment.SLACK_CLIENT_SECRET ?? environment.SLACK_SECRET
);
@IsOptional()
@CannotUseWithout("SLACK_CLIENT_ID")
public SLACK_SIGNING_SECRET = this.toOptionalString(
environment.SLACK_SIGNING_SECRET
);
/**
* Secret to verify webhook requests received from Slack.
*/
+41
View File
@@ -3,9 +3,50 @@ import { InvalidRequestError } from "@server/errors";
import fetch from "@server/utils/fetch";
import { SlackUtils } from "../shared/SlackUtils";
import env from "./env";
import type { AdapterPostableMessage } from "chat";
const SLACK_API_URL = "https://slack.com/api";
/**
* send a message to a Slack channel
*
* @param token - the Slack API token to authenticate the request.
* @param channel - the channel ID or user ID to send the message to.
* @param message - the message content to send.
*
* @returns the response after posting the message.
*/
export async function postMessage({
token,
channel,
message,
}: {
token: string;
channel: string;
message: AdapterPostableMessage;
}) {
if (!token) {
throw InvalidRequestError("Slack API token is required");
}
try {
const adapter = createSlackAdapter({
botToken: token,
signingSecret: env.SLACK_SIGNING_SECRET,
});
const threadId = await adapter.openDM(channel);
const result = await adapter.postMessage(threadId, message);
return result;
} catch (err) {
if (err.data) {
throw InvalidRequestError(err.data.error || err.message);
}
throw InvalidRequestError(err.message);
}
}
/**
* Makes a POST request to the Slack API with JSON body.
*
@@ -0,0 +1,26 @@
"use strict";
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
return queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.addColumn(
"notifications",
"slackSentAt",
{
type: Sequelize.DATE,
allowNull: true,
},
{ transaction }
);
});
},
async down(queryInterface) {
return queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.removeColumn("notifications", "slackSentAt", {
transaction,
});
});
},
};
+4
View File
@@ -117,6 +117,10 @@ class Notification extends Model<
@Column
emailedAt?: Date | null;
@AllowNull
@Column
slackSentAt?: Date | null;
@AllowNull
@Column
viewedAt: Date | null;
+84 -11
View File
@@ -40,6 +40,7 @@ import type {
} from "@shared/types";
import {
CollectionPermission,
NotificationChannelType,
NotificationEventDefaults,
UserRole,
DocumentPermission,
@@ -348,28 +349,100 @@ class User extends ParanoidModel<
/**
* Sets a preference for the users notification settings.
*
* @param type The type of notification event
* @param value Set the preference to true/false
* @param type The type of notification event.
* @param value Set the preference to true/false or channel-specific settings.
* @param channel Optional channel type for channel-specific settings.
*/
public setNotificationEventType = (
type: NotificationEventType,
value = true
value: boolean | Record<NotificationChannelType, boolean>,
channel?: NotificationChannelType
) => {
this.notificationSettings = {
...this.notificationSettings,
[type]: value,
};
if (channel !== undefined) {
// Setting a specific channel preference
const currentSetting = this.notificationSettings[type];
const channelSettings =
typeof currentSetting === "object" ? currentSetting : {};
this.notificationSettings = {
...this.notificationSettings,
[type]: {
...channelSettings,
[channel]: value,
},
};
} else {
// Setting all channels or simple boolean
this.notificationSettings = {
...this.notificationSettings,
[type]: value,
};
}
};
/**
* Returns the current preference for the given notification event type taking
* into account the default system value.
*
* @param type The type of notification event
* @returns The current preference
* @param type The type of notification event.
* @param channel Optional channel type for channel-specific check.
* @returns The current preference.
*/
public subscribedToEventType = (type: NotificationEventType) =>
this.notificationSettings[type] ?? NotificationEventDefaults[type] ?? false;
public subscribedToEventType = (
type: NotificationEventType,
channel = NotificationChannelType.Email
): boolean => {
const setting = this.notificationSettings[type];
const defaultValue = NotificationEventDefaults[type] ?? false;
if (setting === undefined) {
return defaultValue;
}
if (typeof setting === "boolean") {
return setting;
}
if (typeof setting === "object") {
return setting[channel] ?? defaultValue;
}
return defaultValue;
};
/**
* Returns the user's Slack user ID if they have linked their Slack account.
*
* @returns The Slack user ID or null.
*/
public getSlackUserId = async (): Promise<string | null> => {
const { Integration } = await import("./index");
const { IntegrationType } = await import("@shared/types");
const integration = await Integration.findOne({
where: {
userId: this.id,
service: "slack",
type: IntegrationType.LinkedAccount,
},
});
if (!integration || typeof integration.settings !== "object") {
return null;
}
const { settings } = integration;
if (
settings &&
"slack" in settings &&
settings.slack &&
typeof settings.slack === "object" &&
"serviceUserId" in settings.slack
) {
return settings.slack.serviceUserId as string;
}
return null;
};
/**
* User flags are for storing information on a user record that is not visible
+183
View File
@@ -0,0 +1,183 @@
import {
IntegrationService,
NotificationChannelType,
NotificationEventType,
} from "@shared/types";
import { Notification, IntegrationAuthentication } from "@server/models";
import type { Event, NotificationEvent } from "@server/types";
import * as Slack from "../../../plugins/slack/server/slack";
import BaseProcessor from "./BaseProcessor";
import Logger from "@server/logging/Logger";
import { paragraph, root, strong, text, link } from "chat";
/**
* Processor for sending Slack DM notifications.
* Listens for notification.create events and sends Slack DMs to users
* who have linked their Slack accounts and enabled Slack notifications.
*/
export default class SlackNotificationsProcessor extends BaseProcessor {
static applicableEvents: Event["name"][] = ["notifications.create"];
async perform(event: NotificationEvent) {
const notification = await Notification.scope([
"withTeam",
"withUser",
"withActor",
]).findByPk(event.modelId);
if (!notification) {
return;
}
if (notification.user.isSuspended) {
return;
}
// Check if user has Slack notifications enabled for this event type
if (
!notification.user.subscribedToEventType(
notification.event,
NotificationChannelType.Slack
)
) {
return;
}
const slackUserId = await notification.user.getSlackUserId();
if (!slackUserId) {
Logger.info(
"processor",
`User ${notification.userId} has no linked Slack account`
);
return;
}
const auth = await IntegrationAuthentication.findOne({
where: {
service: IntegrationService.Slack,
teamId: notification.user.teamId,
},
});
if (!auth) {
Logger.debug(
"plugins",
"No Slack integration authentication found for team",
{
teamId: notification.user.teamId,
}
);
return;
}
try {
const message = this.formatSlackMessage(notification);
await Slack.postMessage({
token: auth?.token,
channel: slackUserId,
message,
});
await notification.update({
slackSentAt: new Date(),
});
Logger.info(
"processor",
`Slack DM sent for notification ${notification.id}`
);
} catch (error) {
Logger.error(
`Failed to send Slack DM for notification ${notification.id}`,
error
);
}
}
/**
* Format a notification into a Slack message with rich formatting.
*
* @param notification - the notification to format.
* @returns the formatted Slack message.
*/
private formatSlackMessage(notification: Notification) {
const actorName = notification.actor.name;
const teamUrl = notification.team.url;
let textContent = "";
let url = "";
switch (notification.event) {
case NotificationEventType.PublishDocument:
textContent = `${actorName} published a new document`;
url = `${teamUrl}/doc/${notification.documentId}`;
break;
case NotificationEventType.UpdateDocument:
textContent = `${actorName} updated a document you're subscribed to`;
url = `${teamUrl}/doc/${notification.documentId}`;
break;
case NotificationEventType.MentionedInDocument:
textContent = `${actorName} mentioned you in a document`;
url = `${teamUrl}/doc/${notification.documentId}`;
break;
case NotificationEventType.MentionedInComment:
textContent = `${actorName} mentioned you in a comment`;
url = `${teamUrl}/doc/${notification.documentId}?commentId=${notification.commentId}`;
break;
case NotificationEventType.GroupMentionedInDocument:
textContent = `${actorName} mentioned a group you're in`;
url = `${teamUrl}/doc/${notification.documentId}`;
break;
case NotificationEventType.GroupMentionedInComment:
textContent = `${actorName} mentioned a group you're in`;
url = `${teamUrl}/doc/${notification.documentId}?commentId=${notification.commentId}`;
break;
case NotificationEventType.CreateComment:
textContent = `${actorName} commented on a document you're subscribed to`;
url = `${teamUrl}/doc/${notification.documentId}?commentId=${notification.commentId}`;
break;
case NotificationEventType.ResolveComment:
textContent = `${actorName} resolved a comment thread you participated in`;
url = `${teamUrl}/doc/${notification.documentId}?commentId=${notification.commentId}`;
break;
case NotificationEventType.CreateCollection:
textContent = `A new collection was created`;
url = `${teamUrl}/collection/${notification.collectionId}`;
break;
case NotificationEventType.AddUserToDocument:
textContent = `${actorName} shared a document with you`;
url = `${teamUrl}/doc/${notification.documentId}`;
break;
case NotificationEventType.AddUserToCollection:
textContent = `${actorName} shared a collection with you`;
url = `${teamUrl}/collection/${notification.collectionId}`;
break;
default:
textContent = `You have a new notification from ${actorName}`;
url = teamUrl;
}
const message = {
ast: root([
paragraph([
strong([text(textContent)]),
text("\n"),
link(url, [text("View")]),
]),
]),
};
return message;
}
}
+1
View File
@@ -1,6 +1,7 @@
import { z } from "zod";
import {
NotificationBadgeType,
NotificationChannelType,
NotificationEventType,
UserPreference,
UserRole,
+2 -2
View File
@@ -681,7 +681,7 @@ router.post(
validate(T.UsersNotificationsSubscribeSchema),
transaction(),
async (ctx: APIContext<T.UsersNotificationsSubscribeReq>) => {
const { eventType } = ctx.input.body;
const { eventType, channel } = ctx.input.body;
const { user } = ctx.state.auth;
const eventTypes = eventType
? [eventType]
@@ -705,7 +705,7 @@ router.post(
validate(T.UsersNotificationsUnsubscribeSchema),
transaction(),
async (ctx: APIContext<T.UsersNotificationsUnsubscribeReq>) => {
const { eventType } = ctx.input.body;
const { eventType, channel } = ctx.input.body;
const { user } = ctx.state.auth;
const eventTypes = eventType
? [eventType]
+320 -6
View File
@@ -6646,7 +6646,7 @@ __metadata:
languageName: node
linkType: hard
"@types/debug@npm:^4.1.8":
"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.8":
version: 4.1.12
resolution: "@types/debug@npm:4.1.12"
dependencies:
@@ -7041,6 +7041,15 @@ __metadata:
languageName: node
linkType: hard
"@types/mdast@npm:^4.0.0":
version: 4.0.4
resolution: "@types/mdast@npm:4.0.4"
dependencies:
"@types/unist": "npm:*"
checksum: 10c0/84f403dbe582ee508fd9c7643ac781ad8597fcbfc9ccb8d4715a2c92e4545e5772cbd0dbdf18eda65789386d81b009967fdef01b24faf6640f817287f54d9c82
languageName: node
linkType: hard
"@types/mdurl@npm:^2":
version: 2.0.0
resolution: "@types/mdurl@npm:2.0.0"
@@ -7093,12 +7102,12 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:*, @types/node@npm:>=10.0.0":
version: 25.0.1
resolution: "@types/node@npm:25.0.1"
"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=18.0.0":
version: 25.0.3
resolution: "@types/node@npm:25.0.3"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/1d5ca9f240d0cf8e43d5281c0e6ee96fb22d37dc2e5ef52c6ca71de47957a6128e124990cedf5b14c03d0250737bd78ad370d93bcf1729a75ca4e54384fdd51a
checksum: 10c0/b7568f0d765d9469621615e2bb257c7fd1953d95e9acbdb58dffb6627a2c4150d405a4600aa1ad8a40182a94fe5f903cafd3c0a2f5132814debd0e3bfd61f835
languageName: node
linkType: hard
@@ -7339,6 +7348,13 @@ __metadata:
languageName: node
linkType: hard
"@types/retry@npm:0.12.0":
version: 0.12.0
resolution: "@types/retry@npm:0.12.0"
checksum: 10c0/7c5c9086369826f569b83a4683661557cab1361bac0897a1cefa1a915ff739acd10ca0d62b01071046fe3f5a3f7f2aec80785fe283b75602dc6726781ea3e328
languageName: node
linkType: hard
"@types/sanitize-filename@npm:^1.6.3":
version: 1.6.3
resolution: "@types/sanitize-filename@npm:1.6.3"
@@ -7478,6 +7494,13 @@ __metadata:
languageName: node
linkType: hard
"@types/unist@npm:*, @types/unist@npm:^3.0.0":
version: 3.0.3
resolution: "@types/unist@npm:3.0.3"
checksum: 10c0/2b1e4adcab78388e088fcc3c0ae8700f76619dbcb4741d7d201f87e2cb346bfc29a89003cfea2d76c996e1061452e14fcd737e8b25aacf949c1f2d6b2bc3dd60
languageName: node
linkType: hard
"@types/unist@npm:^2":
version: 2.0.11
resolution: "@types/unist@npm:2.0.11"
@@ -7678,6 +7701,13 @@ __metadata:
languageName: node
linkType: hard
"@workflow/serde@npm:4.1.0-beta.2":
version: 4.1.0-beta.2
resolution: "@workflow/serde@npm:4.1.0-beta.2"
checksum: 10c0/c84efdb22106ab77d95873022de1b645b1ac7288d85a78e74946c11b83fb22a89e301da649aa0750e1f65ff4655a5d7bf02d27b5022aff86ef4d3d8362a97fe7
languageName: node
linkType: hard
"@xmldom/xmldom@npm:^0.8.6":
version: 0.8.13
resolution: "@xmldom/xmldom@npm:0.8.13"
@@ -8616,6 +8646,13 @@ __metadata:
languageName: node
linkType: hard
"character-entities@npm:^2.0.0":
version: 2.0.2
resolution: "character-entities@npm:2.0.2"
checksum: 10c0/b0c645a45bcc90ff24f0e0140f4875a8436b8ef13b6bcd31ec02cfb2ca502b680362aa95386f7815bdc04b6464d48cf191210b3840d7c04241a149ede591a308
languageName: node
linkType: hard
"character-reference-invalid@npm:^1.0.0":
version: 1.1.4
resolution: "character-reference-invalid@npm:1.1.4"
@@ -8623,6 +8660,21 @@ __metadata:
languageName: node
linkType: hard
"chat@npm:4.17.0, chat@npm:^4.15.0":
version: 4.17.0
resolution: "chat@npm:4.17.0"
dependencies:
"@workflow/serde": "npm:4.1.0-beta.2"
mdast-util-to-string: "npm:^4.0.0"
remark-gfm: "npm:^4.0.0"
remark-parse: "npm:^11.0.0"
remark-stringify: "npm:^11.0.0"
remend: "npm:^1.2.1"
unified: "npm:^11.0.5"
checksum: 10c0/951ee54520e40711e001039147f6081751d6d52f05ebf80bee79c772edcfc2204cc14dc1a9afef2e4673e99453f93cae54879f7dfbe798d09465dbc65444cb30
languageName: node
linkType: hard
"cheerio-select@npm:^2.1.0":
version: 2.1.0
resolution: "cheerio-select@npm:2.1.0"
@@ -9906,6 +9958,15 @@ __metadata:
languageName: node
linkType: hard
"decode-named-character-reference@npm:^1.0.0":
version: 1.2.0
resolution: "decode-named-character-reference@npm:1.2.0"
dependencies:
character-entities: "npm:^2.0.0"
checksum: 10c0/761a89de6b0e0a2d4b21ae99074e4cc3344dd11eb29f112e23cc5909f2e9f33c5ed20cd6b146b27fb78170bce0f3f9b3362a84b75638676a05c938c24a60f5d7
languageName: node
linkType: hard
"decode-uri-component@npm:^0.2.2":
version: 0.2.2
resolution: "decode-uri-component@npm:0.2.2"
@@ -10021,6 +10082,15 @@ __metadata:
languageName: node
linkType: hard
"devlop@npm:^1.0.0, devlop@npm:^1.1.0":
version: 1.1.0
resolution: "devlop@npm:1.1.0"
dependencies:
dequal: "npm:^2.0.0"
checksum: 10c0/e0928ab8f94c59417a2b8389c45c55ce0a02d9ac7fd74ef62d01ba48060129e1d594501b77de01f3eeafc7cb00773819b0df74d96251cf20b31c5b3071f45c0e
languageName: node
linkType: hard
"dezalgo@npm:^1.0.4":
version: 1.0.4
resolution: "dezalgo@npm:1.0.4"
@@ -10755,6 +10825,13 @@ __metadata:
languageName: node
linkType: hard
"eventemitter3@npm:^4.0.4":
version: 4.0.7
resolution: "eventemitter3@npm:4.0.7"
checksum: 10c0/5f6d97cbcbac47be798e6355e3a7639a84ee1f7d9b199a07017f1d2f1e2fe236004d14fa5dfaeba661f94ea57805385e326236a6debbc7145c8877fbc0297c6b
languageName: node
linkType: hard
"eventemitter3@npm:^5.0.1":
version: 5.0.1
resolution: "eventemitter3@npm:5.0.1"
@@ -12553,6 +12630,13 @@ __metadata:
languageName: node
linkType: hard
"is-electron@npm:2.2.2":
version: 2.2.2
resolution: "is-electron@npm:2.2.2"
checksum: 10c0/327bb373f7be01b16cdff3998b5ddaa87d28f576092affaa7fe0659571b3306fdd458afbf0683a66841e7999af13f46ad0e1b51647b469526cd05a4dd736438a
languageName: node
linkType: hard
"is-extendable@npm:^0.1.0":
version: 0.1.1
resolution: "is-extendable@npm:0.1.1"
@@ -12743,7 +12827,7 @@ __metadata:
languageName: node
linkType: hard
"is-stream@npm:^2.0.0":
"is-stream@npm:^2, is-stream@npm:^2.0.0":
version: 2.0.1
resolution: "is-stream@npm:2.0.1"
checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5
@@ -13911,6 +13995,13 @@ __metadata:
languageName: node
linkType: hard
"longest-streak@npm:^3.0.0":
version: 3.1.0
resolution: "longest-streak@npm:3.1.0"
checksum: 10c0/7c2f02d0454b52834d1bcedef79c557bd295ee71fdabb02d041ff3aa9da48a90b5df7c0409156dedbc4df9b65da18742652aaea4759d6ece01f08971af6a7eaa
languageName: node
linkType: hard
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.2.0, loose-envify@npm:^1.3.1, loose-envify@npm:^1.4.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
@@ -14111,6 +14202,151 @@ __metadata:
languageName: node
linkType: hard
"mdast-util-find-and-replace@npm:^3.0.0":
version: 3.0.2
resolution: "mdast-util-find-and-replace@npm:3.0.2"
dependencies:
"@types/mdast": "npm:^4.0.0"
escape-string-regexp: "npm:^5.0.0"
unist-util-is: "npm:^6.0.0"
unist-util-visit-parents: "npm:^6.0.0"
checksum: 10c0/c8417a35605d567772ff5c1aa08363ff3010b0d60c8ea68c53cba09bf25492e3dd261560425c1756535f3b7107f62e7ff3857cdd8fb1e62d1b2cc2ea6e074ca2
languageName: node
linkType: hard
"mdast-util-from-markdown@npm:^2.0.0":
version: 2.0.2
resolution: "mdast-util-from-markdown@npm:2.0.2"
dependencies:
"@types/mdast": "npm:^4.0.0"
"@types/unist": "npm:^3.0.0"
decode-named-character-reference: "npm:^1.0.0"
devlop: "npm:^1.0.0"
mdast-util-to-string: "npm:^4.0.0"
micromark: "npm:^4.0.0"
micromark-util-decode-numeric-character-reference: "npm:^2.0.0"
micromark-util-decode-string: "npm:^2.0.0"
micromark-util-normalize-identifier: "npm:^2.0.0"
micromark-util-symbol: "npm:^2.0.0"
micromark-util-types: "npm:^2.0.0"
unist-util-stringify-position: "npm:^4.0.0"
checksum: 10c0/76eb2bd2c6f7a0318087c73376b8af6d7561c1e16654e7667e640f391341096c56142618fd0ff62f6d39e5ab4895898b9789c84cd7cec2874359a437a0e1ff15
languageName: node
linkType: hard
"mdast-util-gfm-autolink-literal@npm:^2.0.0":
version: 2.0.1
resolution: "mdast-util-gfm-autolink-literal@npm:2.0.1"
dependencies:
"@types/mdast": "npm:^4.0.0"
ccount: "npm:^2.0.0"
devlop: "npm:^1.0.0"
mdast-util-find-and-replace: "npm:^3.0.0"
micromark-util-character: "npm:^2.0.0"
checksum: 10c0/963cd22bd42aebdec7bdd0a527c9494d024d1ad0739c43dc040fee35bdfb5e29c22564330a7418a72b5eab51d47a6eff32bc0255ef3ccb5cebfe8970e91b81b6
languageName: node
linkType: hard
"mdast-util-gfm-footnote@npm:^2.0.0":
version: 2.1.0
resolution: "mdast-util-gfm-footnote@npm:2.1.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
devlop: "npm:^1.1.0"
mdast-util-from-markdown: "npm:^2.0.0"
mdast-util-to-markdown: "npm:^2.0.0"
micromark-util-normalize-identifier: "npm:^2.0.0"
checksum: 10c0/8ab965ee6be3670d76ec0e95b2ba3101fc7444eec47564943ab483d96ac17d29da2a4e6146a2a288be30c21b48c4f3938a1e54b9a46fbdd321d49a5bc0077ed0
languageName: node
linkType: hard
"mdast-util-gfm-strikethrough@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-strikethrough@npm:2.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
mdast-util-from-markdown: "npm:^2.0.0"
mdast-util-to-markdown: "npm:^2.0.0"
checksum: 10c0/b053e93d62c7545019bd914271ea9e5667ad3b3b57d16dbf68e56fea39a7e19b4a345e781312714eb3d43fdd069ff7ee22a3ca7f6149dfa774554f19ce3ac056
languageName: node
linkType: hard
"mdast-util-gfm-table@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-table@npm:2.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
devlop: "npm:^1.0.0"
markdown-table: "npm:^3.0.0"
mdast-util-from-markdown: "npm:^2.0.0"
mdast-util-to-markdown: "npm:^2.0.0"
checksum: 10c0/128af47c503a53bd1c79f20642561e54a510ad5e2db1e418d28fefaf1294ab839e6c838e341aef5d7e404f9170b9ca3d1d89605f234efafde93ee51174a6e31e
languageName: node
linkType: hard
"mdast-util-gfm-task-list-item@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-task-list-item@npm:2.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
devlop: "npm:^1.0.0"
mdast-util-from-markdown: "npm:^2.0.0"
mdast-util-to-markdown: "npm:^2.0.0"
checksum: 10c0/258d725288482b636c0a376c296431390c14b4f29588675297cb6580a8598ed311fc73ebc312acfca12cc8546f07a3a285a53a3b082712e2cbf5c190d677d834
languageName: node
linkType: hard
"mdast-util-gfm@npm:^3.0.0":
version: 3.1.0
resolution: "mdast-util-gfm@npm:3.1.0"
dependencies:
mdast-util-from-markdown: "npm:^2.0.0"
mdast-util-gfm-autolink-literal: "npm:^2.0.0"
mdast-util-gfm-footnote: "npm:^2.0.0"
mdast-util-gfm-strikethrough: "npm:^2.0.0"
mdast-util-gfm-table: "npm:^2.0.0"
mdast-util-gfm-task-list-item: "npm:^2.0.0"
mdast-util-to-markdown: "npm:^2.0.0"
checksum: 10c0/4bedcfb6a20e39901c8772f0d2bb2d7a64ae87a54c13cbd92eec062cf470fbb68c2ad754e149af5b30794e2de61c978ab1de1ace03c0c40f443ca9b9b8044f81
languageName: node
linkType: hard
"mdast-util-phrasing@npm:^4.0.0":
version: 4.1.0
resolution: "mdast-util-phrasing@npm:4.1.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
unist-util-is: "npm:^6.0.0"
checksum: 10c0/bf6c31d51349aa3d74603d5e5a312f59f3f65662ed16c58017169a5fb0f84ca98578f626c5ee9e4aa3e0a81c996db8717096705521bddb4a0185f98c12c9b42f
languageName: node
linkType: hard
"mdast-util-to-markdown@npm:^2.0.0":
version: 2.1.2
resolution: "mdast-util-to-markdown@npm:2.1.2"
dependencies:
"@types/mdast": "npm:^4.0.0"
"@types/unist": "npm:^3.0.0"
longest-streak: "npm:^3.0.0"
mdast-util-phrasing: "npm:^4.0.0"
mdast-util-to-string: "npm:^4.0.0"
micromark-util-classify-character: "npm:^2.0.0"
micromark-util-decode-string: "npm:^2.0.0"
unist-util-visit: "npm:^5.0.0"
zwitch: "npm:^2.0.0"
checksum: 10c0/4649722a6099f12e797bd8d6469b2b43b44e526b5182862d9c7766a3431caad2c0112929c538a972f214e63c015395e5d3f54bd81d9ac1b16e6d8baaf582f749
languageName: node
linkType: hard
"mdast-util-to-string@npm:^4.0.0":
version: 4.0.0
resolution: "mdast-util-to-string@npm:4.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
checksum: 10c0/2d3c1af29bf3fe9c20f552ee9685af308002488f3b04b12fa66652c9718f66f41a32f8362aa2d770c3ff464c034860b41715902ada2306bb0a055146cef064d7
languageName: node
linkType: hard
"mdurl@npm:^2.0.0":
version: 2.0.0
resolution: "mdurl@npm:2.0.0"
@@ -15599,6 +15835,35 @@ __metadata:
languageName: node
linkType: hard
"p-queue@npm:^6":
version: 6.6.2
resolution: "p-queue@npm:6.6.2"
dependencies:
eventemitter3: "npm:^4.0.4"
p-timeout: "npm:^3.2.0"
checksum: 10c0/5739ecf5806bbeadf8e463793d5e3004d08bb3f6177bd1a44a005da8fd81bb90f80e4633e1fb6f1dfd35ee663a5c0229abe26aebb36f547ad5a858347c7b0d3e
languageName: node
linkType: hard
"p-retry@npm:^4":
version: 4.6.2
resolution: "p-retry@npm:4.6.2"
dependencies:
"@types/retry": "npm:0.12.0"
retry: "npm:^0.13.1"
checksum: 10c0/d58512f120f1590cfedb4c2e0c42cb3fa66f3cea8a4646632fcb834c56055bb7a6f138aa57b20cc236fb207c9d694e362e0b5c2b14d9b062f67e8925580c73b0
languageName: node
linkType: hard
"p-timeout@npm:^3.2.0":
version: 3.2.0
resolution: "p-timeout@npm:3.2.0"
dependencies:
p-finally: "npm:^1.0.0"
checksum: 10c0/524b393711a6ba8e1d48137c5924749f29c93d70b671e6db761afa784726572ca06149c715632da8f70c090073afb2af1c05730303f915604fd38ee207b70a61
languageName: node
linkType: hard
"p-try@npm:^2.0.0":
version: 2.2.0
resolution: "p-try@npm:2.2.0"
@@ -19202,6 +19467,13 @@ __metadata:
languageName: node
linkType: hard
"trough@npm:^2.0.0":
version: 2.2.0
resolution: "trough@npm:2.2.0"
checksum: 10c0/58b671fc970e7867a48514168894396dd94e6d9d6456aca427cc299c004fe67f35ed7172a36449086b2edde10e78a71a284ec0076809add6834fb8f857ccb9b0
languageName: node
linkType: hard
"truncate-utf8-bytes@npm:^1.0.0":
version: 1.0.2
resolution: "truncate-utf8-bytes@npm:1.0.2"
@@ -19554,6 +19826,21 @@ __metadata:
languageName: node
linkType: hard
"unified@npm:^11.0.0, unified@npm:^11.0.5":
version: 11.0.5
resolution: "unified@npm:11.0.5"
dependencies:
"@types/unist": "npm:^3.0.0"
bail: "npm:^2.0.0"
devlop: "npm:^1.0.0"
extend: "npm:^3.0.0"
is-plain-obj: "npm:^4.0.0"
trough: "npm:^2.0.0"
vfile: "npm:^6.0.0"
checksum: 10c0/53c8e685f56d11d9d458a43e0e74328a4d6386af51c8ac37a3dcabec74ce5026da21250590d4aff6733ccd7dc203116aae2b0769abc18cdf9639a54ae528dfc9
languageName: node
linkType: hard
"unique-filename@npm:^5.0.0":
version: 5.0.0
resolution: "unique-filename@npm:5.0.0"
@@ -19821,6 +20108,26 @@ __metadata:
languageName: node
linkType: hard
"vfile-message@npm:^4.0.0":
version: 4.0.3
resolution: "vfile-message@npm:4.0.3"
dependencies:
"@types/unist": "npm:^3.0.0"
unist-util-stringify-position: "npm:^4.0.0"
checksum: 10c0/33d9f219610d27987689bb14fa5573d2daa146941d1a05416dd7702c4215b23f44ed81d059e70d0e4e24f9a57d5f4dc9f18d35a993f04cf9446a7abe6d72d0c0
languageName: node
linkType: hard
"vfile@npm:^6.0.0":
version: 6.0.3
resolution: "vfile@npm:6.0.3"
dependencies:
"@types/unist": "npm:^3.0.0"
vfile-message: "npm:^4.0.0"
checksum: 10c0/e5d9eb4810623f23758cfc2205323e33552fb5972e5c2e6587babe08fe4d24859866277404fb9e2a20afb71013860d96ec806cb257536ae463c87d70022ab9ef
languageName: node
linkType: hard
"vinyl-contents@npm:^2.0.0":
version: 2.0.0
resolution: "vinyl-contents@npm:2.0.0"
@@ -20908,3 +21215,10 @@ __metadata:
checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3
languageName: node
linkType: hard
"zwitch@npm:^2.0.0":
version: 2.0.4
resolution: "zwitch@npm:2.0.4"
checksum: 10c0/3c7830cdd3378667e058ffdb4cf2bb78ac5711214e2725900873accb23f3dfe5f9e7e5a06dcdc5f29605da976fc45c26d9a13ca334d6eea2245a15e77b8fc06e
languageName: node
linkType: hard