From 09d2586990feb0f6c2cb225b7cf1893f61972cd9 Mon Sep 17 00:00:00 2001 From: Taylor Beeston Date: Thu, 18 Dec 2025 13:39:45 -0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20Correctly=20invalidate=20did?= =?UTF-8?q?=20web=20cache=20when=20claiming=20a=20boost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../credential/relationships/create.ts | 20 +++++--- .../credential/relationships/read.ts | 31 ++++++++++-- .../src/helpers/claim-hooks.helpers.ts | 7 +-- .../brain-service/test/credentials.spec.ts | 49 ++++++++++++++++++- 4 files changed, 92 insertions(+), 15 deletions(-) diff --git a/services/learn-card-network/brain-service/src/accesslayer/credential/relationships/create.ts b/services/learn-card-network/brain-service/src/accesslayer/credential/relationships/create.ts index d834ac61c9..31eda499e9 100644 --- a/services/learn-card-network/brain-service/src/accesslayer/credential/relationships/create.ts +++ b/services/learn-card-network/brain-service/src/accesslayer/credential/relationships/create.ts @@ -13,6 +13,7 @@ import { import { flattenObject } from '@helpers/objects.helpers'; import { ProfileType } from 'types/profile'; import { clearDidWebCacheForChildProfileManagers } from '@accesslayer/boost/relationships/update'; +import { getBoostIdForCredentialInstance } from '@accesslayer/credential/relationships/read'; import { DbTermsType } from 'types/consentflowcontract'; export const createSentCredentialRelationship = async ( @@ -38,7 +39,9 @@ export const createSentCredentialRelationship = async ( { model: Credential, where: { id: credential.id }, identifier: 'credential' }, ], }) - .create(`(profile)-[r:${Profile.getRelationshipByAlias('credentialSent').name}]->(credential)`) + .create( + `(profile)-[r:${Profile.getRelationshipByAlias('credentialSent').name}]->(credential)` + ) .set('r = $params') .run(); }; @@ -66,7 +69,11 @@ export const createReceivedCredentialRelationship = async ( { model: Profile, where: { profileId: to.profileId }, identifier: 'profile' }, ], }) - .create(`(credential)-[r:${Credential.getRelationshipByAlias('credentialReceived').name}]->(profile)`) + .create( + `(credential)-[r:${ + Credential.getRelationshipByAlias('credentialReceived').name + }]->(profile)` + ) .set('r = $params') .run(); }; @@ -92,7 +99,8 @@ export const setDefaultClaimedRole = async ( .with('boost, role') .match({ model: Profile, where: { profileId: profile.profileId }, identifier: 'profile' }) .where( - `NOT EXISTS { MATCH (profile)-[:${Boost.getRelationshipByAlias('hasRole').name + `NOT EXISTS { MATCH (profile)-[:${ + Boost.getRelationshipByAlias('hasRole').name }]-(boost)}` ) .create({ @@ -105,11 +113,11 @@ export const setDefaultClaimedRole = async ( .run(); try { - const vc = JSON.parse(credential.credential); + const boostId = await getBoostIdForCredentialInstance(credential); - if (vc.boostId) await clearDidWebCacheForChildProfileManagers(vc.boostId); + if (boostId) await clearDidWebCacheForChildProfileManagers(boostId); } catch (error) { - console.error('Invalid credential JSON?', error); + console.error('Could not clear did:web cache for accepted boost credential', error); } }; diff --git a/services/learn-card-network/brain-service/src/accesslayer/credential/relationships/read.ts b/services/learn-card-network/brain-service/src/accesslayer/credential/relationships/read.ts index a04495fe1a..137dc89cb3 100644 --- a/services/learn-card-network/brain-service/src/accesslayer/credential/relationships/read.ts +++ b/services/learn-card-network/brain-service/src/accesslayer/credential/relationships/read.ts @@ -21,10 +21,10 @@ export const getCredentialSentToProfile = async ( to: ProfileType ): Promise< | { - source: ProfileType; - relationship: ProfileRelationships['credentialSent']['RelationshipProperties']; - target: CredentialInstance; - } + source: ProfileType; + relationship: ProfileRelationships['credentialSent']['RelationshipProperties']; + target: CredentialInstance; + } | undefined > => { const data = ( @@ -199,7 +199,8 @@ export const getAllCredentialsForProfileTerms = async ( if (!includeReceived) { query = query.where( - `NOT EXISTS { MATCH (credential)-[:${Credential.getRelationshipByAlias('credentialReceived').name + `NOT EXISTS { MATCH (credential)-[:${ + Credential.getRelationshipByAlias('credentialReceived').name }]-(profile) }` ); } @@ -243,3 +244,23 @@ export const getAllCredentialsForProfileTerms = async ( transaction: inflateObject(record.transaction), })); }; + +export const getBoostIdForCredentialInstance = async ( + credential: CredentialInstance +): Promise => { + const results = convertQueryResultToPropertiesObjectArray<{ boostId: string }>( + await new QueryBuilder() + .match({ + related: [ + { identifier: 'credential', model: Credential, where: { id: credential.id } }, + Credential.getRelationshipByAlias('instanceOf'), + { identifier: 'boost', model: Boost }, + ], + }) + .return('boost.id AS boostId') + .limit(1) + .run() + ); + + return results[0]?.boostId; +}; diff --git a/services/learn-card-network/brain-service/src/helpers/claim-hooks.helpers.ts b/services/learn-card-network/brain-service/src/helpers/claim-hooks.helpers.ts index 7ab779fe87..120c49fd1e 100644 --- a/services/learn-card-network/brain-service/src/helpers/claim-hooks.helpers.ts +++ b/services/learn-card-network/brain-service/src/helpers/claim-hooks.helpers.ts @@ -3,6 +3,7 @@ import { QueryBuilder } from 'neogma'; import { CredentialInstance, Credential, Boost, Profile, ClaimHook, Role } from '@models'; import { ProfileType } from 'types/profile'; import { clearDidWebCacheForChildProfileManagers } from '@accesslayer/boost/relationships/update'; +import { getBoostIdForCredentialInstance } from '@accesslayer/credential/relationships/read'; import { getAdminRole } from '@accesslayer/role/read'; const processPermissionsClaimHooks = async ( @@ -128,10 +129,10 @@ export const processClaimHooks = async ( ]); try { - const vc = JSON.parse(credential.credential); + const boostId = await getBoostIdForCredentialInstance(credential); - if (vc.boostId) await clearDidWebCacheForChildProfileManagers(vc.boostId); + if (boostId) await clearDidWebCacheForChildProfileManagers(boostId); } catch (error) { - console.error('Invalid credential JSON?', error); + console.error('Could not clear did:web cache for accepted boost credential', error); } }; diff --git a/services/learn-card-network/brain-service/test/credentials.spec.ts b/services/learn-card-network/brain-service/test/credentials.spec.ts index da105e28db..555128c296 100644 --- a/services/learn-card-network/brain-service/test/credentials.spec.ts +++ b/services/learn-card-network/brain-service/test/credentials.spec.ts @@ -1,9 +1,15 @@ import { vi } from 'vitest'; import { getClient, getUser } from './helpers/getClient'; -import { testVc, sendCredential } from './helpers/send'; +import { testVc, sendBoost, sendCredential, testUnsignedBoost } from './helpers/send'; import { Profile, Credential } from '@models'; import * as Notifications from '@helpers/notifications.helpers'; import { addNotificationToQueueSpy } from './helpers/spies'; +import { + getDidDocForProfile, + getDidDocForProfileManager, + setDidDocForProfile, + setDidDocForProfileManager, +} from '@cache/did-docs'; const noAuthClient = getClient(); let userA: Awaited>; @@ -173,6 +179,47 @@ describe('Credentials', () => { message: expect.stringContaining('already been received'), }); }); + + it('should clear did:web cache for managed profiles when accepting a boost that grants canManageChildrenProfiles', async () => { + const boostUri = await userA.clients.fullAuth.boost.createBoost({ + credential: testUnsignedBoost, + claimPermissions: { canManageChildrenProfiles: true }, + }); + + const managerDid = + await userA.clients.fullAuth.profileManager.createChildProfileManager({ + parentUri: boostUri, + profile: {}, + }); + + const managerId = managerDid.split(':')[4]!; + + const managerClient = getClient({ did: managerDid, isChallengeValid: true }); + + const managedProfileId = 'managed-profile'; + + await managerClient.profileManager.createManagedProfile({ + profileId: managedProfileId, + }); + + await setDidDocForProfileManager(managerId, { id: managerDid } as any); + await setDidDocForProfile(managedProfileId, { id: managedProfileId } as any); + + expect(await getDidDocForProfileManager(managerId)).toBeTruthy(); + expect(await getDidDocForProfile(managedProfileId)).toBeTruthy(); + + const credentialUri = await sendBoost( + { profileId: 'usera', user: userA }, + { profileId: 'userb', user: userB }, + boostUri, + false + ); + + await userB.clients.fullAuth.credential.acceptCredential({ uri: credentialUri }); + + expect(await getDidDocForProfileManager(managerId)).toBeFalsy(); + expect(await getDidDocForProfile(managedProfileId)).toBeFalsy(); + }); }); describe('receivedCredentials', () => { From 9023a5b9b25dbd95044a1c64a9d595730f2aac26 Mon Sep 17 00:00:00 2001 From: Taylor Beeston Date: Thu, 18 Dec 2025 13:41:52 -0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=94=96=20Changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/eight-mugs-cover.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eight-mugs-cover.md diff --git a/.changeset/eight-mugs-cover.md b/.changeset/eight-mugs-cover.md new file mode 100644 index 0000000000..abf4e21e12 --- /dev/null +++ b/.changeset/eight-mugs-cover.md @@ -0,0 +1,5 @@ +--- +'@learncard/network-brain-service': patch +--- + +Correctly invalidate did web cache when claming a boost