From 3ee7ece82ca66885b85b7d3e89dde5ff62c4fb90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7=20Muntaner?= Date: Fri, 17 Jan 2025 10:28:37 +0100 Subject: [PATCH] Add storage utils to persist cancelled RP IDs (#2784) * Add storage utils to persist cancelled RP IDs * Rename var --- src/frontend/src/storage/index.test.ts | 76 ++++++++++++++++++++ src/frontend/src/storage/index.ts | 98 ++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) diff --git a/src/frontend/src/storage/index.test.ts b/src/frontend/src/storage/index.test.ts index 475d3cc80f..df622ea81a 100644 --- a/src/frontend/src/storage/index.test.ts +++ b/src/frontend/src/storage/index.test.ts @@ -11,9 +11,12 @@ import { expect } from "vitest"; import { MAX_SAVED_ANCHORS, MAX_SAVED_PRINCIPALS, + addAnchorCancelledRpId, + cleanUpRpIdMapper, getAnchorByPrincipal, getAnchorIfLastUsed, getAnchors, + getCancelledRpIds, setAnchorUsed, setKnownPrincipal, } from "."; @@ -414,6 +417,79 @@ test( }) ); +test( + "should create an anchor after adding a cancelled RP ID", + withStorage(async () => { + const origin = "https://example.com"; + const userNumber = BigInt(10000); + const cancelledRpId = "https://identity.ic0.app"; + + expect(await getAnchors()).toEqual([]); + await addAnchorCancelledRpId({ userNumber, origin, cancelledRpId }); + expect(await getAnchors()).toEqual([userNumber]); + }) +); + +test( + "should reset the cancelled RP IDs by user number", + withStorage(async () => { + const origin = "https://example.com"; + const userNumber = BigInt(10000); + const anotherUserNumber = BigInt(10001); + const cancelledRpId = "https://identity.ic0.app"; + + expect(await getCancelledRpIds({ userNumber, origin })).toEqual( + new Set([]) + ); + await addAnchorCancelledRpId({ userNumber, origin, cancelledRpId }); + await addAnchorCancelledRpId({ + userNumber: anotherUserNumber, + origin, + cancelledRpId, + }); + expect(await getCancelledRpIds({ userNumber, origin })).toEqual( + new Set([cancelledRpId]) + ); + expect( + await getCancelledRpIds({ userNumber: anotherUserNumber, origin }) + ).toEqual(new Set([cancelledRpId])); + await cleanUpRpIdMapper(userNumber); + expect(await getCancelledRpIds({ userNumber, origin })).toEqual( + new Set([]) + ); + expect( + await getCancelledRpIds({ userNumber: anotherUserNumber, origin }) + ).toEqual(new Set([cancelledRpId])); + }) +); + +test( + "should add cancelled RP IDs per origin", + withStorage(async () => { + const origin = "https://example.com"; + const undefinedOrigin = undefined; + const anotherOrigin = "https://another.com"; + const userNumber = BigInt(10000); + const cancelledRpId = "https://identity.ic0.app"; + + expect(await getCancelledRpIds({ userNumber, origin })).toEqual( + new Set([]) + ); + await addAnchorCancelledRpId({ userNumber, origin, cancelledRpId }); + await addAnchorCancelledRpId({ + userNumber, + origin, + cancelledRpId: undefinedOrigin, + }); + expect(await getCancelledRpIds({ userNumber, origin })).toEqual( + new Set([cancelledRpId, undefined]) + ); + expect( + await getCancelledRpIds({ userNumber, origin: anotherOrigin }) + ).toEqual(new Set([])); + }) +); + /** Test storage usage. Storage is cleared after the callback has returned. * If `before` is specified, storage is populated with its content before the test is run. * If `after` is specified, the content of storage are checked against `after` after the diff --git a/src/frontend/src/storage/index.ts b/src/frontend/src/storage/index.ts index 7752a8ad54..71e2ee8b3e 100644 --- a/src/frontend/src/storage/index.ts +++ b/src/frontend/src/storage/index.ts @@ -189,6 +189,94 @@ export const setKnownPrincipal = async ({ }); }; +/** + * Sets the `cancelledRpIdsMapper` field to an empty object. + * + * This is needed when a user removes a device from their account. + * @param userNumber {bigint} The anchor number. + */ +export const cleanUpRpIdMapper = async (userNumber: bigint) => { + await withStorage((storage) => { + const anchorIndex = userNumber.toString(); + const anchors = storage.anchors; + const oldAnchor = anchors[anchorIndex]; + + if (isNullish(oldAnchor)) { + return storage; + } + + storage.anchors[anchorIndex] = { + ...oldAnchor, + cancelledRpIdsMapper: {}, + }; + + return storage; + }); +}; + +/** + * Adds a RP ID as cancelled RP ID into the set of cancelled RP IDs for that anchor and origin. + * + * @param userNumber + * @param origin + * @param cancelledRpId + */ +export const addAnchorCancelledRpId = async ({ + userNumber, + origin, + cancelledRpId, +}: { + userNumber: bigint; + origin: string; + cancelledRpId: string | undefined; +}) => { + await withStorage((storage) => { + const anchorIndex = userNumber.toString(); + const anchors = storage.anchors; + const defaultAnchor: Omit = { + knownPrincipals: [], + }; + const oldAnchor = anchors[anchorIndex] ?? defaultAnchor; + + const cancelledRpIdsMapper = oldAnchor?.cancelledRpIdsMapper ?? {}; + const originCancelledRpIds = cancelledRpIdsMapper[origin] ?? []; + originCancelledRpIds.push(cancelledRpId); + + storage.anchors[anchorIndex] = { + ...oldAnchor, + lastUsedTimestamp: nowMillis(), + cancelledRpIdsMapper: { + ...cancelledRpIdsMapper, + [origin]: originCancelledRpIds, + }, + }; + + return storage; + }); +}; + +/** + * Returns the last RP ID successfully used for the specific anchor in the specific ii origin. + * + * @param params + * @param params.userNumber The anchor number. + * @param params.origin The origin of the ii. + * @returns {Set} The set of cancelled RP IDs for the anchor and origin. `undefined` is a valid cancelled RP ID. + */ +export const getCancelledRpIds = async ({ + userNumber, + origin, +}: { + userNumber: bigint; + origin: string; +}): Promise> => { + const storage = await readStorage(); + const anchors = storage.anchors; + + const anchorData = anchors[userNumber.toString()]; + return new Set(anchorData?.cancelledRpIdsMapper?.[origin] ?? []); +}; + /** Accessing functions */ // Simply read the storage without updating it @@ -653,9 +741,19 @@ const PrincipalDataV4 = z.object({ lastUsedTimestamp: z.number(), }); +/** + * Mapper of which RP ID didn't work for the user + * + * Record> + */ +const cancelledRpIdsMapper = z.record( + z.array(z.union([z.string(), z.undefined()])) +); + const AnchorV4 = z.object({ /** Timestamp (mills since epoch) of when anchor was last used */ lastUsedTimestamp: z.number(), + cancelledRpIdsMapper: cancelledRpIdsMapper.optional(), knownPrincipals: z.array(PrincipalDataV4), });