Permissions
(v2.1.0) A client->server event may carry an optional permission
requirement — the flag names a gateway guard and the client both check. Only
outbound events carry it: the client authorizes what it sends, never what it
receives.
export const contracts = defineSocketContracts({
chat: {
deleteAny: {
direction: "client->server",
permission: { require: ["chat.MANAGE_MESSAGES"] }, // ← new, optional
request: z.object({ id: z.string() }),
ack: z.object({ ok: z.boolean() }),
},
},
});The value is a PermissionRequirement — { require?, any?, reason? } (require
= all flags, any = at least one). The flag names reference a
@tahanabavi/type-permission bit map, but the type is
redeclared structurally, so typesocket gains no new dependency.
Server — a Socket.IO gateway guard reads
def.permissionand rejects the frame when the actor lacks the flags.Client — register
createPermissionMiddlewarevia theauthorizeOutboundoption; a denied emit throws before it reaches the wire.
import { SocketClient, createPermissionMiddleware } from "@tahanabavi/typesocket";
import { P } from "./permissions";
const client = new SocketClient(config, contracts, {
authorizeOutbound: createPermissionMiddleware({
getPermissions: () => store.getSnapshot().global, // synchronous
authorize: P.authorize,
}),
});
// blocked emits throw PermissionDeniedError — an ack'd emit rejects, a
// fire-and-forget one throws synchronously; other events are untouched:
await client.modules.chat.deleteAny({ id });It goes through authorizeOutbound rather than client.use() on purpose: a
SocketMiddleware can only drop a frame silently, whereas this throws to the
call site. getPermissions is synchronous so a fire-and-forget emit can fail
synchronously. The check is UX only — the server re-authorizes every frame.
Additive: events without a permission key are unaffected. See
docs/releases/v2.1.0.md.