From ed422fe2930275b7d793a2c8b73af8dde12c186c Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 09:13:26 +0200 Subject: [PATCH] startCommunityAuthentication refactored --- .../src/federation/authenticateCommunities.ts | 133 +++++++++++------- core/src/index.ts | 1 + .../logic/CommunityHandshakeState.logic.ts | 22 +++ core/src/logic/index.ts | 1 + ...95-add_community_handshake_states_table.ts | 3 +- .../src/entity/CommunityHandshakeState.ts | 5 +- database/src/entity/index.ts | 6 +- .../src/enum/CommunityHandshakeStateType.ts | 1 + database/src/index.ts | 60 +------- .../CommunityHandshakeStateLogging.view.ts | 25 ++++ database/src/logging/CommunityLogging.view.ts | 5 +- database/src/logging/index.ts | 2 + database/src/queries/communities.test.ts | 20 ++- database/src/queries/communities.ts | 9 +- database/src/queries/communityHandshakes.ts | 15 ++ database/src/queries/index.ts | 1 + database/src/seeds/community.ts | 22 ++- 17 files changed, 212 insertions(+), 119 deletions(-) create mode 100644 core/src/logic/CommunityHandshakeState.logic.ts create mode 100644 core/src/logic/index.ts create mode 100644 database/src/logging/CommunityHandshakeStateLogging.view.ts create mode 100644 database/src/queries/communityHandshakes.ts diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index f186adb35..0a94b24c8 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -1,5 +1,14 @@ -import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, getHomeCommunity } from 'database' -import { validate as validateUUID, version as versionUUID } from 'uuid' +import { + CommunityHandshakeState as DbCommunityHandshakeState, + CommunityHandshakeStateLoggingView, + CommunityLoggingView, + Community as DbCommunity, + FederatedCommunity as DbFederatedCommunity, + FederatedCommunityLoggingView, + findPendingCommunityHandshake, + getHomeCommunityWithFederatedCommunityOrFail, + CommunityHandshakeStateType +} from 'database' import { randombytes_random } from 'sodium-native' import { CONFIG as CONFIG_CORE } from 'core' @@ -11,65 +20,91 @@ import { communityAuthenticatedSchema, encryptAndSign, OpenConnectionJwtPayloadT import { getLogger } from 'log4js' import { AuthenticationClientFactory } from './client/AuthenticationClientFactory' import { EncryptedTransferArgs } from 'core' +import { CommunityHandshakeStateLogic } from 'core' -const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities`) +const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.${functionName}`) export async function startCommunityAuthentication( fedComB: DbFederatedCommunity, ): Promise { - const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.startCommunityAuthentication`) + const methodLogger = createLogger('startCommunityAuthentication') const handshakeID = randombytes_random().toString() methodLogger.addContext('handshakeID', handshakeID) methodLogger.debug(`startCommunityAuthentication()...`, { fedComB: new FederatedCommunityLoggingView(fedComB), }) - const homeComA = await getHomeCommunity() - methodLogger.debug('homeComA', new CommunityLoggingView(homeComA!)) - const homeFedComA = await DbFederatedCommunity.findOneByOrFail({ - foreign: false, - apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API, - }) - methodLogger.debug('homeFedComA', new FederatedCommunityLoggingView(homeFedComA)) + const homeComA = await getHomeCommunityWithFederatedCommunityOrFail(fedComB.apiVersion) + methodLogger.debug('homeComA', new CommunityLoggingView(homeComA)) + // check if result is like expected + // TODO: use zod/valibot + if ( + !homeComA.federatedCommunities || + homeComA.federatedCommunities.length === 0 || + homeComA.federatedCommunities[0].apiVersion !== fedComB.apiVersion + ) { + throw new Error(`Missing home community or federated community with api version ${fedComB.apiVersion}`) + } + const homeFedComA = homeComA.federatedCommunities[0] const comB = await DbCommunity.findOneByOrFail({ publicKey: fedComB.publicKey }) methodLogger.debug('started with comB:', new CommunityLoggingView(comB)) // check if communityUuid is not a valid v4Uuid - try { - // communityAuthenticatedSchema.safeParse return true - // - if communityUuid is a valid v4Uuid and - // - if authenticatedAt is a valid date - if (comB && !communityAuthenticatedSchema.safeParse(comB).success) { - methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', comB.communityUuid || 'null', comB.authenticatedAt || 'null') - const client = AuthenticationClientFactory.getInstance(fedComB) - - if (client instanceof V1_0_AuthenticationClient) { - if (!comB.publicJwtKey) { - throw new Error('Public JWT key still not exist for comB ' + comB.name) - } - //create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey - const payload = new OpenConnectionJwtPayloadType(handshakeID, - ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion), - ) - methodLogger.debug('payload', payload) - const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!) - methodLogger.debug('jws', jws) - // prepare the args for the client invocation - const args = new EncryptedTransferArgs() - args.publicKey = homeComA!.publicKey.toString('hex') - args.jwt = jws - args.handshakeID = handshakeID - methodLogger.debug('before client.openConnection() args:', args) - const result = await client.openConnection(args) - if (result) { - methodLogger.debug(`successful initiated at community:`, fedComB.endPoint) - } else { - methodLogger.error(`can't initiate at community:`, fedComB.endPoint) - } - } - } else { - methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`) - } - } catch (err) { - methodLogger.error(`Error:`, err) + + // communityAuthenticatedSchema.safeParse return true + // - if communityUuid is a valid v4Uuid and + // - if authenticatedAt is a valid date + if (communityAuthenticatedSchema.safeParse(comB).success) { + methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`) + return + } + methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', + comB.communityUuid || 'null', comB.authenticatedAt || 'null' + ) + + // check if a authentication is already in progress + const existingState = await findPendingCommunityHandshake(fedComB, false) + if (existingState) { + const logic = new CommunityHandshakeStateLogic(existingState) + if (!await logic.isTimeoutUpdate()) { + // authentication with community and api version is still in progress and it is not timeout yet + methodLogger.debug('existingState', new CommunityHandshakeStateLoggingView(existingState)) + return + } + } + + const state = new DbCommunityHandshakeState() + state.publicKey = fedComB.publicKey + state.apiVersion = fedComB.apiVersion + state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION + state.handshakeId = parseInt(handshakeID) + + const client = AuthenticationClientFactory.getInstance(fedComB) + + if (client instanceof V1_0_AuthenticationClient) { + if (!comB.publicJwtKey) { + state.lastError = 'Public JWT key still not exist for comB ' + comB.name + await state.save() + throw new Error(state.lastError) + } + const stateSaveResolver = state.save() + //create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey + const payload = new OpenConnectionJwtPayloadType(handshakeID, + ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion), + ) + methodLogger.debug('payload', payload) + const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!) + methodLogger.debug('jws', jws) + // prepare the args for the client invocation + const args = new EncryptedTransferArgs() + args.publicKey = homeComA!.publicKey.toString('hex') + args.jwt = jws + args.handshakeID = handshakeID + await stateSaveResolver + methodLogger.debug('before client.openConnection() args:', args) + const result = await client.openConnection(args) + if (result) { + methodLogger.info(`successful initiated at community:`, fedComB.endPoint) + } else { + methodLogger.error(`can't initiate at community:`, fedComB.endPoint) + } } - methodLogger.removeContext('handshakeID') } diff --git a/core/src/index.ts b/core/src/index.ts index a355bb9bd..a10eb6caa 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -22,4 +22,5 @@ export * from './util/calculateSenderBalance' export * from './util/utilities' export * from './validation/user' export * from './config/index' +export * from './logic' diff --git a/core/src/logic/CommunityHandshakeState.logic.ts b/core/src/logic/CommunityHandshakeState.logic.ts new file mode 100644 index 000000000..a3e8631eb --- /dev/null +++ b/core/src/logic/CommunityHandshakeState.logic.ts @@ -0,0 +1,22 @@ +import { CommunityHandshakeState, CommunityHandshakeStateType } from 'database' +import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared' + +export class CommunityHandshakeStateLogic { + public constructor(private communityHandshakeStateEntity: CommunityHandshakeState) {} + + /** + * Check for expired state and if not, check timeout and update (write into db) to expired state + * @returns true if the community handshake state is expired + */ + public async isTimeoutUpdate(): Promise { + if (this.communityHandshakeStateEntity.status === CommunityHandshakeStateType.EXPIRED) { + return true + } + if (Date.now() - this.communityHandshakeStateEntity.updatedAt.getTime() > FEDERATION_AUTHENTICATION_TIMEOUT_MS) { + this.communityHandshakeStateEntity.status = CommunityHandshakeStateType.EXPIRED + await this.communityHandshakeStateEntity.save() + return true + } + return false + } +} diff --git a/core/src/logic/index.ts b/core/src/logic/index.ts new file mode 100644 index 000000000..3a5c48f58 --- /dev/null +++ b/core/src/logic/index.ts @@ -0,0 +1 @@ +export { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic' \ No newline at end of file diff --git a/database/migration/migrations/0095-add_community_handshake_states_table.ts b/database/migration/migrations/0095-add_community_handshake_states_table.ts index a00d9f6d7..38553c336 100644 --- a/database/migration/migrations/0095-add_community_handshake_states_table.ts +++ b/database/migration/migrations/0095-add_community_handshake_states_table.ts @@ -5,12 +5,13 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis handshake_id int unsigned NOT NULL, one_time_code int unsigned NOT NULL, public_key binary(32) NOT NULL, + api_version varchar(255) NOT NULL, status varchar(255) NOT NULL DEFAULT 'OPEN_CONNECTION', last_error text, created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), - KEY idx_public_key (public_key), + KEY idx_public_key (public_key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) } diff --git a/database/src/entity/CommunityHandshakeState.ts b/database/src/entity/CommunityHandshakeState.ts index 25352e2b4..4fb62a733 100644 --- a/database/src/entity/CommunityHandshakeState.ts +++ b/database/src/entity/CommunityHandshakeState.ts @@ -16,10 +16,13 @@ export class CommunityHandshakeState extends BaseEntity { @Column({ name: 'public_key', type: 'binary', length: 32 }) publicKey: Buffer + @Column({ name: 'api_version', type: 'varchar', length: 255 }) + apiVersion: string + @Column({ type: 'varchar', length: 255, - default: CommunityHandshakeStateType.OPEN_CONNECTION, + default: CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION, nullable: false, }) status: CommunityHandshakeStateType diff --git a/database/src/entity/index.ts b/database/src/entity/index.ts index 4d4b5d803..32bbe239b 100644 --- a/database/src/entity/index.ts +++ b/database/src/entity/index.ts @@ -19,6 +19,7 @@ import { UserRole } from './UserRole' export { Community, + CommunityHandshakeState, Contribution, ContributionLink, ContributionMessage, @@ -26,8 +27,7 @@ export { Event, FederatedCommunity, LoginElopageBuys, - Migration, - CommunityHandshakeState, + Migration, ProjectBranding, OpenaiThreads, PendingTransaction, @@ -40,6 +40,7 @@ export { export const entities = [ Community, + CommunityHandshakeState, Contribution, ContributionLink, ContributionMessage, @@ -48,7 +49,6 @@ export const entities = [ FederatedCommunity, LoginElopageBuys, Migration, - CommunityHandshakeState, ProjectBranding, OpenaiThreads, PendingTransaction, diff --git a/database/src/enum/CommunityHandshakeStateType.ts b/database/src/enum/CommunityHandshakeStateType.ts index e79c4a04c..a047e740d 100644 --- a/database/src/enum/CommunityHandshakeStateType.ts +++ b/database/src/enum/CommunityHandshakeStateType.ts @@ -1,4 +1,5 @@ export enum CommunityHandshakeStateType { + START_COMMUNITY_AUTHENTICATION = 'START_COMMUNITY_AUTHENTICATION', OPEN_CONNECTION = 'OPEN_CONNECTION', OPEN_CONNECTION_CALLBACK = 'OPEN_CONNECTION_CALLBACK', diff --git a/database/src/index.ts b/database/src/index.ts index b694567a2..45b60530c 100644 --- a/database/src/index.ts +++ b/database/src/index.ts @@ -1,63 +1,7 @@ import { latestDbVersion } from './detectLastDBVersion' -import { Community } from './entity/Community' -import { Contribution } from './entity/Contribution' -import { ContributionLink } from './entity/ContributionLink' -import { ContributionMessage } from './entity/ContributionMessage' -import { DltTransaction } from './entity/DltTransaction' -import { Event } from './entity/Event' -import { FederatedCommunity } from './entity/FederatedCommunity' -import { LoginElopageBuys } from './entity/LoginElopageBuys' -import { Migration } from './entity/Migration' -import { OpenaiThreads } from './entity/OpenaiThreads' -import { PendingTransaction } from './entity/PendingTransaction' -import { ProjectBranding } from './entity/ProjectBranding' -import { Transaction } from './entity/Transaction' -import { TransactionLink } from './entity/TransactionLink' -import { User } from './entity/User' -import { UserContact } from './entity/UserContact' -import { UserRole } from './entity/UserRole' - -export { - Community, - Contribution, - ContributionLink, - ContributionMessage, - DltTransaction, - Event, - FederatedCommunity, - LoginElopageBuys, - Migration, - ProjectBranding, - OpenaiThreads, - PendingTransaction, - Transaction, - TransactionLink, - User, - UserContact, - UserRole, -} - -export const entities = [ - Community, - Contribution, - ContributionLink, - ContributionMessage, - DltTransaction, - Event, - FederatedCommunity, - LoginElopageBuys, - Migration, - ProjectBranding, - OpenaiThreads, - PendingTransaction, - Transaction, - TransactionLink, - User, - UserContact, - UserRole, -] - export { latestDbVersion } + +export * from './entity' export * from './logging' export * from './queries' export * from './util' diff --git a/database/src/logging/CommunityHandshakeStateLogging.view.ts b/database/src/logging/CommunityHandshakeStateLogging.view.ts new file mode 100644 index 000000000..5345474a0 --- /dev/null +++ b/database/src/logging/CommunityHandshakeStateLogging.view.ts @@ -0,0 +1,25 @@ +import { CommunityHandshakeState } from '..' +import { AbstractLoggingView } from './AbstractLogging.view' +import { FederatedCommunityLoggingView } from './FederatedCommunityLogging.view' + +export class CommunityHandshakeStateLoggingView extends AbstractLoggingView { + public constructor(private self: CommunityHandshakeState) { + super() + } + + public toJSON(): any { + return { + id: this.self.id, + handshakeId: this.self.handshakeId, + oneTimeCode: this.self.oneTimeCode, + publicKey: this.self.publicKey.toString(this.bufferStringFormat), + status: this.self.status, + lastError: this.self.lastError, + createdAt: this.dateToString(this.self.createdAt), + updatedAt: this.dateToString(this.self.updatedAt), + federatedCommunity: this.self.federatedCommunity + ? new FederatedCommunityLoggingView(this.self.federatedCommunity) + : undefined, + } + } +} \ No newline at end of file diff --git a/database/src/logging/CommunityLogging.view.ts b/database/src/logging/CommunityLogging.view.ts index c06a4db41..1d675828c 100644 --- a/database/src/logging/CommunityLogging.view.ts +++ b/database/src/logging/CommunityLogging.view.ts @@ -1,5 +1,5 @@ import { Community } from '../entity' - +import { FederatedCommunityLoggingView } from './FederatedCommunityLogging.view' import { AbstractLoggingView } from './AbstractLogging.view' export class CommunityLoggingView extends AbstractLoggingView { @@ -21,6 +21,9 @@ export class CommunityLoggingView extends AbstractLoggingView { creationDate: this.dateToString(this.self.creationDate), createdAt: this.dateToString(this.self.createdAt), updatedAt: this.dateToString(this.self.updatedAt), + federatedCommunities: this.self.federatedCommunities?.map( + (federatedCommunity) => new FederatedCommunityLoggingView(federatedCommunity) + ), } } } diff --git a/database/src/logging/index.ts b/database/src/logging/index.ts index c19bd9a57..522fc3b56 100644 --- a/database/src/logging/index.ts +++ b/database/src/logging/index.ts @@ -11,6 +11,7 @@ import { TransactionLoggingView } from './TransactionLogging.view' import { UserContactLoggingView } from './UserContactLogging.view' import { UserLoggingView } from './UserLogging.view' import { UserRoleLoggingView } from './UserRoleLogging.view' +import { CommunityHandshakeStateLoggingView } from './CommunityHandshakeStateLogging.view' export { AbstractLoggingView, @@ -24,6 +25,7 @@ export { UserContactLoggingView, UserLoggingView, UserRoleLoggingView, + CommunityHandshakeStateLoggingView, } export const logger = getLogger(LOG4JS_BASE_CATEGORY_NAME) diff --git a/database/src/queries/communities.test.ts b/database/src/queries/communities.test.ts index 18975256c..2d208a224 100644 --- a/database/src/queries/communities.test.ts +++ b/database/src/queries/communities.test.ts @@ -1,6 +1,6 @@ import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from '..' import { AppDatabase } from '../AppDatabase' -import { getHomeCommunity, getReachableCommunities } from './communities' +import { getHomeCommunity, getHomeCommunityWithFederatedCommunityOrFail, getReachableCommunities } from './communities' import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest' import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community' @@ -39,6 +39,24 @@ describe('community.queries', () => { expect(community?.privateKey).toStrictEqual(homeCom.privateKey) }) }) + describe('getHomeCommunityWithFederatedCommunityOrFail', () => { + it('should return the home community with federated communities', async () => { + const homeCom = await createCommunity(false) + await createVerifiedFederatedCommunity('1_0', 100, homeCom) + const community = await getHomeCommunityWithFederatedCommunityOrFail('1_0') + expect(community).toBeDefined() + expect(community?.federatedCommunities).toHaveLength(1) + }) + + it('should throw if no home community exists', async () => { + expect(() => getHomeCommunityWithFederatedCommunityOrFail('1_0')).rejects.toThrow() + }) + + it('should throw if no federated community exists', async () => { + await createCommunity(false) + expect(() => getHomeCommunityWithFederatedCommunityOrFail('1_0')).rejects.toThrow() + }) + }) describe('getReachableCommunities', () => { it('home community counts also to reachable communities', async () => { await createCommunity(false) diff --git a/database/src/queries/communities.ts b/database/src/queries/communities.ts index cdb7cabb6..265be41d4 100644 --- a/database/src/queries/communities.ts +++ b/database/src/queries/communities.ts @@ -10,7 +10,14 @@ export async function getHomeCommunity(): Promise { // TODO: Put in Cache, it is needed nearly always // TODO: return only DbCommunity or throw to reduce unnecessary checks, because there should be always a home community return await DbCommunity.findOne({ - where: { foreign: false }, + where: { foreign: false } + }) +} + +export async function getHomeCommunityWithFederatedCommunityOrFail(apiVersion: string): Promise { + return await DbCommunity.findOneOrFail({ + where: { foreign: false, federatedCommunities: { apiVersion } }, + relations: { federatedCommunities: true }, }) } diff --git a/database/src/queries/communityHandshakes.ts b/database/src/queries/communityHandshakes.ts new file mode 100644 index 000000000..991bd9c47 --- /dev/null +++ b/database/src/queries/communityHandshakes.ts @@ -0,0 +1,15 @@ +import { CommunityHandshakeState } from '../entity' +import { FederatedCommunity } from '../entity/FederatedCommunity' + +/** + * Find a pending community handshake by public key. + * @param publicKey The public key of the community. + * @param withRelations Whether to include the federated community and community in the result, default true. + * @returns The CommunityHandshakeState with associated federated community and community. + */ +export function findPendingCommunityHandshake(federatedCommunity: FederatedCommunity, withRelations = true): Promise { + return CommunityHandshakeState.findOne({ + where: { publicKey: federatedCommunity.publicKey, apiVersion: federatedCommunity.apiVersion }, + relations: withRelations ? { federatedCommunity: { community: true } } : undefined, + }) +} \ No newline at end of file diff --git a/database/src/queries/index.ts b/database/src/queries/index.ts index 1fec568bf..73a2cc15b 100644 --- a/database/src/queries/index.ts +++ b/database/src/queries/index.ts @@ -5,5 +5,6 @@ export * from './communities' export * from './pendingTransactions' export * from './transactions' export * from './transactionLinks' +export * from './communityHandshakes' export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries` diff --git a/database/src/seeds/community.ts b/database/src/seeds/community.ts index 4db872398..12a5bd67f 100644 --- a/database/src/seeds/community.ts +++ b/database/src/seeds/community.ts @@ -2,7 +2,13 @@ import { Community, FederatedCommunity } from '../entity' import { randomBytes } from 'node:crypto' import { v4 as uuidv4 } from 'uuid' -export async function createCommunity(foreign: boolean, save: boolean = true): Promise { +/** + * Creates a community. + * @param foreign + * @param store if true, write to db, default: true + * @returns + */ +export async function createCommunity(foreign: boolean, store: boolean = true): Promise { const community = new Community() community.publicKey = randomBytes(32) community.communityUuid = uuidv4() @@ -23,14 +29,22 @@ export async function createCommunity(foreign: boolean, save: boolean = true): P community.description = 'HomeCommunity-description' community.url = 'http://localhost/api' } - return save ? await community.save() : community + return store ? await community.save() : community } +/** + * Creates a verified federated community. + * @param apiVersion + * @param verifiedBeforeMs time in ms before the current time + * @param community + * @param store if true, write to db, default: true + * @returns + */ export async function createVerifiedFederatedCommunity( apiVersion: string, verifiedBeforeMs: number, community: Community, - save: boolean = true + store: boolean = true ): Promise { const federatedCommunity = new FederatedCommunity() federatedCommunity.apiVersion = apiVersion @@ -38,5 +52,5 @@ export async function createVerifiedFederatedCommunity( federatedCommunity.publicKey = community.publicKey federatedCommunity.community = community federatedCommunity.verifiedAt = new Date(Date.now() - verifiedBeforeMs) - return save ? await federatedCommunity.save() : federatedCommunity + return store ? await federatedCommunity.save() : federatedCommunity }