feat(communication): federate tenant collaboration with signed delivery

This commit is contained in:
2026-09-30 14:48:05 +02:00
parent ea641bb4d0
commit 7b5696eda4
16 changed files with 914 additions and 14 deletions

View File

@@ -10,6 +10,7 @@ export type CollaborationOptions = {
guestPepper: () => string;
sendMail: (to: string, subject: string, html: string) => Promise<{ success: boolean }>;
changed?: (roomId: string, db: any) => Promise<void>;
invited?: (ctx: UserContext, roomId: string, target: Principal, invitationId: string, db: any) => Promise<void>;
}
/** Owner-side authority. All membership changes and sends serialize on the room row. */
@@ -184,7 +185,8 @@ export class CollaborationService {
const body = text(input.text, 10000), clientId = uuid(input.clientId)
return this.db.transaction(async (db) => {
const room = await this.roomRow(db, roomId, true)
await this.membership(db, room, principal, true)
const member = await this.membership(db, room, principal, true)
if (principal.kind === "remote" && sequence(input.revision) !== Number(member.revision)) fail("Teilnahmerechte haben sich geändert", 409)
const [existing] = await rows(db, sql`SELECT id,client_id AS "clientId",seq::float8 AS seq,body,created_at AS "createdAt" FROM collaboration_messages
WHERE room_id=${room.id} AND principal_id=${principal.id} AND client_id=${clientId}`)
if (existing) {
@@ -271,6 +273,10 @@ export class CollaborationService {
VALUES (${room.id},${target.id},${hash(token)},${role},${input.overviewAllowed === true},${ctx.userId},
LEAST(now()+interval '7 days',${accessExpires}::timestamptz),${accessExpires}) RETURNING id,expires_at AS "expiresAt"`)
await this.audit(db, room, ctx.principal, "invitation.created", invite.id)
if (target.kind === "remote") {
if (!this.options.invited) fail("Föderation ist nicht konfiguriert", 503)
await this.options.invited(ctx, room.id, target, invite.id, db)
}
return invite
})
return { ...result, token }