@helyx/module-account-gate 1.0.14 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +55 -90
- package/dist/challenge-interactions.d.ts +12 -0
- package/dist/challenge-interactions.js +238 -0
- package/dist/commands.js +3 -2
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.js +1 -1
- package/dist/expiry-workflows.d.ts +32 -0
- package/dist/expiry-workflows.js +281 -0
- package/dist/gateway-interactions.d.ts +5 -0
- package/dist/gateway-interactions.js +124 -0
- package/dist/health.d.ts +10 -0
- package/dist/health.js +55 -0
- package/dist/index.d.ts +8 -19
- package/dist/index.js +35 -3157
- package/dist/member-lifecycle.d.ts +21 -0
- package/dist/member-lifecycle.js +305 -0
- package/dist/member-resource.js +14 -74
- package/dist/member-restart.d.ts +4 -0
- package/dist/member-restart.js +76 -0
- package/dist/member-revocation.d.ts +21 -0
- package/dist/member-revocation.js +127 -0
- package/dist/member-start.d.ts +5 -0
- package/dist/member-start.js +199 -0
- package/dist/member-summary.d.ts +47 -0
- package/dist/member-summary.js +200 -0
- package/dist/panel-maintenance.d.ts +26 -0
- package/dist/panel-maintenance.js +143 -0
- package/dist/panel-model.d.ts +47 -0
- package/dist/panel-model.js +128 -0
- package/dist/panel-mutations.d.ts +24 -0
- package/dist/panel-mutations.js +89 -0
- package/dist/panel-presentation.d.ts +8 -0
- package/dist/panel-presentation.js +67 -0
- package/dist/panel-resource.d.ts +2 -13
- package/dist/panel-resource.js +122 -310
- package/dist/panel-runtime.d.ts +8 -0
- package/dist/panel-runtime.js +34 -0
- package/dist/records.d.ts +42 -2
- package/dist/records.js +31 -0
- package/dist/review-decisions.d.ts +3 -0
- package/dist/review-decisions.js +274 -0
- package/dist/review-delivery.d.ts +34 -0
- package/dist/review-delivery.js +252 -0
- package/dist/review-presentation.d.ts +3 -0
- package/dist/review-presentation.js +28 -0
- package/dist/review-store.d.ts +4 -0
- package/dist/review-store.js +94 -0
- package/dist/role-recovery.d.ts +23 -0
- package/dist/role-recovery.js +176 -0
- package/dist/runtime-dependencies.d.ts +32 -0
- package/dist/runtime-dependencies.js +27 -0
- package/dist/runtime-support.d.ts +19 -0
- package/dist/runtime-support.js +74 -0
- package/dist/setup-presentation.d.ts +15 -0
- package/dist/setup-presentation.js +378 -0
- package/dist/strict-mode.d.ts +17 -0
- package/dist/strict-mode.js +154 -0
- package/dist/task-contributions.d.ts +4 -0
- package/dist/task-contributions.js +113 -0
- package/dist/task-scheduling.d.ts +12 -0
- package/dist/task-scheduling.js +79 -0
- package/dist/terminal-finalization.d.ts +12 -0
- package/dist/terminal-finalization.js +238 -0
- package/dist/workflow-fences.d.ts +6 -0
- package/dist/workflow-fences.js +73 -0
- package/manifest.json +9 -2
- package/migrations/0002_panel_mutation_claims.sql +34 -0
- package/package.json +5 -4
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES, ACCOUNT_GATE_IDENTITY, JOIN_DEADLINE_TASK, SESSION_EXPIRY_TASK, REVIEW_DELIVERY_TASK, TERMINAL_FINALIZATION_TASK, } from "./runtime-dependencies.js";
|
|
2
|
+
export async function scheduleJoinDeadline(services, guildId, memberUserId, generation, deadlineAt) {
|
|
3
|
+
await services
|
|
4
|
+
.get(HELYX_SERVICE_NAMES.scheduledTasks)
|
|
5
|
+
.schedule({
|
|
6
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
7
|
+
taskKind: JOIN_DEADLINE_TASK,
|
|
8
|
+
guildId,
|
|
9
|
+
idempotencyKey: `${guildId}:${memberUserId}:join:${generation}`,
|
|
10
|
+
payload: {
|
|
11
|
+
guildId,
|
|
12
|
+
memberUserId,
|
|
13
|
+
},
|
|
14
|
+
scheduledFor: deadlineAt,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export async function scheduleSessionExpiry(services, input) {
|
|
18
|
+
await services
|
|
19
|
+
.get(HELYX_SERVICE_NAMES.scheduledTasks)
|
|
20
|
+
.schedule({
|
|
21
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
22
|
+
taskKind: SESSION_EXPIRY_TASK,
|
|
23
|
+
guildId: input.guildId,
|
|
24
|
+
idempotencyKey: `${input.sessionId}:expiry`,
|
|
25
|
+
payload: {
|
|
26
|
+
guildId: input.guildId,
|
|
27
|
+
userId: input.userId,
|
|
28
|
+
sessionId: input.sessionId,
|
|
29
|
+
},
|
|
30
|
+
scheduledFor: input.expiredAt,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export async function scheduleReviewDelivery(services, guildId, reviewId, scheduledFor) {
|
|
34
|
+
await services
|
|
35
|
+
.get(HELYX_SERVICE_NAMES.scheduledTasks)
|
|
36
|
+
.schedule({
|
|
37
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
38
|
+
taskKind: REVIEW_DELIVERY_TASK,
|
|
39
|
+
guildId,
|
|
40
|
+
idempotencyKey: `${guildId}:${reviewId}:delivery`,
|
|
41
|
+
payload: { guildId, reviewId },
|
|
42
|
+
scheduledFor,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export async function scheduleTerminalFinalization(services, guildId, finalizationId, scheduledFor) {
|
|
46
|
+
await services
|
|
47
|
+
.get(HELYX_SERVICE_NAMES.scheduledTasks)
|
|
48
|
+
.schedule({
|
|
49
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
50
|
+
taskKind: TERMINAL_FINALIZATION_TASK,
|
|
51
|
+
guildId,
|
|
52
|
+
idempotencyKey: `${guildId}:${finalizationId}:terminal`,
|
|
53
|
+
payload: { guildId, finalizationId },
|
|
54
|
+
scheduledFor,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function armRecordRetention(records, guildId, collection, idField, id, scheduledFor) {
|
|
58
|
+
await records.updateOne({
|
|
59
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
60
|
+
collection,
|
|
61
|
+
where: { [idField]: id },
|
|
62
|
+
expected: { field: idField, value: id },
|
|
63
|
+
values: { retention_until: scheduledFor },
|
|
64
|
+
retention: [
|
|
65
|
+
{
|
|
66
|
+
collection,
|
|
67
|
+
where: { guild_id: guildId, [idField]: id },
|
|
68
|
+
scheduledFor,
|
|
69
|
+
idempotencyKey: `account-gate-retention:${collection}:${id}`,
|
|
70
|
+
audit: {
|
|
71
|
+
action: `account_gate.${collection}_retention`,
|
|
72
|
+
targetType: `account_gate_${collection}`,
|
|
73
|
+
targetId: id,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=task-scheduling.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ModuleRecordService, RuntimeContext, ScheduledTaskPayload, ServiceAccess, AccountGateSessionState, AccountGateConfiguration } from "./runtime-dependencies.js";
|
|
2
|
+
export declare function ensureTerminalFinalization(services: ServiceAccess, guildId: string, memberUserId: string, sessionId: string, state: AccountGateSessionState, outcome: "passed" | "failed"): Promise<boolean>;
|
|
3
|
+
export declare function validateTerminalFinalizationPayload(input: unknown): ScheduledTaskPayload;
|
|
4
|
+
export declare function runTerminalFinalizationTask(context: RuntimeContext, task: {
|
|
5
|
+
payload: ScheduledTaskPayload;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
outcome: "completed";
|
|
8
|
+
}>;
|
|
9
|
+
export declare function finalizeTerminalFinalization(services: ServiceAccess, guildId: string, finalizationId: string): Promise<void>;
|
|
10
|
+
export declare function finishTerminalRecord(records: ModuleRecordService, row: Readonly<Record<string, unknown>>, status: "completed" | "cancelled"): Promise<void>;
|
|
11
|
+
export declare function applyDurableFailureAction(services: ServiceAccess, configuration: AccountGateConfiguration, guildId: string, memberUserId: string, generation: number, terminal: Readonly<Record<string, unknown>>): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=terminal-finalization.d.ts.map
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES, ACCOUNT_GATE_IDENTITY, ACCOUNT_GATE_SESSION_PURPOSE, applyRoleTransition, neutralKickDirectMessage, accountGateRecords, isAccountGateWorkSuppressed, emitAccountGateCompletedLog, emitAccountGateKickLog, successfulKickOutcome, VIOLET, TERMINAL_FINALIZATION_TASK, RETENTION_MS, } from "./runtime-dependencies.js";
|
|
2
|
+
import { loadMemberSummary, markSummaryPassed, recordFailureSummary, setSummaryDecision, } from "./member-summary.js";
|
|
3
|
+
import { createReview } from "./review-store.js";
|
|
4
|
+
import { compensateVerifiedRoleAddition, terminalPassStillCurrent, } from "./workflow-fences.js";
|
|
5
|
+
import { persistPartialTransition } from "./role-recovery.js";
|
|
6
|
+
import { dateValue, numberValue, stringValue } from "./runtime-support.js";
|
|
7
|
+
import { requiredRuntimeConfiguration } from "./setup-presentation.js";
|
|
8
|
+
import { armRecordRetention, scheduleTerminalFinalization, } from "./task-scheduling.js";
|
|
9
|
+
export async function ensureTerminalFinalization(services, guildId, memberUserId, sessionId, state, outcome) {
|
|
10
|
+
const records = services.get(HELYX_SERVICE_NAMES.records);
|
|
11
|
+
const summary = await loadMemberSummary(records, guildId, memberUserId);
|
|
12
|
+
if (!summary ||
|
|
13
|
+
!["pending", "finalizing"].includes(stringValue(summary.last_decision)))
|
|
14
|
+
return false;
|
|
15
|
+
const generation = state.joinGeneration;
|
|
16
|
+
if (numberValue(summary.join_generation) !== generation)
|
|
17
|
+
return false;
|
|
18
|
+
const challenge = outcome === "passed" ? state.chain.at(-1) : state.chain[state.currentStep];
|
|
19
|
+
if (!challenge)
|
|
20
|
+
return false;
|
|
21
|
+
const now = new Date();
|
|
22
|
+
const retentionUntil = new Date(now.getTime() + RETENTION_MS);
|
|
23
|
+
const created = await records.appendUnique({
|
|
24
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
25
|
+
collection: "terminal_finalizations",
|
|
26
|
+
uniqueBy: ["guild_id", "session_id"],
|
|
27
|
+
values: {
|
|
28
|
+
finalization_id: sessionId,
|
|
29
|
+
guild_id: guildId,
|
|
30
|
+
member_user_id: memberUserId,
|
|
31
|
+
session_id: sessionId,
|
|
32
|
+
join_generation: generation,
|
|
33
|
+
outcome,
|
|
34
|
+
challenge_type: challenge.type,
|
|
35
|
+
challenge_types_json: JSON.stringify(state.chain.map(({ type }) => type)),
|
|
36
|
+
work_created_at: new Date(state.startedAt),
|
|
37
|
+
status: "pending",
|
|
38
|
+
revision: 1,
|
|
39
|
+
retention_until: retentionUntil,
|
|
40
|
+
created_at: now,
|
|
41
|
+
updated_at: now,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
if (created.created)
|
|
45
|
+
await armRecordRetention(records, guildId, "terminal_finalizations", "finalization_id", sessionId, retentionUntil);
|
|
46
|
+
if (summary.last_decision === "pending") {
|
|
47
|
+
const revision = numberValue(summary.revision);
|
|
48
|
+
const claimed = await records.updateOne({
|
|
49
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
50
|
+
collection: "member_summaries",
|
|
51
|
+
where: { guild_id: guildId, member_user_id: memberUserId },
|
|
52
|
+
expected: { field: "revision", value: revision },
|
|
53
|
+
values: {
|
|
54
|
+
last_decision: "finalizing",
|
|
55
|
+
revision: revision + 1,
|
|
56
|
+
updated_at: now,
|
|
57
|
+
},
|
|
58
|
+
schedule: {
|
|
59
|
+
taskKind: TERMINAL_FINALIZATION_TASK,
|
|
60
|
+
guildId,
|
|
61
|
+
idempotencyKey: `${guildId}:${sessionId}:terminal`,
|
|
62
|
+
payload: { guildId, finalizationId: sessionId },
|
|
63
|
+
scheduledFor: now,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
if (!claimed.updated)
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
await scheduleTerminalFinalization(services, guildId, sessionId, now);
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
export function validateTerminalFinalizationPayload(input) {
|
|
75
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
76
|
+
throw new Error("Account Gate terminal-finalization payload is invalid");
|
|
77
|
+
const value = input;
|
|
78
|
+
if (typeof value.guildId !== "string" ||
|
|
79
|
+
typeof value.finalizationId !== "string" ||
|
|
80
|
+
value.finalizationId.length > 100)
|
|
81
|
+
throw new Error("Account Gate terminal-finalization payload is invalid");
|
|
82
|
+
return {
|
|
83
|
+
guildId: value.guildId,
|
|
84
|
+
finalizationId: value.finalizationId,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export async function runTerminalFinalizationTask(context, task) {
|
|
88
|
+
await finalizeTerminalFinalization(context.services, task.payload.guildId, task.payload.finalizationId);
|
|
89
|
+
return { outcome: "completed" };
|
|
90
|
+
}
|
|
91
|
+
export async function finalizeTerminalFinalization(services, guildId, finalizationId) {
|
|
92
|
+
const records = services.get(HELYX_SERVICE_NAMES.records);
|
|
93
|
+
const row = await records.findOne({
|
|
94
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
95
|
+
collection: "terminal_finalizations",
|
|
96
|
+
select: accountGateRecords.terminal_finalizations.readableFields,
|
|
97
|
+
where: { guild_id: guildId, finalization_id: finalizationId },
|
|
98
|
+
});
|
|
99
|
+
if (!row || ["completed", "cancelled"].includes(stringValue(row.status)))
|
|
100
|
+
return;
|
|
101
|
+
const memberUserId = stringValue(row.member_user_id);
|
|
102
|
+
const generation = numberValue(row.join_generation);
|
|
103
|
+
const summary = await loadMemberSummary(records, guildId, memberUserId);
|
|
104
|
+
const enabled = await services
|
|
105
|
+
.get(HELYX_SERVICE_NAMES.installations)
|
|
106
|
+
.isModuleEnabled(guildId, ACCOUNT_GATE_IDENTITY.moduleId);
|
|
107
|
+
if (!enabled ||
|
|
108
|
+
!summary ||
|
|
109
|
+
numberValue(summary.join_generation) !== generation ||
|
|
110
|
+
!["finalizing", "failed", "passed", "review"].includes(stringValue(summary.last_decision)) ||
|
|
111
|
+
(await isAccountGateWorkSuppressed(services, guildId, memberUserId, dateValue(row.work_created_at) ?? new Date(0)))) {
|
|
112
|
+
await finishTerminalRecord(records, row, "cancelled");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const configuration = await requiredRuntimeConfiguration(services, guildId);
|
|
116
|
+
const sessions = services.get(HELYX_SERVICE_NAMES.sessions);
|
|
117
|
+
if (row.outcome === "passed") {
|
|
118
|
+
if (summary.last_decision === "passed") {
|
|
119
|
+
await emitAccountGateCompletedLog(services, guildId, {
|
|
120
|
+
memberUserId,
|
|
121
|
+
roleOutcome: "already_applied",
|
|
122
|
+
idempotencyKey: `account-gate-completed:${finalizationId}`,
|
|
123
|
+
});
|
|
124
|
+
await sessions.delete(stringValue(row.session_id));
|
|
125
|
+
await finishTerminalRecord(records, row, "completed");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const transition = await applyRoleTransition({
|
|
129
|
+
guildId,
|
|
130
|
+
memberUserId,
|
|
131
|
+
verifiedRoleId: configuration.verifiedRoleId,
|
|
132
|
+
temporaryRoleIds: configuration.temporaryRoleIds,
|
|
133
|
+
resources: services.get(HELYX_SERVICE_NAMES.discordResources),
|
|
134
|
+
roles: services.get(HELYX_SERVICE_NAMES.memberRoles),
|
|
135
|
+
revalidate: () => terminalPassStillCurrent(services, guildId, memberUserId, generation, dateValue(row.work_created_at) ?? new Date(0)),
|
|
136
|
+
});
|
|
137
|
+
if (transition.outcome === "stale") {
|
|
138
|
+
await finishTerminalRecord(records, row, "cancelled");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (transition.outcome === "blocked") {
|
|
142
|
+
await setSummaryDecision(records, guildId, memberUserId, generation, "review", "finalizing");
|
|
143
|
+
await createReview(services, guildId, memberUserId, "role_assignment_failed", null, generation, JSON.parse(stringValue(row.challenge_types_json)), dateValue(row.work_created_at) ?? new Date(0));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
if (transition.outcome === "partial")
|
|
147
|
+
await persistPartialTransition(services, guildId, memberUserId, configuration.verifiedRoleId, transition.pendingRoleIds, dateValue(row.work_created_at) ?? new Date(0));
|
|
148
|
+
if (!(await markSummaryPassed(records, guildId, memberUserId, new Date(), generation))) {
|
|
149
|
+
await compensateVerifiedRoleAddition(services, guildId, memberUserId, configuration.verifiedRoleId, transition);
|
|
150
|
+
await finishTerminalRecord(records, row, "cancelled");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
await emitAccountGateCompletedLog(services, guildId, {
|
|
154
|
+
memberUserId,
|
|
155
|
+
roleOutcome: transition.outcome,
|
|
156
|
+
idempotencyKey: `account-gate-completed:${finalizationId}`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const failures = await recordFailureSummary(records, guildId, memberUserId, new Date(), configuration.retryCooldownSeconds, configuration.attempts, finalizationId, generation);
|
|
162
|
+
if (failures === null) {
|
|
163
|
+
await finishTerminalRecord(records, row, "cancelled");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
await sessions.delete(stringValue(row.session_id));
|
|
167
|
+
if (failures >= configuration.attempts)
|
|
168
|
+
await applyDurableFailureAction(services, configuration, guildId, memberUserId, generation, row);
|
|
169
|
+
}
|
|
170
|
+
await sessions.delete(stringValue(row.session_id));
|
|
171
|
+
await finishTerminalRecord(records, row, "completed");
|
|
172
|
+
}
|
|
173
|
+
export async function finishTerminalRecord(records, row, status) {
|
|
174
|
+
const revision = numberValue(row.revision);
|
|
175
|
+
await records.updateOne({
|
|
176
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
177
|
+
collection: "terminal_finalizations",
|
|
178
|
+
where: { finalization_id: stringValue(row.finalization_id) },
|
|
179
|
+
expected: { field: "revision", value: revision },
|
|
180
|
+
values: { status, revision: revision + 1, updated_at: new Date() },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
export async function applyDurableFailureAction(services, configuration, guildId, memberUserId, generation, terminal) {
|
|
184
|
+
if (configuration.failureAction === "review") {
|
|
185
|
+
await createReview(services, guildId, memberUserId, "attempt_limit_reached", null, generation, JSON.parse(stringValue(terminal.challenge_types_json)), dateValue(terminal.work_created_at) ?? new Date(0));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (configuration.failureAction !== "kick")
|
|
189
|
+
return;
|
|
190
|
+
const records = services.get(HELYX_SERVICE_NAMES.records);
|
|
191
|
+
const result = await services
|
|
192
|
+
.get(HELYX_SERVICE_NAMES.memberEnforcement)
|
|
193
|
+
.kick({
|
|
194
|
+
guildId,
|
|
195
|
+
memberUserId,
|
|
196
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
197
|
+
reason: "Access Control final failure",
|
|
198
|
+
directMessage: {
|
|
199
|
+
embed: {
|
|
200
|
+
title: "Removed after incomplete Access Control",
|
|
201
|
+
description: neutralKickDirectMessage(`Discord server ${guildId}`),
|
|
202
|
+
color: VIOLET,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
revalidate: async () => {
|
|
206
|
+
const [enabled, summary, session] = await Promise.all([
|
|
207
|
+
services
|
|
208
|
+
.get(HELYX_SERVICE_NAMES.installations)
|
|
209
|
+
.isModuleEnabled(guildId, ACCOUNT_GATE_IDENTITY.moduleId),
|
|
210
|
+
loadMemberSummary(records, guildId, memberUserId),
|
|
211
|
+
services
|
|
212
|
+
.get(HELYX_SERVICE_NAMES.sessions)
|
|
213
|
+
.getFor(guildId, memberUserId, ACCOUNT_GATE_SESSION_PURPOSE),
|
|
214
|
+
]);
|
|
215
|
+
if (!enabled || session !== null || !summary)
|
|
216
|
+
return false;
|
|
217
|
+
if (numberValue(summary.join_generation) !== generation ||
|
|
218
|
+
summary.last_decision !== "failed")
|
|
219
|
+
return false;
|
|
220
|
+
try {
|
|
221
|
+
await requiredRuntimeConfiguration(services, guildId);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
return !(await isAccountGateWorkSuppressed(services, guildId, memberUserId, dateValue(terminal.work_created_at) ?? new Date(0)));
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
if (["kicked", "member_missing"].includes(result.outcome))
|
|
230
|
+
await emitAccountGateKickLog(services, guildId, {
|
|
231
|
+
memberUserId,
|
|
232
|
+
outcome: successfulKickOutcome(result.outcome),
|
|
233
|
+
directMessage: result.directMessage,
|
|
234
|
+
source: "Failed check",
|
|
235
|
+
idempotencyKey: `account-gate-kick:terminal:${stringValue(terminal.finalization_id)}`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=terminal-finalization.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RoleTransitionResult, ServiceAccess } from "./runtime-dependencies.js";
|
|
2
|
+
export declare function reviewStillCurrent(services: ServiceAccess, guildId: string, memberUserId: string, joinGeneration: number, verifiedRoleId: string, allowVerifiedRole?: boolean): Promise<boolean>;
|
|
3
|
+
export declare function terminalPassStillCurrent(services: ServiceAccess, guildId: string, memberUserId: string, joinGeneration: number, workCreatedAt: Date): Promise<boolean>;
|
|
4
|
+
export declare function compensateVerifiedRoleAddition(services: ServiceAccess, guildId: string, memberUserId: string, verifiedRoleId: string, transition: RoleTransitionResult): Promise<void>;
|
|
5
|
+
export declare function roleRetryStillCurrent(services: ServiceAccess, guildId: string, memberUserId: string, workCreatedAt: Date): Promise<boolean>;
|
|
6
|
+
//# sourceMappingURL=workflow-fences.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES, ACCOUNT_GATE_IDENTITY, isAccountGateWorkSuppressed, } from "./runtime-dependencies.js";
|
|
2
|
+
import { loadMemberSummary } from "./member-summary.js";
|
|
3
|
+
import { numberValue, stringValue } from "./runtime-support.js";
|
|
4
|
+
export async function reviewStillCurrent(services, guildId, memberUserId, joinGeneration, verifiedRoleId, allowVerifiedRole = false) {
|
|
5
|
+
const [enabled, summary, members] = await Promise.all([
|
|
6
|
+
services
|
|
7
|
+
.get(HELYX_SERVICE_NAMES.installations)
|
|
8
|
+
.isModuleEnabled(guildId, ACCOUNT_GATE_IDENTITY.moduleId),
|
|
9
|
+
loadMemberSummary(services.get(HELYX_SERVICE_NAMES.records), guildId, memberUserId),
|
|
10
|
+
services
|
|
11
|
+
.get(HELYX_SERVICE_NAMES.discordResources)
|
|
12
|
+
.resolveMemberSnapshots({ guildId, userIds: [memberUserId] }),
|
|
13
|
+
]);
|
|
14
|
+
const member = members[0];
|
|
15
|
+
return (enabled &&
|
|
16
|
+
summary !== null &&
|
|
17
|
+
numberValue(summary.join_generation) === joinGeneration &&
|
|
18
|
+
["review", "failed", "finalizing"].includes(stringValue(summary.last_decision)) &&
|
|
19
|
+
member?.exists === true &&
|
|
20
|
+
!member.isBot &&
|
|
21
|
+
(allowVerifiedRole || !member.roleIds.includes(verifiedRoleId)));
|
|
22
|
+
}
|
|
23
|
+
export async function terminalPassStillCurrent(services, guildId, memberUserId, joinGeneration, workCreatedAt) {
|
|
24
|
+
const [enabled, summary, members, suppressed] = await Promise.all([
|
|
25
|
+
services
|
|
26
|
+
.get(HELYX_SERVICE_NAMES.installations)
|
|
27
|
+
.isModuleEnabled(guildId, ACCOUNT_GATE_IDENTITY.moduleId),
|
|
28
|
+
loadMemberSummary(services.get(HELYX_SERVICE_NAMES.records), guildId, memberUserId),
|
|
29
|
+
services
|
|
30
|
+
.get(HELYX_SERVICE_NAMES.discordResources)
|
|
31
|
+
.resolveMemberSnapshots({ guildId, userIds: [memberUserId] }),
|
|
32
|
+
isAccountGateWorkSuppressed(services, guildId, memberUserId, workCreatedAt),
|
|
33
|
+
]);
|
|
34
|
+
const member = members[0];
|
|
35
|
+
return (enabled &&
|
|
36
|
+
!suppressed &&
|
|
37
|
+
summary !== null &&
|
|
38
|
+
numberValue(summary.join_generation) === joinGeneration &&
|
|
39
|
+
summary.last_decision === "finalizing" &&
|
|
40
|
+
member?.exists === true &&
|
|
41
|
+
!member.isBot);
|
|
42
|
+
}
|
|
43
|
+
export async function compensateVerifiedRoleAddition(services, guildId, memberUserId, verifiedRoleId, transition) {
|
|
44
|
+
if (!transition.accessRoleAdded)
|
|
45
|
+
return;
|
|
46
|
+
await services
|
|
47
|
+
.get(HELYX_SERVICE_NAMES.memberRoles)
|
|
48
|
+
.removeApprovedRole({
|
|
49
|
+
guildId,
|
|
50
|
+
memberUserId,
|
|
51
|
+
roleId: verifiedRoleId,
|
|
52
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
53
|
+
reason: "Access Control stale-generation compensation",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export async function roleRetryStillCurrent(services, guildId, memberUserId, workCreatedAt) {
|
|
57
|
+
const [enabled, summary, members, suppressed] = await Promise.all([
|
|
58
|
+
services
|
|
59
|
+
.get(HELYX_SERVICE_NAMES.installations)
|
|
60
|
+
.isModuleEnabled(guildId, ACCOUNT_GATE_IDENTITY.moduleId),
|
|
61
|
+
loadMemberSummary(services.get(HELYX_SERVICE_NAMES.records), guildId, memberUserId),
|
|
62
|
+
services
|
|
63
|
+
.get(HELYX_SERVICE_NAMES.discordResources)
|
|
64
|
+
.resolveMemberSnapshots({ guildId, userIds: [memberUserId] }),
|
|
65
|
+
isAccountGateWorkSuppressed(services, guildId, memberUserId, workCreatedAt),
|
|
66
|
+
]);
|
|
67
|
+
return (enabled &&
|
|
68
|
+
!suppressed &&
|
|
69
|
+
summary?.last_decision === "passed" &&
|
|
70
|
+
members[0]?.exists === true &&
|
|
71
|
+
!members[0].isBot);
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=workflow-fences.js.map
|
package/manifest.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"manifestVersion": "0.
|
|
2
|
+
"manifestVersion": "0.4",
|
|
3
3
|
"releaseStatus": "live",
|
|
4
|
+
"deploymentSupport": "both",
|
|
5
|
+
"credentialRisk": "none",
|
|
6
|
+
"deploymentReason": "Uses only the Helyx runtime and Discord capabilities available in hosted and self-hosted installations.",
|
|
7
|
+
"moduleKind": "customer_feature",
|
|
8
|
+
"customerSelectable": true,
|
|
9
|
+
"countsAsInstalledModule": true,
|
|
10
|
+
"healthExposure": "direct",
|
|
4
11
|
"id": "helyx.account-gate",
|
|
5
12
|
"name": "Access Control",
|
|
6
|
-
"version": "1.
|
|
13
|
+
"version": "1.1.1",
|
|
7
14
|
"description": "Require new members to complete configurable Discord interaction checks before gaining access.",
|
|
8
15
|
"entry": "./dist/index.js",
|
|
9
16
|
"framework": "^0.1.0",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
ALTER TABLE account_gate_gate_panels
|
|
2
|
+
ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'posted';
|
|
3
|
+
|
|
4
|
+
DO $$
|
|
5
|
+
BEGIN
|
|
6
|
+
IF NOT EXISTS (
|
|
7
|
+
SELECT 1
|
|
8
|
+
FROM pg_constraint
|
|
9
|
+
WHERE conname = 'account_gate_gate_panels_status'
|
|
10
|
+
AND conrelid = 'account_gate_gate_panels'::regclass
|
|
11
|
+
) THEN
|
|
12
|
+
ALTER TABLE account_gate_gate_panels
|
|
13
|
+
ADD CONSTRAINT account_gate_gate_panels_status
|
|
14
|
+
CHECK (status IN ('posted', 'missing'));
|
|
15
|
+
END IF;
|
|
16
|
+
END;
|
|
17
|
+
$$;
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS account_gate_panel_message_cleanup (
|
|
20
|
+
cleanup_id TEXT PRIMARY KEY,
|
|
21
|
+
guild_id TEXT NOT NULL,
|
|
22
|
+
panel_id TEXT NOT NULL,
|
|
23
|
+
channel_id TEXT NOT NULL,
|
|
24
|
+
message_id TEXT NOT NULL,
|
|
25
|
+
attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
|
26
|
+
next_attempt_at TIMESTAMPTZ NOT NULL,
|
|
27
|
+
completed_at TIMESTAMPTZ,
|
|
28
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
29
|
+
UNIQUE (guild_id, channel_id, message_id)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE INDEX IF NOT EXISTS account_gate_panel_cleanup_pending_idx
|
|
33
|
+
ON account_gate_panel_message_cleanup (next_attempt_at, cleanup_id)
|
|
34
|
+
WHERE completed_at IS NULL;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helyx/module-account-gate",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Require configurable Discord interaction checks before new members gain access.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helyx",
|
|
@@ -45,10 +45,11 @@
|
|
|
45
45
|
"./package.json": "./package.json"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
|
-
"build": "tsc -b"
|
|
48
|
+
"build": "tsc -b",
|
|
49
|
+
"test": "vitest run --root ../.. modules/account-gate/tests"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
|
-
"@helyx/module-manifest": ">=0.
|
|
52
|
-
"@helyx/sdk": ">=0.
|
|
52
|
+
"@helyx/module-manifest": ">=0.8.0 <0.9.0",
|
|
53
|
+
"@helyx/sdk": ">=0.10.0"
|
|
53
54
|
}
|
|
54
55
|
}
|