Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
[![website](https://img.shields.io/badge/web-opencorejs.dev-black?style=flat-square)](https://opencorejs.dev)


# OpenCore Framework - Open Stable beta
# OpenCore Framework - Stable v1

OpenCore is a TypeScript multiplayer runtime framework targeting CitizenFX runtimes (Cfx) via adapters.
OpenCore is a TypeScript multiplayer runtime framework targeting CitizenFX runtimes (Cfx/RageMP) via adapters.

It is not a gamemode or RP framework. It provides:

Expand Down Expand Up @@ -277,12 +277,6 @@ pnpm lint:fix
pnpm format
```

## Ecosystem

OpenCore is designed to be extended via separate packages/resources.

- `@open-core/identity`: identity and permission system

## License

MPL-2.0. See `LICENSE`.
26 changes: 12 additions & 14 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
## OpenCore Framework v1.0.5
## OpenCore Framework v1.0.6

### Added
- Added new client adapter ports for camera, ped, vehicle, progress, spawn, local player, runtime bridge, and WebView integration, with matching node runtime implementations.
- Added support for WebView chat mode, richer client UI/runtime abstractions, and cleaner adapter-facing contracts/exports.
- Added server-side improvements for command handling, including command validation, default function parameter support, and standardized system event names.
- Added more coverage around parallel compute, vehicle modification, vehicle sync state, player state sync, adapters, and command execution flows.
- Added Husky pre-commit and pre-push hooks for local quality checks.
- Added `RpcPublicError` and `serializeRpcError()` for safe RPC error exposure.
- Added `PUBLIC_RPC_ERROR_MESSAGE` as the default public message for unexpected RPC failures.
- Added transport exports for RPC error helpers through `src/adapters/contracts/transport`.
- Added unit and integration coverage for RPC error serialization and server RPC flow logging.

### Changed
- Refactored client services to rely on explicit adapter ports instead of direct runtime assumptions, especially for camera, ped, progress, spawn, and vehicle flows.
- Refactored logging so logger writes use string log levels and runtime log domain labels are derived dynamically from the active resource.
- Refactored worker execution to use inline worker scripts with performance tracking in the parallel compute pipeline.
- Updated package/tooling setup to TypeScript 6 and refreshed package exports, scripts, and dependency configuration.
- Updated server RPC processing to log handler failures with event, handler, player, and account context.
- Updated RPC handling to preserve explicit public errors while masking unexpected internal errors.
- Refined the RPC path so invalid payloads and session issues are logged with clearer warnings.

### Fixed
- Fixed command schema handling so exported/remote commands support default parameters more reliably.
- Fixed transport/event contract alignment across node events and RPC layers.
- Fixed several test, lint, and export consistency issues while expanding automated coverage.
- Fixed RPC error leakage by sanitizing unexpected exceptions before they are returned to the client.
- Fixed RPC logger behavior so exposed errors can pass through with their original message and name.
- Fixed contract alignment across transport, server RPC processing, and test coverage.

### Notes
- This release covers the full `master...v1` delta and keeps the release notes compact by grouping related adapter/runtime refactors instead of listing each port separately.
- This release tracks the `fix/rpc-logger` merge request (#51) and keeps the release note focused on the RPC error-handling changes.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@open-core/framework",
"version": "1.0.5",
"version": "1.0.6",
"description": "Secure, event-driven TypeScript Framework & Runtime engine for CitizenFX (Cfx).",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions src/adapters/contracts/transport/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './context'
export * from './events.api'
export * from './rpc.api'
export * from './rpc-error'
export * from './messaging.transport'
46 changes: 46 additions & 0 deletions src/adapters/contracts/transport/rpc-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export const PUBLIC_RPC_ERROR_MESSAGE = 'An internal server error occurred'

export type RpcErrorInfo = {
message: string
name?: string
}

type ExposedRpcError = {
message: string
name?: string
expose: true
}

export class RpcPublicError extends Error {
readonly expose = true

constructor(message: string, name?: string) {
super(message)
if (name) {
this.name = name
}
Object.setPrototypeOf(this, new.target.prototype)
}
}

export function isExposedRpcError(error: unknown): error is ExposedRpcError {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof error.message === 'string' &&
'expose' in error &&
error.expose === true
)
}

export function serializeRpcError(error: unknown): RpcErrorInfo {
if (isExposedRpcError(error)) {
return {
message: error.message,
name: 'name' in error && typeof error.name === 'string' ? error.name : undefined,
}
}

return { message: PUBLIC_RPC_ERROR_MESSAGE }
}
22 changes: 18 additions & 4 deletions src/runtime/server/system/processors/onRpc.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,25 @@ export class OnRpcProcessor implements DecoratorProcessor {
throw error
}

if (hasNoDeclaredParams) {
return handler()
}
try {
if (hasNoDeclaredParams) {
return await handler()
}

return handler(player, ...validatedArgs)
return await handler(player, ...validatedArgs)
} catch (error) {
loggers.netEvent.error(
`Handler error in RPC`,
{
event: metadata.eventName,
handler: handlerName,
playerId: player.clientID,
accountId: player.accountID,
},
error as Error,
)
throw error
}
})

loggers.netEvent.debug(`Registered RPC: ${metadata.eventName} -> ${handlerName}`)
Expand Down
71 changes: 71 additions & 0 deletions tests/integration/server/rpc-flow.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'reflect-metadata'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { z } from 'zod'
import { loggers } from '../../../src/kernel/logger'
import { RpcPublicError } from '../../../src/adapters/contracts/transport/rpc-error'
import { NodeRpc } from '../../../src/adapters/node/transport/node.rpc'
import type { RpcHandlerOptions } from '../../../src/runtime/server/decorators/onRPC'
import { Public } from '../../../src/runtime/server/decorators/public'
Expand Down Expand Up @@ -59,6 +61,7 @@ describe('OnRpcProcessor – Server RPC Flow', () => {
} as any

processor = new OnRpcProcessor(mockPlayerService, rpc)
vi.spyOn(loggers.netEvent, 'error').mockImplementation(() => undefined)
})

afterEach(() => {
Expand Down Expand Up @@ -270,6 +273,74 @@ describe('OnRpcProcessor – Server RPC Flow', () => {
})
})

describe('Handler errors', () => {
it('should log and rethrow unexpected handler errors', async () => {
class TestController {
async handle(_player: Player) {
throw new Error('db connection string: secret')
}
}

const instance = new TestController()
const metadata = buildMeta('rpc:error:unexpected', z.tuple([]), [Player])

const mockPlayer = createMockPlayer({ clientID: 7, accountID: 'acc-7' })
vi.mocked(mockPlayerService.getByClient).mockReturnValue(mockPlayer)

processor.process(instance, 'handle', metadata)

const handler = (rpc as any).handlers.get('rpc:error:unexpected')

await expect(handler({ requestId: 'req-1', clientId: 7 })).rejects.toThrow(
'db connection string: secret',
)

expect(loggers.netEvent.error).toHaveBeenCalledWith(
'Handler error in RPC',
expect.objectContaining({
event: 'rpc:error:unexpected',
handler: 'TestController.handle',
playerId: 7,
accountId: 'acc-7',
}),
expect.any(Error),
)
})

it('should log and rethrow public handler errors', async () => {
class TestController {
async handle(_player: Player) {
throw new RpcPublicError('Character not found', 'CharacterLookupError')
}
}

const instance = new TestController()
const metadata = buildMeta('rpc:error:public', z.tuple([]), [Player])

const mockPlayer = createMockPlayer({ clientID: 8, accountID: 'acc-8' })
vi.mocked(mockPlayerService.getByClient).mockReturnValue(mockPlayer)

processor.process(instance, 'handle', metadata)

const handler = (rpc as any).handlers.get('rpc:error:public')

await expect(handler({ requestId: 'req-2', clientId: 8 })).rejects.toThrow(
'Character not found',
)

expect(loggers.netEvent.error).toHaveBeenCalledWith(
'Handler error in RPC',
expect.objectContaining({
event: 'rpc:error:public',
handler: 'TestController.handle',
playerId: 8,
accountId: 'acc-8',
}),
expect.any(Error),
)
})
})

// ═══════════════════════════════════════════════════════════════════════════
// Player resolution
// ═══════════════════════════════════════════════════════════════════════════
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/transport/rpc-error.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'reflect-metadata'
import { describe, expect, it } from 'vitest'
import {
PUBLIC_RPC_ERROR_MESSAGE,
RpcPublicError,
serializeRpcError,
} from '../../../src/adapters/contracts/transport/rpc-error'

describe('serializeRpcError', () => {
it('should sanitize unexpected Error instances', () => {
expect(serializeRpcError(new Error('database credentials leaked'))).toEqual({
message: PUBLIC_RPC_ERROR_MESSAGE,
})
})

it('should preserve message and name for RpcPublicError', () => {
const error = new RpcPublicError('Character not found', 'CharacterLookupError')

expect(serializeRpcError(error)).toEqual({
message: 'Character not found',
name: 'CharacterLookupError',
})
})

it('should preserve message for exposed structural errors', () => {
expect(
serializeRpcError({
message: 'Inventory full',
name: 'InventoryError',
expose: true,
}),
).toEqual({
message: 'Inventory full',
name: 'InventoryError',
})
})

it('should sanitize non-Error values', () => {
expect(serializeRpcError('raw failure')).toEqual({
message: PUBLIC_RPC_ERROR_MESSAGE,
})
})
})
Loading