diff --git a/RELEASE.md b/RELEASE.md index 232352c..e240632 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,10 +1,9 @@ -## OpenCore Framework v1.0.8 +## OpenCore Framework v1.0.10 ### Added -- Added framework event bridging from `CORE` to `RESOURCE` for `@OnFrameworkEvent` listeners. - -### Changed -- Updated `@OnRuntimeEvent` and `@OnFrameworkEvent` documentation to clarify handler arguments and cross-context behavior. +- Added authoritative CORE exports to link and unlink player accounts from remote resources. +- Added debug logs for remote player session mutations delegated from `RESOURCE` to `CORE`. ### Fixed -- Fixed `@OnFrameworkEvent` delivery so built-in framework lifecycle events can reach `RESOURCE` listeners with hydrated payloads. \ No newline at end of file +- Fixed `RESOURCE` player session mutations so `player.linkAccount()`, `player.unlinkAccount()`, `player.setMeta()` and state changes propagate to CORE. +- Fixed secure `@OnNet` and `@OnRPC` handlers being blocked in sibling resources after successful authentication performed from a `RESOURCE` auth module. \ No newline at end of file diff --git a/package.json b/package.json index a43f385..df8bd4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@open-core/framework", - "version": "1.0.9", + "version": "1.0.10", "description": "Secure, event-driven TypeScript Framework & Runtime engine for CitizenFX (Cfx).", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/runtime/server/controllers/player-export.controller.ts b/src/runtime/server/controllers/player-export.controller.ts index d409c8e..564cc9e 100644 --- a/src/runtime/server/controllers/player-export.controller.ts +++ b/src/runtime/server/controllers/player-export.controller.ts @@ -1,4 +1,5 @@ import { inject } from 'tsyringe' +import { loggers } from '../../../kernel/logger' import { Controller } from '../decorators/controller' import { Export } from '../decorators/export' import { serializeServerPlayerData } from '../adapter/serialization' @@ -77,6 +78,31 @@ export class PlayerExportController implements InternalPlayerExports { this.playerService.setMeta(clientID, key, value) } + @Export() + linkPlayerAccount(clientID: number, accountID: string): void { + const player = this.playerService.getByClient(clientID) + if (!player) return + + player.linkAccount(accountID) + loggers.session.debug('Remote player account linked in CORE', { + clientID, + accountID, + }) + } + + @Export() + unlinkPlayerAccount(clientID: number): void { + const player = this.playerService.getByClient(clientID) + if (!player) return + + const previousAccountID = player.accountID + player.unlinkAccount() + loggers.session.debug('Remote player account unlinked in CORE', { + clientID, + accountID: previousAccountID, + }) + } + // ═══════════════════════════════════════════════════════════════ // State Management // ═══════════════════════════════════════════════════════════════ diff --git a/src/runtime/server/implementations/remote/player.remote.ts b/src/runtime/server/implementations/remote/player.remote.ts index 2ca47fa..5975070 100644 --- a/src/runtime/server/implementations/remote/player.remote.ts +++ b/src/runtime/server/implementations/remote/player.remote.ts @@ -74,7 +74,80 @@ export class RemotePlayerImplementation extends Players { } private createPlayerFromData(data: SerializedPlayerData): Player { - return createRemoteServerPlayer(data, this.playerAdapters) + const player = createRemoteServerPlayer(data, this.playerAdapters) + this.attachAuthoritativeMutators(player) + return player + } + + /** + * Proxies remote session mutations to CORE so security-critical data remains authoritative. + */ + private attachAuthoritativeMutators(player: Player): void { + const core = this.core + const originalSetMeta = player.setMeta.bind(player) + const originalLinkAccount = player.linkAccount.bind(player) + const originalUnlinkAccount = player.unlinkAccount.bind(player) + const originalAddState = player.addState.bind(player) + const originalRemoveState = player.removeState.bind(player) + const originalToggleState = player.toggleState.bind(player) + + player.setMeta = (key: string, value: T): void => { + core.setPlayerMeta(player.clientID, key, value) + originalSetMeta(key, value) + loggers.session.debug('Remote player meta delegated to CORE', { + clientID: player.clientID, + key, + }) + } + + player.linkAccount = (accountID): void => { + core.linkPlayerAccount(player.clientID, accountID.toString()) + originalLinkAccount(accountID) + loggers.session.debug('Remote player linkAccount delegated to CORE', { + clientID: player.clientID, + accountID: accountID.toString(), + }) + } + + player.unlinkAccount = (): void => { + const previousAccountID = player.accountID + core.unlinkPlayerAccount(player.clientID) + originalUnlinkAccount() + loggers.session.debug('Remote player unlinkAccount delegated to CORE', { + clientID: player.clientID, + accountID: previousAccountID, + }) + } + + player.addState = (state: string): void => { + core.addPlayerState(player.clientID, state) + originalAddState(state) + loggers.session.debug('Remote player state added in CORE', { + clientID: player.clientID, + state, + }) + } + + player.removeState = (state: string): void => { + core.removePlayerState(player.clientID, state) + originalRemoveState(state) + loggers.session.debug('Remote player state removed in CORE', { + clientID: player.clientID, + state, + }) + } + + player.toggleState = (state: string, force?: boolean): boolean => { + const next = force ?? !player.hasState(state) + if (next) { + player.addState(state) + } else { + player.removeState(state) + } + + originalToggleState(state, next) + return next + } } /** diff --git a/src/runtime/server/types/core-exports.types.ts b/src/runtime/server/types/core-exports.types.ts index aee52a1..01e0623 100644 --- a/src/runtime/server/types/core-exports.types.ts +++ b/src/runtime/server/types/core-exports.types.ts @@ -270,6 +270,21 @@ export interface InternalPlayerExports { */ setPlayerMeta(clientID: number, key: string, value: unknown): void + /** + * Links a persistent account to the authoritative CORE player session. + * + * @param clientID - FiveM client/server ID + * @param accountID - Persistent account identifier + */ + linkPlayerAccount(clientID: number, accountID: string): void + + /** + * Removes any linked account from the authoritative CORE player session. + * + * @param clientID - FiveM client/server ID + */ + unlinkPlayerAccount(clientID: number): void + /** * Gets complete serialized player data. *