fix(discord): bind stops to approving admin

This commit is contained in:
Jarvis
2026-07-13 03:36:18 -05:00
parent 6af68e76de
commit 318a7bdda8
3 changed files with 175 additions and 32 deletions

View File

@@ -19,12 +19,22 @@ export interface DiscordPluginConfig {
export type DiscordInteractionOperation = 'bind' | 'attach' | 'send' | 'approve' | 'stop';
export type DiscordInteractionRole = 'viewer' | 'operator' | 'admin';
/** A provisioned Discord-to-Mosaic identity pairing. */
export interface DiscordInteractionUserBinding {
role: DiscordInteractionRole;
/** Required for privileged approval and stop operations. */
mosaicUserId?: string;
}
/** Legacy role-only pairings remain valid for non-privileged Discord ingress. */
export type DiscordInteractionPairing = DiscordInteractionRole | DiscordInteractionUserBinding;
export interface DiscordInteractionBinding {
instanceId: string;
guildId: string;
channelId: string;
/** Pairing roster keyed by Discord user ID. */
pairedUsers: Readonly<Record<string, DiscordInteractionRole>>;
pairedUsers: Readonly<Record<string, DiscordInteractionPairing>>;
}
const operationRoles: Readonly<
@@ -49,10 +59,22 @@ export function resolveDiscordInteractionBinding(
(candidate) => candidate.guildId === guildId && candidate.channelId === channelId,
);
if (!binding) return null;
const role = binding.pairedUsers[userId];
const pairing = binding.pairedUsers[userId];
const role = typeof pairing === 'string' ? pairing : pairing?.role;
return role && operationRoles[operation].includes(role) ? binding : null;
}
/** Resolves the provisioned Mosaic identity for an already-authorized Discord user. */
export function resolveDiscordInteractionActorId(
binding: DiscordInteractionBinding,
discordUserId: string,
): string | null {
const pairing = binding.pairedUsers[discordUserId];
if (typeof pairing === 'string') return null;
const mosaicUserId = pairing?.mosaicUserId?.trim();
return mosaicUserId || null;
}
/** Parses provisioned binding roster JSON and rejects malformed or empty data. */
export function parseDiscordInteractionBindings(
value: string | undefined,
@@ -75,11 +97,42 @@ export function parseDiscordInteractionBindings(
) {
throw new Error('Invalid Discord interaction binding');
}
const pairedUsers = Object.fromEntries(
Object.entries(candidate.pairedUsers).map(([discordUserId, pairing]: [string, unknown]) => {
if (!discordUserId.trim()) {
throw new Error('Invalid Discord interaction user binding');
}
if (typeof pairing === 'string') {
if (!['viewer', 'operator', 'admin'].includes(pairing)) {
throw new Error('Invalid Discord interaction user binding');
}
return [discordUserId, pairing];
}
if (typeof pairing !== 'object' || pairing === null) {
throw new Error('Invalid Discord interaction user binding');
}
const userBinding = pairing as Partial<DiscordInteractionUserBinding>;
if (
!userBinding.role ||
!['viewer', 'operator', 'admin'].includes(userBinding.role) ||
(userBinding.mosaicUserId !== undefined && !userBinding.mosaicUserId.trim())
) {
throw new Error('Invalid Discord interaction user binding');
}
return [
discordUserId,
{
role: userBinding.role,
...(userBinding.mosaicUserId ? { mosaicUserId: userBinding.mosaicUserId } : {}),
},
];
}),
) as Record<string, DiscordInteractionPairing>;
return {
instanceId: candidate.instanceId,
guildId: candidate.guildId,
channelId: candidate.channelId,
pairedUsers: candidate.pairedUsers,
pairedUsers,
};
});
}