@helyx/bot 0.4.26 → 0.4.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/cloud-connector.js +3 -0
- package/dist/cloud-control-dispatcher.js +29 -0
- package/dist/control-server.d.ts +45 -1
- package/dist/control-server.js +211 -17
- package/dist/interactions.js +125 -92
- package/dist/permissions-module.js +29 -4
- package/dist/resource-authorization.js +2 -1
- package/dist/shard-contracts.d.ts +69 -0
- package/dist/shard-contracts.js +44 -0
- package/dist/shard-coordinator.js +16 -2
- package/dist/shard-router.d.ts +3 -0
- package/dist/shard-router.js +19 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.28
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add the private testing-stage Forms module contracts and runtime integration,
|
|
8
|
+
and introduce server-configurable Helyx Administrator, Moderator, dashboard
|
|
9
|
+
access, and command-permission policies with fresh Discord authorization.
|
|
10
|
+
|
|
11
|
+
## 0.4.27
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Allow a strictly newer coordinated deployment to atomically replace an incompatible module-catalogue generation while preserving application and environment namespace isolation.
|
|
16
|
+
|
|
3
17
|
## 0.4.26
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/cloud-connector.js
CHANGED
|
@@ -719,6 +719,9 @@ function connectorCapabilities() {
|
|
|
719
719
|
return [
|
|
720
720
|
"installation.health.read.v1",
|
|
721
721
|
"guild.state.read.v1",
|
|
722
|
+
"guild.dashboard.access.read.v1",
|
|
723
|
+
"guild.access-policy.read.v1",
|
|
724
|
+
"guild.access-policy.write.v1",
|
|
722
725
|
"guild.module.public.read.v1",
|
|
723
726
|
"guild.module.status.write.v1",
|
|
724
727
|
"guild.module.configuration.write.v1",
|
|
@@ -25,6 +25,33 @@ export class CloudControlDispatcher {
|
|
|
25
25
|
result = await this.control.getGuildState(input.guildId);
|
|
26
26
|
break;
|
|
27
27
|
}
|
|
28
|
+
case "guild.dashboard.access.read.v1": {
|
|
29
|
+
const input = parseOperationInput(request.operation, request.input);
|
|
30
|
+
result = {
|
|
31
|
+
allowed: await this.control.canAccessDashboard({
|
|
32
|
+
guildId: input.guildId,
|
|
33
|
+
actorUserId: requiredActor(actorUserId),
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case "guild.access-policy.read.v1": {
|
|
39
|
+
const input = parseOperationInput(request.operation, request.input);
|
|
40
|
+
result = await this.control.getGuildAccessPolicy({
|
|
41
|
+
guildId: input.guildId,
|
|
42
|
+
actorUserId: requiredActor(actorUserId),
|
|
43
|
+
});
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "guild.access-policy.write.v1": {
|
|
47
|
+
const input = parseOperationInput(request.operation, request.input);
|
|
48
|
+
result = await this.control.updateGuildAccessPolicy({
|
|
49
|
+
...input,
|
|
50
|
+
actorUserId: requiredActor(actorUserId),
|
|
51
|
+
correlationId,
|
|
52
|
+
});
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
28
55
|
case "guild.module.public.read.v1": {
|
|
29
56
|
const input = parseOperationInput(request.operation, request.input);
|
|
30
57
|
if (request.actor.kind !== "public")
|
|
@@ -116,6 +143,8 @@ export class CloudControlDispatcher {
|
|
|
116
143
|
resourceId: input.resourceId,
|
|
117
144
|
relationId: input.relationId,
|
|
118
145
|
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
|
|
146
|
+
...(input.search === undefined ? {} : { search: input.search }),
|
|
147
|
+
...(input.filters === undefined ? {} : { filters: input.filters }),
|
|
119
148
|
limit: input.limit,
|
|
120
149
|
actorUserId: requiredActor(actorUserId),
|
|
121
150
|
correlationId,
|
package/dist/control-server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DatabasePool, type GuildShardOwnership } from "@helyx/database";
|
|
2
|
-
import { type DashboardActionResult, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type PublicAchievementProjectionPage, type ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
import { type DashboardActionResult, type GuildAccessPolicy, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type PublicAchievementProjectionPage, type ServiceAccess } from "@helyx/sdk";
|
|
3
3
|
import { type ActionReceipt, type SignedAssessmentEnvelope } from "@helyx/verification-contracts";
|
|
4
4
|
import { type Client } from "discord.js";
|
|
5
5
|
import { type FastifyInstance } from "fastify";
|
|
@@ -45,6 +45,7 @@ interface DashboardModuleDefinition {
|
|
|
45
45
|
actions: DashboardActionDefinition[];
|
|
46
46
|
achievementTriggers: NonNullable<ModuleDefinition["manifest"]["contributions"]["achievementTriggers"]>;
|
|
47
47
|
resources: readonly DashboardResourceDefinition[];
|
|
48
|
+
workspace?: NonNullable<ModuleDefinition["manifest"]["dashboard"]>["workspace"];
|
|
48
49
|
}
|
|
49
50
|
interface DashboardLoggingPolicy {
|
|
50
51
|
eventId: string;
|
|
@@ -71,6 +72,7 @@ export interface BotControlGuildResources {
|
|
|
71
72
|
id: string;
|
|
72
73
|
name: string;
|
|
73
74
|
colour: string;
|
|
75
|
+
position: number;
|
|
74
76
|
}>;
|
|
75
77
|
}
|
|
76
78
|
export interface BotPublicModuleProjection {
|
|
@@ -146,6 +148,14 @@ export interface BotControlState {
|
|
|
146
148
|
permissions: Record<string, DashboardPermissionState>;
|
|
147
149
|
logging: Record<string, DashboardLoggingPolicy[]>;
|
|
148
150
|
}
|
|
151
|
+
export interface DashboardGuildAccessState extends GuildAccessPolicy {
|
|
152
|
+
version: number;
|
|
153
|
+
canManage: boolean;
|
|
154
|
+
actorAuthority: "owner" | "administrator" | "moderator" | "member";
|
|
155
|
+
actorRoleIds: string[];
|
|
156
|
+
actorHighestRolePosition: number | null;
|
|
157
|
+
updatedAt: string | null;
|
|
158
|
+
}
|
|
149
159
|
export interface BotControlHandler {
|
|
150
160
|
sendSupportNotification(input: SupportNotificationInput): Promise<void>;
|
|
151
161
|
listGuildPresence(input: {
|
|
@@ -153,6 +163,21 @@ export interface BotControlHandler {
|
|
|
153
163
|
correlationId: string;
|
|
154
164
|
}): Promise<BotGuildPresenceObservation[]>;
|
|
155
165
|
getGuildState(guildId: string): Promise<BotControlState>;
|
|
166
|
+
canAccessDashboard(input: {
|
|
167
|
+
guildId: string;
|
|
168
|
+
actorUserId: string;
|
|
169
|
+
}): Promise<boolean>;
|
|
170
|
+
getGuildAccessPolicy(input: {
|
|
171
|
+
guildId: string;
|
|
172
|
+
actorUserId: string;
|
|
173
|
+
}): Promise<DashboardGuildAccessState>;
|
|
174
|
+
updateGuildAccessPolicy(input: {
|
|
175
|
+
guildId: string;
|
|
176
|
+
actorUserId: string;
|
|
177
|
+
correlationId: string;
|
|
178
|
+
version: number;
|
|
179
|
+
policy: GuildAccessPolicy;
|
|
180
|
+
}): Promise<DashboardGuildAccessState>;
|
|
156
181
|
readPublicModuleProjection(input: {
|
|
157
182
|
guildId: string;
|
|
158
183
|
moduleId: string;
|
|
@@ -235,6 +260,8 @@ export interface BotControlHandler {
|
|
|
235
260
|
relationId: string;
|
|
236
261
|
cursor?: string;
|
|
237
262
|
limit: number;
|
|
263
|
+
search?: string;
|
|
264
|
+
filters?: Readonly<Record<string, string>>;
|
|
238
265
|
actorUserId: string;
|
|
239
266
|
correlationId: string;
|
|
240
267
|
}): Promise<ManagedResourceRelationPage>;
|
|
@@ -354,6 +381,21 @@ export declare class BotControlService implements BotControlHandler {
|
|
|
354
381
|
cursor?: string;
|
|
355
382
|
}): Promise<PrivacyErasePage>;
|
|
356
383
|
getGuildState(guildId: string): Promise<BotControlState>;
|
|
384
|
+
canAccessDashboard(input: {
|
|
385
|
+
guildId: string;
|
|
386
|
+
actorUserId: string;
|
|
387
|
+
}): Promise<boolean>;
|
|
388
|
+
getGuildAccessPolicy(input: {
|
|
389
|
+
guildId: string;
|
|
390
|
+
actorUserId: string;
|
|
391
|
+
}): Promise<DashboardGuildAccessState>;
|
|
392
|
+
updateGuildAccessPolicy(input: {
|
|
393
|
+
guildId: string;
|
|
394
|
+
actorUserId: string;
|
|
395
|
+
correlationId: string;
|
|
396
|
+
version: number;
|
|
397
|
+
policy: GuildAccessPolicy;
|
|
398
|
+
}): Promise<DashboardGuildAccessState>;
|
|
357
399
|
readPublicModuleProjection(input: {
|
|
358
400
|
guildId: string;
|
|
359
401
|
moduleId: string;
|
|
@@ -436,6 +478,8 @@ export declare class BotControlService implements BotControlHandler {
|
|
|
436
478
|
relationId: string;
|
|
437
479
|
cursor?: string;
|
|
438
480
|
limit: number;
|
|
481
|
+
search?: string;
|
|
482
|
+
filters?: Readonly<Record<string, string>>;
|
|
439
483
|
actorUserId: string;
|
|
440
484
|
correlationId: string;
|
|
441
485
|
}): Promise<ManagedResourceRelationPage>;
|
package/dist/control-server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AuditRepository, InstallationRepository, ModuleLoggingPolicyRepository, OptimisticConcurrencyError, PermissionPolicyRepository, withTransaction, } from "@helyx/database";
|
|
2
|
-
import { DashboardActionValidationError, HELYX_SERVICE_NAMES, ModuleStateValidationError, } from "@helyx/sdk";
|
|
1
|
+
import { AuditRepository, GuildAccessPolicyRepository, InstallationRepository, ModuleLoggingPolicyRepository, OptimisticConcurrencyError, PermissionPolicyRepository, withTransaction, } from "@helyx/database";
|
|
2
|
+
import { DashboardActionValidationError, DEFAULT_GUILD_ACCESS_POLICY, HELYX_SERVICE_NAMES, ModuleStateValidationError, } from "@helyx/sdk";
|
|
3
3
|
import { actionReceiptSchema, signedAssessmentEnvelopeSchema, } from "@helyx/verification-contracts";
|
|
4
|
-
import { canUseCommand } from "@helyx/core";
|
|
4
|
+
import { canAccessGuildDashboard, canMutatePermissionPolicy, canUseCommand, permissionMutationDenialMessage, resolveHelyxAuthority, } from "@helyx/core";
|
|
5
5
|
import { ChannelType, PermissionFlagsBits } from "discord.js";
|
|
6
6
|
import Fastify, {} from "fastify";
|
|
7
7
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
@@ -41,6 +41,15 @@ const permissionState = z.object({
|
|
|
41
41
|
}))
|
|
42
42
|
.max(100),
|
|
43
43
|
});
|
|
44
|
+
const guildAccessPolicy = z
|
|
45
|
+
.object({
|
|
46
|
+
administratorRoleIds: z.array(snowflake).max(25),
|
|
47
|
+
moderatorRoleIds: z.array(snowflake).max(25),
|
|
48
|
+
dashboardModeratorsEnabled: z.boolean(),
|
|
49
|
+
dashboardAllowedRoleIds: z.array(snowflake).max(25),
|
|
50
|
+
dashboardDeniedRoleIds: z.array(snowflake).max(25),
|
|
51
|
+
})
|
|
52
|
+
.strict();
|
|
44
53
|
const mutationContext = z.object({
|
|
45
54
|
actorUserId: snowflake,
|
|
46
55
|
correlationId: z.uuid(),
|
|
@@ -146,6 +155,7 @@ export class DiscordGuildResourceDirectory {
|
|
|
146
155
|
id: role.id,
|
|
147
156
|
name: role.name,
|
|
148
157
|
colour: role.hexColor,
|
|
158
|
+
position: role.position,
|
|
149
159
|
})),
|
|
150
160
|
};
|
|
151
161
|
}
|
|
@@ -181,7 +191,13 @@ export class DiscordGuildResourceDirectory {
|
|
|
181
191
|
async resolveActor(guildId, actorUserId) {
|
|
182
192
|
try {
|
|
183
193
|
const guild = await this.client.guilds.fetch(guildId);
|
|
184
|
-
|
|
194
|
+
// Dashboard authorization must reflect current Discord roles and
|
|
195
|
+
// permissions even when a member-update gateway event was missed.
|
|
196
|
+
const member = await guild.members.fetch({
|
|
197
|
+
user: actorUserId,
|
|
198
|
+
force: true,
|
|
199
|
+
cache: true,
|
|
200
|
+
});
|
|
185
201
|
return {
|
|
186
202
|
userId: member.id,
|
|
187
203
|
roleIds: [...member.roles.cache.keys()],
|
|
@@ -190,6 +206,7 @@ export class DiscordGuildResourceDirectory {
|
|
|
190
206
|
.map(normalizeDiscordPermission),
|
|
191
207
|
isServerOwner: guild.ownerId === member.id,
|
|
192
208
|
isAdministrator: member.permissions.has(PermissionFlagsBits.Administrator),
|
|
209
|
+
highestRolePosition: member.roles.highest.position,
|
|
193
210
|
};
|
|
194
211
|
}
|
|
195
212
|
catch (error) {
|
|
@@ -425,6 +442,88 @@ export class BotControlService {
|
|
|
425
442
|
logging,
|
|
426
443
|
};
|
|
427
444
|
}
|
|
445
|
+
async canAccessDashboard(input) {
|
|
446
|
+
const actor = await this.resources.resolveActor(input.guildId, input.actorUserId);
|
|
447
|
+
return canAccessGuildDashboard(actor, await this.#readGuildAccessPolicy(input.guildId));
|
|
448
|
+
}
|
|
449
|
+
async getGuildAccessPolicy(input) {
|
|
450
|
+
const actor = await this.resources.resolveActor(input.guildId, input.actorUserId);
|
|
451
|
+
const record = await new GuildAccessPolicyRepository(this.database).get(input.guildId);
|
|
452
|
+
const policy = record ?? DEFAULT_GUILD_ACCESS_POLICY;
|
|
453
|
+
return {
|
|
454
|
+
administratorRoleIds: [...policy.administratorRoleIds],
|
|
455
|
+
moderatorRoleIds: [...policy.moderatorRoleIds],
|
|
456
|
+
dashboardModeratorsEnabled: policy.dashboardModeratorsEnabled,
|
|
457
|
+
dashboardAllowedRoleIds: [...policy.dashboardAllowedRoleIds],
|
|
458
|
+
dashboardDeniedRoleIds: [...policy.dashboardDeniedRoleIds],
|
|
459
|
+
version: record ? safeVersion(record.version) : 0,
|
|
460
|
+
canManage: actor.isServerOwner || actor.isAdministrator,
|
|
461
|
+
actorAuthority: resolveHelyxAuthority(actor, policy),
|
|
462
|
+
actorRoleIds: [...actor.roleIds],
|
|
463
|
+
actorHighestRolePosition: actor.highestRolePosition ?? null,
|
|
464
|
+
updatedAt: record?.updatedAt.toISOString() ?? null,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async updateGuildAccessPolicy(input) {
|
|
468
|
+
const actor = await this.resources.resolveActor(input.guildId, input.actorUserId);
|
|
469
|
+
if (!actor.isServerOwner && !actor.isAdministrator)
|
|
470
|
+
throw new BotControlForbiddenError("Only the server owner or a Discord Administrator can change dashboard access");
|
|
471
|
+
const resources = await this.resources.getGuildResources(input.guildId);
|
|
472
|
+
const availableRoles = new Set(resources.roles.map(({ id }) => id));
|
|
473
|
+
const roleIds = [
|
|
474
|
+
...input.policy.administratorRoleIds,
|
|
475
|
+
...input.policy.moderatorRoleIds,
|
|
476
|
+
...input.policy.dashboardAllowedRoleIds,
|
|
477
|
+
...input.policy.dashboardDeniedRoleIds,
|
|
478
|
+
];
|
|
479
|
+
if (roleIds.some((roleId) => !availableRoles.has(roleId)))
|
|
480
|
+
throw new BotControlValidationError("Dashboard access contains a Discord role that is no longer available");
|
|
481
|
+
const saved = await withTransaction(this.database, async (connection) => {
|
|
482
|
+
const record = await new GuildAccessPolicyRepository(connection).save({
|
|
483
|
+
guildId: input.guildId,
|
|
484
|
+
administratorRoleIds: [...input.policy.administratorRoleIds],
|
|
485
|
+
moderatorRoleIds: [...input.policy.moderatorRoleIds],
|
|
486
|
+
dashboardModeratorsEnabled: input.policy.dashboardModeratorsEnabled,
|
|
487
|
+
dashboardAllowedRoleIds: [...input.policy.dashboardAllowedRoleIds],
|
|
488
|
+
dashboardDeniedRoleIds: [...input.policy.dashboardDeniedRoleIds],
|
|
489
|
+
expectedVersion: input.version === 0 ? null : BigInt(input.version),
|
|
490
|
+
updatedBy: input.actorUserId,
|
|
491
|
+
});
|
|
492
|
+
await new AuditRepository(connection).append({
|
|
493
|
+
guildId: input.guildId,
|
|
494
|
+
actorUserId: input.actorUserId,
|
|
495
|
+
action: "guild.access.updated",
|
|
496
|
+
source: "dashboard",
|
|
497
|
+
correlationId: input.correlationId,
|
|
498
|
+
afterState: {
|
|
499
|
+
version: record.version.toString(),
|
|
500
|
+
administratorRoleCount: record.administratorRoleIds.length,
|
|
501
|
+
moderatorRoleCount: record.moderatorRoleIds.length,
|
|
502
|
+
dashboardAllowedRoleCount: record.dashboardAllowedRoleIds.length,
|
|
503
|
+
dashboardDeniedRoleCount: record.dashboardDeniedRoleIds.length,
|
|
504
|
+
dashboardModeratorsEnabled: record.dashboardModeratorsEnabled,
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
return record;
|
|
508
|
+
});
|
|
509
|
+
return {
|
|
510
|
+
administratorRoleIds: [...saved.administratorRoleIds],
|
|
511
|
+
moderatorRoleIds: [...saved.moderatorRoleIds],
|
|
512
|
+
dashboardModeratorsEnabled: saved.dashboardModeratorsEnabled,
|
|
513
|
+
dashboardAllowedRoleIds: [...saved.dashboardAllowedRoleIds],
|
|
514
|
+
dashboardDeniedRoleIds: [...saved.dashboardDeniedRoleIds],
|
|
515
|
+
version: safeVersion(saved.version),
|
|
516
|
+
canManage: true,
|
|
517
|
+
actorAuthority: resolveHelyxAuthority(actor, saved),
|
|
518
|
+
actorRoleIds: [...actor.roleIds],
|
|
519
|
+
actorHighestRolePosition: actor.highestRolePosition ?? null,
|
|
520
|
+
updatedAt: saved.updatedAt.toISOString(),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
async #readGuildAccessPolicy(guildId) {
|
|
524
|
+
return ((await new GuildAccessPolicyRepository(this.database).get(guildId)) ??
|
|
525
|
+
DEFAULT_GUILD_ACCESS_POLICY);
|
|
526
|
+
}
|
|
428
527
|
async readPublicModuleProjection(input) {
|
|
429
528
|
const module = this.#requireModule(input.moduleId);
|
|
430
529
|
if (!module.publicProjection)
|
|
@@ -583,6 +682,26 @@ export class BotControlService {
|
|
|
583
682
|
])));
|
|
584
683
|
for (const command of commands)
|
|
585
684
|
assertVersion(currentCommands.get(command.id).version, supplied.get(command.id).version);
|
|
685
|
+
const [actor, guildAccess, resources] = await Promise.all([
|
|
686
|
+
this.resources.resolveActor(input.guildId, input.actorUserId),
|
|
687
|
+
this.#permissions.getGuildAccessPolicy?.(input.guildId) ??
|
|
688
|
+
Promise.resolve(DEFAULT_GUILD_ACCESS_POLICY),
|
|
689
|
+
this.resources.getGuildResources(input.guildId),
|
|
690
|
+
]);
|
|
691
|
+
const rolePositions = Object.fromEntries(resources.roles.map((role) => [role.id, role.position]));
|
|
692
|
+
const requestedModulePolicy = fromDashboardPolicy(input.state.module, currentModule.policy);
|
|
693
|
+
if (!permissionPoliciesEqual(currentModule.policy, requestedModulePolicy))
|
|
694
|
+
assertPermissionMutationAllowed(canMutatePermissionPolicy(actor, currentModule.policy, requestedModulePolicy, guildAccess, rolePositions));
|
|
695
|
+
for (const command of commands) {
|
|
696
|
+
const requested = supplied.get(command.id);
|
|
697
|
+
const current = currentCommands.get(command.id);
|
|
698
|
+
const target = requested.enabled
|
|
699
|
+
? fromDashboardPolicy(requested.policy, current.policy)
|
|
700
|
+
: requestedModulePolicy;
|
|
701
|
+
const enabledChanged = requested.enabled === current.inherited;
|
|
702
|
+
if (enabledChanged || !permissionPoliciesEqual(current.policy, target))
|
|
703
|
+
assertPermissionMutationAllowed(canMutatePermissionPolicy(actor, current.policy, target, guildAccess, rolePositions));
|
|
704
|
+
}
|
|
586
705
|
await withTransaction(this.database, async (connection) => {
|
|
587
706
|
const policies = new PermissionPolicyRepository(connection);
|
|
588
707
|
const audit = new AuditRepository(connection);
|
|
@@ -765,6 +884,8 @@ export class BotControlService {
|
|
|
765
884
|
return normalizeManagedResourceRelationPage(managedResourceRelationPageSchema.parse(await resource.listRelation(context, input.resourceId, input.relationId, {
|
|
766
885
|
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
|
|
767
886
|
limit: input.limit,
|
|
887
|
+
...(input.search === undefined ? {} : { search: input.search }),
|
|
888
|
+
...(input.filters === undefined ? {} : { filters: input.filters }),
|
|
768
889
|
})), relation.columns.map(({ key }) => key));
|
|
769
890
|
}
|
|
770
891
|
async previewManagedResource(input) {
|
|
@@ -877,7 +998,8 @@ export class BotControlService {
|
|
|
877
998
|
async #authorizeAction(guildId, module, actorUserId, permissionId) {
|
|
878
999
|
const actor = await this.resources.resolveActor(guildId, actorUserId);
|
|
879
1000
|
const policy = await this.#permissions.getEffectivePolicy(guildId, module.manifest.id, permissionId);
|
|
880
|
-
|
|
1001
|
+
const guildAccess = await this.#permissions.getGuildAccessPolicy?.(guildId);
|
|
1002
|
+
if (!canUseCommand(actor, policy, guildAccess))
|
|
881
1003
|
throw new BotControlForbiddenError();
|
|
882
1004
|
return actor;
|
|
883
1005
|
}
|
|
@@ -1139,6 +1261,35 @@ export async function createBotControlServer(input) {
|
|
|
1139
1261
|
const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
|
|
1140
1262
|
return input.service.getGuildState(guildId);
|
|
1141
1263
|
});
|
|
1264
|
+
server.post("/v1/guilds/:guildId/dashboard-access", async (request) => {
|
|
1265
|
+
const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
|
|
1266
|
+
const { actorUserId } = z
|
|
1267
|
+
.object({ actorUserId: snowflake })
|
|
1268
|
+
.strict()
|
|
1269
|
+
.parse(request.body);
|
|
1270
|
+
return {
|
|
1271
|
+
allowed: await input.service.canAccessDashboard({
|
|
1272
|
+
guildId,
|
|
1273
|
+
actorUserId,
|
|
1274
|
+
}),
|
|
1275
|
+
};
|
|
1276
|
+
});
|
|
1277
|
+
server.get("/v1/guilds/:guildId/access-policy", async (request) => {
|
|
1278
|
+
const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
|
|
1279
|
+
const { actorUserId } = z
|
|
1280
|
+
.object({ actorUserId: snowflake })
|
|
1281
|
+
.strict()
|
|
1282
|
+
.parse(request.query);
|
|
1283
|
+
return input.service.getGuildAccessPolicy({ guildId, actorUserId });
|
|
1284
|
+
});
|
|
1285
|
+
server.put("/v1/guilds/:guildId/access-policy", async (request) => {
|
|
1286
|
+
const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
|
|
1287
|
+
const body = mutationContext
|
|
1288
|
+
.extend({ version: z.number().int().min(0), policy: guildAccessPolicy })
|
|
1289
|
+
.strict()
|
|
1290
|
+
.parse(request.body);
|
|
1291
|
+
return input.service.updateGuildAccessPolicy({ guildId, ...body });
|
|
1292
|
+
});
|
|
1142
1293
|
server.get("/v1/guilds/:guildId/modules/:moduleId/public-projection", async (request) => {
|
|
1143
1294
|
const params = z
|
|
1144
1295
|
.object({ guildId: snowflake, moduleId })
|
|
@@ -1272,6 +1423,21 @@ export async function createBotControlServer(input) {
|
|
|
1272
1423
|
.object({
|
|
1273
1424
|
cursor: resourceCursor.optional(),
|
|
1274
1425
|
limit: z.coerce.number().int().min(1).max(50).default(50),
|
|
1426
|
+
search: z.string().trim().min(1).max(200).optional(),
|
|
1427
|
+
filters: z
|
|
1428
|
+
.preprocess((value) => {
|
|
1429
|
+
if (typeof value !== "string")
|
|
1430
|
+
return value;
|
|
1431
|
+
if (value.length > 2_048)
|
|
1432
|
+
return value;
|
|
1433
|
+
try {
|
|
1434
|
+
return JSON.parse(value);
|
|
1435
|
+
}
|
|
1436
|
+
catch {
|
|
1437
|
+
return value;
|
|
1438
|
+
}
|
|
1439
|
+
}, managedResourceFilters)
|
|
1440
|
+
.optional(),
|
|
1275
1441
|
actorUserId: snowflake,
|
|
1276
1442
|
correlationId: z.uuid(),
|
|
1277
1443
|
})
|
|
@@ -1281,6 +1447,8 @@ export async function createBotControlServer(input) {
|
|
|
1281
1447
|
...params,
|
|
1282
1448
|
...(query.cursor === undefined ? {} : { cursor: query.cursor }),
|
|
1283
1449
|
limit: query.limit,
|
|
1450
|
+
...(query.search === undefined ? {} : { search: query.search }),
|
|
1451
|
+
...(query.filters === undefined ? {} : { filters: query.filters }),
|
|
1284
1452
|
actorUserId: query.actorUserId,
|
|
1285
1453
|
correlationId: query.correlationId,
|
|
1286
1454
|
});
|
|
@@ -1668,6 +1836,9 @@ function mapDashboardModule(module) {
|
|
|
1668
1836
|
actions: (dashboard.actions ?? []).map((action) => ({ ...action })),
|
|
1669
1837
|
achievementTriggers: (module.manifest.contributions?.achievementTriggers ?? []).map((trigger) => structuredClone(trigger)),
|
|
1670
1838
|
resources: (dashboard.resources ?? []).map((resource) => structuredClone(resource)),
|
|
1839
|
+
workspace: dashboard.workspace
|
|
1840
|
+
? structuredClone(dashboard.workspace)
|
|
1841
|
+
: undefined,
|
|
1671
1842
|
};
|
|
1672
1843
|
}
|
|
1673
1844
|
function dashboardCommands(module) {
|
|
@@ -1702,14 +1873,18 @@ function dashboardCommands(module) {
|
|
|
1702
1873
|
function toDashboardPolicy(policy) {
|
|
1703
1874
|
const preset = policy.everyone
|
|
1704
1875
|
? "everyone"
|
|
1705
|
-
: policy.
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
? "
|
|
1712
|
-
: "
|
|
1876
|
+
: policy.accessLevel === "administrator"
|
|
1877
|
+
? "administrators"
|
|
1878
|
+
: policy.accessLevel === "moderator"
|
|
1879
|
+
? "moderators"
|
|
1880
|
+
: policy.allowedRoleIds.length > 0 &&
|
|
1881
|
+
policy.requiredPermissions.length === 0
|
|
1882
|
+
? "custom"
|
|
1883
|
+
: policy.requiredPermissions.includes("ADMINISTRATOR")
|
|
1884
|
+
? "administrators"
|
|
1885
|
+
: policy.requiredPermissions.includes("MANAGE_GUILD")
|
|
1886
|
+
? "moderators"
|
|
1887
|
+
: "custom";
|
|
1713
1888
|
return {
|
|
1714
1889
|
preset,
|
|
1715
1890
|
allowedRoleIds: [...policy.allowedRoleIds],
|
|
@@ -1719,11 +1894,12 @@ function toDashboardPolicy(policy) {
|
|
|
1719
1894
|
function fromDashboardPolicy(input, current) {
|
|
1720
1895
|
return {
|
|
1721
1896
|
everyone: input.preset === "everyone",
|
|
1722
|
-
|
|
1723
|
-
?
|
|
1897
|
+
...(input.preset === "administrators"
|
|
1898
|
+
? { accessLevel: "administrator" }
|
|
1724
1899
|
: input.preset === "moderators"
|
|
1725
|
-
?
|
|
1726
|
-
:
|
|
1900
|
+
? { accessLevel: "moderator" }
|
|
1901
|
+
: {}),
|
|
1902
|
+
requiredPermissions: [],
|
|
1727
1903
|
allowedRoleIds: input.preset === "custom" ? input.allowedRoleIds : [],
|
|
1728
1904
|
deniedRoleIds: input.deniedRoleIds,
|
|
1729
1905
|
allowedUserIds: [...current.allowedUserIds],
|
|
@@ -1740,6 +1916,24 @@ function emptyPolicy() {
|
|
|
1740
1916
|
deniedUserIds: [],
|
|
1741
1917
|
};
|
|
1742
1918
|
}
|
|
1919
|
+
function permissionPoliciesEqual(left, right) {
|
|
1920
|
+
return (left.everyone === right.everyone &&
|
|
1921
|
+
left.accessLevel === right.accessLevel &&
|
|
1922
|
+
sameStrings(left.requiredPermissions, right.requiredPermissions) &&
|
|
1923
|
+
sameStrings(left.allowedRoleIds, right.allowedRoleIds) &&
|
|
1924
|
+
sameStrings(left.deniedRoleIds, right.deniedRoleIds) &&
|
|
1925
|
+
sameStrings(left.allowedUserIds, right.allowedUserIds) &&
|
|
1926
|
+
sameStrings(left.deniedUserIds, right.deniedUserIds));
|
|
1927
|
+
}
|
|
1928
|
+
function sameStrings(left, right) {
|
|
1929
|
+
return (left.length === right.length &&
|
|
1930
|
+
left.every((value, index) => value === right[index]));
|
|
1931
|
+
}
|
|
1932
|
+
function assertPermissionMutationAllowed(decision) {
|
|
1933
|
+
if (decision.allowed)
|
|
1934
|
+
return;
|
|
1935
|
+
throw new BotControlForbiddenError(permissionMutationDenialMessage(decision.reason));
|
|
1936
|
+
}
|
|
1743
1937
|
function safeVersion(version) {
|
|
1744
1938
|
const value = Number(version);
|
|
1745
1939
|
if (!Number.isSafeInteger(value) || value < 1)
|
package/dist/interactions.js
CHANGED
|
@@ -372,6 +372,9 @@ function createContext(interaction, services) {
|
|
|
372
372
|
userRoleIds: interaction.member instanceof GuildMember
|
|
373
373
|
? [...interaction.member.roles.cache.keys()]
|
|
374
374
|
: (interaction.member?.roles ?? []),
|
|
375
|
+
...(interaction.member instanceof GuildMember
|
|
376
|
+
? { userHighestRolePosition: interaction.member.roles.highest.position }
|
|
377
|
+
: {}),
|
|
375
378
|
userPermissions: interaction.memberPermissions
|
|
376
379
|
?.toArray()
|
|
377
380
|
.map(normalizeDiscordPermission) ?? [],
|
|
@@ -491,6 +494,9 @@ function createModalContext(interaction, services) {
|
|
|
491
494
|
userRoleIds: interaction.member instanceof GuildMember
|
|
492
495
|
? [...interaction.member.roles.cache.keys()]
|
|
493
496
|
: (interaction.member?.roles ?? []),
|
|
497
|
+
...(interaction.member instanceof GuildMember
|
|
498
|
+
? { userHighestRolePosition: interaction.member.roles.highest.position }
|
|
499
|
+
: {}),
|
|
494
500
|
userPermissions: interaction.memberPermissions
|
|
495
501
|
?.toArray()
|
|
496
502
|
.map(normalizeDiscordPermission) ?? [],
|
|
@@ -525,6 +531,12 @@ function createModalContext(interaction, services) {
|
|
|
525
531
|
roleIds: [...(selected?.roles.keys() ?? [])],
|
|
526
532
|
};
|
|
527
533
|
},
|
|
534
|
+
getSelectedRolePositions: (customId) => {
|
|
535
|
+
const roles = interaction.fields.getSelectedRoles(customId, false);
|
|
536
|
+
return Object.fromEntries([...(roles?.values() ?? [])]
|
|
537
|
+
.filter((role) => role !== null)
|
|
538
|
+
.map((role) => [role.id, role.position]));
|
|
539
|
+
},
|
|
528
540
|
getSelectedChannelIds: (customId) => [
|
|
529
541
|
...(interaction.fields.getSelectedChannels(customId, false)?.keys() ??
|
|
530
542
|
[]),
|
|
@@ -624,7 +636,8 @@ export async function authorizeCommand(interaction, moduleId, commandId, rateLim
|
|
|
624
636
|
const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
625
637
|
const policy = await permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId);
|
|
626
638
|
const actor = permissionActor(interaction);
|
|
627
|
-
|
|
639
|
+
const guildAccess = await permissions.getGuildAccessPolicy?.(interaction.guildId);
|
|
640
|
+
if (!canUseCommand(actor, policy, guildAccess)) {
|
|
628
641
|
await interaction.reply({
|
|
629
642
|
content: "You do not have permission to use this command.",
|
|
630
643
|
flags: MessageFlags.Ephemeral,
|
|
@@ -662,7 +675,8 @@ async function authorizeAutocomplete(interaction, moduleId, commandId, services,
|
|
|
662
675
|
return true;
|
|
663
676
|
const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
664
677
|
const policy = await permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId);
|
|
665
|
-
|
|
678
|
+
const guildAccess = await permissions.getGuildAccessPolicy?.(interaction.guildId);
|
|
679
|
+
return canUseCommand(permissionActor(interaction), policy, guildAccess);
|
|
666
680
|
}
|
|
667
681
|
function permissionActor(interaction) {
|
|
668
682
|
return {
|
|
@@ -676,6 +690,9 @@ function permissionActor(interaction) {
|
|
|
676
690
|
isServerOwner: interaction.guild?.ownerId === interaction.user.id,
|
|
677
691
|
isAdministrator: interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ??
|
|
678
692
|
false,
|
|
693
|
+
...(interaction.member instanceof GuildMember
|
|
694
|
+
? { highestRolePosition: interaction.member.roles.highest.position }
|
|
695
|
+
: {}),
|
|
679
696
|
};
|
|
680
697
|
}
|
|
681
698
|
export function normalizeDiscordPermission(permission) {
|
|
@@ -824,100 +841,116 @@ export function toModal(definition) {
|
|
|
824
841
|
const modal = new ModalBuilder()
|
|
825
842
|
.setCustomId(definition.customId)
|
|
826
843
|
.setTitle(definition.title);
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
if (field.maxLength !== undefined)
|
|
844
|
-
input.setMaxLength(field.maxLength);
|
|
845
|
-
if (field.placeholder !== undefined)
|
|
846
|
-
input.setPlaceholder(field.placeholder);
|
|
847
|
-
if (field.value !== undefined)
|
|
848
|
-
input.setValue(field.value);
|
|
849
|
-
label.setTextInputComponent(input);
|
|
850
|
-
}
|
|
851
|
-
else if (field.type === "string-select") {
|
|
852
|
-
const input = new StringSelectMenuBuilder()
|
|
853
|
-
.setCustomId(field.customId)
|
|
854
|
-
.setRequired(field.required ?? false)
|
|
855
|
-
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
856
|
-
.setMaxValues(field.maxValues ?? 1)
|
|
857
|
-
.addOptions(field.options.map((option) => ({ ...option })));
|
|
858
|
-
if (field.placeholder !== undefined)
|
|
859
|
-
input.setPlaceholder(field.placeholder);
|
|
860
|
-
label.setStringSelectMenuComponent(input);
|
|
861
|
-
}
|
|
862
|
-
else if (field.type === "discord-user") {
|
|
863
|
-
const input = new UserSelectMenuBuilder()
|
|
864
|
-
.setCustomId(field.customId)
|
|
865
|
-
.setRequired(field.required ?? false)
|
|
866
|
-
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
867
|
-
.setMaxValues(field.maxValues ?? 1);
|
|
868
|
-
if (field.placeholder !== undefined)
|
|
869
|
-
input.setPlaceholder(field.placeholder);
|
|
870
|
-
if (field.defaultValues?.length)
|
|
871
|
-
input.setDefaultUsers(...field.defaultValues);
|
|
872
|
-
label.setUserSelectMenuComponent(input);
|
|
873
|
-
}
|
|
874
|
-
else if (field.type === "discord-role") {
|
|
875
|
-
const input = new RoleSelectMenuBuilder()
|
|
876
|
-
.setCustomId(field.customId)
|
|
877
|
-
.setRequired(field.required ?? false)
|
|
878
|
-
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
879
|
-
.setMaxValues(field.maxValues ?? 1);
|
|
880
|
-
if (field.placeholder !== undefined)
|
|
881
|
-
input.setPlaceholder(field.placeholder);
|
|
882
|
-
if (field.defaultValues?.length)
|
|
883
|
-
input.setDefaultRoles(...field.defaultValues);
|
|
884
|
-
label.setRoleSelectMenuComponent(input);
|
|
885
|
-
}
|
|
886
|
-
else if (field.type === "discord-mentionable") {
|
|
887
|
-
const input = new MentionableSelectMenuBuilder()
|
|
888
|
-
.setCustomId(field.customId)
|
|
889
|
-
.setRequired(field.required ?? false)
|
|
890
|
-
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
891
|
-
.setMaxValues(field.maxValues ?? 1);
|
|
892
|
-
if (field.placeholder !== undefined)
|
|
893
|
-
input.setPlaceholder(field.placeholder);
|
|
894
|
-
if (field.defaultValues?.length) {
|
|
895
|
-
input.addDefaultUsers(...field.defaultValues
|
|
896
|
-
.filter((value) => value.type === "user")
|
|
897
|
-
.map((value) => value.id));
|
|
898
|
-
input.addDefaultRoles(...field.defaultValues
|
|
899
|
-
.filter((value) => value.type === "role")
|
|
900
|
-
.map((value) => value.id));
|
|
901
|
-
}
|
|
902
|
-
label.setMentionableSelectMenuComponent(input);
|
|
903
|
-
}
|
|
904
|
-
else if (field.type === "discord-channel") {
|
|
905
|
-
const input = new ChannelSelectMenuBuilder()
|
|
906
|
-
.setCustomId(field.customId)
|
|
907
|
-
.setRequired(field.required ?? false)
|
|
908
|
-
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
909
|
-
.setMaxValues(field.maxValues ?? 1)
|
|
910
|
-
.setChannelTypes(...field.channelTypes.map(discordChannelType));
|
|
911
|
-
if (field.placeholder !== undefined)
|
|
912
|
-
input.setPlaceholder(field.placeholder);
|
|
913
|
-
if (field.defaultValues?.length)
|
|
914
|
-
input.setDefaultChannels(...field.defaultValues);
|
|
915
|
-
label.setChannelSelectMenuComponent(input);
|
|
844
|
+
if (definition.items !== undefined &&
|
|
845
|
+
((definition.text?.length ?? 0) > 0 || definition.fields.length > 0))
|
|
846
|
+
throw new Error("Ordered modal items cannot be combined with legacy text or fields");
|
|
847
|
+
const items = definition.items ?? [
|
|
848
|
+
...(definition.text ?? []).map((text) => ({
|
|
849
|
+
type: "text-display",
|
|
850
|
+
...text,
|
|
851
|
+
})),
|
|
852
|
+
...definition.fields,
|
|
853
|
+
];
|
|
854
|
+
if (items.length > 5)
|
|
855
|
+
throw new Error("Discord modals support at most five components");
|
|
856
|
+
for (const item of items) {
|
|
857
|
+
if (item.type !== "text-display") {
|
|
858
|
+
modal.addLabelComponents(toModalLabel(item));
|
|
859
|
+
continue;
|
|
916
860
|
}
|
|
917
|
-
modal.
|
|
861
|
+
modal.addTextDisplayComponents(new TextDisplayBuilder().setContent(item.markdown === false ? escapeMarkdown(item.content) : item.content));
|
|
918
862
|
}
|
|
919
863
|
return modal;
|
|
920
864
|
}
|
|
865
|
+
function toModalLabel(field) {
|
|
866
|
+
const label = new LabelBuilder().setLabel(field.label);
|
|
867
|
+
if (field.description !== undefined)
|
|
868
|
+
label.setDescription(field.description);
|
|
869
|
+
if (field.type === undefined || field.type === "text") {
|
|
870
|
+
const input = new TextInputBuilder()
|
|
871
|
+
.setCustomId(field.customId)
|
|
872
|
+
.setStyle(field.style === "paragraph"
|
|
873
|
+
? TextInputStyle.Paragraph
|
|
874
|
+
: TextInputStyle.Short)
|
|
875
|
+
.setRequired(field.required ?? false);
|
|
876
|
+
if (field.minLength !== undefined)
|
|
877
|
+
input.setMinLength(field.minLength);
|
|
878
|
+
if (field.maxLength !== undefined)
|
|
879
|
+
input.setMaxLength(field.maxLength);
|
|
880
|
+
if (field.placeholder !== undefined)
|
|
881
|
+
input.setPlaceholder(field.placeholder);
|
|
882
|
+
if (field.value !== undefined)
|
|
883
|
+
input.setValue(field.value);
|
|
884
|
+
label.setTextInputComponent(input);
|
|
885
|
+
}
|
|
886
|
+
else if (field.type === "string-select") {
|
|
887
|
+
const input = new StringSelectMenuBuilder()
|
|
888
|
+
.setCustomId(field.customId)
|
|
889
|
+
.setRequired(field.required ?? false)
|
|
890
|
+
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
891
|
+
.setMaxValues(field.maxValues ?? 1)
|
|
892
|
+
.addOptions(field.options.map((option) => ({ ...option })));
|
|
893
|
+
if (field.placeholder !== undefined)
|
|
894
|
+
input.setPlaceholder(field.placeholder);
|
|
895
|
+
label.setStringSelectMenuComponent(input);
|
|
896
|
+
}
|
|
897
|
+
else if (field.type === "discord-user") {
|
|
898
|
+
const input = new UserSelectMenuBuilder()
|
|
899
|
+
.setCustomId(field.customId)
|
|
900
|
+
.setRequired(field.required ?? false)
|
|
901
|
+
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
902
|
+
.setMaxValues(field.maxValues ?? 1);
|
|
903
|
+
if (field.placeholder !== undefined)
|
|
904
|
+
input.setPlaceholder(field.placeholder);
|
|
905
|
+
if (field.defaultValues?.length)
|
|
906
|
+
input.setDefaultUsers(...field.defaultValues);
|
|
907
|
+
label.setUserSelectMenuComponent(input);
|
|
908
|
+
}
|
|
909
|
+
else if (field.type === "discord-role") {
|
|
910
|
+
const input = new RoleSelectMenuBuilder()
|
|
911
|
+
.setCustomId(field.customId)
|
|
912
|
+
.setRequired(field.required ?? false)
|
|
913
|
+
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
914
|
+
.setMaxValues(field.maxValues ?? 1);
|
|
915
|
+
if (field.placeholder !== undefined)
|
|
916
|
+
input.setPlaceholder(field.placeholder);
|
|
917
|
+
if (field.defaultValues?.length)
|
|
918
|
+
input.setDefaultRoles(...field.defaultValues);
|
|
919
|
+
label.setRoleSelectMenuComponent(input);
|
|
920
|
+
}
|
|
921
|
+
else if (field.type === "discord-mentionable") {
|
|
922
|
+
const input = new MentionableSelectMenuBuilder()
|
|
923
|
+
.setCustomId(field.customId)
|
|
924
|
+
.setRequired(field.required ?? false)
|
|
925
|
+
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
926
|
+
.setMaxValues(field.maxValues ?? 1);
|
|
927
|
+
if (field.placeholder !== undefined)
|
|
928
|
+
input.setPlaceholder(field.placeholder);
|
|
929
|
+
if (field.defaultValues?.length) {
|
|
930
|
+
input.addDefaultUsers(...field.defaultValues
|
|
931
|
+
.filter((value) => value.type === "user")
|
|
932
|
+
.map((value) => value.id));
|
|
933
|
+
input.addDefaultRoles(...field.defaultValues
|
|
934
|
+
.filter((value) => value.type === "role")
|
|
935
|
+
.map((value) => value.id));
|
|
936
|
+
}
|
|
937
|
+
label.setMentionableSelectMenuComponent(input);
|
|
938
|
+
}
|
|
939
|
+
else if (field.type === "discord-channel") {
|
|
940
|
+
const input = new ChannelSelectMenuBuilder()
|
|
941
|
+
.setCustomId(field.customId)
|
|
942
|
+
.setRequired(field.required ?? false)
|
|
943
|
+
.setMinValues(field.minValues ?? (field.required ? 1 : 0))
|
|
944
|
+
.setMaxValues(field.maxValues ?? 1)
|
|
945
|
+
.setChannelTypes(...field.channelTypes.map(discordChannelType));
|
|
946
|
+
if (field.placeholder !== undefined)
|
|
947
|
+
input.setPlaceholder(field.placeholder);
|
|
948
|
+
if (field.defaultValues?.length)
|
|
949
|
+
input.setDefaultChannels(...field.defaultValues);
|
|
950
|
+
label.setChannelSelectMenuComponent(input);
|
|
951
|
+
}
|
|
952
|
+
return label;
|
|
953
|
+
}
|
|
921
954
|
function discordChannelType(type) {
|
|
922
955
|
if (type === "guild_announcement")
|
|
923
956
|
return ChannelType.GuildAnnouncement;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineModule, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
2
2
|
import { PermissionFlagsBits } from "discord.js";
|
|
3
|
+
import { canMutatePermissionPolicy, permissionMutationDenialMessage, } from "@helyx/core";
|
|
3
4
|
const MODULE_SELECT_ID = "helyx.permissions.module";
|
|
4
5
|
const TARGET_SELECT_ID = "helyx.permissions.target";
|
|
5
6
|
const MODULE_PREVIOUS_ID = "helyx.permissions.module.previous";
|
|
@@ -243,9 +244,32 @@ export function createPermissionsModule(configurableModules) {
|
|
|
243
244
|
allowedUserIds: [...allowed.userIds],
|
|
244
245
|
deniedUserIds: [...denied.userIds],
|
|
245
246
|
};
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
const permissionService = context.services.get(HELYX_SERVICE_NAMES.permissions);
|
|
248
|
+
const current = await permissionService.getPolicy(context.serverId, moduleId, commandId);
|
|
249
|
+
const guildAccess = (await permissionService.getGuildAccessPolicy?.(context.serverId)) ?? {
|
|
250
|
+
administratorRoleIds: [],
|
|
251
|
+
moderatorRoleIds: [],
|
|
252
|
+
dashboardModeratorsEnabled: false,
|
|
253
|
+
dashboardAllowedRoleIds: [],
|
|
254
|
+
dashboardDeniedRoleIds: [],
|
|
255
|
+
};
|
|
256
|
+
const rolePositions = {
|
|
257
|
+
...(context.getSelectedRolePositions?.(ALLOWED_MENTIONABLES_FIELD_ID) ?? {}),
|
|
258
|
+
...(context.getSelectedRolePositions?.(DENIED_MENTIONABLES_FIELD_ID) ?? {}),
|
|
259
|
+
};
|
|
260
|
+
const decision = canMutatePermissionPolicy({
|
|
261
|
+
userId: context.userId,
|
|
262
|
+
roleIds: context.userRoleIds,
|
|
263
|
+
permissions: context.userPermissions ?? [],
|
|
264
|
+
isServerOwner: context.isServerOwner ?? false,
|
|
265
|
+
isAdministrator: context.isAdministrator,
|
|
266
|
+
...(context.userHighestRolePosition === undefined
|
|
267
|
+
? {}
|
|
268
|
+
: { highestRolePosition: context.userHighestRolePosition }),
|
|
269
|
+
}, current.policy, policy, guildAccess, rolePositions);
|
|
270
|
+
if (!decision.allowed)
|
|
271
|
+
throw new Error(permissionMutationDenialMessage(decision.reason));
|
|
272
|
+
await permissionService.setPolicy({
|
|
249
273
|
guildId: context.serverId,
|
|
250
274
|
moduleId,
|
|
251
275
|
...(commandId ? { commandId } : {}),
|
|
@@ -326,7 +350,8 @@ export function createPermissionsModule(configurableModules) {
|
|
|
326
350
|
function restrictedPolicy() {
|
|
327
351
|
return {
|
|
328
352
|
everyone: false,
|
|
329
|
-
|
|
353
|
+
accessLevel: "moderator",
|
|
354
|
+
requiredPermissions: [],
|
|
330
355
|
allowedRoleIds: [],
|
|
331
356
|
deniedRoleIds: [],
|
|
332
357
|
allowedUserIds: [],
|
|
@@ -6,7 +6,8 @@ export class CoreResourceAuthorizationService {
|
|
|
6
6
|
}
|
|
7
7
|
async canUse(input) {
|
|
8
8
|
const policy = await this.#permissions.getEffectivePolicy(input.guildId, input.moduleId, input.permissionId);
|
|
9
|
-
|
|
9
|
+
const guildAccess = await this.#permissions.getGuildAccessPolicy?.(input.guildId);
|
|
10
|
+
return canUseCommand(input.actor, policy, guildAccess);
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
13
|
//# sourceMappingURL=resource-authorization.js.map
|
|
@@ -169,6 +169,27 @@ export type ShardHealthSnapshot = z.infer<typeof shardHealthSnapshotSchema>;
|
|
|
169
169
|
export declare const shardControlOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
170
170
|
kind: z.ZodLiteral<"guild.state.get">;
|
|
171
171
|
guildId: z.ZodString;
|
|
172
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
173
|
+
kind: z.ZodLiteral<"guild.dashboard.access">;
|
|
174
|
+
guildId: z.ZodString;
|
|
175
|
+
actorUserId: z.ZodString;
|
|
176
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
177
|
+
kind: z.ZodLiteral<"guild.access-policy.get">;
|
|
178
|
+
guildId: z.ZodString;
|
|
179
|
+
actorUserId: z.ZodString;
|
|
180
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
181
|
+
actorUserId: z.ZodString;
|
|
182
|
+
correlationId: z.ZodString;
|
|
183
|
+
kind: z.ZodLiteral<"guild.access-policy.update">;
|
|
184
|
+
guildId: z.ZodString;
|
|
185
|
+
version: z.ZodNumber;
|
|
186
|
+
policy: z.ZodObject<{
|
|
187
|
+
administratorRoleIds: z.ZodArray<z.ZodString>;
|
|
188
|
+
moderatorRoleIds: z.ZodArray<z.ZodString>;
|
|
189
|
+
dashboardModeratorsEnabled: z.ZodBoolean;
|
|
190
|
+
dashboardAllowedRoleIds: z.ZodArray<z.ZodString>;
|
|
191
|
+
dashboardDeniedRoleIds: z.ZodArray<z.ZodString>;
|
|
192
|
+
}, z.core.$strict>;
|
|
172
193
|
}, z.core.$strict>, z.ZodObject<{
|
|
173
194
|
kind: z.ZodLiteral<"guild.module.public.read">;
|
|
174
195
|
guildId: z.ZodString;
|
|
@@ -285,6 +306,8 @@ export declare const shardControlOperationSchema: z.ZodDiscriminatedUnion<[z.Zod
|
|
|
285
306
|
relationId: z.ZodString;
|
|
286
307
|
cursor: z.ZodOptional<z.ZodString>;
|
|
287
308
|
limit: z.ZodNumber;
|
|
309
|
+
search: z.ZodOptional<z.ZodString>;
|
|
310
|
+
filters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
288
311
|
}, z.core.$strict>, z.ZodObject<{
|
|
289
312
|
actorUserId: z.ZodString;
|
|
290
313
|
correlationId: z.ZodString;
|
|
@@ -485,6 +508,27 @@ export declare const shardControlRequestSchema: z.ZodObject<{
|
|
|
485
508
|
operation: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
486
509
|
kind: z.ZodLiteral<"guild.state.get">;
|
|
487
510
|
guildId: z.ZodString;
|
|
511
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
512
|
+
kind: z.ZodLiteral<"guild.dashboard.access">;
|
|
513
|
+
guildId: z.ZodString;
|
|
514
|
+
actorUserId: z.ZodString;
|
|
515
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
516
|
+
kind: z.ZodLiteral<"guild.access-policy.get">;
|
|
517
|
+
guildId: z.ZodString;
|
|
518
|
+
actorUserId: z.ZodString;
|
|
519
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
520
|
+
actorUserId: z.ZodString;
|
|
521
|
+
correlationId: z.ZodString;
|
|
522
|
+
kind: z.ZodLiteral<"guild.access-policy.update">;
|
|
523
|
+
guildId: z.ZodString;
|
|
524
|
+
version: z.ZodNumber;
|
|
525
|
+
policy: z.ZodObject<{
|
|
526
|
+
administratorRoleIds: z.ZodArray<z.ZodString>;
|
|
527
|
+
moderatorRoleIds: z.ZodArray<z.ZodString>;
|
|
528
|
+
dashboardModeratorsEnabled: z.ZodBoolean;
|
|
529
|
+
dashboardAllowedRoleIds: z.ZodArray<z.ZodString>;
|
|
530
|
+
dashboardDeniedRoleIds: z.ZodArray<z.ZodString>;
|
|
531
|
+
}, z.core.$strict>;
|
|
488
532
|
}, z.core.$strict>, z.ZodObject<{
|
|
489
533
|
kind: z.ZodLiteral<"guild.module.public.read">;
|
|
490
534
|
guildId: z.ZodString;
|
|
@@ -601,6 +645,8 @@ export declare const shardControlRequestSchema: z.ZodObject<{
|
|
|
601
645
|
relationId: z.ZodString;
|
|
602
646
|
cursor: z.ZodOptional<z.ZodString>;
|
|
603
647
|
limit: z.ZodNumber;
|
|
648
|
+
search: z.ZodOptional<z.ZodString>;
|
|
649
|
+
filters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
604
650
|
}, z.core.$strict>, z.ZodObject<{
|
|
605
651
|
actorUserId: z.ZodString;
|
|
606
652
|
correlationId: z.ZodString;
|
|
@@ -1306,6 +1352,27 @@ export declare const supervisorMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
|
|
|
1306
1352
|
operation: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1307
1353
|
kind: z.ZodLiteral<"guild.state.get">;
|
|
1308
1354
|
guildId: z.ZodString;
|
|
1355
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
1356
|
+
kind: z.ZodLiteral<"guild.dashboard.access">;
|
|
1357
|
+
guildId: z.ZodString;
|
|
1358
|
+
actorUserId: z.ZodString;
|
|
1359
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
1360
|
+
kind: z.ZodLiteral<"guild.access-policy.get">;
|
|
1361
|
+
guildId: z.ZodString;
|
|
1362
|
+
actorUserId: z.ZodString;
|
|
1363
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
1364
|
+
actorUserId: z.ZodString;
|
|
1365
|
+
correlationId: z.ZodString;
|
|
1366
|
+
kind: z.ZodLiteral<"guild.access-policy.update">;
|
|
1367
|
+
guildId: z.ZodString;
|
|
1368
|
+
version: z.ZodNumber;
|
|
1369
|
+
policy: z.ZodObject<{
|
|
1370
|
+
administratorRoleIds: z.ZodArray<z.ZodString>;
|
|
1371
|
+
moderatorRoleIds: z.ZodArray<z.ZodString>;
|
|
1372
|
+
dashboardModeratorsEnabled: z.ZodBoolean;
|
|
1373
|
+
dashboardAllowedRoleIds: z.ZodArray<z.ZodString>;
|
|
1374
|
+
dashboardDeniedRoleIds: z.ZodArray<z.ZodString>;
|
|
1375
|
+
}, z.core.$strict>;
|
|
1309
1376
|
}, z.core.$strict>, z.ZodObject<{
|
|
1310
1377
|
kind: z.ZodLiteral<"guild.module.public.read">;
|
|
1311
1378
|
guildId: z.ZodString;
|
|
@@ -1422,6 +1489,8 @@ export declare const supervisorMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
|
|
|
1422
1489
|
relationId: z.ZodString;
|
|
1423
1490
|
cursor: z.ZodOptional<z.ZodString>;
|
|
1424
1491
|
limit: z.ZodNumber;
|
|
1492
|
+
search: z.ZodOptional<z.ZodString>;
|
|
1493
|
+
filters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
1425
1494
|
}, z.core.$strict>, z.ZodObject<{
|
|
1426
1495
|
actorUserId: z.ZodString;
|
|
1427
1496
|
correlationId: z.ZodString;
|
package/dist/shard-contracts.js
CHANGED
|
@@ -46,6 +46,15 @@ const permissionState = z
|
|
|
46
46
|
.max(100),
|
|
47
47
|
})
|
|
48
48
|
.strict();
|
|
49
|
+
const guildAccessPolicy = z
|
|
50
|
+
.object({
|
|
51
|
+
administratorRoleIds: z.array(snowflake).max(25),
|
|
52
|
+
moderatorRoleIds: z.array(snowflake).max(25),
|
|
53
|
+
dashboardModeratorsEnabled: z.boolean(),
|
|
54
|
+
dashboardAllowedRoleIds: z.array(snowflake).max(25),
|
|
55
|
+
dashboardDeniedRoleIds: z.array(snowflake).max(25),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
49
58
|
const configurationValue = z.record(z.string().min(1).max(100), z.union([
|
|
50
59
|
z.null(),
|
|
51
60
|
z.string().max(4_000),
|
|
@@ -222,6 +231,29 @@ export const shardHealthSnapshotSchema = z
|
|
|
222
231
|
});
|
|
223
232
|
export const shardControlOperationSchema = z.discriminatedUnion("kind", [
|
|
224
233
|
z.object({ kind: z.literal("guild.state.get"), guildId: snowflake }).strict(),
|
|
234
|
+
z
|
|
235
|
+
.object({
|
|
236
|
+
kind: z.literal("guild.dashboard.access"),
|
|
237
|
+
guildId: snowflake,
|
|
238
|
+
actorUserId: snowflake,
|
|
239
|
+
})
|
|
240
|
+
.strict(),
|
|
241
|
+
z
|
|
242
|
+
.object({
|
|
243
|
+
kind: z.literal("guild.access-policy.get"),
|
|
244
|
+
guildId: snowflake,
|
|
245
|
+
actorUserId: snowflake,
|
|
246
|
+
})
|
|
247
|
+
.strict(),
|
|
248
|
+
z
|
|
249
|
+
.object({
|
|
250
|
+
kind: z.literal("guild.access-policy.update"),
|
|
251
|
+
guildId: snowflake,
|
|
252
|
+
version: z.number().int().min(0),
|
|
253
|
+
policy: guildAccessPolicy,
|
|
254
|
+
...mutationContext.shape,
|
|
255
|
+
})
|
|
256
|
+
.strict(),
|
|
225
257
|
z
|
|
226
258
|
.object({
|
|
227
259
|
kind: z.literal("guild.module.public.read"),
|
|
@@ -330,6 +362,8 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
|
|
|
330
362
|
relationId: resourceType,
|
|
331
363
|
cursor: resourceCursor.optional(),
|
|
332
364
|
limit: z.number().int().min(1).max(50),
|
|
365
|
+
search: z.string().trim().min(1).max(200).optional(),
|
|
366
|
+
filters: managedResourceFilters.optional(),
|
|
333
367
|
...mutationContext.shape,
|
|
334
368
|
})
|
|
335
369
|
.strict(),
|
|
@@ -911,6 +945,12 @@ export async function executeShardControlOperation(handler, operation) {
|
|
|
911
945
|
});
|
|
912
946
|
case "guild.state.get":
|
|
913
947
|
return handler.getGuildState(operation.guildId);
|
|
948
|
+
case "guild.dashboard.access":
|
|
949
|
+
return handler.canAccessDashboard(operation);
|
|
950
|
+
case "guild.access-policy.get":
|
|
951
|
+
return handler.getGuildAccessPolicy(operation);
|
|
952
|
+
case "guild.access-policy.update":
|
|
953
|
+
return handler.updateGuildAccessPolicy(operation);
|
|
914
954
|
case "guild.module.public.read":
|
|
915
955
|
return handler.readPublicModuleProjection({
|
|
916
956
|
guildId: operation.guildId,
|
|
@@ -1008,6 +1048,10 @@ export async function executeShardControlOperation(handler, operation) {
|
|
|
1008
1048
|
resourceId: operation.resourceId,
|
|
1009
1049
|
relationId: operation.relationId,
|
|
1010
1050
|
...(operation.cursor === undefined ? {} : { cursor: operation.cursor }),
|
|
1051
|
+
...(operation.search === undefined ? {} : { search: operation.search }),
|
|
1052
|
+
...(operation.filters === undefined
|
|
1053
|
+
? {}
|
|
1054
|
+
: { filters: operation.filters }),
|
|
1011
1055
|
limit: operation.limit,
|
|
1012
1056
|
actorUserId: operation.actorUserId,
|
|
1013
1057
|
correlationId: operation.correlationId,
|
|
@@ -137,8 +137,22 @@ export class RedisShardCoordinator {
|
|
|
137
137
|
const parsed = activeGenerationSchema.safeParse(parseJson(current ?? ""));
|
|
138
138
|
if (parsed.success) {
|
|
139
139
|
if (JSON.stringify(withoutDeploymentOrder(parsed.data)) !==
|
|
140
|
-
JSON.stringify(accepted))
|
|
141
|
-
|
|
140
|
+
JSON.stringify(accepted)) {
|
|
141
|
+
if (parsed.data.applicationId !== accepted.applicationId ||
|
|
142
|
+
parsed.data.environment !== accepted.environment ||
|
|
143
|
+
deploymentOrder <= parsed.data.deploymentOrder ||
|
|
144
|
+
!current)
|
|
145
|
+
throw new Error("Shard coordination namespace is owned by an incompatible generation");
|
|
146
|
+
const replaced = await compareAndReplace(this.#command, this.#generationKey(), current, serialized, GENERATION_TTL_MS);
|
|
147
|
+
if (replaced)
|
|
148
|
+
return deploymentOrder;
|
|
149
|
+
const winner = activeGenerationSchema.safeParse(parseJson((await this.#command.get(this.#generationKey())) ?? ""));
|
|
150
|
+
if (winner.success &&
|
|
151
|
+
JSON.stringify(withoutDeploymentOrder(winner.data)) ===
|
|
152
|
+
JSON.stringify(accepted))
|
|
153
|
+
return winner.data.deploymentOrder;
|
|
154
|
+
throw new Error("Shard coordination generation changed while it was being validated");
|
|
155
|
+
}
|
|
142
156
|
if (!current ||
|
|
143
157
|
!(await compareAndExpire(this.#command, this.#generationKey(), current, GENERATION_TTL_MS)))
|
|
144
158
|
throw new Error("Shard coordination generation changed while it was being validated");
|
package/dist/shard-router.d.ts
CHANGED
|
@@ -16,6 +16,9 @@ export declare class ShardControlRouter implements BotControlHandler {
|
|
|
16
16
|
notificationGuildId?: string;
|
|
17
17
|
});
|
|
18
18
|
getGuildState(guildId: string): ReturnType<BotControlHandler["getGuildState"]>;
|
|
19
|
+
canAccessDashboard(input: Parameters<BotControlHandler["canAccessDashboard"]>[0]): ReturnType<BotControlHandler["canAccessDashboard"]>;
|
|
20
|
+
getGuildAccessPolicy(input: Parameters<BotControlHandler["getGuildAccessPolicy"]>[0]): ReturnType<BotControlHandler["getGuildAccessPolicy"]>;
|
|
21
|
+
updateGuildAccessPolicy(input: Parameters<BotControlHandler["updateGuildAccessPolicy"]>[0]): ReturnType<BotControlHandler["updateGuildAccessPolicy"]>;
|
|
19
22
|
readPublicModuleProjection(input: Parameters<BotControlHandler["readPublicModuleProjection"]>[0]): ReturnType<BotControlHandler["readPublicModuleProjection"]>;
|
|
20
23
|
canAccessManagedResource(input: Parameters<BotControlHandler["canAccessManagedResource"]>[0]): ReturnType<BotControlHandler["canAccessManagedResource"]>;
|
|
21
24
|
listGuildPresence(input: Parameters<BotControlHandler["listGuildPresence"]>[0]): ReturnType<BotControlHandler["listGuildPresence"]>;
|
package/dist/shard-router.js
CHANGED
|
@@ -10,6 +10,25 @@ export class ShardControlRouter {
|
|
|
10
10
|
getGuildState(guildId) {
|
|
11
11
|
return this.#route({ kind: "guild.state.get", guildId });
|
|
12
12
|
}
|
|
13
|
+
canAccessDashboard(input) {
|
|
14
|
+
return this.#route({ kind: "guild.dashboard.access", ...input });
|
|
15
|
+
}
|
|
16
|
+
getGuildAccessPolicy(input) {
|
|
17
|
+
return this.#route({ kind: "guild.access-policy.get", ...input });
|
|
18
|
+
}
|
|
19
|
+
updateGuildAccessPolicy(input) {
|
|
20
|
+
return this.#route({
|
|
21
|
+
kind: "guild.access-policy.update",
|
|
22
|
+
...input,
|
|
23
|
+
policy: {
|
|
24
|
+
administratorRoleIds: [...input.policy.administratorRoleIds],
|
|
25
|
+
moderatorRoleIds: [...input.policy.moderatorRoleIds],
|
|
26
|
+
dashboardModeratorsEnabled: input.policy.dashboardModeratorsEnabled,
|
|
27
|
+
dashboardAllowedRoleIds: [...input.policy.dashboardAllowedRoleIds],
|
|
28
|
+
dashboardDeniedRoleIds: [...input.policy.dashboardDeniedRoleIds],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
13
32
|
readPublicModuleProjection(input) {
|
|
14
33
|
return this.#route({ kind: "guild.module.public.read", ...input });
|
|
15
34
|
}
|