From 3148cd3260875ac1d21a064718df37129d7fcd01 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Thu, 4 Jun 2026 15:54:15 +0700 Subject: [PATCH 1/2] fix(accounts): preserve challenge comment updates --- src/stores/accounts/accounts-actions.test.ts | 77 ++++++++++++++++++++ src/stores/accounts/accounts-actions.ts | 23 ++++++ 2 files changed, 100 insertions(+) diff --git a/src/stores/accounts/accounts-actions.test.ts b/src/stores/accounts/accounts-actions.test.ts index 41b9628e..84e58d08 100644 --- a/src/stores/accounts/accounts-actions.test.ts +++ b/src/stores/accounts/accounts-actions.test.ts @@ -478,6 +478,83 @@ describe("accounts-actions", () => { expect(comments.some((c: any) => c.content === "from named account")).toBe(true); }); + test("publishComment preserves challenge commentUpdate fields on the account comment", async () => { + await act(async () => { + await accountsActions.createAccount(); + }); + const account = Object.values(accountsStore.getState().accounts)[0]; + const createComment = account.pkc.createComment.bind(account.pkc); + vi.spyOn(account.pkc, "createComment").mockImplementation(async (opts: any) => { + const publication: any = await createComment(opts); + vi.spyOn(publication, "simulateChallengeVerificationEvent").mockImplementation(() => { + const cid = "moderated comment cid"; + publication.cid = cid; + publication.emit("challengeverification", { + type: "CHALLENGEVERIFICATION", + challengeRequestId: publication.challengeRequestId, + challengeAnswerId: publication.challengeAnswerId, + challengeSuccess: true, + commentUpdate: { + cid, + pendingApproval: true, + reason: "AI moderation reason", + removed: false, + }, + }); + publication.publishingState = "succeeded"; + publication.emit("publishingstatechange", "succeeded"); + }); + return publication; + }); + const onChallengeVerification = vi.fn(); + + await act(async () => { + await accountsActions.publishComment({ + communityAddress: "sub.eth", + content: "moderated comment", + onChallenge: (challenge: any, comment: any) => comment.publishChallengeAnswers(["4"]), + onChallengeVerification, + }); + }); + + const startedAt = Date.now(); + while (Date.now() - startedAt < 2000) { + await act(async () => {}); + const comment = accountsStore.getState().accountsComments[account.id]?.[0]; + if (comment?.reason === "AI moderation reason") { + break; + } + await new Promise((resolve) => setTimeout(resolve, 25)); + } + + const storedComment = accountsStore.getState().accountsComments[account.id]?.[0]; + expect(onChallengeVerification).toHaveBeenCalledWith( + expect.objectContaining({ + commentUpdate: expect.objectContaining({ reason: "AI moderation reason" }), + }), + expect.objectContaining({ + cid: "moderated comment cid", + pendingApproval: true, + reason: "AI moderation reason", + removed: false, + }), + ); + expect(storedComment).toMatchObject({ + cid: "moderated comment cid", + pendingApproval: true, + reason: "AI moderation reason", + removed: false, + }); + + const persistedComments = await accountsDatabase.getAccountComments(account.id); + expect(persistedComments[0]).toMatchObject({ + cid: "moderated comment cid", + pendingApproval: true, + reason: "AI moderation reason", + removed: false, + }); + }); + test("publishVote with accountName uses named account", async () => { await act(async () => { await accountsActions.createAccount(); diff --git a/src/stores/accounts/accounts-actions.ts b/src/stores/accounts/accounts-actions.ts index 3dc3f31e..2341de5f 100644 --- a/src/stores/accounts/accounts-actions.ts +++ b/src/stores/accounts/accounts-actions.ts @@ -77,6 +77,24 @@ const getClientsSnapshotForState = (clients: any): any => { return Object.keys(snapshot).length > 0 ? snapshot : undefined; }; +const applyChallengeVerificationCommentUpdateToPublication = ( + challengeVerification: ChallengeVerification | undefined, + publication: any, +) => { + const commentUpdate = challengeVerification?.commentUpdate; + if ( + !commentUpdate || + typeof commentUpdate !== "object" || + Array.isArray(commentUpdate) || + !publication || + typeof publication !== "object" + ) { + return; + } + + Object.assign(publication, commentUpdate); +}; + const syncCommentClientsSnapshot = ( publishSessionId: string, accountId: string, @@ -1129,6 +1147,7 @@ export const publishComment = async ( activeComment.once( "challengeverification", async (challengeVerification: ChallengeVerification) => { + applyChallengeVerificationCommentUpdateToPublication(challengeVerification, activeComment); publishCommentOptions.onChallengeVerification(challengeVerification, activeComment); if (!challengeVerification.challengeSuccess && lastChallenge) { // publish again automatically on fail @@ -1204,6 +1223,10 @@ export const publishComment = async ( const updatingComment = await account.pkc.createComment( normalizePublicationOptionsForPkc(account.pkc, { ...comment }), ); + applyChallengeVerificationCommentUpdateToPublication( + challengeVerification, + updatingComment, + ); accountsActionsInternal .startUpdatingAccountCommentOnCommentUpdateEvents( updatingComment, From d5eea80aa33e9fef7c43b31a589ece43d79acfe0 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Thu, 4 Jun 2026 17:17:20 +0700 Subject: [PATCH 2/2] fix(accounts): harden challenge comment updates --- src/stores/accounts/accounts-actions.test.ts | 25 +++++++++++++++----- src/stores/accounts/accounts-actions.ts | 10 ++++++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/stores/accounts/accounts-actions.test.ts b/src/stores/accounts/accounts-actions.test.ts index 84e58d08..305b96f8 100644 --- a/src/stores/accounts/accounts-actions.test.ts +++ b/src/stores/accounts/accounts-actions.test.ts @@ -488,18 +488,24 @@ describe("accounts-actions", () => { const publication: any = await createComment(opts); vi.spyOn(publication, "simulateChallengeVerificationEvent").mockImplementation(() => { const cid = "moderated comment cid"; + const commentUpdate: any = { + cid, + pendingApproval: true, + reason: "AI moderation reason", + removed: false, + }; + Object.defineProperties(commentUpdate, { + __proto__: { value: { polluted: true }, enumerable: true }, + constructor: { value: { polluted: true }, enumerable: true }, + prototype: { value: { polluted: true }, enumerable: true }, + }); publication.cid = cid; publication.emit("challengeverification", { type: "CHALLENGEVERIFICATION", challengeRequestId: publication.challengeRequestId, challengeAnswerId: publication.challengeAnswerId, challengeSuccess: true, - commentUpdate: { - cid, - pendingApproval: true, - reason: "AI moderation reason", - removed: false, - }, + commentUpdate, }); publication.publishingState = "succeeded"; publication.emit("publishingstatechange", "succeeded"); @@ -545,6 +551,10 @@ describe("accounts-actions", () => { reason: "AI moderation reason", removed: false, }); + expect(Object.prototype.hasOwnProperty.call(storedComment, "__proto__")).toBe(false); + expect(Object.prototype.hasOwnProperty.call(storedComment, "constructor")).toBe(false); + expect(Object.prototype.hasOwnProperty.call(storedComment, "prototype")).toBe(false); + expect(({} as any).polluted).toBeUndefined(); const persistedComments = await accountsDatabase.getAccountComments(account.id); expect(persistedComments[0]).toMatchObject({ @@ -553,6 +563,9 @@ describe("accounts-actions", () => { reason: "AI moderation reason", removed: false, }); + expect(Object.prototype.hasOwnProperty.call(persistedComments[0], "__proto__")).toBe(false); + expect(Object.prototype.hasOwnProperty.call(persistedComments[0], "constructor")).toBe(false); + expect(Object.prototype.hasOwnProperty.call(persistedComments[0], "prototype")).toBe(false); }); test("publishVote with accountName uses named account", async () => { diff --git a/src/stores/accounts/accounts-actions.ts b/src/stores/accounts/accounts-actions.ts index 2341de5f..887ca1ba 100644 --- a/src/stores/accounts/accounts-actions.ts +++ b/src/stores/accounts/accounts-actions.ts @@ -77,9 +77,11 @@ const getClientsSnapshotForState = (clients: any): any => { return Object.keys(snapshot).length > 0 ? snapshot : undefined; }; +const unsafeCommentUpdatePropertyNames = new Set(["__proto__", "constructor", "prototype"]); + const applyChallengeVerificationCommentUpdateToPublication = ( challengeVerification: ChallengeVerification | undefined, - publication: any, + publication: Record | undefined, ) => { const commentUpdate = challengeVerification?.commentUpdate; if ( @@ -92,7 +94,11 @@ const applyChallengeVerificationCommentUpdateToPublication = ( return; } - Object.assign(publication, commentUpdate); + for (const key of Object.keys(commentUpdate)) { + if (!unsafeCommentUpdatePropertyNames.has(key)) { + publication[key] = commentUpdate[key]; + } + } }; const syncCommentClientsSnapshot = (