From 59e1d760d621908b4accf32dde43b6b550b6c0e9 Mon Sep 17 00:00:00 2001 From: Nadeem Date: Thu, 4 Jun 2026 12:33:34 +0530 Subject: [PATCH] adds appType to distinguish rooms from ddocs and dsheets --- src/database/models/document-commit.ts | 6 + src/database/models/document-update.ts | 6 + src/database/models/session.ts | 3 + src/scripts/backfill-apptype.ts | 46 +++++ src/services/auth-middleware.ts | 1 + src/services/mongodb-store.ts | 2 + src/services/session-manager.ts | 8 +- src/services/socket-handlers.ts | 31 +++ src/tests/services/session-manager.test.ts | 58 ++++++ .../socket-handlers.handleAuth.test.ts | 177 ++++++++++++++++++ .../socket-handlers.handleAwareness.test.ts | 1 + ...cket-handlers.handleDocumentCommit.test.ts | 33 ++++ ...cket-handlers.handleDocumentUpdate.test.ts | 33 ++++ src/types/index.ts | 12 ++ 14 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 src/scripts/backfill-apptype.ts diff --git a/src/database/models/document-commit.ts b/src/database/models/document-commit.ts index 5d3bfa9..cc55ccc 100644 --- a/src/database/models/document-commit.ts +++ b/src/database/models/document-commit.ts @@ -9,6 +9,7 @@ interface IDocumentCommit extends MongooseDocument { updates: string[]; createdAt: number; sessionDid: string; + appType: "ddoc" | "dsheet"; } const DocumentCommitSchema = new Schema({ @@ -20,12 +21,17 @@ const DocumentCommitSchema = new Schema({ updates: [{ type: String }], createdAt: { type: Number, required: true, index: true }, sessionDid: { type: String, required: true }, + // Which Fileverse app produced this commit. Absent ⇒ "ddoc" (legacy). + appType: { type: String, enum: ["ddoc", "dsheet"], default: "ddoc" }, }); DocumentCommitSchema.index({ documentId: 1, createdAt: -1 }, { background: true }); DocumentCommitSchema.index({ documentId: 1, sessionDid: 1 }, { background: true }); +// Supports per-app lifecycle/analytics queries (e.g. "all dsheet commits"). +DocumentCommitSchema.index({ appType: 1, createdAt: 1 }, { background: true }); + export const DocumentCommitModel = mongoose.model( "DocumentCommit", DocumentCommitSchema diff --git a/src/database/models/document-update.ts b/src/database/models/document-update.ts index dc30547..7726cce 100644 --- a/src/database/models/document-update.ts +++ b/src/database/models/document-update.ts @@ -9,6 +9,7 @@ interface IDocumentUpdate extends MongooseDocument { commitCid: string | null; createdAt: number; sessionDid: string; + appType: "ddoc" | "dsheet"; } const DocumentUpdateSchema = new Schema({ @@ -20,6 +21,8 @@ const DocumentUpdateSchema = new Schema({ commitCid: { type: String, default: null }, createdAt: { type: Number, required: true, index: true }, sessionDid: { type: String, required: true }, + // Which Fileverse app produced this update. Absent ⇒ "ddoc" (legacy). + appType: { type: String, enum: ["ddoc", "dsheet"], default: "ddoc" }, }); DocumentUpdateSchema.index({ documentId: 1, committed: 1, createdAt: 1 }, { background: true }); @@ -34,6 +37,9 @@ DocumentUpdateSchema.index( DocumentUpdateSchema.index({ documentId: 1, sessionDid: 1 }, { background: true }); +// Supports per-app lifecycle/analytics queries (e.g. "all dsheet updates"). +DocumentUpdateSchema.index({ appType: 1, createdAt: 1 }, { background: true }); + export const DocumentUpdateModel = mongoose.model( "DocumentUpdate", DocumentUpdateSchema diff --git a/src/database/models/session.ts b/src/database/models/session.ts index a66fbbd..ff24ec9 100644 --- a/src/database/models/session.ts +++ b/src/database/models/session.ts @@ -7,6 +7,7 @@ export interface ISession extends MongooseDocument { createdAt: Date; state: "active" | "inactive" | "terminated"; roomInfo: string; + appType: "ddoc" | "dsheet"; } const SessionSchema = new Schema({ @@ -16,6 +17,8 @@ const SessionSchema = new Schema({ createdAt: { type: Date, default: Date.now }, state: { type: String, enum: ["active", "inactive", "terminated"], default: "active" }, roomInfo: { type: String, default: null }, + // Which Fileverse app owns this session. Absent ⇒ "ddoc" (legacy). + appType: { type: String, enum: ["ddoc", "dsheet"], default: "ddoc" }, }); SessionSchema.index({ documentId: 1, createdAt: 1, sessionDid: 1 }); diff --git a/src/scripts/backfill-apptype.ts b/src/scripts/backfill-apptype.ts new file mode 100644 index 0000000..da640e6 --- /dev/null +++ b/src/scripts/backfill-apptype.ts @@ -0,0 +1,46 @@ +/** + * One-time backfill: tag every pre-`appType` row as "ddoc". + * + * The collaboration server is shared between ddoc and dsheet. dsheet is the first + * app to write the `appType` field, so every row that predates it belongs to ddoc. + * Running this makes every row explicit, so per-app queries are plain equality + * with no null-handling. + * + * Idempotent — only touches rows where `appType` is missing. Safe to re-run. + * + * Usage: tsx src/scripts/backfill-apptype.ts + */ +import "dotenv/config"; +import { databaseService } from "../database"; +import { + DocumentUpdateModel, + DocumentCommitModel, + SessionModel, +} from "../database/models"; + +async function backfill(): Promise { + await databaseService.connect(); + + const filter = { appType: { $exists: false } }; + const set = { $set: { appType: "ddoc" as const } }; + + const [updates, commits, sessions] = await Promise.all([ + DocumentUpdateModel.updateMany(filter, set), + DocumentCommitModel.updateMany(filter, set), + SessionModel.updateMany(filter, set), + ]); + + console.log("appType backfill complete:"); + console.log(` DocumentUpdate: ${updates.modifiedCount} tagged "ddoc"`); + console.log(` DocumentCommit: ${commits.modifiedCount} tagged "ddoc"`); + console.log(` Session: ${sessions.modifiedCount} tagged "ddoc"`); + + await databaseService.disconnect(); +} + +backfill() + .then(() => process.exit(0)) + .catch((err) => { + console.error("appType backfill failed:", err); + process.exit(1); + }); diff --git a/src/services/auth-middleware.ts b/src/services/auth-middleware.ts index d70f25a..ce94fb0 100644 --- a/src/services/auth-middleware.ts +++ b/src/services/auth-middleware.ts @@ -6,6 +6,7 @@ export function authMiddleware(socket: AppSocket, next: (err?: Error) => void): socket.data.documentId = ""; socket.data.sessionDid = ""; socket.data.role = "editor"; + socket.data.appType = "ddoc"; next(); } diff --git a/src/services/mongodb-store.ts b/src/services/mongodb-store.ts index 291e9b5..b5ea54e 100644 --- a/src/services/mongodb-store.ts +++ b/src/services/mongodb-store.ts @@ -15,6 +15,7 @@ export class MongoDBStore { commitCid: update.commitCid, createdAt: update.createdAt, sessionDid: update.sessionDid, + appType: update.appType, }); await mongoUpdate.save(); @@ -119,6 +120,7 @@ export class MongoDBStore { updates: commit.updates, createdAt: commit.createdAt, sessionDid: commit.sessionDid, + appType: commit.appType, }); await mongoCommit.save(); diff --git a/src/services/session-manager.ts b/src/services/session-manager.ts index d9a0485..5f86f88 100644 --- a/src/services/session-manager.ts +++ b/src/services/session-manager.ts @@ -6,6 +6,7 @@ interface RuntimeSession { ownerDid: string; clients: Set; roomInfo?: string; + appType?: "ddoc" | "dsheet"; } export class SessionManager { @@ -55,7 +56,11 @@ export class SessionManager { sessionDid: sessionData.sessionDid, ownerDid: sessionData.ownerDid, }, - { state: "active", roomInfo: sessionData.roomInfo }, + { + state: "active", + roomInfo: sessionData.roomInfo, + appType: sessionData.appType ?? "ddoc", + }, { upsert: true, new: true } ); } catch (error) { @@ -87,6 +92,7 @@ export class SessionManager { ownerDid: dbSession.ownerDid, clients: new Set(), roomInfo: dbSession.roomInfo, + appType: dbSession.appType, }; // Store in memory diff --git a/src/services/socket-handlers.ts b/src/services/socket-handlers.ts index 412a626..8709a78 100644 --- a/src/services/socket-handlers.ts +++ b/src/services/socket-handlers.ts @@ -16,6 +16,7 @@ import { DocumentUpdate, AppServer, AppSocket, + AppType, ErrorCode, } from "../types/index"; import { requireAuth } from "./auth-middleware"; @@ -45,6 +46,15 @@ export function getRoomName(documentId: string, sessionDid: string): string { return `session::${documentId}__${sessionDid}`; } +/** + * Coerce an untrusted appType into a known AppType. Anything that is not exactly + * "dsheet" becomes "ddoc" (the legacy default), so missing or invalid values + * never fail enum validation and never widen access during the isolation check. + */ +function normalizeAppType(value: unknown): AppType { + return value === "dsheet" ? "dsheet" : "ddoc"; +} + export function registerEventHandlers(io: AppServer, broadcastBridge?: BroadcastBridge): void { if (broadcastBridge) { bridge = broadcastBridge; @@ -100,6 +110,7 @@ export async function handleAuth( try { const { authService, sessionManager } = deps; const { documentId, collaborationToken, sessionDid } = args; + const claimedAppType = normalizeAppType(args.appType); if (!collaborationToken) { return callback({ @@ -133,6 +144,7 @@ export async function handleAuth( let role: "owner" | "editor"; let sessionType: "new" | "existing"; let roomInfo: string | undefined; + let resolvedAppType: AppType; if (!existingSession && args.ownerToken) { // - Set up a new session (owner flow) @@ -214,11 +226,13 @@ export async function handleAuth( sessionDid, ownerDid, roomInfo: args.roomInfo, + appType: claimedAppType, }); role = "owner"; sessionType = "new"; roomInfo = args.roomInfo; + resolvedAppType = claimedAppType; } else if (existingSession) { // Join an existing session const userDid = await authService.verifyCollaborationToken( @@ -236,6 +250,19 @@ export async function handleAuth( }); } + // Isolation guard: a document belongs to exactly one app. Reject a client + // whose declared app (missing ⇒ "ddoc") differs from the document's stored + // app (missing ⇒ "ddoc"), so ddoc and dsheet can never join the same room. + const storedAppType = normalizeAppType(existingSession.appType); + if (claimedAppType !== storedAppType) { + return callback({ + status: false, + statusCode: 403, + error: "App type mismatch for this document", + errorCode: ErrorCode.APP_MISMATCH, + }); + } + let ownerDid = null; if (args.ownerToken && args.ownerAddress && args.contractAddress) { if (!validateHexAddress(args.contractAddress, "contractAddress") || @@ -267,6 +294,7 @@ export async function handleAuth( sessionType = "existing"; roomInfo = existingSession.roomInfo; + resolvedAppType = storedAppType; } else { return callback({ status: false, @@ -281,6 +309,7 @@ export async function handleAuth( socket.data.documentId = documentId; socket.data.sessionDid = sessionDid; socket.data.role = role; + socket.data.appType = resolvedAppType; // Join the Socket.IO room const roomName = getRoomName(documentId, sessionDid); @@ -410,6 +439,7 @@ export async function handleDocumentUpdate( commitCid: null, createdAt, sessionDid, + appType: socket.data.appType, }); callback({ @@ -518,6 +548,7 @@ export async function handleDocumentCommit( updates, createdAt: Date.now(), sessionDid, + appType: socket.data.appType, }); callback({ diff --git a/src/tests/services/session-manager.test.ts b/src/tests/services/session-manager.test.ts index 8cbc195..50471f5 100644 --- a/src/tests/services/session-manager.test.ts +++ b/src/tests/services/session-manager.test.ts @@ -238,4 +238,62 @@ describe("SessionManager", () => { expect(clients).toBeUndefined(); }); + + // appType + + it("persists the provided appType and exposes it on the in-memory session", async () => { + const freshManager = new SessionManager(); + + await freshManager.createSession({ + documentId: "doc-2", + sessionDid: "session-2", + ownerDid: "owner-did", + appType: "dsheet", + }); + + expect(SessionModel.findOneAndUpdate).toHaveBeenCalledWith( + { documentId: "doc-2", sessionDid: "session-2", ownerDid: "owner-did" }, + expect.objectContaining({ appType: "dsheet" }), + { upsert: true, new: true } + ); + const session = await freshManager.getSession("doc-2", "session-2"); + expect(session?.appType).toBe("dsheet"); + + await freshManager.destroy(); + }); + + it("defaults appType to ddoc when none is provided", async () => { + const freshManager = new SessionManager(); + + await freshManager.createSession({ + documentId: "doc-3", + sessionDid: "session-3", + ownerDid: "owner-did", + }); + + expect(SessionModel.findOneAndUpdate).toHaveBeenCalledWith( + { documentId: "doc-3", sessionDid: "session-3", ownerDid: "owner-did" }, + expect.objectContaining({ appType: "ddoc" }), + { upsert: true, new: true } + ); + + await freshManager.destroy(); + }); + + it("hydrates appType from the database on fallback", async () => { + const freshManager = new SessionManager(); + vi.mocked(SessionModel.findOne).mockResolvedValue({ + documentId: "doc-1", + sessionDid: "session-1", + ownerDid: "owner-did", + roomInfo: "room-info", + appType: "dsheet", + } as any); + + const session = await freshManager.getSession("doc-1", "session-1"); + + expect(session?.appType).toBe("dsheet"); + + await freshManager.destroy(); + }); }); diff --git a/src/tests/services/socket-handlers.handleAuth.test.ts b/src/tests/services/socket-handlers.handleAuth.test.ts index 750d3d2..f41f912 100644 --- a/src/tests/services/socket-handlers.handleAuth.test.ts +++ b/src/tests/services/socket-handlers.handleAuth.test.ts @@ -170,8 +170,11 @@ describe("handleAuth", () => { sessionDid: fakeArgs.sessionDid, ownerDid: "owner-did", roomInfo: fakeArgs.roomInfo, + appType: "ddoc", }); + expect(fakeSocket.data.appType).toBe("ddoc"); + expect(fakeSocket.data.authenticated).toBe(true); expect(fakeSocket.data.documentId).toBe(fakeArgs.documentId); expect(fakeSocket.data.sessionDid).toBe(fakeArgs.sessionDid); @@ -651,5 +654,179 @@ describe("handleAuth", () => { errorCode: ErrorCode.INTERNAL_ERROR, }); }); + + it("stores appType from auth args when creating a new owner session", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket({ emit: vi.fn() }); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + ownerToken: "owner-token", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", + roomInfo: "room-info", + appType: "dsheet", + }; + const callback = vi.fn(); + + fakeSessionManager.getSession.mockResolvedValue(undefined); + fakeAuthService.verifyOwnerToken.mockResolvedValue("owner-did"); + fakeSessionManager.getOtherActiveSessions.mockResolvedValue([]); + fakeSessionManager.createSession.mockResolvedValue(undefined); + fakeSessionManager.addClientToSession.mockResolvedValue(undefined); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeSessionManager.createSession).toHaveBeenCalledWith({ + documentId: fakeArgs.documentId, + sessionDid: fakeArgs.sessionDid, + ownerDid: "owner-did", + roomInfo: fakeArgs.roomInfo, + appType: "dsheet", + }); + expect(fakeSocket.data.appType).toBe("dsheet"); + }); + + it("joins an existing dsheet session when client declares the matching appType", async () => { + const fakeIO = createFakeIO(); + const fakeBroadcastOperator = { emit: vi.fn() }; + const fakeSocket = createFakeSocket(fakeBroadcastOperator); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + appType: "dsheet", + }; + const callback = vi.fn(); + + const existingSession = { + sessionDid: fakeArgs.sessionDid, + ownerDid: "owner-did", + roomInfo: "existing-room-info", + appType: "dsheet", + }; + + fakeSessionManager.getSession.mockResolvedValue(existingSession); + fakeAuthService.verifyCollaborationToken.mockResolvedValue("user-did"); + fakeSessionManager.addClientToSession.mockResolvedValue(undefined); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + const roomName = getRoomName(fakeArgs.documentId, fakeArgs.sessionDid); + expect(fakeSocket.join).toHaveBeenCalledWith(roomName); + expect(fakeSocket.data.appType).toBe("dsheet"); + expect(callback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + message: "Authentication successful", + role: "editor", + sessionType: "existing", + roomInfo: existingSession.roomInfo, + }, + }); + }); + + it("rejects with APP_MISMATCH when the client appType differs from the document's app", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket({ emit: vi.fn() }); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + appType: "dsheet", + }; + const callback = vi.fn(); + + const existingSession = { + sessionDid: fakeArgs.sessionDid, + ownerDid: "owner-did", + roomInfo: "existing-room-info", + appType: "ddoc", + }; + + fakeSessionManager.getSession.mockResolvedValue(existingSession); + fakeAuthService.verifyCollaborationToken.mockResolvedValue("user-did"); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 403, + error: "App type mismatch for this document", + errorCode: ErrorCode.APP_MISMATCH, + }); + expect(fakeSocket.join).not.toHaveBeenCalled(); + }); + + it("rejects with APP_MISMATCH when a non-declaring (ddoc) client joins a dsheet document", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket({ emit: vi.fn() }); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + // no appType → treated as "ddoc" + }; + const callback = vi.fn(); + + const existingSession = { + sessionDid: fakeArgs.sessionDid, + ownerDid: "owner-did", + roomInfo: "existing-room-info", + appType: "dsheet", + }; + + fakeSessionManager.getSession.mockResolvedValue(existingSession); + fakeAuthService.verifyCollaborationToken.mockResolvedValue("user-did"); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 403, + error: "App type mismatch for this document", + errorCode: ErrorCode.APP_MISMATCH, + }); + expect(fakeSocket.join).not.toHaveBeenCalled(); + }); + + it("treats a legacy session (no appType) as ddoc and admits a non-declaring client", async () => { + const fakeIO = createFakeIO(); + const fakeBroadcastOperator = { emit: vi.fn() }; + const fakeSocket = createFakeSocket(fakeBroadcastOperator); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + }; + const callback = vi.fn(); + + const existingSession = { + sessionDid: fakeArgs.sessionDid, + ownerDid: "owner-did", + roomInfo: "existing-room-info", + // no appType (legacy row) → resolves to "ddoc" + }; + + fakeSessionManager.getSession.mockResolvedValue(existingSession); + fakeAuthService.verifyCollaborationToken.mockResolvedValue("user-did"); + fakeSessionManager.addClientToSession.mockResolvedValue(undefined); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeSocket.data.appType).toBe("ddoc"); + expect(callback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + message: "Authentication successful", + role: "editor", + sessionType: "existing", + roomInfo: existingSession.roomInfo, + }, + }); + }); }); diff --git a/src/tests/services/socket-handlers.handleAwareness.test.ts b/src/tests/services/socket-handlers.handleAwareness.test.ts index d30197f..f4c325e 100644 --- a/src/tests/services/socket-handlers.handleAwareness.test.ts +++ b/src/tests/services/socket-handlers.handleAwareness.test.ts @@ -8,6 +8,7 @@ const defaultSocketData: SocketData = { sessionDid: "test-session-did", role: "owner", authenticated: true, + appType: "ddoc", }; function createFakeIO(): AppServer { diff --git a/src/tests/services/socket-handlers.handleDocumentCommit.test.ts b/src/tests/services/socket-handlers.handleDocumentCommit.test.ts index d698b9f..735d4ab 100644 --- a/src/tests/services/socket-handlers.handleDocumentCommit.test.ts +++ b/src/tests/services/socket-handlers.handleDocumentCommit.test.ts @@ -275,6 +275,39 @@ describe("handleDocumentCommit", () => { }); }); + it("stamps the connection's appType onto the persisted commit", async () => { + const fakeSocket = createFakeSocket(); + fakeSocket.data.appType = "dsheet"; + const fakeArgs: DocumentCommitArgs = { + documentId: "doc-1", + updates: ["u1", "u2"], + cid: "cid-1", + ownerToken: "owner-token", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", + }; + const callback = vi.fn(); + + const runtimeSession = { sessionDid: fakeSocket.data.sessionDid }; + fakeSessionManager.getRuntimeSession.mockResolvedValue(runtimeSession); + fakeAuthService.verifyOwnerToken.mockResolvedValue(true); + fakeMongoDBStore.createCommit.mockResolvedValue({ + id: "commit-id", + documentId: fakeArgs.documentId, + cid: fakeArgs.cid, + updates: fakeArgs.updates, + createdAt: 1, + sessionDid: runtimeSession.sessionDid, + appType: "dsheet", + }); + + await handleDocumentCommit(deps, fakeSocket, fakeArgs, callback); + + expect(fakeMongoDBStore.createCommit).toHaveBeenCalledWith( + expect.objectContaining({ appType: "dsheet" }) + ); + }); + it("returns 500 when an unexpected error occurs in document commit handler", async () => { const fakeSocket = createFakeSocket(); const fakeArgs: DocumentCommitArgs = { diff --git a/src/tests/services/socket-handlers.handleDocumentUpdate.test.ts b/src/tests/services/socket-handlers.handleDocumentUpdate.test.ts index cf9d334..f961c26 100644 --- a/src/tests/services/socket-handlers.handleDocumentUpdate.test.ts +++ b/src/tests/services/socket-handlers.handleDocumentUpdate.test.ts @@ -224,6 +224,39 @@ describe("handleDocumentUpdate", () => { }); }); + it("stamps the connection's appType onto the persisted update", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket({ emit: vi.fn() }); + fakeSocket.data.appType = "dsheet"; + const fakeArgs: DocumentUpdateArgs = { + documentId: "doc-1", + data: "update-data", + collaborationToken: "token", + }; + const callback = vi.fn(); + + const runtimeSession = { sessionDid: fakeSocket.data.sessionDid }; + fakeSessionManager.getRuntimeSession.mockResolvedValue(runtimeSession); + fakeAuthService.verifyCollaborationToken.mockResolvedValue(true); + fakeMongoDBStore.createUpdate.mockResolvedValue({ + id: "u1", + documentId: fakeArgs.documentId, + data: fakeArgs.data, + updateType: "yjs_update", + committed: false, + commitCid: null, + createdAt: 1, + sessionDid: runtimeSession.sessionDid, + appType: "dsheet", + }); + + await handleDocumentUpdate(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeMongoDBStore.createUpdate).toHaveBeenCalledWith( + expect.objectContaining({ appType: "dsheet" }) + ); + }); + it("returns 500 when an unexpected error occurs in document update handler", async () => { const fakeIO = createFakeIO(); const fakeSocket = createFakeSocket(); diff --git a/src/types/index.ts b/src/types/index.ts index 8d9a5bf..d105d7c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -44,6 +44,13 @@ export interface AuthenticatedWebSocket extends WebSocket { // Domain Models (unchanged) // *************************************** +/** + * Identifies which Fileverse app a document/session/update belongs to. + * The collaboration server is shared between ddoc and dsheet. + * Absent ⇒ "ddoc" (legacy: all data predating this field is ddoc). + */ +export type AppType = "ddoc" | "dsheet"; + export interface DocumentUpdate { id: string; documentId: string; @@ -53,6 +60,7 @@ export interface DocumentUpdate { commitCid: string | null; createdAt: number; sessionDid: string; + appType?: AppType; } export interface DocumentCommit { @@ -62,6 +70,7 @@ export interface DocumentCommit { updates: string[]; // list of update IDs included in this commit createdAt: number; sessionDid: string; + appType?: AppType; } export interface IPFSUploadResponse { @@ -104,6 +113,7 @@ export enum ErrorCode { COMMIT_MISSING_DATA = "COMMIT_MISSING_DATA", INVALID_ADDRESS = "INVALID_ADDRESS", NOT_AUTHENTICATED = "NOT_AUTHENTICATED", + APP_MISMATCH = "APP_MISMATCH", DB_ERROR = "DB_ERROR", INTERNAL_ERROR = "INTERNAL_ERROR", } @@ -132,6 +142,7 @@ export interface AuthArgs { ownerAddress?: string; contractAddress?: string; roomInfo?: string; + appType?: AppType; } export interface AuthResponseData { @@ -298,6 +309,7 @@ export interface SocketData { sessionDid: string; role: "owner" | "editor"; authenticated: boolean; + appType: AppType; } // ***************************************