@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [bbc4eee]
|
|
8
|
+
- @helyx/sdk@0.10.0
|
|
9
|
+
|
|
10
|
+
## 1.1.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- Add declared command-operation permission targets, project them through both permission-management surfaces, and harden module lifecycle, health, visibility, bounded-work, optional-side-effect, command-permission, and Forms durable-workflow behavior. Preserve existing FAQ, Access Control, Achievements, Giveaways and Levels overrides through additive migrations while assigning every slash-command path its own permission identity.
|
|
15
|
+
|
|
16
|
+
## 1.0.15
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Add manifest-backed deployment support and credential-risk policy, enforce it
|
|
21
|
+
through hosted and self-host composition, and carry it into generated build
|
|
22
|
+
manifests and self-host module labels.
|
|
23
|
+
- Updated dependencies
|
|
24
|
+
- Updated dependencies
|
|
25
|
+
- Updated dependencies
|
|
26
|
+
- @helyx/sdk@0.9.0
|
|
27
|
+
- @helyx/module-manifest@0.8.0
|
|
28
|
+
|
|
3
29
|
## 1.0.14
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,90 +1,55 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
Access Control
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
not reduce attempts or enable kicking, Password Entry or Emoji Entry.
|
|
57
|
-
|
|
58
|
-
Join deadlines and active-check expiry use durable one-shot tasks with exact
|
|
59
|
-
join generations and session versions. Restarted or duplicate workers cannot
|
|
60
|
-
enforce an old join or expire a newer check. A failed challenge applies the
|
|
61
|
-
configured retry cooldown monotonically; passing or an authorised Approve or
|
|
62
|
-
Retry decision clears it. Account age uses Discord's account-created and joined
|
|
63
|
-
timestamps, and accepts the exact configured age boundary.
|
|
64
|
-
|
|
65
|
-
Returning members are restored only when a prior success is retained, restore
|
|
66
|
-
is enabled, re-verification is disabled, and the current access role is still
|
|
67
|
-
assignable. Otherwise they enter a fresh pending generation. Adaptive chains
|
|
68
|
-
add a generated challenge for a prior-failure or young-account signal, while
|
|
69
|
-
temporary strict mode may add one more, capped at four.
|
|
70
|
-
|
|
71
|
-
Password Entry and Emoji Entry are single-step access types rather than members
|
|
72
|
-
of a generated challenge chain. Helyx sends answer-free private resume guidance
|
|
73
|
-
that identifies the Discord server and gate channel. Closed direct messages
|
|
74
|
-
never cause user-specific controls or answers to be posted publicly; the member
|
|
75
|
-
can use the panel or `/access control start` to resume the persisted check.
|
|
76
|
-
|
|
77
|
-
## Stored data
|
|
78
|
-
|
|
79
|
-
The module stores server questions, safe attempt outcomes, member summaries,
|
|
80
|
-
staff-review decisions, recoverable role transitions and strict-mode
|
|
81
|
-
activations. Raw Password Entry messages are never retained. Active challenge
|
|
82
|
-
answers use the shared expiring Helyx session service rather than a module-owned
|
|
83
|
-
session table. A versioned Privacy Service provider supports bounded access and
|
|
84
|
-
erasure.
|
|
85
|
-
|
|
86
|
-
## Release state
|
|
87
|
-
|
|
88
|
-
Access Control is a Live Helyx module. Runtime enforcement rechecks module
|
|
89
|
-
state, member state, kick permission and role hierarchy immediately before
|
|
90
|
-
every kick.
|
|
1
|
+
# @helyx/module-account-gate
|
|
2
|
+
|
|
3
|
+
Access Control keeps joining members in a Discord-managed restricted area until they complete a configured interaction, password, emoji, or server-question gate and Helyx safely grants the access role.
|
|
4
|
+
|
|
5
|
+
## Availability and deployment
|
|
6
|
+
|
|
7
|
+
Access Control is Live and supported by Hosted Helyx and Helyx Self-Hosted. It uses Helyx runtime and Discord capabilities only and requires no external provider credentials. It cannot be enabled alongside Verification because both own the server entry-role workflow.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
Managers choose generated Challenges, Password Entry, or Emoji Entry. Generated flows can combine quick sums, button sequences, number ordering, symbol matching, modal recall, and managed server questions. The module also supports bounded attempts and cooldowns, account-age rules, returning-member restoration, staff review, join deadlines, and temporary strict mode.
|
|
12
|
+
|
|
13
|
+
Access Control is a Discord-native human-interaction gate. It can deter basic automated joins, but it does not assess devices, network risk, alternate accounts, or ban evasion. Discord channel permissions remain the authority for what pending members can see.
|
|
14
|
+
|
|
15
|
+
## Commands and interactions
|
|
16
|
+
|
|
17
|
+
| Path or control | Default access | Purpose |
|
|
18
|
+
| -------------------------------- | -------------- | -------------------------------------------------------- |
|
|
19
|
+
| `/access control start` | Everyone | Start or resume the caller's current pending check. |
|
|
20
|
+
| `/access control reset` | Manage Roles | Revoke the caller's access and begin a fresh generation. |
|
|
21
|
+
| `/access control status` | Manage Roles | Inspect setup health and temporary strict-mode state. |
|
|
22
|
+
| `/access control strict` | Manage Roles | Add one generated challenge for a bounded period. |
|
|
23
|
+
| Published gate panel | Everyone | Start the configured member flow. |
|
|
24
|
+
| Password message or emoji action | Pending member | Complete the selected single-step gate. |
|
|
25
|
+
| Dashboard and review actions | Manage Roles | Approve, retry, kick, dismiss, or revoke a current case. |
|
|
26
|
+
|
|
27
|
+
Every slash-command path has its own permission identity and dashboard override. Responses are private and server-only. Start is limited to five uses per member per minute; Reset is limited to ten.
|
|
28
|
+
|
|
29
|
+
## Dashboard configuration
|
|
30
|
+
|
|
31
|
+
Settings cover access type, roles, gate and staff-review channels, attempt and timing controls, account age, returning-member behavior, final-failure action, and optional strict mode. Managed resources publish, edit, repair, or repost gate panels and maintain server questions. The Members resource exposes bounded current state and guarded review or revoke actions.
|
|
32
|
+
|
|
33
|
+
Activation validates the selected roles, channels, Discord capabilities, published panel, and at least one enabled question when Server Questions is used. Helyx does not maintain a second allowlist for every channel visible to a pending role; managers configure those Discord overwrites directly.
|
|
34
|
+
|
|
35
|
+
## Discord requirements
|
|
36
|
+
|
|
37
|
+
The ordinary flow needs View Channel, Send Messages, Embed Links, Read Message History, and Manage Roles. Kick Members is required only when final failure is configured to kick. Password Entry additionally needs Manage Messages, Guild Messages, and Message Content. Emoji Entry may require Add Reactions and the Guild Message Reactions capability. Helyx must sit above the access role and each temporary role it removes.
|
|
38
|
+
|
|
39
|
+
The staff-review channel must not be visible to `@everyone` or configured temporary roles. Helyx validates the audiences it can inspect, but managers remain responsible for keeping every other visible role staff-only.
|
|
40
|
+
|
|
41
|
+
## Data, privacy and retention
|
|
42
|
+
|
|
43
|
+
The module stores question-creator references, bounded attempt outcomes, member summaries, staff-review decisions, terminal finalisations, recoverable role transitions, strict-mode activations, and privacy suppression state. Raw Password Entry messages and generated answers are never retained; active challenge answers use the shared expiring interaction-session service.
|
|
44
|
+
|
|
45
|
+
The registered privacy provider supports bounded access and erasure even while the module is disabled. Subject erasure removes or detaches declared member and actor records and uses suppression state to prevent in-flight work from recreating deleted personal data.
|
|
46
|
+
|
|
47
|
+
## Operations and failure behavior
|
|
48
|
+
|
|
49
|
+
Join deadlines, challenge expiry, strict-mode expiry, and recovery use durable versioned tasks. Exact join generations, session versions, write fences, and terminal receipts prevent restarted or duplicate workers from enforcing stale work. Helyx adds the access role before removing temporary roles, so a partial Discord failure does not remove the member's only access route.
|
|
50
|
+
|
|
51
|
+
Final kick decisions recheck current module, member, permission, and role state immediately before enforcement. A failed direct message does not block a configured kick. Optional Logging cannot reverse a committed decision, and health exposes bounded setup or recovery state without raw answers or exception details.
|
|
52
|
+
|
|
53
|
+
## Documentation and support
|
|
54
|
+
|
|
55
|
+
See the [Access Control guide](https://docs.helyx.gg/modules/access-control/), [settings](https://docs.helyx.gg/modules/access-control/settings/), [commands](https://docs.helyx.gg/modules/access-control/commands/), [permissions](https://docs.helyx.gg/modules/access-control/permissions/), [resources](https://docs.helyx.gg/modules/access-control/resources/), [members](https://docs.helyx.gg/modules/access-control/members/), [logging](https://docs.helyx.gg/modules/access-control/logging/), and [FAQ](https://docs.helyx.gg/modules/access-control/faq/).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ModalCapableInteractionContext, ModalInteractionContext, Challenge, ChallengeAction, WorkflowResult } from "./runtime-dependencies.js";
|
|
2
|
+
import type { ComponentBinding } from "./runtime-support.js";
|
|
3
|
+
export declare function processAction(context: ModalCapableInteractionContext | ModalInteractionContext, action: ChallengeAction, binding: ComponentBinding): Promise<void>;
|
|
4
|
+
export declare function finaliseAttempt(context: ModalCapableInteractionContext | ModalInteractionContext, sessionId: string, result: Extract<WorkflowResult, {
|
|
5
|
+
outcome: "passed" | "failed";
|
|
6
|
+
}>, challengeType: Challenge["type"]): Promise<void>;
|
|
7
|
+
export declare function rerollChallenge(context: ModalCapableInteractionContext): Promise<void>;
|
|
8
|
+
export declare function openRecallModal(context: ModalCapableInteractionContext): Promise<void>;
|
|
9
|
+
export declare function submitRecallModal(context: ModalInteractionContext): Promise<void>;
|
|
10
|
+
export declare function processBoundOption(context: ModalCapableInteractionContext): Promise<void>;
|
|
11
|
+
export declare function processBoundSequence(context: ModalCapableInteractionContext): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=challenge-interactions.d.ts.map
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES, ACCOUNT_GATE_COMPONENTS, ACCOUNT_GATE_SESSION_PURPOSE, generatedChallengePool, isAccountGateWorkSuppressed, applyChallengeAction, parseAccountGateSessionState, rerollForAccessibility, GREEN, RED, } from "./runtime-dependencies.js";
|
|
2
|
+
import { isCurrentPendingMember } from "./member-summary.js";
|
|
3
|
+
import { recordAccountGateAttempt } from "./member-restart.js";
|
|
4
|
+
import { boundComponentId, parseComponentBinding, replyError, secureRandom, } from "./runtime-support.js";
|
|
5
|
+
import { challengeMessage, requiredRuntimeConfiguration, } from "./setup-presentation.js";
|
|
6
|
+
import { scheduleSessionExpiry } from "./task-scheduling.js";
|
|
7
|
+
import { ensureTerminalFinalization, finalizeTerminalFinalization, } from "./terminal-finalization.js";
|
|
8
|
+
export async function processAction(context, action, binding) {
|
|
9
|
+
if (!context.serverId) {
|
|
10
|
+
await replyError(context, "This Access Control interaction is unavailable.");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const sessions = context.services.get(HELYX_SERVICE_NAMES.sessions);
|
|
14
|
+
const session = await sessions.getFor(context.serverId, context.userId, ACCOUNT_GATE_SESSION_PURPOSE);
|
|
15
|
+
if (!session) {
|
|
16
|
+
await replyError(context, "This check has expired. Start a new check.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (session.sessionId !== binding.sessionId ||
|
|
20
|
+
session.version !== binding.version ||
|
|
21
|
+
parseAccountGateSessionState(session.state).step !== binding.step) {
|
|
22
|
+
await replyError(context, "That control belongs to an older check or challenge step.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const configuration = await requiredRuntimeConfiguration(context.services, context.serverId);
|
|
26
|
+
if (!(await isCurrentPendingMember(context.services, context.serverId, context.userId, configuration.verifiedRoleId))) {
|
|
27
|
+
await sessions.delete(session.sessionId);
|
|
28
|
+
await replyError(context, "This pending Access Control check is no longer valid.");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const state = parseAccountGateSessionState(session.state);
|
|
32
|
+
const result = applyChallengeAction(state, action);
|
|
33
|
+
if (result.outcome === "stale") {
|
|
34
|
+
await replyError(context, "That interaction is no longer part of your current check.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (result.outcome === "progress") {
|
|
38
|
+
const updated = await sessions.update({
|
|
39
|
+
sessionId: session.sessionId,
|
|
40
|
+
expectedVersion: session.version,
|
|
41
|
+
state: result.state,
|
|
42
|
+
});
|
|
43
|
+
await scheduleSessionExpiry(context.services, {
|
|
44
|
+
guildId: context.serverId,
|
|
45
|
+
userId: context.userId,
|
|
46
|
+
sessionId: session.sessionId,
|
|
47
|
+
expiredAt: session.expiresAt,
|
|
48
|
+
});
|
|
49
|
+
await context.reply(challengeMessage(result.state, {
|
|
50
|
+
sessionId: session.sessionId,
|
|
51
|
+
version: updated.version,
|
|
52
|
+
}));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const advanced = await sessions.advance({
|
|
56
|
+
sessionId: session.sessionId,
|
|
57
|
+
guildId: context.serverId,
|
|
58
|
+
userId: context.userId,
|
|
59
|
+
purpose: ACCOUNT_GATE_SESSION_PURPOSE,
|
|
60
|
+
expectedVersion: session.version,
|
|
61
|
+
expectedStep: state.step,
|
|
62
|
+
nextStep: state.step + 1,
|
|
63
|
+
state: { ...result.state, step: state.step + 1 },
|
|
64
|
+
});
|
|
65
|
+
await scheduleSessionExpiry(context.services, {
|
|
66
|
+
guildId: context.serverId,
|
|
67
|
+
userId: context.userId,
|
|
68
|
+
sessionId: session.sessionId,
|
|
69
|
+
expiredAt: session.expiresAt,
|
|
70
|
+
});
|
|
71
|
+
if (result.outcome === "advanced") {
|
|
72
|
+
await context.reply(challengeMessage(result.state, {
|
|
73
|
+
sessionId: session.sessionId,
|
|
74
|
+
version: advanced.version,
|
|
75
|
+
}));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await finaliseAttempt(context, session.sessionId, result, state.chain[state.currentStep].type);
|
|
79
|
+
}
|
|
80
|
+
export async function finaliseAttempt(context, sessionId, result, challengeType) {
|
|
81
|
+
const guildId = context.serverId;
|
|
82
|
+
await requiredRuntimeConfiguration(context.services, guildId);
|
|
83
|
+
if (await isAccountGateWorkSuppressed(context.services, guildId, context.userId, new Date(result.state.startedAt))) {
|
|
84
|
+
await context.services
|
|
85
|
+
.get(HELYX_SERVICE_NAMES.sessions)
|
|
86
|
+
.delete(sessionId);
|
|
87
|
+
await replyError(context, "This check can no longer be completed. Start a new check if you still require access.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const records = context.services.get(HELYX_SERVICE_NAMES.records);
|
|
91
|
+
await recordAccountGateAttempt(records, guildId, context.userId, sessionId, result.state, challengeType, result.outcome);
|
|
92
|
+
if (!(await ensureTerminalFinalization(context.services, guildId, context.userId, sessionId, result.state, result.outcome)))
|
|
93
|
+
return;
|
|
94
|
+
await finalizeTerminalFinalization(context.services, guildId, sessionId);
|
|
95
|
+
await context.reply({
|
|
96
|
+
ephemeral: true,
|
|
97
|
+
componentsV2: {
|
|
98
|
+
accentColor: result.outcome === "passed" ? GREEN : RED,
|
|
99
|
+
text: [
|
|
100
|
+
{
|
|
101
|
+
content: result.outcome === "passed"
|
|
102
|
+
? "## Check submitted"
|
|
103
|
+
: "## Attempt submitted",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
content: result.outcome === "passed"
|
|
107
|
+
? "Your result is being finalized. If access is not applied, the server team will receive a review."
|
|
108
|
+
: "That answer was not correct. Start a new check when the cooldown ends if another attempt is available.",
|
|
109
|
+
markdown: false,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export async function rerollChallenge(context) {
|
|
116
|
+
if (!context.serverId)
|
|
117
|
+
return replyError(context, "This check is unavailable.");
|
|
118
|
+
const sessions = context.services.get(HELYX_SERVICE_NAMES.sessions);
|
|
119
|
+
const session = await sessions.getFor(context.serverId, context.userId, ACCOUNT_GATE_SESSION_PURPOSE);
|
|
120
|
+
if (!session)
|
|
121
|
+
return replyError(context, "This check has expired.");
|
|
122
|
+
const binding = parseComponentBinding(context.customId, ACCOUNT_GATE_COMPONENTS.rerollPrefix);
|
|
123
|
+
if (!binding ||
|
|
124
|
+
binding.sessionId !== session.sessionId ||
|
|
125
|
+
binding.version !== session.version ||
|
|
126
|
+
binding.step !== parseAccountGateSessionState(session.state).step)
|
|
127
|
+
return replyError(context, "That reroll control is no longer current.");
|
|
128
|
+
const configuration = await requiredRuntimeConfiguration(context.services, context.serverId);
|
|
129
|
+
const currentState = parseAccountGateSessionState(session.state);
|
|
130
|
+
if (await isAccountGateWorkSuppressed(context.services, context.serverId, context.userId, new Date(currentState.startedAt))) {
|
|
131
|
+
await sessions.delete(session.sessionId);
|
|
132
|
+
await replyError(context, "This check can no longer be changed. Start a new check if you still require access.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const state = rerollForAccessibility({
|
|
136
|
+
state: currentState,
|
|
137
|
+
enabledTypes: generatedChallengePool(configuration),
|
|
138
|
+
random: secureRandom(context.services),
|
|
139
|
+
operationContext: `${context.serverId}:${context.userId}:reroll`,
|
|
140
|
+
});
|
|
141
|
+
if (!state) {
|
|
142
|
+
await replyError(context, "A different challenge is not available for this check.");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const updated = await sessions.update({
|
|
146
|
+
sessionId: session.sessionId,
|
|
147
|
+
expectedVersion: session.version,
|
|
148
|
+
state,
|
|
149
|
+
});
|
|
150
|
+
await scheduleSessionExpiry(context.services, {
|
|
151
|
+
guildId: context.serverId,
|
|
152
|
+
userId: context.userId,
|
|
153
|
+
sessionId: session.sessionId,
|
|
154
|
+
expiredAt: session.expiresAt,
|
|
155
|
+
});
|
|
156
|
+
await context.reply(challengeMessage(state, {
|
|
157
|
+
sessionId: session.sessionId,
|
|
158
|
+
version: updated.version,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
export async function openRecallModal(context) {
|
|
162
|
+
if (!context.serverId)
|
|
163
|
+
return replyError(context, "This check is unavailable.");
|
|
164
|
+
const binding = parseComponentBinding(context.customId, ACCOUNT_GATE_COMPONENTS.recallOpenPrefix);
|
|
165
|
+
if (!binding)
|
|
166
|
+
return replyError(context, "This recall challenge has expired.");
|
|
167
|
+
await context.showModal({
|
|
168
|
+
customId: boundComponentId(ACCOUNT_GATE_COMPONENTS.recallSubmitPrefix, binding),
|
|
169
|
+
title: "Enter the recalled code",
|
|
170
|
+
fields: [
|
|
171
|
+
{
|
|
172
|
+
customId: "code",
|
|
173
|
+
label: "Recall code",
|
|
174
|
+
description: "Letter case and surrounding spaces are ignored.",
|
|
175
|
+
style: "short",
|
|
176
|
+
required: true,
|
|
177
|
+
minLength: 5,
|
|
178
|
+
maxLength: 8,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
let session;
|
|
183
|
+
try {
|
|
184
|
+
session = await context.services
|
|
185
|
+
.get(HELYX_SERVICE_NAMES.sessions)
|
|
186
|
+
.getFor(context.serverId, context.userId, ACCOUNT_GATE_SESSION_PURPOSE);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const state = session ? parseAccountGateSessionState(session.state) : null;
|
|
192
|
+
if (!session ||
|
|
193
|
+
!state ||
|
|
194
|
+
binding.sessionId !== session.sessionId ||
|
|
195
|
+
binding.version !== session.version ||
|
|
196
|
+
binding.step !== state.step ||
|
|
197
|
+
state.chain[state.currentStep]?.type !== "modal_recall") {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await context
|
|
201
|
+
.editSourceMessage?.(challengeMessage(state, {
|
|
202
|
+
sessionId: session.sessionId,
|
|
203
|
+
version: session.version,
|
|
204
|
+
}, { redactRecallCode: true }))
|
|
205
|
+
.catch(() => undefined);
|
|
206
|
+
}
|
|
207
|
+
export async function submitRecallModal(context) {
|
|
208
|
+
const binding = parseComponentBinding(context.customId, ACCOUNT_GATE_COMPONENTS.recallSubmitPrefix);
|
|
209
|
+
if (!binding) {
|
|
210
|
+
await replyError(context, "This recall challenge is no longer current.");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
await processAction(context, {
|
|
214
|
+
type: "recall",
|
|
215
|
+
value: context.getField("code"),
|
|
216
|
+
}, binding);
|
|
217
|
+
}
|
|
218
|
+
export async function processBoundOption(context) {
|
|
219
|
+
const binding = parseComponentBinding(context.customId, ACCOUNT_GATE_COMPONENTS.answerPrefix);
|
|
220
|
+
const option = binding ? Number(binding.value) : -1;
|
|
221
|
+
if (!binding || !Number.isInteger(option) || option < 0 || option > 3) {
|
|
222
|
+
await replyError(context, "This answer control is invalid.");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
await processAction(context, { type: "option", option }, binding);
|
|
226
|
+
}
|
|
227
|
+
export async function processBoundSequence(context) {
|
|
228
|
+
const binding = parseComponentBinding(context.customId, ACCOUNT_GATE_COMPONENTS.sequencePrefix);
|
|
229
|
+
if (!binding || !["red", "blue", "green"].includes(binding.value ?? "")) {
|
|
230
|
+
await replyError(context, "This sequence control is invalid.");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
await processAction(context, {
|
|
234
|
+
type: "sequence",
|
|
235
|
+
value: binding.value,
|
|
236
|
+
}, binding);
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=challenge-interactions.js.map
|
package/dist/commands.js
CHANGED
|
@@ -5,6 +5,7 @@ export function accountGateCommands(handlers) {
|
|
|
5
5
|
path: "control.start",
|
|
6
6
|
description: "Start or resume your Access Control check",
|
|
7
7
|
acknowledgement: "deferred-ephemeral",
|
|
8
|
+
permissionId: "account-gate.start",
|
|
8
9
|
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
9
10
|
execute: handlers.start,
|
|
10
11
|
},
|
|
@@ -22,7 +23,7 @@ export function accountGateCommands(handlers) {
|
|
|
22
23
|
path: "control.status",
|
|
23
24
|
description: "Show Access Control health and strict-mode status",
|
|
24
25
|
acknowledgement: "deferred-ephemeral",
|
|
25
|
-
permissionId: "account-gate.
|
|
26
|
+
permissionId: "account-gate.status",
|
|
26
27
|
execute: handlers.status,
|
|
27
28
|
},
|
|
28
29
|
{
|
|
@@ -30,7 +31,7 @@ export function accountGateCommands(handlers) {
|
|
|
30
31
|
path: "control.strict",
|
|
31
32
|
description: "Activate temporary strict mode",
|
|
32
33
|
acknowledgement: "deferred-ephemeral",
|
|
33
|
-
permissionId: "account-gate.
|
|
34
|
+
permissionId: "account-gate.strict",
|
|
34
35
|
execute: handlers.strict,
|
|
35
36
|
},
|
|
36
37
|
];
|
package/dist/contracts.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export declare const ACCOUNT_GATE_IDENTITY: {
|
|
|
2
2
|
readonly packageName: "@helyx/module-account-gate";
|
|
3
3
|
readonly moduleId: "helyx.account-gate";
|
|
4
4
|
readonly name: "Access Control";
|
|
5
|
-
readonly version: "1.
|
|
5
|
+
readonly version: "1.1.1";
|
|
6
6
|
readonly releaseStatus: "live";
|
|
7
7
|
readonly category: "safety";
|
|
8
8
|
readonly icon: "shield-check";
|
package/dist/contracts.js
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RuntimeContext, ScheduledTaskPayload } from "./runtime-dependencies.js";
|
|
2
|
+
export declare function validateJoinDeadlinePayload(input: unknown): ScheduledTaskPayload;
|
|
3
|
+
export declare function validateLeaveRetentionPayload(input: unknown): ScheduledTaskPayload;
|
|
4
|
+
export declare function runLeaveRetention(context: RuntimeContext, task: {
|
|
5
|
+
payload: ScheduledTaskPayload;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
outcome: "completed";
|
|
8
|
+
delayMs?: never;
|
|
9
|
+
safeCode?: never;
|
|
10
|
+
} | {
|
|
11
|
+
outcome: "deferred";
|
|
12
|
+
delayMs: number;
|
|
13
|
+
safeCode: string;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function enforceJoinDeadline(context: RuntimeContext, task: {
|
|
16
|
+
payload: ScheduledTaskPayload;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
outcome: "completed";
|
|
19
|
+
delayMs?: never;
|
|
20
|
+
safeCode?: never;
|
|
21
|
+
} | {
|
|
22
|
+
outcome: "deferred";
|
|
23
|
+
delayMs: number;
|
|
24
|
+
safeCode: string;
|
|
25
|
+
}>;
|
|
26
|
+
export declare function validateSessionExpiryPayload(input: unknown): ScheduledTaskPayload;
|
|
27
|
+
export declare function expireSession(context: RuntimeContext, task: {
|
|
28
|
+
payload: ScheduledTaskPayload;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
outcome: "completed";
|
|
31
|
+
}>;
|
|
32
|
+
//# sourceMappingURL=expiry-workflows.d.ts.map
|