From 7123595831d30e665306cec9b72d3a63bc6a968c Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:13:59 +0800 Subject: [PATCH] chore: bump deps (#15059) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### PR Dependency Tree * **PR #15059** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit * **New Features** * Configurable minimum account age before new accounts can invite members or create share links (default: 24 hours). * Sign-in now returns and caches user info for improved session handling. * **Bug Fixes** * Queue handling accepts and resolves job IDs with special characters. * Improved clipboard/rich-text caret handling and nested-list paste reliability. * Calendar tests use dynamic current-month dates. * AI search returns explicit "No matching documents" when none found. * Auth session responses are explicitly non-cacheable. * **Chores** * Dependency and toolchain bumps; admin UI config/schema exposes the new account-age setting. --- .docker/selfhost/schema.json | 5 ++ packages/backend/server/package.json | 2 +- .../src/__tests__/auth/controller.spec.ts | 8 ++ .../base/job/queue/__tests__/queue.spec.ts | 17 ++++ .../server/src/base/job/queue/queue.ts | 34 ++++++-- .../backend/server/src/core/auth/config.ts | 6 ++ .../server/src/core/auth/controller.ts | 1 + .../core/workspaces/__tests__/abuse.spec.ts | 25 +++--- .../server/src/core/workspaces/abuse.ts | 10 ++- .../src/core/workspaces/resolvers/doc.ts | 30 ++++++-- .../src/core/workspaces/resolvers/member.ts | 40 ++++++++-- .../src/plugins/copilot/mcp/provider.ts | 12 ++- .../native/src/doc_parser/markdown/parser.rs | 6 +- .../common/y-octo/core/src/doc/types/mod.rs | 4 +- packages/frontend/admin/src/config.json | 4 + .../admin/src/modules/settings/config.ts | 5 ++ .../apps/electron/src/main/auth/handlers.ts | 15 +++- packages/frontend/core/package.json | 2 +- .../core/src/modules/cloud/impl/auth.ts | 3 +- .../core/src/modules/cloud/provider/auth.ts | 11 ++- .../core/src/modules/cloud/services/auth.ts | 5 +- .../core/src/modules/cloud/stores/auth.ts | 25 +++++- .../frontend/native/nbstore/src/indexer.rs | 2 +- rust-toolchain.toml | 2 +- .../e2e/clipboard/clipboard.spec.ts | 5 +- tests/blocksuite/e2e/clipboard/list.spec.ts | 5 +- .../blocksuite/e2e/database/calendar.spec.ts | 72 +++++++++-------- tests/blocksuite/e2e/utils/actions/misc.ts | 41 ++++++++-- yarn.lock | 77 ++++++++++--------- 29 files changed, 344 insertions(+), 130 deletions(-) diff --git a/.docker/selfhost/schema.json b/.docker/selfhost/schema.json index 55ee949d4f..d08e11ae52 100644 --- a/.docker/selfhost/schema.json +++ b/.docker/selfhost/schema.json @@ -175,6 +175,11 @@ "description": "Whether require email verification before accessing restricted resources(not implemented).\n@default true", "default": true }, + "newAccountShareActionDelay": { + "type": "number", + "description": "Minimum account age in seconds before new accounts can invite members or create share links.\n@default 86400", + "default": 86400 + }, "passwordRequirements": { "type": "object", "description": "The password strength requirements when set new password.\n@default {\"min\":8,\"max\":32}", diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 0d0ef79f21..5e7cf1653f 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -65,7 +65,7 @@ "@queuedash/api": "^3.16.0", "@react-email/components": "^0.5.7", "@socket.io/redis-adapter": "^8.3.0", - "bullmq": "5.53.0", + "bullmq": "5.77.6", "commander": "^13.1.0", "cookie-parser": "^1.4.7", "cross-env": "^10.1.0", diff --git a/packages/backend/server/src/__tests__/auth/controller.spec.ts b/packages/backend/server/src/__tests__/auth/controller.spec.ts index feca0c8105..4dc9886674 100644 --- a/packages/backend/server/src/__tests__/auth/controller.spec.ts +++ b/packages/backend/server/src/__tests__/auth/controller.spec.ts @@ -70,6 +70,14 @@ test('should be able to sign in with credential', async t => { t.is(session?.id, u1.id); }); +test('should not cache auth session response', async t => { + const { app } = t.context; + + const res = await app.GET('/api/auth/session').expect(200); + + t.is(res.headers['cache-control'], 'no-store'); +}); + async function exchangeSession(app: TestingApp, code: string) { return await supertest(app.getHttpServer()) .post('/api/auth/native/exchange') diff --git a/packages/backend/server/src/base/job/queue/__tests__/queue.spec.ts b/packages/backend/server/src/base/job/queue/__tests__/queue.spec.ts index e4ba4cf19e..05464fc08e 100644 --- a/packages/backend/server/src/base/job/queue/__tests__/queue.spec.ts +++ b/packages/backend/server/src/base/job/queue/__tests__/queue.spec.ts @@ -104,6 +104,23 @@ test('should add job to queue', async t => { t.is(queuedJob!.name, job.name); }); +test('should accept job id containing colon', async t => { + const job = await queue.add( + 'nightly.__test__job', + { name: 'test' }, + { jobId: 'custom:id' } + ); + + const queuedJob = await queue.get('custom:id', 'nightly.__test__job'); + const roundTripJob = await queue.get(job.id!, 'nightly.__test__job'); + const data = await queue.remove(job.id!, 'nightly.__test__job'); + + t.is(job.id, 'custom%3Aid'); + t.is(queuedJob!.name, job.name); + t.is(roundTripJob!.name, job.name); + t.deepEqual(data, { name: 'test' }); +}); + test('should remove job from queue', async t => { const job = await queue.add('nightly.__test__job', { name: 'test' }); diff --git a/packages/backend/server/src/base/job/queue/queue.ts b/packages/backend/server/src/base/job/queue/queue.ts index c5729d968d..fcc897b114 100644 --- a/packages/backend/server/src/base/job/queue/queue.ts +++ b/packages/backend/server/src/base/job/queue/queue.ts @@ -21,6 +21,19 @@ const removableJobStates = [ ] as const; const removeWhereBatchSize = 100; +function normalizeJobId(jobId: string) { + return encodeURIComponent(jobId); +} + +function normalizedJobIds(jobId: string) { + const normalized = normalizeJobId(jobId); + if (jobId.includes(':')) { + return [normalized]; + } + + return normalized === jobId ? [jobId] : [jobId, normalized]; +} + @Injectable() export class JobQueue { private readonly logger = new Logger(JobQueue.name); @@ -30,6 +43,9 @@ export class JobQueue { async add(name: T, payload: Jobs[T], opts?: JobsOptions) { const ns = namespace(name); const queue = this.getQueue(ns); + const normalizedOpts = opts?.jobId + ? { ...opts, jobId: normalizeJobId(opts.jobId) } + : opts; const job = await queue.add( name, { @@ -37,7 +53,7 @@ export class JobQueue { ClsServiceManager.getClsService().getId() ?? genRequestId('job'), payload, } as JobData, - opts + normalizedOpts ); this.logger.debug(`Job [${name}] added; id=${job.id}`); return job; @@ -49,15 +65,16 @@ export class JobQueue { ): Promise { const ns = namespace(jobName); const queue = this.getQueue(ns); - const job = (await queue.getJob(jobId)) as Job> | undefined; + const job = await this.get(jobId, jobName); if (!job) { return; } - const removed = await queue.remove(jobId); + if (!job.id) return; + const removed = await queue.remove(job.id); if (removed) { - this.logger.log(`Job ${jobName}(id=${jobId}) removed from queue ${ns}`); + this.logger.log(`Job ${jobName}(id=${job.id}) removed from queue ${ns}`); return job.data.payload; } @@ -120,7 +137,14 @@ export class JobQueue { async get(jobId: string, jobName: T) { const ns = namespace(jobName); const queue = this.getQueue(ns); - return (await queue.getJob(jobId)) as Job> | undefined; + for (const id of normalizedJobIds(jobId)) { + const job = (await queue.getJob(id)) as Job> | undefined; + if (job) { + return job; + } + } + + return undefined; } private getQueue(ns: string): Queue { diff --git a/packages/backend/server/src/core/auth/config.ts b/packages/backend/server/src/core/auth/config.ts index afd5900541..6e47bf7995 100644 --- a/packages/backend/server/src/core/auth/config.ts +++ b/packages/backend/server/src/core/auth/config.ts @@ -11,6 +11,7 @@ export interface AuthConfig { allowSignupForOauth: boolean; requireEmailDomainVerification: boolean; requireEmailVerification: boolean; + newAccountShareActionDelay: number; passwordRequirements: ConfigItem<{ min: number; max: number; @@ -40,6 +41,11 @@ defineModuleConfig('auth', { desc: 'Whether require email verification before accessing restricted resources(not implemented).', default: true, }, + newAccountShareActionDelay: { + desc: 'Minimum account age in seconds before new accounts can invite members or create share links.', + default: 24 * 60 * 60, + shape: z.number().int().min(0), + }, passwordRequirements: { desc: 'The password strength requirements when set new password.', default: { diff --git a/packages/backend/server/src/core/auth/controller.ts b/packages/backend/server/src/core/auth/controller.ts index cadc45fcfa..1d72e8719c 100644 --- a/packages/backend/server/src/core/auth/controller.ts +++ b/packages/backend/server/src/core/auth/controller.ts @@ -256,6 +256,7 @@ export class AuthController { @Throttle('default', { limit: 1200 }) @Public() @Get('/session') + @Header('Cache-Control', 'no-store') async currentSessionUser(@CurrentUser() user?: CurrentUser) { return { user }; } diff --git a/packages/backend/server/src/core/workspaces/__tests__/abuse.spec.ts b/packages/backend/server/src/core/workspaces/__tests__/abuse.spec.ts index 26e6b4bc89..ade7fd49fc 100644 --- a/packages/backend/server/src/core/workspaces/__tests__/abuse.spec.ts +++ b/packages/backend/server/src/core/workspaces/__tests__/abuse.spec.ts @@ -1,10 +1,6 @@ import test from 'ava'; -import { - containsUrlOrDomain, - isUserOldEnoughForShareActions, - SHARE_ACTION_ACCOUNT_AGE_MS, -} from '../abuse'; +import { canUserExecuteLimitedActions, containsUrlOrDomain } from '../abuse'; test('should detect links and bare domains in workspace names', t => { t.true(containsUrlOrDomain('BTC https://spam.example')); @@ -20,10 +16,21 @@ test('should not detect email addresses or partial domain words', t => { }); test('should check account age for share actions', t => { - t.false(isUserOldEnoughForShareActions({ createdAt: new Date() })); + const minimumAccountAgeMs = 24 * 60 * 60 * 1000; + + t.false( + canUserExecuteLimitedActions({ createdAt: new Date() }, minimumAccountAgeMs) + ); t.true( - isUserOldEnoughForShareActions({ - createdAt: new Date(Date.now() - SHARE_ACTION_ACCOUNT_AGE_MS - 1), - }) + canUserExecuteLimitedActions( + { + createdAt: new Date(Date.now() - minimumAccountAgeMs - 1), + }, + minimumAccountAgeMs + ) ); }); + +test('should skip account age check when share action delay is disabled', t => { + t.true(canUserExecuteLimitedActions({ createdAt: new Date() }, 0)); +}); diff --git a/packages/backend/server/src/core/workspaces/abuse.ts b/packages/backend/server/src/core/workspaces/abuse.ts index 190899d9e9..a6fc8c1336 100644 --- a/packages/backend/server/src/core/workspaces/abuse.ts +++ b/packages/backend/server/src/core/workspaces/abuse.ts @@ -1,5 +1,3 @@ -export const SHARE_ACTION_ACCOUNT_AGE_MS = 24 * 60 * 60 * 1000; - const URL_OR_DOMAIN_PATTERN = /(?:https?:\/\/|www\.|(?= SHARE_ACTION_ACCOUNT_AGE_MS; +export function canUserExecuteLimitedActions( + user: { createdAt: Date }, + minimumAccountAgeMs: number +) { + if (minimumAccountAgeMs <= 0) return true; + return Date.now() - user.createdAt.getTime() >= minimumAccountAgeMs; } diff --git a/packages/backend/server/src/core/workspaces/resolvers/doc.ts b/packages/backend/server/src/core/workspaces/resolvers/doc.ts index 353c8255bf..951e74d64c 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/doc.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/doc.ts @@ -17,6 +17,7 @@ import { SafeIntResolver } from 'graphql-scalars'; import { ActionForbidden, Cache, + Config, DocActionDenied, DocDefaultRoleCanNotBeOwner, DocNotFound, @@ -45,7 +46,7 @@ import { PermissionService, } from '../../permission'; import { PublicUserType, WorkspaceUserType } from '../../user'; -import { isUserOldEnoughForShareActions } from '../abuse'; +import { canUserExecuteLimitedActions } from '../abuse'; import { DocGrantsService } from '../doc-grants'; import { WorkspaceType } from '../types'; import { TimeBucket, TimeWindow } from './analytics-types'; @@ -301,14 +302,27 @@ export class WorkspaceDocResolver { private readonly permission: PermissionService, private readonly models: Models, private readonly cache: Cache, - private readonly event: EventBus + private readonly event: EventBus, + private readonly config: Config ) {} - private async assertCanShare(userId: string) { + private async assertCanShare( + userId: string, + context: { workspaceId: string; docId: string; action: 'publishDoc' } + ) { const user = await this.models.user.get(userId); - if (!user || !isUserOldEnoughForShareActions(user)) { + const newAccountAgeMs = this.config.auth.newAccountShareActionDelay * 1000; + if (!user || !canUserExecuteLimitedActions(user, newAccountAgeMs)) { + this.logger.warn('Share action blocked for new account', { + userId, + email: user?.email, + createdAt: user?.createdAt, + accountAgeMs: user ? Date.now() - user.createdAt.getTime() : null, + minimumAccountAgeMs: newAccountAgeMs, + ...context, + }); throw new ActionForbidden( - 'Sharing links is unavailable during the first 24 hours after signup.' + 'This feature is temporarily unavailable for you.' ); } } @@ -452,7 +466,11 @@ export class WorkspaceDocResolver { } await this.ac.user(user.id).doc(workspaceId, docId).assert('Doc.Publish'); - await this.assertCanShare(user.id); + await this.assertCanShare(user.id, { + workspaceId, + docId, + action: 'publishDoc', + }); const doc = await this.models.doc.publish(workspaceId, docId, mode); this.event.emit('doc.public_state.changed', { workspaceId, docId }); diff --git a/packages/backend/server/src/core/workspaces/resolvers/member.ts b/packages/backend/server/src/core/workspaces/resolvers/member.ts index da43be2ddd..7a777910b2 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/member.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/member.ts @@ -1,3 +1,4 @@ +import { Logger } from '@nestjs/common'; import { Args, Int, @@ -21,6 +22,7 @@ import { AuthenticationRequired, Cache, CanNotRevokeYourself, + Config, EventBus, InvalidInvitation, isValidCacheTtl, @@ -46,7 +48,7 @@ import { import { QuotaService } from '../../quota'; import { UserType } from '../../user'; import { validators } from '../../utils/validators'; -import { containsUrlOrDomain, isUserOldEnoughForShareActions } from '../abuse'; +import { canUserExecuteLimitedActions, containsUrlOrDomain } from '../abuse'; import { WorkspaceService } from '../service'; import { InvitationType, @@ -64,6 +66,8 @@ import { */ @Resolver(() => WorkspaceType) export class WorkspaceMemberResolver { + private readonly logger = new Logger(WorkspaceMemberResolver.name); + constructor( private readonly cache: Cache, private readonly event: EventBus, @@ -73,14 +77,30 @@ export class WorkspaceMemberResolver { private readonly mutex: RequestMutex, private readonly policy: WorkspacePolicyService, private readonly workspaceService: WorkspaceService, - private readonly quota: QuotaService + private readonly quota: QuotaService, + private readonly config: Config ) {} - private async assertCanInviteOrShare(userId: string) { + private async assertCanInviteOrShare( + userId: string, + context: { + workspaceId: string; + action: 'inviteMembers' | 'createInviteLink'; + } + ) { const user = await this.models.user.get(userId); - if (!user || !isUserOldEnoughForShareActions(user)) { + const newAccountAgeMs = this.config.auth.newAccountShareActionDelay * 1000; + if (!user || !canUserExecuteLimitedActions(user, newAccountAgeMs)) { + this.logger.warn('Share action blocked for new account', { + userId, + email: user?.email, + createdAt: user?.createdAt, + accountAgeMs: user ? Date.now() - user.createdAt.getTime() : null, + minimumAccountAgeMs: newAccountAgeMs, + ...context, + }); throw new ActionForbidden( - 'Inviting members and creating share links are unavailable during the first 24 hours after signup.' + 'This feature is temporarily unavailable for you.' ); } } @@ -169,7 +189,10 @@ export class WorkspaceMemberResolver { .user(me.id) .workspace(workspaceId) .assert('Workspace.Users.Manage'); - await this.assertCanInviteOrShare(me.id); + await this.assertCanInviteOrShare(me.id, { + workspaceId, + action: 'inviteMembers', + }); await this.assertWorkspaceNameCanInvite(workspaceId); if (emails.length > 512) { @@ -302,7 +325,10 @@ export class WorkspaceMemberResolver { .user(user.id) .workspace(workspaceId) .assert('Workspace.Users.Manage'); - await this.assertCanInviteOrShare(user.id); + await this.assertCanInviteOrShare(user.id, { + workspaceId, + action: 'createInviteLink', + }); await this.assertWorkspaceNameCanInvite(workspaceId); const cacheWorkspaceId = `workspace:inviteLink:${workspaceId}`; diff --git a/packages/backend/server/src/plugins/copilot/mcp/provider.ts b/packages/backend/server/src/plugins/copilot/mcp/provider.ts index 8ed8bcb191..16927cef43 100644 --- a/packages/backend/server/src/plugins/copilot/mcp/provider.ts +++ b/packages/backend/server/src/plugins/copilot/mcp/provider.ts @@ -190,11 +190,15 @@ export class WorkspaceMcpProvider { const abortedAfterDocs = abortIfNeeded(options.signal); if (abortedAfterDocs) return abortedAfterDocs; + if (!docs || docs.length === 0) { + return toolText('No matching documents found.'); + } + return { - content: (docs?.map(doc => ({ + content: docs.map(doc => ({ type: 'text', text: clearEmbeddingChunk(doc).content, - })) ?? []), + })), }; }, }); @@ -235,10 +239,10 @@ export class WorkspaceMcpProvider { } return { - content: (docs?.map(doc => ({ + content: docs.map(doc => ({ type: 'text', text: JSON.stringify(pick(doc, 'docId', 'title', 'createdAt')), - })) ?? []), + })), }; }, }); diff --git a/packages/common/native/src/doc_parser/markdown/parser.rs b/packages/common/native/src/doc_parser/markdown/parser.rs index ea7194d997..efe6db0761 100644 --- a/packages/common/native/src/doc_parser/markdown/parser.rs +++ b/packages/common/native/src/doc_parser/markdown/parser.rs @@ -740,10 +740,8 @@ fn parse_markdown_inner(markdown: &str) -> Result inline.push(InlineAttr::link(dest_url.to_string())); } } - Event::End(TagEnd::Link) => { - if pending_bookmark.is_none() { - inline.pop(InlineAttr::new(InlineStyle::Link)); - } + Event::End(TagEnd::Link) if pending_bookmark.is_none() => { + inline.pop(InlineAttr::new(InlineStyle::Link)); } _ => {} } diff --git a/packages/common/y-octo/core/src/doc/types/mod.rs b/packages/common/y-octo/core/src/doc/types/mod.rs index 3468952123..86de148ec8 100644 --- a/packages/common/y-octo/core/src/doc/types/mod.rs +++ b/packages/common/y-octo/core/src/doc/types/mod.rs @@ -136,11 +136,11 @@ impl YTypeRef { #[allow(dead_code)] pub fn read(&self) -> Option<(RwLockReadGuard<'_, DocStore>, RwLockReadGuard<'_, YType>)> { - self.store().and_then(|store| self.ty().map(|ty| (store, ty))) + self.store().zip(self.ty()) } pub fn write(&self) -> Option<(RwLockWriteGuard<'_, DocStore>, RwLockWriteGuard<'_, YType>)> { - self.store_mut().and_then(|store| self.ty_mut().map(|ty| (store, ty))) + self.store_mut().zip(self.ty_mut()) } } diff --git a/packages/frontend/admin/src/config.json b/packages/frontend/admin/src/config.json index 95930fc08d..8f61424ec0 100644 --- a/packages/frontend/admin/src/config.json +++ b/packages/frontend/admin/src/config.json @@ -79,6 +79,10 @@ "type": "Boolean", "desc": "Whether require email verification before accessing restricted resources(not implemented)." }, + "newAccountShareActionDelay": { + "type": "Number", + "desc": "Minimum account age in seconds before new accounts can invite members or create share links." + }, "passwordRequirements": { "type": "Object", "desc": "The password strength requirements when set new password." diff --git a/packages/frontend/admin/src/modules/settings/config.ts b/packages/frontend/admin/src/modules/settings/config.ts index e44ce454a3..e47e18cc0d 100644 --- a/packages/frontend/admin/src/modules/settings/config.ts +++ b/packages/frontend/admin/src/modules/settings/config.ts @@ -62,6 +62,11 @@ export const KNOWN_CONFIG_GROUPS = [ fields: [ 'allowSignup', 'allowSignupForOauth', + { + key: 'newAccountShareActionDelay', + type: 'Number', + desc: 'Minimum account age in seconds before new accounts can invite members or create share links.', + }, // nested json object { key: 'passwordRequirements', diff --git a/packages/frontend/apps/electron/src/main/auth/handlers.ts b/packages/frontend/apps/electron/src/main/auth/handlers.ts index e11e017279..895f3465ae 100644 --- a/packages/frontend/apps/electron/src/main/auth/handlers.ts +++ b/packages/frontend/apps/electron/src/main/auth/handlers.ts @@ -8,7 +8,13 @@ import { setNativeAuthToken, } from './native-token'; -interface SignInResponse { +export interface SignInResponse { + id?: string; + email?: string; + name?: string; + hasPassword?: boolean | null; + avatarUrl?: string | null; + emailVerified?: boolean; exchangeCode?: string; redirectUri?: string; } @@ -97,7 +103,8 @@ export const authHandlers = { token, client_nonce: clientNonce, }); - await exchangeSession(endpoint, await readJson(response)); + const body = await readJson(response); + await exchangeSession(endpoint, body); }, signInOauth: async ( @@ -145,7 +152,9 @@ export const authHandlers = { password: credential.password, }), }); - await exchangeSession(endpoint, await readJson(response)); + const body = await readJson(response); + await exchangeSession(endpoint, body); + return body; }, signInOpenAppSignInCode: async (_e, endpoint: string, code: string) => { diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index e7839f19f7..e55a691bba 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -74,7 +74,7 @@ "lit": "^3.2.1", "lodash-es": "^4.17.23", "lottie-react": "^2.4.0", - "mermaid": "^11.13.0", + "mermaid": "^11.15.0", "mp4-muxer": "^5.2.2", "nanoid": "^5.1.6", "next-themes": "^0.4.4", diff --git a/packages/frontend/core/src/modules/cloud/impl/auth.ts b/packages/frontend/core/src/modules/cloud/impl/auth.ts index 5ca70c20c0..b0e1962be8 100644 --- a/packages/frontend/core/src/modules/cloud/impl/auth.ts +++ b/packages/frontend/core/src/modules/cloud/impl/auth.ts @@ -70,7 +70,7 @@ export function configureDefaultAuthProvider(framework: Framework) { headers['x-captcha-challenge'] = credential.challenge; } - await fetchService.fetch('/api/auth/sign-in', { + const res = await fetchService.fetch('/api/auth/sign-in', { method: 'POST', body: JSON.stringify(credential), headers: { @@ -78,6 +78,7 @@ export function configureDefaultAuthProvider(framework: Framework) { ...headers, }, }); + return await res.json(); }, async signInOpenAppSignInCode(code: string) { await fetchService.fetch('/api/auth/open-app/sign-in', { diff --git a/packages/frontend/core/src/modules/cloud/provider/auth.ts b/packages/frontend/core/src/modules/cloud/provider/auth.ts index 5f34887174..6776548a4a 100644 --- a/packages/frontend/core/src/modules/cloud/provider/auth.ts +++ b/packages/frontend/core/src/modules/cloud/provider/auth.ts @@ -1,5 +1,14 @@ import { createIdentifier } from '@toeverything/infra'; +export interface SignInUserInfo { + id: string; + email: string; + name: string; + hasPassword: boolean | null; + avatarUrl: string | null; + emailVerified: boolean; +} + export interface AuthProvider { signInMagicLink( email: string, @@ -19,7 +28,7 @@ export interface AuthProvider { password: string; verifyToken?: string; challenge?: string; - }): Promise; + }): Promise; signInOpenAppSignInCode(code: string): Promise; diff --git a/packages/frontend/core/src/modules/cloud/services/auth.ts b/packages/frontend/core/src/modules/cloud/services/auth.ts index fac156fdbc..c81c21febf 100644 --- a/packages/frontend/core/src/modules/cloud/services/auth.ts +++ b/packages/frontend/core/src/modules/cloud/services/auth.ts @@ -235,7 +235,10 @@ export class AuthService extends Service { }) { track.$.$.auth.signIn({ method: 'password' }); try { - await this.store.signInPassword(credential); + const user = await this.store.signInPassword(credential); + if (user) { + this.store.setCachedSignInUser(user); + } this.session.revalidate(); track.$.$.auth.signedIn({ method: 'password' }); } catch (e) { diff --git a/packages/frontend/core/src/modules/cloud/stores/auth.ts b/packages/frontend/core/src/modules/cloud/stores/auth.ts index 0e76dcc114..75c9afe8f4 100644 --- a/packages/frontend/core/src/modules/cloud/stores/auth.ts +++ b/packages/frontend/core/src/modules/cloud/stores/auth.ts @@ -9,7 +9,7 @@ import { Store } from '@toeverything/infra'; import type { GlobalState, NbstoreService } from '../../storage'; import type { AuthSessionInfo } from '../entities/session'; -import type { AuthProvider } from '../provider/auth'; +import type { AuthProvider, SignInUserInfo } from '../provider/auth'; import type { FetchService } from '../services/fetch'; import type { GraphQLService } from '../services/graphql'; import type { ServerService } from '../services/server'; @@ -57,6 +57,25 @@ export class AuthStore extends Store { this.globalState.set(`${this.serverService.server.id}-auth`, session); } + setCachedSignInUser(user: SignInUserInfo) { + this.setCachedAuthSession({ + account: { + id: user.id, + email: user.email, + label: user.name, + avatar: user.avatarUrl, + info: { + id: user.id, + email: user.email, + name: user.name, + hasPassword: Boolean(user.hasPassword), + avatarUrl: user.avatarUrl, + emailVerified: user.emailVerified ? 'true' : null, + }, + }, + }); + } + getClientNonce() { return this.globalState.get('auth-client-nonce'); } @@ -67,7 +86,7 @@ export class AuthStore extends Store { async fetchSession() { const { user } = await this.fetchService - .fetch('/api/auth/session') + .fetch('/api/auth/session', { cache: 'no-store' }) .then(res => res.json()); const authMethods = user ? await this.fetchService @@ -109,7 +128,7 @@ export class AuthStore extends Store { verifyToken?: string; challenge?: string; }) { - await this.authProvider.signInPassword(credential); + return await this.authProvider.signInPassword(credential); } async signInOpenAppSignInCode(code: string) { diff --git a/packages/frontend/native/nbstore/src/indexer.rs b/packages/frontend/native/nbstore/src/indexer.rs index 10c31fcf19..5a8ee894e3 100644 --- a/packages/frontend/native/nbstore/src/indexer.rs +++ b/packages/frontend/native/nbstore/src/indexer.rs @@ -112,7 +112,7 @@ impl SqliteDocStorage { return Ok(None); } - updates.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)); + updates.sort_by_key(|a| a.timestamp); let mut segments = Vec::with_capacity(snapshot.as_ref().map(|_| 1).unwrap_or(0) + updates.len()); if let Some(record) = snapshot { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f92020c653..ce98f16495 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.95.0" +channel = "1.96.0" profile = "default" diff --git a/tests/blocksuite/e2e/clipboard/clipboard.spec.ts b/tests/blocksuite/e2e/clipboard/clipboard.spec.ts index 6d22eaf409..d7a7907710 100644 --- a/tests/blocksuite/e2e/clipboard/clipboard.spec.ts +++ b/tests/blocksuite/e2e/clipboard/clipboard.spec.ts @@ -8,6 +8,7 @@ import { dragOverTitle, enterPlaygroundRoom, focusRichText, + focusRichTextEnd, focusTitle, getClipboardHTML, getClipboardSnapshot, @@ -342,7 +343,7 @@ test(scoped`paste parent block`, async ({ page }) => { await type(page, 'This is child 2'); await setInlineRangeInSelectedRichText(page, 0, 3); await copyByKeyboard(page); - await focusRichText(page, 2); + await focusRichTextEnd(page, 2); await page.keyboard.press(`${SHORT_KEY}+v`); await assertRichTexts(page, [ 'This is parent', @@ -363,7 +364,7 @@ test(scoped`clipboard copy multi selection`, async ({ page }) => { await waitNextFrame(page); await copyByKeyboard(page); await waitNextFrame(page); - await focusRichText(page, 1); + await focusRichTextEnd(page, 1); await pasteByKeyboard(page); await waitNextFrame(page); await type(page, 'cursor'); diff --git a/tests/blocksuite/e2e/clipboard/list.spec.ts b/tests/blocksuite/e2e/clipboard/list.spec.ts index a11b725085..2497cd6b19 100644 --- a/tests/blocksuite/e2e/clipboard/list.spec.ts +++ b/tests/blocksuite/e2e/clipboard/list.spec.ts @@ -12,6 +12,7 @@ import { dragBetweenCoords, enterPlaygroundRoom, focusRichText, + focusRichTextEnd, getAllNoteIds, getClipboardHTML, getClipboardSnapshot, @@ -192,7 +193,7 @@ test('paste a nested list to a nested list', async ({ page }) => { // paste on end await undoByKeyboard(page); - await page.keyboard.press('Control+ArrowRight'); + await focusRichTextEnd(page, 1); /** * - aaa @@ -284,7 +285,7 @@ test('paste nested lists to a nested list', async ({ page }) => { // paste on end await undoByKeyboard(page); - await page.keyboard.press('Control+ArrowRight'); + await focusRichTextEnd(page, 1); /** * - aaa diff --git a/tests/blocksuite/e2e/database/calendar.spec.ts b/tests/blocksuite/e2e/database/calendar.spec.ts index 5852a80181..5a32af997e 100644 --- a/tests/blocksuite/e2e/database/calendar.spec.ts +++ b/tests/blocksuite/e2e/database/calendar.spec.ts @@ -9,6 +9,28 @@ import { } from '../utils/actions/index.js'; import { scoped, test } from '../utils/playwright.js'; +const currentMonthAnchor = () => { + const date = new Date(); + date.setHours(0, 0, 0, 0); + date.setDate(1); + return date.getTime(); +}; + +const timestampFromMonthAnchor = ({ + monthAnchor, + day, +}: { + monthAnchor: number; + day: number; +}) => { + const date = new Date(monthAnchor); + date.setDate(day); + return date.getTime(); +}; + +const getMonthTimestamp = (page: Page, monthAnchor: number, day: number) => + page.evaluate(timestampFromMonthAnchor, { monthAnchor, day }); + const createCalendarDatabase = async ( page: Page, options?: { @@ -20,6 +42,10 @@ const createCalendarDatabase = async ( withEndDateColumn?: boolean; } ) => { + const monthAnchor = await page.evaluate(currentMonthAnchor); + const dateTimestamp = await getMonthTimestamp(page, monthAnchor, 15); + const endDateTimestamp = await getMonthTimestamp(page, monthAnchor, 17); + return page.evaluate( ({ withDateColumn, @@ -28,6 +54,9 @@ const createCalendarDatabase = async ( rowCount, linkedDocTitle, withEndDateColumn, + dateTimestamp, + endDateTimestamp, + monthAnchor, }) => { const { doc } = window; const rows = rowCount ?? 1; @@ -93,20 +122,12 @@ const createCalendarDatabase = async ( name: 'End Date', }) : undefined; - if (dateColumnId) { - for (const id of rowIds) { - datasource.cellValueChange( - id, - dateColumnId, - new Date('2026-05-15T00:00:00').getTime() - ); - if (endDateColumnId) { - datasource.cellValueChange( - id, - endDateColumnId, - new Date('2026-05-17T00:00:00').getTime() - ); - } + for (const id of rowIds) { + if (dateColumnId) { + datasource.cellValueChange(id, dateColumnId, dateTimestamp); + } + if (endDateColumnId) { + datasource.cellValueChange(id, endDateColumnId, endDateTimestamp); } } const viewId = datasource.viewManager.viewAdd('calendar'); @@ -135,9 +156,10 @@ const createCalendarDatabase = async ( endDateColumnId, viewId, linkedDocId, + monthAnchor, }; }, - options ?? {} + { ...options, dateTimestamp, endDateTimestamp, monthAnchor } ); }; @@ -212,9 +234,7 @@ test(scoped`database calendar creates row from empty day`, async ({ page }) => { await expect(page.locator('affine-data-view-record-detail')).toBeVisible(); - const expectedDate = await page.evaluate(() => - new Date('2026-05-20T00:00:00').getTime() - ); + const expectedDate = await getMonthTimestamp(page, ids.monthAnchor, 20); await expect .poll(() => page.evaluate( @@ -327,9 +347,7 @@ test( .first(); await entry.dragTo(targetDay); - const expectedDate = await page.evaluate(() => - new Date('2026-05-20T00:00:00').getTime() - ); + const expectedDate = await getMonthTimestamp(page, ids.monthAnchor, 20); await expect .poll(async () => page.evaluate(({ databaseId, rowId, dateColumnId }) => { @@ -390,12 +408,8 @@ test( .first(); await entry.dragTo(targetDay); - const expectedStart = await page.evaluate(() => - new Date('2026-05-20T00:00:00').getTime() - ); - const expectedEnd = await page.evaluate(() => - new Date('2026-05-22T00:00:00').getTime() - ); + const expectedStart = await getMonthTimestamp(page, ids.monthAnchor, 20); + const expectedEnd = await getMonthTimestamp(page, ids.monthAnchor, 22); await expect .poll(() => page.evaluate( @@ -456,9 +470,7 @@ test(scoped`database calendar resizes row range end date`, async ({ page }) => { await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + 16); await page.mouse.up(); - const expectedEnd = await page.evaluate(() => - new Date('2026-05-20T00:00:00').getTime() - ); + const expectedEnd = await getMonthTimestamp(page, ids.monthAnchor, 20); await expect .poll(() => page.evaluate(({ databaseId, rowId, endDateColumnId }) => { diff --git a/tests/blocksuite/e2e/utils/actions/misc.ts b/tests/blocksuite/e2e/utils/actions/misc.ts index bfcff6b6e7..35e59217b8 100644 --- a/tests/blocksuite/e2e/utils/actions/misc.ts +++ b/tests/blocksuite/e2e/utils/actions/misc.ts @@ -667,7 +667,11 @@ export async function getInlineSelectionIndex(page: Page) { const selection = window.getSelection() as Selection; const range = selection.getRangeAt(0); - const component = range.startContainer.parentElement?.closest('rich-text'); + const startElement = + range.startContainer instanceof Element + ? range.startContainer + : range.startContainer.parentElement; + const component = startElement?.closest('rich-text'); const index = component?.inlineEditor?.getInlineRange()?.index; return index !== undefined ? index : -1; }); @@ -677,7 +681,11 @@ export async function getInlineSelectionText(page: Page) { return page.evaluate(() => { const selection = window.getSelection() as Selection; const range = selection.getRangeAt(0); - const component = range.startContainer.parentElement?.closest('rich-text'); + const startElement = + range.startContainer instanceof Element + ? range.startContainer + : range.startContainer.parentElement; + const component = startElement?.closest('rich-text'); return component?.inlineEditor?.yText.toString() ?? ''; }); } @@ -686,7 +694,11 @@ export async function getSelectedTextByInlineEditor(page: Page) { return page.evaluate(() => { const selection = window.getSelection() as Selection; const range = selection.getRangeAt(0); - const component = range.startContainer.parentElement?.closest('rich-text'); + const startElement = + range.startContainer instanceof Element + ? range.startContainer + : range.startContainer.parentElement; + const component = startElement?.closest('rich-text'); const inlineRange = component?.inlineEditor?.getInlineRange(); if (!inlineRange) return ''; @@ -736,12 +748,27 @@ export async function setInlineRangeInSelectedRichText( const selection = window.getSelection() as Selection; const range = selection.getRangeAt(0); - const component = - range.startContainer.parentElement?.closest('rich-text'); - component?.inlineEditor?.setInlineRange({ + const startElement = + range.startContainer instanceof Element + ? range.startContainer + : range.startContainer.parentElement; + const component = startElement?.closest('rich-text'); + const inlineEditor = component?.inlineEditor; + if (!inlineEditor) { + throw new Error('Cannot find inline editor from current selection'); + } + component.focus(); + inlineEditor.setInlineRange({ index, length, }); + const domRange = inlineEditor.toDomRange({ index, length }); + if (!domRange) { + throw new Error('Cannot remap inline range to DOM range'); + } + selection.removeAllRanges(); + selection.addRange(domRange); + document.dispatchEvent(new Event('selectionchange')); }, { index, length } ); @@ -946,6 +973,7 @@ export async function setSelection( length: 0, })!; + anchorRichText.focus(); const sl = getSelection(); if (!sl) throw new Error('Cannot get selection'); const range = document.createRange(); @@ -959,6 +987,7 @@ export async function setSelection( ); sl.removeAllRanges(); sl.addRange(range); + document.dispatchEvent(new Event('selectionchange')); }, { anchorBlockId, diff --git a/yarn.lock b/yarn.lock index a8a30fd287..bbe350fb5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -449,7 +449,7 @@ __metadata: lit: "npm:^3.2.1" lodash-es: "npm:^4.17.23" lottie-react: "npm:^2.4.0" - mermaid: "npm:^11.13.0" + mermaid: "npm:^11.15.0" mp4-muxer: "npm:^5.2.2" nanoid: "npm:^5.1.6" next-themes: "npm:^0.4.4" @@ -990,7 +990,7 @@ __metadata: "@types/sinon": "npm:^21.0.0" "@types/supertest": "npm:^7.0.0" ava: "npm:^7.0.0" - bullmq: "npm:5.53.0" + bullmq: "npm:5.77.6" c8: "npm:^10.1.3" commander: "npm:^13.1.0" cookie-parser: "npm:^1.4.7" @@ -18974,13 +18974,14 @@ __metadata: linkType: hard "axios@npm:^1.15.0": - version: 1.15.2 - resolution: "axios@npm:1.15.2" + version: 1.16.1 + resolution: "axios@npm:1.16.1" dependencies: - follow-redirects: "npm:^1.15.11" + follow-redirects: "npm:^1.16.0" form-data: "npm:^4.0.5" + https-proxy-agent: "npm:^5.0.1" proxy-from-env: "npm:^2.1.0" - checksum: 10/eebbd8cb777316d4252cd994a06ec9fb956ef519214a62dab6c5443ae8b753b5116e9a770502316789e6cdef1101e6aae53b6936d6a3791b2d66d75f4d7d2462 + checksum: 10/9b6218cf96321cfbbf8f160658d695367114bcf4fb62492bdc1ccd647f184b5c71ae400e5ecaaf41079bc561de2ecbaf1fec63f398b3ec53389beff7694df64c languageName: node linkType: hard @@ -19367,18 +19368,22 @@ __metadata: languageName: node linkType: hard -"bullmq@npm:5.53.0": - version: 5.53.0 - resolution: "bullmq@npm:5.53.0" +"bullmq@npm:5.77.6": + version: 5.77.6 + resolution: "bullmq@npm:5.77.6" dependencies: - cron-parser: "npm:^4.9.0" - ioredis: "npm:^5.4.1" - msgpackr: "npm:^1.11.2" - node-abort-controller: "npm:^3.1.1" - semver: "npm:^7.5.4" - tslib: "npm:^2.0.0" - uuid: "npm:^9.0.0" - checksum: 10/470a9cb63d32b6ce8fe0275a249ba983167f6d5f494b4add1fc907a0198e178ece1c8874dc59030f232dab943f4546ecdf95c2b0f1d35c655341c970cbd0d3d9 + cron-parser: "npm:4.9.0" + ioredis: "npm:5.10.1" + msgpackr: "npm:2.0.1" + node-abort-controller: "npm:3.1.1" + semver: "npm:7.8.0" + tslib: "npm:2.8.1" + peerDependencies: + redis: ">=5.0.0" + peerDependenciesMeta: + redis: + optional: true + checksum: 10/6003707988972389574e24ae2685152dc68d53d9a4b51d49866f2910c56cc51dbf8ce301e90f289ee5a9e0c0b233fd96b27a0fca105144642074836e4565f8df languageName: node linkType: hard @@ -20792,7 +20797,7 @@ __metadata: languageName: node linkType: hard -"cron-parser@npm:^4.9.0": +"cron-parser@npm:4.9.0": version: 4.9.0 resolution: "cron-parser@npm:4.9.0" dependencies: @@ -23740,7 +23745,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.11": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.16.0": version: 1.16.0 resolution: "follow-redirects@npm:1.16.0" peerDependenciesMeta: @@ -25068,7 +25073,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^5.0.0": +"https-proxy-agent@npm:^5.0.0, https-proxy-agent@npm:^5.0.1": version: 5.0.1 resolution: "https-proxy-agent@npm:5.0.1" dependencies: @@ -27780,7 +27785,7 @@ __metadata: languageName: node linkType: hard -"mermaid@npm:^11.13.0": +"mermaid@npm:^11.15.0": version: 11.15.0 resolution: "mermaid@npm:11.15.0" dependencies: @@ -28545,15 +28550,15 @@ __metadata: languageName: node linkType: hard -"msgpackr@npm:^1.11.2": - version: 1.11.10 - resolution: "msgpackr@npm:1.11.10" +"msgpackr@npm:2.0.1": + version: 2.0.1 + resolution: "msgpackr@npm:2.0.1" dependencies: msgpackr-extract: "npm:^3.0.2" dependenciesMeta: msgpackr-extract: optional: true - checksum: 10/e210128fac395b6173cb6784926eec724ea5e3fca72639cd07fb0af762f4027a4026bb4096703bd3b8510eee7e9b351fd52721ddf9bc2091e354d5dff93d45dd + checksum: 10/9b51a18832e86b27a3187cea5dfbff5d146d643f4beb24f8cf086a5c5a1ae3e4bfaa0459c7fd9b1cca6d2d68cfc3c03b2235ff63f3895d9d8a46bceda241ec4f languageName: node linkType: hard @@ -28812,7 +28817,7 @@ __metadata: languageName: node linkType: hard -"node-abort-controller@npm:^3.1.1": +"node-abort-controller@npm:3.1.1": version: 3.1.1 resolution: "node-abort-controller@npm:3.1.1" checksum: 10/0a2cdb7ec0aeaf3cb31e1ca0e192f5add48f1c5c9c9ed822129f9dddbd9432f69b7425982f94ce803c56a2104884530aa67cd57696e5774b2e5b8ec2f58de042 @@ -32503,6 +32508,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:7.8.0, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3, semver@npm:^7.7.4": + version: 7.8.0 + resolution: "semver@npm:7.8.0" + bin: + semver: bin/semver.js + checksum: 10/039a8f68a581c03c1ac17c990316da57a79a93af9b109b712739c50cd4d464079f7e3fee31c008b472e390c7ba48a11ed2b86e91d8602bf06059d4a266db1426 + languageName: node + linkType: hard + "semver@npm:^6.2.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -32512,15 +32526,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3, semver@npm:^7.7.4": - version: 7.8.0 - resolution: "semver@npm:7.8.0" - bin: - semver: bin/semver.js - checksum: 10/039a8f68a581c03c1ac17c990316da57a79a93af9b109b712739c50cd4d464079f7e3fee31c008b472e390c7ba48a11ed2b86e91d8602bf06059d4a266db1426 - languageName: node - linkType: hard - "semver@npm:~7.7.3": version: 7.7.4 resolution: "semver@npm:7.7.4" @@ -35033,7 +35038,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0, uuid@npm:^9.0.1": +"uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: