Web push notifications (#4942)

* WIP push notifications

* testing push

* cleanup for web-push bootstrapping
This commit is contained in:
Timothy Carambat 2026-02-02 10:56:58 -08:00 committed by GitHub
parent 97b140b4b4
commit 2c513ae396
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 519 additions and 51 deletions

View File

@ -6,19 +6,19 @@ concurrency:
on:
push:
branches: ['onboarding-flag'] # put your current branch to create a build. Core team only.
branches: ["web-push-notifications-bootstrap"] # put your current branch to create a build. Core team only.
paths-ignore:
- '**.md'
- 'cloud-deployments/*'
- 'images/**/*'
- '.vscode/**/*'
- '**/.env.example'
- '.github/ISSUE_TEMPLATE/**/*'
- '.devcontainer/**/*'
- 'embed/**/*' # Embed should be published to frontend (yarn build:publish) if any changes are introduced
- 'browser-extension/**/*' # Chrome extension is submodule
- 'server/utils/agents/aibitat/example/**/*' # Do not push new image for local dev testing of new aibitat images.
- 'extras/**/*' # Extra is just for news and other local content.
- "**.md"
- "cloud-deployments/*"
- "images/**/*"
- ".vscode/**/*"
- "**/.env.example"
- ".github/ISSUE_TEMPLATE/**/*"
- ".devcontainer/**/*"
- "embed/**/*" # Embed should be published to frontend (yarn build:publish) if any changes are introduced
- "browser-extension/**/*" # Chrome extension is submodule
- "server/utils/agents/aibitat/example/**/*" # Do not push new image for local dev testing of new aibitat images.
- "extras/**/*" # Extra is just for news and other local content.
jobs:
push_dev_build_to_dockerhub:
@ -48,15 +48,15 @@ jobs:
uses: docker/setup-buildx-action@v3
with:
version: v0.22.0
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
# Only login to the Docker Hub if the repo is mintplex/anythingllm, to allow for forks to build on GHCR
if: steps.dockerhub.outputs.enabled == 'true'
if: steps.dockerhub.outputs.enabled == 'true'
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
@ -82,7 +82,7 @@ jobs:
# For Docker scout there are some intermediary reported CVEs which exists outside
# of execution content or are unreachable by an attacker but exist in image.
# We create VEX files for these so they don't show in scout summary.
# We create VEX files for these so they don't show in scout summary.
- name: Collect known and verified CVE exceptions
id: cve-list
run: |
@ -116,4 +116,4 @@ jobs:
$tag
done
done
shell: bash
shell: bash

View File

@ -0,0 +1,26 @@
function parseEventData(event) {
try {
return event.data.json();
} catch (e) {
console.error('Failed to parse event data - is payload valid? .text():\n', event.data.text());
return null
}
}
self.addEventListener('push', function (event) {
const payload = parseEventData(event);
if (!payload) return;
// options: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#options
self.registration.showNotification(payload.title || 'AnythingLLM', {
...payload,
icon: '/favicon.png',
});
});
self.addEventListener('notificationclick', function (event) {
event.notification.close();
const { onClickUrl = null } = event.notification.data || {};
if (!onClickUrl) return;
event.waitUntil(clients.openWindow(onClickUrl));
});

View File

@ -0,0 +1,123 @@
import { useEffect } from "react";
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";
const PUSH_PUBKEY_URL = `${API_BASE}/web-push/pubkey`;
const PUSH_USER_SUBSCRIBE_URL = `${API_BASE}/web-push/subscribe`;
// If you update the service worker, increment this version or else
// the service worker will not be updated with new changes -
// Its version ID is independent of the app version to prevent reloading
// or cache busting when not needed.
const SW_VERSION = "1.0.0";
function log(message, ...args) {
if (typeof message === "object") message = JSON.stringify(message, null, 2);
console.log(`[useWebPushNotifications] ${message}`, ...args);
}
/**
* Subscribes to push notifications for the current client - can be called multiple times without re-subscribing
* or generating infinite tokens.
* @returns {void}
*/
export async function subscribeToPushNotifications() {
try {
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
log("Push notifications not supported");
return;
}
// Check current permission status
const permission = await Notification.requestPermission();
if (permission !== "granted") {
log("Notification permission not granted");
return;
}
const publicKey = await fetch(PUSH_PUBKEY_URL, { headers: baseHeaders() })
.then((res) => res.json())
.then(({ publicKey }) => {
if (!publicKey) throw new Error("No public key found or generated");
return publicKey;
})
.catch(() => null);
if (!publicKey) return log("No public key found or generated");
const swReg = await navigator.serviceWorker.register(
`/service-workers/push-notifications.js?v=${SW_VERSION}`
);
// Check for updates
swReg.addEventListener("updatefound", () => {
const newWorker = swReg.installing;
log("Service worker update found");
newWorker.addEventListener("statechange", () => {
if (
newWorker.state === "installed" &&
navigator.serviceWorker.controller
) {
// New service worker is installed and ready
log("New service worker installed, ready to activate");
// Optionally show a notification to the user
if (confirm("A new version is available. Reload to update?")) {
window.location.reload();
}
}
});
});
// Handle service worker updates
navigator.serviceWorker.addEventListener("controllerchange", () => {
log("Service worker controller changed");
});
if (swReg.installing) {
await new Promise((resolve) => {
swReg.installing.addEventListener("statechange", () => {
if (swReg.installing?.state === "activated") resolve();
});
});
} else if (swReg.waiting) {
await new Promise((resolve) => {
swReg.waiting.addEventListener("statechange", () => {
if (swReg.waiting?.state === "activated") resolve();
});
});
}
const subscription = await swReg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey),
});
await fetch(PUSH_USER_SUBSCRIBE_URL, {
method: "POST",
body: JSON.stringify(subscription),
headers: baseHeaders(),
});
} catch (error) {
log("Error subscribing to push notifications", error);
}
}
/**
* Hook that registers a service worker for push notifications.
* @returns {void}
*/
export default function useWebPushNotifications() {
useEffect(() => {
subscribeToPushNotifications();
}, []);
}
function urlBase64ToUint8Array(base64String) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, "+")
.replace(/_/g, "/");
const rawData = atob(base64);
return new Uint8Array([...rawData].map((char) => char.charCodeAt(0)));
}

