From d4df0f91301e91fec95a77ec56194eebf6d94299 Mon Sep 17 00:00:00 2001 From: Nadeem Date: Fri, 9 Oct 2026 16:34:51 +0530 Subject: [PATCH] feat(opaque-links): recognise 0x link ids (TEC-3037) Opaque link ids are now 0x plus 18 base58 characters, a product requirement. Rooms are matched by that exact shape, never by the 0x prefix alone, since addresses, hashes and on-chain ids also start with 0x. - acceptedDdocId stores the on-chain ddocId only for a room named by a link id, and refuses a link id as the ddocId. - The publish reconciler skips link rooms that have no ddocId. - Tests cover link ids, the old o- shape, address-named rooms and on-chain ids that start with 0x. Deploy before any ddocs.new that mints 0x ids, or no ddocId is stored for new opaque rooms. --- src/database/models/document-meta.ts | 2 +- src/services/mongodb-store.ts | 9 +++++--- src/tests/services/mongodb-store.test.ts | 29 ++++++++++++++++++++++-- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/database/models/document-meta.ts b/src/database/models/document-meta.ts index 739ff2d..21bc208 100644 --- a/src/database/models/document-meta.ts +++ b/src/database/models/document-meta.ts @@ -6,7 +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 + ddocId: string | null; // on-chain id when documentId is an opaque link id; owner-only, never emitted appType?: "ddoc" | "dsheet"; editLock: string | null; // roomKey-wrapped title: string | null; // roomKey-encrypted diff --git a/src/services/mongodb-store.ts b/src/services/mongodb-store.ts index 0258ec7..16f23f4 100644 --- a/src/services/mongodb-store.ts +++ b/src/services/mongodb-store.ts @@ -14,12 +14,15 @@ const HYDRATION_SLOW_MS = 500; const HYDRATION_LARGE_BYTES = 2 * 1024 * 1024; const HYDRATION_LARGE_ROWS = 2_000; +// An opaque link id, by exact shape: addresses, hashes and on-chain ids can also start with 0x. +const LINK_ID_RE = /^0x[1-9A-HJ-NP-Za-km-z]{18}$/; + export const acceptedDdocId = (documentId: string, ddocId: unknown): string | null => typeof ddocId === "string" && ddocId.length > 0 && ddocId.length <= 64 && - documentId.startsWith("o-") && - !ddocId.startsWith("o-") + LINK_ID_RE.test(documentId) && + !LINK_ID_RE.test(ddocId) ? ddocId : null; @@ -528,7 +531,7 @@ export class MongoDBStore { const rows: any[] = await DocumentMetaModel.find({ isPublished: { $ne: true }, portalAddress: { $ne: null }, - $or: [{ _id: { $not: /^o-/ } }, { ddocId: { $ne: null } }], + $or: [{ _id: { $not: LINK_ID_RE } }, { ddocId: { $ne: null } }], }) .select("portalAddress ddocId") .sort({ updatedAt: -1 }) diff --git a/src/tests/services/mongodb-store.test.ts b/src/tests/services/mongodb-store.test.ts index b34b360..bad5feb 100644 --- a/src/tests/services/mongodb-store.test.ts +++ b/src/tests/services/mongodb-store.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { DocumentUpdateModel } from "../../database/models/document-update"; import { CounterModel } from "../../database/models/counter"; -import { MongoDBStore, SessionTerminatedError } from "../../services/mongodb-store"; +import { MongoDBStore, SessionTerminatedError, acceptedDdocId } from "../../services/mongodb-store"; vi.mock("../../database/models", () => { const save = vi.fn().mockResolvedValue(undefined); @@ -313,7 +313,7 @@ describe("listDocumentsForOwner", () => { expect(DocumentMetaModel.find).toHaveBeenCalledWith({ isPublished: { $ne: true }, portalAddress: { $ne: null }, - $or: [{ _id: { $not: /^o-/ } }, { ddocId: { $ne: null } }], + $or: [{ _id: { $not: /^0x[1-9A-HJ-NP-Za-km-z]{18}$/ } }, { ddocId: { $ne: null } }], }); expect(sort).toHaveBeenCalledWith({ updatedAt: -1 }); expect(limit).toHaveBeenCalledWith(500); @@ -794,3 +794,28 @@ describe("lockWireFormat: duplicate-key race", () => { await expect(store.lockWireFormat("doc-1")).resolves.toBe(false); }); }); + +describe("acceptedDdocId", () => { + const LINK_ID = "0x" + "Zk".repeat(9); + const DDOC_ID = "7sLHvGNqsvNDhRm8tPKZSu"; + + it("keeps the on-chain id for a room named by a link id", () => { + expect(acceptedDdocId(LINK_ID, DDOC_ID)).toBe(DDOC_ID); + }); + + it("keeps an on-chain id that itself starts with 0x", () => { + expect(acceptedDdocId(LINK_ID, "0xabc")).toBe("0xabc"); + }); + + it("refuses a link id as the on-chain id", () => { + expect(acceptedDdocId(LINK_ID, "0x" + "Gh".repeat(9))).toBeNull(); + }); + + it.each([ + ["a plain room", DDOC_ID], + ["a room named by an address", "0x" + "a1".repeat(20)], + ["a room with the old o- prefix", "o-" + "Zk".repeat(9)], + ])("stores nothing for %s", (_, documentId) => { + expect(acceptedDdocId(documentId, DDOC_ID)).toBeNull(); + }); +});