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
2 changes: 1 addition & 1 deletion packages/fluxer-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"lint": "eslint src --max-warnings 0 --config ../../eslint.config.js",
"lint:fix": "eslint src --fix --config ../../eslint.config.js",
"test": "vitest run --passWithNoTests",
"test:coverage": "vitest run --coverage --passWithNoTests"
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@fluxerjs/rest": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions packages/fluxer-core/src/structures/Guild.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest';
import { Guild } from './Guild.js';
import { Guild, Client } from '../';

function createMockClient() {
return {} as Parameters<typeof Guild>[0];
return {} as Client;
}

function createGuild(
Expand Down
12 changes: 5 additions & 7 deletions packages/fluxer-core/src/structures/GuildMember.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { describe, it, expect } from 'vitest';
import { GuildMember } from './GuildMember.js';
import { Guild } from './Guild.js';
import { User } from './User.js';
import { Guild, Client, User, GuildMember } from '../';

function createMockClient() {
const client = {
getOrCreateUser: (data: { id: string; username: string; global_name?: string | null }) =>
new User(client as never, data),
};
return client as Parameters<typeof GuildMember>[0];
getOrCreateUser: (data: { id: string; username: string; discriminator: string; global_name?: string | null }) =>
new User(client as Client, data),
}
return client as Client;
}

