diff --git a/src/database/models/document-meta.ts b/src/database/models/document-meta.ts index c3cab6b..739ff2d 100644 --- a/src/database/models/document-meta.ts +++ b/src/database/models/document-meta.ts @@ -6,6 +6,7 @@ interface IDocumentMeta extends MongooseDocument { ownerDid: string | null; ownerIdentityDid: string | null; portalAddress: string | null; + ddocId: string | null; // on-chain id when documentId is an opaque o- id; owner-only, never emitted appType?: "ddoc" | "dsheet"; editLock: string | null; // roomKey-wrapped title: string | null; // roomKey-encrypted @@ -22,6 +23,7 @@ const DocumentMetaSchema = new Schema({ ownerDid: { type: String, default: null }, ownerIdentityDid: { type: String, default: null }, portalAddress: { type: String, default: null }, + ddocId: { type: String, default: null }, // Which Fileverse app owns this document. Absent ⇒ "ddoc" (legacy). appType: { type: String, enum: ["ddoc", "dsheet"], default: "ddoc" }, editLock: { type: String, default: null }, @@ -40,6 +42,7 @@ DocumentMetaSchema.index({ ownerIdentityDid: 1, isPublished: 1 }, { background: DocumentMetaSchema.index({ ownerDid: 1 }, { background: true }); // Grace-window sweep: docs eligible for irreversible purge (see docs/architecture/edit-permission.md). DocumentMetaSchema.index({ tombstonedAt: 1 }, { background: true }); +DocumentMetaSchema.index({ ddocId: 1, portalAddress: 1 }, { background: true, sparse: true }); export const DocumentMetaModel = mongoose.model("DocumentMeta", DocumentMetaSchema); export type { IDocumentMeta }; diff --git a/src/services/deleted-file-webhook.ts b/src/services/deleted-file-webhook.ts index a19d649..7c1604e 100644 --- a/src/services/deleted-file-webhook.ts +++ b/src/services/deleted-file-webhook.ts @@ -24,13 +24,25 @@ export function createDeletedFileWebhookHandler(deps: DeletedFileWebhookDeps) { res.status(400).json({ error: "appFileId and portalAddress are required" }); return; } - const meta = await DocumentMetaModel.findById(appFileId).select("portalAddress").lean(); - if (!meta || (meta.portalAddress ?? "").toLowerCase() !== portalAddress.toLowerCase()) { + // ddocId is client-asserted, so a row on another portal can claim the same id: every + // candidate is portal-matched, never just the first one found. + const rows = await DocumentMetaModel.find({ + $or: [{ _id: appFileId }, { ddocId: appFileId }], + portalAddress: { $ne: null }, + }) + .select("_id portalAddress") + .lean(); + const portal = portalAddress.toLowerCase(); + const matches = rows.filter((m) => (m.portalAddress ?? "").toLowerCase() === portal); + if (matches.length === 0) { res.status(200).json({ ok: true, matched: false }); // idempotent no-op return; } - const done = await deps.mongodbStore.tombstoneDocument(appFileId, "onchain-delete"); - if (done && deps.onTombstoned) await deps.onTombstoned(appFileId); + for (const meta of matches) { + const documentId = String(meta._id); + const done = await deps.mongodbStore.tombstoneDocument(documentId, "onchain-delete"); + if (done && deps.onTombstoned) await deps.onTombstoned(documentId); + } res.status(200).json({ ok: true, matched: true }); }; } diff --git a/src/services/mongodb-store.ts b/src/services/mongodb-store.ts index 624438f..0258ec7 100644 --- a/src/services/mongodb-store.ts +++ b/src/services/mongodb-store.ts @@ -14,6 +14,15 @@ const HYDRATION_SLOW_MS = 500; const HYDRATION_LARGE_BYTES = 2 * 1024 * 1024; const HYDRATION_LARGE_ROWS = 2_000; +export const acceptedDdocId = (documentId: string, ddocId: unknown): string | null => + typeof ddocId === "string" && + ddocId.length > 0 && + ddocId.length <= 64 && + documentId.startsWith("o-") && + !ddocId.startsWith("o-") + ? ddocId + : null; + export class MongoDBStore { // Update management async createUpdate(update: DocumentUpdate): Promise { @@ -303,6 +312,7 @@ export class MongoDBStore { ownerDid: string | null; ownerIdentityDid: string | null; portalAddress: string | null; + ddocId: string | null; appType: AppType; editLock: string | null; title: string | null; @@ -314,6 +324,7 @@ export class MongoDBStore { ownerDid: meta.ownerDid, ownerIdentityDid: meta.ownerIdentityDid, portalAddress: meta.portalAddress, + ddocId: meta.ddocId, appType: meta.appType, }, $set: { @@ -325,6 +336,12 @@ export class MongoDBStore { }, { upsert: true, new: true, writeConcern: { w: "majority", j: true } } ); + if (meta.ddocId) { + await DocumentMetaModel.updateOne( + { _id: meta.documentId, $or: [{ ddocId: null }, { ddocId: { $exists: false } }] }, + { $set: { ddocId: meta.ddocId } } + ); + } } // First-writer-immutable document->owner binding (see docs/architecture/edit-permission.md). @@ -340,6 +357,7 @@ export class MongoDBStore { ownerDid: string | null; ownerIdentityDid: string | null; sessionDid: string; + ddocId: string | null; appType: AppType; }): Promise<{ portalAddress: string | null }> { // 1. Ensure the row exists; pin all binding fields on INSERT only. @@ -353,6 +371,7 @@ export class MongoDBStore { sessionDid: p.sessionDid, updatedAt: Date.now(), isPublished: false, + ddocId: p.ddocId, appType: p.appType, }, }, @@ -365,7 +384,16 @@ export class MongoDBStore { ); // 3. Read the effective pin. const doc: any = await DocumentMetaModel.findById(p.documentId).select("portalAddress").lean(); - return { portalAddress: doc?.portalAddress ?? null }; + const effective: string | null = doc?.portalAddress ?? null; + // 4. Backfill ddocId only for the pinned portal: the caller is rejected on a mismatch, and + // a write-once value must not be settable by that rejected caller. + if (p.ddocId && effective && effective.toLowerCase() === p.portalAddress.toLowerCase()) { + await DocumentMetaModel.updateOne( + { _id: p.documentId, $or: [{ ddocId: null }, { ddocId: { $exists: false } }] }, + { $set: { ddocId: p.ddocId } } + ); + } + return { portalAddress: effective }; } async getDocumentMeta( @@ -439,19 +467,26 @@ export class MongoDBStore { async listDocumentsForOwner( by: { ownerIdentityDid?: string; ownerDid?: string; portalAddress?: string } ): Promise<{ - documents: Array<{ documentId: string; editLock: string | null; title: string | null; appType: AppType }>; + documents: Array<{ + documentId: string; + ddocId: string | null; + editLock: string | null; + title: string | null; + appType: AppType; + }>; deletedDocumentIds: string[]; + deletedDocuments: Array<{ documentId: string; ddocId: string | null }>; }> { const filter: Record = {}; if (by.ownerIdentityDid) filter.ownerIdentityDid = by.ownerIdentityDid; else if (by.ownerDid) filter.ownerDid = by.ownerDid; - else return { documents: [], deletedDocumentIds: [] }; + else return { documents: [], deletedDocumentIds: [], deletedDocuments: [] }; // Published docs are discovered via the indexer; the collab server only lists // unpublished durable docs (the publish reconciler flips this flag). filter.isPublished = { $ne: true }; const metas: any[] = await DocumentMetaModel.find(filter) - .select("editLock title appType portalAddress tombstonedAt") + .select("editLock title appType portalAddress tombstonedAt ddocId") .lean(); // Scope to the portal the caller proved. A team editLock is an identity envelope and @@ -473,27 +508,37 @@ export class MongoDBStore { .filter((m) => !m.tombstonedAt) .map((m) => ({ documentId: m._id, + ddocId: m.ddocId ?? null, editLock: m.editLock ?? null, title: m.title ?? null, appType: (m.appType as AppType) ?? "ddoc", })); const deletedDocumentIds = scoped.filter((m) => m.tombstonedAt).map((m) => String(m._id)); - return { documents, deletedDocumentIds }; + const deletedDocuments = scoped + .filter((m) => m.tombstonedAt) + .map((m) => ({ documentId: String(m._id), ddocId: m.ddocId ?? null })); + return { documents, deletedDocumentIds, deletedDocuments }; } // Publish-reconciler candidate set: unpublished durable docs that have a portal to // resolve on-chain. Rows without a portalAddress can't be checked, so they are skipped. async listUnpublishedMetaRefs( limit: number - ): Promise> { + ): Promise> { const rows: any[] = await DocumentMetaModel.find({ isPublished: { $ne: true }, portalAddress: { $ne: null }, + $or: [{ _id: { $not: /^o-/ } }, { ddocId: { $ne: null } }], }) - .select("portalAddress") + .select("portalAddress ddocId") + .sort({ updatedAt: -1 }) .limit(limit) .lean(); - return rows.map((r) => ({ documentId: r._id, portalAddress: r.portalAddress })); + return rows.map((r) => ({ + documentId: r._id, + portalAddress: r.portalAddress, + ddocId: r.ddocId ?? null, + })); } async markDocumentsPublished( diff --git a/src/services/owner-op-routes.ts b/src/services/owner-op-routes.ts index 5cc905c..bd58b0d 100644 --- a/src/services/owner-op-routes.ts +++ b/src/services/owner-op-routes.ts @@ -225,11 +225,12 @@ export function createListMyDocumentsHandler(deps: ListMyDocumentsDeps) { if (signingDid) { // portalAddress is safe to filter on: verifyIdentityToken only returns a signingDid // when the UCAN was signed for exactly this hierPart. - const { documents, deletedDocumentIds } = await deps.mongodbStore.listDocumentsForOwner({ - ownerIdentityDid: signingDid, - portalAddress, - }); - res.status(200).json({ documents, deletedDocumentIds }); + const { documents, deletedDocumentIds, deletedDocuments } = + await deps.mongodbStore.listDocumentsForOwner({ + ownerIdentityDid: signingDid, + portalAddress, + }); + res.status(200).json({ documents, deletedDocumentIds, deletedDocuments }); return; } } diff --git a/src/services/published-reconciler.ts b/src/services/published-reconciler.ts index 6e03917..d7f5d09 100644 --- a/src/services/published-reconciler.ts +++ b/src/services/published-reconciler.ts @@ -5,7 +5,7 @@ import type { MongoDBStore } from "./mongodb-store"; export const reconcilePublishedDocuments = async (deps: { mongodbStore: Pick; resolvePublishedDocumentIds: ( - refs: Array<{ documentId: string; portalAddress: string }> + refs: Array<{ documentId: string; portalAddress: string; ddocId: string | null }> ) => Promise>; batchSize: number; }): Promise<{ scanned: number; published: number }> => { diff --git a/src/services/socket-handlers.ts b/src/services/socket-handlers.ts index 0be0f90..d35eb9f 100644 --- a/src/services/socket-handlers.ts +++ b/src/services/socket-handlers.ts @@ -25,7 +25,7 @@ import { } from "../types/index"; import { requireAuth } from "./auth-middleware"; import { authService } from "./auth"; -import { mongodbStore, SessionTerminatedError } from "./mongodb-store"; +import { mongodbStore, SessionTerminatedError, acceptedDdocId } from "./mongodb-store"; import { sessionManager } from "./session-manager"; import { rotationCoordinator } from "./rotation-coordinator"; import { Hex, isAddress } from "viem"; @@ -393,6 +393,7 @@ export async function handleAuth( ownerDid, ownerIdentityDid: boundOwnerIdentityDid, sessionDid, + ddocId: acceptedDdocId(documentId, args.ddocId), appType: claimedAppType, }); @@ -1418,6 +1419,7 @@ export async function handleSetDocumentMeta( ownerDid: session.ownerDid ?? null, ownerIdentityDid: session.ownerIdentityDid ?? null, portalAddress: session.portalAddress ?? null, + ddocId: acceptedDdocId(documentId, args.ddocId), appType: socket.data.appType, editLock: args.editLock, title: args.title, diff --git a/src/tests/services/deleted-file-webhook.test.ts b/src/tests/services/deleted-file-webhook.test.ts index 5088156..c7ab7a3 100644 --- a/src/tests/services/deleted-file-webhook.test.ts +++ b/src/tests/services/deleted-file-webhook.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -const findById = vi.fn(); +const find = vi.fn(); vi.mock("../../database/models", () => ({ - DocumentMetaModel: { findById: (...a: unknown[]) => findById(...a) }, + DocumentMetaModel: { find: (...a: unknown[]) => find(...a) }, DocumentUpdateModel: {}, DocumentCommitModel: {}, CounterModel: {}, SessionModel: {}, DocumentMirrorModel: {}, DocumentEditEpochModel: {}, })); @@ -40,8 +40,8 @@ describe("POST /webhooks/file-deleted", () => { }); it("tombstones and drops sessions on valid secret + portal match", async () => { - findById.mockReturnValue({ - select: () => ({ lean: () => Promise.resolve({ portalAddress: "0xAbC" }) }), + find.mockReturnValue({ + select: () => ({ lean: () => Promise.resolve([{ _id: "doc-1", portalAddress: "0xAbC" }]) }), }); const handler = createDeletedFileWebhookHandler({ mongodbStore, onTombstoned }); const r = res(); @@ -68,12 +68,12 @@ describe("POST /webhooks/file-deleted", () => { expect(r.status).toHaveBeenCalledWith(401); expect(mongodbStore.tombstoneDocument).not.toHaveBeenCalled(); - expect(findById).not.toHaveBeenCalled(); + expect(find).not.toHaveBeenCalled(); }); it("no-ops on portal mismatch (colliding-appFileId defense) and never tombstones", async () => { - findById.mockReturnValue({ - select: () => ({ lean: () => Promise.resolve({ portalAddress: "0xOwnerPortal" }) }), + find.mockReturnValue({ + select: () => ({ lean: () => Promise.resolve([{ _id: "doc-1", portalAddress: "0xOwnerPortal" }]) }), }); const handler = createDeletedFileWebhookHandler({ mongodbStore, onTombstoned }); const r = res(); @@ -89,7 +89,7 @@ describe("POST /webhooks/file-deleted", () => { }); it("no-ops on unknown appFileId and never tombstones", async () => { - findById.mockReturnValue({ select: () => ({ lean: () => Promise.resolve(null) }) }); + find.mockReturnValue({ select: () => ({ lean: () => Promise.resolve([]) }) }); const handler = createDeletedFileWebhookHandler({ mongodbStore, onTombstoned }); const r = res(); @@ -117,7 +117,7 @@ describe("POST /webhooks/file-deleted", () => { expect(r.status).toHaveBeenCalledWith(401); expect(mongodbStore.tombstoneDocument).not.toHaveBeenCalled(); - expect(findById).not.toHaveBeenCalled(); + expect(find).not.toHaveBeenCalled(); } finally { config.webhook.apiKey = original; } @@ -131,6 +131,6 @@ describe("POST /webhooks/file-deleted", () => { expect(r.status).toHaveBeenCalledWith(400); expect(mongodbStore.tombstoneDocument).not.toHaveBeenCalled(); - expect(findById).not.toHaveBeenCalled(); + expect(find).not.toHaveBeenCalled(); }); }); diff --git a/src/tests/services/mongodb-store.pinPortal.test.ts b/src/tests/services/mongodb-store.pinPortal.test.ts index 927d592..400183a 100644 --- a/src/tests/services/mongodb-store.pinPortal.test.ts +++ b/src/tests/services/mongodb-store.pinPortal.test.ts @@ -37,6 +37,7 @@ describe("pinDocumentPortalIfAbsent", () => { ownerDid: "did:key:o", ownerIdentityDid: "did:key:i", sessionDid: "did:key:s1", + ddocId: null, appType: "ddoc", }); @@ -63,6 +64,7 @@ describe("pinDocumentPortalIfAbsent", () => { ownerDid: "did:key:o", ownerIdentityDid: "did:key:i", sessionDid: "did:key:s1", + ddocId: null, appType: "ddoc", }); @@ -81,6 +83,7 @@ describe("pinDocumentPortalIfAbsent", () => { ownerDid: "did:key:o2", ownerIdentityDid: "did:key:i2", sessionDid: "did:key:s2", + ddocId: null, appType: "ddoc", }); @@ -101,6 +104,7 @@ describe("pinDocumentPortalIfAbsent", () => { ownerDid: null, ownerIdentityDid: "did:key:i", sessionDid: "did:key:s", + ddocId: null, appType: "ddoc", }); @@ -117,6 +121,7 @@ describe("pinDocumentPortalIfAbsent", () => { ownerDid: null, ownerIdentityDid: null, sessionDid: "did:key:s", + ddocId: null, appType: "ddoc", }); @@ -138,6 +143,7 @@ describe("upsertDocumentMeta binding immutability", () => { ownerDid: "did:key:x", ownerIdentityDid: "did:key:x", portalAddress: "0xEVIL", + ddocId: null, appType: "ddoc", editLock: "lk", title: "t", @@ -150,6 +156,7 @@ describe("upsertDocumentMeta binding immutability", () => { ownerDid: "did:key:x", ownerIdentityDid: "did:key:x", portalAddress: "0xEVIL", + ddocId: null, appType: "ddoc", }, $set: expect.objectContaining({ diff --git a/src/tests/services/mongodb-store.test.ts b/src/tests/services/mongodb-store.test.ts index 570beac..b34b360 100644 --- a/src/tests/services/mongodb-store.test.ts +++ b/src/tests/services/mongodb-store.test.ts @@ -225,7 +225,7 @@ describe("upsertDocumentMeta", () => { const store = new MongoDBStore(); await store.upsertDocumentMeta({ documentId: "doc-1", sessionDid: "room-did", ownerDid: "od", ownerIdentityDid: "oid", - portalAddress: "0xP", appType: "ddoc", editLock: "el", title: "t", + portalAddress: "0xP", ddocId: null, appType: "ddoc", editLock: "el", title: "t", }); expect(DocumentMetaModel.findOneAndUpdate).toHaveBeenCalledWith( @@ -269,10 +269,11 @@ describe("listDocumentsForOwner", () => { }); expect(result).toEqual({ documents: [ - { documentId: "doc-1", editLock: "el-1", title: "t1", appType: "ddoc" }, - { documentId: "doc-2", editLock: "el-2", title: "t2", appType: "ddoc" }, + { documentId: "doc-1", ddocId: null, editLock: "el-1", title: "t1", appType: "ddoc" }, + { documentId: "doc-2", ddocId: null, editLock: "el-2", title: "t2", appType: "ddoc" }, ], deletedDocumentIds: [], + deletedDocuments: [], }); }); @@ -303,7 +304,8 @@ describe("listDocumentsForOwner", () => { { _id: "d2", portalAddress: "0xQ" }, ]), }); - (DocumentMetaModel.find as any).mockReturnValue({ select: vi.fn().mockReturnValue({ limit }) }); + const sort = vi.fn().mockReturnValue({ limit }); + (DocumentMetaModel.find as any).mockReturnValue({ select: vi.fn().mockReturnValue({ sort }) }); const store = new MongoDBStore(); const refs = await store.listUnpublishedMetaRefs(500); @@ -311,11 +313,13 @@ describe("listDocumentsForOwner", () => { expect(DocumentMetaModel.find).toHaveBeenCalledWith({ isPublished: { $ne: true }, portalAddress: { $ne: null }, + $or: [{ _id: { $not: /^o-/ } }, { ddocId: { $ne: null } }], }); + expect(sort).toHaveBeenCalledWith({ updatedAt: -1 }); expect(limit).toHaveBeenCalledWith(500); expect(refs).toEqual([ - { documentId: "d1", portalAddress: "0xP" }, - { documentId: "d2", portalAddress: "0xQ" }, + { documentId: "d1", portalAddress: "0xP", ddocId: null }, + { documentId: "d2", portalAddress: "0xQ", ddocId: null }, ]); }); @@ -387,7 +391,7 @@ describe("listDocumentsForOwner", () => { const store = new MongoDBStore(); const result = await store.listDocumentsForOwner({}); - expect(result).toEqual({ documents: [], deletedDocumentIds: [] }); + expect(result).toEqual({ documents: [], deletedDocumentIds: [], deletedDocuments: [] }); expect(DocumentMetaModel.find).not.toHaveBeenCalled(); }); }); @@ -407,10 +411,10 @@ describe("listDocumentsForOwner: appType routing", () => { const store = new MongoDBStore(); const docs = await store.listDocumentsForOwner({ ownerIdentityDid: "did:key:x" }); - expect(select).toHaveBeenCalledWith("editLock title appType portalAddress tombstonedAt"); + expect(select).toHaveBeenCalledWith("editLock title appType portalAddress tombstonedAt ddocId"); expect(docs.documents).toEqual([ - { documentId: "doc-1", editLock: "lock", title: "t", appType: "dsheet" }, - { documentId: "doc-2", editLock: null, title: null, appType: "ddoc" }, + { documentId: "doc-1", ddocId: null, editLock: "lock", title: "t", appType: "dsheet" }, + { documentId: "doc-2", ddocId: null, editLock: null, title: null, appType: "ddoc" }, ]); }); }); diff --git a/src/tests/services/owner-op-routes.test.ts b/src/tests/services/owner-op-routes.test.ts index 2d621cc..be887b3 100644 --- a/src/tests/services/owner-op-routes.test.ts +++ b/src/tests/services/owner-op-routes.test.ts @@ -193,8 +193,9 @@ describe("POST /list-my-documents", () => { authService: { verifyIdentityToken: vi.fn(), verifyOwnerToken: vi.fn() }, mongodbStore: { listDocumentsForOwner: vi.fn().mockResolvedValue({ - documents: [{ documentId: "d1", editLock: "el", title: "t", appType: "dsheet" }], + documents: [{ documentId: "d1", ddocId: null, editLock: "el", title: "t", appType: "dsheet" }], deletedDocumentIds: ["d0"], + deletedDocuments: [{ documentId: "d0", ddocId: null }], }), }, }; @@ -212,8 +213,9 @@ describe("POST /list-my-documents", () => { }); expect(r.status).toHaveBeenCalledWith(200); expect(r.json).toHaveBeenCalledWith({ - documents: [{ documentId: "d1", editLock: "el", title: "t", appType: "dsheet" }], + documents: [{ documentId: "d1", ddocId: null, editLock: "el", title: "t", appType: "dsheet" }], deletedDocumentIds: ["d0"], + deletedDocuments: [{ documentId: "d0", ddocId: null }], }); }); diff --git a/src/tests/services/published-reconciler.test.ts b/src/tests/services/published-reconciler.test.ts index 2914d85..471be60 100644 --- a/src/tests/services/published-reconciler.test.ts +++ b/src/tests/services/published-reconciler.test.ts @@ -9,9 +9,9 @@ const makeStore = (refs: any[]) => ({ describe("reconcilePublishedDocuments", () => { it("marks exactly the published subset and reports counts", async () => { const refs = [ - { documentId: "d1", portalAddress: "0xP" }, - { documentId: "d2", portalAddress: "0xP" }, - { documentId: "d3", portalAddress: "0xP" }, + { documentId: "d1", portalAddress: "0xP", ddocId: null }, + { documentId: "d2", portalAddress: "0xP", ddocId: null }, + { documentId: "d3", portalAddress: "0xP", ddocId: null }, ]; const store = makeStore(refs); const resolve = vi @@ -50,7 +50,7 @@ describe("reconcilePublishedDocuments", () => { }); it("does not call markDocumentsPublished when nothing resolved published", async () => { - const store = makeStore([{ documentId: "d1", portalAddress: "0xP" }]); + const store = makeStore([{ documentId: "d1", portalAddress: "0xP", ddocId: null }]); const resolve = vi.fn().mockResolvedValue([]); const out = await reconcilePublishedDocuments({ mongodbStore: store as any, diff --git a/src/tests/services/socket-handlers.handleAuth.test.ts b/src/tests/services/socket-handlers.handleAuth.test.ts index 6334c32..598671c 100644 --- a/src/tests/services/socket-handlers.handleAuth.test.ts +++ b/src/tests/services/socket-handlers.handleAuth.test.ts @@ -269,6 +269,7 @@ describe("handleAuth", () => { ownerDid: "owner-did", ownerIdentityDid: "owner-identity-did", sessionDid: pinArgs.sessionDid, + ddocId: null, appType: "ddoc", }); expect(fakeSessionManager.createSession).toHaveBeenCalled(); diff --git a/src/tests/utils/contract.published.test.ts b/src/tests/utils/contract.published.test.ts index 9038462..82356ab 100644 --- a/src/tests/utils/contract.published.test.ts +++ b/src/tests/utils/contract.published.test.ts @@ -30,9 +30,9 @@ describe("resolvePublishedDocumentIds", () => { ] as any); const refs = [ - { documentId: "da", portalAddress: "0xP" }, - { documentId: "db", portalAddress: "0xP" }, - { documentId: "dc", portalAddress: "0xP" }, + { documentId: "da", portalAddress: "0xP", ddocId: null }, + { documentId: "db", portalAddress: "0xP", ddocId: null }, + { documentId: "dc", portalAddress: "0xP", ddocId: null }, ]; const out = await resolvePublishedDocumentIds(refs); @@ -47,7 +47,7 @@ describe("resolvePublishedDocumentIds", () => { vi.spyOn(publicClient, "multicall") .mockResolvedValueOnce([{ status: "failure", error: new Error("rpc") }] as any) .mockResolvedValueOnce([] as any); - const out = await resolvePublishedDocumentIds([{ documentId: "dx", portalAddress: "0xP" }]); + const out = await resolvePublishedDocumentIds([{ documentId: "dx", portalAddress: "0xP", ddocId: null }]); expect(out).toEqual([]); }); }); diff --git a/src/types/index.ts b/src/types/index.ts index fbbde8b..db3e1cf 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -112,6 +112,7 @@ export interface AuthArgs { ownerToken?: string; ownerAddress?: string; contractAddress?: string; + ddocId?: string; roomInfo?: string; appType?: AppType; identityToken?: string; @@ -171,6 +172,7 @@ export interface MirrorSnapshotArgs { export interface DocumentMetaArgs { documentId?: string; + ddocId?: string; editLock: string | null; title: string | null; } diff --git a/src/utils/contract.ts b/src/utils/contract.ts index ddbac03..0022832 100644 Binary files a/src/utils/contract.ts and b/src/utils/contract.ts differ