From 2e268dbb8493e9b01252ee5501db71c9fe805201 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 24 Oct 2026 15:54:57 -0600 Subject: [PATCH] fix a single conditional such that reference to a previous claim isn't lost --- src/libs/confirmationTarget.ts | 33 ++++++++++++++++++++ src/libs/endorserServer.ts | 24 +++------------ src/test/confirmationTarget.test.ts | 47 +++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 20 deletions(-) create mode 100644 src/libs/confirmationTarget.ts create mode 100644 src/test/confirmationTarget.test.ts diff --git a/src/libs/confirmationTarget.ts b/src/libs/confirmationTarget.ts new file mode 100644 index 00000000..91145d4c --- /dev/null +++ b/src/libs/confirmationTarget.ts @@ -0,0 +1,33 @@ +/** + * @fileoverview How a confirmation names the claim it confirms + * + * This module depends only on ramda so that it stays loadable and testable + * without a database, a network client, or a build-time environment. + * + * @module confirmationTarget + */ + +import * as R from "ramda"; + +// similar logic is found in endorser-mobile +// The lastClaimId is added even when there's an identifier, because one handle +// can hold different claims (eg. every Person claim about a DID shares that DID +// as its handle) and the server would otherwise take the latest one. +export function addLastClaimOrHandleAsIdIfMissing( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + obj: any, + lastClaimId?: string, + handleId?: string, +) { + if (!obj.lastClaimId && lastClaimId) { + const result = R.clone(obj); + result.lastClaimId = lastClaimId; + return result; + } else if (!obj.identifier && handleId) { + const result = R.clone(obj); + result.identifier = handleId; + return result; + } else { + return obj; + } +} diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index c0ecf92f..e0f4c68d 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -22,6 +22,8 @@ import { sha256 } from "ethereum-cryptography/sha256"; import { LRUCache } from "lru-cache"; import * as R from "ramda"; +import { addLastClaimOrHandleAsIdIfMissing } from "./confirmationTarget"; + import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app"; import { NOTIFICATION_TIMEOUTS } from "../utils/notificationUtils"; import { createNotifyHelpers } from "../utils/notify"; @@ -112,6 +114,8 @@ export { addEndorserPrefix, stripEndorserPrefix, } from "./endorserHandleIds"; +// Same reason as above. +export { addLastClaimOrHandleAsIdIfMissing } from "./confirmationTarget"; export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper = { @@ -233,26 +237,6 @@ export function removeSchemaContext(obj: any) { : obj; } -// similar logic is found in endorser-mobile -export function addLastClaimOrHandleAsIdIfMissing( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - obj: any, - lastClaimId?: string, - handleId?: string, -) { - if (!obj.identifier && lastClaimId) { - const result = R.clone(obj); - result.lastClaimId = lastClaimId; - return result; - } else if (!obj.identifier && handleId) { - const result = R.clone(obj); - result.identifier = handleId; - return result; - } else { - return obj; - } -} - // return clone of object without any nested *VisibleToDids keys // similar code is also contained in endorser-mobile // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/test/confirmationTarget.test.ts b/src/test/confirmationTarget.test.ts new file mode 100644 index 00000000..f04b0b3d --- /dev/null +++ b/src/test/confirmationTarget.test.ts @@ -0,0 +1,47 @@ +import { addLastClaimOrHandleAsIdIfMissing } from "@/libs/confirmationTarget"; + +/** + * A confirmation names the claim it confirms. Every Person claim about a DID + * has that DID as its identifier, so the identifier alone points the server at + * whichever of them is latest, not at the one on screen. + */ + +const DID = "did:ethr:0xce097412b1Cc8E0DBe294b3d8243e2b3Cf8E80ba"; +const CLAIM_ID = "01M0QQ897FZ15R2KMZFSZ66W7V"; +const HANDLE_ID = "https://endorser.ch/entity/01M0QQ897FZ15R2KMZFSZ66W7V"; + +describe("addLastClaimOrHandleAsIdIfMissing", () => { + it("names the exact claim even when it has an identifier", () => { + const claim = { "@type": "Person", identifier: DID }; + expect(addLastClaimOrHandleAsIdIfMissing(claim, CLAIM_ID, DID)).toEqual({ + "@type": "Person", + identifier: DID, + lastClaimId: CLAIM_ID, + }); + }); + + it("does not change the claim it was given", () => { + const claim = { "@type": "Person", identifier: DID }; + addLastClaimOrHandleAsIdIfMissing(claim, CLAIM_ID, DID); + expect(claim).toEqual({ "@type": "Person", identifier: DID }); + }); + + it("keeps a lastClaimId the claim already carries", () => { + const claim = { "@type": "Person", lastClaimId: "01EARLIER" }; + expect(addLastClaimOrHandleAsIdIfMissing(claim, CLAIM_ID)).toBe(claim); + }); + + it("falls back to the handle as identifier when there is no claim ID", () => { + const claim = { "@type": "GiveAction" }; + expect( + addLastClaimOrHandleAsIdIfMissing(claim, undefined, HANDLE_ID), + ).toEqual({ "@type": "GiveAction", identifier: HANDLE_ID }); + }); + + it("keeps an identifier the claim already carries over the handle", () => { + const claim = { "@type": "Person", identifier: DID }; + expect(addLastClaimOrHandleAsIdIfMissing(claim, undefined, HANDLE_ID)).toBe( + claim, + ); + }); +});