diff --git a/Dockerfile b/Dockerfile index 33cac222..43594146 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM node:26-slim AS base LABEL org.opencontainers.image.source="https://github.com/docmost/docmost" -RUN npm install -g pnpm@11.25.0 +RUN npm install -g pnpm@11.28.2 FROM base AS builder diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 7c685ac6..76a4cf88 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -471,7 +471,9 @@ export const collabExtensions: CollabExtensions = (provider, user) => [ CollaborationCaret.configure({ provider, user: { + id: user.id, name: user.name, + avatarUrl: user.avatarUrl, color: randomElement(userColors), }, }), diff --git a/apps/server/package.json b/apps/server/package.json index c744fcc5..3c19de8a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -97,7 +97,7 @@ "nestjs-cls": "6.2.0", "nestjs-kysely": "3.1.2", "nestjs-pino": "4.6.1", - "nodemailer": "9.1.1", + "nodemailer": "10.0.10", "openid-client": "6.8.2", "otpauth": "9.5.0", "p-limit": "7.3.0", @@ -120,7 +120,7 @@ "tlds": "1.261.0", "tmp-promise": "3.0.3", "typesense": "3.0.5", - "undici": "7.29.0", + "undici": "7.29.1", "ws": "8.21.3", "yauzl": "3.4.0", "zod": "4.3.6" @@ -136,7 +136,6 @@ "@types/jest": "^30.0.0", "@types/mime-types": "^3.0.1", "@types/node": "^25.5.0", - "@types/nodemailer": "^7.0.11", "@types/passport-google-oauth20": "^2.0.17", "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.3", diff --git a/apps/server/src/collaboration/collaboration.gateway.ts b/apps/server/src/collaboration/collaboration.gateway.ts index 254b8d43..5556fef1 100644 --- a/apps/server/src/collaboration/collaboration.gateway.ts +++ b/apps/server/src/collaboration/collaboration.gateway.ts @@ -146,8 +146,14 @@ export class CollaborationGateway { eventName: TName, documentName: string, payload: Parameters[1], + onlyIfOpen = false, ) { - return this.redisSync?.handleEvent(eventName, documentName, payload); + return this.redisSync?.handleEvent( + eventName, + documentName, + payload, + onlyIfOpen, + ); } openDirectConnection(documentName: string, context?: any) { diff --git a/apps/server/src/collaboration/collaboration.handler.ts b/apps/server/src/collaboration/collaboration.handler.ts index 992f9b74..2df9d972 100644 --- a/apps/server/src/collaboration/collaboration.handler.ts +++ b/apps/server/src/collaboration/collaboration.handler.ts @@ -21,6 +21,17 @@ export class CollaborationHandler { getHandlers(hocuspocus: Hocuspocus) { return { + getConnectedUserIds: async (documentName: string) => { + const document = hocuspocus.documents.get(documentName); + if (!document) return []; + + const userIds = new Set(); + for (const state of document.awareness.getStates().values()) { + const userId = state?.user?.id; + if (typeof userId === 'string') userIds.add(userId); + } + return [...userIds]; + }, alterState: async (documentName: string, payload: { pageId: string }) => { // dummy // this.logger.log('Processing', documentName, payload); diff --git a/apps/server/src/collaboration/collaboration.util.ts b/apps/server/src/collaboration/collaboration.util.ts index e5c55c77..1c527ea2 100644 --- a/apps/server/src/collaboration/collaboration.util.ts +++ b/apps/server/src/collaboration/collaboration.util.ts @@ -247,3 +247,7 @@ export function jsonToMarkdown(tiptapJson: any): string { const html = jsonToHtml(tiptapJson); return htmlToMarkdown(html); } + +export function isRenderableObject(value: unknown): boolean { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/apps/server/src/collaboration/extensions/persistence.extension.ts b/apps/server/src/collaboration/extensions/persistence.extension.ts index 85efb5ec..f13ff440 100644 --- a/apps/server/src/collaboration/extensions/persistence.extension.ts +++ b/apps/server/src/collaboration/extensions/persistence.extension.ts @@ -1,5 +1,6 @@ import { afterUnloadDocumentPayload, + beforeHandleAwarenessPayload, Extension, onChangePayload, onLoadDocumentPayload, @@ -8,7 +9,12 @@ import { import * as Y from 'yjs'; import { Injectable, Logger } from '@nestjs/common'; import { TiptapTransformer } from '@hocuspocus/transformer'; -import { getPageId, jsonToText, tiptapExtensions } from '../collaboration.util'; +import { + getPageId, + isRenderableObject, + jsonToText, + tiptapExtensions, +} from '../collaboration.util'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB } from '@docmost/db/types/kysely.types'; @@ -132,6 +138,8 @@ export class PersistenceExtension implements Extension { return; } + await this.collabHistory.addContributors(pageId, editingUserIds); + let contributorIds = undefined; try { const existingContributors = page.contributorIds || []; @@ -162,6 +170,10 @@ export class PersistenceExtension implements Extension { }); } catch (err) { this.logger.error(`Failed to update page ${pageId}`, err); + page = null; + editingUserIds.forEach((userId) => + this.trackContributor(documentName, userId), + ); } if (page) { @@ -184,8 +196,6 @@ export class PersistenceExtension implements Extension { } if (page) { - await this.collabHistory.addContributors(pageId, editingUserIds); - const mentions = extractMentions(tiptapJson); const userMentions = extractUserMentions(mentions); @@ -217,12 +227,45 @@ export class PersistenceExtension implements Extension { } } + // Drop malformed awareness before it is broadcast + async beforeHandleAwareness({ + states, + context, + }: beforeHandleAwarenessPayload) { + const user = context?.user; + + for (const [clientId, state] of states) { + if (!isRenderableObject(state)) { + states.delete(clientId); + continue; + } + + if ('user' in state && !isRenderableObject(state.user)) { + delete state.user; + } + + if (state.user && user) { + state.user.id = user.id; + state.user.avatarUrl = user.avatarUrl ?? null; + } + + if ( + 'cursor' in state && + state.cursor !== null && + !isRenderableObject(state.cursor) + ) { + delete state.cursor; + } + } + } + async onChange(data: onChangePayload) { - const documentName = data.documentName; const userId = data.context?.user?.id; - if (!userId) return; + this.trackContributor(data.documentName, userId); + } + private trackContributor(documentName: string, userId: string) { if (!this.contributors.has(documentName)) { this.contributors.set(documentName, new Set()); } diff --git a/apps/server/src/collaboration/extensions/redis-sync/redis-sync.extension.ts b/apps/server/src/collaboration/extensions/redis-sync/redis-sync.extension.ts index 42139097..3a667f01 100644 --- a/apps/server/src/collaboration/extensions/redis-sync/redis-sync.extension.ts +++ b/apps/server/src/collaboration/extensions/redis-sync/redis-sync.extension.ts @@ -235,13 +235,11 @@ export class RedisSyncExtension implements Extension { }; async maintainLock(documentName: string) { + clearInterval(this.locks[documentName]); this.locks[documentName] = setInterval(() => { - this.pub.set( - this.getKey(documentName), - this.serverId, - 'PX', - this.lockTTL, - ); + this.pub + .set(this.getKey(documentName), this.serverId, 'PX', this.lockTTL) + .catch(() => {}); }, this.lockTTL / 2); } diff --git a/apps/server/src/collaboration/server/collab-main.ts b/apps/server/src/collaboration/server/collab-main.ts index 3b3de243..89b1605b 100644 --- a/apps/server/src/collaboration/server/collab-main.ts +++ b/apps/server/src/collaboration/server/collab-main.ts @@ -37,6 +37,14 @@ async function bootstrap() { const logger = new Logger('CollabServer'); + process.on('unhandledRejection', (reason, promise) => { + logger.error(`UnhandledRejection, reason: ${reason}`, promise); + }); + + process.on('uncaughtException', (error) => { + logger.error('UncaughtException:', error); + }); + const port = process.env.COLLAB_PORT || 3001; const host = process.env.HOST || '0.0.0.0'; await app.listen(port, host, () => { diff --git a/apps/server/src/core/notification/notification.module.ts b/apps/server/src/core/notification/notification.module.ts index 9aa452a5..543ca0cc 100644 --- a/apps/server/src/core/notification/notification.module.ts +++ b/apps/server/src/core/notification/notification.module.ts @@ -6,9 +6,10 @@ import { CommentNotificationService } from './services/comment.notification'; import { PageNotificationService } from './services/page.notification'; import { VerificationNotificationService } from './services/verification.notification'; import { PageUpdateEmailRateLimiter } from './services/page-update-email-rate-limiter'; +import { CollaborationModule } from '../../collaboration/collaboration.module'; @Module({ - imports: [], + imports: [CollaborationModule], controllers: [NotificationController], providers: [ NotificationService, diff --git a/apps/server/src/core/notification/services/page.notification.ts b/apps/server/src/core/notification/services/page.notification.ts index 77ab967a..dcbc6e86 100644 --- a/apps/server/src/core/notification/services/page.notification.ts +++ b/apps/server/src/core/notification/services/page.notification.ts @@ -21,6 +21,7 @@ import { PageUpdateDigestEmail } from '@docmost/transactional/emails/page-update import { PermissionGrantedEmail } from '@docmost/transactional/emails/permission-granted-email'; import { getPageTitle } from '../../../common/helpers'; import { QueueJob, QueueName } from '../../../integrations/queue/constants'; +import { CollaborationGateway } from '../../../collaboration/collaboration.gateway'; const PAGE_UPDATE_COOLDOWN_HOURS = 7; const DIGEST_DELAY_MS = 12 * 60 * 60 * 1000; // 12 hours @@ -37,6 +38,7 @@ export class PageNotificationService { private readonly pagePermissionRepo: PagePermissionRepo, private readonly watcherRepo: WatcherRepo, private readonly rateLimiter: PageUpdateEmailRateLimiter, + private readonly collaborationGateway: CollaborationGateway, @InjectQueue(QueueName.NOTIFICATION_QUEUE) private notificationQueue: Queue, ) {} @@ -186,8 +188,22 @@ export class PageNotificationService { if (watcherIds.length === 0) return; - const actorSet = new Set(actorIds); - const candidateIds = watcherIds.filter((id) => !actorSet.has(id)); + let connectedUserIds: string[] = []; + try { + connectedUserIds = + (await this.collaborationGateway.handleYjsEvent( + 'getConnectedUserIds', + `page.${pageId}`, + undefined, + true, + )) ?? []; + } catch (err) { + this.logger.warn( + `Failed to get connected users for page ${pageId}: ${err?.['message']}`, + ); + } + const excludedIds = new Set([...actorIds, ...connectedUserIds]); + const candidateIds = watcherIds.filter((id) => !excludedIds.has(id)); if (candidateIds.length === 0) return; const eligibleUsers = await this.getEligiblePageUpdateUsers(candidateIds); @@ -370,13 +386,14 @@ export class PageNotificationService { const pages = spaceFilteredPages.filter((p) => accessiblePageIds.has(p.id)); if (pages.length === 0) return; - const actors = actorIds.length > 0 - ? await this.db - .selectFrom('users') - .select(['id', 'name']) - .where('id', 'in', actorIds) - .execute() - : []; + const actors = + actorIds.length > 0 + ? await this.db + .selectFrom('users') + .select(['id', 'name']) + .where('id', 'in', actorIds) + .execute() + : []; const actorMap = new Map(actors.map((a) => [a.id, a.name])); const pageActors = new Map>(); diff --git a/apps/server/src/core/page/services/page.service.ts b/apps/server/src/core/page/services/page.service.ts index 990d8fae..fbe8a4d5 100644 --- a/apps/server/src/core/page/services/page.service.ts +++ b/apps/server/src/core/page/services/page.service.ts @@ -56,6 +56,7 @@ import { markdownToHtml } from '@docmost/editor-ext'; import { WatcherService } from '../../watcher/watcher.service'; import { sql } from 'kysely'; import { TransclusionService } from '../transclusion/transclusion.service'; +import { LabelRepo } from '@docmost/db/repos/label/label.repo'; @Injectable() export class PageService { @@ -74,6 +75,7 @@ export class PageService { private collaborationGateway: CollaborationGateway, private readonly watcherService: WatcherService, private readonly transclusionService: TransclusionService, + private readonly labelRepo: LabelRepo, ) {} async findById( @@ -715,7 +717,13 @@ export class PageService { }), ); - await this.db.insertInto('pages').values(insertablePages).execute(); + await executeTx(this.db, async (trx) => { + await trx.insertInto('pages').values(insertablePages).execute(); + await this.labelRepo.copyLabelsToPages( + new Map([...pageMap].map(([oldId, entry]) => [oldId, entry.newPageId])), + trx, + ); + }); // Extract transclusions from every duplicated page and persist them in // one statement. Duplication bypasses Yjs onStoreDocument; brand-new diff --git a/apps/server/src/core/search/dto/search.dto.ts b/apps/server/src/core/search/dto/search.dto.ts index b9d09d17..e966dc8a 100644 --- a/apps/server/src/core/search/dto/search.dto.ts +++ b/apps/server/src/core/search/dto/search.dto.ts @@ -6,11 +6,15 @@ import { IsOptional, IsString, IsUUID, + MaxLength, } from 'class-validator'; +export const SEARCH_QUERY_MAX_LENGTH = 200; + export class SearchDTO { @IsOptional() @IsString() + @MaxLength(SEARCH_QUERY_MAX_LENGTH) query?: string; @IsOptional() @@ -61,6 +65,7 @@ export class SearchPublicSpaceDTO extends SearchDTO { export class SearchSuggestionDTO { @IsString() + @MaxLength(SEARCH_QUERY_MAX_LENGTH) query: string; @IsOptional() diff --git a/apps/server/src/core/search/search.service.ts b/apps/server/src/core/search/search.service.ts index f8e8e551..4e548dd9 100644 --- a/apps/server/src/core/search/search.service.ts +++ b/apps/server/src/core/search/search.service.ts @@ -55,11 +55,6 @@ export class SearchService { : 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') @@ -73,7 +68,6 @@ export class SearchService { 'createdAt', 'updatedAt', rankColumn, - highlightColumn, ]) .$if(!browseByFilters && !titleOnly, (qb) => qb.where( @@ -189,10 +183,32 @@ export class SearchService { results = results.filter((r: any) => accessibleSet.has(r.id)); } + if (!browseByFilters && !titleOnly && results.length > 0) { + const highlights = await this.db + .selectFrom('pages') + .select([ + 'id', + sql`ts_headline('english', substring(text_content, 1, 100000), to_tsquery('english', f_unaccent(${searchQuery})),'MinWords=9, MaxWords=10, MaxFragments=3')`.as( + 'highlight', + ), + ]) + .where( + 'id', + 'in', + results.map((r: any) => r.id), + ) + .execute(); + const highlightById = new Map(highlights.map((h) => [h.id, h.highlight])); + for (const result of results) { + result.highlight = highlightById.get(result.id) ?? ''; + } + } + //@ts-ignore const searchResults = results.map((result: SearchResponseDto) => { result.wholeWord = true if (!result.highlight) { + result.highlight = ''; result.matchedText = []; return result; } diff --git a/apps/server/src/database/repos/label/label.repo.ts b/apps/server/src/database/repos/label/label.repo.ts index bac199ed..e2a6e7ed 100644 --- a/apps/server/src/database/repos/label/label.repo.ts +++ b/apps/server/src/database/repos/label/label.repo.ts @@ -176,6 +176,32 @@ export class LabelRepo { .execute(); } + async copyLabelsToPages( + pageIdMap: Map, + trx?: KyselyTransaction, + ): Promise { + if (pageIdMap.size === 0) return; + const db = dbOrTx(this.db, trx); + + const sourceLabels = await db + .selectFrom('pageLabels') + .select(['pageId', 'labelId']) + .where('pageId', 'in', [...pageIdMap.keys()]) + .execute(); + if (sourceLabels.length === 0) return; + + await db + .insertInto('pageLabels') + .values( + sourceLabels.map((row) => ({ + pageId: pageIdMap.get(row.pageId), + labelId: row.labelId, + })), + ) + .onConflict((oc) => oc.doNothing()) + .execute(); + } + async removeLabelFromPage( pageId: string, labelId: string, diff --git a/apps/server/src/ee b/apps/server/src/ee index 5e7120dc..8190e218 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 5e7120dcc86a344f5af09ccda0a29d5784659938 +Subproject commit 8190e2182cbbcc5e291217a13147fcd246ede3fe diff --git a/apps/server/src/integrations/export/html-to-markdown.spec.ts b/apps/server/src/integrations/export/html-to-markdown.spec.ts new file mode 100644 index 00000000..8e4e43d0 --- /dev/null +++ b/apps/server/src/integrations/export/html-to-markdown.spec.ts @@ -0,0 +1,54 @@ +import { htmlToMarkdown } from '@docmost/editor-ext'; + +const colgroup = + ''; + +describe('htmlToMarkdown', () => { + it('uses the table header row as the markdown header', () => { + const markdown = htmlToMarkdown( + `${colgroup}` + + '' + + '' + + '

Name

Role

Ada

Engineer

', + ); + + expect(markdown.trim().split('\n')).toEqual([ + '| Name | Role |', + '| --- | --- |', + '| Ada | Engineer |', + ]); + }); + + it('adds an empty header when the table has no header row', () => { + const markdown = htmlToMarkdown( + `${colgroup}` + + '' + + '' + + '

Ada

Engineer

Alan

Mathematician

', + ); + + expect(markdown.trim().split('\n')).toEqual([ + '| | |', + '| --- | --- |', + '| Ada | Engineer |', + '| Alan | Mathematician |', + ]); + }); + + it('marks details blocks so their content is parsed as markdown', () => { + const markdown = htmlToMarkdown( + '
Title' + + '

Some bold

' + + '
', + ); + + expect(markdown.trim().split('\n')).toEqual([ + '
', + 'Title', + '', + 'Some **bold**', + '', + '
', + ]); + }); +}); diff --git a/apps/server/src/integrations/throttle/user-throttler.guard.spec.ts b/apps/server/src/integrations/throttle/user-throttler.guard.spec.ts new file mode 100644 index 00000000..1110d8ad --- /dev/null +++ b/apps/server/src/integrations/throttle/user-throttler.guard.spec.ts @@ -0,0 +1,51 @@ +import { Reflector } from '@nestjs/core'; +import { ThrottlerStorageService } from '@nestjs/throttler'; +import { JwtType } from '../../core/auth/dto/jwt-payload'; +import { UserThrottlerGuard } from './user-throttler.guard'; + +const guard = new UserThrottlerGuard( + [], + new ThrottlerStorageService(), + new Reflector(), +); + +function getTracker(req: Record): Promise { + return guard['getTracker'](req); +} + +describe('UserThrottlerGuard.getTracker', () => { + it('tracks a signed-in request by its user id', async () => { + const req = { + ip: '203.0.113.7', + user: { + user: { id: 'user_1' }, + workspace: { id: 'ws_1' }, + authType: JwtType.ACCESS, + }, + }; + + await expect(getTracker(req)).resolves.toBe('user:user_1'); + }); + + it('falls back to the client ip when nobody is signed in', async () => { + const req = { ip: '203.0.113.7', user: null }; + + await expect(getTracker(req)).resolves.toBe('203.0.113.7'); + }); + + it('falls back to the socket address when the ip is empty', async () => { + const req = { + ip: '', + user: null, + socket: { remoteAddress: '198.51.100.4' }, + }; + + await expect(getTracker(req)).resolves.toBe('198.51.100.4'); + }); + + it('uses a constant tracker for an unidentifiable client', async () => { + const req = { ip: '', user: null, socket: {} }; + + await expect(getTracker(req)).resolves.toBe('unknown'); + }); +}); diff --git a/apps/server/src/integrations/throttle/user-throttler.guard.ts b/apps/server/src/integrations/throttle/user-throttler.guard.ts index 35744c09..379cf37d 100644 --- a/apps/server/src/integrations/throttle/user-throttler.guard.ts +++ b/apps/server/src/integrations/throttle/user-throttler.guard.ts @@ -1,13 +1,19 @@ import { Injectable } from '@nestjs/common'; import { ThrottlerGuard } from '@nestjs/throttler'; -type AuthedRequest = { user?: { id?: string } }; +type AuthedRequest = { + user?: { user?: { id?: string } } | null; + socket?: { remoteAddress?: string }; +}; @Injectable() export class UserThrottlerGuard extends ThrottlerGuard { protected async getTracker(req: AuthedRequest): Promise { - const userId = req.user?.id; + const userId = req.user?.user?.id; if (userId) return `user:${userId}`; - return super.getTracker(req as Parameters[0]); + const ip = await super.getTracker( + req as Parameters[0], + ); + return ip || req.socket?.remoteAddress || 'unknown'; } } diff --git a/package.json b/package.json index 7ef894cc..4333fc27 100644 --- a/package.json +++ b/package.json @@ -95,5 +95,5 @@ "packages/*" ] }, - "packageManager": "pnpm@11.25.0" + "packageManager": "pnpm@11.28.2" } diff --git a/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts b/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts index 55f4afd3..3914d792 100644 --- a/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts +++ b/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts @@ -37,7 +37,11 @@ export function htmlToMarkdown(html: string): string { footnoteRef, footnotesList, ]); - return turndownService.turndown(html).replaceAll('
', ' '); + const htmlWithoutColgroups = html.replace( + /]*>[\s\S]*?<\/colgroup>/gi, + '', + ); + return turndownService.turndown(htmlWithoutColgroups).replaceAll('
', ' '); } function listParagraph(turndownService: _TurndownService) { @@ -146,7 +150,7 @@ function preserveDetail(turndownService: _TurndownService) { ) .join(''); - return `\n
\n${detailSummary}\n\n${detailsContent}\n\n
\n`; + return `\n
\n${detailSummary}\n\n${detailsContent}\n\n
\n`; }, }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de69cd4c..337fdc35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ overrides: ws: 8.21.3 dompurify: 3.4.13 mermaid: 11.16.1 - undici: 7.29.0 + undici: 7.29.1 tmp: 0.2.7 nanoid@^3: 3.3.18 lodash-es: 4.18.1 @@ -21,7 +21,7 @@ overrides: yaml@>=2.0.0 <2.8.3: 2.8.3 brace-expansion@^5: 5.0.9 axios: 1.18.1 - ip-address: 10.3.1 + ip-address: 10.5.1 fast-uri: 3.1.7 form-data@>=4.0.0 <4.0.6: 4.0.6 nanoid@>=4.0.0 <5.1.16: 5.1.16 @@ -29,6 +29,7 @@ overrides: '@opentelemetry/core@>=2.0.0 <2.8.0': 2.9.0 js-yaml@>=3.0.0 <3.15.2: 3.15.2 js-yaml@>=4.0.0 <4.3.2: 4.3.2 + smol-toml@<1.7.1: 1.7.1 patchedDependencies: scimmy@1.3.5: 775d80f86830b2c5dd1a250c9802c10f8fc3da3c7898373de5aa0c23993d1673 @@ -694,8 +695,8 @@ importers: specifier: 4.6.1 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.1.1 - version: 9.1.1 + specifier: 10.0.10 + version: 10.0.10 openid-client: specifier: 6.8.2 version: 6.8.2 @@ -763,8 +764,8 @@ importers: specifier: 3.0.5 version: 3.0.5(@babel/runtime@7.29.2)(debug@4.4.3(supports-color@7.2.0))(supports-color@10.2.2) undici: - specifier: 7.29.0 - version: 7.29.0 + specifier: 7.29.1 + version: 7.29.1 ws: specifier: 8.21.3 version: 8.21.3 @@ -805,9 +806,6 @@ importers: '@types/node': specifier: ^25.5.0 version: 25.5.0 - '@types/nodemailer': - specifier: ^7.0.11 - version: 7.0.11 '@types/passport-google-oauth20': specifier: ^2.0.17 version: 2.0.17 @@ -4633,9 +4631,6 @@ packages: '@types/node@25.5.0': resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} - '@types/nodemailer@7.0.11': - resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==} - '@types/oauth@0.9.6': resolution: {integrity: sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==} @@ -6906,8 +6901,8 @@ packages: resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} - ip-address@10.3.1: - resolution: {integrity: sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==} + ip-address@10.5.1: + resolution: {integrity: sha512-EXujUp9jyOI/chPgtqk6uy7fDq8AeCB/WlfEuPg9LN0fN9lzKAKfuDYi60SMhHwgUiEhZvVYsbGZN+RUU1INiA==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -7984,9 +7979,9 @@ packages: resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} engines: {node: '>=18'} - nodemailer@9.1.1: - resolution: {integrity: sha512-izw9mVKFix6YSnC9eLgV6g1opl9DUlRio9ZNcq+Wu9Ujn2UwF+8Nl0B8nz22kEC+CTZCvinkxwJ0DeFbb6NwcQ==} - engines: {node: '>=6.0.0'} + nodemailer@10.0.10: + resolution: {integrity: sha512-He9XskOFms62SyAKkLu8CcGcYHAo+BSHSsORI8s4PJH8qZgZY4L4lDfvyN0twvmbpyG+eGrxpyBlskj9d9rJ8A==} + engines: {node: '>=20.0.0'} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -9058,8 +9053,8 @@ packages: resolution: {integrity: sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==} deprecated: Unsupported - smol-toml@1.6.1: - resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + smol-toml@1.7.1: + resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==} engines: {node: '>= 18'} socket.io-adapter@2.5.4: @@ -9570,8 +9565,8 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - undici@7.29.0: - resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + undici@7.29.1: + resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} engines: {node: '>=20.18.1'} unicode-canonical-property-names-ecmascript@2.0.0: @@ -10163,7 +10158,7 @@ snapshots: '@ai-sdk/provider': 3.0.16 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.1.1 - undici: 7.29.0 + undici: 7.29.1 zod: 4.3.6 '@ai-sdk/provider@3.0.16': @@ -14362,7 +14357,7 @@ snapshots: '@turbopuffer/turbopuffer@2.8.0': dependencies: pako: 2.2.0 - undici: 7.29.0 + undici: 7.29.1 '@tybys/wasm-util@0.10.2': dependencies: @@ -14640,10 +14635,6 @@ snapshots: dependencies: undici-types: 7.18.2 - '@types/nodemailer@7.0.11': - dependencies: - '@types/node': 25.5.0 - '@types/oauth@0.9.6': dependencies: '@types/node': 25.5.0 @@ -15617,7 +15608,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.29.0 + undici: 7.29.1 whatwg-mimetype: 4.0.0 chevrotain-allstar@0.3.1(chevrotain@11.0.3): @@ -16684,7 +16675,7 @@ snapshots: express-rate-limit@8.2.2(express@5.2.1(supports-color@10.2.2)): dependencies: express: 5.2.1(supports-color@10.2.2) - ip-address: 10.3.1 + ip-address: 10.5.1 express@5.2.1(supports-color@10.2.2): dependencies: @@ -17269,7 +17260,7 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@10.3.1: {} + ip-address@10.5.1: {} ipaddr.js@1.9.1: {} @@ -18418,7 +18409,7 @@ snapshots: node-releases@2.0.54: {} - nodemailer@9.1.1: {} + nodemailer@10.0.10: {} normalize-path@3.0.0: {} @@ -18537,7 +18528,7 @@ snapshots: safe-buffer: 5.2.1 semver: 7.8.4 signal-exit: 3.0.7 - smol-toml: 1.6.1 + smol-toml: 1.7.1 string-width: 4.2.3 string_decoder: 1.3.0 strip-ansi: 6.0.1 @@ -19776,7 +19767,7 @@ snapshots: sliced@1.0.1: {} - smol-toml@1.6.1: {} + smol-toml@1.7.1: {} socket.io-adapter@2.5.4(supports-color@10.2.2): dependencies: @@ -20340,7 +20331,7 @@ snapshots: undici-types@7.18.2: {} - undici@7.29.0: {} + undici@7.29.1: {} unicode-canonical-property-names-ecmascript@2.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d75680b4..e3b58a88 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,7 +9,7 @@ overrides: ws: 8.21.3 dompurify: 3.4.13 mermaid: 11.16.1 - undici: 7.29.0 + undici: 7.29.1 tmp: 0.2.7 nanoid@^3: 3.3.18 lodash-es: 4.18.1 @@ -20,7 +20,7 @@ overrides: yaml@>=2.0.0 <2.8.3: 2.8.3 brace-expansion@^5: 5.0.9 axios: 1.18.1 - ip-address: 10.3.1 + ip-address: 10.5.1 fast-uri: 3.1.7 form-data@>=4.0.0 <4.0.6: 4.0.6 nanoid@>=4.0.0 <5.1.16: 5.1.16 @@ -28,6 +28,7 @@ overrides: '@opentelemetry/core@>=2.0.0 <2.8.0': 2.9.0 js-yaml@>=3.0.0 <3.15.2: 3.15.2 js-yaml@>=4.0.0 <4.3.2: 4.3.2 + smol-toml@<1.7.1: 1.7.1 shamefullyHoist: true minimumReleaseAge: 4320 allowBuilds: