From c093c18bf30852dad8d18fb9bce040b2aa98832a Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:08:28 +0100 Subject: [PATCH 01/32] fix(client): keep space switcher dropdown inside its popover (#2392) --- .../space/components/sidebar/space-select.tsx | 21 ++++++++++++++----- .../space/components/sidebar/switch-space.tsx | 1 + 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/client/src/features/space/components/sidebar/space-select.tsx b/apps/client/src/features/space/components/sidebar/space-select.tsx index dab784103..47c499757 100644 --- a/apps/client/src/features/space/components/sidebar/space-select.tsx +++ b/apps/client/src/features/space/components/sidebar/space-select.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useDebouncedValue } from "@mantine/hooks"; import { Group, Select, SelectProps, Text } from "@mantine/core"; import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts"; @@ -14,6 +14,7 @@ interface SpaceSelectProps { width?: number; opened?: boolean; clearable?: boolean; + withinPortal?: boolean; } const renderSelectOption: SelectProps["renderOption"] = ({ option }) => ( @@ -41,6 +42,7 @@ export function SpaceSelect({ width, opened, clearable, + withinPortal = true, }: SpaceSelectProps) { const { t } = useTranslation(); const [searchValue, setSearchValue] = useState(""); @@ -50,9 +52,13 @@ export function SpaceSelect({ limit: 50, }); const [data, setData] = useState([]); + const fetchedSpaces = useRef(new Map()); useEffect(() => { if (spaces) { + spaces.items.forEach((space: ISpace) => + fetchedSpaces.current.set(space.slug, space), + ); const spaceData = spaces?.items .filter((space: ISpace) => space.slug !== value) .map((space: ISpace) => { @@ -83,14 +89,19 @@ export function SpaceSelect({ onSearchChange={setSearchValue} clearable={clearable} variant="filled" - onChange={(slug) => - onChange(spaces.items?.find((item) => item.slug === slug)) - } + onChange={(slug) => { + // options accumulate across fetches; resolve against everything + // fetched, not just the latest query result + const space = slug && fetchedSpaces.current.get(slug); + if (space) { + onChange(space); + } + }} onClick={(e) => e.stopPropagation()} nothingFoundMessage={t("No space found")} limit={50} checkIconPosition="right" - comboboxProps={{ width, withinPortal: true, position: "bottom", keepMounted: false, dropdownPadding: 0 }} + comboboxProps={{ width, withinPortal, position: "bottom", keepMounted: false, dropdownPadding: 0 }} dropdownOpened={opened} /> ); diff --git a/apps/client/src/features/space/components/sidebar/switch-space.tsx b/apps/client/src/features/space/components/sidebar/switch-space.tsx index 7531a5926..ff07572fd 100644 --- a/apps/client/src/features/space/components/sidebar/switch-space.tsx +++ b/apps/client/src/features/space/components/sidebar/switch-space.tsx @@ -70,6 +70,7 @@ export function SwitchSpace({ onChange={(space) => handleSelect(space.slug)} width={300} opened={true} + withinPortal={false} /> From d136864ef16138695ec581ecee305502105ee1f4 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:46:50 +0100 Subject: [PATCH 02/32] sync --- apps/server/src/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/ee b/apps/server/src/ee index b38dc5ecd..22a5f200d 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit b38dc5ecd10118b687e6cb073cf7202e8c2fe914 +Subproject commit 22a5f200dfb7d042e88c6a1397568da5bdf9477b From 911c1057d632716f79eba7d3bdcf4df940c02229 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:35:37 +0100 Subject: [PATCH 03/32] feat(server): add global encryption module (AES-256-GCM) (#2400) Provides an injectable EncryptionService that encrypts/decrypts strings with AES-256-GCM using a key derived from APP_SECRET with domain separation. --- apps/server/src/app.module.ts | 2 + .../encryption/encryption.errors.ts | 13 ++ .../encryption/encryption.module.ts | 9 + .../encryption/encryption.service.spec.ts | 184 ++++++++++++++++++ .../encryption/encryption.service.ts | 108 ++++++++++ 5 files changed, 316 insertions(+) create mode 100644 apps/server/src/integrations/encryption/encryption.errors.ts create mode 100644 apps/server/src/integrations/encryption/encryption.module.ts create mode 100644 apps/server/src/integrations/encryption/encryption.service.spec.ts create mode 100644 apps/server/src/integrations/encryption/encryption.service.ts diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index b8cfc5877..4f5411eca 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -27,6 +27,7 @@ import { LoggerModule } from './common/logger/logger.module'; import { ClsModule } from 'nestjs-cls'; import { NoopAuditModule } from './integrations/audit/audit.module'; import { ThrottleModule } from './integrations/throttle/throttle.module'; +import { EncryptionModule } from './integrations/encryption/encryption.module'; const enterpriseModules = []; try { @@ -53,6 +54,7 @@ try { CoreModule, DatabaseModule, EnvironmentModule, + EncryptionModule, RedisModule.forRootAsync({ useClass: RedisConfigService, }), diff --git a/apps/server/src/integrations/encryption/encryption.errors.ts b/apps/server/src/integrations/encryption/encryption.errors.ts new file mode 100644 index 000000000..06b5cca81 --- /dev/null +++ b/apps/server/src/integrations/encryption/encryption.errors.ts @@ -0,0 +1,13 @@ +export class UnableToInitialize extends Error { + constructor(message: string) { + super(`Unable to initialize the encryption service: ${message}`); + this.name = 'UnableToInitialize'; + } +} + +export class UnableToDecrypt extends Error { + constructor(reason: string) { + super(`Unable to decrypt the ciphertext: ${reason}`); + this.name = 'UnableToDecrypt'; + } +} diff --git a/apps/server/src/integrations/encryption/encryption.module.ts b/apps/server/src/integrations/encryption/encryption.module.ts new file mode 100644 index 000000000..75022234e --- /dev/null +++ b/apps/server/src/integrations/encryption/encryption.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { EncryptionService } from './encryption.service'; + +@Global() +@Module({ + providers: [EncryptionService], + exports: [EncryptionService], +}) +export class EncryptionModule {} diff --git a/apps/server/src/integrations/encryption/encryption.service.spec.ts b/apps/server/src/integrations/encryption/encryption.service.spec.ts new file mode 100644 index 000000000..64b1a8e00 --- /dev/null +++ b/apps/server/src/integrations/encryption/encryption.service.spec.ts @@ -0,0 +1,184 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EncryptionService } from './encryption.service'; +import { UnableToDecrypt, UnableToInitialize } from './encryption.errors'; +import { EnvironmentService } from '../environment/environment.service'; + +const APP_SECRET = 'test-app-secret-with-plenty-of-entropy-1234567890'; + +const buildService = (appSecret: string | undefined) => { + const env = { getAppSecret: () => appSecret } as EnvironmentService; + return new EncryptionService(env); +}; + +const decodeEnvelope = (encrypted: string) => + JSON.parse(Buffer.from(encrypted, 'base64').toString()) as { + iv: string; + authTag: string; + cipherText: string; + }; + +const encodeEnvelope = (envelope: { + iv: string; + authTag: string; + cipherText: string; +}) => Buffer.from(JSON.stringify(envelope)).toString('base64'); + +describe('EncryptionService', () => { + let service: EncryptionService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + EncryptionService, + { + provide: EnvironmentService, + useValue: { getAppSecret: () => APP_SECRET }, + }, + ], + }).compile(); + + service = module.get(EncryptionService); + }); + + describe('initialization', () => { + it('compiles via Nest DI', () => { + expect(service).toBeDefined(); + }); + + it('throws UnableToInitialize when APP_SECRET is missing', () => { + expect(() => buildService(undefined)).toThrow(UnableToInitialize); + expect(() => buildService('')).toThrow(UnableToInitialize); + }); + }); + + describe('encrypt + decrypt round-trip', () => { + it('decrypts back to the original plaintext', () => { + const plaintext = 'hello world'; + const encrypted = service.encrypt(plaintext); + expect(service.decrypt(encrypted)).toBe(plaintext); + }); + + it('handles empty string', () => { + const encrypted = service.encrypt(''); + expect(service.decrypt(encrypted)).toBe(''); + }); + + it('handles unicode (multi-byte UTF-8)', () => { + const plaintext = 'héllo 🔐 世界'; + const encrypted = service.encrypt(plaintext); + expect(service.decrypt(encrypted)).toBe(plaintext); + }); + + it('handles long plaintext (>1 block)', () => { + const plaintext = 'a'.repeat(10_000); + const encrypted = service.encrypt(plaintext); + expect(service.decrypt(encrypted)).toBe(plaintext); + }); + + it('produces distinct ciphertexts for the same plaintext (random IV)', () => { + const plaintext = 'same input'; + const a = service.encrypt(plaintext); + const b = service.encrypt(plaintext); + expect(a).not.toBe(b); + expect(service.decrypt(a)).toBe(plaintext); + expect(service.decrypt(b)).toBe(plaintext); + }); + }); + + describe('cross-key isolation', () => { + it('cannot decrypt ciphertext produced under a different APP_SECRET', () => { + const other = buildService('totally-different-secret-value-9876543210'); + const encrypted = service.encrypt('secret'); + expect(() => other.decrypt(encrypted)).toThrow(UnableToDecrypt); + }); + }); + + describe('tamper detection', () => { + it('rejects modified ciphertext', () => { + const encrypted = service.encrypt('hello'); + const env = decodeEnvelope(encrypted); + const tamperedCipher = Buffer.from(env.cipherText, 'base64'); + tamperedCipher[0] ^= 0x01; + const tampered = encodeEnvelope({ + ...env, + cipherText: tamperedCipher.toString('base64'), + }); + expect(() => service.decrypt(tampered)).toThrow(UnableToDecrypt); + }); + + it('rejects modified auth tag', () => { + const encrypted = service.encrypt('hello'); + const env = decodeEnvelope(encrypted); + const tamperedTag = Buffer.from(env.authTag, 'base64'); + tamperedTag[0] ^= 0x01; + const tampered = encodeEnvelope({ + ...env, + authTag: tamperedTag.toString('base64'), + }); + expect(() => service.decrypt(tampered)).toThrow(UnableToDecrypt); + }); + + it('rejects modified IV', () => { + const encrypted = service.encrypt('hello'); + const env = decodeEnvelope(encrypted); + const tamperedIV = Buffer.from(env.iv, 'base64'); + tamperedIV[0] ^= 0x01; + const tampered = encodeEnvelope({ + ...env, + iv: tamperedIV.toString('base64'), + }); + expect(() => service.decrypt(tampered)).toThrow(UnableToDecrypt); + }); + }); + + describe('malformed payloads', () => { + it('rejects non-base64 garbage', () => { + expect(() => service.decrypt('!!!not-valid-base64!!!')).toThrow( + UnableToDecrypt, + ); + }); + + it('rejects base64 of non-JSON', () => { + const garbage = Buffer.from('not json at all').toString('base64'); + expect(() => service.decrypt(garbage)).toThrow(UnableToDecrypt); + }); + + it('rejects JSON missing required fields', () => { + const partial = encodeEnvelope({ + iv: Buffer.alloc(12).toString('base64'), + authTag: Buffer.alloc(16).toString('base64'), + } as never); + expect(() => service.decrypt(partial)).toThrow(UnableToDecrypt); + }); + + it('rejects wrong-length IV', () => { + const encrypted = service.encrypt('hello'); + const env = decodeEnvelope(encrypted); + const bad = encodeEnvelope({ + ...env, + iv: Buffer.alloc(8).toString('base64'), + }); + expect(() => service.decrypt(bad)).toThrow(UnableToDecrypt); + }); + + it('rejects wrong-length auth tag', () => { + const encrypted = service.encrypt('hello'); + const env = decodeEnvelope(encrypted); + const bad = encodeEnvelope({ + ...env, + authTag: Buffer.alloc(8).toString('base64'), + }); + expect(() => service.decrypt(bad)).toThrow(UnableToDecrypt); + }); + }); + + describe('envelope format', () => { + it('returns base64 of JSON envelope with iv (12B), authTag (16B), cipherText', () => { + const encrypted = service.encrypt('hello'); + const env = decodeEnvelope(encrypted); + expect(Buffer.from(env.iv, 'base64')).toHaveLength(12); + expect(Buffer.from(env.authTag, 'base64')).toHaveLength(16); + expect(Buffer.from(env.cipherText, 'base64').length).toBeGreaterThan(0); + }); + }); +}); diff --git a/apps/server/src/integrations/encryption/encryption.service.ts b/apps/server/src/integrations/encryption/encryption.service.ts new file mode 100644 index 000000000..63dc4ce99 --- /dev/null +++ b/apps/server/src/integrations/encryption/encryption.service.ts @@ -0,0 +1,108 @@ +// https://github.com/nhedger/nestjs-encryption - MIT +import { Injectable } from '@nestjs/common'; +import { + createCipheriv, + createDecipheriv, + createHash, + randomBytes, +} from 'node:crypto'; +import { UnableToDecrypt, UnableToInitialize } from './encryption.errors'; +import { EnvironmentService } from '../environment/environment.service'; + +const ALGORITHM = 'aes-256-gcm'; +const KEY_DOMAIN = 'docmost:encryption:v1'; +const IV_LENGTH = 12; +const AUTH_TAG_LENGTH = 16; + +type AEADPayload = { + iv: TFormat; + authTag: TFormat; + cipherText: TFormat; +}; + +@Injectable() +export class EncryptionService { + private readonly key: Buffer; + + constructor(environmentService: EnvironmentService) { + const appSecret = environmentService.getAppSecret(); + if (!appSecret) { + throw new UnableToInitialize('APP_SECRET is not set.'); + } + this.key = createHash('sha256') + .update(KEY_DOMAIN) + .update(appSecret) + .digest(); + } + + public encrypt(plaintext: string): string { + const iv = randomBytes(IV_LENGTH); + const cipher = createCipheriv(ALGORITHM, this.key, iv); + const cipherText = Buffer.concat([ + cipher.update(plaintext, 'utf8'), + cipher.final(), + ]); + const authTag = cipher.getAuthTag(); + + const aead: AEADPayload = { + iv: iv.toString('base64'), + authTag: authTag.toString('base64'), + cipherText: cipherText.toString('base64'), + }; + + return Buffer.from(JSON.stringify(aead)).toString('base64'); + } + + public decrypt(encrypted: string): string { + try { + const { iv, authTag, cipherText } = this.decodeAEADPayload(encrypted); + const decipher = createDecipheriv(ALGORITHM, this.key, iv); + decipher.setAuthTag(authTag); + const decrypted = Buffer.concat([ + decipher.update(cipherText), + decipher.final(), + ]); + return decrypted.toString('utf8'); + } catch (e: unknown) { + throw new UnableToDecrypt((e as Error).message); + } + } + + private decodeAEADPayload(encodedPayload: string): AEADPayload { + const payload = Buffer.from(encodedPayload, 'base64'); + + let deserializedPkg: Record; + try { + deserializedPkg = JSON.parse(payload.toString()); + } catch { + throw new Error('The decoded AEAD payload is not a valid JSON string.'); + } + + for (const field of ['iv', 'authTag', 'cipherText']) { + if (!Object.prototype.hasOwnProperty.call(deserializedPkg, field)) { + throw new Error(`The AEAD payload is missing the ${field} field.`); + } + } + + const iv = Buffer.from(deserializedPkg.iv as string, 'base64'); + if (iv.length !== IV_LENGTH) { + throw new Error( + `The decoded IV is not the correct length. Expected ${IV_LENGTH} bytes, got ${iv.length} bytes.`, + ); + } + + const authTag = Buffer.from(deserializedPkg.authTag as string, 'base64'); + if (authTag.length !== AUTH_TAG_LENGTH) { + throw new Error( + `The decoded auth tag is not the correct length. Expected ${AUTH_TAG_LENGTH} bytes, got ${authTag.length} bytes.`, + ); + } + + const cipherText = Buffer.from( + deserializedPkg.cipherText as string, + 'base64', + ); + + return { iv, authTag, cipherText }; + } +} From 232beda471a97938d961e1a7b7fed1453dcaf82e Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:18:16 +0100 Subject: [PATCH 04/32] feat(ee): turbopuffer ai vector search driver (#2402) * feat(ai): add AI_VECTOR_DRIVER and turbopuffer configuration * feat(ai): carry workspace and target space ids on vector lifecycle events * feat(ai): add vector driver interface and turbopuffer request helpers * feat(ai): add pgvector driver behind the vector driver interface * refactor(ai): route vector reads and writes through the vector driver * feat(ai): add turbopuffer vector driver * feat(ai): rebuild turbopuffer namespaces when the embedding model changes * feat(ai): warm the vector namespace cache on session start * fix(ai): harden turbopuffer misconfiguration and reset failure paths * fix(ai): collapse blank-line runs in extracted page text * fix(ai): skip full re-embed when ai search is re-enabled within the delete grace window * sync * fix(ai): store real embedding dimensions instead of serialized vector length * fix(ai): filter search hits by the page's current space at query time * fix(ai): retry the page moved-to-space vector patch job * sync * feat(ai): pre-warm the vector namespace * fix(ai): pass AI_VECTOR_DRIVER through the client build config --- .../src/ee/ai/services/ai-search-service.ts | 8 +++++ .../search/components/search-spotlight.tsx | 15 ++++++++ apps/client/src/lib/config.ts | 4 +++ apps/client/vite.config.ts | 2 ++ apps/server/package.json | 1 + .../src/collaboration/collaboration.util.ts | 3 +- apps/server/src/common/helpers/index.ts | 1 + .../src/common/helpers/text.utils.spec.ts | 14 ++++++++ apps/server/src/common/helpers/text.utils.ts | 3 ++ .../src/core/page/services/page.service.ts | 19 +++++++--- .../src/database/listeners/space.listener.ts | 5 +-- .../src/database/repos/space/space.repo.ts | 1 + .../repos/workspace/workspace.repo.ts | 18 ++++++++++ apps/server/src/ee | 2 +- .../environment/environment.service.ts | 25 +++++++++++++ .../environment/environment.validation.ts | 36 +++++++++++++++++++ .../queue/constants/queue.constants.ts | 1 + .../src/integrations/static/static.module.ts | 4 +++ pnpm-lock.yaml | 17 +++++++++ 19 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 apps/server/src/common/helpers/text.utils.spec.ts create mode 100644 apps/server/src/common/helpers/text.utils.ts diff --git a/apps/client/src/ee/ai/services/ai-search-service.ts b/apps/client/src/ee/ai/services/ai-search-service.ts index 8c2af64a8..d580a2090 100644 --- a/apps/client/src/ee/ai/services/ai-search-service.ts +++ b/apps/client/src/ee/ai/services/ai-search-service.ts @@ -15,6 +15,14 @@ export interface IAiSearchResponse { }>; } +export async function hintVectorCache(): Promise { + try { + await api.post("/ai/vector-cache-hint"); + } catch { + // best-effort cache hint + } +} + export async function aiAnswers( params: IPageSearchParams, onChunk?: (chunk: { content?: string; sources?: any[] }) => void, diff --git a/apps/client/src/features/search/components/search-spotlight.tsx b/apps/client/src/features/search/components/search-spotlight.tsx index 4c5269f15..3ada7ab1f 100644 --- a/apps/client/src/features/search/components/search-spotlight.tsx +++ b/apps/client/src/features/search/components/search-spotlight.tsx @@ -13,11 +13,16 @@ import { SearchResultItem } from "./search-result-item.tsx"; import { AiSearchResult } from "../../../ee/ai/components/ai-search-result.tsx"; import { useHasFeature } from "@/ee/hooks/use-feature"; import { Feature } from "@/ee/features"; +import { useAtomValue } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { hintVectorCache } from "@/ee/ai/services/ai-search-service.ts"; +import { getAiVectorDriver } from "@/lib/config.ts"; interface SearchSpotlightProps { spaceId?: string; } export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { + const workspace = useAtomValue(workspaceAtom); const { t } = useTranslation(); const hasAiFeature = useHasFeature(Feature.AI); const hasAttachmentIndexing = useHasFeature(Feature.ATTACHMENT_INDEXING); @@ -96,6 +101,15 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { /> )); + const handleSpotlightOpen = () => { + if ( + workspace?.settings?.ai?.search === true && + getAiVectorDriver() === "turbopuffer" + ) { + hintVectorCache(); + } + }; + const handleFiltersChange = (newFilters: any) => { setFilters(newFilters); }; @@ -115,6 +129,7 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { { BILLING_TRIAL_DAYS, POSTHOG_HOST, POSTHOG_KEY, + AI_VECTOR_DRIVER, } = loadEnv(mode, envPath, ""); return { @@ -31,6 +32,7 @@ export default defineConfig(({ mode }) => { BILLING_TRIAL_DAYS, POSTHOG_HOST, POSTHOG_KEY, + AI_VECTOR_DRIVER, }, APP_VERSION: JSON.stringify(process.env.npm_package_version), }, diff --git a/apps/server/package.json b/apps/server/package.json index cb32657a7..3e5ae9592 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -66,6 +66,7 @@ "@nestjs/websockets": "11.1.28", "@node-saml/passport-saml": "5.1.0", "@socket.io/redis-adapter": "8.3.0", + "@turbopuffer/turbopuffer": "^2.8.0", "ai": "6.0.134", "ai-sdk-ollama": "3.8.1", "bcrypt": "6.0.0", diff --git a/apps/server/src/collaboration/collaboration.util.ts b/apps/server/src/collaboration/collaboration.util.ts index ed182cf7f..e5c55c77a 100644 --- a/apps/server/src/collaboration/collaboration.util.ts +++ b/apps/server/src/collaboration/collaboration.util.ts @@ -57,6 +57,7 @@ import { JSONContent, } from '@tiptap/core'; import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html'; +import { collapseBlankLines } from '../common/helpers'; // @tiptap/html library works best for generating prosemirror json state but not HTML // see: https://github.com/ueberdosis/tiptap/issues/5352 // see:https://github.com/ueberdosis/tiptap/issues/4089 @@ -146,7 +147,7 @@ export function htmlToJson(html: string) { } export function jsonToText(tiptapJson: JSONContent) { - return generateText(tiptapJson, tiptapExtensions); + return collapseBlankLines(generateText(tiptapJson, tiptapExtensions)); } export function jsonToNode(tiptapJson: JSONContent) { diff --git a/apps/server/src/common/helpers/index.ts b/apps/server/src/common/helpers/index.ts index 80e9a9028..c9fac94c5 100644 --- a/apps/server/src/common/helpers/index.ts +++ b/apps/server/src/common/helpers/index.ts @@ -1,4 +1,5 @@ export * from './utils'; +export * from './text.utils'; export * from './nanoid.utils'; export * from './file.helper'; export * from './constants'; diff --git a/apps/server/src/common/helpers/text.utils.spec.ts b/apps/server/src/common/helpers/text.utils.spec.ts new file mode 100644 index 000000000..6d049a541 --- /dev/null +++ b/apps/server/src/common/helpers/text.utils.spec.ts @@ -0,0 +1,14 @@ +import { collapseBlankLines } from './text.utils'; + +describe('collapseBlankLines', () => { + it.each([ + ['a\n\n\n\nb', 'a\n\nb'], + ['a\n\nb', 'a\n\nb'], + ['a\nb', 'a\nb'], + ['\n\n\n\na\n\n\n', '\n\na\n\n'], + ['no newlines', 'no newlines'], + ['', ''], + ])('collapses %j to %j', (input, expected) => { + expect(collapseBlankLines(input)).toBe(expected); + }); +}); diff --git a/apps/server/src/common/helpers/text.utils.ts b/apps/server/src/common/helpers/text.utils.ts new file mode 100644 index 000000000..8a9cd1a27 --- /dev/null +++ b/apps/server/src/common/helpers/text.utils.ts @@ -0,0 +1,3 @@ +export function collapseBlankLines(text: string): string { + return text.replace(/\n{2,}/g, '\n\n'); +} diff --git a/apps/server/src/core/page/services/page.service.ts b/apps/server/src/core/page/services/page.service.ts index 387abc5cc..cabfb0c74 100644 --- a/apps/server/src/core/page/services/page.service.ts +++ b/apps/server/src/core/page/services/page.service.ts @@ -496,10 +496,21 @@ export class PageService { }, ); - await this.aiQueue.add(QueueJob.PAGE_MOVED_TO_SPACE, { - pageIds: pageIdsToMove, - workspaceId: rootPage.workspaceId, - }); + await this.aiQueue.add( + QueueJob.PAGE_MOVED_TO_SPACE, + { + pageIds: pageIdsToMove, + spaceId, + workspaceId: rootPage.workspaceId, + }, + { + attempts: 2, + backoff: { + type: 'fixed', + delay: 2 * 60 * 1000, + }, + }, + ); } }); diff --git a/apps/server/src/database/listeners/space.listener.ts b/apps/server/src/database/listeners/space.listener.ts index af6e3a222..3a766b6ef 100644 --- a/apps/server/src/database/listeners/space.listener.ts +++ b/apps/server/src/database/listeners/space.listener.ts @@ -8,6 +8,7 @@ import { EnvironmentService } from '../../integrations/environment/environment.s export class SpaceEvent { spaceId: string; + workspaceId: string; } @Injectable() @@ -22,12 +23,12 @@ export class SpaceListener { @OnEvent(EventName.SPACE_DELETED) async handleSpaceDeleted(event: SpaceEvent) { - const { spaceId } = event; + const { spaceId, workspaceId } = event; if (this.isTypesense()) { await this.searchQueue.add(QueueJob.SPACE_DELETED, { spaceId }); } - await this.aiQueue.add(QueueJob.SPACE_DELETED, { spaceId }); + await this.aiQueue.add(QueueJob.SPACE_DELETED, { spaceId, workspaceId }); } isTypesense(): boolean { diff --git a/apps/server/src/database/repos/space/space.repo.ts b/apps/server/src/database/repos/space/space.repo.ts index 905d2fac6..82fc170bd 100644 --- a/apps/server/src/database/repos/space/space.repo.ts +++ b/apps/server/src/database/repos/space/space.repo.ts @@ -230,6 +230,7 @@ export class SpaceRepo { this.eventEmitter.emit(EventName.SPACE_DELETED, { spaceId, + workspaceId, }); } } diff --git a/apps/server/src/database/repos/workspace/workspace.repo.ts b/apps/server/src/database/repos/workspace/workspace.repo.ts index d0f9b5cee..a8323d7ce 100644 --- a/apps/server/src/database/repos/workspace/workspace.repo.ts +++ b/apps/server/src/database/repos/workspace/workspace.repo.ts @@ -211,6 +211,24 @@ export class WorkspaceRepo { .executeTakeFirst(); } + async updateAiEmbeddingFingerprint( + workspaceId: string, + fingerprint: { driver: string; model: string; dimensions: number }, + trx?: KyselyTransaction, + ) { + const db = dbOrTx(this.db, trx); + return db + .updateTable('workspaces') + .set({ + settings: sql`COALESCE(settings, '{}'::jsonb) + || jsonb_build_object('ai', COALESCE(settings->'ai', '{}'::jsonb) + || jsonb_build_object('embedding', ${JSON.stringify(fingerprint)}::text::jsonb))`, + updatedAt: new Date(), + }) + .where('id', '=', workspaceId) + .execute(); + } + async updateSharingSettings( workspaceId: string, prefKey: string, diff --git a/apps/server/src/ee b/apps/server/src/ee index 22a5f200d..660418ac2 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 22a5f200dfb7d042e88c6a1397568da5bdf9477b +Subproject commit 660418ac2c175b37b2491ab3cd2560c2fd406412 diff --git a/apps/server/src/integrations/environment/environment.service.ts b/apps/server/src/integrations/environment/environment.service.ts index 5667bf5a6..c483df626 100644 --- a/apps/server/src/integrations/environment/environment.service.ts +++ b/apps/server/src/integrations/environment/environment.service.ts @@ -310,6 +310,31 @@ export class EnvironmentService { return val === 'true'; } + getAiVectorDriver(): string { + return this.configService + .get('AI_VECTOR_DRIVER', 'pgvector') + .toLowerCase(); + } + + getTurbopufferApiKey(): string { + return this.configService.get('TURBOPUFFER_API_KEY'); + } + + getTurbopufferRegion(): string { + return this.configService.get('TURBOPUFFER_REGION'); + } + + getTurbopufferBaseUrl(): string { + return this.configService.get('TURBOPUFFER_BASE_URL'); + } + + getTurbopufferNamespacePrefix(): string { + return this.configService.get( + 'TURBOPUFFER_NAMESPACE_PREFIX', + 'docmost', + ); + } + getOpenAiApiKey(): string { return this.configService.get('OPENAI_API_KEY'); } diff --git a/apps/server/src/integrations/environment/environment.validation.ts b/apps/server/src/integrations/environment/environment.validation.ts index ef3c420cf..d431d7e3a 100644 --- a/apps/server/src/integrations/environment/environment.validation.ts +++ b/apps/server/src/integrations/environment/environment.validation.ts @@ -5,6 +5,7 @@ import { IsOptional, IsString, IsUrl, + Matches, MinLength, ValidateIf, validateSync, @@ -108,6 +109,41 @@ export class EnvironmentVariables { @IsString() AI_DRIVER: string; + @IsOptional() + @ValidateIf((obj) => obj.AI_VECTOR_DRIVER) + @IsIn(['pgvector', 'turbopuffer']) + @IsString() + AI_VECTOR_DRIVER: string; + + @ValidateIf((obj) => obj.AI_VECTOR_DRIVER === 'turbopuffer') + @IsNotEmpty() + @IsString() + TURBOPUFFER_API_KEY: string; + + @ValidateIf( + (obj) => + obj.AI_VECTOR_DRIVER === 'turbopuffer' && !obj.TURBOPUFFER_BASE_URL, + ) + @IsNotEmpty({ + message: + 'TURBOPUFFER_REGION is required when AI_VECTOR_DRIVER is turbopuffer, unless TURBOPUFFER_BASE_URL is set', + }) + @IsString() + TURBOPUFFER_REGION: string; + + @IsOptional() + @ValidateIf((obj) => obj.TURBOPUFFER_BASE_URL != '' && obj.TURBOPUFFER_BASE_URL != null) + @IsUrl({ protocols: ['http', 'https'], require_tld: false }) + TURBOPUFFER_BASE_URL: string; + + @IsOptional() + @IsString() + @Matches(/^[A-Za-z0-9\-_.]{1,90}$/, { + message: + 'TURBOPUFFER_NAMESPACE_PREFIX may only contain letters, digits, dot, dash, underscore (max 90 chars)', + }) + TURBOPUFFER_NAMESPACE_PREFIX: string; + @IsOptional() @IsString() AI_EMBEDDING_MODEL: string; diff --git a/apps/server/src/integrations/queue/constants/queue.constants.ts b/apps/server/src/integrations/queue/constants/queue.constants.ts index 546553438..8b7c03a1f 100644 --- a/apps/server/src/integrations/queue/constants/queue.constants.ts +++ b/apps/server/src/integrations/queue/constants/queue.constants.ts @@ -61,6 +61,7 @@ export enum QueueJob { WORKSPACE_DELETED = 'workspace-deleted', WORKSPACE_CREATE_EMBEDDINGS = 'workspace-create-embeddings', WORKSPACE_DELETE_EMBEDDINGS = 'workspace-delete-embeddings', + WORKSPACE_RESET_EMBEDDINGS = 'workspace-reset-embeddings', GENERATE_PAGE_EMBEDDINGS = 'generate-page-embeddings', DELETE_PAGE_EMBEDDINGS = 'delete-page-embeddings', diff --git a/apps/server/src/integrations/static/static.module.ts b/apps/server/src/integrations/static/static.module.ts index f0b7e8319..8805804f3 100644 --- a/apps/server/src/integrations/static/static.module.ts +++ b/apps/server/src/integrations/static/static.module.ts @@ -49,6 +49,10 @@ export class StaticModule implements OnModuleInit { : undefined, POSTHOG_HOST: this.environmentService.getPostHogHost(), POSTHOG_KEY: this.environmentService.getPostHogKey(), + AI_VECTOR_DRIVER: + this.environmentService.getAiVectorDriver() === 'turbopuffer' + ? 'turbopuffer' + : undefined, }; const windowScriptContent = ``; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e26d0a45d..3a6ca01a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -593,6 +593,9 @@ importers: '@socket.io/redis-adapter': specifier: 8.3.0 version: 8.3.0(socket.io-adapter@2.5.4(supports-color@10.2.2))(supports-color@10.2.2) + '@turbopuffer/turbopuffer': + specifier: ^2.8.0 + version: 2.8.0 ai: specifier: 6.0.134 version: 6.0.134(zod@4.3.6) @@ -4382,6 +4385,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@turbopuffer/turbopuffer@2.8.0': + resolution: {integrity: sha512-lurmVByLynuWtPtVl7SBEJ0bNakb8bs4KsafKzeLWnXA6BsQiGS9obT/7ExQVb9/yecyRn/grj8VgwUzi2xMyg==} + '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} @@ -5698,6 +5704,7 @@ packages: cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + deprecated: v4 is no longer maintained, upgrade to v5 cron@4.4.0: resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} @@ -8168,6 +8175,9 @@ packages: pako@2.0.3: resolution: {integrity: sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw==} + pako@2.2.0: + resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -14341,6 +14351,11 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@turbopuffer/turbopuffer@2.8.0': + dependencies: + pako: 2.2.0 + undici: 7.29.0 + '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1 @@ -18749,6 +18764,8 @@ snapshots: pako@2.0.3: {} + pako@2.2.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 From 8913d20aa0435e7c1f86146c87d4a42384ae52c4 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:59:00 +0100 Subject: [PATCH 05/32] chore: package updates (#2412) --- apps/client/package.json | 2 +- apps/server/package.json | 20 +- apps/server/src/ee | 2 +- pnpm-lock.yaml | 468 +++++++++++++++------------------------ pnpm-workspace.yaml | 4 +- 5 files changed, 197 insertions(+), 299 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index e5bb5ad5a..cca0ef4be 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -52,7 +52,7 @@ "mantine-form-zod-resolver": "1.3.0", "mermaid": "11.16.1", "mitt": "3.0.1", - "nanoid": "3.3.17", + "nanoid": "3.3.18", "posthog-js": "1.391.2", "react": "19.2.7", "react-clear-modal": "^2.0.18", diff --git a/apps/server/package.json b/apps/server/package.json index 3e5ae9592..94463aacc 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -40,30 +40,30 @@ "@clickhouse/client": "1.18.2", "@docmost/base-formula": "workspace:*", "@docmost/pdf-inspector": "1.9.6", - "@fastify/cookie": "11.0.2", - "@fastify/multipart": "10.0.0", - "@fastify/static": "10.1.2", + "@fastify/cookie": "11.1.2", + "@fastify/multipart": "10.1.1", + "@fastify/static": "10.1.3", "@keyv/redis": "5.1.6", "@langchain/core": "1.1.46", "@langchain/textsplitters": "1.0.1", "@modelcontextprotocol/sdk": "1.30.0", "@nest-lab/throttler-storage-redis": "1.2.0", "@nestjs-labs/nestjs-ioredis": "11.0.4", - "@nestjs/bullmq": "11.0.4", + "@nestjs/bullmq": "11.0.5", "@nestjs/cache-manager": "3.1.3", - "@nestjs/common": "11.1.28", + "@nestjs/common": "11.2.1", "@nestjs/config": "4.0.4", - "@nestjs/core": "11.1.27", + "@nestjs/core": "11.2.1", "@nestjs/event-emitter": "3.1.0", "@nestjs/jwt": "11.0.2", "@nestjs/mapped-types": "2.1.1", "@nestjs/passport": "11.0.5", - "@nestjs/platform-fastify": "11.1.28", - "@nestjs/platform-socket.io": "11.1.28", + "@nestjs/platform-fastify": "11.2.1", + "@nestjs/platform-socket.io": "11.2.1", "@nestjs/schedule": "6.1.3", "@nestjs/terminus": "11.1.1", "@nestjs/throttler": "6.5.0", - "@nestjs/websockets": "11.1.28", + "@nestjs/websockets": "11.2.1", "@node-saml/passport-saml": "5.1.0", "@socket.io/redis-adapter": "8.3.0", "@turbopuffer/turbopuffer": "^2.8.0", @@ -120,7 +120,7 @@ "tmp-promise": "3.0.3", "typesense": "3.0.5", "undici": "7.29.0", - "ws": "8.21.0", + "ws": "8.21.3", "yauzl": "3.4.0", "zod": "4.3.6" }, diff --git a/apps/server/src/ee b/apps/server/src/ee index 660418ac2..e13af0ce0 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 660418ac2c175b37b2491ab3cd2560c2fd406412 +Subproject commit e13af0ce055cdc83962533f0a7865ad60a11e55e diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a6ca01a7..a4dffce5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,12 +7,12 @@ settings: overrides: prosemirror-changeset: 2.4.0 glob: 13.0.6 - ws: 8.21.0 + ws: 8.21.3 dompurify: 3.4.13 mermaid: 11.16.1 undici: 7.29.0 tmp: 0.2.7 - nanoid@^3: 3.3.17 + nanoid@^3: 3.3.18 lodash-es: 4.18.1 express-rate-limit: 8.2.2 flatted: 3.4.2 @@ -362,8 +362,8 @@ importers: specifier: 3.0.1 version: 3.0.1 nanoid: - specifier: 3.3.17 - version: 3.3.17 + specifier: 3.3.18 + version: 3.3.18 posthog-js: specifier: 1.391.2 version: 1.391.2 @@ -436,7 +436,7 @@ importers: version: 19.2.3(@types/react@19.2.17) '@vitejs/plugin-react': specifier: 6.0.1 - version: 6.0.1(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 6.0.1(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) eslint: specifier: 9.28.0 version: 9.28.0(jiti@2.4.2)(supports-color@10.2.2) @@ -478,10 +478,10 @@ importers: version: 8.57.1(eslint@9.28.0(jiti@2.4.2)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) vite: specifier: 8.0.16 - version: 8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + version: 8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 4.1.6 - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0(supports-color@10.2.2))(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0(supports-color@10.2.2))(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) apps/server: dependencies: @@ -516,77 +516,77 @@ importers: specifier: 1.9.6 version: 1.9.6 '@fastify/cookie': - specifier: 11.0.2 - version: 11.0.2 + specifier: 11.1.2 + version: 11.1.2 '@fastify/multipart': - specifier: 10.0.0 - version: 10.0.0 + specifier: 10.1.1 + version: 10.1.1 '@fastify/static': - specifier: 10.1.2 - version: 10.1.2 + specifier: 10.1.3 + version: 10.1.3 '@keyv/redis': specifier: 5.1.6 version: 5.1.6(keyv@5.6.0) '@langchain/core': specifier: 1.1.46 - version: 1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.0)(zod@4.3.6))(ws@8.21.0) + version: 1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.3)(zod@4.3.6))(ws@8.21.3) '@langchain/textsplitters': specifier: 1.0.1 - version: 1.0.1(@langchain/core@1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.0)(zod@4.3.6))(ws@8.21.0)) + version: 1.0.1(@langchain/core@1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.3)(zod@4.3.6))(ws@8.21.3)) '@modelcontextprotocol/sdk': specifier: 1.30.0 version: 1.30.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(zod@4.3.6) '@nest-lab/throttler-storage-redis': specifier: 1.2.0 - version: 1.2.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2))(ioredis@5.10.1(supports-color@10.2.2))(reflect-metadata@0.2.2) + version: 1.2.0(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(@nestjs/throttler@6.5.0(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(reflect-metadata@0.2.2))(ioredis@5.10.1(supports-color@10.2.2))(reflect-metadata@0.2.2) '@nestjs-labs/nestjs-ioredis': specifier: 11.0.4 - version: 11.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(ioredis@5.10.1(supports-color@10.2.2)) + version: 11.0.4(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(ioredis@5.10.1(supports-color@10.2.2)) '@nestjs/bullmq': - specifier: 11.0.4 - version: 11.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(bullmq@5.79.0(supports-color@10.2.2)) + specifier: 11.0.5 + version: 11.0.5(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(bullmq@5.79.0(supports-color@10.2.2)) '@nestjs/cache-manager': specifier: 3.1.3 - version: 3.1.3(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2) + version: 3.1.3(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2) '@nestjs/common': - specifier: 11.1.28 - version: 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + specifier: 11.2.1 + version: 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) '@nestjs/config': specifier: 4.0.4 - version: 4.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(rxjs@7.8.2) + version: 4.0.4(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(rxjs@7.8.2) '@nestjs/core': - specifier: 11.1.27 - version: 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: 11.2.1 + version: 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/event-emitter': specifier: 3.1.0 - version: 3.1.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27) + version: 3.1.0(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1) '@nestjs/jwt': specifier: 11.0.2 - version: 11.0.2(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2)) + version: 11.0.2(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2)) '@nestjs/mapped-types': specifier: 2.1.1 - version: 2.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + version: 2.1.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) '@nestjs/passport': specifier: 11.0.5 - version: 11.0.5(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(passport@0.7.0) + version: 11.0.5(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(passport@0.7.0) '@nestjs/platform-fastify': - specifier: 11.1.28 - version: 11.1.28(@fastify/static@10.1.2)(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27) + specifier: 11.2.1 + version: 11.2.1(@fastify/static@10.1.3)(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1) '@nestjs/platform-socket.io': - specifier: 11.1.28 - version: 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(rxjs@7.8.2)(supports-color@10.2.2) + specifier: 11.2.1 + version: 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(rxjs@7.8.2)(supports-color@10.2.2) '@nestjs/schedule': specifier: 6.1.3 - version: 6.1.3(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27) + version: 6.1.3(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1) '@nestjs/terminus': specifier: 11.1.1 - version: 11.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/throttler': specifier: 6.5.0 - version: 6.5.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2) + version: 6.5.0(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(reflect-metadata@0.2.2) '@nestjs/websockets': - specifier: 11.1.28 - version: 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: 11.2.1 + version: 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(@nestjs/platform-socket.io@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@node-saml/passport-saml': specifier: 5.1.0 version: 5.1.0(supports-color@10.2.2) @@ -676,13 +676,13 @@ importers: version: 5.1.16 nestjs-cls: specifier: 6.2.0 - version: 6.2.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 6.2.0(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: 3.1.2 - version: 3.1.2(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(kysely@0.28.17)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(kysely@0.28.17)(reflect-metadata@0.2.2) nestjs-pino: specifier: 4.6.1 - version: 4.6.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) + version: 4.6.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) nodemailer: specifier: 9.0.1 version: 9.0.1 @@ -756,8 +756,8 @@ importers: specifier: 7.29.0 version: 7.29.0 ws: - specifier: 8.21.0 - version: 8.21.0 + specifier: 8.21.3 + version: 8.21.3 yauzl: specifier: 3.4.0 version: 3.4.0 @@ -776,7 +776,7 @@ importers: version: 11.0.10(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.1.19 - version: 11.1.19(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27) + version: 11.1.19(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1) '@types/bcrypt': specifier: ^6.0.0 version: 6.0.0 @@ -2159,11 +2159,11 @@ packages: '@fastify/busboy@3.1.1': resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} - '@fastify/cookie@11.0.2': - resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} + '@fastify/cookie@11.1.2': + resolution: {integrity: sha512-Dtrpk/YOGUsbRMvP/8ZqPpwnMRv0qSqodFdoQ2B589Obc7jw4s4Qla+cV72Bsm7WsZJnqlYFX/i7uSBq0xzg6g==} - '@fastify/cors@11.2.0': - resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==} + '@fastify/cors@11.3.0': + resolution: {integrity: sha512-ggQGua+xHv1MvePbPr0v//xLYEsCXbWspquXCJS9Ot5YoRXq8J8ZWzHnxDBVnbtXosvistXo6LtNzOJswf64Fw==} '@fastify/deepmerge@3.1.0': resolution: {integrity: sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==} @@ -2183,8 +2183,8 @@ packages: '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} - '@fastify/multipart@10.0.0': - resolution: {integrity: sha512-pUx3Z1QStY7E7kwvDTIvB6P+rF5lzP+iqPgZyJyG3yBJVPvQaZxzDHYbQD89rbY0ciXrMOyGi8ezHDVexLvJDA==} + '@fastify/multipart@10.1.1': + resolution: {integrity: sha512-jyRHgnFVdchZRKjJRf6kGPEiDp3Bg4MLo4d6/krt8Z4RutLrqL5IYWihx8a4tnv7Tu7JfuHcyC+dU9zPzTxiSg==} '@fastify/proxy-addr@5.0.0': resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==} @@ -2192,8 +2192,8 @@ packages: '@fastify/send@4.1.0': resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} - '@fastify/static@10.1.2': - resolution: {integrity: sha512-G/g18cG9tLutT/OVyN1AIsHIl9L1UwmJ+S3dkyhVpplIx0nEMicd7RGQ+uJLyhKKF4a3tTcQydccn3Mop1fX+Q==} + '@fastify/static@10.1.3': + resolution: {integrity: sha512-W6jqajYS974XjPjB5hQWoxPM8NKM4+p8YmQT6G5IbCa4uhdWSVadZUv75siy1wEA/3ty8RYdpBydfWeu9AqAqQ==} '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -2710,18 +2710,18 @@ packages: '@nestjs/core': ^10.0.0 || ^11.0.0 ioredis: ^5.0.0 - '@nestjs/bull-shared@11.0.4': - resolution: {integrity: sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==} + '@nestjs/bull-shared@11.0.5': + resolution: {integrity: sha512-QI7GHPk4oePBwszAZn/Kz5hgB79KijB1RDoo+E/BrSt33sHUrvRZXVyO0+knZleyloRYy0LT2VwsqSP/aQvg4Q==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 - '@nestjs/bullmq@11.0.4': - resolution: {integrity: sha512-wBzK9raAVG0/6NTMdvLGM4/FQ1lsB35/pYS8L6a0SDgkTiLpd7mAjQ8R692oMx5s7IjvgntaZOuTUrKYLNfIkA==} + '@nestjs/bullmq@11.0.5': + resolution: {integrity: sha512-u0VGVc5aER8A+4jEYXDin2rtqIHR8sFL68RUgNDgZslvjdeb1QL4CF+Sr0v4zMXJEfaLSVD606Oq0cryNNCxHg==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 - bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 + bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 '@nestjs/cache-manager@3.1.3': resolution: {integrity: sha512-HMtiOfHz75NZX7mJn1VnZGLSachVI04TnUc5wvEogIaKwk5BDQHgtP5htxreizjv7oxKalJbuTyxtiF6bE+bgQ==} @@ -2745,8 +2745,8 @@ packages: '@swc/core': optional: true - '@nestjs/common@11.1.28': - resolution: {integrity: sha512-bRImsxibie+AM7xjdwcrm/gr5YeacI65kSBNzTufa1Ib5iwziaY/lqMtRh9THq6pbV4e1HP9aI2ZxGUumnmaoQ==} + '@nestjs/common@11.2.1': + resolution: {integrity: sha512-SEgtP+M9DqNhQkgJIlJ3oTp3gemo/8owySovzMGmJj2kcfIH1G6QP45AAb8dE4a3IpVicpUvdDAy7Syk7ebjBw==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -2764,8 +2764,8 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 rxjs: ^7.1.0 - '@nestjs/core@11.1.27': - resolution: {integrity: sha512-K6DX7hcqmZdeXkv7tsPakKBRCgqL19a4mtbX4FluY0hWtFdtPKp6lbe+lb8gWPfvLdbOWr/CPScn7BSjBX+Ecg==} + '@nestjs/core@11.2.1': + resolution: {integrity: sha512-M5PWFU8NdRTgX9Po49d7TQKg7f5t8GAVUa/Esy4tmaMWcKugTzg3ZzpJfD3LEPMuRHjfs6+8pQYgtuP2uz3rDw==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -2812,10 +2812,10 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 passport: ^0.5.0 || ^0.6.0 || ^0.7.0 - '@nestjs/platform-fastify@11.1.28': - resolution: {integrity: sha512-utUfyxRzZsoFxz1GU3Z0OthHpijB605iqtxwFnk9yKowLrU1t1ZkxMUuad/csemImY6AsuaTpTjCecKbmfl+pQ==} + '@nestjs/platform-fastify@11.2.1': + resolution: {integrity: sha512-dX00kr+yAA7Eh8u/LsYxaxdUizmhvaeUT4pbpNTixNDLFBP73XVuatSrgBdmq7HVAsk0mozI/zL/g7R0MSVUrA==} peerDependencies: - '@fastify/static': ^8.0.0 || ^9.0.0 + '@fastify/static': ^10.1.2 '@fastify/view': ^10.0.0 || ^11.0.0 || ^12.0.0 '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -2825,8 +2825,8 @@ packages: '@fastify/view': optional: true - '@nestjs/platform-socket.io@11.1.28': - resolution: {integrity: sha512-vY+GmU2jBcymvgm5rEnftUx4qNxK8cDJmXjl1/1NcpITTNJo0vg07xYR43MwXHcMqe7b0jwqt5+UCTzxqQFIqA==} + '@nestjs/platform-socket.io@11.2.1': + resolution: {integrity: sha512-BZuk8PpZCN5Dp1nXRuzRIMP9GwOH7YkP10nXQDtKnJfJnMhEfi5ANTjHrk/j0qHB/dhxy6WgFzib7ncSg7wMDw==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/websockets': ^11.0.0 @@ -2911,8 +2911,8 @@ packages: '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 reflect-metadata: ^0.1.13 || ^0.2.0 - '@nestjs/websockets@11.1.28': - resolution: {integrity: sha512-jeyclAURCJTN8S8lctDhfLdiJeDKjZmYWWLav653Fb9hl9c+zx5jPhavI8Xk5++R8u+lX9qzaRxtsjEoxTtjyw==} + '@nestjs/websockets@11.2.1': + resolution: {integrity: sha512-HWBSJl4kciGJW+PfSgVUKa2tsrM9IqmjsovGQOla44x6VnE/N57y/dJmEJISaoHUJm+bDm4whO5ggXxQHhLwMw==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -5647,12 +5647,13 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cookie@2.0.1: + resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==} + engines: {node: '>=22'} + cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - copy-anything@2.0.6: - resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} - core-js-compat@3.35.0: resolution: {integrity: sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==} @@ -6200,10 +6201,6 @@ packages: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - errno@0.1.8: - resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} - hasBin: true - error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -6466,8 +6463,8 @@ packages: fastify-plugin@6.0.0: resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==} - fastify@5.10.0: - resolution: {integrity: sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==} + fastify@5.11.3: + resolution: {integrity: sha512-W6hzDP8s0iSeL7LGwY6Oc/ZxuXWOvFEMs6p2L0Si415YRo27W5pBKdOTXxhemBDeSTAcpYf5evRA9onF2OYhPA==} fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -6842,11 +6839,6 @@ packages: engines: {node: '>=18'} hasBin: true - image-size@0.5.5: - resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} - engines: {node: '>=0.10.0'} - hasBin: true - immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} @@ -7049,9 +7041,6 @@ packages: resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} engines: {node: '>= 0.4'} - is-what@3.14.1: - resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} - is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -7482,7 +7471,7 @@ packages: '@opentelemetry/exporter-trace-otlp-proto': '*' '@opentelemetry/sdk-trace-base': '*' openai: '*' - ws: 8.21.0 + ws: 8.21.3 peerDependenciesMeta: '@opentelemetry/api': optional: true @@ -7508,11 +7497,6 @@ packages: leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} - less@4.2.0: - resolution: {integrity: sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==} - engines: {node: '>=6'} - hasBin: true - leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -7729,10 +7713,6 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - make-dir@2.1.0: - resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} - engines: {node: '>=6'} - make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -7819,11 +7799,6 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} engines: {node: '>=4.0.0'} @@ -7897,8 +7872,8 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} - nanoid@3.3.17: - resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -7915,11 +7890,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - needle@3.5.0: - resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==} - engines: {node: '>= 4.4.x'} - hasBin: true - negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -8095,7 +8065,7 @@ packages: resolution: {integrity: sha512-qqjzHls7F5xkXNGy9P1Ei1rorI5LWupUUFWP66zPU8FlZbiITX8SFcHMKNZg/NATJ0LpIZcMUFxSwQmdeQPwSw==} hasBin: true peerDependencies: - ws: 8.21.0 + ws: 8.21.3 zod: ^3.25 || ^4.0 peerDependenciesMeta: ws: @@ -8186,10 +8156,6 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse-node-version@1.0.1: - resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} - engines: {node: '>= 0.10'} - parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -8333,10 +8299,6 @@ packages: resolution: {integrity: sha512-lGA1TNsmy2bxvRsTI2cV01kfTwKzZjnZSDmF9llYNyMHMrU4sP87lQ5taiIKm88L3cbswjl008nwyGc3WpNvzg==} engines: {node: '>=18.0.0'} - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -8555,9 +8517,6 @@ packages: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} - prr@1.0.1: - resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} - psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} @@ -8969,10 +8928,6 @@ packages: selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} - semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -9931,8 +9886,8 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ws@8.21.0: - resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -11908,7 +11863,7 @@ snapshots: jotai-scope: 0.7.2(jotai@2.11.0(@types/react@19.2.17)(react@19.2.7))(react@19.2.7) lodash.debounce: 4.0.8 lodash.throttle: 4.1.1 - nanoid: 3.3.17 + nanoid: 3.3.18 pako: 2.0.3 perfect-freehand: 1.2.0 pica: 7.1.1 @@ -11955,14 +11910,14 @@ snapshots: '@fastify/busboy@3.1.1': {} - '@fastify/cookie@11.0.2': + '@fastify/cookie@11.1.2': dependencies: - cookie: 1.1.1 - fastify-plugin: 5.1.0 + cookie: 2.0.1 + fastify-plugin: 6.0.0 - '@fastify/cors@11.2.0': + '@fastify/cors@11.3.0': dependencies: - fastify-plugin: 5.1.0 + fastify-plugin: 6.0.0 toad-cache: 3.7.0 '@fastify/deepmerge@3.1.0': {} @@ -11984,12 +11939,12 @@ snapshots: dependencies: dequal: 2.0.3 - '@fastify/multipart@10.0.0': + '@fastify/multipart@10.1.1': dependencies: '@fastify/busboy': 3.1.1 '@fastify/deepmerge': 3.1.0 '@fastify/error': 4.0.0 - fastify-plugin: 5.1.0 + fastify-plugin: 6.0.0 secure-json-parse: 4.0.0 '@fastify/proxy-addr@5.0.0': @@ -12005,7 +11960,7 @@ snapshots: http-errors: 2.0.1 mime: 3.0.0 - '@fastify/static@10.1.2': + '@fastify/static@10.1.3': dependencies: '@fastify/accept-negotiator': 2.0.1 '@fastify/error': 4.0.0 @@ -12492,12 +12447,12 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@langchain/core@1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.0)(zod@4.3.6))(ws@8.21.0)': + '@langchain/core@1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.3)(zod@4.3.6))(ws@8.21.3)': dependencies: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 js-tiktoken: 1.0.21 - langsmith: 0.8.9(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.0)(zod@4.3.6))(ws@8.21.0) + langsmith: 0.8.9(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.3)(zod@4.3.6))(ws@8.21.3) mustache: 4.2.0 p-queue: 6.6.2 zod: 4.3.6 @@ -12508,9 +12463,9 @@ snapshots: - openai - ws - '@langchain/textsplitters@1.0.1(@langchain/core@1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.0)(zod@4.3.6))(ws@8.21.0))': + '@langchain/textsplitters@1.0.1(@langchain/core@1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.3)(zod@4.3.6))(ws@8.21.3))': dependencies: - '@langchain/core': 1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.0)(zod@4.3.6))(ws@8.21.0) + '@langchain/core': 1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.3)(zod@4.3.6))(ws@8.21.3) js-tiktoken: 1.0.21 '@lifeomic/attempt@3.1.0': {} @@ -12650,40 +12605,40 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true - '@nest-lab/throttler-storage-redis@1.2.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2))(ioredis@5.10.1(supports-color@10.2.2))(reflect-metadata@0.2.2)': + '@nest-lab/throttler-storage-redis@1.2.0(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(@nestjs/throttler@6.5.0(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(reflect-metadata@0.2.2))(ioredis@5.10.1(supports-color@10.2.2))(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/throttler': 6.5.0(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(reflect-metadata@0.2.2) ioredis: 5.10.1(supports-color@10.2.2) reflect-metadata: 0.2.2 tslib: 2.8.1 - '@nestjs-labs/nestjs-ioredis@11.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(ioredis@5.10.1(supports-color@10.2.2))': + '@nestjs-labs/nestjs-ioredis@11.0.4(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(ioredis@5.10.1(supports-color@10.2.2))': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) ioredis: 5.10.1(supports-color@10.2.2) tslib: 2.8.1 - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)': + '@nestjs/bull-shared@11.0.5(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(bullmq@5.79.0(supports-color@10.2.2))': + '@nestjs/bullmq@11.0.5(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(bullmq@5.79.0(supports-color@10.2.2))': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27) - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/bull-shared': 11.0.5(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) bullmq: 5.79.0(supports-color@10.2.2) tslib: 2.8.1 - '@nestjs/cache-manager@3.1.3(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)': + '@nestjs/cache-manager@3.1.3(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) cache-manager: 7.2.8 keyv: 5.6.0 rxjs: 7.8.2 @@ -12716,7 +12671,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2)': + '@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2)': dependencies: file-type: 21.3.4(supports-color@10.2.2) iterare: 1.2.1 @@ -12731,17 +12686,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.4(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.4(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) dotenv: 17.4.1 dotenv-expand: 12.0.3 lodash: 4.18.1 rxjs: 7.8.2 - '@nestjs/core@11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 8.4.2 @@ -12750,41 +12705,41 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/websockets': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(@nestjs/platform-socket.io@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/event-emitter@3.1.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)': + '@nestjs/event-emitter@3.1.0(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) eventemitter2: 6.4.9 - '@nestjs/jwt@11.0.2(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))': + '@nestjs/jwt@11.0.2(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) '@types/jsonwebtoken': 9.0.10 jsonwebtoken: 9.0.3 - '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.15.1 - '@nestjs/passport@11.0.5(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(passport@0.7.0)': + '@nestjs/passport@11.0.5(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(passport@0.7.0)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) passport: 0.7.0 - '@nestjs/platform-fastify@11.1.28(@fastify/static@10.1.2)(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)': + '@nestjs/platform-fastify@11.2.1(@fastify/static@10.1.3)(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)': dependencies: - '@fastify/cors': 11.2.0 + '@fastify/cors': 11.3.0 '@fastify/formbody': 8.0.2 - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) fast-querystring: 1.1.2 - fastify: 5.10.0 + fastify: 5.11.3 fastify-plugin: 6.0.0 find-my-way: 9.7.0 light-my-request: 6.6.0 @@ -12792,12 +12747,12 @@ snapshots: reusify: 1.1.0 tslib: 2.8.1 optionalDependencies: - '@fastify/static': 10.1.2 + '@fastify/static': 10.1.3 - '@nestjs/platform-socket.io@11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(rxjs@7.8.2)(supports-color@10.2.2)': + '@nestjs/platform-socket.io@11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(rxjs@7.8.2)(supports-color@10.2.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/websockets': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/websockets': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(@nestjs/platform-socket.io@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) rxjs: 7.8.2 socket.io: 4.8.3(supports-color@10.2.2) tslib: 2.8.1 @@ -12806,10 +12761,10 @@ snapshots: - supports-color - utf-8-validate - '@nestjs/schedule@6.1.3(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)': + '@nestjs/schedule@6.1.3(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) cron: 4.4.0 '@nestjs/schematics@11.0.10(chokidar@4.0.3)(typescript@5.9.3)': @@ -12823,38 +12778,38 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/terminus@11.1.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/terminus@11.1.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) boxen: 5.1.2 check-disk-space: 3.4.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 - '@nestjs/testing@11.1.19(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)': + '@nestjs/testing@11.1.19(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/throttler@6.5.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2)': + '@nestjs/throttler@6.5.0(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 - '@nestjs/websockets@11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/websockets@11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(@nestjs/platform-socket.io@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/platform-socket.io': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(rxjs@7.8.2)(supports-color@10.2.2) '@noble/hashes@1.8.0': {} @@ -14926,10 +14881,10 @@ snapshots: '@vercel/oidc@3.1.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/expect@4.1.6': dependencies: @@ -14940,13 +14895,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.6(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.6(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@4.1.6': dependencies: @@ -15783,12 +15738,9 @@ snapshots: cookie@1.1.1: {} - cookiejar@2.1.4: {} + cookie@2.0.1: {} - copy-anything@2.0.6: - dependencies: - is-what: 3.14.1 - optional: true + cookiejar@2.1.4: {} core-js-compat@3.35.0: dependencies: @@ -16312,7 +16264,7 @@ snapshots: '@socket.io/component-emitter': 3.1.0 debug: 4.3.7(supports-color@10.2.2) engine.io-parser: 5.2.2 - ws: 8.21.0 + ws: 8.21.3 xmlhttprequest-ssl: 2.1.2 transitivePeerDependencies: - bufferutil @@ -16332,7 +16284,7 @@ snapshots: cors: 2.8.5 debug: 4.4.3(supports-color@10.2.2) engine.io-parser: 5.2.2 - ws: 8.21.0 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - supports-color @@ -16357,11 +16309,6 @@ snapshots: env-paths@3.0.0: {} - errno@0.1.8: - dependencies: - prr: 1.0.1 - optional: true - error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -16780,7 +16727,7 @@ snapshots: fastify-plugin@6.0.0: {} - fastify@5.10.0: + fastify@5.11.3: dependencies: '@fastify/ajv-compiler': 4.0.5 '@fastify/error': 4.0.0 @@ -17049,7 +16996,7 @@ snapshots: '@types/ws': 8.18.1 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.21.0 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -17219,9 +17166,6 @@ snapshots: image-dimensions@2.5.0: {} - image-size@0.5.5: - optional: true - immediate@3.0.6: {} immutable@4.3.9: {} @@ -17404,9 +17348,6 @@ snapshots: call-bind: 1.0.8 get-intrinsic: 1.3.0 - is-what@3.14.1: - optional: true - is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -17844,7 +17785,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.21.0 + ws: 8.21.3 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -17871,7 +17812,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.21.0 + ws: 8.21.3 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -18018,14 +17959,14 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.0.8 - langsmith@0.8.9(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.0)(zod@4.3.6))(ws@8.21.0): + langsmith@0.8.9(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.21.3)(zod@4.3.6))(ws@8.21.3): dependencies: p-queue: 6.6.2 optionalDependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - openai: 6.2.0(ws@8.21.0)(zod@4.3.6) - ws: 8.21.0 + openai: 6.2.0(ws@8.21.3)(zod@4.3.6) + ws: 8.21.3 layout-base@1.0.2: {} @@ -18037,21 +17978,6 @@ snapshots: leac@0.6.0: {} - less@4.2.0: - dependencies: - copy-anything: 2.0.6 - parse-node-version: 1.0.1 - tslib: 2.8.1 - optionalDependencies: - errno: 0.1.8 - graceful-fs: 4.2.11 - image-size: 0.5.5 - make-dir: 2.1.0 - mime: 1.6.0 - needle: 3.5.0 - source-map: 0.6.1 - optional: true - leven@3.1.0: {} levn@0.4.1: @@ -18222,12 +18148,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - make-dir@2.1.0: - dependencies: - pify: 4.0.1 - semver: 5.7.2 - optional: true - make-dir@4.0.0: dependencies: semver: 7.7.4 @@ -18319,9 +18239,6 @@ snapshots: dependencies: mime-db: 1.54.0 - mime@1.6.0: - optional: true - mime@2.6.0: {} mime@3.0.0: {} @@ -18388,7 +18305,7 @@ snapshots: mute-stream@2.0.0: {} - nanoid@3.3.17: {} + nanoid@3.3.18: {} nanoid@5.1.16: {} @@ -18396,35 +18313,29 @@ snapshots: natural-compare@1.4.0: {} - needle@3.5.0: - dependencies: - iconv-lite: 0.6.3 - sax: 1.6.0 - optional: true - negotiator@0.6.3: {} negotiator@1.0.0: {} neo-async@2.6.2: {} - nestjs-cls@6.2.0(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@6.2.0(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(kysely@0.28.17)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.1)(kysely@0.28.17)(reflect-metadata@0.2.2): dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) - '@nestjs/core': 11.1.27(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/core': 11.2.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) kysely: 0.28.17 reflect-metadata: 0.2.2 - nestjs-pino@4.6.1(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): + nestjs-pino@4.6.1(@nestjs/common@11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) + '@nestjs/common': 11.2.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2) pino: 10.1.0 pino-http: 11.0.0 rxjs: 7.8.2 @@ -18677,9 +18588,9 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 - openai@6.2.0(ws@8.21.0)(zod@4.3.6): + openai@6.2.0(ws@8.21.3)(zod@4.3.6): optionalDependencies: - ws: 8.21.0 + ws: 8.21.3 zod: 4.3.6 optional: true @@ -18777,9 +18688,6 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse-node-version@1.0.1: - optional: true - parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -18918,9 +18826,6 @@ snapshots: picospinner@3.0.0: {} - pify@4.0.1: - optional: true - pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -19045,7 +18950,7 @@ snapshots: postcss@8.5.25: dependencies: - nanoid: 3.3.17 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -19201,9 +19106,6 @@ snapshots: proxy-from-env@2.1.0: {} - prr@1.0.1: - optional: true - psl@1.15.0: dependencies: punycode: 2.3.1 @@ -19704,9 +19606,6 @@ snapshots: dependencies: parseley: 0.12.1 - semver@5.7.2: - optional: true - semver@6.3.1: {} semver@7.7.4: {} @@ -19825,7 +19724,7 @@ snapshots: socket.io-adapter@2.5.4(supports-color@10.2.2): dependencies: debug: 4.3.7(supports-color@10.2.2) - ws: 8.21.0 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - supports-color @@ -20481,7 +20380,7 @@ snapshots: vary@1.1.2: {} - vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -20493,16 +20392,15 @@ snapshots: esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.4.2 - less: 4.2.0 sugarss: 5.0.1(postcss@8.5.25) terser: 5.39.0 tsx: 4.21.0 yaml: 2.8.3 - vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0(supports-color@10.2.2))(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0(supports-color@10.2.2))(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.6(vite@8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.6 '@vitest/runner': 4.1.6 '@vitest/snapshot': 4.1.6 @@ -20519,7 +20417,7 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@22.19.1)(esbuild@0.28.1)(jiti@2.4.2)(sugarss@5.0.1(postcss@8.5.25))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -20720,7 +20618,7 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 - ws@8.21.0: {} + ws@8.21.3: {} xml-crypto@6.1.2: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ba6f1af39..817e408d5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,12 +6,12 @@ patchedDependencies: overrides: prosemirror-changeset: 2.4.0 glob: 13.0.6 - ws: 8.21.0 + ws: 8.21.3 dompurify: 3.4.13 mermaid: 11.16.1 undici: 7.29.0 tmp: 0.2.7 - nanoid@^3: 3.3.17 + nanoid@^3: 3.3.18 lodash-es: 4.18.1 express-rate-limit: 8.2.2 flatted: 3.4.2 From 8c2c49ea6dfd4c995695e8ba43869e9fd099721f Mon Sep 17 00:00:00 2001 From: Michael Lohr Date: Wed, 19 Aug 2026 22:05:44 +0200 Subject: [PATCH 06/32] fix: handle empty password, missing port, and TLS in Redis URL parsing (#2021) * fix: handle empty password, missing port, and TLS in Redis URL parsing --------- Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com> --- apps/server/src/app.module.ts | 15 +++++++++++++-- .../src/collaboration/collaboration.gateway.ts | 1 + apps/server/src/common/helpers/utils.ts | 16 ++++++++++++---- .../src/integrations/health/redis.health.ts | 5 ++++- .../src/integrations/queue/queue.module.ts | 1 + .../integrations/redis/redis-config.service.ts | 1 + apps/server/src/ws/adapter/ws-redis.adapter.ts | 1 + 7 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 4f5411eca..2de94a662 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -22,7 +22,8 @@ import { TelemetryModule } from './integrations/telemetry/telemetry.module'; import { RedisModule } from '@nestjs-labs/nestjs-ioredis'; import { RedisConfigService } from './integrations/redis/redis-config.service'; import { CacheModule } from '@nestjs/cache-manager'; -import KeyvRedis from '@keyv/redis'; +import KeyvRedis, { defaultReconnectStrategy } from '@keyv/redis'; +import { parseRedisUrl } from './common/helpers'; import { LoggerModule } from './common/logger/logger.module'; import { ClsModule } from 'nestjs-cls'; import { NoopAuditModule } from './integrations/audit/audit.module'; @@ -62,10 +63,20 @@ try { isGlobal: true, useFactory: async (environmentService: EnvironmentService) => { const redisUrl = environmentService.getRedisUrl(); + const { family, tls } = parseRedisUrl(redisUrl); return { ttl: 5 * 1000, - stores: [new KeyvRedis(redisUrl)], + stores: [ + new KeyvRedis({ + url: redisUrl, + socket: { + family, + reconnectStrategy: defaultReconnectStrategy, + ...tls, + }, + }), + ], }; }, inject: [EnvironmentService], diff --git a/apps/server/src/collaboration/collaboration.gateway.ts b/apps/server/src/collaboration/collaboration.gateway.ts index b799ce366..05536664e 100644 --- a/apps/server/src/collaboration/collaboration.gateway.ts +++ b/apps/server/src/collaboration/collaboration.gateway.ts @@ -66,6 +66,7 @@ export class CollaborationGateway { password: this.redisConfig.password, db: this.redisConfig.db, family: this.redisConfig.family, + tls: this.redisConfig.tls, retryStrategy: createRetryStrategy(), }), serverId: `collab-${os?.hostname()}-${nanoid(10)}`, diff --git a/apps/server/src/common/helpers/utils.ts b/apps/server/src/common/helpers/utils.ts index 21475c143..100d55d92 100644 --- a/apps/server/src/common/helpers/utils.ts +++ b/apps/server/src/common/helpers/utils.ts @@ -30,13 +30,14 @@ export type RedisConfig = { db: number; password?: string; family?: number; + tls?: { rejectUnauthorized?: boolean }; }; export function parseRedisUrl(redisUrl: string): RedisConfig { - // format - redis[s]://[[username][:password]@][host][:port][/db-number][?family=4|6] + // format - redis[s]://[[username][:password]@][host][:port][/db-number][?family=4|6][&rejectUnauthorized=false] const url = new URL(redisUrl); - const { hostname, port, password, pathname, searchParams } = url; - const portInt = parseInt(port, 10); + const { hostname, port, password, pathname, protocol, searchParams } = url; + const portInt = port ? parseInt(port, 10) : 6379; let db: number = 0; // extract db value if present @@ -54,7 +55,14 @@ export function parseRedisUrl(redisUrl: string): RedisConfig { family = parseInt(familyParam, 10); } - return { host: hostname, port: portInt, password, db, family }; + const tls = + protocol === 'rediss:' + ? searchParams.get('rejectUnauthorized') === 'false' + ? { rejectUnauthorized: false } + : {} + : undefined; + + return { host: hostname, port: portInt, password: password || undefined, db, family, tls }; } export function createRetryStrategy() { diff --git a/apps/server/src/integrations/health/redis.health.ts b/apps/server/src/integrations/health/redis.health.ts index 96ed53b6b..1c9f43611 100644 --- a/apps/server/src/integrations/health/redis.health.ts +++ b/apps/server/src/integrations/health/redis.health.ts @@ -5,6 +5,7 @@ import { import { Injectable, Logger } from '@nestjs/common'; import { EnvironmentService } from '../environment/environment.service'; import { Redis } from 'ioredis'; +import { parseRedisUrl } from '../../common/helpers'; @Injectable() export class RedisHealthIndicator { @@ -19,8 +20,10 @@ export class RedisHealthIndicator { const indicator = this.healthIndicatorService.check(key); try { - const redis = new Redis(this.environmentService.getRedisUrl(), { + const redisUrl = this.environmentService.getRedisUrl(); + const redis = new Redis(redisUrl, { maxRetriesPerRequest: 15, + tls: parseRedisUrl(redisUrl).tls, }); await redis.ping(); diff --git a/apps/server/src/integrations/queue/queue.module.ts b/apps/server/src/integrations/queue/queue.module.ts index eeb74cbb3..fcb317dbf 100644 --- a/apps/server/src/integrations/queue/queue.module.ts +++ b/apps/server/src/integrations/queue/queue.module.ts @@ -18,6 +18,7 @@ import { GeneralQueueProcessor } from './processors/general-queue.processor'; password: redisConfig.password, db: redisConfig.db, family: redisConfig.family, + tls: redisConfig.tls, retryStrategy: createRetryStrategy(), }, defaultJobOptions: { diff --git a/apps/server/src/integrations/redis/redis-config.service.ts b/apps/server/src/integrations/redis/redis-config.service.ts index 719f89f12..7f3e90174 100644 --- a/apps/server/src/integrations/redis/redis-config.service.ts +++ b/apps/server/src/integrations/redis/redis-config.service.ts @@ -19,6 +19,7 @@ export class RedisConfigService implements RedisOptionsFactory { password: redisConfig.password, db: redisConfig.db, family: redisConfig.family, + tls: redisConfig.tls, retryStrategy: createRetryStrategy(), }, }; diff --git a/apps/server/src/ws/adapter/ws-redis.adapter.ts b/apps/server/src/ws/adapter/ws-redis.adapter.ts index a221c84a3..ee1be320c 100644 --- a/apps/server/src/ws/adapter/ws-redis.adapter.ts +++ b/apps/server/src/ws/adapter/ws-redis.adapter.ts @@ -17,6 +17,7 @@ export class WsRedisIoAdapter extends IoAdapter { const options: RedisOptions = { family: this.redisConfig.family, + tls: this.redisConfig.tls, retryStrategy: createRetryStrategy(), }; From ab43031375c05a2142480dacfddc049eeaf751b4 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:46:01 +0100 Subject: [PATCH 07/32] fix race issue --- .../space/services/space-member.service.ts | 32 +++++++++++++------ .../database/repos/space/space-member.repo.ts | 13 ++++++-- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/apps/server/src/core/space/services/space-member.service.ts b/apps/server/src/core/space/services/space-member.service.ts index bc280f252..e9b836f1f 100644 --- a/apps/server/src/core/space/services/space-member.service.ts +++ b/apps/server/src/core/space/services/space-member.service.ts @@ -339,15 +339,25 @@ export class SpaceMemberService { return; } - if (spaceMember.role === SpaceRole.ADMIN) { - await this.validateLastAdmin(dto.spaceId); - } + await executeTx(this.db, async (trx) => { + await trx + .selectFrom('spaces') + .select('id') + .where('id', '=', dto.spaceId) + .forUpdate() + .executeTakeFirst(); - await this.spaceMemberRepo.updateSpaceMember( - { role: dto.role }, - spaceMember.id, - dto.spaceId, - ); + if (spaceMember.role === SpaceRole.ADMIN) { + await this.validateLastAdmin(dto.spaceId, trx); + } + + await this.spaceMemberRepo.updateSpaceMember( + { role: dto.role }, + spaceMember.id, + dto.spaceId, + trx, + ); + }); this.auditService.log({ event: AuditEvent.SPACE_MEMBER_ROLE_CHANGED, @@ -368,10 +378,14 @@ export class SpaceMemberService { }); } - async validateLastAdmin(spaceId: string): Promise { + async validateLastAdmin( + spaceId: string, + trx?: KyselyTransaction, + ): Promise { const spaceOwnerCount = await this.spaceMemberRepo.roleCountBySpaceId( SpaceRole.ADMIN, spaceId, + trx, ); if (spaceOwnerCount === 1) { throw new BadRequestException( diff --git a/apps/server/src/database/repos/space/space-member.repo.ts b/apps/server/src/database/repos/space/space-member.repo.ts index 6711c30c7..d6172e282 100644 --- a/apps/server/src/database/repos/space/space-member.repo.ts +++ b/apps/server/src/database/repos/space/space-member.repo.ts @@ -46,8 +46,10 @@ export class SpaceMemberRepo { updatableSpaceMember: UpdatableSpaceMember, spaceMemberId: string, spaceId: string, + trx?: KyselyTransaction, ): Promise { - await this.db + const db = dbOrTx(this.db, trx); + await db .updateTable('spaceMembers') .set(updatableSpaceMember) .where('id', '=', spaceMemberId) @@ -92,8 +94,13 @@ export class SpaceMemberRepo { .execute(); } - async roleCountBySpaceId(role: string, spaceId: string): Promise { - const { count } = await this.db + async roleCountBySpaceId( + role: string, + spaceId: string, + trx?: KyselyTransaction, + ): Promise { + const db = dbOrTx(this.db, trx); + const { count } = await db .selectFrom('spaceMembers') .select((eb) => eb.fn.count('role').as('count')) .where('role', '=', role) From 66b424a3b88397b11a0c8bcb3cde6e2e6b681634 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:25:45 +0100 Subject: [PATCH 08/32] fix: preserve hash in vimeo embed url --- packages/editor-ext/src/lib/embed-provider.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/editor-ext/src/lib/embed-provider.ts b/packages/editor-ext/src/lib/embed-provider.ts index 4c286a8b8..4c775532a 100644 --- a/packages/editor-ext/src/lib/embed-provider.ts +++ b/packages/editor-ext/src/lib/embed-provider.ts @@ -73,9 +73,13 @@ export const embedProviders: IEmbedProvider[] = [ id: "vimeo", name: "Vimeo", regex: - /^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)/, - getEmbedUrl: (match) => { - return `https://player.vimeo.com/video/${match[4]}`; + /^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:\/([\da-zA-Z]+))?/, + getEmbedUrl: (match, url: string) => { + // preserve ?h= hash for unlisted videos + const hash = + match[5] ?? new URL(url, "https://vimeo.com").searchParams.get("h"); + const base = `https://player.vimeo.com/video/${match[4]}`; + return hash ? `${base}?h=${hash}` : base; }, }, { From 3b858746e3144e3ce06263ab165fe864dae5b513 Mon Sep 17 00:00:00 2001 From: Salihu Date: Fri, 21 Aug 2026 19:21:14 +0100 Subject: [PATCH 09/32] fix: checkbox filtering --- apps/server/src/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/ee b/apps/server/src/ee index e13af0ce0..6dfbcb924 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit e13af0ce055cdc83962533f0a7865ad60a11e55e +Subproject commit 6dfbcb92412008e612c89800125d27c58da2bae5 From b86abd3d40b8767b36e66dc8a0486b0246653328 Mon Sep 17 00:00:00 2001 From: Salihu Date: Fri, 21 Aug 2026 19:23:48 +0100 Subject: [PATCH 10/32] Revert "fix: checkbox filtering" This reverts commit 3b858746e3144e3ce06263ab165fe864dae5b513. --- apps/server/src/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/ee b/apps/server/src/ee index 6dfbcb924..e13af0ce0 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 6dfbcb92412008e612c89800125d27c58da2bae5 +Subproject commit e13af0ce055cdc83962533f0a7865ad60a11e55e From b814bd0f12df5912a3d8b62eb57e92a3aa9b2fc5 Mon Sep 17 00:00:00 2001 From: Salihu Date: Fri, 21 Aug 2026 19:29:10 +0100 Subject: [PATCH 11/32] fix: checkbox filtering --- apps/server/src/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/ee b/apps/server/src/ee index e13af0ce0..6dfbcb924 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit e13af0ce055cdc83962533f0a7865ad60a11e55e +Subproject commit 6dfbcb92412008e612c89800125d27c58da2bae5 From e14f499f3d8c9ad69e096b8ca20e50b98c46478f Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Sat, 22 Aug 2026 03:46:00 +0100 Subject: [PATCH 12/32] fix: pass tls to redis in throttle module (#2415) --- apps/server/src/integrations/throttle/throttle.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/server/src/integrations/throttle/throttle.module.ts b/apps/server/src/integrations/throttle/throttle.module.ts index 42dd0ec4a..827f671c1 100644 --- a/apps/server/src/integrations/throttle/throttle.module.ts +++ b/apps/server/src/integrations/throttle/throttle.module.ts @@ -3,7 +3,7 @@ import { ThrottlerModule } from '@nestjs/throttler'; import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'; import { EnvironmentService } from '../environment/environment.service'; import { EnvironmentModule } from '../environment/environment.module'; -import { parseRedisUrl } from '../../common/helpers'; +import { createRetryStrategy, parseRedisUrl } from '../../common/helpers'; import { AUTH_THROTTLER, AI_CHAT_THROTTLER } from './throttler-names'; import Redis from 'ioredis'; @@ -27,6 +27,8 @@ import Redis from 'ioredis'; password: redisConfig.password, db: redisConfig.db, family: redisConfig.family, + tls: redisConfig.tls, + retryStrategy: createRetryStrategy(), keyPrefix: 'throttle:', }), ), From 549cf7c0053bb4f4c3c4e08d588b1f0c69297daf Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Sat, 22 Aug 2026 13:20:35 +0100 Subject: [PATCH 13/32] fix: skip pgvector table check when enabling AI search on turbopuffer (#2416) --- apps/server/src/core/workspace/services/workspace.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/server/src/core/workspace/services/workspace.service.ts b/apps/server/src/core/workspace/services/workspace.service.ts index 77ccb90c9..68dfd271a 100644 --- a/apps/server/src/core/workspace/services/workspace.service.ts +++ b/apps/server/src/core/workspace/services/workspace.service.ts @@ -396,7 +396,10 @@ export class WorkspaceService { } } - if (updateWorkspaceDto.aiSearch) { + if ( + updateWorkspaceDto.aiSearch && + this.environmentService.getAiVectorDriver() !== 'turbopuffer' + ) { const tableExists = await isPageEmbeddingsTableExists(this.db); if (!tableExists) { throw new BadRequestException( From b7f0b063dc576015482e7aed20357ee014626afa Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:29:21 +0100 Subject: [PATCH 14/32] feat(ee): add enterprise ai chat controls (#2421) * feat(ee): add enterprise ai chat controls * feat: add comment listing tool to ai chat * fix: scope comment endpoints to workspace and skip trashed pages --- .../public/locales/de-DE/translation.json | 2 +- .../public/locales/en-US/translation.json | 10 ++- .../public/locales/es-ES/translation.json | 2 +- .../public/locales/fr-FR/translation.json | 2 +- .../public/locales/it-IT/translation.json | 2 +- .../public/locales/ja-JP/translation.json | 2 +- .../public/locales/ko-KR/translation.json | 2 +- .../public/locales/nl-NL/translation.json | 2 +- .../public/locales/pt-BR/translation.json | 2 +- .../public/locales/ru-RU/translation.json | 2 +- .../public/locales/uk-UA/translation.json | 2 +- .../public/locales/zh-CN/translation.json | 2 +- .../ai-chat/components/ai-chat-read-only.tsx | 74 +++++++++++++++++++ .../ai-chat-workspace-knowledge-only.tsx | 74 +++++++++++++++++++ .../ai-chat/components/chat-empty-state.tsx | 17 ++++- .../src/ee/ai-chat/components/chat-input.tsx | 35 ++++++++- apps/client/src/ee/ai/pages/ai-settings.tsx | 24 +++++- apps/client/src/ee/features.ts | 1 + .../workspace/types/workspace.types.ts | 4 + apps/server/src/common/features.ts | 1 + .../src/core/comment/comment.controller.ts | 17 +++-- .../workspace/dto/update-workspace.dto.ts | 8 ++ .../workspace/services/workspace.service.ts | 49 +++++++++++- apps/server/src/ee | 2 +- 24 files changed, 308 insertions(+), 30 deletions(-) create mode 100644 apps/client/src/ee/ai-chat/components/ai-chat-read-only.tsx create mode 100644 apps/client/src/ee/ai-chat/components/ai-chat-workspace-knowledge-only.tsx diff --git a/apps/client/public/locales/de-DE/translation.json b/apps/client/public/locales/de-DE/translation.json index f067ecc27..084c92ef5 100644 --- a/apps/client/public/locales/de-DE/translation.json +++ b/apps/client/public/locales/de-DE/translation.json @@ -696,7 +696,7 @@ "Upgrade your plan": "Upgrade Ihres Plans", "Available with a paid license": "Verfügbar mit einer kostenpflichtigen Lizenz", "Upgrade your license tier.": "Stufen Sie Ihre Lizenz hoch.", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "KI ist nur in der Docmost Enterprise-Edition verfügbar. Kontaktieren Sie sales@docmost.com.", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "KI ist in den kostenpflichtigen Docmost-Editionen verfügbar. Kontaktieren Sie sales@docmost.com.", "AI & MCP": "KI & MCP", "AI": "KI", "MCP": "MCP", diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 03e10c53e..e5768c7e4 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -698,7 +698,7 @@ "Upgrade your plan": "Upgrade your plan", "Available with a paid license": "Available with a paid license", "Upgrade your license tier.": "Upgrade your license tier.", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI is available in the Docmost paid editions. Contact sales@docmost.com.", "AI & MCP": "AI & MCP", "AI": "AI", "MCP": "MCP", @@ -1302,5 +1302,11 @@ "Error loading attachments.": "Error loading attachments.", "No attachments on this page yet.": "No attachments on this page yet.", "Uploaded by {{name}}": "Uploaded by {{name}}", - "Download {{name}}": "Download {{name}}" + "Download {{name}}": "Download {{name}}", + "Workspace knowledge only": "Workspace knowledge only", + "Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.", + "Toggle workspace knowledge only": "Toggle workspace knowledge only", + "Read-only mode": "Read-only mode", + "AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat can search and read workspace content, but cannot create or edit pages.", + "Toggle AI Chat read-only mode": "Toggle AI Chat read-only mode" } diff --git a/apps/client/public/locales/es-ES/translation.json b/apps/client/public/locales/es-ES/translation.json index fd834a2c6..fb5364725 100644 --- a/apps/client/public/locales/es-ES/translation.json +++ b/apps/client/public/locales/es-ES/translation.json @@ -696,7 +696,7 @@ "Upgrade your plan": "Mejora tu plan", "Available with a paid license": "Disponible con una licencia de pago", "Upgrade your license tier.": "Mejora el nivel de tu licencia.", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "La IA solo está disponible en la edición empresarial de Docmost. Contacte con sales@docmost.com.", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "La IA está disponible en las ediciones de pago de Docmost. Contacte con sales@docmost.com.", "AI & MCP": "IA y MCP", "AI": "IA", "MCP": "MCP", diff --git a/apps/client/public/locales/fr-FR/translation.json b/apps/client/public/locales/fr-FR/translation.json index 04ebbc6ad..e9fbeed06 100644 --- a/apps/client/public/locales/fr-FR/translation.json +++ b/apps/client/public/locales/fr-FR/translation.json @@ -696,7 +696,7 @@ "Upgrade your plan": "Mettez à niveau votre forfait", "Available with a paid license": "Disponible avec une licence payante", "Upgrade your license tier.": "Mettez à niveau votre niveau de licence.", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "L'IA n'est disponible que dans l'édition Entreprise de Docmost. Contactez sales@docmost.com.", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "L'IA est disponible dans les éditions payantes de Docmost. Contactez sales@docmost.com.", "AI & MCP": "IA & MCP", "AI": "IA", "MCP": "MCP", diff --git a/apps/client/public/locales/it-IT/translation.json b/apps/client/public/locales/it-IT/translation.json index e3aaa82ce..81eb1727f 100644 --- a/apps/client/public/locales/it-IT/translation.json +++ b/apps/client/public/locales/it-IT/translation.json @@ -696,7 +696,7 @@ "Upgrade your plan": "Aggiorna il tuo piano", "Available with a paid license": "Disponibile con una licenza a pagamento", "Upgrade your license tier.": "Aggiorna il livello della tua licenza.", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "L'IA è disponibile solo nell'edizione Enterprise di Docmost. Contatta sales@docmost.com.", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "L'IA è disponibile nelle edizioni a pagamento di Docmost. Contatta sales@docmost.com.", "AI & MCP": "IA e MCP", "AI": "IA", "MCP": "MCP", diff --git a/apps/client/public/locales/ja-JP/translation.json b/apps/client/public/locales/ja-JP/translation.json index e408bfda8..c1bfa1652 100644 --- a/apps/client/public/locales/ja-JP/translation.json +++ b/apps/client/public/locales/ja-JP/translation.json @@ -696,7 +696,7 @@ "Upgrade your plan": "プランをアップグレードする", "Available with a paid license": "有料ライセンスで利用可能", "Upgrade your license tier.": "ライセンスタイアをアップグレードしてください。", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI は Docmost のエンタープライズ版でのみ利用可能です。sales@docmost.com までお問い合わせください。", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI は Docmost の有料版で利用可能です。sales@docmost.com までお問い合わせください。", "AI & MCP": "AI と MCP", "AI": "AI", "MCP": "MCP", diff --git a/apps/client/public/locales/ko-KR/translation.json b/apps/client/public/locales/ko-KR/translation.json index fb8e9884a..a7d795454 100644 --- a/apps/client/public/locales/ko-KR/translation.json +++ b/apps/client/public/locales/ko-KR/translation.json @@ -696,7 +696,7 @@ "Upgrade your plan": "요금제를 업그레이드하세요", "Available with a paid license": "유료 라이선스에서만 사용 가능합니다", "Upgrade your license tier.": "라이선스 등급을 업그레이드하세요.", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI는 Docmost 엔터프라이즈 에디션에서만 제공됩니다. sales@docmost.com으로 문의하세요.", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI는 Docmost 유료 에디션에서 제공됩니다. sales@docmost.com으로 문의하세요.", "AI & MCP": "AI 및 MCP", "AI": "AI", "MCP": "MCP", diff --git a/apps/client/public/locales/nl-NL/translation.json b/apps/client/public/locales/nl-NL/translation.json index 59c1d7a73..1f1465ab1 100644 --- a/apps/client/public/locales/nl-NL/translation.json +++ b/apps/client/public/locales/nl-NL/translation.json @@ -696,7 +696,7 @@ "Upgrade your plan": "Upgrade je abonnement", "Available with a paid license": "Beschikbaar met een betaalde licentie", "Upgrade your license tier.": "Upgrade je licentieniveau.", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI is alleen beschikbaar in de Docmost Enterprise-editie. Neem contact op met sales@docmost.com.", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI is beschikbaar in de betaalde edities van Docmost. Neem contact op met sales@docmost.com.", "AI & MCP": "AI & MCP", "AI": "AI", "MCP": "MCP", diff --git a/apps/client/public/locales/pt-BR/translation.json b/apps/client/public/locales/pt-BR/translation.json index 2e4cd89ef..82f213703 100644 --- a/apps/client/public/locales/pt-BR/translation.json +++ b/apps/client/public/locales/pt-BR/translation.json @@ -696,7 +696,7 @@ "Upgrade your plan": "Faça upgrade do seu plano", "Available with a paid license": "Disponível com uma licença paga", "Upgrade your license tier.": "Faça upgrade do seu nível de licença.", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "A IA está disponível apenas na edição empresarial do Docmost. Contate sales@docmost.com.", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "A IA está disponível nas edições pagas do Docmost. Contate sales@docmost.com.", "AI & MCP": "IA e MCP", "AI": "IA", "MCP": "MCP", diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json index bae44d133..9d4ddc8a6 100644 --- a/apps/client/public/locales/ru-RU/translation.json +++ b/apps/client/public/locales/ru-RU/translation.json @@ -696,7 +696,7 @@ "Upgrade your plan": "Обновите свой тарифный план", "Available with a paid license": "Доступно с платной лицензией", "Upgrade your license tier.": "Обновите уровень вашей лицензии.", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "ИИ доступен только в корпоративной версии Docmost. Свяжитесь по адресу sales@docmost.com.", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "ИИ доступен в платных версиях Docmost. Свяжитесь по адресу sales@docmost.com.", "AI & MCP": "ИИ и MCP", "AI": "ИИ", "MCP": "MCP", diff --git a/apps/client/public/locales/uk-UA/translation.json b/apps/client/public/locales/uk-UA/translation.json index d3dd687ea..be09a646f 100644 --- a/apps/client/public/locales/uk-UA/translation.json +++ b/apps/client/public/locales/uk-UA/translation.json @@ -696,7 +696,7 @@ "Upgrade your plan": "Оновіть свій тарифний план", "Available with a paid license": "Доступно за платною ліцензією", "Upgrade your license tier.": "Оновіть рівень своєї ліцензії.", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "ШІ доступний лише в корпоративній редакції Docmost. Зверніться до sales@docmost.com.", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "ШІ доступний у платних редакціях Docmost. Зверніться до sales@docmost.com.", "AI & MCP": "ШІ та MCP", "AI": "ШІ", "MCP": "MCP", diff --git a/apps/client/public/locales/zh-CN/translation.json b/apps/client/public/locales/zh-CN/translation.json index 97ff9ce85..1b273e71c 100644 --- a/apps/client/public/locales/zh-CN/translation.json +++ b/apps/client/public/locales/zh-CN/translation.json @@ -696,7 +696,7 @@ "Upgrade your plan": "升级您的方案", "Available with a paid license": "需付费许可才可用", "Upgrade your license tier.": "升级您的许可等级。", - "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI 仅在 Docmost 企业版中提供。请联系 sales@docmost.com。", + "AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI 在 Docmost 付费版中提供。请联系 sales@docmost.com。", "AI & MCP": "AI 与 MCP", "AI": "AI", "MCP": "MCP", diff --git a/apps/client/src/ee/ai-chat/components/ai-chat-read-only.tsx b/apps/client/src/ee/ai-chat/components/ai-chat-read-only.tsx new file mode 100644 index 000000000..732800299 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/ai-chat-read-only.tsx @@ -0,0 +1,74 @@ +import { Badge, Group, Text, Switch, Tooltip } from "@mantine/core"; +import { useAtom } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts"; +import { notifications } from "@mantine/notifications"; +import { useHasFeature } from "@/ee/hooks/use-feature"; +import { Feature } from "@/ee/features"; +import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label"; + +export default function AiChatReadOnly() { + const { t } = useTranslation(); + const hasAccess = useHasFeature(Feature.AI_CONTROLS); + + return ( + +
+ + {t("Read-only mode")} + {!hasAccess && ( + + {t("Enterprise")} + + )} + + + {t( + "AI Chat can search and read workspace content, but cannot create or edit pages.", + )} + +
+ + +
+ ); +} + +function AiChatReadOnlyToggle() { + const { t } = useTranslation(); + const [workspace, setWorkspace] = useAtom(workspaceAtom); + const [checked, setChecked] = useState( + workspace?.settings?.ai?.chatReadOnly, + ); + const hasAccess = useHasFeature(Feature.AI_CONTROLS); + const upgradeLabel = useUpgradeLabel(); + + const handleChange = async (event: React.ChangeEvent) => { + const value = event.currentTarget.checked; + try { + const updatedWorkspace = await updateWorkspace({ + aiChatReadOnly: value, + }); + setChecked(value); + setWorkspace(updatedWorkspace); + } catch (err: any) { + notifications.show({ + message: err?.response?.data?.message, + color: "red", + }); + } + }; + + return ( + + + + ); +} diff --git a/apps/client/src/ee/ai-chat/components/ai-chat-workspace-knowledge-only.tsx b/apps/client/src/ee/ai-chat/components/ai-chat-workspace-knowledge-only.tsx new file mode 100644 index 000000000..497a337b4 --- /dev/null +++ b/apps/client/src/ee/ai-chat/components/ai-chat-workspace-knowledge-only.tsx @@ -0,0 +1,74 @@ +import { Badge, Group, Text, Switch, Tooltip } from "@mantine/core"; +import { useAtom } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts"; +import { notifications } from "@mantine/notifications"; +import { useHasFeature } from "@/ee/hooks/use-feature"; +import { Feature } from "@/ee/features"; +import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label"; + +export default function AiChatWorkspaceKnowledgeOnly() { + const { t } = useTranslation(); + const hasAccess = useHasFeature(Feature.AI_CONTROLS); + + return ( + +
+ + {t("Workspace knowledge only")} + {!hasAccess && ( + + {t("Enterprise")} + + )} + + + {t( + "Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.", + )} + +
+ + +
+ ); +} + +function AiChatWorkspaceKnowledgeOnlyToggle() { + const { t } = useTranslation(); + const [workspace, setWorkspace] = useAtom(workspaceAtom); + const [checked, setChecked] = useState( + workspace?.settings?.ai?.chatWorkspaceKnowledgeOnly, + ); + const hasAccess = useHasFeature(Feature.AI_CONTROLS); + const upgradeLabel = useUpgradeLabel(); + + const handleChange = async (event: React.ChangeEvent) => { + const value = event.currentTarget.checked; + try { + const updatedWorkspace = await updateWorkspace({ + aiChatWorkspaceKnowledgeOnly: value, + }); + setChecked(value); + setWorkspace(updatedWorkspace); + } catch (err: any) { + notifications.show({ + message: err?.response?.data?.message, + color: "red", + }); + } + }; + + return ( + + + + ); +} diff --git a/apps/client/src/ee/ai-chat/components/chat-empty-state.tsx b/apps/client/src/ee/ai-chat/components/chat-empty-state.tsx index 9583a2f03..423557c53 100644 --- a/apps/client/src/ee/ai-chat/components/chat-empty-state.tsx +++ b/apps/client/src/ee/ai-chat/components/chat-empty-state.tsx @@ -6,7 +6,10 @@ import { IconFileText, } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; -import ChatInput from "./chat-input"; +import { useAtomValue } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { useRef } from "react"; +import ChatInput, { ChatInputHandle } from "./chat-input"; import type { ChatAttachment, PageMention } from "../types/ai-chat.types"; import classes from "../styles/ai-chat.module.css"; @@ -14,6 +17,7 @@ type Suggestion = { icon: React.ReactNode; text: string; prompt: string; + write?: boolean; }; const SUGGESTIONS: Suggestion[] = [ @@ -26,6 +30,7 @@ const SUGGESTIONS: Suggestion[] = [ icon: , text: "Create a new page", prompt: "Create a new page titled ", + write: true, }, { icon: , @@ -36,6 +41,7 @@ const SUGGESTIONS: Suggestion[] = [ icon: , text: "Update page content", prompt: "Update the page @", + write: true, }, ]; @@ -47,9 +53,13 @@ type Props = { export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) { const { t } = useTranslation(); + const workspace = useAtomValue(workspaceAtom); + const writesDisabled = workspace?.settings?.ai?.chatReadOnly === true; + + const inputRef = useRef(null); const handleSuggestionClick = (prompt: string) => { - onSend(prompt, [], []); + inputRef.current?.prefill(prompt); }; return ( @@ -62,6 +72,7 @@ export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) {

{t("Get started")}

- {SUGGESTIONS.map((s) => ( + {SUGGESTIONS.filter((s) => !writesDisabled || !s.write).map((s) => ( + )} + + {!isAiMode && + orderedVisibleFilters.map((filterKey) => { + if (filterKey === "creator") { + return ( + + setOpenedFilter(opened ? "creator" : null) + } + > + + + ); + } + + if (filterKey === "labels") { + return ( + + setOpenedFilter(opened ? "labels" : null) + } + > + + + ); + } + + return null; + })} + + {!isAiMode && addableFilters.length > 0 && ( + + + + + + {addableFilters.map((filter) => ( + } + onClick={() => revealFilter(filter.key)} + > + {filter.label} + + ))} + + + )}
); } diff --git a/apps/client/src/features/search/components/search-spotlight.tsx b/apps/client/src/features/search/components/search-spotlight.tsx index 3ada7ab1f..4e7977650 100644 --- a/apps/client/src/features/search/components/search-spotlight.tsx +++ b/apps/client/src/features/search/components/search-spotlight.tsx @@ -1,7 +1,7 @@ import { Spotlight } from "@mantine/spotlight"; import { IconSearch, IconSparkles } from "@tabler/icons-react"; -import { Group, Button, VisuallyHidden } from "@mantine/core"; -import React, { useState, useMemo, useEffect } from "react"; +import { Group, Button, VisuallyHidden, Text } from "@mantine/core"; +import React, { useState, useMemo, useEffect, useCallback } from "react"; import { useDebouncedValue } from "@mantine/hooks"; import { useTranslation } from "react-i18next"; import { notifications } from "@mantine/notifications"; @@ -31,6 +31,9 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { const [filters, setFilters] = useState<{ spaceId?: string | null; contentType?: string; + creatorId?: string | null; + labelIds?: string[]; + titleOnly?: boolean; }>({ contentType: "page", }); @@ -48,10 +51,25 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { params.spaceId = filters.spaceId; } + if (filters.creatorId) { + params.creatorId = filters.creatorId; + } + + if (filters.labelIds?.length) { + params.labelIds = filters.labelIds; + } + + if (filters.titleOnly) { + params.titleOnly = true; + } + return params; }, [debouncedSearchQuery, filters]); - const { data: searchResults, isLoading } = useUnifiedSearch( + const { + data: searchResults, + isFetching, + } = useUnifiedSearch( searchParams, !isAiMode // Disable regular search when in AI mode ); @@ -88,6 +106,11 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { } }, [aiSearchError, t]); + const isFilterBrowse = + (filters.labelIds?.length ?? 0) > 0 || !!filters.creatorId; + // while the debounce is pending the empty list is not a settled "no results" + const isQuerySettled = query === debouncedSearchQuery; + // Determine result type for rendering const isAttachmentSearch = filters.contentType === "attachment" && hasAttachmentIndexing; @@ -110,9 +133,9 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { } }; - const handleFiltersChange = (newFilters: any) => { + const handleFiltersChange = useCallback((newFilters: any) => { setFilters(newFilters); - }; + }, [setFilters]); const handleAskClick = () => { setIsAiMode(!isAiMode); @@ -182,7 +205,7 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { ? query.length > 0 && !isAiLoading && !aiSearchResult ? t("No answer available") : "" - : query.length > 0 && !isLoading + : (query.length > 0 || isFilterBrowse) && !isFetching ? resultItems.length === 0 ? t("No results found") : t("{{count}} results found", { count: resultItems.length }) @@ -209,15 +232,28 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { ) : ( <> - {query.length === 0 && resultItems.length === 0 && ( + {query.length === 0 && !isFilterBrowse && resultItems.length === 0 && ( {t("Start typing to search...")} )} - {query.length > 0 && !isLoading && resultItems.length === 0 && ( - {t("No results found...")} - )} + {(query.length > 0 || isFilterBrowse) && + !isFetching && + isQuerySettled && + resultItems.length === 0 && ( + {t("No results found...")} + )} {resultItems.length > 0 && <>{resultItems}} + + {(query.length > 0 || isFilterBrowse) && + isFetching && + resultItems.length === 0 && ( + + + {t("Searching...")} + + + )} )} diff --git a/apps/client/src/features/search/hooks/use-unified-search.ts b/apps/client/src/features/search/hooks/use-unified-search.ts index 5d294f4bd..9270477b9 100644 --- a/apps/client/src/features/search/hooks/use-unified-search.ts +++ b/apps/client/src/features/search/hooks/use-unified-search.ts @@ -39,6 +39,20 @@ export function useUnifiedSearch( return await searchPage(backendParams); } }, - enabled: !!params.query && enabled, + enabled: + (!!params.query || + (params.labelIds?.length ?? 0) > 0 || + !!params.creatorId) && + enabled, + // keep previous results only within the same search type; page results + // rendered as attachments (or vice versa) crash on missing fields + placeholderData: (previousData, previousQuery) => { + if (!params.query && !params.labelIds?.length && !params.creatorId) + return undefined; + if (previousQuery && previousQuery.queryKey[1] !== searchType) { + return undefined; + } + return previousData; + }, }); } diff --git a/apps/client/src/features/search/types/search.types.ts b/apps/client/src/features/search/types/search.types.ts index 9962b9ca2..60ae5d985 100644 --- a/apps/client/src/features/search/types/search.types.ts +++ b/apps/client/src/features/search/types/search.types.ts @@ -36,6 +36,9 @@ export interface IPageSearchParams { query: string; spaceId?: string; shareId?: string; + creatorId?: string; + labelIds?: string[]; + titleOnly?: boolean; } export interface IAttachmentSearch { diff --git a/apps/client/src/styles/a11y-overrides.css b/apps/client/src/styles/a11y-overrides.css index 25c1eafe7..1fb4264d9 100644 --- a/apps/client/src/styles/a11y-overrides.css +++ b/apps/client/src/styles/a11y-overrides.css @@ -33,3 +33,31 @@ color: var(--mantine-color-dimmed); -webkit-text-fill-color: var(--mantine-color-dimmed); } + +/* Spotlight's selected action ships as primary-filled blue with white text, + * but our custom action children (gray badge, dimmed snippet, gray icons) + * keep their light-surface colors on it and drop below WCAG AA 4.5:1 + * (WCAG 1.4.3). Use a gray selection one step above the gray-0/dark-6 + * hover instead - it matches the app's selection styling (we use no blue + * fills) and keeps every child at its already-passing resting contrast. + */ +.mantine-Spotlight-action[data-selected] { + background-color: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); + color: var(--mantine-color-text); + --action-description-color: var(--mantine-color-dimmed); + --action-description-opacity: 1; +} + +/* Result actions are tab stops (tabIndex restored via ref); give keyboard + * focus the same gray treatment as arrow-key selection so both navigation + * modes read identically (WCAG 2.4.7 focus visible). + */ +.mantine-Spotlight-action:focus-visible { + background-color: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); +} diff --git a/apps/server/src/core/attachment/services/attachment.service.ts b/apps/server/src/core/attachment/services/attachment.service.ts index 710f9d10b..2db13c817 100644 --- a/apps/server/src/core/attachment/services/attachment.service.ts +++ b/apps/server/src/core/attachment/services/attachment.service.ts @@ -116,8 +116,8 @@ export class AttachmentService { }); } - // Only index PDFs and DOCX files - if (['.pdf', '.docx'].includes(attachment.fileExt.toLowerCase())) { + // Only index PDF, DOCX and TXT files + if (['.pdf', '.docx', '.txt'].includes(attachment.fileExt.toLowerCase())) { await this.attachmentQueue.add( QueueJob.ATTACHMENT_INDEX_CONTENT, { diff --git a/apps/server/src/core/label/label.service.ts b/apps/server/src/core/label/label.service.ts index f0d63eba6..9ec0ab300 100644 --- a/apps/server/src/core/label/label.service.ts +++ b/apps/server/src/core/label/label.service.ts @@ -7,12 +7,15 @@ import { executeTx } from '@docmost/db/utils'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo'; import { normalizeLabelName } from './utils'; +import { EventEmitter2 } from "@nestjs/event-emitter"; +import { EventName } from "src/common/events/event.contants"; @Injectable() export class LabelService { constructor( private readonly labelRepo: LabelRepo, private readonly pagePermissionRepo: PagePermissionRepo, + private readonly eventEmitter: EventEmitter2, @InjectKysely() private readonly db: KyselyDB, ) {} @@ -34,6 +37,12 @@ export class LabelService { attached.push(label); } }); + + this.eventEmitter.emit(EventName.PAGE_UPDATED, { + pageIds: [pageId], + workspaceId: workspaceId, + }); + return attached; } @@ -64,6 +73,11 @@ export class LabelService { await this.labelRepo.deleteLabel(labelId, workspaceId, trx); } }); + + this.eventEmitter.emit(EventName.PAGE_UPDATED, { + pageIds: [pageId], + workspaceId: workspaceId, + }); } async getPageLabels(pageId: string, pagination: PaginationOptions) { diff --git a/apps/server/src/core/search/dto/search.dto.ts b/apps/server/src/core/search/dto/search.dto.ts index 8be6d338d..89fb1b289 100644 --- a/apps/server/src/core/search/dto/search.dto.ts +++ b/apps/server/src/core/search/dto/search.dto.ts @@ -1,4 +1,5 @@ import { + IsArray, IsBoolean, IsNotEmpty, IsNumber, @@ -8,9 +9,9 @@ import { } from 'class-validator'; export class SearchDTO { - @IsNotEmpty() + @IsOptional() @IsString() - query: string; + query?: string; @IsOptional() @IsUUID() @@ -24,6 +25,15 @@ export class SearchDTO { @IsUUID() creatorId?: string; + @IsOptional() + @IsArray() + @IsUUID('all', { each: true }) + labelIds?: string[]; + + @IsOptional() + @IsBoolean() + titleOnly?: boolean; + @IsOptional() @IsNumber() limit?: number; diff --git a/apps/server/src/core/search/search.service.ts b/apps/server/src/core/search/search.service.ts index 9883b2654..3db29b274 100644 --- a/apps/server/src/core/search/search.service.ts +++ b/apps/server/src/core/search/search.service.ts @@ -29,12 +29,36 @@ export class SearchService { workspaceId: string; }, ): Promise<{ items: SearchResponseDto[] }> { - const { query } = searchParams; + const query = searchParams.query?.trim() ?? ''; + const labelIds = [...new Set(searchParams.labelIds ?? [])]; + // selected filters (labels, creator) are browsable without a query + const browseByFilters = + query.length < 1 && + (labelIds.length > 0 || Boolean(searchParams.creatorId)); - if (query.length < 1) { + if (query.length < 1 && !browseByFilters) { return { items: [] }; } - const searchQuery = tsquery(query.trim() + '*'); + const searchQuery = tsquery(query + '*'); + const titleOnly = searchParams.titleOnly === true; + const titleQuery = query; + // escape LIKE wildcards; ranking keeps the raw query + const titleLikeQuery = query.replace(/[\\%_]/g, '\\$&'); + + const rankColumn = browseByFilters + ? sql`0`.as('rank') + : titleOnly + ? sql`word_similarity(lower(f_unaccent(${titleQuery})), lower(f_unaccent(pages.title)))`.as( + 'rank', + ) + : sql`ts_rank(tsv, to_tsquery('english', f_unaccent(${searchQuery})))`.as( + 'rank', + ); + const highlightColumn = browseByFilters || titleOnly + ? sql`''`.as('highlight') + : sql`ts_headline('english', text_content, to_tsquery('english', f_unaccent(${searchQuery})),'MinWords=9, MaxWords=10, MaxFragments=3')`.as( + 'highlight', + ); let queryResults = this.db .selectFrom('pages') @@ -47,23 +71,41 @@ export class SearchService { 'creatorId', 'createdAt', 'updatedAt', - sql`ts_rank(tsv, to_tsquery('english', f_unaccent(${searchQuery})))`.as( - 'rank', - ), - sql`ts_headline('english', text_content, to_tsquery('english', f_unaccent(${searchQuery})),'MinWords=9, MaxWords=10, MaxFragments=3')`.as( - 'highlight', - ), + rankColumn, + highlightColumn, ]) - .where( - 'tsv', - '@@', - sql`to_tsquery('english', f_unaccent(${searchQuery}))`, + .$if(!browseByFilters && !titleOnly, (qb) => + qb.where( + 'tsv', + '@@', + sql`to_tsquery('english', f_unaccent(${searchQuery}))`, + ), + ) + .$if(!browseByFilters && titleOnly, (qb) => + qb.where((eb) => + eb( + sql`lower(f_unaccent(pages.title))`, + 'like', + sql`lower(f_unaccent(${`%${titleLikeQuery}%`}))`, + ), + ), ) .$if(Boolean(searchParams.creatorId), (qb) => qb.where('creatorId', '=', searchParams.creatorId), ) + .$if(labelIds?.length > 0, (qb) => + qb.where( + 'id', + 'in', + this.db + .selectFrom('pageLabels') + .select('pageId') + .where('labelId', 'in', labelIds), + ), + ) .where('deletedAt', 'is', null) - .orderBy('rank', 'desc') + .$if(browseByFilters, (qb) => qb.orderBy('updatedAt', 'desc')) + .$if(!browseByFilters, (qb) => qb.orderBy('rank', 'desc')) .limit(searchParams.limit || 25) .offset(searchParams.offset || 0); @@ -71,8 +113,7 @@ export class SearchService { queryResults = queryResults.select((eb) => this.pageRepo.withSpace(eb)); } - if (searchParams.spaceId) { - // search by spaceId + if (searchParams.spaceId && opts.userId) { queryResults = queryResults.where('spaceId', '=', searchParams.spaceId); } else if (opts.userId && !searchParams.spaceId) { // only search spaces the user is a member of diff --git a/apps/server/src/core/share/share.service.ts b/apps/server/src/core/share/share.service.ts index 03ee31555..e567e6caa 100644 --- a/apps/server/src/core/share/share.service.ts +++ b/apps/server/src/core/share/share.service.ts @@ -46,8 +46,9 @@ export class ShareService { throw new NotFoundException('Share not found'); } - const isRestricted = - await this.pagePermissionRepo.hasRestrictedAncestor(share.pageId); + const isRestricted = await this.pagePermissionRepo.hasRestrictedAncestor( + share.pageId, + ); if (isRestricted) { throw new NotFoundException('Share not found'); } @@ -110,6 +111,9 @@ export class ShareService { } async getSharedPage(dto: ShareInfoDto, workspaceId: string) { + //TODO: we should resolve the page from the share id + if (!dto.pageId) throw new NotFoundException('Shared page not found'); + const share = await this.getShareForPage(dto.pageId, workspaceId); if (!share) { @@ -126,8 +130,9 @@ export class ShareService { } // Block access to restricted pages - const isRestricted = - await this.pagePermissionRepo.hasRestrictedAncestor(page.id); + const isRestricted = await this.pagePermissionRepo.hasRestrictedAncestor( + page.id, + ); if (isRestricted) { throw new NotFoundException('Shared page not found'); } diff --git a/apps/server/src/database/migrations/20260824T211732-page-title-trgm-index.ts b/apps/server/src/database/migrations/20260824T211732-page-title-trgm-index.ts new file mode 100644 index 000000000..3cadad07b --- /dev/null +++ b/apps/server/src/database/migrations/20260824T211732-page-title-trgm-index.ts @@ -0,0 +1,17 @@ +import { type Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE INDEX IF NOT EXISTS pages_title_trgm_idx ON pages USING gin (lower(f_unaccent(title)) gin_trgm_ops)`.execute( + db, + ); + + // separators normalized to spaces so space-typed queries match How_to_export.pdf + await sql`CREATE INDEX IF NOT EXISTS attachments_file_name_trgm_idx ON attachments USING gin (lower(f_unaccent(translate(file_name, '_.-', ' '))) gin_trgm_ops)`.execute( + db, + ); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX IF EXISTS attachments_file_name_trgm_idx`.execute(db); + await sql`DROP INDEX IF EXISTS pages_title_trgm_idx`.execute(db); +} diff --git a/apps/server/src/ee b/apps/server/src/ee index 62f745608..b51a46ef1 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 62f7456089d97ceedbcbad1ceabce09a4c7e1301 +Subproject commit b51a46ef104cb407a670462a36be79ef0da94f97 From 3a25336d3d6d4a7e9b5884fbdac1bf6c37ae4e2e Mon Sep 17 00:00:00 2001 From: Salihu <91833785+salihudickson@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:39:55 +0100 Subject: [PATCH 17/32] fix: display subpages correctly on first load (#2423) --- .../editor/components/subpages/subpages-view.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/client/src/features/editor/components/subpages/subpages-view.tsx b/apps/client/src/features/editor/components/subpages/subpages-view.tsx index cfea74e10..4264a1021 100644 --- a/apps/client/src/features/editor/components/subpages/subpages-view.tsx +++ b/apps/client/src/features/editor/components/subpages/subpages-view.tsx @@ -13,14 +13,17 @@ import { import { useTranslation } from "react-i18next"; import { sortPositionKeys } from "@/features/page/tree/utils/utils"; import { useSharedPageSubpages } from "@/features/share/hooks/use-shared-page-subpages"; +import { extractPageSlugId } from "@/lib"; export default function SubpagesView(props: NodeViewProps) { const { editor } = props; - const { spaceSlug, shareId } = useParams(); + const { spaceSlug, shareId, pageSlug } = useParams(); const { t } = useTranslation(); - - //@ts-ignore - const currentPageId = editor.storage.pageId; + + // @ts-ignore + const storagePageId = editor.storage.pageId; + const routePageId = extractPageSlugId(pageSlug); + const currentPageId = storagePageId ?? routePageId; // Get subpages from shared tree if we're in a shared context const sharedSubpages = useSharedPageSubpages(currentPageId); From 7bd73f77a4a5e15f5f84eca4e2d4361908c95899 Mon Sep 17 00:00:00 2001 From: Salihu <91833785+salihudickson@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:43:19 +0100 Subject: [PATCH 18/32] feat: lightbox media display (#2420) * lightbox media display init * revert uninteded changes * load page media more accurately * restrict lock file changes to installed packages * revert changes in lock file * update lockfile * - support diagrams - support readonly mode - use softer backdrop - other enhancments --------- Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com> --- apps/client/package.json | 1 + .../public/locales/en-US/translation.json | 1 + .../src/features/editor/atoms/editor-atoms.ts | 8 + .../components/bubble-menu/bubble-menu.tsx | 31 ++- .../components/bubble-menu/color-selector.tsx | 161 ++++++++-------- .../components/bubble-menu/link-selector.tsx | 2 +- .../bubble-menu/text-alignment-selector.tsx | 7 +- .../components/common/lightbox-view.tsx | 176 ++++++++++++++++++ .../editor/components/drawio/drawio-menu.tsx | 21 +++ .../components/excalidraw/excalidraw-menu.tsx | 21 +++ .../editor/components/image/image-menu.tsx | 21 +++ .../components/subpages/subpages-view.tsx | 8 +- .../editor/components/video/video-menu.tsx | 21 +++ .../src/features/editor/page-editor.tsx | 31 +++ .../features/editor/readonly-page-editor.tsx | 48 ++++- apps/server/src/ee | 2 +- pnpm-lock.yaml | 25 +++ 17 files changed, 489 insertions(+), 96 deletions(-) create mode 100644 apps/client/src/features/editor/components/common/lightbox-view.tsx diff --git a/apps/client/package.json b/apps/client/package.json index cca0ef4be..d836d5db6 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -64,6 +64,7 @@ "react-router-dom": "7.18.2", "semver": "7.7.4", "socket.io-client": "4.8.3", + "yet-another-react-lightbox": "^3.32.2", "zod": "4.3.6" }, "devDependencies": { diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 14bb06d85..098fb45dd 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -294,6 +294,7 @@ "Export space": "Export space", "Export {{type}}": "Export {{type}}", "File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit", + "Media": "Media", "Align left": "Align left", "Align right": "Align right", "Align center": "Align center", diff --git a/apps/client/src/features/editor/atoms/editor-atoms.ts b/apps/client/src/features/editor/atoms/editor-atoms.ts index 74692d916..2de76fe70 100644 --- a/apps/client/src/features/editor/atoms/editor-atoms.ts +++ b/apps/client/src/features/editor/atoms/editor-atoms.ts @@ -16,6 +16,14 @@ export const showAiMenuAtom = atom(false); export const showLinkMenuAtom = atom(false); +export type LightboxRequest = { + src: string; + type: "image" | "video"; +} | null; + +const initialLightboxRequest: LightboxRequest = null; +export const lightboxRequestAtom = atom(initialLightboxRequest); + // Current page's edit mode — initialized from the user's saved preference on // first load, can be toggled locally without persisting to the server. export const currentPageEditModeAtom = atom(PageEditMode.Edit); diff --git a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx index a1283e34a..02108292e 100644 --- a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx @@ -23,11 +23,21 @@ import { } from "@/features/comment/atoms/comment-atom"; import { useAtom, useAtomValue } from "jotai"; import { v7 as uuid7 } from "uuid"; -import { isCellSelection, isEditorReady, isTextSelected } from "@docmost/editor-ext"; +import { + isCellSelection, + isEditorReady, + isTextSelected, +} from "@docmost/editor-ext"; import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx"; import { useTranslation } from "react-i18next"; -import { showAiMenuAtom, showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms"; -import { userAtom, workspaceAtom } from "@/features/user/atoms/current-user-atom"; +import { + showAiMenuAtom, + showLinkMenuAtom, +} from "@/features/editor/atoms/editor-atoms"; +import { + userAtom, + workspaceAtom, +} from "@/features/user/atoms/current-user-atom"; export interface BubbleMenuItem { name: string; @@ -217,7 +227,12 @@ export const EditorBubbleMenu: FC = (props) => { {items.map((item, index) => ( - + = (props) => { aria-label={t(item.name)} className={clsx({ [classes.active]: item.isActive() })} style={{ border: "none" }} - onClick={() => isEditorReady(props.editor) && item.command()} + onClick={() => + isEditorReady(props.editor) && item.command() + } > @@ -256,7 +273,9 @@ export const EditorBubbleMenu: FC = (props) => { radius="6px" aria-label={t(commentItem.name)} style={{ border: "none" }} - onClick={() => isEditorReady(props.editor) && commentItem.command()} + onClick={() => + isEditorReady(props.editor) && commentItem.command() + } > diff --git a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx index a228dc645..533bcda8a 100644 --- a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx @@ -129,8 +129,7 @@ function handleColorKeyNav( grid: "text" | "highlight", ) { const cols = COLOR_GRID_COLS; - const total = - grid === "text" ? TEXT_COLORS.length : HIGHLIGHT_COLORS.length; + const total = grid === "text" ? TEXT_COLORS.length : HIGHLIGHT_COLORS.length; const col = index % cols; if (e.key === "ArrowRight") { @@ -163,8 +162,7 @@ function handleColorKeyNav( if (prev >= 0) { focusSwatch(grid, prev); } else if (grid === "highlight") { - const lastRowStart = - Math.floor((TEXT_COLORS.length - 1) / cols) * cols; + const lastRowStart = Math.floor((TEXT_COLORS.length - 1) / cols) * cols; focusSwatch("text", Math.min(lastRowStart + col, TEXT_COLORS.length - 1)); } return; @@ -222,7 +220,7 @@ export const ColorSelector: FC = ({ withArrow > - + - + + ); diff --git a/apps/client/src/features/editor/components/bubble-menu/link-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/link-selector.tsx index fdacf6a18..0b7992f18 100644 --- a/apps/client/src/features/editor/components/bubble-menu/link-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/link-selector.tsx @@ -10,7 +10,7 @@ export const LinkSelector: FC = () => { const setShowLinkMenu = useSetAtom(showLinkMenuAtom); return ( - + = ({ onChange={setIsOpen} > - +