@helyx/module-account-gate 1.0.15 → 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.
Files changed (69) hide show
  1. package/CHANGELOG.md +13 -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 +4 -3
@@ -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
@@ -10,7 +10,7 @@
10
10
  "healthExposure": "direct",
11
11
  "id": "helyx.account-gate",
12
12
  "name": "Access Control",
13
- "version": "1.0.15",
13
+ "version": "1.1.1",
14
14
  "description": "Require new members to complete configurable Discord interaction checks before gaining access.",
15
15
  "entry": "./dist/index.js",
16
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.0.15",
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
52
  "@helyx/module-manifest": ">=0.8.0 <0.9.0",
52
- "@helyx/sdk": ">=0.9.0 <0.10.0"
53
+ "@helyx/sdk": ">=0.10.0"
53
54
  }
54
55
  }