@helyx/module-account-gate 1.0.15 → 1.1.3

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +55 -90
  3. package/dist/challenge-interactions.d.ts +12 -0
  4. package/dist/challenge-interactions.js +238 -0
  5. package/dist/commands.js +3 -2
  6. package/dist/contracts.d.ts +1 -1
  7. package/dist/contracts.js +1 -1
  8. package/dist/expiry-workflows.d.ts +32 -0
  9. package/dist/expiry-workflows.js +281 -0
  10. package/dist/gateway-interactions.d.ts +5 -0
  11. package/dist/gateway-interactions.js +124 -0
  12. package/dist/health.d.ts +10 -0
  13. package/dist/health.js +55 -0
  14. package/dist/index.d.ts +8 -19
  15. package/dist/index.js +35 -3157
  16. package/dist/member-lifecycle.d.ts +21 -0
  17. package/dist/member-lifecycle.js +305 -0
  18. package/dist/member-resource.js +14 -74
  19. package/dist/member-restart.d.ts +4 -0
  20. package/dist/member-restart.js +76 -0
  21. package/dist/member-revocation.d.ts +21 -0
  22. package/dist/member-revocation.js +127 -0
  23. package/dist/member-start.d.ts +5 -0
  24. package/dist/member-start.js +199 -0
  25. package/dist/member-summary.d.ts +47 -0
  26. package/dist/member-summary.js +200 -0
  27. package/dist/panel-maintenance.d.ts +26 -0
  28. package/dist/panel-maintenance.js +143 -0
  29. package/dist/panel-model.d.ts +47 -0
  30. package/dist/panel-model.js +128 -0
  31. package/dist/panel-mutations.d.ts +24 -0
  32. package/dist/panel-mutations.js +89 -0
  33. package/dist/panel-presentation.d.ts +8 -0
  34. package/dist/panel-presentation.js +67 -0
  35. package/dist/panel-resource.d.ts +2 -13
  36. package/dist/panel-resource.js +122 -310
  37. package/dist/panel-runtime.d.ts +8 -0
  38. package/dist/panel-runtime.js +34 -0
  39. package/dist/records.d.ts +42 -2
  40. package/dist/records.js +31 -0
  41. package/dist/review-decisions.d.ts +3 -0
  42. package/dist/review-decisions.js +274 -0
  43. package/dist/review-delivery.d.ts +34 -0
  44. package/dist/review-delivery.js +252 -0
  45. package/dist/review-presentation.d.ts +3 -0
  46. package/dist/review-presentation.js +28 -0
  47. package/dist/review-store.d.ts +4 -0
  48. package/dist/review-store.js +94 -0
  49. package/dist/role-recovery.d.ts +23 -0
  50. package/dist/role-recovery.js +176 -0
  51. package/dist/runtime-dependencies.d.ts +32 -0
  52. package/dist/runtime-dependencies.js +27 -0
  53. package/dist/runtime-support.d.ts +19 -0
  54. package/dist/runtime-support.js +74 -0
  55. package/dist/setup-presentation.d.ts +15 -0
  56. package/dist/setup-presentation.js +378 -0
  57. package/dist/strict-mode.d.ts +17 -0
  58. package/dist/strict-mode.js +154 -0
  59. package/dist/task-contributions.d.ts +4 -0
  60. package/dist/task-contributions.js +113 -0
  61. package/dist/task-scheduling.d.ts +12 -0
  62. package/dist/task-scheduling.js +79 -0
  63. package/dist/terminal-finalization.d.ts +12 -0
  64. package/dist/terminal-finalization.js +238 -0
  65. package/dist/workflow-fences.d.ts +6 -0
  66. package/dist/workflow-fences.js +73 -0
  67. package/manifest.json +1 -1
  68. package/migrations/0002_panel_mutation_claims.sql +34 -0
  69. package/package.json +5 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [1dfbf5b]
8
+ - @helyx/module-manifest@0.9.0
9
+
10
+ ## 1.1.2
11
+
12
+ ### Patch Changes
13
+
14
+ - Republish the current shared platform and Account Gate artifacts so their
15
+ immutable npm packages match the reviewed runtime permission contracts.
16
+
17
+ ## 1.1.1
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies [bbc4eee]
22
+ - @helyx/sdk@0.10.0
23
+
24
+ ## 1.1.0
25
+
26
+ ### Minor Changes
27
+
28
+ - 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.
29
+
3
30
  ## 1.0.15
