@helyx/bot 1.5.2 → 1.5.4
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 +27 -0
- package/command-root-catalogue.json +452 -0
- package/dist/cached-message-deletion-authority.d.ts +27 -0
- package/dist/cached-message-deletion-authority.js +288 -0
- package/dist/channel-audit-configuration.d.ts +11 -1
- package/dist/channel-audit-configuration.js +28 -6
- package/dist/channel-audit-control.js +42 -2
- package/dist/channel-audit-permissions.js +76 -3
- package/dist/cloud-command-policy-sync.d.ts +26 -0
- package/dist/cloud-command-policy-sync.js +198 -0
- package/dist/cloud-connector.d.ts +6 -1
- package/dist/cloud-connector.js +224 -53
- package/dist/cloud-control-dispatcher.js +18 -0
- package/dist/command-policy-runtime.d.ts +23 -0
- package/dist/command-policy-runtime.js +53 -0
- package/dist/command-root-catalogue.d.ts +5 -0
- package/dist/command-root-catalogue.js +16 -0
- package/dist/control-error-mapping.d.ts +8 -0
- package/dist/control-error-mapping.js +60 -0
- package/dist/control-errors.d.ts +3 -1
- package/dist/control-errors.js +4 -1
- package/dist/control-health-routes.d.ts +10 -0
- package/dist/control-health-routes.js +28 -0
- package/dist/control-server-schemas.d.ts +7 -0
- package/dist/control-server-schemas.js +9 -0
- package/dist/control-server.d.ts +27 -6
- package/dist/control-server.js +103 -122
- package/dist/dashboard-feature-control.d.ts +2 -1
- package/dist/dashboard-feature-control.js +1 -0
- package/dist/dashboard-logging.d.ts +20 -0
- package/dist/dashboard-logging.js +39 -0
- package/dist/dashboard-module-projection.d.ts +1 -1
- package/dist/dashboard-module-projection.js +5 -2
- package/dist/dashboard-preview-import-routes.js +8 -0
- package/dist/database-bootstrap.d.ts +1 -1
- package/dist/database-bootstrap.js +4 -3
- package/dist/discord-channel-audit.js +1 -0
- package/dist/discord-user-identities.d.ts +14 -0
- package/dist/discord-user-identities.js +64 -0
- package/dist/emergency-deletion-dispatcher.d.ts +68 -0
- package/dist/emergency-deletion-dispatcher.js +502 -0
- package/dist/emergency-deletion-rest-transport.d.ts +24 -0
- package/dist/emergency-deletion-rest-transport.js +253 -0
- package/dist/events.d.ts +5 -0
- package/dist/events.js +53 -10
- package/dist/guarded-discord-dispatcher.d.ts +29 -0
- package/dist/guarded-discord-dispatcher.js +204 -0
- package/dist/guild-command-discord-client.d.ts +2 -0
- package/dist/guild-command-discord-client.js +7 -0
- package/dist/guild-command-projection.d.ts +4 -1
- package/dist/guild-command-projection.js +25 -0
- package/dist/guild-command-read-service.d.ts +10 -1
- package/dist/guild-command-read-service.js +37 -0
- package/dist/guild-command-worker.d.ts +19 -4
- package/dist/guild-command-worker.js +356 -26
- package/dist/guild-message-protection-author-roles.d.ts +7 -0
- package/dist/guild-message-protection-author-roles.js +93 -0
- package/dist/guild-message-protection-deletion.d.ts +21 -0
- package/dist/guild-message-protection-deletion.js +138 -0
- package/dist/guild-message-protection-delivery.d.ts +41 -0
- package/dist/guild-message-protection-delivery.js +223 -0
- package/dist/guild-message-update-metadata.d.ts +32 -0
- package/dist/guild-message-update-metadata.js +255 -0
- package/dist/guilds.d.ts +2 -1
- package/dist/guilds.js +11 -1
- package/dist/hosted-server-moderation.d.ts +2 -2
- package/dist/index.js +68 -4
- package/dist/interactions.d.ts +3 -1
- package/dist/interactions.js +77 -12
- package/dist/invite-rollup-maintenance.d.ts +9 -0
- package/dist/invite-rollup-maintenance.js +12 -0
- package/dist/managed-resource-authorization.d.ts +15 -0
- package/dist/managed-resource-authorization.js +37 -0
- package/dist/managed-resource-create-defaults.d.ts +21 -0
- package/dist/managed-resource-create-defaults.js +83 -0
- package/dist/managed-resource-query.d.ts +6 -0
- package/dist/managed-resource-query.js +15 -0
- package/dist/member-role-ids.d.ts +17 -0
- package/dist/member-role-ids.js +66 -0
- package/dist/message-protection-admission.d.ts +60 -0
- package/dist/message-protection-admission.js +279 -0
- package/dist/message-protection-cold-data.d.ts +42 -0
- package/dist/message-protection-cold-data.js +180 -0
- package/dist/message-protection-coordinator.d.ts +73 -0
- package/dist/message-protection-coordinator.js +622 -0
- package/dist/message-protection-settings.d.ts +40 -0
- package/dist/message-protection-settings.js +275 -0
- package/dist/message-protection-summary.d.ts +8 -0
- package/dist/message-protection-summary.js +98 -0
- package/dist/module-availability-control.js +20 -12
- package/dist/module-logging.d.ts +3 -2
- package/dist/module-logging.js +6 -1
- package/dist/modules.js +56 -1
- package/dist/mysql-runtime-persistence.d.ts +1 -0
- package/dist/mysql-runtime-persistence.js +4 -1
- package/dist/nexus-command-recovery-control-routes.d.ts +5 -0
- package/dist/nexus-command-recovery-control-routes.js +48 -0
- package/dist/nexus-command-references.d.ts +6 -0
- package/dist/nexus-command-references.js +33 -0
- package/dist/scheduled-task-runner.js +23 -4
- package/dist/shard-command-startup.d.ts +13 -0
- package/dist/shard-command-startup.js +27 -0
- package/dist/shard-contracts.d.ts +84 -0
- package/dist/shard-contracts.js +33 -0
- package/dist/shard-core-readiness.d.ts +5 -0
- package/dist/shard-core-readiness.js +19 -0
- package/dist/shard-router.d.ts +3 -0
- package/dist/shard-router.js +10 -1
- package/dist/shard-supervisor.d.ts +1 -1
- package/dist/shard-supervisor.js +124 -43
- package/dist/shard-worker.js +25 -15
- package/documentation/README.md +1 -0
- package/documentation/examples/.env.example +0 -45
- package/documentation/guides/self-hosting.md +54 -0
- package/package.json +18 -17
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { ChannelType, PermissionFlagsBits, PermissionsBitField, } from "discord.js";
|
|
2
|
+
const snowflake = /^\d{17,20}$/u;
|
|
3
|
+
const messageTypes = new Set([
|
|
4
|
+
ChannelType.GuildText,
|
|
5
|
+
ChannelType.GuildAnnouncement,
|
|
6
|
+
ChannelType.GuildVoice,
|
|
7
|
+
ChannelType.GuildStageVoice,
|
|
8
|
+
ChannelType.PublicThread,
|
|
9
|
+
ChannelType.AnnouncementThread,
|
|
10
|
+
ChannelType.PrivateThread,
|
|
11
|
+
]);
|
|
12
|
+
const maximumAssignedRoles = 1024; // Engineering bound, not a Discord product limit.
|
|
13
|
+
/** Called only by future Core exact-event capture. Cache membership alone is not event provenance. */
|
|
14
|
+
export function captureCachedMessageDeletionAuthority(options) {
|
|
15
|
+
let stopped = false, checking = false;
|
|
16
|
+
try {
|
|
17
|
+
const { client, expectedApplicationId, eligibility, isStopped } = options;
|
|
18
|
+
const source = { ...options.source };
|
|
19
|
+
const { channel } = source;
|
|
20
|
+
const scope = Object.freeze({
|
|
21
|
+
guildId: source.guildId,
|
|
22
|
+
moduleId: source.moduleId,
|
|
23
|
+
channelId: source.channelId,
|
|
24
|
+
messageId: source.messageId,
|
|
25
|
+
admissionToken: source.admissionToken,
|
|
26
|
+
});
|
|
27
|
+
if (!snowflake.test(expectedApplicationId) ||
|
|
28
|
+
![scope.guildId, scope.channelId, scope.messageId].every((id) => typeof id === "string" && snowflake.test(id)) ||
|
|
29
|
+
typeof scope.moduleId !== "string" ||
|
|
30
|
+
!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u.test(scope.moduleId) ||
|
|
31
|
+
scope.moduleId.length > 128 ||
|
|
32
|
+
typeof scope.admissionToken !== "string" ||
|
|
33
|
+
!/^[A-Za-z0-9._:-]{1,256}$/u.test(scope.admissionToken) ||
|
|
34
|
+
typeof isStopped !== "function" ||
|
|
35
|
+
typeof eligibility?.read !== "function" ||
|
|
36
|
+
typeof eligibility.isCurrent !== "function")
|
|
37
|
+
return undefined;
|
|
38
|
+
const readEligibility = eligibility.read, tokenCurrent = eligibility.isCurrent;
|
|
39
|
+
const hold = () => {
|
|
40
|
+
stopped = true;
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
const running = () => {
|
|
44
|
+
if (stopped)
|
|
45
|
+
return false;
|
|
46
|
+
try {
|
|
47
|
+
const external = isStopped();
|
|
48
|
+
return !stopped && external === false ? true : hold();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return hold();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const eligible = () => {
|
|
55
|
+
if (!running())
|
|
56
|
+
return false;
|
|
57
|
+
const observed = readEligibility(scope.guildId, scope.moduleId);
|
|
58
|
+
if (!running() ||
|
|
59
|
+
observed.state !== "enabled" ||
|
|
60
|
+
observed.generation !== scope.admissionToken)
|
|
61
|
+
return hold();
|
|
62
|
+
const current = tokenCurrent(scope.admissionToken);
|
|
63
|
+
return running() && current === true ? true : hold();
|
|
64
|
+
};
|
|
65
|
+
const ready = () => {
|
|
66
|
+
if (!running())
|
|
67
|
+
return false;
|
|
68
|
+
const current = client.isReady();
|
|
69
|
+
if (!running() || current !== true)
|
|
70
|
+
return hold();
|
|
71
|
+
const user = client.user, application = client.application;
|
|
72
|
+
return running() &&
|
|
73
|
+
user?.id === expectedApplicationId &&
|
|
74
|
+
application?.id === expectedApplicationId
|
|
75
|
+
? true
|
|
76
|
+
: hold();
|
|
77
|
+
};
|
|
78
|
+
if (!eligible() || !ready())
|
|
79
|
+
return undefined;
|
|
80
|
+
const guild = client.guilds.cache.get(scope.guildId);
|
|
81
|
+
if (!running() ||
|
|
82
|
+
!guild ||
|
|
83
|
+
guild.id !== scope.guildId ||
|
|
84
|
+
guild.client !== client ||
|
|
85
|
+
guild.available !== true ||
|
|
86
|
+
guild.channels.cache.get(scope.channelId) !== channel ||
|
|
87
|
+
channel.id !== scope.channelId ||
|
|
88
|
+
!supported(channel))
|
|
89
|
+
return undefined;
|
|
90
|
+
const bindings = captureBindings(guild, channel, client);
|
|
91
|
+
if (!running() || !bindings)
|
|
92
|
+
return undefined;
|
|
93
|
+
const authority = Object.freeze({
|
|
94
|
+
scope,
|
|
95
|
+
stop: () => {
|
|
96
|
+
stopped = true;
|
|
97
|
+
},
|
|
98
|
+
isCurrent: () => {
|
|
99
|
+
if (stopped)
|
|
100
|
+
return false;
|
|
101
|
+
if (checking)
|
|
102
|
+
return hold();
|
|
103
|
+
checking = true;
|
|
104
|
+
try {
|
|
105
|
+
if (!eligible() || !ready())
|
|
106
|
+
return hold();
|
|
107
|
+
if (client.guilds.cache.get(scope.guildId) !== guild ||
|
|
108
|
+
guild.id !== scope.guildId ||
|
|
109
|
+
guild.client !== client ||
|
|
110
|
+
guild.available !== true ||
|
|
111
|
+
!running())
|
|
112
|
+
return hold();
|
|
113
|
+
for (const binding of bindings) {
|
|
114
|
+
if (!running() || !sameBinding(guild, binding, client))
|
|
115
|
+
return hold();
|
|
116
|
+
}
|
|
117
|
+
if (!running() || !supported(channel))
|
|
118
|
+
return hold();
|
|
119
|
+
const member = guild.members.cache.get(expectedApplicationId);
|
|
120
|
+
if (!running() ||
|
|
121
|
+
!member ||
|
|
122
|
+
member.id !== expectedApplicationId ||
|
|
123
|
+
member.user.id !== expectedApplicationId ||
|
|
124
|
+
member.guild !== guild ||
|
|
125
|
+
member.client !== client ||
|
|
126
|
+
member.partial !== false)
|
|
127
|
+
return hold();
|
|
128
|
+
if (!running() || !completeRoles(guild, member, client, running))
|
|
129
|
+
return hold();
|
|
130
|
+
if (channel.type === ChannelType.PrivateThread) {
|
|
131
|
+
if (!running() || !channel.isThread())
|
|
132
|
+
return hold();
|
|
133
|
+
const joined = channel.members.cache.get(expectedApplicationId);
|
|
134
|
+
if (!running() ||
|
|
135
|
+
!joined ||
|
|
136
|
+
joined.id !== expectedApplicationId ||
|
|
137
|
+
joined.thread !== channel ||
|
|
138
|
+
joined.client !== client ||
|
|
139
|
+
joined.partial !== false ||
|
|
140
|
+
typeof joined.joinedTimestamp !== "number" ||
|
|
141
|
+
!Number.isFinite(joined.joinedTimestamp) ||
|
|
142
|
+
joined.joinedTimestamp < 0)
|
|
143
|
+
return hold();
|
|
144
|
+
}
|
|
145
|
+
if (!running())
|
|
146
|
+
return hold();
|
|
147
|
+
const permissions = channel.permissionsFor(member);
|
|
148
|
+
if (!running() || !(permissions instanceof PermissionsBitField))
|
|
149
|
+
return hold();
|
|
150
|
+
const allowed = permissions.has([
|
|
151
|
+
PermissionFlagsBits.ViewChannel,
|
|
152
|
+
PermissionFlagsBits.ManageMessages,
|
|
153
|
+
]);
|
|
154
|
+
if (!running() || allowed !== true)
|
|
155
|
+
return hold();
|
|
156
|
+
const administrator = permissions.has(PermissionFlagsBits.Administrator, false);
|
|
157
|
+
if (!running())
|
|
158
|
+
return hold();
|
|
159
|
+
const until = member.communicationDisabledUntilTimestamp;
|
|
160
|
+
if (!running() ||
|
|
161
|
+
(until !== null &&
|
|
162
|
+
(typeof until !== "number" ||
|
|
163
|
+
!Number.isFinite(until) ||
|
|
164
|
+
until < 0)))
|
|
165
|
+
return hold();
|
|
166
|
+
if (!administrator && until !== null) {
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
if (!running() || !Number.isFinite(now) || now < 0 || until > now)
|
|
169
|
+
return hold();
|
|
170
|
+
}
|
|
171
|
+
if (!eligible() || !running())
|
|
172
|
+
return hold();
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return hold();
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
checking = false;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
return authority.isCurrent() ? authority : undefined;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
stopped = true;
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function supported(channel) {
|
|
191
|
+
return (messageTypes.has(channel.type) &&
|
|
192
|
+
channel.partial === false &&
|
|
193
|
+
channel.isTextBased() &&
|
|
194
|
+
!channel.isDMBased());
|
|
195
|
+
}
|
|
196
|
+
function sameBinding(guild, binding, client) {
|
|
197
|
+
const channel = binding.channel;
|
|
198
|
+
return (channel.id === binding.id &&
|
|
199
|
+
guild.channels.cache.get(binding.id) === channel &&
|
|
200
|
+
channel.guild === guild &&
|
|
201
|
+
channel.guildId === guild.id &&
|
|
202
|
+
channel.client === client &&
|
|
203
|
+
channel.type === binding.type &&
|
|
204
|
+
channel.parentId === binding.parentId &&
|
|
205
|
+
channel.partial === false);
|
|
206
|
+
}
|
|
207
|
+
function captureBindings(guild, channel, client) {
|
|
208
|
+
const result = [];
|
|
209
|
+
const bind = (value) => {
|
|
210
|
+
const binding = Object.freeze({
|
|
211
|
+
channel: value,
|
|
212
|
+
id: value.id,
|
|
213
|
+
type: value.type,
|
|
214
|
+
parentId: value.parentId,
|
|
215
|
+
});
|
|
216
|
+
if (!(binding.parentId === null ||
|
|
217
|
+
(typeof binding.parentId === "string" &&
|
|
218
|
+
snowflake.test(binding.parentId))) ||
|
|
219
|
+
!sameBinding(guild, binding, client))
|
|
220
|
+
return false;
|
|
221
|
+
result.push(binding);
|
|
222
|
+
return true;
|
|
223
|
+
};
|
|
224
|
+
if (!bind(channel))
|
|
225
|
+
return undefined;
|
|
226
|
+
let categoryId = channel.parentId;
|
|
227
|
+
if (channel.isThread()) {
|
|
228
|
+
if (!categoryId)
|
|
229
|
+
return undefined;
|
|
230
|
+
const parent = guild.channels.cache.get(categoryId);
|
|
231
|
+
const allowed = channel.type === ChannelType.AnnouncementThread
|
|
232
|
+
? [ChannelType.GuildAnnouncement]
|
|
233
|
+
: channel.type === ChannelType.PrivateThread
|
|
234
|
+
? [ChannelType.GuildText]
|
|
235
|
+
: [
|
|
236
|
+
ChannelType.GuildText,
|
|
237
|
+
ChannelType.GuildForum,
|
|
238
|
+
ChannelType.GuildMedia,
|
|
239
|
+
];
|
|
240
|
+
if (!parent || !allowed.includes(parent.type) || !bind(parent))
|
|
241
|
+
return undefined;
|
|
242
|
+
categoryId = parent.parentId;
|
|
243
|
+
}
|
|
244
|
+
if (categoryId) {
|
|
245
|
+
const category = guild.channels.cache.get(categoryId);
|
|
246
|
+
if (!category ||
|
|
247
|
+
category.type !== ChannelType.GuildCategory ||
|
|
248
|
+
category.parentId !== null ||
|
|
249
|
+
!bind(category))
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
return Object.freeze(result);
|
|
253
|
+
}
|
|
254
|
+
function completeRoles(guild, member, client, running) {
|
|
255
|
+
const ids = Reflect.get(member, "_roles");
|
|
256
|
+
if (!running() || !Array.isArray(ids) || ids.length > maximumAssignedRoles)
|
|
257
|
+
return false;
|
|
258
|
+
const length = ids.length;
|
|
259
|
+
const roleIds = new Set();
|
|
260
|
+
for (let index = 0; index < length; index++) {
|
|
261
|
+
const id = ids[index];
|
|
262
|
+
if (!running() ||
|
|
263
|
+
typeof id !== "string" ||
|
|
264
|
+
!snowflake.test(id) ||
|
|
265
|
+
roleIds.has(id))
|
|
266
|
+
return false;
|
|
267
|
+
roleIds.add(id);
|
|
268
|
+
}
|
|
269
|
+
if (ids.length !== length)
|
|
270
|
+
return false;
|
|
271
|
+
roleIds.add(guild.id);
|
|
272
|
+
for (const id of roleIds) {
|
|
273
|
+
if (!running())
|
|
274
|
+
return false;
|
|
275
|
+
const role = guild.roles.cache.get(id);
|
|
276
|
+
if (!running() ||
|
|
277
|
+
!role ||
|
|
278
|
+
role.id !== id ||
|
|
279
|
+
role.guild !== guild ||
|
|
280
|
+
role.client !== client ||
|
|
281
|
+
!(role.permissions instanceof PermissionsBitField) ||
|
|
282
|
+
typeof role.permissions.bitfield !== "bigint" ||
|
|
283
|
+
role.permissions.bitfield < 0n)
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=cached-message-deletion-authority.js.map
|
|
@@ -3,5 +3,15 @@ import type { ChannelAuditConfiguration, ChannelAuditModuleId } from "@helyx/sdk
|
|
|
3
3
|
export declare function channelAuditConfiguration(moduleId: ChannelAuditModuleId, configuration: {
|
|
4
4
|
version: number;
|
|
5
5
|
value: Record<string, unknown>;
|
|
6
|
-
} | null
|
|
6
|
+
} | null, welcome?: {
|
|
7
|
+
status: "loaded";
|
|
8
|
+
version: number | null;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
roleId: string | null;
|
|
11
|
+
} | {
|
|
12
|
+
status: "unavailable";
|
|
13
|
+
version: null;
|
|
14
|
+
enabled: null;
|
|
15
|
+
roleId: null;
|
|
16
|
+
}): ChannelAuditConfiguration;
|
|
7
17
|
//# sourceMappingURL=channel-audit-configuration.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CHANNEL_AUDIT_LIMITS, channelAuditConfigurationSchema, } from "@helyx/control-protocol/channel-audit";
|
|
2
2
|
/** Deliberate allowlist: never return provider state, answers or the saved settings object. */
|
|
3
|
-
export function channelAuditConfiguration(moduleId, configuration) {
|
|
3
|
+
export function channelAuditConfiguration(moduleId, configuration, welcome) {
|
|
4
4
|
const value = configuration?.value ?? {};
|
|
5
5
|
const mappedFields = [];
|
|
6
6
|
const id = (field) => {
|
|
@@ -13,7 +13,7 @@ export function channelAuditConfiguration(moduleId, configuration) {
|
|
|
13
13
|
const entryChannelId = id(moduleId === "helyx.account-gate"
|
|
14
14
|
? "gateChannelId"
|
|
15
15
|
: "verificationChannelId");
|
|
16
|
-
const
|
|
16
|
+
const configuredAfterRoleId = id("verifiedRoleId");
|
|
17
17
|
const reviewChannelId = moduleId === "helyx.account-gate" ? id("reviewChannelId") : null;
|
|
18
18
|
const configuredRoles = moduleId === "helyx.account-gate" && Array.isArray(value.temporaryRoleIds)
|
|
19
19
|
? [
|
|
@@ -23,18 +23,40 @@ export function channelAuditConfiguration(moduleId, configuration) {
|
|
|
23
23
|
if (configuredRoles.length)
|
|
24
24
|
mappedFields.push("temporaryRoleIds");
|
|
25
25
|
const suggested = moduleId === "helyx.verification" ? id("roleToRemoveId") : null;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const configuredRemovedRoleIds = suggested ? [suggested] : configuredRoles;
|
|
27
|
+
// An existing invalid save can grant and remove the same role. Do not
|
|
28
|
+
// project that as a usable journey: require the owner to select the grant
|
|
29
|
+
// role again while preserving the configured removal evidence.
|
|
30
|
+
const afterRoleId = configuredRemovedRoleIds.includes(configuredAfterRoleId ?? "")
|
|
31
|
+
? null
|
|
32
|
+
: configuredAfterRoleId;
|
|
33
|
+
const welcomeRoleId = welcome?.status === "loaded" && welcome.enabled ? welcome.roleId : null;
|
|
34
|
+
const reserveWelcomeSlot = welcomeRoleId !== null &&
|
|
35
|
+
!configuredRemovedRoleIds
|
|
36
|
+
.slice(0, CHANNEL_AUDIT_LIMITS.beforeRoles)
|
|
37
|
+
.includes(welcomeRoleId);
|
|
38
|
+
const boundedRemovedRoleIds = configuredRemovedRoleIds.slice(0, CHANNEL_AUDIT_LIMITS.beforeRoles - (reserveWelcomeSlot ? 1 : 0));
|
|
39
|
+
const removedRoleIds = welcomeRoleId &&
|
|
40
|
+
configuredRemovedRoleIds.includes(welcomeRoleId) &&
|
|
41
|
+
!boundedRemovedRoleIds.includes(welcomeRoleId)
|
|
42
|
+
? [...boundedRemovedRoleIds, welcomeRoleId]
|
|
43
|
+
: boundedRemovedRoleIds;
|
|
44
|
+
const beforeRoleIds = [
|
|
45
|
+
...new Set([...removedRoleIds, ...(welcomeRoleId ? [welcomeRoleId] : [])]),
|
|
46
|
+
].slice(0, CHANNEL_AUDIT_LIMITS.beforeRoles);
|
|
29
47
|
return channelAuditConfigurationSchema.parse({
|
|
30
48
|
moduleId,
|
|
31
49
|
version: configuration?.version ?? null,
|
|
32
50
|
entryChannelId,
|
|
33
51
|
beforeRoleIds,
|
|
52
|
+
removedRoleIds,
|
|
34
53
|
afterRoleId,
|
|
35
54
|
reviewChannelId,
|
|
55
|
+
...(welcome ? { welcome } : {}),
|
|
36
56
|
mappedFields,
|
|
37
|
-
truncated:
|
|
57
|
+
truncated: configuredRemovedRoleIds.length > removedRoleIds.length ||
|
|
58
|
+
new Set([...removedRoleIds, ...(welcomeRoleId ? [welcomeRoleId] : [])])
|
|
59
|
+
.size > CHANNEL_AUDIT_LIMITS.beforeRoles,
|
|
38
60
|
});
|
|
39
61
|
}
|
|
40
62
|
//# sourceMappingURL=channel-audit-configuration.js.map
|
|
@@ -25,17 +25,57 @@ export function createChannelAuditControl(input) {
|
|
|
25
25
|
if (!installed.some((item) => item.moduleId === context.moduleId))
|
|
26
26
|
throw new BotControlNotFoundError("This module is not installed for this server");
|
|
27
27
|
await input.requireAccessible(context.guildId, context.moduleId);
|
|
28
|
+
return { actor, guildAccess, installed };
|
|
29
|
+
};
|
|
30
|
+
const unavailableWelcome = () => ({
|
|
31
|
+
status: "unavailable",
|
|
32
|
+
version: null,
|
|
33
|
+
enabled: null,
|
|
34
|
+
roleId: null,
|
|
35
|
+
});
|
|
36
|
+
const readWelcome = async (context, authority) => {
|
|
37
|
+
const moduleId = "helyx.welcome";
|
|
38
|
+
const installation = authority.installed.find((item) => item.moduleId === moduleId);
|
|
39
|
+
const module = input.modules.get(moduleId);
|
|
40
|
+
if (!installation || !module?.manifest.dashboard)
|
|
41
|
+
return unavailableWelcome();
|
|
42
|
+
try {
|
|
43
|
+
const resolvedPolicy = await input.permissions.getPolicy(context.guildId, moduleId);
|
|
44
|
+
if (!canAccessGuildDashboard(authority.actor, authority.guildAccess) ||
|
|
45
|
+
!canUseCommand(authority.actor, resolvedPolicy.policy, authority.guildAccess))
|
|
46
|
+
return unavailableWelcome();
|
|
47
|
+
await input.requireAccessible(context.guildId, moduleId);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return unavailableWelcome();
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const configuration = await input.configurations.get(context.guildId, moduleId);
|
|
54
|
+
const candidate = configuration?.value.roleId;
|
|
55
|
+
return {
|
|
56
|
+
status: "loaded",
|
|
57
|
+
version: configuration ? safeVersion(configuration.version) : null,
|
|
58
|
+
enabled: installation.enabled,
|
|
59
|
+
roleId: typeof candidate === "string" && /^\d{17,20}$/u.test(candidate)
|
|
60
|
+
? candidate
|
|
61
|
+
: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return unavailableWelcome();
|
|
66
|
+
}
|
|
28
67
|
};
|
|
29
68
|
return {
|
|
30
69
|
readChannelAuditConfiguration: async (context) => {
|
|
31
|
-
await authorize(context);
|
|
70
|
+
const authority = await authorize(context);
|
|
32
71
|
const configuration = await input.configurations.get(context.guildId, context.moduleId);
|
|
72
|
+
const welcome = await readWelcome(context, authority);
|
|
33
73
|
return channelAuditConfiguration(context.moduleId, configuration
|
|
34
74
|
? {
|
|
35
75
|
version: safeVersion(configuration.version),
|
|
36
76
|
value: configuration.value,
|
|
37
77
|
}
|
|
38
|
-
: null);
|
|
78
|
+
: null, welcome);
|
|
39
79
|
},
|
|
40
80
|
checkChannelAudit: async (context) => {
|
|
41
81
|
const assumptions = channelAuditInputSchema.parse(context.assumptions);
|
|
@@ -93,6 +93,9 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
|
|
|
93
93
|
const channels = [];
|
|
94
94
|
const add = (finding) => findings.push(finding);
|
|
95
95
|
const roles = new Map(snapshot.roles.map((role) => [role.id, role]));
|
|
96
|
+
const removedRoleIds = assumptions.removedRoleIds ?? assumptions.beforeRoleIds;
|
|
97
|
+
const retainedBeforeRoleIds = assumptions.beforeRoleIds.filter((roleId) => !removedRoleIds.includes(roleId));
|
|
98
|
+
const afterRoleIds = [assumptions.afterRoleId, ...retainedBeforeRoleIds];
|
|
96
99
|
const requiredRoles = new Set([
|
|
97
100
|
...assumptions.beforeRoleIds,
|
|
98
101
|
assumptions.afterRoleId,
|
|
@@ -124,7 +127,7 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
|
|
|
124
127
|
detail: afterRole.assignmentPolicyReason,
|
|
125
128
|
roleId: afterRole.id,
|
|
126
129
|
});
|
|
127
|
-
for (const roleId of
|
|
130
|
+
for (const roleId of removedRoleIds) {
|
|
128
131
|
const beforeRole = roles.get(roleId);
|
|
129
132
|
if (beforeRole?.assignmentPolicyReason)
|
|
130
133
|
add({
|
|
@@ -198,6 +201,7 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
|
|
|
198
201
|
const selectedIds = new Set([
|
|
199
202
|
assumptions.entryChannelId,
|
|
200
203
|
...assumptions.allowedChannelIds,
|
|
204
|
+
...(assumptions.requiredAfterChannelIds ?? []),
|
|
201
205
|
...(assumptions.reviewChannelId ? [assumptions.reviewChannelId] : []),
|
|
202
206
|
]);
|
|
203
207
|
const ordered = [...snapshot.channels].sort((left, right) => Number(selectedIds.has(right.id)) - Number(selectedIds.has(left.id)) ||
|
|
@@ -220,7 +224,7 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
|
|
|
220
224
|
}
|
|
221
225
|
const before = effectiveAuditPermissions(snapshot.guildId, snapshot.roles, assumptions.beforeRoleIds, channel);
|
|
222
226
|
const everyone = effectiveAuditPermissions(snapshot.guildId, snapshot.roles, [], channel);
|
|
223
|
-
const after = effectiveAuditPermissions(snapshot.guildId, snapshot.roles,
|
|
227
|
+
const after = effectiveAuditPermissions(snapshot.guildId, snapshot.roles, afterRoleIds, channel);
|
|
224
228
|
const retained = effectiveAuditPermissions(snapshot.guildId, snapshot.roles, [...assumptions.beforeRoleIds, assumptions.afterRoleId], channel);
|
|
225
229
|
const row = {
|
|
226
230
|
id: channel.id,
|
|
@@ -291,6 +295,47 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
|
|
|
291
295
|
channelId: channel.id,
|
|
292
296
|
});
|
|
293
297
|
}
|
|
298
|
+
if (assumptions.entryAfter && assumptions.entryAfter !== "either") {
|
|
299
|
+
if (after === null)
|
|
300
|
+
add({
|
|
301
|
+
code: "entry_after_unknown",
|
|
302
|
+
category: "unknown",
|
|
303
|
+
title: "Could not verify entrance visibility after the check",
|
|
304
|
+
detail: "One or more roles needed for the after-check view are unavailable. This is not evidence that the expected transition works.",
|
|
305
|
+
channelId: channel.id,
|
|
306
|
+
});
|
|
307
|
+
else {
|
|
308
|
+
const visible = has(after, AUDIT_PERMISSIONS.ViewChannel);
|
|
309
|
+
const expectedVisible = assumptions.entryAfter === "visible";
|
|
310
|
+
add(visible === expectedVisible
|
|
311
|
+
? {
|
|
312
|
+
code: expectedVisible
|
|
313
|
+
? "entry_after_visible"
|
|
314
|
+
: "entry_after_hidden",
|
|
315
|
+
category: "working",
|
|
316
|
+
title: expectedVisible
|
|
317
|
+
? "Entrance remains visible after the check"
|
|
318
|
+
: "Entrance is hidden after the check",
|
|
319
|
+
detail: expectedVisible
|
|
320
|
+
? "The granted role plus every before role not selected for removal can view the entrance."
|
|
321
|
+
: "The granted role plus every before role not selected for removal cannot view the entrance.",
|
|
322
|
+
channelId: channel.id,
|
|
323
|
+
}
|
|
324
|
+
: {
|
|
325
|
+
code: expectedVisible
|
|
326
|
+
? "entry_after_not_visible"
|
|
327
|
+
: "entry_after_still_visible",
|
|
328
|
+
category: "blocked",
|
|
329
|
+
title: expectedVisible
|
|
330
|
+
? "Entrance is not visible after the check"
|
|
331
|
+
: "Entrance remains visible after the check",
|
|
332
|
+
detail: expectedVisible
|
|
333
|
+
? "The configured after-check role set cannot view the entrance."
|
|
334
|
+
: "The granted role or a retained before role keeps the entrance visible after the transition.",
|
|
335
|
+
channelId: channel.id,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
294
339
|
const publishing = AUDIT_PERMISSIONS.ViewChannel | AUDIT_PERMISSIONS.SendMessages;
|
|
295
340
|
if (channel.botPermissions === null)
|
|
296
341
|
add({
|
|
@@ -321,6 +366,32 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
|
|
|
321
366
|
detail: "Review this explicitly selected staff-review destination separately from entrance visibility.",
|
|
322
367
|
channelId: channel.id,
|
|
323
368
|
});
|
|
369
|
+
if ((assumptions.requiredAfterChannelIds ?? []).includes(channel.id)) {
|
|
370
|
+
if (after === null)
|
|
371
|
+
add({
|
|
372
|
+
code: "required_after_channel_unknown",
|
|
373
|
+
category: "unknown",
|
|
374
|
+
title: "Required after-check channel could not be verified",
|
|
375
|
+
detail: "One or more roles needed for the after-check view are unavailable. This required channel is not counted as passing.",
|
|
376
|
+
channelId: channel.id,
|
|
377
|
+
});
|
|
378
|
+
else if (has(after, AUDIT_PERMISSIONS.ViewChannel))
|
|
379
|
+
add({
|
|
380
|
+
code: "required_after_channel_visible",
|
|
381
|
+
category: "working",
|
|
382
|
+
title: "Required after-check channel is visible",
|
|
383
|
+
detail: "The granted role plus every before role not selected for removal can view this required channel.",
|
|
384
|
+
channelId: channel.id,
|
|
385
|
+
});
|
|
386
|
+
else
|
|
387
|
+
add({
|
|
388
|
+
code: "required_after_channel_hidden",
|
|
389
|
+
category: "blocked",
|
|
390
|
+
title: "Required after-check channel is not visible",
|
|
391
|
+
detail: "The configured after-check role set cannot view this required channel.",
|
|
392
|
+
channelId: channel.id,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
324
395
|
}
|
|
325
396
|
for (const channelId of selectedIds) {
|
|
326
397
|
const invalid = snapshot.invalidSelectedChannels?.find((item) => item.id === channelId);
|
|
@@ -373,7 +444,9 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
|
|
|
373
444
|
moduleId === "helyx.account-gate"
|
|
374
445
|
? "Access Control grants the access role, then attempts removal of its configured temporary roles. Selected before roles are assumptions, and delivery can be partial."
|
|
375
446
|
: "Verification grants the verified role and removes its configured removal role when present. It does not assign the before roles, and other member roles can remain.",
|
|
376
|
-
|
|
447
|
+
assumptions.removedRoleIds
|
|
448
|
+
? "After-check view grants the selected after role, removes only the explicitly selected removal roles, and retains every other before role. Actual transitions can differ or partially complete."
|
|
449
|
+
: "Legacy after-check view assumes every selected before role was removed; the retained view shows their combination with the after role. Actual transitions can differ or partially complete.",
|
|
377
450
|
"The retained view includes all selected before roles; it does not enumerate every partially retained role combination.",
|
|
378
451
|
...snapshot.fetchLimitations,
|
|
379
452
|
];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CommandPolicyDeliverySession, type CommandPolicyOwnerToken, type CommandPolicyPull } from "@helyx/database-contracts";
|
|
2
|
+
import type { CommandPolicyDeliveryService } from "@helyx/platform";
|
|
3
|
+
/** One authenticated socket, one bounded transfer, no customer policy authority. */
|
|
4
|
+
export declare class CloudCommandPolicySynchronizer {
|
|
5
|
+
#private;
|
|
6
|
+
private readonly input;
|
|
7
|
+
constructor(input: {
|
|
8
|
+
applicationId: string;
|
|
9
|
+
owner: CommandPolicyOwnerToken;
|
|
10
|
+
session: CommandPolicyDeliverySession;
|
|
11
|
+
delivery: CommandPolicyDeliveryService;
|
|
12
|
+
send(correlationId: string, pull: CommandPolicyPull): void;
|
|
13
|
+
isOwnerCurrent?(): Promise<boolean>;
|
|
14
|
+
onHeld(): void;
|
|
15
|
+
intervalMs?: number;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
});
|
|
18
|
+
start(): Promise<void>;
|
|
19
|
+
runOnce(): Promise<void>;
|
|
20
|
+
/** Caller must first pass the current accepted envelope's sequence/epoch guard. */
|
|
21
|
+
accept(correlationId: string, payload: unknown, expiresAt: string): void;
|
|
22
|
+
/** Stop local activity before an atomic receipt-first identity transition. */
|
|
23
|
+
cancel(): void;
|
|
24
|
+
stop(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=cloud-command-policy-sync.d.ts.map
|