Skip to content

Support Discord role permissions; grant maintainers external emojis - #247

Merged
dend merged 2 commits into
mainfrom
discord-role-permissions
Nov 11, 2026
Merged

dend merged 2 commits into
mainfrom
discord-role-permissions

Conversation

@dend

@dend dend commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Adds an optional permissions field to a role's Discord config and uses it to grant Use External Emoji to maintainers (synced).

discord: { role: 'maintainers (synced)', permissions: ['USE_EXTERNAL_EMOJIS'] },

Safety for existing roles

  • Roles without permissions are not touched. Their resource inputs are identical to before (the key is omitted, not set to undefined), so Pulumi sees no change. Even on a rename, their update stays a name-only PATCH.
  • Pulumi owns only the bits it declares. For a role with permissions, the provider reads the live permissions and sets the declared bits. Every other bit is kept, including permissions set by hand in Discord. Removing an entry later clears only that bit.
  • No replacement. The role provider has no diff(), so changed inputs update in place. Roles are never recreated, and delete() never runs.
  • Allowlist. Only ADD_REACTIONS, EMBED_LINKS, ATTACH_FILES, USE_EXTERNAL_EMOJIS and USE_EXTERNAL_STICKERS can be granted. validate-config rejects anything else, and a test checks that no administrative or moderation bit is on the list.

Reviewers: the preview comment should show exactly one change, an update to discord-role-maintainers adding permissions: "262144". Anything else is unexpected.

Notes

  • The bot can only grant permissions it has itself, and only on roles below its own. If the apply fails with "Missing Permissions", give the bot Use External Emoji.
  • Discord only lets Nitro users use emoji from other servers. Channel overrides that deny external emoji still apply.

Testing

  • npm test: 87 passed, including new tests for the bitfield, preserving hand-set bits, no-op re-apply, revoke-only-declared, and the allowlist.
  • npm run validate: passes, with the new role-permission check.
  • prettier --check: clean.

🤖 Generated with Claude Code

Add an optional `permissions` list to a role's Discord config, limited to
an allowlist of low-risk permissions, and grant USE_EXTERNAL_EMOJIS to the
maintainers role.

Existing roles are unaffected: roles without `permissions` produce the
same resource inputs as before (the key is omitted, not undefined), and
their update path stays a name-only PATCH. For roles that do declare
permissions, Pulumi owns only the declared bits and merges them into the
live value, so permissions set by hand in Discord are preserved; removing
an entry later clears only that bit. Dynamic resources update in place
(no diff() means no replaces), so no role is recreated or deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dend
dend requested a review from a team as a code owner November 11, 2026 10:44
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Pulumi Preview

Click to expand preview output
Previewing update (prod):
  pulumi:pulumi:Stack: (same)
    [urn=urn:pulumi:prod::mcp-access::pulumi:pulumi:Stack::mcp-access-prod]
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760679064535238]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-lead-maintainers]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1569703044600299570]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-release-managers]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1461488567162503189]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-community-managers]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1568252368624033867]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-members]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760682076307516]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-core-maintainers]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760673498959945]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-administrators]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760661985464506]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-reference-servers-maintainers]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760656226680903]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-moderators]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760676367597740]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-inspector-maintainers]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760690611720404]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-registry-maintainers]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760684643221556]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-use-mcp-maintainers]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760687444758623]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-sdk-maintainers]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760711796887821]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-php-sdk]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760698316652557]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-kotlin-sdk]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760717434163281]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-python-sdk]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760769758101595]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-ruby-sdk]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760773134651505]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-transport-wg]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No special permissions - roles are for organization only
          +                 // Only the declared permissions; '0' (organization only) when none are declared
          +                 permissions: inputs.permissions ?? '0',
                            mentionable: false,
                            hoist: false,
            ...
              }).apply(undefined, undefined).apply(this, arguments);
            }
          + function __mergeDiscordRolePermissions(__0, __1, __2) {
          +   return (function() {
          +     with({ mergeDiscordRolePermissions: __mergeDiscordRolePermissions, this: undefined, arguments: undefined }) {
          + return function /*mergeDiscordRolePermissions*/(live, previouslyManaged, managed) {
          +     const previous = BigInt(previouslyManaged ?? '0');
          +     const next = BigInt(managed ?? '0');
          +     return ((BigInt(live) & ~previous) | next).toString();
          + };
          +     }
          +   }).apply(undefined, undefined).apply(this, arguments);
          + }
            function __f3(__0, __1, __2) {
              return (function() {
          -     with({ discordFetch: __discordFetch, this: undefined, arguments: undefined }) {
          +     with({ discordFetch: __discordFetch, discordPermissions_1: __discordPermissions_1, this: undefined, arguments: undefined }) {
          - return async function /*update*/(id, _olds, news) {
          + return async function /*update*/(id, olds, news) {
          +         const body = { name: news.roleName };
          +         // Touch permissions only if this role declares them now or did before; otherwise the
          +         // PATCH stays name-only, exactly as for every role prior to permission support.
          +         if (news.permissions !== undefined || olds.permissions !== undefined) {
          +             const liveRoles = await discordFetch(news.token, `/guilds/${news.guildId}/roles`);
          +             const live = liveRoles.find((r) => r.id === id);
          +             if (!live) {
          +                 throw new Error(`Role ${id} not found; refusing to write its permissions`);
          +             }
          +             // Keep every bit Pulumi does not own; set/clear only the declared ones
          +             body.permissions = (0, discordPermissions_1.mergeDiscordRolePermissions)(live.permissions, olds.permissions, news.permissions);
          +         }
                    await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
                        method: 'PATCH',
          -             body: JSON.stringify({
          -                 name: news.roleName,
          -             }),
          +             body: JSON.stringify(body),
                    });
                    return {
            ...
    ~ pulumi-nodejs:dynamic:Resource: (update)
        [id=1460760804134752443]
        [urn=urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-go-sdk]
      ~ __provider: 
            exports.handler = __f0;
          + var __discordPermissions_1 = {mergeDiscordRolePermissions: __mergeDiscordRolePermissions};
            var __provider = {create: __f1, read: __f2, update: __f3, delete: __f4};
            function __tryParseJson(__0) {
            ...
                        body: JSON.stringify({
                            name: inputs.roleName,
          -                 permissions: '0', // No s

... (truncated)

@PederHP

PederHP commented Nov 11, 2026

Copy link
Copy Markdown
Member

LGTM - pulumi preview truncates so the discord-role-maintainers with permissions: "262144" doesn't show, but the full CI log has it.

@PederHP PederHP left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Verified that permission "262144" is added to discord-role-maintainers in the pulumi preview in the CI.

@dend
dend merged commit f89698a into main Nov 11, 2026
5 checks passed
@dend
dend deleted the discord-role-permissions branch November 11, 2026 11:01

This branch was previously deployed

1 inactive deployment
production — 649dad86 Deployed Nov 11, 2026 by dend via Preview Changes #412
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants