@helyx/module-account-gate 1.0.14 → 1.1.1
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 +26 -0
- package/README.md +55 -90
- package/dist/challenge-interactions.d.ts +12 -0
- package/dist/challenge-interactions.js +238 -0
- package/dist/commands.js +3 -2
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.js +1 -1
- package/dist/expiry-workflows.d.ts +32 -0
- package/dist/expiry-workflows.js +281 -0
- package/dist/gateway-interactions.d.ts +5 -0
- package/dist/gateway-interactions.js +124 -0
- package/dist/health.d.ts +10 -0
- package/dist/health.js +55 -0
- package/dist/index.d.ts +8 -19
- package/dist/index.js +35 -3157
- package/dist/member-lifecycle.d.ts +21 -0
- package/dist/member-lifecycle.js +305 -0
- package/dist/member-resource.js +14 -74
- package/dist/member-restart.d.ts +4 -0
- package/dist/member-restart.js +76 -0
- package/dist/member-revocation.d.ts +21 -0
- package/dist/member-revocation.js +127 -0
- package/dist/member-start.d.ts +5 -0
- package/dist/member-start.js +199 -0
- package/dist/member-summary.d.ts +47 -0
- package/dist/member-summary.js +200 -0
- package/dist/panel-maintenance.d.ts +26 -0
- package/dist/panel-maintenance.js +143 -0
- package/dist/panel-model.d.ts +47 -0
- package/dist/panel-model.js +128 -0
- package/dist/panel-mutations.d.ts +24 -0
- package/dist/panel-mutations.js +89 -0
- package/dist/panel-presentation.d.ts +8 -0
- package/dist/panel-presentation.js +67 -0
- package/dist/panel-resource.d.ts +2 -13
- package/dist/panel-resource.js +122 -310
- package/dist/panel-runtime.d.ts +8 -0
- package/dist/panel-runtime.js +34 -0
- package/dist/records.d.ts +42 -2
- package/dist/records.js +31 -0
- package/dist/review-decisions.d.ts +3 -0
- package/dist/review-decisions.js +274 -0
- package/dist/review-delivery.d.ts +34 -0
- package/dist/review-delivery.js +252 -0
- package/dist/review-presentation.d.ts +3 -0
- package/dist/review-presentation.js +28 -0
- package/dist/review-store.d.ts +4 -0
- package/dist/review-store.js +94 -0
- package/dist/role-recovery.d.ts +23 -0
- package/dist/role-recovery.js +176 -0
- package/dist/runtime-dependencies.d.ts +32 -0
- package/dist/runtime-dependencies.js +27 -0
- package/dist/runtime-support.d.ts +19 -0
- package/dist/runtime-support.js +74 -0
- package/dist/setup-presentation.d.ts +15 -0
- package/dist/setup-presentation.js +378 -0
- package/dist/strict-mode.d.ts +17 -0
- package/dist/strict-mode.js +154 -0
- package/dist/task-contributions.d.ts +4 -0
- package/dist/task-contributions.js +113 -0
- package/dist/task-scheduling.d.ts +12 -0
- package/dist/task-scheduling.js +79 -0
- package/dist/terminal-finalization.d.ts +12 -0
- package/dist/terminal-finalization.js +238 -0
- package/dist/workflow-fences.d.ts +6 -0
- package/dist/workflow-fences.js +73 -0
- package/manifest.json +9 -2
- package/migrations/0002_panel_mutation_claims.sql +34 -0
- package/package.json +5 -4
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { DashboardActionValidationError, HELYX_SERVICE_NAMES, validateServerQuestion, ACCOUNT_GATE_COMPONENTS, ACCOUNT_GATE_IDENTITY, isStrictModeActive, parseAccountGateConfiguration, validateRuntimeCapabilities, accountGateRecords, hasPasswordGatePanel, VIOLET, GREEN, } from "./runtime-dependencies.js";
|
|
2
|
+
import { boundComponentId, dateValue, stringValue } from "./runtime-support.js";
|
|
3
|
+
export function configurationForPanel(configuration, accessType) {
|
|
4
|
+
if (accessType === "challenges")
|
|
5
|
+
return configuration;
|
|
6
|
+
const challengeType = accessType === "password" ? "password_entry" : "emoji_entry";
|
|
7
|
+
return {
|
|
8
|
+
...configuration,
|
|
9
|
+
accessType,
|
|
10
|
+
preset: "custom",
|
|
11
|
+
allocationMode: "fixed_chain",
|
|
12
|
+
enabledChallengeTypes: [challengeType],
|
|
13
|
+
fixedChallengeChain: [challengeType],
|
|
14
|
+
challengeCount: 1,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function reviewMessage(memberUserId, reasonCode) {
|
|
18
|
+
return {
|
|
19
|
+
allowedUserMentionIds: [memberUserId],
|
|
20
|
+
componentsV2: {
|
|
21
|
+
accentColor: VIOLET,
|
|
22
|
+
text: [
|
|
23
|
+
{ content: "## Access Control review" },
|
|
24
|
+
{ content: `Member: <@${memberUserId}> (${memberUserId})` },
|
|
25
|
+
{ content: `Reason: ${reasonCode}`, markdown: false },
|
|
26
|
+
{
|
|
27
|
+
content: "Challenge answers and Password Entry content are not included.",
|
|
28
|
+
markdown: false,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
buttons: [
|
|
33
|
+
{
|
|
34
|
+
type: "button",
|
|
35
|
+
customId: ACCOUNT_GATE_COMPONENTS.review.approve,
|
|
36
|
+
label: "Approve",
|
|
37
|
+
style: "primary",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: "button",
|
|
41
|
+
customId: ACCOUNT_GATE_COMPONENTS.review.retry,
|
|
42
|
+
label: "Retry",
|
|
43
|
+
style: "secondary",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: "button",
|
|
47
|
+
customId: ACCOUNT_GATE_COMPONENTS.review.kick,
|
|
48
|
+
label: "Kick",
|
|
49
|
+
style: "danger",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: "button",
|
|
53
|
+
customId: ACCOUNT_GATE_COMPONENTS.review.dismiss,
|
|
54
|
+
label: "Dismiss",
|
|
55
|
+
style: "secondary",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export async function validateAccountGateSetup(services, guildId, configuration, throwOnError = true, allowMissingPanel = false) {
|
|
61
|
+
const resources = services.get(HELYX_SERVICE_NAMES.discordResources);
|
|
62
|
+
const records = services.get(HELYX_SERVICE_NAMES.records);
|
|
63
|
+
const verificationEnabled = await services
|
|
64
|
+
.get(HELYX_SERVICE_NAMES.installations)
|
|
65
|
+
.isModuleEnabled(guildId, "helyx.verification");
|
|
66
|
+
const [gateChannel, gateAudience, guildCapabilities, verifiedRole, capabilities,] = await Promise.all([
|
|
67
|
+
resources.inspectChannel({
|
|
68
|
+
guildId,
|
|
69
|
+
channelId: configuration.gateChannelId,
|
|
70
|
+
}),
|
|
71
|
+
resources.inspectChannelAudience({
|
|
72
|
+
guildId,
|
|
73
|
+
channelId: configuration.gateChannelId,
|
|
74
|
+
roleIds: configuration.temporaryRoleIds,
|
|
75
|
+
}),
|
|
76
|
+
resources.inspectGuildCapabilities({ guildId }),
|
|
77
|
+
resources.inspectRole({
|
|
78
|
+
guildId,
|
|
79
|
+
roleId: configuration.verifiedRoleId,
|
|
80
|
+
}),
|
|
81
|
+
resources.inspectRuntimeCapabilities(),
|
|
82
|
+
]);
|
|
83
|
+
const errors = [...validateRuntimeCapabilities(configuration, capabilities)];
|
|
84
|
+
const panel = await records.findOne({
|
|
85
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
86
|
+
collection: "gate_panels",
|
|
87
|
+
select: accountGateRecords.gate_panels.readableFields,
|
|
88
|
+
where: { guild_id: guildId },
|
|
89
|
+
});
|
|
90
|
+
if (!panel) {
|
|
91
|
+
if (!allowMissingPanel)
|
|
92
|
+
errors.push("The managed Access Control panel has not been posted. Post or repair it before processing joins.");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const panelMessage = await resources.inspectMessage({
|
|
96
|
+
guildId,
|
|
97
|
+
channelId: stringValue(panel.channel_id),
|
|
98
|
+
messageId: stringValue(panel.message_id),
|
|
99
|
+
});
|
|
100
|
+
if (!panelMessage.exists ||
|
|
101
|
+
!panelMessage.guildMatches ||
|
|
102
|
+
!panelMessage.channelMatches)
|
|
103
|
+
errors.push("The managed Access Control panel message is missing. Repost it before processing joins.");
|
|
104
|
+
}
|
|
105
|
+
if (verificationEnabled)
|
|
106
|
+
errors.push("Helyx Verification already owns this server's entry-role workflow. Disable it before enabling Access Control.");
|
|
107
|
+
if (!gateChannel.viewable || !gateChannel.sendMessages)
|
|
108
|
+
errors.push("Helyx cannot view and send messages in the gate channel.");
|
|
109
|
+
if (!gateAudience.everyoneCanView &&
|
|
110
|
+
(gateAudience.roles.length === 0 ||
|
|
111
|
+
gateAudience.roles.some(({ exists, canView }) => !exists || !canView)))
|
|
112
|
+
errors.push("The gate channel must be visible to @everyone or every configured temporary member role.");
|
|
113
|
+
if (!guildCapabilities.manageRoles)
|
|
114
|
+
errors.push("Access Control requires Manage Roles.");
|
|
115
|
+
if (!verifiedRole.assignable)
|
|
116
|
+
errors.push("The access role is missing or is above Helyx.");
|
|
117
|
+
for (const roleId of configuration.temporaryRoleIds) {
|
|
118
|
+
const role = await resources.inspectRole({ guildId, roleId });
|
|
119
|
+
if (!role.exists || !role.assignable)
|
|
120
|
+
errors.push(`Temporary role ${roleId} is missing or is above Helyx.`);
|
|
121
|
+
}
|
|
122
|
+
if (configuration.reviewChannelId) {
|
|
123
|
+
const [reviewChannel, reviewAudience] = await Promise.all([
|
|
124
|
+
resources.inspectChannel({
|
|
125
|
+
guildId,
|
|
126
|
+
channelId: configuration.reviewChannelId,
|
|
127
|
+
}),
|
|
128
|
+
resources.inspectChannelAudience({
|
|
129
|
+
guildId,
|
|
130
|
+
channelId: configuration.reviewChannelId,
|
|
131
|
+
roleIds: configuration.temporaryRoleIds,
|
|
132
|
+
}),
|
|
133
|
+
]);
|
|
134
|
+
if (!reviewChannel.viewable || !reviewChannel.sendMessages)
|
|
135
|
+
errors.push("Helyx cannot use the configured staff review channel.");
|
|
136
|
+
if (reviewAudience.everyoneCanView ||
|
|
137
|
+
reviewAudience.roles.some(({ canView }) => canView))
|
|
138
|
+
errors.push("The staff review channel must not be visible to @everyone or temporary pre-gate roles.");
|
|
139
|
+
}
|
|
140
|
+
if (configuration.failureAction === "kick" && !guildCapabilities.kickMembers)
|
|
141
|
+
errors.push("The kick outcome requires Kick Members.");
|
|
142
|
+
if (configuration.enabledChallengeTypes.includes("password_entry")) {
|
|
143
|
+
const [passwordChannel, passwordAudience] = await Promise.all([
|
|
144
|
+
resources.inspectChannel({
|
|
145
|
+
guildId,
|
|
146
|
+
channelId: configuration.passwordChannelId,
|
|
147
|
+
}),
|
|
148
|
+
resources.inspectChannelAudience({
|
|
149
|
+
guildId,
|
|
150
|
+
channelId: configuration.passwordChannelId,
|
|
151
|
+
roleIds: configuration.temporaryRoleIds,
|
|
152
|
+
}),
|
|
153
|
+
]);
|
|
154
|
+
if (!passwordChannel.viewable ||
|
|
155
|
+
!passwordChannel.readMessageHistory ||
|
|
156
|
+
!passwordChannel.manageMessages)
|
|
157
|
+
errors.push("Password Entry requires View Channel, Read Message History and Manage Messages.");
|
|
158
|
+
if (passwordAudience.everyoneCanView)
|
|
159
|
+
errors.push("Password Entry must not be visible to @everyone.");
|
|
160
|
+
if (configuration.temporaryRoleIds.length === 0 ||
|
|
161
|
+
!configuration.temporaryRoleIds.every((roleId) => passwordAudience.roles.some((role) => role.roleId === roleId && role.exists && role.canView)))
|
|
162
|
+
errors.push("Password Entry requires every configured temporary pre-gate role to view its isolated channel.");
|
|
163
|
+
const store = services.get(HELYX_SERVICE_NAMES.records);
|
|
164
|
+
if (!(await hasPasswordGatePanel(store, guildId)))
|
|
165
|
+
errors.push("Password Entry needs a published panel with a password.");
|
|
166
|
+
}
|
|
167
|
+
if (configuration.enabledChallengeTypes.includes("server_question") &&
|
|
168
|
+
(await loadQuestions(services, guildId)).length === 0)
|
|
169
|
+
errors.push("Server Question requires at least one enabled question.");
|
|
170
|
+
if (configuration.enabledChallengeTypes.includes("emoji_entry")) {
|
|
171
|
+
const [message, emoji, channel] = await Promise.all([
|
|
172
|
+
resources.inspectMessage({
|
|
173
|
+
guildId,
|
|
174
|
+
channelId: configuration.emojiChannelId,
|
|
175
|
+
messageId: configuration.emojiMessageId,
|
|
176
|
+
}),
|
|
177
|
+
resources.inspectEmoji({
|
|
178
|
+
guildId,
|
|
179
|
+
emojiIdentifier: configuration.emojiIdentifier,
|
|
180
|
+
}),
|
|
181
|
+
resources.inspectChannel({
|
|
182
|
+
guildId,
|
|
183
|
+
channelId: configuration.emojiChannelId,
|
|
184
|
+
}),
|
|
185
|
+
]);
|
|
186
|
+
if (!message.exists || !message.guildMatches || !message.channelMatches)
|
|
187
|
+
errors.push("The Emoji Entry message is unavailable.");
|
|
188
|
+
if (!emoji.exists || !emoji.usable)
|
|
189
|
+
errors.push("The Emoji Entry emoji is unavailable.");
|
|
190
|
+
if (!channel.viewable ||
|
|
191
|
+
!channel.readMessageHistory ||
|
|
192
|
+
!channel.addReactions)
|
|
193
|
+
errors.push("Emoji Entry requires View Channel, Read Message History and Add Reactions.");
|
|
194
|
+
if (errors.length === 0 && throwOnError)
|
|
195
|
+
await resources.seedMessageReaction({
|
|
196
|
+
guildId,
|
|
197
|
+
channelId: configuration.emojiChannelId,
|
|
198
|
+
messageId: configuration.emojiMessageId,
|
|
199
|
+
emojiIdentifier: configuration.emojiIdentifier,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (throwOnError && errors.length > 0)
|
|
203
|
+
throw new DashboardActionValidationError(errors[0]);
|
|
204
|
+
return errors;
|
|
205
|
+
}
|
|
206
|
+
export function challengeMessage(state, binding, options = {}) {
|
|
207
|
+
const challenge = state.chain[state.currentStep];
|
|
208
|
+
if (!challenge) {
|
|
209
|
+
return {
|
|
210
|
+
ephemeral: true,
|
|
211
|
+
componentsV2: {
|
|
212
|
+
accentColor: GREEN,
|
|
213
|
+
text: [
|
|
214
|
+
{ content: "## Check complete" },
|
|
215
|
+
{
|
|
216
|
+
content: "Your Access Control check is complete.",
|
|
217
|
+
markdown: false,
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const base = {
|
|
224
|
+
ephemeral: true,
|
|
225
|
+
componentsV2: {
|
|
226
|
+
accentColor: VIOLET,
|
|
227
|
+
text: [
|
|
228
|
+
{ content: "## Complete Access Control" },
|
|
229
|
+
{
|
|
230
|
+
content: options.redactRecallCode && challenge.type === "modal_recall"
|
|
231
|
+
? "Remember this code: **-****"
|
|
232
|
+
: challenge.prompt,
|
|
233
|
+
markdown: false,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
content: `Challenge ${state.currentStep + 1} of ${state.chain.length}`,
|
|
237
|
+
markdown: false,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
switch (challenge.type) {
|
|
243
|
+
case "quick_sum":
|
|
244
|
+
case "number_order":
|
|
245
|
+
case "symbol_match":
|
|
246
|
+
case "server_question":
|
|
247
|
+
return {
|
|
248
|
+
...base,
|
|
249
|
+
buttons: [
|
|
250
|
+
...challenge.options.map((label, index) => ({
|
|
251
|
+
type: "button",
|
|
252
|
+
customId: boundComponentId(ACCOUNT_GATE_COMPONENTS.answerPrefix, {
|
|
253
|
+
...binding,
|
|
254
|
+
step: state.step,
|
|
255
|
+
value: String(index),
|
|
256
|
+
}),
|
|
257
|
+
label,
|
|
258
|
+
style: "secondary",
|
|
259
|
+
...(challenge.type === "number_order" &&
|
|
260
|
+
challenge.expectedSequence
|
|
261
|
+
.slice(0, challenge.progress)
|
|
262
|
+
.includes(index)
|
|
263
|
+
? { disabled: true }
|
|
264
|
+
: {}),
|
|
265
|
+
})),
|
|
266
|
+
{
|
|
267
|
+
type: "button",
|
|
268
|
+
customId: boundComponentId(ACCOUNT_GATE_COMPONENTS.rerollPrefix, {
|
|
269
|
+
...binding,
|
|
270
|
+
step: state.step,
|
|
271
|
+
}),
|
|
272
|
+
label: "Different challenge",
|
|
273
|
+
style: "secondary",
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
case "button_sequence":
|
|
278
|
+
return {
|
|
279
|
+
...base,
|
|
280
|
+
buttons: [
|
|
281
|
+
{
|
|
282
|
+
type: "button",
|
|
283
|
+
customId: boundComponentId(ACCOUNT_GATE_COMPONENTS.sequencePrefix, {
|
|
284
|
+
...binding,
|
|
285
|
+
step: state.step,
|
|
286
|
+
value: "red",
|
|
287
|
+
}),
|
|
288
|
+
label: "Red",
|
|
289
|
+
style: "secondary",
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
type: "button",
|
|
293
|
+
customId: boundComponentId(ACCOUNT_GATE_COMPONENTS.sequencePrefix, {
|
|
294
|
+
...binding,
|
|
295
|
+
step: state.step,
|
|
296
|
+
value: "blue",
|
|
297
|
+
}),
|
|
298
|
+
label: "Blue",
|
|
299
|
+
style: "secondary",
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
type: "button",
|
|
303
|
+
customId: boundComponentId(ACCOUNT_GATE_COMPONENTS.sequencePrefix, {
|
|
304
|
+
...binding,
|
|
305
|
+
step: state.step,
|
|
306
|
+
value: "green",
|
|
307
|
+
}),
|
|
308
|
+
label: "Green",
|
|
309
|
+
style: "secondary",
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
case "modal_recall":
|
|
314
|
+
return {
|
|
315
|
+
...base,
|
|
316
|
+
buttons: [
|
|
317
|
+
{
|
|
318
|
+
type: "button",
|
|
319
|
+
customId: boundComponentId(ACCOUNT_GATE_COMPONENTS.recallOpenPrefix, { ...binding, step: state.step }),
|
|
320
|
+
label: "Enter code",
|
|
321
|
+
style: "primary",
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
};
|
|
325
|
+
case "password_entry":
|
|
326
|
+
case "emoji_entry":
|
|
327
|
+
return base;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
export async function loadQuestions(services, guildId) {
|
|
331
|
+
const rows = await services
|
|
332
|
+
.get(HELYX_SERVICE_NAMES.records)
|
|
333
|
+
.findMany({
|
|
334
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
335
|
+
collection: "questions",
|
|
336
|
+
select: accountGateRecords.questions.readableFields,
|
|
337
|
+
where: { guild_id: guildId, enabled: true },
|
|
338
|
+
orderBy: [
|
|
339
|
+
{ field: "guild_id", direction: "asc" },
|
|
340
|
+
{ field: "sort_order", direction: "asc" },
|
|
341
|
+
{ field: "question_id", direction: "asc" },
|
|
342
|
+
],
|
|
343
|
+
limit: 100,
|
|
344
|
+
});
|
|
345
|
+
return rows.records.map((row) => validateServerQuestion({
|
|
346
|
+
id: row.question_id,
|
|
347
|
+
question: row.question_text,
|
|
348
|
+
answers: JSON.parse(stringValue(row.answers_json)),
|
|
349
|
+
correctOption: row.correct_option_index,
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
export async function currentStrictMode(services, guildId) {
|
|
353
|
+
const row = await services
|
|
354
|
+
.get(HELYX_SERVICE_NAMES.records)
|
|
355
|
+
.findOne({
|
|
356
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
357
|
+
collection: "strict_modes",
|
|
358
|
+
select: accountGateRecords.strict_modes.readableFields,
|
|
359
|
+
where: { guild_id: guildId, status: "active" },
|
|
360
|
+
});
|
|
361
|
+
return !!row && isStrictModeActive(dateValue(row.ends_at));
|
|
362
|
+
}
|
|
363
|
+
export async function getConfiguration(services, guildId) {
|
|
364
|
+
const record = await services
|
|
365
|
+
.get(HELYX_SERVICE_NAMES.configuration)
|
|
366
|
+
.get(guildId, ACCOUNT_GATE_IDENTITY.moduleId);
|
|
367
|
+
return record ? parseAccountGateConfiguration(record.value) : null;
|
|
368
|
+
}
|
|
369
|
+
export async function requiredRuntimeConfiguration(services, guildId) {
|
|
370
|
+
const configuration = await getConfiguration(services, guildId);
|
|
371
|
+
if (!configuration)
|
|
372
|
+
throw new DashboardActionValidationError("Save the Access Control settings first.");
|
|
373
|
+
const errors = await validateAccountGateSetup(services, guildId, configuration, false);
|
|
374
|
+
if (errors.length > 0)
|
|
375
|
+
throw new DashboardActionValidationError(errors[0]);
|
|
376
|
+
return configuration;
|
|
377
|
+
}
|
|
378
|
+
//# sourceMappingURL=setup-presentation.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CommandInteractionContext, RuntimeContext, ScheduledTaskPayload, ServiceAccess } from "./runtime-dependencies.js";
|
|
2
|
+
export declare function showStatus(context: CommandInteractionContext): Promise<void>;
|
|
3
|
+
export declare function activateStrictModeCommand(context: CommandInteractionContext): Promise<void>;
|
|
4
|
+
export declare function activateStrictMode(services: ServiceAccess, guildId: string, actorUserId: string, durationMinutes: number, correlationId: string): Promise<Date>;
|
|
5
|
+
export declare function validateStrictTaskPayload(input: unknown): ScheduledTaskPayload;
|
|
6
|
+
export declare function expireStrictMode(context: RuntimeContext, task: {
|
|
7
|
+
payload: ScheduledTaskPayload;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
outcome: "completed";
|
|
10
|
+
delayMs?: never;
|
|
11
|
+
safeCode?: never;
|
|
12
|
+
} | {
|
|
13
|
+
outcome: "deferred";
|
|
14
|
+
delayMs: number;
|
|
15
|
+
safeCode: string;
|
|
16
|
+
}>;
|
|
17
|
+
//# sourceMappingURL=strict-mode.d.ts.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES, ACCOUNT_GATE_IDENTITY, accountGateRecords, emitLog, VIOLET, GREEN, RED, STRICT_EXPIRY_TASK, } from "./runtime-dependencies.js";
|
|
2
|
+
import { dateValue, numberValue, replyError } from "./runtime-support.js";
|
|
3
|
+
import { currentStrictMode, requiredRuntimeConfiguration, validateAccountGateSetup, } from "./setup-presentation.js";
|
|
4
|
+
export async function showStatus(context) {
|
|
5
|
+
if (!context.serverId)
|
|
6
|
+
return replyError(context, "This command is server-only.");
|
|
7
|
+
const configuration = await requiredRuntimeConfiguration(context.services, context.serverId);
|
|
8
|
+
const errors = await validateAccountGateSetup(context.services, context.serverId, configuration, false);
|
|
9
|
+
const strict = await currentStrictMode(context.services, context.serverId);
|
|
10
|
+
await context.reply({
|
|
11
|
+
ephemeral: true,
|
|
12
|
+
embed: {
|
|
13
|
+
title: "Access Control status",
|
|
14
|
+
description: `${errors.length === 0 ? "Configuration checks passed." : errors.join("\n")}\nTemporary strict mode: ${strict ? "Active" : "Off"}.`,
|
|
15
|
+
color: errors.length === 0 ? GREEN : RED,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export async function activateStrictModeCommand(context) {
|
|
20
|
+
if (!context.serverId)
|
|
21
|
+
return replyError(context, "This command is server-only.");
|
|
22
|
+
const configuration = await requiredRuntimeConfiguration(context.services, context.serverId);
|
|
23
|
+
const endsAt = await activateStrictMode(context.services, context.serverId, context.userId, configuration.strictModeDurationMinutes, context.interactionId);
|
|
24
|
+
await context.reply({
|
|
25
|
+
ephemeral: true,
|
|
26
|
+
embed: {
|
|
27
|
+
title: "Temporary strict mode active",
|
|
28
|
+
description: `One generated challenge has been added until <t:${Math.floor(endsAt.getTime() / 1_000)}:F>.`,
|
|
29
|
+
color: VIOLET,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export async function activateStrictMode(services, guildId, actorUserId, durationMinutes, correlationId) {
|
|
34
|
+
const records = services.get(HELYX_SERVICE_NAMES.records);
|
|
35
|
+
let generation = 0;
|
|
36
|
+
let endsAt = new Date();
|
|
37
|
+
for (let retry = 0; retry < 4; retry += 1) {
|
|
38
|
+
const current = await records.findOne({
|
|
39
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
40
|
+
collection: "strict_modes",
|
|
41
|
+
select: accountGateRecords.strict_modes.readableFields,
|
|
42
|
+
where: { guild_id: guildId },
|
|
43
|
+
});
|
|
44
|
+
const now = new Date();
|
|
45
|
+
endsAt = new Date(now.getTime() + durationMinutes * 60_000);
|
|
46
|
+
generation = current ? numberValue(current.generation) + 1 : 1;
|
|
47
|
+
const schedule = {
|
|
48
|
+
taskKind: STRICT_EXPIRY_TASK,
|
|
49
|
+
guildId,
|
|
50
|
+
idempotencyKey: `${guildId}:strict:${generation}`,
|
|
51
|
+
payload: { guildId },
|
|
52
|
+
scheduledFor: endsAt,
|
|
53
|
+
};
|
|
54
|
+
const stored = current
|
|
55
|
+
? await records.updateOne({
|
|
56
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
57
|
+
collection: "strict_modes",
|
|
58
|
+
where: { guild_id: guildId },
|
|
59
|
+
expected: {
|
|
60
|
+
field: "revision",
|
|
61
|
+
value: numberValue(current.revision),
|
|
62
|
+
},
|
|
63
|
+
values: {
|
|
64
|
+
status: "active",
|
|
65
|
+
ends_at: endsAt,
|
|
66
|
+
generation,
|
|
67
|
+
activated_by_user_id: actorUserId,
|
|
68
|
+
revision: numberValue(current.revision) + 1,
|
|
69
|
+
updated_at: now,
|
|
70
|
+
},
|
|
71
|
+
schedule,
|
|
72
|
+
})
|
|
73
|
+
: await records.appendUnique({
|
|
74
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
75
|
+
collection: "strict_modes",
|
|
76
|
+
uniqueBy: ["guild_id"],
|
|
77
|
+
values: {
|
|
78
|
+
guild_id: guildId,
|
|
79
|
+
status: "active",
|
|
80
|
+
ends_at: endsAt,
|
|
81
|
+
generation,
|
|
82
|
+
activated_by_user_id: actorUserId,
|
|
83
|
+
revision: 1,
|
|
84
|
+
created_at: now,
|
|
85
|
+
updated_at: now,
|
|
86
|
+
},
|
|
87
|
+
schedule,
|
|
88
|
+
});
|
|
89
|
+
if ("updated" in stored ? stored.updated : stored.created)
|
|
90
|
+
break;
|
|
91
|
+
if (retry === 3)
|
|
92
|
+
throw new Error("Access Control strict mode changed concurrently");
|
|
93
|
+
}
|
|
94
|
+
await emitLog(services, guildId, {
|
|
95
|
+
eventId: "account-gate-strict-mode-changed",
|
|
96
|
+
summary: "Temporary Access Control strict mode was activated.",
|
|
97
|
+
details: [
|
|
98
|
+
{ label: "Duration minutes", value: String(durationMinutes) },
|
|
99
|
+
{ label: "Generation", value: String(generation) },
|
|
100
|
+
],
|
|
101
|
+
idempotencyKey: correlationId,
|
|
102
|
+
});
|
|
103
|
+
return endsAt;
|
|
104
|
+
}
|
|
105
|
+
export function validateStrictTaskPayload(input) {
|
|
106
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
107
|
+
throw new Error("Account Gate strict task payload is invalid");
|
|
108
|
+
const value = input;
|
|
109
|
+
if (typeof value.guildId !== "string" || !/^\d{17,20}$/u.test(value.guildId))
|
|
110
|
+
throw new Error("Account Gate strict task payload is invalid");
|
|
111
|
+
return { guildId: value.guildId };
|
|
112
|
+
}
|
|
113
|
+
export async function expireStrictMode(context, task) {
|
|
114
|
+
const records = context.services.get(HELYX_SERVICE_NAMES.records);
|
|
115
|
+
const row = await records.findOne({
|
|
116
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
117
|
+
collection: "strict_modes",
|
|
118
|
+
select: accountGateRecords.strict_modes.readableFields,
|
|
119
|
+
where: { guild_id: task.payload.guildId },
|
|
120
|
+
});
|
|
121
|
+
if (!row || row.status !== "active")
|
|
122
|
+
return { outcome: "completed" };
|
|
123
|
+
const endsAt = dateValue(row.ends_at);
|
|
124
|
+
if (!endsAt)
|
|
125
|
+
return { outcome: "completed" };
|
|
126
|
+
if (endsAt.getTime() > Date.now())
|
|
127
|
+
return {
|
|
128
|
+
outcome: "deferred",
|
|
129
|
+
delayMs: Math.max(100, endsAt.getTime() - Date.now()),
|
|
130
|
+
safeCode: "strict-expiry-not-due",
|
|
131
|
+
};
|
|
132
|
+
const generation = numberValue(row.generation);
|
|
133
|
+
const result = await records.updateOne({
|
|
134
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
135
|
+
collection: "strict_modes",
|
|
136
|
+
where: { guild_id: task.payload.guildId },
|
|
137
|
+
expected: { field: "generation", value: generation },
|
|
138
|
+
dateAtOrBeforeNow: { field: "ends_at" },
|
|
139
|
+
values: {
|
|
140
|
+
status: "expired",
|
|
141
|
+
revision: numberValue(row.revision) + 1,
|
|
142
|
+
updated_at: new Date(),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
if (result.updated) {
|
|
146
|
+
await emitLog(context.services, task.payload.guildId, {
|
|
147
|
+
eventId: "account-gate-strict-mode-changed",
|
|
148
|
+
summary: "Temporary Access Control strict mode expired.",
|
|
149
|
+
details: [{ label: "Generation", value: String(generation) }],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return { outcome: "completed" };
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=strict-mode.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ModuleScheduledTaskContribution } from "@helyx/sdk";
|
|
2
|
+
import type { AccountGateHealth } from "./health.js";
|
|
3
|
+
export declare function createAccountGateTasks(health: AccountGateHealth): readonly ModuleScheduledTaskContribution[];
|
|
4
|
+
//# sourceMappingURL=task-contributions.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { enforceJoinDeadline, expireSession, runLeaveRetention, validateJoinDeadlinePayload, validateLeaveRetentionPayload, validateSessionExpiryPayload, } from "./expiry-workflows.js";
|
|
2
|
+
import { executePanelMessageCleanup, PANEL_MESSAGE_CLEANUP_TASK, validatePanelCleanupPayload, } from "./panel-maintenance.js";
|
|
3
|
+
import { reconcileRevocationTask, validateRevocationPayload, } from "./member-revocation.js";
|
|
4
|
+
import { deliverReview, recoverReviewClaim, validateReviewClaimRecoveryPayload, validateReviewDeliveryPayload, } from "./review-delivery.js";
|
|
5
|
+
import { retryRoleTransition, validateRoleRetryPayload, } from "./role-recovery.js";
|
|
6
|
+
import { JOIN_DEADLINE_TASK, LEAVE_RETENTION_TASK, REVIEW_CLAIM_RECOVERY_TASK, REVIEW_DELIVERY_TASK, ROLE_RETRY_TASK, REVOCATION_RECONCILE_TASK, SESSION_EXPIRY_TASK, STRICT_EXPIRY_TASK, TERMINAL_FINALIZATION_TASK, } from "./runtime-dependencies.js";
|
|
7
|
+
import { expireStrictMode, validateStrictTaskPayload } from "./strict-mode.js";
|
|
8
|
+
import { runTerminalFinalizationTask, validateTerminalFinalizationPayload, } from "./terminal-finalization.js";
|
|
9
|
+
export function createAccountGateTasks(health) {
|
|
10
|
+
return [
|
|
11
|
+
trackedTask(health, {
|
|
12
|
+
kind: STRICT_EXPIRY_TASK,
|
|
13
|
+
version: 1,
|
|
14
|
+
delivery: "one-shot",
|
|
15
|
+
attemptLimit: 5,
|
|
16
|
+
validatePayload: validateStrictTaskPayload,
|
|
17
|
+
execute: expireStrictMode,
|
|
18
|
+
}),
|
|
19
|
+
trackedTask(health, {
|
|
20
|
+
kind: ROLE_RETRY_TASK,
|
|
21
|
+
version: 1,
|
|
22
|
+
delivery: "one-shot",
|
|
23
|
+
attemptLimit: 5,
|
|
24
|
+
validatePayload: validateRoleRetryPayload,
|
|
25
|
+
execute: retryRoleTransition,
|
|
26
|
+
}),
|
|
27
|
+
trackedTask(health, {
|
|
28
|
+
kind: JOIN_DEADLINE_TASK,
|
|
29
|
+
version: 1,
|
|
30
|
+
delivery: "one-shot",
|
|
31
|
+
attemptLimit: 5,
|
|
32
|
+
validatePayload: validateJoinDeadlinePayload,
|
|
33
|
+
execute: enforceJoinDeadline,
|
|
34
|
+
}),
|
|
35
|
+
trackedTask(health, {
|
|
36
|
+
kind: SESSION_EXPIRY_TASK,
|
|
37
|
+
version: 1,
|
|
38
|
+
delivery: "one-shot",
|
|
39
|
+
attemptLimit: 5,
|
|
40
|
+
validatePayload: validateSessionExpiryPayload,
|
|
41
|
+
execute: expireSession,
|
|
42
|
+
}),
|
|
43
|
+
trackedTask(health, {
|
|
44
|
+
kind: REVIEW_DELIVERY_TASK,
|
|
45
|
+
version: 1,
|
|
46
|
+
delivery: "one-shot",
|
|
47
|
+
attemptLimit: 8,
|
|
48
|
+
validatePayload: validateReviewDeliveryPayload,
|
|
49
|
+
execute: deliverReview,
|
|
50
|
+
}),
|
|
51
|
+
trackedTask(health, {
|
|
52
|
+
kind: REVIEW_CLAIM_RECOVERY_TASK,
|
|
53
|
+
version: 1,
|
|
54
|
+
delivery: "one-shot",
|
|
55
|
+
attemptLimit: 5,
|
|
56
|
+
validatePayload: validateReviewClaimRecoveryPayload,
|
|
57
|
+
execute: recoverReviewClaim,
|
|
58
|
+
}),
|
|
59
|
+
trackedTask(health, {
|
|
60
|
+
kind: TERMINAL_FINALIZATION_TASK,
|
|
61
|
+
version: 1,
|
|
62
|
+
delivery: "one-shot",
|
|
63
|
+
attemptLimit: 10,
|
|
64
|
+
validatePayload: validateTerminalFinalizationPayload,
|
|
65
|
+
execute: runTerminalFinalizationTask,
|
|
66
|
+
}),
|
|
67
|
+
trackedTask(health, {
|
|
68
|
+
kind: REVOCATION_RECONCILE_TASK,
|
|
69
|
+
version: 1,
|
|
70
|
+
delivery: "one-shot",
|
|
71
|
+
attemptLimit: 10,
|
|
72
|
+
validatePayload: validateRevocationPayload,
|
|
73
|
+
execute: reconcileRevocationTask,
|
|
74
|
+
}),
|
|
75
|
+
trackedTask(health, {
|
|
76
|
+
kind: LEAVE_RETENTION_TASK,
|
|
77
|
+
version: 1,
|
|
78
|
+
delivery: "one-shot",
|
|
79
|
+
attemptLimit: 5,
|
|
80
|
+
validatePayload: validateLeaveRetentionPayload,
|
|
81
|
+
execute: runLeaveRetention,
|
|
82
|
+
}),
|
|
83
|
+
trackedTask(health, {
|
|
84
|
+
kind: PANEL_MESSAGE_CLEANUP_TASK,
|
|
85
|
+
version: 1,
|
|
86
|
+
delivery: "one-shot",
|
|
87
|
+
attemptLimit: 10,
|
|
88
|
+
validatePayload: validatePanelCleanupPayload,
|
|
89
|
+
execute: executePanelMessageCleanup,
|
|
90
|
+
}),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
function trackedTask(health, contribution) {
|
|
94
|
+
return {
|
|
95
|
+
...contribution,
|
|
96
|
+
execute: (context, task) => executeTrackedTask(health, contribution, context, task),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function executeTrackedTask(health, contribution, context, task) {
|
|
100
|
+
try {
|
|
101
|
+
const result = await contribution.execute(context, task);
|
|
102
|
+
if (result.outcome === "completed")
|
|
103
|
+
health.recordTaskSuccess(task.taskKind);
|
|
104
|
+
else
|
|
105
|
+
health.recordTaskFailure(task.taskKind, result.safeCode);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
health.recordTaskFailure(task.taskKind, "account_gate.task_failed");
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=task-contributions.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ModuleRecordService, ServiceAccess } from "./runtime-dependencies.js";
|
|
2
|
+
export declare function scheduleJoinDeadline(services: ServiceAccess, guildId: string, memberUserId: string, generation: number, deadlineAt: Date): Promise<void>;
|
|
3
|
+
export declare function scheduleSessionExpiry(services: ServiceAccess, input: {
|
|
4
|
+
guildId: string;
|
|
5
|
+
userId: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
expiredAt: Date;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export declare function scheduleReviewDelivery(services: ServiceAccess, guildId: string, reviewId: string, scheduledFor: Date): Promise<void>;
|
|
10
|
+
export declare function scheduleTerminalFinalization(services: ServiceAccess, guildId: string, finalizationId: string, scheduledFor: Date): Promise<void>;
|
|
11
|
+
export declare function armRecordRetention(records: ModuleRecordService, guildId: string, collection: "attempts" | "reviews" | "terminal_finalizations", idField: "attempt_id" | "review_id" | "finalization_id", id: string, scheduledFor: Date): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=task-scheduling.d.ts.map
|