diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index e699a630f68..4576438698b 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['4034-version-control'] # put your current branch to create a build. Core team only. + branches: ['web-push-notifications-service'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' diff --git a/frontend/public/service-workers/push-notifications.js b/frontend/public/service-workers/push-notifications.js new file mode 100644 index 00000000000..78fa095f7f0 --- /dev/null +++ b/frontend/public/service-workers/push-notifications.js @@ -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)); +}); \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6c8a64234cd..f008deabd69 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -18,6 +18,7 @@ import { LogoProvider } from "./LogoContext"; import { FullScreenLoader } from "./components/Preloader"; import { ThemeProvider } from "./ThemeContext"; import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp"; +import useWebPushNotifications from "./hooks/useWebPushNotifications"; const Main = lazy(() => import("@/pages/Main")); const InvitePage = lazy(() => import("@/pages/Invite")); @@ -91,6 +92,8 @@ const SystemPromptVariables = lazy( ); export default function App() { + useWebPushNotifications(); + return ( }> diff --git a/frontend/src/hooks/useWebPushNotifications.js b/frontend/src/hooks/useWebPushNotifications.js new file mode 100644 index 00000000000..e712e9e8236 --- /dev/null +++ b/frontend/src/hooks/useWebPushNotifications.js @@ -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))); +} diff --git a/server/.gitignore b/server/.gitignore index 45a0f0371da..c27fe00dfcc 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -13,6 +13,7 @@ storage/plugins/agent-skills/* storage/plugins/agent-flows/* storage/plugins/office-extensions/* storage/plugins/anythingllm_mcp_servers.json +storage/push-notifications/* !storage/documents/DOCUMENTS.md logs/server.log *.db diff --git a/server/endpoints/webPush.js b/server/endpoints/webPush.js new file mode 100644 index 00000000000..5f406235ddb --- /dev/null +++ b/server/endpoints/webPush.js @@ -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 }; diff --git a/server/index.js b/server/index.js index 4e79e8fdc31..0f9bfaf5c85 100644 --- a/server/index.js +++ b/server/index.js @@ -28,6 +28,7 @@ const { browserExtensionEndpoints } = require("./endpoints/browserExtension"); const { communityHubEndpoints } = require("./endpoints/communityHub"); const { agentFlowEndpoints } = require("./endpoints/agentFlows"); const { mcpServersEndpoints } = require("./endpoints/mcpServers"); +const { webPushEndpoints } = require("./endpoints/webPush"); const app = express(); const apiRouter = express.Router(); const FILE_LIMIT = "3GB"; @@ -65,6 +66,7 @@ developerEndpoints(app, apiRouter); communityHubEndpoints(apiRouter); agentFlowEndpoints(apiRouter); mcpServersEndpoints(apiRouter); +webPushEndpoints(apiRouter); // Externally facing embedder endpoints embeddedEndpoints(apiRouter); diff --git a/server/models/user.js b/server/models/user.js index 35e8271bcfd..39a8a358c16 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -75,7 +75,7 @@ const User = { }, filterFields: function (user = {}) { - const { password, ...rest } = user; + const { password, web_push_subscription_config, ...rest } = user; return { ...rest }; }, @@ -198,9 +198,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} The updated user object. + */ _update: async function (id = null, data = {}) { if (!id) throw new Error("No user id provided for update"); @@ -269,6 +274,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>} 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 []; + } + }, + checkPasswordComplexity: function (passwordInput = "") { const passwordComplexity = require("joi-password-complexity"); // Can be set via ENV variable on boot. No frontend config at this time. diff --git a/server/package.json b/server/package.json index f8e31289b9f..ce81efa379b 100644 --- a/server/package.json +++ b/server/package.json @@ -80,6 +80,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" }, "devDependencies": { @@ -100,4 +101,4 @@ "nodemon": "^2.0.22", "prettier": "^3.0.3" } -} +} \ No newline at end of file diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 999cf65dfb2..beb78e69df4 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -58,30 +58,31 @@ 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[] + 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[] } model recovery_codes { diff --git a/server/utils/PushNotifications/index.js b/server/utils/PushNotifications/index.js new file mode 100644 index 00000000000..de6de9294d4 --- /dev/null +++ b/server/utils/PushNotifications/index.js @@ -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} + */ + #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} + */ + 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} + */ + 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 {"all"|"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} + */ + 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, +}; diff --git a/server/utils/boot/index.js b/server/utils/boot/index.js index 979c8e137ca..71845aca250 100644 --- a/server/utils/boot/index.js +++ b/server/utils/boot/index.js @@ -3,6 +3,7 @@ const { BackgroundService } = require("../BackgroundWorkers"); const { EncryptionManager } = require("../EncryptionManager"); const { CommunicationKey } = require("../comKey"); const setupTelemetry = require("../telemetry"); +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 @@ -31,6 +32,7 @@ function bootSSL(app, port = 3001) { new CommunicationKey(true); new EncryptionManager(); new BackgroundService().boot(); + await PushNotifications.setupPushNotificationService(); console.log(`Primary server in HTTPS mode listening on port ${port}`); }) .on("error", catchSigTerms); @@ -60,6 +62,7 @@ function bootHTTP(app, port = 3001) { new CommunicationKey(true); new EncryptionManager(); new BackgroundService().boot(); + await PushNotifications.setupPushNotificationService(); console.log(`Primary server in HTTP mode listening on port ${port}`); }) .on("error", catchSigTerms); diff --git a/server/yarn.lock b/server/yarn.lock index e0abfad8350..c80742c63a4 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -3004,6 +3004,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" @@ -3125,6 +3135,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.2, body-parser@^1.20.2: version "1.20.2" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz" @@ -5011,6 +5026,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@^5.0.0: version "5.0.1" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" @@ -5103,7 +5123,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, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5914,6 +5934,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" @@ -5921,7 +5946,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== @@ -7079,7 +7104,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.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -7912,6 +7937,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"