ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

tldraw 同步服务端评论写授权:深入 @tldraw/sync-collaboration 的 createCommentAuthorizers 实战指南

tldraw 同步服务端评论写授权:深入 @tldraw/sync-collaboration 的 createCommentAuthorizers 实战指南 tldraw 同步服务端评论写授权深入 tldraw/sync-collaboration 的 createCommentAuthorizers 实战指南【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldrawtldraw/sync-collaboration是 tldraw SDK 中专门承载协作评论服务端逻辑的轻量包它为评论comment、评论线程comment-thread与评论表情回应comment-reaction三类记录提供服务端写授权write authorization用于注入同步服务器如TLSocketRoom的authorizeRecord选项。本文将以该包为骨架结合其源码、测试与配套的tldraw/sync-core、tldraw/tlschema实现完整讲解三种回调的语义、每条授权规则的来龙去脉、接入步骤与底层原理帮助你为自己的多人在线白板搭建身份不可伪造、归属不可篡改、删除受限的评论后端。包的定位只做服务端授权零客户端依赖tldraw/sync-collaboration的 README 开宗明义它提供 tldraw 协作功能的服务端逻辑——针对 comment、comment-thread、comment-reaction 记录的写授权专供同步服务器的authorizeRecord选项使用该选项定义于tldraw/sync-core的TLSocketRoom。其关键约束是This package has no react or client-editor dependencies, so it is safe to import from workers and node servers.这意味着它可以在 Cloudflare Workers、Node 服务器等任何同步服务端环境中安全引入不会把 React 或编辑器客户端代码拖进服务端 bundle。从 package.json 可见其依赖仅为tldraw/store、tldraw/sync-core、tldraw/tlschema、tldraw/utils均以workspace:*形式引用且要求 Node22.12.0。同时它在 tldraw 产品体系中属于 Commenting 特性tldraw_product.stableId: tldraw:commentingpremium: truelicenseFlagFEAT_COMMENTING是一个付费business license能力。包的核心导出位于 src/index.tscreateCommentAuthorizers函数以及CommentAuthorizerOptions、CommentModification、CommentModificationAuthContext三个类型。核心 APIcreateCommentAuthorizers 与三个回调全部实现集中在 src/comment-authorizers.ts。函数签名为export function createCommentAuthorizersSessionMeta( opts: CommentAuthorizerOptionsSessionMeta ): TLRecordAuthorizersTLComment | TLCommentThread | TLCommentReaction, SessionMeta它接收配置对象返回一个以typeName为键的授权器映射TLRecordAuthorizers可直接展开进TLSocketRoom的authorizeRecord。配置项共三个配置项必填默认值作用getUserId✅ 必填无从会话的宿主自定义meta中解析出已认证用户 id匿名会话返回nullcanComment可选({ isReadonly }) !isReadonly会话是否允许写入任何评论记录在逐类型规则之前被检查canModifyComment可选({ userId, ownerId }) userId ownerId谁可以编辑评论、删除评论、删除线程默认仅限记录归属者getUserId身份的唯一来源getUserId(session: { sessionId: string; isReadonly: boolean; meta: SessionMeta }): string | null这是身份属性attribution的唯一来源。匿名会话返回null无法创建任何评论记录因为没有身份可供盖章它们也不拥有任何记录因此默认的canModifyComment不会授予它们任何编辑或删除权限。源码中withUserId包装器保证每个授权器在一次授权中只解析一次用户 idgetUserId(args.session)测试 comment-authorizers.test.ts#L46-L61 专门验证了这一点——防止不纯的宿主回调在同一写入的不同检查点之间出现身份分歧。canComment总闸门在任何逐类型规则之前withUserId会先执行if (!canComment(args.session)) return null。默认实现把评论写入与画布读写状态绑定只读会话isReadonly: true可以阅读线程但不能发帖。覆盖为() true则可以实现只读画布上也能评论的评论专属通道覆盖为({ meta }) meta.userId ! banned则可实现按会话元数据拉黑。测试 comment-authorizers.test.ts#L517-L539 演示了用自定义canComment允许只读会话发言的场景。canModifyComment三种修改的细粒度裁定canModifyComment?(ctx: CommentModificationAuthContextSessionMeta): boolean它只裁定三种写操作对应CommentModification联合类型export type CommentModification | { readonly action: edit-comment; readonly comment: TLComment } | { readonly action: delete-comment; readonly comment: TLComment } | { readonly action: delete-thread; readonly thread: TLCommentThread }CommentModificationAuthContext在修改信息之上补充了会话、请求者与归属者信息export type CommentModificationAuthContextSessionMeta { readonly session: { sessionId: string; isReadonly: boolean; meta: SessionMeta } readonly userId: string | null readonly ownerId: string } CommentModification其中ownerId是记录的真实归属者评论取authorId、线程取createdBy这样放宽默认权限的回调就不必关心每条记录把归属存在哪个字段里。注释中还强调了一个容易踩坑的细节传入回调的comment/thread永远是房间内存储的记录the record the room holds而不是客户端提交的版本——否则一个宣称这条评论是我的且已被删除的恶意写入会让一个按归属判断的规则读到攻击者的谎言。测试 comment-authorizers.test.ts#L689-L721 用spying回调验证了这一保证。源码给出的典型放宽示例——管理员moderator可以删除任何人的评论或线程但编辑权仍归作者本人createCommentAuthorizersSessionMeta({ getUserId: (session) session.meta.userId, // Moderators may take anything down. Editing stays the authors, whoever you are. canModifyComment: (ctx) (ctx.action ! edit-comment isModerator(ctx.session.meta)) || ctx.userId ctx.ownerId, })三条授权规则逐类型的行为契约createCommentAuthorizers返回三个键comment、comment-thread、comment-reaction。每一条规则都先经canComment闸门再由内部结构规则裁决。comment归属盖章 结构字段冻结createauthorId由服务端从会话身份盖章覆盖客户端提交的任何值匿名创建被拒绝。测试 comment-authorizers.test.ts#L68-L82 验证了客户端声称自己是 alice、实际写入的却是会话用户 real-bob的场景。updateauthorId、threadId、createdAt全部不可变pageId是可变的——它是从线程反规范化denormalized出来的字段当锚定线程在页面间移动时线程内每条评论的pageId都会被重写。测试 comment-authorizers.test.ts#L162-L166 明确放行了pageId更新。可变threadId的隐患是让作者把评论重新挂到别的会话跨文件时甚至是别的文件可变createdAt则允许事后重新排序、甚至把评论钉到通知流顶部。comment-thread谁都能解决/重开但解决者不可伪造createcreatedBy、createdAt由服务端固定。任何有访问权限的会话都可以**解决resolve或重开reopen**线程——这些操作不属于任何人的私有财产因此只受canComment约束canModifyComment不会被询问测试 comment-authorizers.test.ts#L752-L781 验证了asked 0。但非空resolved.by必须是会话自己的用户解决这个动作本身是一次归属attribution不能替别人点掉。authorizeThreadResolution同时堵住了两个偷渡路径——create 时若resolved.by不是自己则拒绝防止删除重新放置伪造他人名下的解决记录update 时若解决状态发生变化且新resolved.by不是自己则拒绝。comment-reaction规范 id 防占位仅限本人删除createuserId被盖章且不可变记录的 id 必须落在(commentId, 会话用户, emoji)三元组对应的规范 id由createCommentReactionId生成上。否则伪造的客户端可以抢占别人的 id 槽位或在同一三元组上落两条记录。测试 comment-authorizers.test.ts#L354-L375 分别验证了抢占 alice 的槽位与id 里的 emoji 与字段里的 emoji 不一致都会被拒绝。update喂给 id 的一切字段commentId、threadId、pageId、emoji都不可变——因此换 emoji 是删除重建而非更新。delete只有回应者本人可以删除自己的回应。注意删除是刻意开放的例外任何人不能动别人的回应但级联删除cascade依然能清扫所有回应者的记录——因为服务端发起的写入不带会话会完全跳过授权器见下文调用契约从不需要客户端开放删除。软删除模型isDeleted 只写一次客户端硬删除永远被拒评论与线程的删除都是软删除isDeleted是**只写一次write-once**的标志且绝不允许在 create 时置位否则会绕过更新检查偷渡删除测试见 comment-authorizers.test.ts#L138-L143一旦置位就不可清除包括作者本人测试 comment-authorizers.test.ts#L126-L130客户端的type: delete硬删除永远被拒绝——记录移除是服务端专属操作一个既翻转isDeleted又改了其他内容的更新会被问两次先问 delete、再问 edit从而阻止仅授予删除权限被套利成顺带改内容。测试 comment-authorizers.test.ts#L631-L637 验证了删除夹带编辑会被一个 delete-only 权限拒绝。从 tlschema 的记录定义 可以看到TLCommentThread的isDeleted语义正是为此设计的Clients set the flag and leave the record in place. Sync servers are expected to enforce it as write-once and creator-only, reject client hard deletes, and drop flagged threads from future room loads.接入 TLSocketRoom最小可运行示例评论记录与文档记录并排存放因此需要两步扩展房间的记录联合类型然后把授权器展开进authorizeRecord映射。源码 JSDoc 给出的完整示例interface SessionMeta { userId: string | null } type MyRecord TLRecord | TLComment | TLCommentThread | TLCommentReaction new TLSocketRoomMyRecord, SessionMeta({ authorizeRecord: { ...createCommentAuthorizersSessionMeta({ getUserId: (session) session.meta.userId }), }, })别忘了注册记录 schema从 TLComment.ts 的注释 可知TLComment、TLCommentThread、TLCommentReaction均为选配记录不属于默认 schemaOpt-in: register withcreateTLSchema({ records: commentSchemaRecords })on the server and the matchingrecordsoption on the client — neither type is part of the default schema, and both sides must register them identically.因此服务端需要createTLSchema({ records: commentSchemaRecords })客户端也要用相同的方式注册否则这两类记录不会被识别和校验。另外这些记录被设计为走同步服务器的对象存储通道object-store lane由会话的objectAccess而非isReadonly门控从而可以表达能评论但不能编辑且不进入文档快照与.tldr导出。底层原理authorizeRecord 的调用契约理解了授权器行为之后再看它的运行环境——TLRecordAuthorizer的定义位于 packages/sync-core/src/lib/TLSyncRoom.ts#L124-L185。这段契约对正确使用至关重要export type TLRecordAuthorizerRec extends UnknownRecord, SessionMeta ( args: { session: { sessionId: string; isReadonly: boolean; meta: SessionMeta } } ( | { type: create; prev: null; next: Rec } | { type: update; prev: Rec; next: Rec } | { type: delete; prev: Rec; next: null } ) ) Rec | null关键语义只在客户端推送client pushes时调用服务端发起的写入例如级联删除、修剪软删除记录从不经过授权器——这正是反应级联能清扫所有用户记录的原因prev与next始终是服务端 schema 版本的记录客户端写入在授权器运行前已完成迁移因此守护或盖章字段时无需关心旧客户端叫它什么返回null表示拒绝——写入被跳过客户端像遭遇objectAccess拒绝一样自我纠偏本地应用、否决、rebase 掉create 时返回的记录会被存储校验之后且之后不再跑迁移因此盖的章不会被后续流程覆盖update/delete 时只使用允许 vs 拒绝的判定返回值内容被忽略⚠️ 授权器在提交事务内同步执行、与每次文档编辑同路径必须足够快且禁止任何 I/Onext/prev都是客户端可控记录必须视为不可信输入优先返回null而非抛异常——抛异常会被捕获、记录并视同拒绝fail closed不会让推送崩溃TLRecordAuthorizers是按typeName索引的映射只有列出的类型会被授权其余记录如形状拖拽原样通过因此不占用热路径。TLSocketRoom通过构造选项接收这份映射TLSocketRoom.ts#L150-L152并下发给内部的TLSyncRoom。此外还有两条实现层面的细节presence在场记录永远不会被授权若把 presence 的typeName注册进authorizeRecordTLSyncRoom构造时直接抛错TLSyncRoom.ts#L429-L434每条授权器都被类型精确绑定到对应记录next在comment条目里是TLComment因此重命名字段会让读取它的授权器编译失败而不是静默守护错字段。为什么结构规则不可逾越canModifyComment 只能放宽谁这是本包设计中最值得注意的一点canModifyComment在结构规则之后被询问因此无论回调多么宽松它只能拓宽谁可以写永远不能改变写入可以包含什么。具体来说即使canModifyComment: () true以下不变量依然成立测试套件the structural rules hold however permissive the callback逐一验证comment-authorizers.test.ts#L821-L923仍拒绝清除软删除标记write-once仍拒绝所有客户端硬删除仍拒绝修改评论的authorId、重新挂接threadId、回填createdAt仍拒绝 create 时预置isDeleted仍拒绝将线程解决状态归属于他人。其中唯一需要留意的是宽松的回调会对匿名会话兑现承诺。默认规则下匿名会话没有身份可核对自然被剥夺全部三种写操作但一个忽略userId、一律返回true的回调会把匿名会话也放行测试 comment-authorizers.test.ts#L913-L922 明确标注了这一点。在编写回调前值得确认你确实想对未认证用户开放写权限。一个完整的自定义授权示例综合以上规则一个作者可编辑、管理员可删不可改、封禁用户禁言、只读画布仍可评论的服务端配置可以这样组织import { createCommentAuthorizers } from tldraw/sync-collaboration interface SessionMeta { userId: string | null role?: user | moderator banned?: boolean } const commentAuthorizers createCommentAuthorizersSessionMeta({ getUserId: (session) session.meta.userId, // 只读画布上也可以评论评论走对象存储通道与画布编辑分离 canComment: (session) !session.meta.banned, canModifyComment: (ctx) { // 管理员可以删除任何评论或线程但不能替作者改写内容 if (ctx.action ! edit-comment ctx.session.meta.role moderator) return true // 否则沿用归属者本人规则 return ctx.userId ctx.ownerId }, }) new TLSocketRoomMyRecord, SessionMeta({ authorizeRecord: { ...commentAuthorizers }, // ...其余选项 })测试即规范用源码测试理解边界行为本包行为均有对应的 Vitest 用例背书comment-authorizers.test.ts 本身就是一份规则规范值得通读。几个与直觉相反、最能帮助理解设计的用例身份盖章优先create 时客户端提交authorId: client-claims-alice授权器返回的记录authorId是real-bob会话身份——你无法冒名发帖L68-L76解决与重开不属于任何人非创建者可以以自己的身份解决/重开线程canModifyComment根本不会被问到L190-L200, L752-L781换表情是删除重建直接 updateemoji会被拒绝因为 id 编码了 emojiL322-L326删除不能夹带编辑一个同时翻转isDeleted和改写body的更新对 delete-only 权限会被整体拒绝L631-L637级联依然工作反应删除虽仅限本人但服务端发起的级联写入不带会话、跳过授权器因此删除评论/线程时清扫所有回应不受影响L341-L349 注释。注意事项小结身份是硬前提getUserId必须可靠解析会话身份匿名会话不能创建记录默认也不能修改任何记录。授权器不是异步检查的地方它运行在提交事务内禁止 I/O昂贵的异步校验如解析 mention 指向谁能访问文件应通过onCommittedChanges事后响应见 TLSyncRoom.ts 的 JSDoc。客户端与服务端要同步放宽canModifyComment是tldraw/commenting中客户端同名选项的服务端镜像——客户端决定 UI 提供哪些操作入口服务端才是真正的规则。如果客户端提供了删除按钮而服务端拒绝删除会在本地应用、被否决、再被 rebase 掉评论会弹回来且没有任何解释源码注释明确描述了这一行为。记录 schema 需双端注册commentSchemaRecords不属于默认 schema服务端与客户端必须一致地通过records选项注册否则记录无法被识别。版本与许可前提本包为 Commenting 特性premium: truelicenseFlagFEAT_COMMENTING需要满足 tldraw SDK 许可条件运行环境要求 Node22.12.0。这些配置与约束均以当前仓库 package.json 为准。【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表