function createMockGuild(client: ReturnType<typeof createMockClient>) {
Expand Down
33 changes: 11 additions & 22 deletions packages/fluxer-core/src/structures/GuildMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { User } from './User.js';
import { Guild } from './Guild.js';
import { GuildChannel } from './Channel.js';
import { APIGuildMember } from '@fluxerjs/types';
import { PermissionFlagsMap, type PermissionResolvable } from '@fluxerjs/util';
import {
BitField,
PermissionFlags
} from '@fluxerjs/util';
import { Routes } from '@fluxerjs/types';
import { cdnMemberAvatarURL, cdnMemberBannerURL } from '../util/cdn.js';
import { computePermissions, hasPermission } from '../util/permissions.js';
import { computePermissions } from '../util/permissions.js';
import { GuildMemberRoleManager } from './GuildMemberRoleManager.js';

/** Represents a member of a guild. */
Expand Down Expand Up @@ -148,19 +151,12 @@ export class GuildMember extends Base {
* const perms = member.permissions;
* if (perms.has(PermissionFlags.BanMembers)) { ... }
*/
get permissions(): { has(permission: PermissionResolvable): boolean } {
get permissions() {
const base = this._computeBasePermissions();
const ownerId = this.guild.ownerId;
const isOwner = ownerId != null && ownerId !== '' && String(ownerId) === String(this.id);
const perms = computePermissions(base, [], [], this.id, isOwner);
return {
has(permission: PermissionResolvable): boolean {
const perm =
typeof permission === 'number' ? permission : PermissionFlagsMap[String(permission)];
if (perm === undefined) return false;
return hasPermission(perms, BigInt(perm));
},
};
return new BitField<keyof typeof PermissionFlags>(perms);
}

/**
Expand All @@ -172,7 +168,7 @@ export class GuildMember extends Base {
* const perms = member.permissionsIn(channel);
* if (perms.has(PermissionFlags.SendMessages)) { ... }
*/
permissionsIn(channel: GuildChannel): { has(permission: PermissionResolvable): boolean } {
permissionsIn(channel: GuildChannel) {
const base = this._computeBasePermissions();
const ownerId = this.guild.ownerId;
const isOwner = ownerId != null && ownerId !== '' && String(ownerId) === String(this.id);
Expand All @@ -183,24 +179,17 @@ export class GuildMember extends Base {
this.id,
isOwner,
);
return {
has(permission: PermissionResolvable): boolean {
const perm =
typeof permission === 'number' ? permission : PermissionFlagsMap[String(permission)];
if (perm === undefined) return false;
return hasPermission(perms, BigInt(perm));
},
};
return new BitField<keyof typeof PermissionFlags>(perms);
}

private _computeBasePermissions(): bigint {
let base = 0n;
const everyone = this.guild.roles.get(this.guild.id);
if (everyone) base |= BigInt(everyone.permissions);
if (everyone) base |= everyone.permissions.bitfield;
for (const roleId of this.roles.roleIds) {
if (roleId === this.guild.id) continue;
const role = this.guild.roles.get(roleId);
if (role) base |= BigInt(role.permissions);
if (role) base |= role.permissions.bitfield;
}
return base;
}
Comment on lines 185 to 195
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -e ts PermissionsBitField --exec cat -n {}

Repository: fluxerjs/core

Length of output: 7709


Fix permission constants that use 2n << N instead of 1n << N, causing bit-position shifts.

The following permission flags in packages/util/src/PermissionsBitField.ts incorrectly use 2n << N as the base, which shifts the bit position one place higher than intended:

Flag Current (line) Produces bit Expected
UseExternalStickers 2n << 37n (44) 38 1n << 37n
ModerateMembers 2n << 40n (45) 41 1n << 40n
CreateExpressions 2n << 43n (46) 44 1n << 43n
PinMessages 2n << 51n (47) 52 1n << 51n
BypassSlowmode 2n << 52n (48) 53 1n << 52n
UpdateRtcRegion 2n << 53n (49) 54 1n << 53n

Additionally, ManageEmojisAndStickers (line 42) and ManageExpressions (line 43) both define 1n << 30n — this is a duplicate alias that should be reviewed.

These errors will cause permission checks in _computeBasePermissions() and permissionsIn() to evaluate against incorrect bit positions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fluxer-core/src/structures/GuildMember.ts` around lines 185 - 195,
The permission constants in PermissionsBitField.ts use incorrect bases like `2n
<< N` shifting bits one position; update the flagged constants
(`UseExternalStickers`, `ModerateMembers`, `CreateExpressions`, `PinMessages`,
`BypassSlowmode`, `UpdateRtcRegion`) to use `1n << N` with the correct N values
(e.g., `1n << 37n`, `1n << 40n`, etc.), and review/fix the duplicate alias where
both `ManageEmojisAndStickers` and `ManageExpressions` are defined as `1n <<
30n` (ensure each permission has the correct unique bit). This will correct bit
positions used by `_computeBasePermissions()` and `permissionsIn()`.

Expand Down
4 changes: 2 additions & 2 deletions packages/fluxer-core/src/structures/Invite.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest';
import { Invite } from './Invite.js';
import { Client, Invite } from '../';

function createMockClient() {
return {} as Parameters<typeof Invite>[0];
return {} as Client;
}

function createInvite(overrides: { code?: string } = {}) {
Expand Down
2 changes: 1 addition & 1 deletion packages/fluxer-core/src/structures/Message.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { Message } from './Message.js';
import { Message } from '../';

describe('Message._createMessageBody', () => {
describe('reply (message_reference)', () => {
Expand Down
51 changes: 25 additions & 26 deletions packages/fluxer-core/src/structures/Role.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { describe, it, expect } from 'vitest';
import { Role } from './Role.js';
import { Client, Role } from '../';
import { PermissionFlags } from '@fluxerjs/util';

/** Minimal client mock — Role.has() only uses role.permissions, not client. */
function createMockClient() {
return {} as Parameters<typeof Role>[0];
return {} as Client;
}

function createRole(permissions: string, overrides: Partial<{ id: string; name: string }> = {}) {
function createRole(permissions: string | bigint, overrides: Partial<{ id: string; name: string }> = {}) {
return new Role(
createMockClient(),
{
permissions,
permissions: permissions.toString(),
id: overrides.id ?? '1',
name: overrides.name ?? 'Role',
color: 0,
Expand All @@ -23,46 +22,46 @@ function createRole(permissions: string, overrides: Partial<{ id: string; name:
);
}

describe('Role', () => {
describe('Role.permissions', () => {
describe('has()', () => {
it('returns true when role has Administrator (grants all permissions)', () => {
const role = createRole('8'); // 1 << 3 = Administrator
expect(role.has(PermissionFlags.Administrator)).toBe(true);
expect(role.has(PermissionFlags.SendMessages)).toBe(true);
expect(role.has(PermissionFlags.BanMembers)).toBe(true);
expect(role.has(PermissionFlags.ManageChannels)).toBe(true);
const role = createRole(PermissionFlags.Administrator); // 1 << 3 = Administrator
expect(role.permissions.has(PermissionFlags.Administrator)).toBe(true);
expect(role.permissions.has(PermissionFlags.SendMessages)).toBe(true);
expect(role.permissions.has(PermissionFlags.BanMembers)).toBe(true);
expect(role.permissions.has(PermissionFlags.ManageChannels)).toBe(true);
});

it('returns true when role has specific permission', () => {
const role = createRole('2048'); // SendMessages
expect(role.has(PermissionFlags.SendMessages)).toBe(true);
expect(role.has(PermissionFlags.BanMembers)).toBe(false);
expect(role.has(PermissionFlags.ViewChannel)).toBe(false);
expect(role.permissions.has(PermissionFlags.SendMessages)).toBe(true);
expect(role.permissions.has(PermissionFlags.BanMembers)).toBe(false);
expect(role.permissions.has(PermissionFlags.ViewChannel)).toBe(false);
});

it('returns true for string permission name', () => {
const role = createRole('2048');
expect(role.has('SendMessages')).toBe(true);
expect(role.has('BanMembers')).toBe(false);
const role = createRole(PermissionFlags.SendMessages);
expect(role.permissions.has('SendMessages')).toBe(true);
expect(role.permissions.has('BanMembers')).toBe(false);
});

it('returns false when role has no permissions', () => {
const role = createRole('0');
expect(role.has(PermissionFlags.SendMessages)).toBe(false);
expect(role.has(PermissionFlags.Administrator)).toBe(false);
expect(role.permissions.has(PermissionFlags.SendMessages)).toBe(false);
expect(role.permissions.has(PermissionFlags.Administrator)).toBe(false);
});

it('returns false for undefined or invalid permission name', () => {
const role = createRole('2048');
expect(role.has('NonExistent' as never)).toBe(false);
it('throws an error for invalid permission name', () => {
const role = createRole(PermissionFlags.Administrator);
expect(() => role.permissions.has('NonExistent' as never)).toThrow(RangeError);
});

it('handles combined permission bitfield', () => {
// SendMessages (2048) | ViewChannel (1024) = 3072
const role = createRole('3072');
expect(role.has(PermissionFlags.SendMessages)).toBe(true);
expect(role.has(PermissionFlags.ViewChannel)).toBe(true);
expect(role.has(PermissionFlags.BanMembers)).toBe(false);
const role = createRole( String(PermissionFlags.SendMessages | PermissionFlags.ViewChannel) );
expect(role.permissions.has(PermissionFlags.SendMessages)).toBe(true);
expect(role.permissions.has(PermissionFlags.ViewChannel)).toBe(true);
expect(role.permissions.has(PermissionFlags.BanMembers)).toBe(false);
});
});

Expand Down
35 changes: 10 additions & 25 deletions packages/fluxer-core/src/structures/Role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
PermissionFlags,
resolvePermissionsToBitfield,
type PermissionResolvable,
ALL_PERMISSIONS_BIGINT,
PermissionsBitField,
} from '@fluxerjs/util';

/** Represents a role in a guild. */
Expand All @@ -16,7 +18,7 @@ export class Role extends Base {
name: string;
color: number;
position: number;
permissions: string;
_permissions: string;
hoist: boolean;
mentionable: boolean;
unicodeEmoji: string | null;
Expand All @@ -34,19 +36,24 @@ export class Role extends Base {
this.name = data.name;
this.color = data.color;
this.position = data.position;
this.permissions = data.permissions;
this._permissions = data.permissions;
this.hoist = !!data.hoist;
this.mentionable = !!data.mentionable;
this.unicodeEmoji = data.unicode_emoji ?? null;
this.hoistPosition = data.hoist_position ?? null;
}

get permissions(): PermissionsBitField {
const bits = BigInt(this._permissions);
return new PermissionsBitField( (bits & PermissionFlags.Administrator) !== 0n ? ALL_PERMISSIONS_BIGINT : bits );
}

/** Update mutable fields from fresh API data. Used by edit and gateway events. */
_patch(data: Partial<APIRole>): void {
if (data.name !== undefined) this.name = data.name;
if (data.color !== undefined) this.color = data.color;
if (data.position !== undefined) this.position = data.position;
if (data.permissions !== undefined) this.permissions = data.permissions;
if (data.permissions !== undefined) this._permissions = data.permissions;
if (data.hoist !== undefined) this.hoist = !!data.hoist;
if (data.mentionable !== undefined) this.mentionable = !!data.mentionable;
if (data.unicode_emoji !== undefined) this.unicodeEmoji = data.unicode_emoji ?? null;
Expand All @@ -58,28 +65,6 @@ export class Role extends Base {
return `<@&${this.id}>`;
}

/**
* Check if this role has a permission. Administrator grants all permissions.
* @param permission - Permission flag, name, or resolvable
* @returns true if the role has the permission
* @example
* if (role.has(PermissionFlags.BanMembers)) { ... }
* if (role.has('ManageChannels')) { ... }
*/
has(permission: PermissionResolvable): boolean {
const perm =
typeof permission === 'number'
? permission
: PermissionFlags[permission as keyof typeof PermissionFlags];
if (perm === undefined) return false;
const permNum = Number(perm);
const rolePerms = BigInt(this.permissions);
const permBig = BigInt(permNum);
if (permBig < 0) return false;
if ((rolePerms & BigInt(PermissionFlags.Administrator)) !== 0n) return true;
return (rolePerms & permBig) === permBig;
}

/**
* Edit this role.
* Requires Manage Roles permission.
Expand Down
4 changes: 2 additions & 2 deletions packages/fluxer-core/src/structures/User.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest';
import { User } from './User.js';
import { Client, User } from '../';

function createMockClient() {
return {} as Parameters<typeof User>[0];
return {} as Client;
}

function createUser(
Expand Down
Loading
Loading