feat(session): resolved permission map snapshot (USER_PERMISSIONS_UPDATED)

Adds the wire pipeline for `Outgoing.UserPermissionsMapComposer = 10070`
shipped by Arcturus-Morningstar-Extended ≥ 4.2.10. The composer sends
the resolved `permission_definitions` map for the current user
(filtered to ALLOWED / ROOM_OWNER entries) at login and after every
`HabboManager.setRank` — so a runtime promote/demote re-derives every
React-side permission gate.

- NitroEventType.USER_PERMISSIONS_UPDATED — new invalidation event.
- IncomingHeader.USER_PERMISSIONS_MAP = 10070.
- UserPermissionsMapParser reads `int count + (string key, int value)*`.
- UserPermissionsMapEvent + NitroMessages registration.
- SessionDataManager._permissions Map + getPermissionsSnapshot()
  referentially-stable per the snapshot convention. New handler
  onUserPermissionsMapEvent copies the parser map into the manager
  (so the parser's mutable reference doesn't leak) and invalidates.
- ISessionDataManager.getPermissionsSnapshot() — public contract.

React-side consumers ship in the companion commit on
feat/react19-modernization. The wire is backward-compatible: older
emulators never send the packet, the snapshot stays empty Map, and
all useHasPermission(key) gates return false (mod-only UI hidden by
default = safe).

Verification: tsgo clean, vitest 138/138.
This commit is contained in:
simoleo89
2026-05-19 18:59:35 +02:00
parent 87e67d58df
commit 159c5eb6e8
9 changed files with 130 additions and 2 deletions
File diff suppressed because one or more lines are too long
@@ -255,6 +255,8 @@ export class IncomingHeader
public static USER_OUTFITS = 3315;
public static USER_PERKS = 2586;
public static USER_PERMISSIONS = 411;
// Resolved permission map (Arcturus extension, Outgoing.UserPermissionsMapComposer = 10070).
public static USER_PERMISSIONS_MAP = 10070;
public static USER_PET_ADD = 2101;
public static USER_PET_REMOVE = 3253;
public static USER_PETS = 3522;
@@ -0,0 +1,16 @@
import { IMessageEvent } from '@nitrots/api';
import { MessageEvent } from '@nitrots/events';
import { UserPermissionsMapParser } from '../../../parser';
export class UserPermissionsMapEvent extends MessageEvent implements IMessageEvent
{
constructor(callBack: Function)
{
super(callBack, UserPermissionsMapParser);
}
public getParser(): UserPermissionsMapParser
{
return this.parser as UserPermissionsMapParser;
}
}
@@ -1 +1,2 @@
export * from './UserPermissionsEvent';
export * from './UserPermissionsMapEvent';
@@ -0,0 +1,54 @@
import { IMessageDataWrapper, IMessageParser } from '@nitrots/api';
/**
* Parses the resolved permission map for the current user
* (Arcturus-Morningstar-Extended ≥ 4.2.10, header
* `Outgoing.UserPermissionsMapComposer = 10070`).
*
* Wire layout:
* int count
* loop: string permission_key + int value (1 = ALLOWED, 2 = ROOM_OWNER)
*
* Only permissions whose `PermissionSetting != DISALLOWED` are sent —
* absence means "no". The renderer-side `SessionDataManager` consumes
* this and exposes it via a snapshot getter; React-side
* `useHasPermission(key)` drives UI gates against the real
* `permission_definitions.permission_key` strings instead of
* deployment-specific rank IDs.
*/
export class UserPermissionsMapParser implements IMessageParser
{
private _permissions: Map<string, number> = new Map();
public flush(): boolean
{
this._permissions = new Map();
return true;
}
public parse(wrapper: IMessageDataWrapper): boolean
{
if(!wrapper) return false;
const count = wrapper.readInt();
const fresh = new Map<string, number>();
for(let i = 0; i < count; i++)
{
const key = wrapper.readString();
const value = wrapper.readInt();
fresh.set(key, value);
}
this._permissions = fresh;
return true;
}
public get permissions(): ReadonlyMap<string, number>
{
return this._permissions;
}
}
@@ -1 +1,2 @@
export * from './UserPermissionsMapParser';
export * from './UserPermissionsParser';