@open-core/framework 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -141,6 +141,44 @@ export class ExampleNetController {
141
141
  - `@Throttle(limit, windowMs)`
142
142
  - `@RequiresState({ missing: [...] })`
143
143
 
144
+ ### Exports
145
+
146
+ `@Export()` defines a public resource API. Adapters may expose both direct/local access through `getResource()` and an optional explicit async helper layer through `getRemoteResource()` / `waitForRemoteResource()`.
147
+
148
+ ```ts
149
+ import { Controller, Export } from '@open-core/framework/server'
150
+ import { IExports } from '@open-core/framework/contracts/server'
151
+
152
+ @Controller()
153
+ export class DatabaseController {
154
+ @Export('pingDatabase')
155
+ async pingDatabase() {
156
+ return { success: true }
157
+ }
158
+ }
159
+
160
+ interface DatabaseExports {
161
+ pingDatabase(): Promise<{ success: boolean }>
162
+ }
163
+
164
+ class ExampleConsumer {
165
+ constructor(private readonly exportsService: IExports) {}
166
+
167
+ async ping() {
168
+ const database = await this.exportsService.waitForRemoteResource<DatabaseExports>('database', {
169
+ exportName: 'pingDatabase',
170
+ })
171
+
172
+ return database.pingDatabase()
173
+ }
174
+ }
175
+ ```
176
+
177
+ Guidance:
178
+
179
+ - `getResource()` is for local/synchronous resolution used by framework internals.
180
+ - `waitForRemoteResource()` / `getRemoteResource()` are optional adapter utilities for explicit async resource-to-resource calls.
181
+
144
182
  ### Library events
145
183
 
146
184
  Use library wrappers to emit domain events and `@OnLibraryEvent()` to observe them.
@@ -1,4 +1,42 @@
1
1
  export declare abstract class IExports {
2
+ /**
3
+ * Registers a local export handler for the current resource.
4
+ *
5
+ * @remarks
6
+ * This is called by the framework during metadata processing when it discovers
7
+ * methods decorated with `@Export()`.
8
+ */
2
9
  abstract register(exportName: string, handler: (...args: unknown[]) => unknown): void;
10
+ /**
11
+ * Resolves exports for a resource using the adapter's direct/local mechanism.
12
+ *
13
+ * @remarks
14
+ * Framework internals rely on this method remaining synchronous and side-effect free.
15
+ * Adapters should return `undefined` when the resource is not directly resolvable.
16
+ */
3
17
  abstract getResource<T = unknown>(resourceName: string): T | undefined;
18
+ /**
19
+ * Returns an async proxy for resource exports when the adapter provides a remote helper layer.
20
+ *
21
+ * @remarks
22
+ * This is optional and should not change the semantics of `getResource()`.
23
+ * Consumers should treat methods on the returned proxy as async.
24
+ */
25
+ getRemoteResource<T = unknown>(_resourceName: string): T;
26
+ /**
27
+ * Calls a single exported method through the adapter's optional remote helper layer.
28
+ */
29
+ callRemoteExport<TResult = unknown>(_resourceName: string, _exportName: string, ..._args: unknown[]): Promise<TResult>;
30
+ /**
31
+ * Waits until a resource exposes exports compatible with the adapter's remote helper layer.
32
+ *
33
+ * @param _options.exportName Optional export name that must be present before resolving.
34
+ * @param _options.timeoutMs Maximum time to wait before failing.
35
+ * @param _options.intervalMs Polling interval used by adapters that implement polling.
36
+ */
37
+ waitForRemoteResource<T = unknown>(_resourceName: string, _options?: {
38
+ exportName?: string;
39
+ timeoutMs?: number;
40
+ intervalMs?: number;
41
+ }): Promise<T>;
4
42
  }
@@ -1,2 +1,28 @@
1
1
  export class IExports {
2
+ /**
3
+ * Returns an async proxy for resource exports when the adapter provides a remote helper layer.
4
+ *
5
+ * @remarks
6
+ * This is optional and should not change the semantics of `getResource()`.
7
+ * Consumers should treat methods on the returned proxy as async.
8
+ */
9
+ getRemoteResource(_resourceName) {
10
+ throw new Error('[OpenCore] Remote exports are not supported by the active adapter.');
11
+ }
12
+ /**
13
+ * Calls a single exported method through the adapter's optional remote helper layer.
14
+ */
15
+ callRemoteExport(_resourceName, _exportName, ..._args) {
16
+ return Promise.reject(new Error('[OpenCore] Remote exports are not supported by the active adapter.'));
17
+ }
18
+ /**
19
+ * Waits until a resource exposes exports compatible with the adapter's remote helper layer.
20
+ *
21
+ * @param _options.exportName Optional export name that must be present before resolving.
22
+ * @param _options.timeoutMs Maximum time to wait before failing.
23
+ * @param _options.intervalMs Polling interval used by adapters that implement polling.
24
+ */
25
+ waitForRemoteResource(_resourceName, _options) {
26
+ return Promise.reject(new Error('[OpenCore] Remote exports are not supported by the active adapter.'));
27
+ }
2
28
  }
@@ -7,6 +7,13 @@ export declare class NodeExports implements IExports {
7
7
  private exports;
8
8
  register(exportName: string, handler: (...args: any[]) => any): void;
9
9
  getResource(resourceName: string): any;
10
+ getRemoteResource<T = unknown>(_resourceName: string): T;
11
+ callRemoteExport<TResult = unknown>(_resourceName: string, _exportName: string, ..._args: unknown[]): Promise<TResult>;
12
+ waitForRemoteResource<T = unknown>(_resourceName: string, _options?: {
13
+ exportName?: string;
14
+ timeoutMs?: number;
15
+ intervalMs?: number;
16
+ }): Promise<T>;
10
17
  /**
11
18
  * Get all registered exports as an object
12
19
  */
@@ -23,6 +23,15 @@ let NodeExports = class NodeExports {
23
23
  throw new Error(`Cross-resource exports not supported in Node.js runtime. ` +
24
24
  `Attempted to access resource: ${resourceName}`);
25
25
  }
26
+ getRemoteResource(_resourceName) {
27
+ throw new Error('[OpenCore] Remote exports are not supported in Node.js runtime.');
28
+ }
29
+ callRemoteExport(_resourceName, _exportName, ..._args) {
30
+ return Promise.reject(new Error('[OpenCore] Remote exports are not supported in Node.js runtime.'));
31
+ }
32
+ waitForRemoteResource(_resourceName, _options) {
33
+ return Promise.reject(new Error('[OpenCore] Remote exports are not supported in Node.js runtime.'));
34
+ }
26
35
  /**
27
36
  * Get all registered exports as an object
28
37
  */
@@ -278,13 +278,32 @@ async function tryImportAutoLoad() {
278
278
  await import('./.opencore/autoload.server.controllers');
279
279
  }
280
280
  catch (err) {
281
- if (err instanceof Error && err.message.includes('Cannot find module')) {
281
+ if (isAutoloadModuleNotFound(err)) {
282
282
  loggers.bootstrap.warn(`[Bootstrap] No server controllers autoload file found, skipping.`);
283
283
  return;
284
284
  }
285
+ const message = err instanceof Error ? err.message : String(err);
286
+ loggers.bootstrap.error(`[Bootstrap] Failed to import server controllers autoload file.`, {
287
+ error: message,
288
+ });
285
289
  throw err;
286
290
  }
287
291
  }
292
+ function isAutoloadModuleNotFound(err) {
293
+ if (!err || typeof err !== 'object') {
294
+ return false;
295
+ }
296
+ const error = err;
297
+ const message = typeof error.message === 'string' ? error.message : '';
298
+ const requireStack = Array.isArray(error.requireStack) ? error.requireStack : [];
299
+ if (error.code !== 'MODULE_NOT_FOUND' && !message.includes('Cannot find module')) {
300
+ return false;
301
+ }
302
+ if (message.includes('autoload.server.controllers')) {
303
+ return true;
304
+ }
305
+ return requireStack.some((entry) => entry.includes('autoload.server.controllers'));
306
+ }
288
307
  /**
289
308
  * Runs session recovery to restore sessions for players already connected.
290
309
  *
@@ -23,6 +23,8 @@ export declare class PlayerExportController implements InternalPlayerExports {
23
23
  isPlayerOnline(accountId: string): boolean;
24
24
  getPlayerMeta(clientID: number, key: string): Promise<any>;
25
25
  setPlayerMeta(clientID: number, key: string, value: any): void;
26
+ linkPlayerAccount(clientID: number, accountID: string): void;
27
+ unlinkPlayerAccount(clientID: number): void;
26
28
  getPlayerStates(clientID: number): string[];
27
29
  hasPlayerState(clientID: number, state: string): boolean;
28
30
  addPlayerState(clientID: number, state: string): void;
@@ -11,6 +11,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
11
  return function (target, key) { decorator(target, key, paramIndex); }
12
12
  };
13
13
  import { inject } from 'tsyringe';
14
+ import { loggers } from '../../../kernel/logger';
14
15
  import { Controller } from '../decorators/controller';
15
16
  import { Export } from '../decorators/export';
16
17
  import { serializeServerPlayerData } from '../adapter/serialization';
@@ -67,6 +68,27 @@ let PlayerExportController = class PlayerExportController {
67
68
  setPlayerMeta(clientID, key, value) {
68
69
  this.playerService.setMeta(clientID, key, value);
69
70
  }
71
+ linkPlayerAccount(clientID, accountID) {
72
+ const player = this.playerService.getByClient(clientID);
73
+ if (!player)
74
+ return;
75
+ player.linkAccount(accountID);
76
+ loggers.session.debug('Remote player account linked in CORE', {
77
+ clientID,
78
+ accountID,
79
+ });
80
+ }
81
+ unlinkPlayerAccount(clientID) {
82
+ const player = this.playerService.getByClient(clientID);
83
+ if (!player)
84
+ return;
85
+ const previousAccountID = player.accountID;
86
+ player.unlinkAccount();
87
+ loggers.session.debug('Remote player account unlinked in CORE', {
88
+ clientID,
89
+ accountID: previousAccountID,
90
+ });
91
+ }
70
92
  // ═══════════════════════════════════════════════════════════════
71
93
  // State Management
72
94
  // ═══════════════════════════════════════════════════════════════
@@ -141,6 +163,18 @@ __decorate([
141
163
  __metadata("design:paramtypes", [Number, String, Object]),
142
164
  __metadata("design:returntype", void 0)
143
165
  ], PlayerExportController.prototype, "setPlayerMeta", null);
166
+ __decorate([
167
+ Export(),
168
+ __metadata("design:type", Function),
169
+ __metadata("design:paramtypes", [Number, String]),
170
+ __metadata("design:returntype", void 0)
171
+ ], PlayerExportController.prototype, "linkPlayerAccount", null);
172
+ __decorate([
173
+ Export(),
174
+ __metadata("design:type", Function),
175
+ __metadata("design:paramtypes", [Number]),
176
+ __metadata("design:returntype", void 0)
177
+ ], PlayerExportController.prototype, "unlinkPlayerAccount", null);
144
178
  __decorate([
145
179
  Export(),
146
180
  __metadata("design:type", Function),
@@ -35,6 +35,10 @@ export declare class RemotePlayerImplementation extends Players {
35
35
  */
36
36
  private get core();
37
37
  private createPlayerFromData;
38
+ /**
39
+ * Proxies remote session mutations to CORE so security-critical data remains authoritative.
40
+ */
41
+ private attachAuthoritativeMutators;
38
42
  /**
39
43
  * Returns a Player instance with real session data from CORE.
40
44
  */
@@ -77,7 +77,73 @@ let RemotePlayerImplementation = class RemotePlayerImplementation extends Player
77
77
  return coreExports;
78
78
  }
79
79
  createPlayerFromData(data) {
80
- return createRemoteServerPlayer(data, this.playerAdapters);
80
+ const player = createRemoteServerPlayer(data, this.playerAdapters);
81
+ this.attachAuthoritativeMutators(player);
82
+ return player;
83
+ }
84
+ /**
85
+ * Proxies remote session mutations to CORE so security-critical data remains authoritative.
86
+ */
87
+ attachAuthoritativeMutators(player) {
88
+ const core = this.core;
89
+ const originalSetMeta = player.setMeta.bind(player);
90
+ const originalLinkAccount = player.linkAccount.bind(player);
91
+ const originalUnlinkAccount = player.unlinkAccount.bind(player);
92
+ const originalAddState = player.addState.bind(player);
93
+ const originalRemoveState = player.removeState.bind(player);
94
+ const originalToggleState = player.toggleState.bind(player);
95
+ player.setMeta = (key, value) => {
96
+ core.setPlayerMeta(player.clientID, key, value);
97
+ originalSetMeta(key, value);
98
+ loggers.session.debug('Remote player meta delegated to CORE', {
99
+ clientID: player.clientID,
100
+ key,
101
+ });
102
+ };
103
+ player.linkAccount = (accountID) => {
104
+ core.linkPlayerAccount(player.clientID, accountID.toString());
105
+ originalLinkAccount(accountID);
106
+ loggers.session.debug('Remote player linkAccount delegated to CORE', {
107
+ clientID: player.clientID,
108
+ accountID: accountID.toString(),
109
+ });
110
+ };
111
+ player.unlinkAccount = () => {
112
+ const previousAccountID = player.accountID;
113
+ core.unlinkPlayerAccount(player.clientID);
114
+ originalUnlinkAccount();
115
+ loggers.session.debug('Remote player unlinkAccount delegated to CORE', {
116
+ clientID: player.clientID,
117
+ accountID: previousAccountID,
118
+ });
119
+ };
120
+ player.addState = (state) => {
121
+ core.addPlayerState(player.clientID, state);
122
+ originalAddState(state);
123
+ loggers.session.debug('Remote player state added in CORE', {
124
+ clientID: player.clientID,
125
+ state,
126
+ });
127
+ };
128
+ player.removeState = (state) => {
129
+ core.removePlayerState(player.clientID, state);
130
+ originalRemoveState(state);
131
+ loggers.session.debug('Remote player state removed in CORE', {
132
+ clientID: player.clientID,
133
+ state,
134
+ });
135
+ };
136
+ player.toggleState = (state, force) => {
137
+ const next = force ?? !player.hasState(state);
138
+ if (next) {
139
+ player.addState(state);
140
+ }
141
+ else {
142
+ player.removeState(state);
143
+ }
144
+ originalToggleState(state, next);
145
+ return next;
146
+ };
81
147
  }
82
148
  /**
83
149
  * Returns a Player instance with real session data from CORE.
@@ -225,6 +225,19 @@ export interface InternalPlayerExports {
225
225
  * @param value - Value to store
226
226
  */
227
227
  setPlayerMeta(clientID: number, key: string, value: unknown): void;
228
+ /**
229
+ * Links a persistent account to the authoritative CORE player session.
230
+ *
231
+ * @param clientID - FiveM client/server ID
232
+ * @param accountID - Persistent account identifier
233
+ */
234
+ linkPlayerAccount(clientID: number, accountID: string): void;
235
+ /**
236
+ * Removes any linked account from the authoritative CORE player session.
237
+ *
238
+ * @param clientID - FiveM client/server ID
239
+ */
240
+ unlinkPlayerAccount(clientID: number): void;
228
241
  /**
229
242
  * Gets complete serialized player data.
230
243
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-core/framework",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "Secure, event-driven TypeScript Framework & Runtime engine for CitizenFX (Cfx).",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",