@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.
- package/CHANGELOG.md +27 -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 +1 -1
- package/migrations/0002_panel_mutation_claims.sql +34 -0
- 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
|
package/dist/health.d.ts
ADDED
|
@@ -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
|
|
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
|
|
13
|
-
export
|
|
14
|
-
export
|
|
15
|
-
export
|
|
16
|
-
export
|
|
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
|