4
31
 
5
32
  ### Patch Changes
package/README.md CHANGED
@@ -1,90 +1,55 @@
1
- # Access Control
2
-
3
- Access Control asks a joining member to complete short Discord interaction checks
4
- before Helyx grants the configured access role. It is a local
5
- human-interaction check: it can deter basic automated accounts, but it does not
6
- detect devices, network risk, alternate accounts or ban evasion.
7
-
8
- The default flow uses two different generated challenges and allows three
9
- attempts. Eight challenge types are implemented: Quick Sum, Button Sequence,
10
- Number Order, Symbol Match, Modal Recall, Server Question, Password Entry and
11
- Emoji Entry. Password Entry and Emoji Entry are static gates and are excluded
12
- from generated challenge allocation. A server selects one access type:
13
- generated Challenges, Password Entry or Emoji Entry.
14
-
15
- ## Permissions and Discord configuration
16
-
17
- Access Control requires View Channel, Send Messages, Embed Links, Read Message
18
- History and Manage Roles. Kick Members is required only when final failure is
19
- configured to kick. Password Entry also requires Manage Messages, Guild
20
- Messages and the privileged Message Content intent; its channel health check
21
- explicitly requires View Channel, Read Message History and Manage Messages.
22
- Emoji Entry may require Add Reactions and the Guild Message Reactions intent.
23
-
24
- Helyx must be above the access role and every temporary role it removes. The
25
- module adds the access role before removing temporary roles so a partial
26
- Discord failure does not remove the member's only access route.
27
-
28
- Discord channel permissions are the sole authority for which channels pending
29
- members can see. Access Control validates its configured gate, Password Entry,
30
- Emoji Entry and staff-review channels, but it does not maintain a second
31
- dashboard allowlist of every other visible server channel. Administrators
32
- should configure the temporary pre-gate role's Discord permissions directly.
33
- The managed Start panel may be absent during the initial
34
- activation-and-publish flow, but runtime joins fail closed until its saved
35
- message exists. Enabling Server Question likewise requires at least one
36
- enabled managed question.
37
-
38
- The review-channel audience check rejects `@everyone` and every configured
39
- temporary pre-gate role. The current Discord inspection contract only reports
40
- roles explicitly requested by the module and Access Control has no configured
41
- staff-role allowlist, so it cannot prove that every other visible role is a
42
- staff role; administrators must keep that channel staff-only.
43
-
44
- ## Failure and strict-mode behaviour
45
-
46
- Staff review is the default final-failure action. When kick is deliberately
47
- selected, Helyx first makes a best-effort attempt to send a neutral direct
48
- message explaining that Access Control was not completed. It then rechecks
49
- the current decision and kicks as soon as possible; a closed direct-message
50
- channel does not prevent the kick. The target is within 30 seconds and there is
51
- no deliberate delay.
52
-
53
- Authorised Access Control managers may activate temporary strict mode. It lasts
54
- 15 minutes by default, can be configured from 5 to 60 minutes, adds one
55
- generated challenge without exceeding four, and expires automatically. It does
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.manage",
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.manage",
34
+ permissionId: "account-gate.strict",
34
35
  execute: handlers.strict,
35
36
  },
36
37
  ];
@@ -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.0.15";
5
+ readonly version: "1.1.3";
6
6
  readonly releaseStatus: "live";
7
7
  readonly category: "safety";
8
8
  readonly icon: "shield-check";
package/dist/contracts.js CHANGED
@@ -2,7 +2,7 @@ export const ACCOUNT_GATE_IDENTITY = {
2
2
  packageName: "@helyx/module-account-gate",
3
3
  moduleId: "helyx.account-gate",
4
4
  name: "Access Control",
5
- version: "1.0.15",
5
+ version: "1.1.3",
6
6
  releaseStatus: "live",
7
7
  category: "safety",
8
8
  icon: "shield-check",
@@ -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