1
server/.gitignore vendored
View File

@ -15,6 +15,7 @@ storage/plugins/agent-flows/*
storage/plugins/office-extensions/*
storage/plugins/anythingllm_mcp_servers.json
!storage/documents/DOCUMENTS.md
storage/push-notifications/*
storage/direct-uploads
logs/server.log
*.db

View File

@ -0,0 +1,27 @@
const { reqBody } = require("../utils/http");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const { pushNotificationService } = require("../utils/PushNotifications");
function webPushEndpoints(app) {
if (!app) return;
app.post(
"/web-push/subscribe",
[validatedRequest],
async (request, response) => {
const subscription = reqBody(request);
await pushNotificationService.registerSubscription(
response.locals.user,
subscription
);
response.status(201).json({});
}
);
app.get("/web-push/pubkey", [validatedRequest], (_request, response) => {
const publicKey = pushNotificationService.publicVapidKey;
response.status(200).json({ publicKey });
});
}
module.exports = { webPushEndpoints };

View File

@ -29,6 +29,7 @@ const { communityHubEndpoints } = require("./endpoints/communityHub");
const { agentFlowEndpoints } = require("./endpoints/agentFlows");
const { mcpServersEndpoints } = require("./endpoints/mcpServers");
const { mobileEndpoints } = require("./endpoints/mobile");
const { webPushEndpoints } = require("./endpoints/webPush");
const { httpLogger } = require("./middleware/httpLogger");
const app = express();
const apiRouter = express.Router();
@ -79,7 +80,7 @@ communityHubEndpoints(apiRouter);
agentFlowEndpoints(apiRouter);
mcpServersEndpoints(apiRouter);
mobileEndpoints(apiRouter);
webPushEndpoints(apiRouter);
// Externally facing embedder endpoints
embeddedEndpoints(apiRouter);

View File

@ -87,7 +87,7 @@ const User = {
},
filterFields: function (user = {}) {
const { password, ...rest } = user;
const { password, web_push_subscription_config, ...rest } = user;
return { ...rest };
},
_identifyErrorAndFormatMessage: function (error) {
@ -217,9 +217,14 @@ const User = {
}
},
// Explicit direct update of user object.
// Only use this method when directly setting a key value
// that takes no user input for the keys being modified.
/**
* Explicit direct update of user object.
* Only use this method when directly setting a key value
* that takes no user input for the keys being modified.
* @param {number} id - The id of the user to update.
* @param {Object} data - The data to update the user with.
* @returns {Promise<Object>} The updated user object.
*/
_update: async function (id = null, data = {}) {
if (!id) throw new Error("No user id provided for update");
@ -235,6 +240,26 @@ const User = {
}
},
/**
* Get all users that match the given clause without filtering the fields.
* Internal use only - do not use this method for user-input flows
* @param {Object} clause - The clause to filter the users by.
* @param {number|null} limit - The maximum number of users to return.
* @returns {Promise<Array<User>>} The users that match the given clause.
*/
_where: async function (clause = {}, limit = null) {
try {
const users = await prisma.users.findMany({
where: clause,
...(limit !== null ? { take: limit } : {}),
});
return users;
} catch (error) {
console.error(error.message);
return [];
}
},
/**
* Returns a user object based on the clause provided.
* @param {Object} clause - The clause to use to find the user.

View File

@ -84,6 +84,7 @@
"uuid": "^9.0.0",
"uuid-apikey": "^1.5.3",
"weaviate-ts-client": "^1.4.0",
"web-push": "^3.6.7",
"winston": "^3.13.0"
},
"resolutions": {

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "web_push_subscription_config" TEXT;

View File

@ -58,32 +58,33 @@ model system_settings {
}
model users {
id Int @id @default(autoincrement())
username String? @unique
password String
pfpFilename String?
role String @default("default")
suspended Int @default(0)
seen_recovery_codes Boolean? @default(false)
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
dailyMessageLimit Int?
bio String? @default("")
workspace_chats workspace_chats[]
workspace_users workspace_users[]
embed_configs embed_configs[]
embed_chats embed_chats[]
threads workspace_threads[]
recovery_codes recovery_codes[]
password_reset_tokens password_reset_tokens[]
workspace_agent_invocations workspace_agent_invocations[]
slash_command_presets slash_command_presets[]
browser_extension_api_keys browser_extension_api_keys[]
temporary_auth_tokens temporary_auth_tokens[]
system_prompt_variables system_prompt_variables[]
prompt_history prompt_history[]
desktop_mobile_devices desktop_mobile_devices[]
workspace_parsed_files workspace_parsed_files[]
id Int @id @default(autoincrement())
username String? @unique
password String
pfpFilename String?
role String @default("default")
suspended Int @default(0)
seen_recovery_codes Boolean? @default(false)
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
dailyMessageLimit Int?
bio String? @default("")
web_push_subscription_config String?
workspace_chats workspace_chats[]
workspace_users workspace_users[]
embed_configs embed_configs[]
embed_chats embed_chats[]
threads workspace_threads[]
recovery_codes recovery_codes[]
password_reset_tokens password_reset_tokens[]
workspace_agent_invocations workspace_agent_invocations[]
slash_command_presets slash_command_presets[]
browser_extension_api_keys browser_extension_api_keys[]
temporary_auth_tokens temporary_auth_tokens[]
system_prompt_variables system_prompt_variables[]
prompt_history prompt_history[]
desktop_mobile_devices desktop_mobile_devices[]
workspace_parsed_files workspace_parsed_files[]
}
model recovery_codes {

View File

@ -0,0 +1,222 @@
const webpush = require("web-push");
const fs = require("fs");
const path = require("path");
const { User } = require("../../models/user");
const { SystemSettings } = require("../../models/systemSettings");
const { safeJsonParse } = require("../http");
/**
* For more options, see:
* https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#options
* @typedef {Object} PushNotificationPayload
* @property {string} title - The title of the notification.
* @property {string} body - The message of the notification.
* @property {Object} data - Unstructured data for the notification. Use this for anything non-standard.
* @property {string} [data.onClickUrl] - The URL to open when the notification is clicked. Note: Can be relative or absolute.
* @property {Object[]} actions - The actions for the notification.
* @property {string} [actions[].action] - The action to perform when the notification is clicked. Handled in the service worker.
* @property {string} [actions[].title] - The title of the action to show in the Options dropdown
* @property {string} image - A string containing the URL of an image to be displayed in the notification.
*/
class PushNotifications {
static mailTo = "anythingllm@localhost";
/**
* @type {PushNotifications}
*/
static instance = null;
/**
* The VAPID keys for the push notification service.
* @type {{publicKey: string | null, privateKey: string | null}}
*/
#vapidKeys = {
publicKey: null,
privateKey: null,
};
/**
* The subscriptions for the push notification service.
* @type {Map<string, Object>}
*/
#subscriptions = new Map();
constructor() {
if (PushNotifications.instance) return PushNotifications.instance;
PushNotifications.instance = this;
}
#log(text, ...args) {
console.log(`\x1b[36m[PushNotifications]\x1b[0m ${text}`, ...args);
}
get pushService() {
try {
const vapidKeys = this.existingVapidKeys;
if (!vapidKeys.publicKey || !vapidKeys.privateKey)
throw new Error(
"VAPID keys not found. Make sure they are generated in the main process first."
);
webpush.setVapidDetails(
`mailto:${this.mailTo}`,
vapidKeys.publicKey,
vapidKeys.privateKey
);
return webpush;
} catch (e) {
console.error("Failed to set VAPID details", e);
return null;
}
}
get storagePath() {
return process.env.NODE_ENV === "development"
? path.resolve(__dirname, `../../storage`, "push-notifications")
: path.resolve(process.env.STORAGE_DIR, "push-notifications");
}
get primarySubscriptionPath() {
return path.resolve(this.storagePath, `primary-subscription.json`);
}
get existingVapidKeys() {
// Already loaded and binded to the instance
if (this.#vapidKeys.publicKey && this.#vapidKeys.privateKey)
return this.#vapidKeys;
const vapidKeysPath = path.resolve(this.storagePath, `vapid-keys.json`);
if (!fs.existsSync(vapidKeysPath))
return { publicKey: null, privateKey: null };
const existingVapidKeys = JSON.parse(
fs.readFileSync(vapidKeysPath, "utf8")
);
this.#log(`Loaded existing VAPID keys!`);
this.#vapidKeys.publicKey = existingVapidKeys.publicKey;
this.#vapidKeys.privateKey = existingVapidKeys.privateKey;
return this.#vapidKeys;
}
get publicVapidKey() {
return this.existingVapidKeys.publicKey;
}
/**
* Load the subscriptions for the push notification service.
* In single user mode, the subscription is stored in the primary-subscription.json file.
* In multi user mode, the subscriptions are stored in the database so we grab them from there
* and store them in the #subscriptions map for reference later.
* @returns {Promise<void>}
*/
async loadSubscriptions() {
const isMultiUserMode = await SystemSettings.isMultiUserMode();
if (isMultiUserMode) {
const users = await User._where({
web_push_subscription_config: { not: null },
});
for (const user of users) {
const subscription = safeJsonParse(
user.web_push_subscription_config,
null
);
if (subscription) this.#subscriptions.set(user.id, subscription);
}
this.#log(`Loaded ${this.#subscriptions.size} existing subscriptions.`);
return;
}
this.#log("Loading single user mode subscriptions...");
if (!fs.existsSync(this.primarySubscriptionPath)) return;
const subscription = JSON.parse(
fs.readFileSync(this.primarySubscriptionPath, "utf8")
);
if (subscription) this.#subscriptions.set("primary", subscription);
this.#log(`Loaded primary user's existing subscription.`);
}
/**
* Register a new subscription for a user.
* In single user mode, the userId is mapped to "primary"
* In multi user mode, the userId is the user's id in the database
*
* @param {Object|null} user - The user to register the subscription for.
* @param {Object} subscription - The subscription to register.
* @returns {Promise<PushNotifications>}
*/
async registerSubscription(user = null, subscription) {
let userId = user?.id || "primary";
this.#subscriptions.set(userId, subscription);
// If this was a real user, write the subscription to the database
if (!!user) {
await User._update(user.id, {
web_push_subscription_config: JSON.stringify(subscription),
});
this.#log(`Registered or updated subscription for user - ${user.id}`);
} else {
if (!fs.existsSync(this.storagePath))
fs.mkdirSync(this.storagePath, { recursive: true });
fs.writeFileSync(
this.primarySubscriptionPath,
JSON.stringify(subscription, null, 2)
);
this.#log(`Registered or updated primary user's subscription.`);
}
return this;
}
/**
* Send a push notification to all subscribed clients.
* @param {Object} options - The options for the notification.
* @param {"primary"|number} [options.to] - The subscription to send the notification to. "all" sends to all subscriptions, "primary" sends to the primary user (single user mode only), a number sends subscription to specific user
* @param {PushNotificationPayload} [options.payload] - The payload to send to the clients.
* @returns {void}
*/
sendNotification({ to = "primary", payload = {} } = {}) {
if (this.#subscriptions.size === 0)
return this.#log(".sendNotification() - No subscriptions found");
if (!this.#subscriptions.has(to))
return this.#log(
`.sendNotification() - Subscription for user ${to} not found`
);
this.#log(`.sendNotification() - Sending notification to user ${to}`);
this.pushService.sendNotification(
this.#subscriptions.get(to),
JSON.stringify(payload)
);
}
/**
* Setup the push notification service.
* This will generate new VAPID keys if they don't exist and save them to the storage path.
* It will also load the subscriptions from the database or the primary-subscription.json file.
* @returns {Promise<void>}
*/
static async setupPushNotificationService() {
const instance = PushNotifications.instance;
const existingVapidKeys = instance.existingVapidKeys;
if (!existingVapidKeys.publicKey || !existingVapidKeys.privateKey) {
instance.#log("Generating new VAPID keys...");
const vapidKeys = webpush.generateVAPIDKeys();
instance.#vapidKeys.publicKey = vapidKeys.publicKey;
instance.#vapidKeys.privateKey = vapidKeys.privateKey;
instance.#log(`New VAPID keys generated!`);
if (!fs.existsSync(instance.storagePath))
fs.mkdirSync(instance.storagePath, { recursive: true });
fs.writeFileSync(
path.resolve(instance.storagePath, `vapid-keys.json`),
JSON.stringify(vapidKeys, null, 2)
);
}
await instance.loadSubscriptions();
instance.pushService;
return;
}
}
module.exports = {
pushNotificationService: new PushNotifications(),
PushNotifications,
};

View File

@ -5,6 +5,7 @@ const { CommunicationKey } = require("../comKey");
const setupTelemetry = require("../telemetry");
const eagerLoadContextWindows = require("./eagerLoadContextWindows");
const markOnboarded = require("./markOnboarded");
const { PushNotifications } = require("../PushNotifications");
// Testing SSL? You can make a self signed certificate and point the ENVs to that location
// make a directory in server called 'sslcert' - cd into it
@ -35,6 +36,7 @@ function bootSSL(app, port = 3001) {
new EncryptionManager();
new BackgroundService().boot();
await eagerLoadContextWindows();
await PushNotifications.setupPushNotificationService();
console.log(`Primary server in HTTPS mode listening on port ${port}`);
})
.on("error", catchSigTerms);
@ -66,6 +68,7 @@ function bootHTTP(app, port = 3001) {
new EncryptionManager();
new BackgroundService().boot();
await eagerLoadContextWindows();
await PushNotifications.setupPushNotificationService();
console.log(`Primary server in HTTP mode listening on port ${port}`);
})
.on("error", catchSigTerms);

