diff --git a/apps/docs/docs/management/assistant.mdx b/apps/docs/docs/management/assistant.mdx
index eda61c6d7..5c624b961 100644
--- a/apps/docs/docs/management/assistant.mdx
+++ b/apps/docs/docs/management/assistant.mdx
@@ -50,7 +50,8 @@ Homarr includes presets for OpenRouter, OpenAI, Claude, Gemini, xAI, Groq, Mistr
LM Studio. Use **Custom endpoint** for another OpenAI-compatible Chat Completions service.
The [Homarr provider](../workshop/homarr-provider) does not require an administrator API key. Each user signs in to the
-Community Workshop and receives a separate daily allowance.
+Community Workshop and receives a separate daily allowance. A shared Workshop daily quota also applies, so requests
+can be rejected after that shared quota is exhausted.
:::info Local model servers
@@ -78,5 +79,7 @@ and delete conversations.
- **A model stops during a tool call:** use a model that supports streaming and structured tool calls.
- **A local endpoint cannot be reached:** check container networking and the endpoint's listen address.
-Homarr exposes the same tool set to external clients through the [MCP server](./mcp). Custom Widget authors can find
-the full workflow in [Agent authoring](./custom-widgets/agent-authoring).
+Homarr exposes its MCP-enabled procedures to external clients through the [MCP server](./mcp). MCP omits
+`customWidget_*` tools for non-admin API keys, while the built-in assistant adds browser actions and optional
+OpenRouter web search. Custom Widget authors can find the full workflow in
+[Agent authoring](./custom-widgets/agent-authoring).
diff --git a/apps/docs/docs/workshop/development.mdx b/apps/docs/docs/workshop/development.mdx
index 2e705e446..f61ef55db 100644
--- a/apps/docs/docs/workshop/development.mdx
+++ b/apps/docs/docs/workshop/development.mdx
@@ -41,7 +41,8 @@ WORKSHOP_API_URL=http://127.0.0.1:8090
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop
```
-Restart `pnpm dev` after changing `.env`. GitHub OAuth is optional locally. See
+Restart `pnpm dev:docs` after changing the Docusaurus URL variables. If Homarr is also running, restart its `pnpm dev`
+process after changing the Workshop URLs. GitHub OAuth is optional locally. See
[`apps/workshop/README.md`](https://github.com/homarr-labs/homarr/blob/HEAD/apps/workshop/README.md) for OAuth,
Workshop moderator promotion, logs, tests, backup, restore, stopping, and intentional volume reset.
diff --git a/apps/docs/package.json b/apps/docs/package.json
index bec22d5ea..ce8dd21ca 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -51,7 +51,6 @@
"postcss": "^8.5.16",
"posthog-docusaurus": "^2.0.5",
"prism-react-renderer": "^2.4.1",
- "random-word-slugs": "^0.1.7",
"react": "catalog:",
"react-day-picker": "catalog:",
"react-dom": "catalog:",
diff --git a/apps/docs/src/components/assistant-provider/open-collective-link.tsx b/apps/docs/src/components/assistant-provider/open-collective-link.tsx
index 866e0841a..75499850f 100644
--- a/apps/docs/src/components/assistant-provider/open-collective-link.tsx
+++ b/apps/docs/src/components/assistant-provider/open-collective-link.tsx
@@ -1,8 +1,10 @@
+import Translate from "@docusaurus/Translate";
+
import styles from "./open-collective-link.module.css";
export const OpenCollectiveLink = () => (
- Donate to Homarr on OpenCollective
+ Donate to Homarr on OpenCollective
);
diff --git a/apps/docs/src/components/pages/home/hero/hero-cards.tsx b/apps/docs/src/components/pages/home/hero/hero-cards.tsx
index a61f8b54e..e76da2c24 100644
--- a/apps/docs/src/components/pages/home/hero/hero-cards.tsx
+++ b/apps/docs/src/components/pages/home/hero/hero-cards.tsx
@@ -1,3 +1,4 @@
+import { translate } from "@docusaurus/Translate";
import { IconBrandDocker, IconCpu, IconDeviceDesktopAnalytics } from "@tabler/icons-react";
import styles from "../../../../pages/index.module.css";
@@ -30,18 +31,31 @@ export const HeroCards = () => {
);
};
+const heroLabels = {
+ systemResources: translate({ id: "homepage.preview.systemResources", message: "System resources" }),
+ online: translate({ id: "homepage.preview.online", message: "Online" }),
+ cpu: translate({ id: "homepage.preview.cpu", message: "CPU" }),
+ memory: translate({ id: "homepage.preview.memory", message: "Memory" }),
+ disk: translate({ id: "homepage.preview.disk", message: "Disk" }),
+ docker: translate({ id: "homepage.preview.docker", message: "Docker" }),
+ dockerSummary: translate({ id: "homepage.preview.dockerSummary", message: "2 running · 1 failed" }),
+ healthy: translate({ id: "homepage.preview.healthy", message: "Healthy" }),
+ running: translate({ id: "homepage.preview.running", message: "Running" }),
+ exited: translate({ id: "homepage.preview.exited", message: "Exited" }),
+};
+
const SystemResourcesWidget = () => (
- System resources
+ {heroLabels.systemResources}
- Online
+ {heroLabels.online}
- } label="CPU" value={34} />
-
-
+ } label={heroLabels.cpu} value={34} />
+
+
);
@@ -63,23 +77,20 @@ const DockerWidget = () => (
- Docker
+ {heroLabels.docker}
- 2 running · 1 failed
+ {heroLabels.dockerSummary}
{[
- ["immich", "Healthy"],
- ["jellyfin", "Running"],
- ["paperless", "Exited"],
- ].map(([name, status]) => (
-
+ { name: "immich", status: heroLabels.healthy, failed: false },
+ { name: "jellyfin", status: heroLabels.running, failed: false },
+ { name: "paperless", status: heroLabels.exited, failed: true },
+ ].map(({ name, status, failed }) => (
+
{name}
-
+
{status}
diff --git a/apps/docs/src/components/pages/home/hero/hero.tsx b/apps/docs/src/components/pages/home/hero/hero.tsx
index 5b119f44b..61128c23f 100644
--- a/apps/docs/src/components/pages/home/hero/hero.tsx
+++ b/apps/docs/src/components/pages/home/hero/hero.tsx
@@ -1,4 +1,5 @@
import Link from "@docusaurus/Link";
+import { translate } from "@docusaurus/Translate";
import { IconDownload, IconExternalLink } from "@tabler/icons-react";
import clsx from "clsx";
import styles from "../../../../pages/index.module.css";
@@ -57,7 +58,10 @@ export default function HomeHero() {
-
+
diff --git a/apps/docs/src/components/pages/home/hero/widgets/app-widget.tsx b/apps/docs/src/components/pages/home/hero/widgets/app-widget.tsx
index a1c224039..ce5158711 100644
--- a/apps/docs/src/components/pages/home/hero/widgets/app-widget.tsx
+++ b/apps/docs/src/components/pages/home/hero/widgets/app-widget.tsx
@@ -14,7 +14,7 @@ export const AppWidget = ({ name, label, className }: AppWidgetProps) => {
return (
{label ?? app.name}
-
+
);
diff --git a/apps/docs/src/components/pages/home/hero/widgets/downloads-widget.tsx b/apps/docs/src/components/pages/home/hero/widgets/downloads-widget.tsx
index e511ba02f..6cd05575d 100644
--- a/apps/docs/src/components/pages/home/hero/widgets/downloads-widget.tsx
+++ b/apps/docs/src/components/pages/home/hero/widgets/downloads-widget.tsx
@@ -1,10 +1,15 @@
-import { getRndInteger } from "@site/src/tools/math";
+import { translate } from "@docusaurus/Translate";
import { useEffect, useState } from "react";
import { CommonWidgetProps, WidgetCard } from "./card";
import clsx from "clsx";
-import { generateSlug } from "random-word-slugs";
import styles from "../../../../../pages/index.module.css";
+const downloadQueue = ["family-photos.zip", "linux-isos.mkv", "server-backup.zip", "holiday-video.mp4"];
+const downloadLabels = {
+ file: translate({ id: "homepage.preview.file", message: "File" }),
+ progress: translate({ id: "homepage.preview.progress", message: "Progress" }),
+};
+
export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
const [downloads, setDownloads] = useState([
{ filename: "photos-backup.zip", progress: 72 },
@@ -18,11 +23,15 @@ export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
const progressedDownloads = filteredPrev.map((download, index) => ({
...download,
- progress: Math.min(100, download.progress + getRndInteger(0, index < 2 ? 16 : 8)),
+ progress: Math.min(100, download.progress + (index < 2 ? 8 : 4)),
}));
if (progressedDownloads.length < 3 && progressedDownloads.length === prev.length) {
- progressedDownloads.push(generateRandomDownload());
+ const availableDownloads = downloadQueue.filter(
+ (filename) => !progressedDownloads.some((download) => download.filename === filename),
+ );
+ const filename = availableDownloads[0];
+ if (filename) progressedDownloads.push({ filename, progress: 0 });
}
return progressedDownloads;
@@ -39,8 +48,8 @@ export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
- | File |
- Progress |
+ {downloadLabels.file} |
+ {downloadLabels.progress} |
@@ -48,12 +57,21 @@ export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
return (
| {file.filename} |
-
+ |
|
@@ -69,15 +87,3 @@ interface Download {
filename: string;
progress: number;
}
-
-const formats = ["mkv", "mp4"];
-
-const generateRandomDownload = (): Download => {
- const randomMovie = generateSlug(2);
- const randomFormat = formats[getRndInteger(0, formats.length - 1)];
-
- return {
- filename: `${randomMovie}.${randomFormat}`,
- progress: 0,
- };
-};
diff --git a/apps/docs/src/css/custom.css b/apps/docs/src/css/custom.css
index f90d26c4f..6359625c6 100644
--- a/apps/docs/src/css/custom.css
+++ b/apps/docs/src/css/custom.css
@@ -1,4 +1,4 @@
-@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap");
+@import "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap";
@import "tailwindcss/theme" layer(theme);
@import "tailwindcss/utilities" layer(utilities);
diff --git a/apps/docs/src/pages/index.module.css b/apps/docs/src/pages/index.module.css
index 511216180..36af84e40 100644
--- a/apps/docs/src/pages/index.module.css
+++ b/apps/docs/src/pages/index.module.css
@@ -77,7 +77,7 @@ section {
flex: none;
min-width: 0;
border-radius: 0.75rem;
- border: 1px solid color-mix(in srgb, currentColor 12%, transparent) !important;
+ border: 1px solid color-mix(in srgb, currentcolor 12%, transparent) !important;
color: inherit;
font: inherit;
background: color-mix(in srgb, var(--ifm-navbar-background-color) 76%, transparent) !important;
@@ -154,7 +154,7 @@ section {
height: 0.375rem;
border: 0;
border-radius: 999px;
- background: color-mix(in srgb, currentColor 16%, transparent);
+ background: color-mix(in srgb, currentcolor 16%, transparent);
}
.resourceTrack::-webkit-progress-bar {
@@ -181,7 +181,7 @@ section {
height: 0.5rem;
border: 0;
border-radius: 999px;
- background: color-mix(in srgb, currentColor 18%, transparent);
+ background: color-mix(in srgb, currentcolor 18%, transparent);
}
.downloadProgress::-webkit-progress-bar {
@@ -215,7 +215,7 @@ section {
display: inline-flex;
align-items: center;
gap: 0.25rem;
- color: color-mix(in srgb, currentColor 82%, transparent);
+ color: color-mix(in srgb, currentcolor 82%, transparent);
}
.statusDot {
diff --git a/apps/docs/src/tools/math.ts b/apps/docs/src/tools/math.ts
deleted file mode 100644
index 966a27d27..000000000
--- a/apps/docs/src/tools/math.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function getRndInteger(min: number, max: number) {
- return Math.round(Math.random() * (max - min)) + min;
-}
diff --git a/apps/workshop/homarr_provider.go b/apps/workshop/homarr_provider.go
index 85857b9ae..c606c99ff 100644
--- a/apps/workshop/homarr_provider.go
+++ b/apps/workshop/homarr_provider.go
@@ -43,7 +43,11 @@ const (
providerRequestTimeout = 5 * time.Minute
)
-var errProviderResponseTooLarge = errors.New("provider response is too large")
+var (
+ errProviderResponseTooLarge = errors.New("provider response is too large")
+ errInvalidTools = errors.New("invalid request tools")
+ errUnsupportedTool = errors.New("unsupported request tool")
+)
var clientControlledRoutingFields = []string{
"models", "provider", "route", "plugins", "transforms", "extra_body", "extra_headers",
@@ -223,7 +227,11 @@ func (provider *homarrProvider) chat(event *core.RequestEvent) error {
return event.JSON(http.StatusRequestEntityTooLarge, openAIError(err.Error()))
}
if err := sanitizeProviderPayload(payload, provider.modelID); err != nil {
- return event.JSON(http.StatusBadRequest, openAIError(err.Error()))
+ message := "The request contains an unsupported tool."
+ if errors.Is(err, errInvalidTools) {
+ message = "The request tools are not valid."
+ }
+ return event.JSON(http.StatusBadRequest, openAIError(message))
}
upstreamBody, err := json.Marshal(payload)
if err != nil {
@@ -277,7 +285,7 @@ func (provider *homarrProvider) chat(event *core.RequestEvent) error {
contentType = "application/json"
}
if upstreamResponse.StatusCode < 200 || upstreamResponse.StatusCode >= 300 {
- return event.JSON(upstreamResponse.StatusCode, openAIError("The model endpoint rejected the request."))
+ return event.JSON(safeUpstreamStatus(upstreamResponse.StatusCode), openAIError("The model endpoint rejected the request."))
}
if requestBody.Stream {
@@ -294,6 +302,13 @@ func (provider *homarrProvider) chat(event *core.RequestEvent) error {
return event.Blob(upstreamResponse.StatusCode, contentType, responseBody)
}
+func safeUpstreamStatus(status int) int {
+ if status == http.StatusUnauthorized || status == http.StatusForbidden {
+ return http.StatusBadGateway
+ }
+ return status
+}
+
func sanitizeProviderPayload(payload map[string]any, upstreamModelID string) error {
for _, field := range clientControlledRoutingFields {
delete(payload, field)
@@ -312,14 +327,14 @@ func sanitizeProviderPayload(payload map[string]any, upstreamModelID string) err
if rawTools, exists := payload["tools"]; exists {
tools, ok := rawTools.([]any)
if !ok {
- return errors.New("The request tools are not valid.")
+ return errInvalidTools
}
sanitizedTools := make([]any, 0, len(tools))
webSearchAdded := false
for _, rawTool := range tools {
tool, ok := rawTool.(map[string]any)
if !ok {
- return errors.New("The request contains an unsupported tool.")
+ return errUnsupportedTool
}
switch tool["type"] {
case "function":
@@ -336,7 +351,7 @@ func sanitizeProviderPayload(payload map[string]any, upstreamModelID string) err
webSearchAdded = true
}
default:
- return errors.New("The request contains an unsupported tool.")
+ return errUnsupportedTool
}
}
payload["tools"] = sanitizedTools
@@ -422,52 +437,66 @@ func (provider *homarrProvider) quota(app core.App, user *core.Record) (quotaSna
now := provider.now().UTC()
var snapshot quotaSnapshot
err := app.RunInTransaction(func(txApp core.App) error {
- quota, current, err := provider.loadQuota(txApp, user, now)
+ quota, current, quotaChanged, err := provider.loadQuota(txApp, user, now)
if err != nil {
return err
}
- globalQuota, globalRemaining, err := provider.loadGlobalQuota(txApp, now)
+ globalQuota, globalRemaining, globalQuotaChanged, err := provider.loadGlobalQuota(txApp, now)
if err != nil {
return err
}
current.Remaining = min(current.Remaining, globalRemaining)
snapshot = current
- if err := txApp.Save(globalQuota); err != nil {
- return err
+ if globalQuotaChanged {
+ if err := txApp.Save(globalQuota); err != nil {
+ return err
+ }
}
- return txApp.Save(quota)
+ if quotaChanged {
+ return txApp.Save(quota)
+ }
+ return nil
})
return snapshot, err
}
-func (provider *homarrProvider) loadGlobalQuota(app core.App, now time.Time) (*core.Record, int, error) {
+func (provider *homarrProvider) loadGlobalQuota(app core.App, now time.Time) (*core.Record, int, bool, error) {
quota, err := app.FindFirstRecordByFilter("assistant_global_quota", "key = 'default'")
if err != nil {
- return nil, 0, err
+ return nil, 0, false, err
}
+ changed := false
if quota.GetString("day") != now.Format(time.DateOnly) {
quota.Set("day", now.Format(time.DateOnly))
quota.Set("used", 0)
+ changed = true
}
- return quota, max(0, provider.globalDailyLimit-quota.GetInt("used")), nil
+ return quota, max(0, provider.globalDailyLimit-quota.GetInt("used")), changed, nil
}
-func (provider *homarrProvider) loadQuota(app core.App, user *core.Record, now time.Time) (*core.Record, quotaSnapshot, error) {
+func (provider *homarrProvider) loadQuota(
+ app core.App,
+ user *core.Record,
+ now time.Time,
+) (*core.Record, quotaSnapshot, bool, error) {
quota, err := app.FindFirstRecordByFilter("assistant_quotas", "user = {:user}", dbx.Params{"user": user.Id})
if err != nil && !errors.Is(err, sql.ErrNoRows) {
- return nil, quotaSnapshot{}, err
+ return nil, quotaSnapshot{}, false, err
}
+ changed := false
if quota == nil {
collection, err := app.FindCollectionByNameOrId("assistant_quotas")
if err != nil {
- return nil, quotaSnapshot{}, err
+ return nil, quotaSnapshot{}, false, err
}
quota = core.NewRecord(collection)
quota.Set("user", user.Id)
+ changed = true
}
if quota.GetString("day") != now.Format(time.DateOnly) {
quota.Set("day", now.Format(time.DateOnly))
quota.Set("used", 0)
+ changed = true
}
limit := provider.dailyLimit
used := quota.GetInt("used")
@@ -476,7 +505,7 @@ func (provider *homarrProvider) loadQuota(app core.App, user *core.Record, now t
Used: used,
Remaining: max(0, limit-used),
ResetsAt: time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC),
- }, nil
+ }, changed, nil
}
func (provider *homarrProvider) startRequest(
@@ -486,11 +515,11 @@ func (provider *homarrProvider) startRequest(
var snapshot quotaSnapshot
err := app.RunInTransaction(func(txApp core.App) error {
now := provider.now().UTC()
- quota, current, err := provider.loadQuota(txApp, user, now)
+ quota, current, _, err := provider.loadQuota(txApp, user, now)
if err != nil {
return err
}
- globalQuota, globalRemaining, err := provider.loadGlobalQuota(txApp, now)
+ globalQuota, globalRemaining, _, err := provider.loadGlobalQuota(txApp, now)
if err != nil {
return err
}
diff --git a/apps/workshop/homarr_provider_test.go b/apps/workshop/homarr_provider_test.go
index 084660a25..50d1d5f45 100644
--- a/apps/workshop/homarr_provider_test.go
+++ b/apps/workshop/homarr_provider_test.go
@@ -100,12 +100,23 @@ func TestSanitizeProviderPayloadRejectsUnsupportedServerTools(t *testing.T) {
}
}
+func TestSafeUpstreamStatus(t *testing.T) {
+ for _, status := range []int{401, 403} {
+ if actual := safeUpstreamStatus(status); actual != 502 {
+ t.Fatalf("expected upstream %d to become 502, got %d", status, actual)
+ }
+ }
+ if actual := safeUpstreamStatus(429); actual != 429 {
+ t.Fatalf("expected upstream 429 to be preserved, got %d", actual)
+ }
+}
+
func TestValidateProviderInput(t *testing.T) {
payload := map[string]any{
"messages": []any{map[string]any{
"role": "user",
"content": []any{map[string]any{
- "type": "image_url",
+ "type": "image_url",
"image_url": map[string]any{"url": "data:image/png;base64," + strings.Repeat("a", 1_300_000)},
}},
}},
diff --git a/e2e/assistant-docs-screenshots.spec.ts b/e2e/assistant-docs-screenshots.spec.ts
index c012a8ffd..fa672fb55 100644
--- a/e2e/assistant-docs-screenshots.spec.ts
+++ b/e2e/assistant-docs-screenshots.spec.ts
@@ -2,6 +2,8 @@ import { mkdir } from "node:fs/promises";
import path from "node:path";
import { chromium, expect } from "@playwright/test";
+import type { Browser } from "@playwright/test";
+import type { StartedTestContainer } from "testcontainers";
import { describe, test } from "vitest";
import * as sqliteSchema from "../packages/db/schema/sqlite";
@@ -33,20 +35,23 @@ describe.skipIf(process.env.UPDATE_DOCS_SCREENSHOTS !== "true")("Assistant docum
const outputDirectory = path.resolve("apps/docs/static/img/assistant");
await mkdir(outputDirectory, { recursive: true });
- const homarrContainer = await createHomarrContainer({
- environment: { AUTH_PROVIDERS: "credentials" },
- mounts: { "/appdata": localMountPath },
- }).start();
- const baseUrl = `http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`;
- const browser = await chromium.launch();
- const context = await browser.newContext({
- colorScheme: "dark",
- deviceScaleFactor: 1,
- viewport: { width: 1440, height: 1000 },
- });
- const page = await context.newPage();
+ let homarrContainer: StartedTestContainer | undefined;
+ let browser: Browser | undefined;
try {
+ homarrContainer = await createHomarrContainer({
+ environment: { AUTH_PROVIDERS: "credentials" },
+ mounts: { "/appdata": localMountPath },
+ }).start();
+ const baseUrl = `http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`;
+ browser = await chromium.launch();
+ const context = await browser.newContext({
+ colorScheme: "dark",
+ deviceScaleFactor: 1,
+ viewport: { width: 1440, height: 1000 },
+ });
+ const page = await context.newPage();
+
await page.goto(`${baseUrl}/auth/login`);
await page.getByLabel("Username").fill(adminCredentials.username);
await page.locator("#password").fill(adminCredentials.password);
@@ -88,8 +93,7 @@ describe.skipIf(process.env.UPDATE_DOCS_SCREENSHOTS !== "true")("Assistant docum
await expect(page.getByRole("button", { name: "Refresh models" })).toBeVisible({ timeout: 15_000 });
await expect(page.getByText("Homarr model")).toBeVisible();
} finally {
- await browser.close();
- await homarrContainer.stop();
+ await Promise.allSettled([browser?.close(), homarrContainer?.stop()]);
}
}, 120_000);
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 00fd327ae..87f061f13 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -885,9 +885,6 @@ importers:
prism-react-renderer:
specifier: ^2.4.1
version: 2.4.1(react@19.2.7)
- random-word-slugs:
- specifier: ^0.1.7
- version: 0.1.7
react:
specifier: 'catalog:'
version: 19.2.7
@@ -14875,9 +14872,6 @@ packages:
radix3@1.1.2:
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
- random-word-slugs@0.1.7:
- resolution: {integrity: sha512-8cyzxOIDeLFvwSPTgCItMXHGT5ZPkjhuFKUTww06Xg1dNMXuGxIKlARvS7upk6JXIm41ZKXmtlKR1iCRWklKmg==}
-
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@@ -30727,8 +30721,6 @@ snapshots:
radix3@1.1.2: {}
- random-word-slugs@0.1.7: {}
-
randombytes@2.1.0:
dependencies:
safe-buffer: 5.2.1