@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
@@ -0,0 +1,281 @@
1
+ import { randomUUID, HELYX_SERVICE_NAMES, ACCOUNT_GATE_IDENTITY, ACCOUNT_GATE_SESSION_PURPOSE, neutralKickDirectMessage, accountGateRecords, isAccountGateWorkSuppressed, parseAccountGateSessionState, emitAccountGateKickLog, successfulKickOutcome, VIOLET, RETENTION_MS, } from "./runtime-dependencies.js";
2
+ import { isCurrentPendingMember, loadMemberSummary, setSummaryDecision, } from "./member-summary.js";
3
+ import { createReview } from "./review-store.js";
4
+ import { dateValue, numberValue } from "./runtime-support.js";
5
+ import { requiredRuntimeConfiguration } from "./setup-presentation.js";
6
+ import { armRecordRetention } from "./task-scheduling.js";
7
+ import { ensureTerminalFinalization, finalizeTerminalFinalization, } from "./terminal-finalization.js";
8
+ export function validateJoinDeadlinePayload(input) {
9
+ if (!input || typeof input !== "object" || Array.isArray(input))
10
+ throw new Error("Account Gate join-deadline payload is invalid");
11
+ const value = input;
12
+ if (typeof value.guildId !== "string" ||
13
+ !/^\d{17,20}$/u.test(value.guildId) ||
14
+ typeof value.memberUserId !== "string" ||
15
+ !/^\d{17,20}$/u.test(value.memberUserId))
16
+ throw new Error("Account Gate join-deadline payload is invalid");
17
+ return {
18
+ guildId: value.guildId,
19
+ memberUserId: value.memberUserId,
20
+ };
21
+ }
22
+ export function validateLeaveRetentionPayload(input) {
23
+ if (!input || typeof input !== "object" || Array.isArray(input))
24
+ throw new Error("Account Gate leave-retention payload is invalid");
25
+ const value = input;
26
+ if (typeof value.guildId !== "string" ||
27
+ !/^\d{17,20}$/u.test(value.guildId) ||
28
+ typeof value.memberUserId !== "string" ||
29
+ !/^\d{17,20}$/u.test(value.memberUserId) ||
30
+ typeof value.summaryId !== "string")
31
+ throw new Error("Account Gate leave-retention payload is invalid");
32
+ return {
33
+ guildId: value.guildId,
34
+ memberUserId: value.memberUserId,
35
+ summaryId: value.summaryId,
36
+ };
37
+ }
38
+ export async function runLeaveRetention(context, task) {
39
+ const guildId = task.payload.guildId;
40
+ const memberUserId = task.payload.memberUserId;
41
+ const summaryId = task.payload.summaryId;
42
+ const records = context.services.get(HELYX_SERVICE_NAMES.records);
43
+ const row = await records.findOne({
44
+ moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
45
+ collection: "member_summaries",
46
+ select: accountGateRecords.member_summaries.readableFields,
47
+ where: { summary_id: summaryId },
48
+ });
49
+ const leftAt = dateValue(row?.left_at);
50
+ const retentionUntil = dateValue(row?.retention_until);
51
+ if (!row ||
52
+ row.guild_id !== guildId ||
53
+ row.member_user_id !== memberUserId ||
54
+ !leftAt ||
55
+ !retentionUntil ||
56
+ (dateValue(row.joined_at)?.getTime() ?? Number.POSITIVE_INFINITY) >
57
+ leftAt.getTime())
58
+ return { outcome: "completed" };
59
+ if (retentionUntil.getTime() > Date.now())
60
+ return {
61
+ outcome: "deferred",
62
+ delayMs: Math.max(100, retentionUntil.getTime() - Date.now()),
63
+ safeCode: "leave-retention-not-due",
64
+ };
65
+ const [member] = await context.services
66
+ .get(HELYX_SERVICE_NAMES.discordResources)
67
+ .resolveMemberSnapshots({ guildId, userIds: [memberUserId] });
68
+ if (member?.exists)
69
+ return { outcome: "completed" };
70
+ await records.deleteMany({
71
+ moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
72
+ collection: "member_summaries",
73
+ where: { summary_id: summaryId },
74
+ limit: 1,
75
+ });
76
+ return { outcome: "completed" };
77
+ }
78
+ export async function enforceJoinDeadline(context, task) {
79
+ const guildId = task.payload.guildId;
80
+ const memberUserId = task.payload.memberUserId;
81
+ const enabled = await context.services
82
+ .get(HELYX_SERVICE_NAMES.installations)
83
+ .isModuleEnabled(guildId, ACCOUNT_GATE_IDENTITY.moduleId);
84
+ if (!enabled)
85
+ return { outcome: "completed" };
86
+ const records = context.services.get(HELYX_SERVICE_NAMES.records);
87
+ const row = await loadMemberSummary(records, guildId, memberUserId);
88
+ if (!row || row.last_decision !== "pending")
89
+ return { outcome: "completed" };
90
+ const generation = numberValue(row.join_generation);
91
+ const joinedAt = dateValue(row.joined_at) ?? new Date(0);
92
+ const deadlineAt = dateValue(row.deadline_at);
93
+ if (!deadlineAt || deadlineAt.getTime() > Date.now()) {
94
+ return deadlineAt
95
+ ? {
96
+ outcome: "deferred",
97
+ delayMs: Math.max(100, deadlineAt.getTime() - Date.now()),
98
+ safeCode: "join-deadline-not-due",
99
+ }
100
+ : { outcome: "completed" };
101
+ }
102
+ if (await isAccountGateWorkSuppressed(context.services, guildId, memberUserId, joinedAt))
103
+ return { outcome: "completed" };
104
+ const [member] = await context.services
105
+ .get(HELYX_SERVICE_NAMES.discordResources)
106
+ .resolveMemberSnapshots({ guildId, userIds: [memberUserId] });
107
+ if (!member?.exists || member.isBot)
108
+ return { outcome: "completed" };
109
+ const sessions = context.services.get(HELYX_SERVICE_NAMES.sessions);
110
+ const active = await sessions.getFor(guildId, memberUserId, ACCOUNT_GATE_SESSION_PURPOSE);
111
+ if (active)
112
+ await sessions.delete(active.sessionId);
113
+ const configuration = await requiredRuntimeConfiguration(context.services, guildId);
114
+ const reason = row.pending_reason === "account_too_new"
115
+ ? "account_too_new"
116
+ : "join_deadline_expired";
117
+ const decision = row.pending_reason === "account_too_new"
118
+ ? "review"
119
+ : configuration.failureAction === "kick"
120
+ ? "kick_pending"
121
+ : configuration.failureAction === "review"
122
+ ? "review"
123
+ : "failed";
124
+ const claimed = await setSummaryDecision(records, guildId, memberUserId, generation, decision);
125
+ if (!claimed)
126
+ return { outcome: "completed" };
127
+ if (decision === "review") {
128
+ await createReview(context.services, guildId, memberUserId, reason, null);
129
+ }
130
+ else if (decision === "kick_pending") {
131
+ const result = await context.services
132
+ .get(HELYX_SERVICE_NAMES.memberEnforcement)
133
+ .kick({
134
+ guildId,
135
+ memberUserId,
136
+ moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
137
+ reason: `Access Control ${reason}`,
138
+ directMessage: {
139
+ embed: {
140
+ title: "Removed after incomplete Access Control",
141
+ description: neutralKickDirectMessage(`Discord server ${guildId}`),
142
+ color: VIOLET,
143
+ },
144
+ },
145
+ revalidate: async () => {
146
+ const [stillEnabled, current, currentSession] = await Promise.all([
147
+ context.services
148
+ .get(HELYX_SERVICE_NAMES.installations)
149
+ .isModuleEnabled(guildId, ACCOUNT_GATE_IDENTITY.moduleId),
150
+ loadMemberSummary(records, guildId, memberUserId),
151
+ sessions.getFor(guildId, memberUserId, ACCOUNT_GATE_SESSION_PURPOSE),
152
+ ]);
153
+ return (stillEnabled &&
154
+ current !== null &&
155
+ numberValue(current.join_generation) === generation &&
156
+ current?.last_decision === "kick_pending" &&
157
+ currentSession === null &&
158
+ !(await isAccountGateWorkSuppressed(context.services, guildId, memberUserId, joinedAt)));
159
+ },
160
+ });
161
+ if (["kicked", "member_missing"].includes(result.outcome)) {
162
+ const committed = await setSummaryDecision(records, guildId, memberUserId, generation, "kicked", "kick_pending");
163
+ if (committed)
164
+ await emitAccountGateKickLog(context.services, guildId, {
165
+ memberUserId,
166
+ outcome: successfulKickOutcome(result.outcome),
167
+ directMessage: result.directMessage,
168
+ source: "Join deadline",
169
+ idempotencyKey: `account-gate-kick:deadline:${memberUserId}:${generation}`,
170
+ });
171
+ }
172
+ }
173
+ return { outcome: "completed" };
174
+ }
175
+ export function validateSessionExpiryPayload(input) {
176
+ if (!input || typeof input !== "object" || Array.isArray(input))
177
+ throw new Error("Account Gate session-expiry payload is invalid");
178
+ const value = input;
179
+ if (typeof value.guildId !== "string" ||
180
+ typeof value.userId !== "string" ||
181
+ typeof value.sessionId !== "string")
182
+ throw new Error("Account Gate session-expiry payload is invalid");
183
+ return {
184
+ guildId: value.guildId,
185
+ userId: value.userId,
186
+ sessionId: value.sessionId,
187
+ };
188
+ }
189
+ export async function expireSession(context, task) {
190
+ const guildId = task.payload.guildId;
191
+ const userId = task.payload.userId;
192
+ const sessionId = task.payload.sessionId;
193
+ const sessions = context.services.get(HELYX_SERVICE_NAMES.sessions);
194
+ const current = await sessions.getFor(guildId, userId, ACCOUNT_GATE_SESSION_PURPOSE);
195
+ if (!current || current.sessionId !== sessionId) {
196
+ return { outcome: "completed" };
197
+ }
198
+ const result = await sessions.expire({
199
+ sessionId,
200
+ guildId,
201
+ userId,
202
+ purpose: ACCOUNT_GATE_SESSION_PURPOSE,
203
+ expiredAt: current.expiresAt,
204
+ });
205
+ if (!result.expired)
206
+ return { outcome: "completed" };
207
+ const expiredState = parseAccountGateSessionState(result.state);
208
+ if (await isAccountGateWorkSuppressed(context.services, guildId, userId, new Date(expiredState.startedAt)))
209
+ return { outcome: "completed" };
210
+ if (expiredState.status === "passed" || expiredState.status === "failed") {
211
+ const configuration = await requiredRuntimeConfiguration(context.services, guildId);
212
+ if (!(await isCurrentPendingMember(context.services, guildId, userId, configuration.verifiedRoleId)))
213
+ return { outcome: "completed" };
214
+ }
215
+ if (expiredState.status === "failed") {
216
+ if (!(await ensureTerminalFinalization(context.services, guildId, userId, sessionId, expiredState, "failed")))
217
+ return { outcome: "completed" };
218
+ await finalizeTerminalFinalization(context.services, guildId, sessionId);
219
+ return { outcome: "completed" };
220
+ }
221
+ if (expiredState.status === "passed") {
222
+ if (!(await ensureTerminalFinalization(context.services, guildId, userId, sessionId, expiredState, "passed")))
223
+ return { outcome: "completed" };
224
+ const now = new Date();
225
+ const challengeType = expiredState.chain.at(-1)?.type;
226
+ if (challengeType) {
227
+ const records = context.services.get(HELYX_SERVICE_NAMES.records);
228
+ const attemptId = randomUUID();
229
+ const retentionUntil = new Date(now.getTime() + RETENTION_MS);
230
+ const recorded = await records.appendUnique({
231
+ moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
232
+ collection: "attempts",
233
+ uniqueBy: ["guild_id", "session_id", "challenge_type"],
234
+ values: {
235
+ attempt_id: attemptId,
236
+ guild_id: guildId,
237
+ member_user_id: userId,
238
+ session_id: sessionId,
239
+ challenge_type: challengeType,
240
+ outcome: "passed",
241
+ reason_code: "challenge_passed_recovered",
242
+ duration_ms: Math.max(0, now.getTime() - Date.parse(expiredState.startedAt)),
243
+ retention_until: retentionUntil,
244
+ created_at: now,
245
+ },
246
+ });
247
+ if (recorded.created)
248
+ await armRecordRetention(records, guildId, "attempts", "attempt_id", attemptId, retentionUntil);
249
+ }
250
+ await finalizeTerminalFinalization(context.services, guildId, sessionId);
251
+ return { outcome: "completed" };
252
+ }
253
+ const challengeType = expiredState.chain[expiredState.currentStep]?.type;
254
+ if (challengeType) {
255
+ const now = new Date();
256
+ const records = context.services.get(HELYX_SERVICE_NAMES.records);
257
+ const attemptId = randomUUID();
258
+ const retentionUntil = new Date(now.getTime() + RETENTION_MS);
259
+ const recorded = await records.appendUnique({
260
+ moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
261
+ collection: "attempts",
262
+ uniqueBy: ["guild_id", "session_id", "challenge_type"],
263
+ values: {
264
+ attempt_id: attemptId,
265
+ guild_id: guildId,
266
+ member_user_id: userId,
267
+ session_id: sessionId,
268
+ challenge_type: challengeType,
269
+ outcome: "expired",
270
+ reason_code: "session_expired",
271
+ duration_ms: Math.max(0, now.getTime() - Date.parse(expiredState.startedAt)),
272
+ retention_until: retentionUntil,
273
+ created_at: now,
274
+ },
275
+ });
276
+ if (recorded.created)
277
+ await armRecordRetention(records, guildId, "attempts", "attempt_id", attemptId, retentionUntil);
278
+ }
279
+ return { outcome: "completed" };
280
+ }
281
+ //# sourceMappingURL=expiry-workflows.js.map
@@ -0,0 +1,5 @@
1
+ import type { InteractionSessionService, MessageReactionAddContext, PrivilegedMessageCreateContext, ServiceAccess, ChallengeAction } from "./runtime-dependencies.js";
2
+ export declare function handlePasswordEntry(context: PrivilegedMessageCreateContext): Promise<void>;
3
+ export declare function handleEmojiEntry(context: MessageReactionAddContext): Promise<void>;
4
+ export declare function processGatewayAction(services: ServiceAccess, guildId: string, userId: string, session: Awaited<ReturnType<InteractionSessionService["getFor"]>> & {}, action: ChallengeAction): Promise<void>;
5
+ //# sourceMappingURL=gateway-interactions.d.ts.map
@@ -0,0 +1,124 @@
1
+ import { HELYX_SERVICE_NAMES, ACCOUNT_GATE_SESSION_PURPOSE, applyChallengeAction, getGatePanelRuntime, parseAccountGateSessionState, verifyPasswordEntry, emitLog, } from "./runtime-dependencies.js";
2
+ import { isCurrentPendingMember } from "./member-summary.js";
3
+ import { recordAccountGateAttempt } from "./member-restart.js";
4
+ import { canonicalEmojiKey, staticResumeMessage } from "./runtime-support.js";
5
+ import { configurationForPanel, getConfiguration, requiredRuntimeConfiguration, validateAccountGateSetup, } from "./setup-presentation.js";
6
+ import { scheduleSessionExpiry } from "./task-scheduling.js";
7
+ import { ensureTerminalFinalization, finalizeTerminalFinalization, } from "./terminal-finalization.js";
8
+ export async function handlePasswordEntry(context) {
9
+ if (context.userIsBot || context.webhookId !== null)
10
+ return;
11
+ const configuration = await getConfiguration(context.services, context.serverId);
12
+ if (!configuration || configuration.passwordChannelId !== context.channelId)
13
+ return;
14
+ const passwordConfiguration = configurationForPanel(configuration, "password");
15
+ if ((await validateAccountGateSetup(context.services, context.serverId, passwordConfiguration, false)).length > 0)
16
+ return;
17
+ const sessions = context.services.get(HELYX_SERVICE_NAMES.sessions);
18
+ const session = await sessions.getFor(context.serverId, context.userId, ACCOUNT_GATE_SESSION_PURPOSE);
19
+ if (!session)
20
+ return;
21
+ const state = parseAccountGateSessionState(session.state);
22
+ if (state.chain[state.currentStep]?.type !== "password_entry")
23
+ return;
24
+ await context.deleteSource();
25
+ const store = context.services.get(HELYX_SERVICE_NAMES.records);
26
+ const secret = state.panelId
27
+ ? ((await getGatePanelRuntime(store, context.serverId, state.panelId))
28
+ ?.password ?? null)
29
+ : null;
30
+ const matched = secret !== null &&
31
+ (await verifyPasswordEntry(context.content, secret.encodedHash, secret));
32
+ await processGatewayAction(context.services, context.serverId, context.userId, session, matched
33
+ ? { type: "static-complete", challenge: "password_entry" }
34
+ : { type: "static-failed", challenge: "password_entry" });
35
+ }
36
+ export async function handleEmojiEntry(context) {
37
+ if (context.reactionType !== "normal")
38
+ return;
39
+ const configuration = await getConfiguration(context.services, context.serverId);
40
+ if (!configuration ||
41
+ !configuration.enabledChallengeTypes.includes("emoji_entry") ||
42
+ configuration.emojiChannelId !== context.channelId ||
43
+ configuration.emojiMessageId !== context.messageId ||
44
+ canonicalEmojiKey(configuration.emojiIdentifier) !== context.emojiKey)
45
+ return;
46
+ if ((await validateAccountGateSetup(context.services, context.serverId, configuration, false)).length > 0)
47
+ return;
48
+ const [member] = await context.services
49
+ .get(HELYX_SERVICE_NAMES.discordResources)
50
+ .resolveMemberSnapshots({
51
+ guildId: context.serverId,
52
+ userIds: [context.userId],
53
+ });
54
+ if (!member?.exists || member.isBot)
55
+ return;
56
+ const sessions = context.services.get(HELYX_SERVICE_NAMES.sessions);
57
+ const session = await sessions.getFor(context.serverId, context.userId, ACCOUNT_GATE_SESSION_PURPOSE);
58
+ if (!session)
59
+ return;
60
+ const state = parseAccountGateSessionState(session.state);
61
+ if (state.chain[state.currentStep]?.type !== "emoji_entry")
62
+ return;
63
+ await processGatewayAction(context.services, context.serverId, context.userId, session, { type: "static-complete", challenge: "emoji_entry" });
64
+ }
65
+ export async function processGatewayAction(services, guildId, userId, session, action) {
66
+ if (!session)
67
+ return;
68
+ const configuration = await requiredRuntimeConfiguration(services, guildId);
69
+ if (!(await isCurrentPendingMember(services, guildId, userId, configuration.verifiedRoleId)))
70
+ return;
71
+ const state = parseAccountGateSessionState(session.state);
72
+ const result = applyChallengeAction(state, action);
73
+ if (result.outcome === "progress" || result.outcome === "stale")
74
+ return;
75
+ const advanced = await services
76
+ .get(HELYX_SERVICE_NAMES.sessions)
77
+ .advance({
78
+ sessionId: session.sessionId,
79
+ guildId,
80
+ userId,
81
+ purpose: ACCOUNT_GATE_SESSION_PURPOSE,
82
+ expectedVersion: session.version,
83
+ expectedStep: state.step,
84
+ nextStep: state.step + 1,
85
+ state: { ...result.state, step: state.step + 1 },
86
+ });
87
+ await scheduleSessionExpiry(services, {
88
+ guildId,
89
+ userId,
90
+ sessionId: session.sessionId,
91
+ expiredAt: session.expiresAt,
92
+ });
93
+ if (result.outcome === "passed" || result.outcome === "failed") {
94
+ await recordAccountGateAttempt(services.get(HELYX_SERVICE_NAMES.records), guildId, userId, session.sessionId, result.state, state.chain[state.currentStep].type, result.outcome);
95
+ if (!(await ensureTerminalFinalization(services, guildId, userId, session.sessionId, result.state, result.outcome)))
96
+ return;
97
+ }
98
+ if (result.outcome === "advanced") {
99
+ let delivery = "failed";
100
+ try {
101
+ delivery = (await services
102
+ .get(HELYX_SERVICE_NAMES.discordMessages)
103
+ .sendDirect({
104
+ userId,
105
+ response: staticResumeMessage(guildId, configuration.gateChannelId),
106
+ })).outcome;
107
+ }
108
+ catch {
109
+ // A failed DM must not expose user-specific challenge controls publicly.
110
+ }
111
+ await emitLog(services, guildId, {
112
+ eventId: "account-gate-resume-notice",
113
+ summary: "Private Access Control resume guidance was attempted.",
114
+ details: [
115
+ { label: "Member ID", value: userId },
116
+ { label: "Delivery", value: delivery },
117
+ ],
118
+ idempotencyKey: `account-gate-resume:${session.sessionId}:${String(advanced.version)}`,
119
+ });
120
+ return;
121
+ }
122
+ await finalizeTerminalFinalization(services, guildId, session.sessionId);
123
+ }
124
+ //# sourceMappingURL=gateway-interactions.js.map
@@ -0,0 +1,10 @@
1
+ import type { HealthResult, RuntimeContext } from "@helyx/sdk";
2
+ export declare class AccountGateHealth {
3
+ #private;
4
+ start(context: RuntimeContext): void;
5
+ stop(): void;
6
+ recordTaskSuccess(taskKind: string): void;
7
+ recordTaskFailure(taskKind: string, code: string): void;
8
+ snapshot(): HealthResult;
9
+ }
10
+ //# sourceMappingURL=health.d.ts.map
package/dist/health.js ADDED
@@ -0,0 +1,55 @@
1
+ import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
2
+ const REQUIRED_SERVICES = [
3
+ HELYX_SERVICE_NAMES.configuration,
4
+ HELYX_SERVICE_NAMES.discordMessages,
5
+ HELYX_SERVICE_NAMES.discordResources,
6
+ HELYX_SERVICE_NAMES.installations,
7
+ HELYX_SERVICE_NAMES.memberEnforcement,
8
+ HELYX_SERVICE_NAMES.memberRoles,
9
+ HELYX_SERVICE_NAMES.records,
10
+ HELYX_SERVICE_NAMES.resourceAuthorization,
11
+ HELYX_SERVICE_NAMES.scheduledTasks,
12
+ HELYX_SERVICE_NAMES.secureRandom,
13
+ HELYX_SERVICE_NAMES.sessions,
14
+ ];
15
+ export class AccountGateHealth {
16
+ #running = false;
17
+ #missingServices = [];
18
+ #taskFailures = new Map();
19
+ start(context) {
20
+ this.#running = true;
21
+ this.#missingServices = REQUIRED_SERVICES.filter((service) => !context.services.has(service));
22
+ this.#taskFailures.clear();
23
+ }
24
+ stop() {
25
+ this.#running = false;
26
+ this.#missingServices = [];
27
+ this.#taskFailures.clear();
28
+ }
29
+ recordTaskSuccess(taskKind) {
30
+ this.#taskFailures.delete(taskKind);
31
+ }
32
+ recordTaskFailure(taskKind, code) {
33
+ this.#taskFailures.set(taskKind, code);
34
+ }
35
+ snapshot() {
36
+ if (!this.#running)
37
+ return { status: "unhealthy", message: "Access Control is stopped" };
38
+ if (this.#missingServices.length > 0)
39
+ return {
40
+ status: "unhealthy",
41
+ message: "Access Control runtime services are unavailable",
42
+ };
43
+ const failureCode = this.#taskFailures.values().next().value;
44
+ if (failureCode)
45
+ return {
46
+ status: "degraded",
47
+ message: `Access Control recovery requires attention (${failureCode})`,
48
+ };
49
+ return {
50
+ status: "healthy",
51
+ message: "Access Control durable workflows are registered",
52
+ };
53
+ }
54
+ }
55
+ //# sourceMappingURL=health.js.map
package/dist/index.d.ts CHANGED
@@ -1,27 +1,16 @@
1
- import type { MessageResponse, ModuleRecordService, ModuleRecordValue, ServiceAccess } from "@helyx/sdk";
2
1
  import { generateChallengeChain, validateServerQuestion } from "./challenges.js";
3
- import { ACCOUNT_GATE_IDENTITY } from "./contracts.js";
4
- import type { AccountGateSessionState, Challenge } from "./contracts.js";
5
2
  import { parseAccountGateConfiguration, validateRuntimeCapabilities } from "./configuration.js";
6
- import type { AccountGateConfiguration } from "./configuration.js";
3
+ import { ACCOUNT_GATE_IDENTITY } from "./contracts.js";
7
4
  import { accountGateRecords } from "./records.js";
5
+ import { validateAccountGateSetup } from "./setup-presentation.js";
6
+ import { ensureTerminalFinalization } from "./terminal-finalization.js";
8
7
  import { applyChallengeAction, createAccountGateSessionState } from "./workflow.js";
9
8
  export declare const accountGateModule: import("@helyx/sdk").ModuleDefinition;
10
9
  export default accountGateModule;
11
10
  export { ACCOUNT_GATE_IDENTITY, accountGateRecords, applyChallengeAction, createAccountGateSessionState, generateChallengeChain, parseAccountGateConfiguration, validateRuntimeCapabilities, validateServerQuestion, };
12
- export declare function isAccountTooYoung(accountCreatedAt: Date, joinedAt: Date, minimumAccountAgeDays: number): boolean;
13
- export declare function hasPreviousSuccess(value: unknown): boolean;
14
- export declare function resumeRevokedMember(services: ServiceAccess, guildId: string, memberUserId: string, summary: Readonly<Record<string, ModuleRecordValue>>, configuration: AccountGateConfiguration, correlationId: string, now?: Date): Promise<Readonly<Record<string, ModuleRecordValue>> | null>;
15
- export declare function recordAccountGateAttempt(records: ModuleRecordService, guildId: string, memberUserId: string, sessionId: string, state: AccountGateSessionState, challengeType: Challenge["type"], outcome: "passed" | "failed", now?: Date): Promise<void>;
16
- export declare function ensureTerminalFinalization(services: ServiceAccess, guildId: string, memberUserId: string, sessionId: string, state: AccountGateSessionState, outcome: "passed" | "failed"): Promise<boolean>;
17
- export declare function validateAccountGateSetup(services: ServiceAccess, guildId: string, configuration: AccountGateConfiguration, throwOnError?: boolean, allowMissingPanel?: boolean): Promise<readonly string[]>;
18
- interface ComponentBinding {
19
- sessionId: string;
20
- version: bigint;
21
- step: number;
22
- value?: string;
23
- }
24
- export declare function boundComponentId(prefix: string, binding: ComponentBinding): string;
25
- export declare function parseComponentBinding(customId: string | undefined, prefix: string): ComponentBinding | null;
26
- export declare function staticResumeMessage(guildId: string, gateChannelId: string): MessageResponse;
11
+ export { hasPreviousSuccess, isAccountTooYoung } from "./member-lifecycle.js";
12
+ export { recordAccountGateAttempt, resumeRevokedMember, } from "./member-restart.js";
13
+ export { ensureTerminalFinalization };
14
+ export { validateAccountGateSetup };
15
+ export { boundComponentId, parseComponentBinding, staticResumeMessage, } from "./runtime-support.js";
27
16
  //# sourceMappingURL=index.d.ts.map