View File

@ -3874,6 +3874,16 @@ arraybuffer.prototype.slice@^1.0.3:
is-array-buffer "^3.0.4"
is-shared-array-buffer "^1.0.2"
asn1.js@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
dependencies:
bn.js "^4.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
safer-buffer "^2.1.0"
async@^3.2.3:
version "3.2.5"
resolved "https://registry.npmjs.org/async/-/async-3.2.5.tgz"
@ -3992,6 +4002,11 @@ bl@^6.0.3:
inherits "^2.0.4"
readable-stream "^4.2.0"
bn.js@^4.0.0:
version "4.12.2"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.2.tgz#3d8fed6796c24e177737f7cc5172ee04ef39ec99"
integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==
body-parser@^1.20.3, body-parser@~1.20.3:
version "1.20.4"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f"
@ -5906,6 +5921,11 @@ http-proxy-agent@^7.0.0:
agent-base "^7.1.0"
debug "^4.3.4"
http_ece@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.2.0.tgz#84d5885f052eae8c9b075eee4d2eb5105f114479"
integrity sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==
https-proxy-agent@^7.0.0:
version "7.0.4"
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz"
@ -5990,7 +6010,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
version "2.0.4"
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -6826,6 +6846,11 @@ mimic-response@^3.1.0:
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimalistic-assert@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
@ -6833,7 +6858,7 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6:
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.8"
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
@ -7927,7 +7952,7 @@ safe-timers@^1.1.0:
resolved "https://registry.npmjs.org/safe-timers/-/safe-timers-1.1.0.tgz"
integrity sha512-9aqY+v5eMvmRaluUEtdRThV1EjlSElzO7HuCj0sTW9xvp++8iJ9t/RWGNWV6/WHcUJLHpyT2SNf/apoKTU2EpA==
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@ -8799,6 +8824,17 @@ weaviate-ts-client@^1.4.0:
isomorphic-fetch "^3.0.0"
uuid "^9.0.1"
web-push@^3.6.7:
version "3.6.7"
resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.6.7.tgz#5f5e645951153e37ef90a6ddea5c150ea0f709e1"
integrity sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==
dependencies:
asn1.js "^5.3.0"
http_ece "1.2.0"
https-proxy-agent "^7.0.0"
jws "^4.0.0"
minimist "^1.2.5"
web-streams-polyfill@4.0.0-beta.3:
version "4.0.0-beta.3"
resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz"