@helyx/module-account-gate 1.0.11 → 1.0.14
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 +21 -0
- package/README.md +4 -4
- package/dist/commands.d.ts +10 -0
- package/dist/commands.js +38 -0
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.js +1 -1
- package/dist/index.js +104 -105
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +84 -0
- package/manifest.json +8 -3
- package/migrations/0001_account_gate_foundation.sql +151 -18
- package/package.json +3 -3
- package/migrations/0002_account_gate_panel_message.sql +0 -27
- package/migrations/0003_account_gate_revoked_member_status.sql +0 -17
- package/migrations/0004_account_gate_multiple_panels.sql +0 -23
- package/migrations/0005_account_gate_panel_passwords.sql +0 -45
- package/migrations/0006_remove_legacy_gate_storage.sql +0 -2
- package/migrations/0007_privacy_suppression_trigger_privileges.sql +0 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Move Access Control commands beneath `/access control` and register them only for servers where the module is selected.
|
|
8
|
+
- Emit the declared completion and kick Logging events for automatic, staff and recovery outcomes.
|
|
9
|
+
|
|
10
|
+
## 1.0.13
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Consolidate the module database migration history into a clean one-file baseline for the planned database rebuild.
|
|
15
|
+
|
|
16
|
+
## 1.0.12
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [26b4c3e]
|
|
21
|
+
- @helyx/module-manifest@0.7.0
|
|
22
|
+
- @helyx/sdk@0.8.0
|
|
23
|
+
|
|
3
24
|
## 1.0.11
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Password Entry and Emoji Entry are single-step access types rather than members
|
|
|
72
72
|
of a generated challenge chain. Helyx sends answer-free private resume guidance
|
|
73
73
|
that identifies the Discord server and gate channel. Closed direct messages
|
|
74
74
|
never cause user-specific controls or answers to be posted publicly; the member
|
|
75
|
-
can use the panel or `/access
|
|
75
|
+
can use the panel or `/access control start` to resume the persisted check.
|
|
76
76
|
|
|
77
77
|
## Stored data
|
|
78
78
|
|
|
@@ -85,6 +85,6 @@ erasure.
|
|
|
85
85
|
|
|
86
86
|
## Release state
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
Access Control is a Live Helyx module. Runtime enforcement rechecks module
|
|
89
|
+
state, member state, kick permission and role hierarchy immediately before
|
|
90
|
+
every kick.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommandInteractionContext, SharedCommandContribution } from "@helyx/sdk";
|
|
2
|
+
type CommandHandler = (context: CommandInteractionContext) => Promise<void>;
|
|
3
|
+
export declare function accountGateCommands(handlers: {
|
|
4
|
+
start: CommandHandler;
|
|
5
|
+
reset: CommandHandler;
|
|
6
|
+
status: CommandHandler;
|
|
7
|
+
strict: CommandHandler;
|
|
8
|
+
}): readonly SharedCommandContribution[];
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=commands.d.ts.map
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function accountGateCommands(handlers) {
|
|
2
|
+
return [
|
|
3
|
+
{
|
|
4
|
+
family: "access",
|
|
5
|
+
path: "control.start",
|
|
6
|
+
description: "Start or resume your Access Control check",
|
|
7
|
+
acknowledgement: "deferred-ephemeral",
|
|
8
|
+
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
9
|
+
execute: handlers.start,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
family: "access",
|
|
13
|
+
path: "control.reset",
|
|
14
|
+
description: "Revoke your access and complete the gate again",
|
|
15
|
+
acknowledgement: "deferred-ephemeral",
|
|
16
|
+
permissionId: "account-gate.reset",
|
|
17
|
+
rateLimit: { scope: "user", limit: 10, windowSeconds: 60 },
|
|
18
|
+
execute: handlers.reset,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
family: "access",
|
|
22
|
+
path: "control.status",
|
|
23
|
+
description: "Show Access Control health and strict-mode status",
|
|
24
|
+
acknowledgement: "deferred-ephemeral",
|
|
25
|
+
permissionId: "account-gate.manage",
|
|
26
|
+
execute: handlers.status,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
family: "access",
|
|
30
|
+
path: "control.strict",
|
|
31
|
+
description: "Activate temporary strict mode",
|
|
32
|
+
acknowledgement: "deferred-ephemeral",
|
|
33
|
+
permissionId: "account-gate.manage",
|
|
34
|
+
execute: handlers.strict,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=commands.js.map
|
package/dist/contracts.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export declare const ACCOUNT_GATE_IDENTITY: {
|
|
|
2
2
|
readonly packageName: "@helyx/module-account-gate";
|
|
3
3
|
readonly moduleId: "helyx.account-gate";
|
|
4
4
|
readonly name: "Access Control";
|
|
5
|
-
readonly version: "1.0.
|
|
5
|
+
readonly version: "1.0.14";
|
|
6
6
|
readonly releaseStatus: "live";
|
|
7
7
|
readonly category: "safety";
|
|
8
8
|
readonly icon: "shield-check";
|
package/dist/contracts.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -13,6 +13,8 @@ import { serverQuestionResource } from "./question-resource.js";
|
|
|
13
13
|
import { gatePanelResource, getGatePanelRuntime, hasPasswordGatePanel, } from "./panel-resource.js";
|
|
14
14
|
import { isAccountGateWorkSuppressed } from "./privacy.js";
|
|
15
15
|
import { applyChallengeAction, createAccountGateSessionState, parseAccountGateSessionState, rerollForAccessibility, } from "./workflow.js";
|
|
16
|
+
import { accountGateCommands } from "./commands.js";
|
|
17
|
+
import { accountGateLogging, emitAccountGateCompletedLog, emitAccountGateKickLog, emitLog, successfulKickOutcome, } from "./logging.js";
|
|
16
18
|
const manifest = parseModuleManifest(JSON.parse(readFileSync(new URL("../manifest.json", import.meta.url), "utf8")));
|
|
17
19
|
const VIOLET = 0x7c3aed;
|
|
18
20
|
const GREEN = 0x16a34a;
|
|
@@ -48,7 +50,7 @@ export const accountGateModule = defineModule({
|
|
|
48
50
|
async validateActivation(context) {
|
|
49
51
|
const configuration = await getConfiguration(context.services, context.guildId);
|
|
50
52
|
if (!configuration)
|
|
51
|
-
|
|
53
|
+
return;
|
|
52
54
|
const errors = await validateAccountGateSetup(context.services, context.guildId, configuration, false, true);
|
|
53
55
|
if (errors.length > 0)
|
|
54
56
|
throw new ModuleStateValidationError(errors[0]);
|
|
@@ -81,77 +83,14 @@ export const accountGateModule = defineModule({
|
|
|
81
83
|
accountGateMemberResource,
|
|
82
84
|
serverQuestionResource,
|
|
83
85
|
],
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
id: "account-gate-review-created",
|
|
94
|
-
name: "Account gate review created",
|
|
95
|
-
description: "Records a staff-review request without copying challenge answers.",
|
|
96
|
-
defaultEnabled: true,
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
id: "account-gate-kicked",
|
|
100
|
-
name: "Account gate kick",
|
|
101
|
-
description: "Records the bounded enforcement and direct-message outcomes.",
|
|
102
|
-
defaultEnabled: true,
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
id: "account-gate-strict-mode-changed",
|
|
106
|
-
name: "Account gate strict mode changed",
|
|
107
|
-
description: "Records authorised strict-mode activation or automatic expiry.",
|
|
108
|
-
defaultEnabled: true,
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
id: "account-gate-resume-notice",
|
|
112
|
-
name: "Account gate resume notice",
|
|
113
|
-
description: "Records only whether private resume guidance could be delivered after a static gate.",
|
|
114
|
-
defaultEnabled: true,
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
},
|
|
86
|
+
sharedCommands: accountGateCommands({
|
|
87
|
+
start: startCheck,
|
|
88
|
+
reset: resetOwnAccess,
|
|
89
|
+
status: showStatus,
|
|
90
|
+
strict: activateStrictModeCommand,
|
|
91
|
+
}),
|
|
92
|
+
logging: accountGateLogging,
|
|
118
93
|
interactions: {
|
|
119
|
-
commands: [
|
|
120
|
-
{
|
|
121
|
-
name: "access-control",
|
|
122
|
-
description: "Start or manage the server's Access Control checks",
|
|
123
|
-
registration: "global",
|
|
124
|
-
contexts: ["guild"],
|
|
125
|
-
integrationTypes: ["guild_install"],
|
|
126
|
-
subcommands: [
|
|
127
|
-
{
|
|
128
|
-
name: "start",
|
|
129
|
-
description: "Start or resume your Access Control check",
|
|
130
|
-
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
131
|
-
execute: startCheck,
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: "reset",
|
|
135
|
-
description: "Revoke your access and complete the gate again",
|
|
136
|
-
permissionId: "account-gate.reset",
|
|
137
|
-
rateLimit: { scope: "user", limit: 10, windowSeconds: 60 },
|
|
138
|
-
execute: resetOwnAccess,
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: "status",
|
|
142
|
-
description: "Show Access Control health and strict-mode status",
|
|
143
|
-
permissionId: "account-gate.manage",
|
|
144
|
-
execute: showStatus,
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
name: "strict",
|
|
148
|
-
description: "Activate temporary strict mode",
|
|
149
|
-
permissionId: "account-gate.manage",
|
|
150
|
-
execute: activateStrictModeCommand,
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
94
|
buttons: [
|
|
156
95
|
{
|
|
157
96
|
customId: ACCOUNT_GATE_COMPONENTS.start,
|
|
@@ -196,6 +135,7 @@ export const accountGateModule = defineModule({
|
|
|
196
135
|
modals: [
|
|
197
136
|
{
|
|
198
137
|
customIdPrefix: ACCOUNT_GATE_COMPONENTS.recallSubmitPrefix,
|
|
138
|
+
acknowledgement: "deferred-ephemeral",
|
|
199
139
|
execute: submitRecallModal,
|
|
200
140
|
},
|
|
201
141
|
],
|
|
@@ -949,20 +889,9 @@ async function rerollChallenge(context) {
|
|
|
949
889
|
async function openRecallModal(context) {
|
|
950
890
|
if (!context.serverId)
|
|
951
891
|
return replyError(context, "This check is unavailable.");
|
|
952
|
-
const session = await context.services
|
|
953
|
-
.get(HELYX_SERVICE_NAMES.sessions)
|
|
954
|
-
.getFor(context.serverId, context.userId, ACCOUNT_GATE_SESSION_PURPOSE);
|
|
955
892
|
const binding = parseComponentBinding(context.customId, ACCOUNT_GATE_COMPONENTS.recallOpenPrefix);
|
|
956
|
-
|
|
957
|
-
if (!session ||
|
|
958
|
-
!state ||
|
|
959
|
-
!binding ||
|
|
960
|
-
binding.sessionId !== session.sessionId ||
|
|
961
|
-
binding.version !== session.version ||
|
|
962
|
-
binding.step !== state.step ||
|
|
963
|
-
state.chain[state.currentStep]?.type !== "modal_recall") {
|
|
893
|
+
if (!binding)
|
|
964
894
|
return replyError(context, "This recall challenge has expired.");
|
|
965
|
-
}
|
|
966
895
|
await context.showModal({
|
|
967
896
|
customId: boundComponentId(ACCOUNT_GATE_COMPONENTS.recallSubmitPrefix, binding),
|
|
968
897
|
title: "Enter the recalled code",
|
|
@@ -978,10 +907,30 @@ async function openRecallModal(context) {
|
|
|
978
907
|
},
|
|
979
908
|
],
|
|
980
909
|
});
|
|
981
|
-
|
|
910
|
+
let session;
|
|
911
|
+
try {
|
|
912
|
+
session = await context.services
|
|
913
|
+
.get(HELYX_SERVICE_NAMES.sessions)
|
|
914
|
+
.getFor(context.serverId, context.userId, ACCOUNT_GATE_SESSION_PURPOSE);
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const state = session ? parseAccountGateSessionState(session.state) : null;
|
|
920
|
+
if (!session ||
|
|
921
|
+
!state ||
|
|
922
|
+
binding.sessionId !== session.sessionId ||
|
|
923
|
+
binding.version !== session.version ||
|
|
924
|
+
binding.step !== state.step ||
|
|
925
|
+
state.chain[state.currentStep]?.type !== "modal_recall") {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
await context
|
|
929
|
+
.editSourceMessage?.(challengeMessage(state, {
|
|
982
930
|
sessionId: session.sessionId,
|
|
983
931
|
version: session.version,
|
|
984
|
-
}, { redactRecallCode: true }))
|
|
932
|
+
}, { redactRecallCode: true }))
|
|
933
|
+
.catch(() => undefined);
|
|
985
934
|
}
|
|
986
935
|
async function submitRecallModal(context) {
|
|
987
936
|
const binding = parseComponentBinding(context.customId, ACCOUNT_GATE_COMPONENTS.recallSubmitPrefix);
|
|
@@ -1507,7 +1456,15 @@ async function enforceJoinDeadline(context, task) {
|
|
|
1507
1456
|
},
|
|
1508
1457
|
});
|
|
1509
1458
|
if (["kicked", "member_missing"].includes(result.outcome)) {
|
|
1510
|
-
await setSummaryDecision(records, guildId, memberUserId, generation, "kicked", "kick_pending");
|
|
1459
|
+
const committed = await setSummaryDecision(records, guildId, memberUserId, generation, "kicked", "kick_pending");
|
|
1460
|
+
if (committed)
|
|
1461
|
+
await emitAccountGateKickLog(context.services, guildId, {
|
|
1462
|
+
memberUserId,
|
|
1463
|
+
outcome: successfulKickOutcome(result.outcome),
|
|
1464
|
+
directMessage: result.directMessage,
|
|
1465
|
+
source: "Join deadline",
|
|
1466
|
+
idempotencyKey: `account-gate-kick:deadline:${memberUserId}:${generation}`,
|
|
1467
|
+
});
|
|
1511
1468
|
}
|
|
1512
1469
|
}
|
|
1513
1470
|
return { outcome: "completed" };
|
|
@@ -1728,6 +1685,11 @@ async function finalizeTerminalFinalization(services, guildId, finalizationId) {
|
|
|
1728
1685
|
const sessions = services.get(HELYX_SERVICE_NAMES.sessions);
|
|
1729
1686
|
if (row.outcome === "passed") {
|
|
1730
1687
|
if (summary.last_decision === "passed") {
|
|
1688
|
+
await emitAccountGateCompletedLog(services, guildId, {
|
|
1689
|
+
memberUserId,
|
|
1690
|
+
roleOutcome: "already_applied",
|
|
1691
|
+
idempotencyKey: `account-gate-completed:${finalizationId}`,
|
|
1692
|
+
});
|
|
1731
1693
|
await sessions.delete(stringValue(row.session_id));
|
|
1732
1694
|
await finishTerminalRecord(records, row, "completed");
|
|
1733
1695
|
return;
|
|
@@ -1757,6 +1719,11 @@ async function finalizeTerminalFinalization(services, guildId, finalizationId) {
|
|
|
1757
1719
|
await finishTerminalRecord(records, row, "cancelled");
|
|
1758
1720
|
return;
|
|
1759
1721
|
}
|
|
1722
|
+
await emitAccountGateCompletedLog(services, guildId, {
|
|
1723
|
+
memberUserId,
|
|
1724
|
+
roleOutcome: transition.outcome,
|
|
1725
|
+
idempotencyKey: `account-gate-completed:${finalizationId}`,
|
|
1726
|
+
});
|
|
1760
1727
|
}
|
|
1761
1728
|
}
|
|
1762
1729
|
else {
|
|
@@ -1790,7 +1757,7 @@ async function applyDurableFailureAction(services, configuration, guildId, membe
|
|
|
1790
1757
|
if (configuration.failureAction !== "kick")
|
|
1791
1758
|
return;
|
|
1792
1759
|
const records = services.get(HELYX_SERVICE_NAMES.records);
|
|
1793
|
-
await services
|
|
1760
|
+
const result = await services
|
|
1794
1761
|
.get(HELYX_SERVICE_NAMES.memberEnforcement)
|
|
1795
1762
|
.kick({
|
|
1796
1763
|
guildId,
|
|
@@ -1828,6 +1795,14 @@ async function applyDurableFailureAction(services, configuration, guildId, membe
|
|
|
1828
1795
|
return !(await isAccountGateWorkSuppressed(services, guildId, memberUserId, dateValue(terminal.work_created_at) ?? new Date(0)));
|
|
1829
1796
|
},
|
|
1830
1797
|
});
|
|
1798
|
+
if (["kicked", "member_missing"].includes(result.outcome))
|
|
1799
|
+
await emitAccountGateKickLog(services, guildId, {
|
|
1800
|
+
memberUserId,
|
|
1801
|
+
outcome: successfulKickOutcome(result.outcome),
|
|
1802
|
+
directMessage: result.directMessage,
|
|
1803
|
+
source: "Failed check",
|
|
1804
|
+
idempotencyKey: `account-gate-kick:terminal:${stringValue(terminal.finalization_id)}`,
|
|
1805
|
+
});
|
|
1831
1806
|
}
|
|
1832
1807
|
function validateReviewDeliveryPayload(input) {
|
|
1833
1808
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
@@ -1961,6 +1936,7 @@ async function recoverReviewClaim(context, task) {
|
|
|
1961
1936
|
}
|
|
1962
1937
|
const action = stringValue(row.claim_action);
|
|
1963
1938
|
if (action === "approve") {
|
|
1939
|
+
let completionRoleOutcome = null;
|
|
1964
1940
|
if (summary.last_decision !== "passed") {
|
|
1965
1941
|
const snapshots = await context.services
|
|
1966
1942
|
.get(HELYX_SERVICE_NAMES.discordResources)
|
|
@@ -1998,8 +1974,15 @@ async function recoverReviewClaim(context, task) {
|
|
|
1998
1974
|
await finalizeReviewClaim(records, guildId, reviewId, claimRevision, "dismissed", row);
|
|
1999
1975
|
return { outcome: "completed" };
|
|
2000
1976
|
}
|
|
1977
|
+
completionRoleOutcome = transition.outcome;
|
|
2001
1978
|
}
|
|
2002
|
-
await finalizeReviewClaim(records, guildId, reviewId, claimRevision, "approved", row);
|
|
1979
|
+
const finalized = await finalizeReviewClaim(records, guildId, reviewId, claimRevision, "approved", row);
|
|
1980
|
+
if (finalized && completionRoleOutcome)
|
|
1981
|
+
await emitAccountGateCompletedLog(context.services, guildId, {
|
|
1982
|
+
memberUserId,
|
|
1983
|
+
roleOutcome: completionRoleOutcome,
|
|
1984
|
+
idempotencyKey: `account-gate-completed:review:${reviewId}`,
|
|
1985
|
+
});
|
|
2003
1986
|
return { outcome: "completed" };
|
|
2004
1987
|
}
|
|
2005
1988
|
if (action === "retry") {
|
|
@@ -2017,6 +2000,8 @@ async function recoverReviewClaim(context, task) {
|
|
|
2017
2000
|
const snapshots = await context.services
|
|
2018
2001
|
.get(HELYX_SERVICE_NAMES.discordResources)
|
|
2019
2002
|
.resolveMemberSnapshots({ guildId, userIds: [memberUserId] });
|
|
2003
|
+
let kickOutcome = "member_missing";
|
|
2004
|
+
let directMessage = "not_requested";
|
|
2020
2005
|
if (snapshots[0]?.exists === true) {
|
|
2021
2006
|
const result = await context.services
|
|
2022
2007
|
.get(HELYX_SERVICE_NAMES.memberEnforcement)
|
|
@@ -2045,8 +2030,18 @@ async function recoverReviewClaim(context, task) {
|
|
|
2045
2030
|
delayMs: 30_000,
|
|
2046
2031
|
safeCode: "review-kick-not-complete",
|
|
2047
2032
|
};
|
|
2033
|
+
kickOutcome = successfulKickOutcome(result.outcome);
|
|
2034
|
+
directMessage = result.directMessage;
|
|
2048
2035
|
}
|
|
2049
|
-
await finalizeReviewClaim(records, guildId, reviewId, claimRevision, "kicked", row);
|
|
2036
|
+
const finalized = await finalizeReviewClaim(records, guildId, reviewId, claimRevision, "kicked", row);
|
|
2037
|
+
if (finalized)
|
|
2038
|
+
await emitAccountGateKickLog(context.services, guildId, {
|
|
2039
|
+
memberUserId,
|
|
2040
|
+
outcome: kickOutcome,
|
|
2041
|
+
directMessage,
|
|
2042
|
+
source: "Staff review recovery",
|
|
2043
|
+
idempotencyKey: `account-gate-kick:review:${reviewId}`,
|
|
2044
|
+
});
|
|
2050
2045
|
return { outcome: "completed" };
|
|
2051
2046
|
}
|
|
2052
2047
|
await finalizeReviewClaim(records, guildId, reviewId, claimRevision, "dismissed", row);
|
|
@@ -2373,6 +2368,8 @@ async function handleReviewDecision(context, decision) {
|
|
|
2373
2368
|
return;
|
|
2374
2369
|
}
|
|
2375
2370
|
let status;
|
|
2371
|
+
let completionRoleOutcome = null;
|
|
2372
|
+
let kickResult = null;
|
|
2376
2373
|
if (decision === "approve") {
|
|
2377
2374
|
const transition = await applyRoleTransition({
|
|
2378
2375
|
guildId: context.serverId,
|
|
@@ -2401,6 +2398,7 @@ async function handleReviewDecision(context, decision) {
|
|
|
2401
2398
|
await replyError(context, "The member rejoined before approval completed; the old review was dismissed.");
|
|
2402
2399
|
return;
|
|
2403
2400
|
}
|
|
2401
|
+
completionRoleOutcome = transition.outcome;
|
|
2404
2402
|
status = "approved";
|
|
2405
2403
|
}
|
|
2406
2404
|
else if (decision === "retry") {
|
|
@@ -2465,6 +2463,10 @@ async function handleReviewDecision(context, decision) {
|
|
|
2465
2463
|
await replyError(context, `The member was not kicked (${result.outcome.replaceAll("_", " ")}). The claimed decision remains queued for safe recovery.`);
|
|
2466
2464
|
return;
|
|
2467
2465
|
}
|
|
2466
|
+
kickResult = {
|
|
2467
|
+
outcome: successfulKickOutcome(result.outcome),
|
|
2468
|
+
directMessage: result.directMessage,
|
|
2469
|
+
};
|
|
2468
2470
|
status = "kicked";
|
|
2469
2471
|
}
|
|
2470
2472
|
else {
|
|
@@ -2505,6 +2507,19 @@ async function handleReviewDecision(context, decision) {
|
|
|
2505
2507
|
await replyError(context, "This review changed in another session. Reload it before trying again.");
|
|
2506
2508
|
return;
|
|
2507
2509
|
}
|
|
2510
|
+
if (status === "kicked" && kickResult)
|
|
2511
|
+
await emitAccountGateKickLog(context.services, context.serverId, {
|
|
2512
|
+
memberUserId,
|
|
2513
|
+
...kickResult,
|
|
2514
|
+
source: "Staff review",
|
|
2515
|
+
idempotencyKey: `account-gate-kick:review:${stringValue(row.review_id)}`,
|
|
2516
|
+
});
|
|
2517
|
+
if (status === "approved" && completionRoleOutcome)
|
|
2518
|
+
await emitAccountGateCompletedLog(context.services, context.serverId, {
|
|
2519
|
+
memberUserId,
|
|
2520
|
+
roleOutcome: completionRoleOutcome,
|
|
2521
|
+
idempotencyKey: `account-gate-completed:review:${stringValue(row.review_id)}`,
|
|
2522
|
+
});
|
|
2508
2523
|
if (context.channelId) {
|
|
2509
2524
|
await context.editChannelMessage(context.channelId, context.messageId, {
|
|
2510
2525
|
allowedUserMentionIds: [],
|
|
@@ -2966,22 +2981,6 @@ function secureRandom(services) {
|
|
|
2966
2981
|
},
|
|
2967
2982
|
};
|
|
2968
2983
|
}
|
|
2969
|
-
async function emitLog(services, guildId, input) {
|
|
2970
|
-
if (!services.has(HELYX_SERVICE_NAMES.moduleLogging))
|
|
2971
|
-
return;
|
|
2972
|
-
try {
|
|
2973
|
-
await services
|
|
2974
|
-
.get(HELYX_SERVICE_NAMES.moduleLogging)
|
|
2975
|
-
.emit({
|
|
2976
|
-
guildId,
|
|
2977
|
-
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
2978
|
-
...input,
|
|
2979
|
-
});
|
|
2980
|
-
}
|
|
2981
|
-
catch {
|
|
2982
|
-
// Customer logging is optional and must not block the gate.
|
|
2983
|
-
}
|
|
2984
|
-
}
|
|
2985
2984
|
async function replyError(context, description) {
|
|
2986
2985
|
await context.reply({
|
|
2987
2986
|
ephemeral: true,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { DirectMessageOutcome, MemberEnforcementOutcome, ModuleLoggingService, ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
export declare const accountGateLogging: {
|
|
3
|
+
readonly events: readonly [{
|
|
4
|
+
readonly id: "account-gate-completed";
|
|
5
|
+
readonly name: "Account gate completed";
|
|
6
|
+
readonly description: "Records successful completion using IDs and bounded outcome metadata.";
|
|
7
|
+
readonly defaultEnabled: true;
|
|
8
|
+
}, {
|
|
9
|
+
readonly id: "account-gate-review-created";
|
|
10
|
+
readonly name: "Account gate review created";
|
|
11
|
+
readonly description: "Records a staff-review request without copying challenge answers.";
|
|
12
|
+
readonly defaultEnabled: true;
|
|
13
|
+
}, {
|
|
14
|
+
readonly id: "account-gate-kicked";
|
|
15
|
+
readonly name: "Account gate kick";
|
|
16
|
+
readonly description: "Records the bounded enforcement and direct-message outcomes.";
|
|
17
|
+
readonly defaultEnabled: true;
|
|
18
|
+
}, {
|
|
19
|
+
readonly id: "account-gate-strict-mode-changed";
|
|
20
|
+
readonly name: "Account gate strict mode changed";
|
|
21
|
+
readonly description: "Records authorised strict-mode activation or automatic expiry.";
|
|
22
|
+
readonly defaultEnabled: true;
|
|
23
|
+
}, {
|
|
24
|
+
readonly id: "account-gate-resume-notice";
|
|
25
|
+
readonly name: "Account gate resume notice";
|
|
26
|
+
readonly description: "Records only whether private resume guidance could be delivered after a static gate.";
|
|
27
|
+
readonly defaultEnabled: true;
|
|
28
|
+
}];
|
|
29
|
+
};
|
|
30
|
+
export declare function successfulKickOutcome(outcome: MemberEnforcementOutcome): Extract<MemberEnforcementOutcome, "kicked" | "member_missing">;
|
|
31
|
+
export declare function emitAccountGateCompletedLog(services: ServiceAccess, guildId: string, input: {
|
|
32
|
+
memberUserId: string;
|
|
33
|
+
roleOutcome: string;
|
|
34
|
+
idempotencyKey: string;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
export declare function emitAccountGateKickLog(services: ServiceAccess, guildId: string, input: {
|
|
37
|
+
memberUserId: string;
|
|
38
|
+
outcome: Extract<MemberEnforcementOutcome, "kicked" | "member_missing">;
|
|
39
|
+
directMessage: DirectMessageOutcome;
|
|
40
|
+
source: string;
|
|
41
|
+
idempotencyKey: string;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
export declare function emitLog(services: ServiceAccess, guildId: string, input: Omit<Parameters<ModuleLoggingService["emit"]>[0], "guildId" | "moduleId">): Promise<void>;
|
|
44
|
+
//# sourceMappingURL=logging.d.ts.map
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
2
|
+
import { ACCOUNT_GATE_IDENTITY } from "./contracts.js";
|
|
3
|
+
export const accountGateLogging = {
|
|
4
|
+
events: [
|
|
5
|
+
{
|
|
6
|
+
id: "account-gate-completed",
|
|
7
|
+
name: "Account gate completed",
|
|
8
|
+
description: "Records successful completion using IDs and bounded outcome metadata.",
|
|
9
|
+
defaultEnabled: true,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: "account-gate-review-created",
|
|
13
|
+
name: "Account gate review created",
|
|
14
|
+
description: "Records a staff-review request without copying challenge answers.",
|
|
15
|
+
defaultEnabled: true,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "account-gate-kicked",
|
|
19
|
+
name: "Account gate kick",
|
|
20
|
+
description: "Records the bounded enforcement and direct-message outcomes.",
|
|
21
|
+
defaultEnabled: true,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "account-gate-strict-mode-changed",
|
|
25
|
+
name: "Account gate strict mode changed",
|
|
26
|
+
description: "Records authorised strict-mode activation or automatic expiry.",
|
|
27
|
+
defaultEnabled: true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "account-gate-resume-notice",
|
|
31
|
+
name: "Account gate resume notice",
|
|
32
|
+
description: "Records only whether private resume guidance could be delivered after a static gate.",
|
|
33
|
+
defaultEnabled: true,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
export function successfulKickOutcome(outcome) {
|
|
38
|
+
if (outcome !== "kicked" && outcome !== "member_missing")
|
|
39
|
+
throw new Error("Access Control kick outcome is not successful");
|
|
40
|
+
return outcome;
|
|
41
|
+
}
|
|
42
|
+
export async function emitAccountGateCompletedLog(services, guildId, input) {
|
|
43
|
+
await emitLog(services, guildId, {
|
|
44
|
+
eventId: "account-gate-completed",
|
|
45
|
+
summary: "A member completed Access Control.",
|
|
46
|
+
details: [
|
|
47
|
+
{ label: "Member ID", value: input.memberUserId },
|
|
48
|
+
{ label: "Role update", value: input.roleOutcome },
|
|
49
|
+
],
|
|
50
|
+
idempotencyKey: input.idempotencyKey,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export async function emitAccountGateKickLog(services, guildId, input) {
|
|
54
|
+
await emitLog(services, guildId, {
|
|
55
|
+
eventId: "account-gate-kicked",
|
|
56
|
+
summary: input.outcome === "kicked"
|
|
57
|
+
? "Access Control removed a member from the server."
|
|
58
|
+
: "Access Control found that the member had already left the server.",
|
|
59
|
+
details: [
|
|
60
|
+
{ label: "Member ID", value: input.memberUserId },
|
|
61
|
+
{ label: "Outcome", value: input.outcome },
|
|
62
|
+
{ label: "Direct message", value: input.directMessage },
|
|
63
|
+
{ label: "Source", value: input.source },
|
|
64
|
+
],
|
|
65
|
+
idempotencyKey: input.idempotencyKey,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export async function emitLog(services, guildId, input) {
|
|
69
|
+
if (!services.has(HELYX_SERVICE_NAMES.moduleLogging))
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
await services
|
|
73
|
+
.get(HELYX_SERVICE_NAMES.moduleLogging)
|
|
74
|
+
.emit({
|
|
75
|
+
guildId,
|
|
76
|
+
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
77
|
+
...input,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Customer logging is optional and must not block the gate.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=logging.js.map
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"releaseStatus": "live",
|
|
4
4
|
"id": "helyx.account-gate",
|
|
5
5
|
"name": "Access Control",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.14",
|
|
7
7
|
"description": "Require new members to complete configurable Discord interaction checks before gaining access.",
|
|
8
8
|
"entry": "./dist/index.js",
|
|
9
9
|
"framework": "^0.1.0",
|
|
@@ -686,7 +686,7 @@
|
|
|
686
686
|
],
|
|
687
687
|
"operations": ["list", "read", "preview", "create", "update", "delete"],
|
|
688
688
|
"creationIntents": ["draft", "publish"],
|
|
689
|
-
"publicationMode": "
|
|
689
|
+
"publicationMode": "validate_on_publish",
|
|
690
690
|
"fields": [
|
|
691
691
|
{
|
|
692
692
|
"key": "question",
|
|
@@ -732,7 +732,12 @@
|
|
|
732
732
|
]
|
|
733
733
|
},
|
|
734
734
|
"contributions": {
|
|
735
|
-
"commands": [
|
|
735
|
+
"commands": [
|
|
736
|
+
"access.control.start",
|
|
737
|
+
"access.control.reset",
|
|
738
|
+
"access.control.status",
|
|
739
|
+
"access.control.strict"
|
|
740
|
+
],
|
|
736
741
|
"events": [
|
|
737
742
|
"discord.guild-member-add",
|
|
738
743
|
"discord.guild-member-remove",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
-- Baseline source: 0001_account_gate_foundation.sql
|
|
2
|
+
|
|
1
3
|
CREATE TABLE IF NOT EXISTS account_gate_gate_panels (
|
|
2
4
|
panel_id text PRIMARY KEY,
|
|
3
5
|
guild_id text NOT NULL UNIQUE,
|
|
@@ -9,13 +11,6 @@ CREATE TABLE IF NOT EXISTS account_gate_gate_panels (
|
|
|
9
11
|
CHECK (updated_at >= created_at)
|
|
10
12
|
);
|
|
11
13
|
|
|
12
|
-
CREATE TABLE IF NOT EXISTS account_gate_public_channels (
|
|
13
|
-
guild_id text NOT NULL,
|
|
14
|
-
channel_id text NOT NULL,
|
|
15
|
-
created_at timestamptz NOT NULL,
|
|
16
|
-
PRIMARY KEY (guild_id, channel_id)
|
|
17
|
-
);
|
|
18
|
-
|
|
19
14
|
CREATE TABLE IF NOT EXISTS account_gate_questions (
|
|
20
15
|
question_id text PRIMARY KEY,
|
|
21
16
|
guild_id text NOT NULL,
|
|
@@ -48,17 +43,6 @@ CREATE INDEX IF NOT EXISTS account_gate_question_creator_privacy_idx
|
|
|
48
43
|
attribution_id
|
|
49
44
|
);
|
|
50
45
|
|
|
51
|
-
CREATE TABLE IF NOT EXISTS account_gate_password_secrets (
|
|
52
|
-
guild_id text PRIMARY KEY,
|
|
53
|
-
encoded_hash text NOT NULL,
|
|
54
|
-
ignore_case boolean NOT NULL,
|
|
55
|
-
trim_whitespace boolean NOT NULL,
|
|
56
|
-
revision integer NOT NULL CHECK (revision > 0),
|
|
57
|
-
created_at timestamptz NOT NULL,
|
|
58
|
-
updated_at timestamptz NOT NULL,
|
|
59
|
-
CHECK (updated_at >= created_at)
|
|
60
|
-
);
|
|
61
|
-
|
|
62
46
|
CREATE TABLE IF NOT EXISTS account_gate_attempts (
|
|
63
47
|
attempt_id text PRIMARY KEY,
|
|
64
48
|
guild_id text NOT NULL,
|
|
@@ -301,3 +285,152 @@ DROP TRIGGER IF EXISTS account_gate_terminal_privacy_suppressed
|
|
|
301
285
|
CREATE TRIGGER account_gate_terminal_privacy_suppressed
|
|
302
286
|
AFTER DELETE ON account_gate_terminal_finalizations
|
|
303
287
|
FOR EACH ROW EXECUTE FUNCTION account_gate_install_privacy_suppression();
|
|
288
|
+
|
|
289
|
+
-- Baseline source: 0002_account_gate_panel_message.sql
|
|
290
|
+
|
|
291
|
+
ALTER TABLE account_gate_gate_panels
|
|
292
|
+
ADD COLUMN IF NOT EXISTS message_body text;
|
|
293
|
+
|
|
294
|
+
UPDATE account_gate_gate_panels
|
|
295
|
+
SET message_body =
|
|
296
|
+
'Complete a short human-interaction check to access the server. Press the button below when you are ready.'
|
|
297
|
+
WHERE message_body IS NULL;
|
|
298
|
+
|
|
299
|
+
ALTER TABLE account_gate_gate_panels
|
|
300
|
+
ALTER COLUMN message_body SET DEFAULT
|
|
301
|
+
'Complete a short human-interaction check to access the server. Press the button below when you are ready.',
|
|
302
|
+
ALTER COLUMN message_body SET NOT NULL;
|
|
303
|
+
|
|
304
|
+
DO $$
|
|
305
|
+
BEGIN
|
|
306
|
+
IF NOT EXISTS (
|
|
307
|
+
SELECT 1
|
|
308
|
+
FROM pg_constraint
|
|
309
|
+
WHERE conname = 'account_gate_gate_panels_message_body_length'
|
|
310
|
+
AND conrelid = 'account_gate_gate_panels'::regclass
|
|
311
|
+
) THEN
|
|
312
|
+
ALTER TABLE account_gate_gate_panels
|
|
313
|
+
ADD CONSTRAINT account_gate_gate_panels_message_body_length
|
|
314
|
+
CHECK (char_length(message_body) BETWEEN 1 AND 1000);
|
|
315
|
+
END IF;
|
|
316
|
+
END
|
|
317
|
+
$$;
|
|
318
|
+
|
|
319
|
+
-- Baseline source: 0003_account_gate_revoked_member_status.sql
|
|
320
|
+
|
|
321
|
+
DO $$
|
|
322
|
+
BEGIN
|
|
323
|
+
IF to_regclass('account_gate_member_summaries') IS NOT NULL THEN
|
|
324
|
+
ALTER TABLE account_gate_member_summaries
|
|
325
|
+
DROP CONSTRAINT IF EXISTS account_gate_member_summaries_last_decision_check;
|
|
326
|
+
|
|
327
|
+
ALTER TABLE account_gate_member_summaries
|
|
328
|
+
ADD CONSTRAINT account_gate_member_summaries_last_decision_check
|
|
329
|
+
CHECK (
|
|
330
|
+
last_decision IN (
|
|
331
|
+
'pending', 'finalizing', 'passed', 'failed', 'review', 'kick_pending',
|
|
332
|
+
'kicked', 'dismissed', 'revoked'
|
|
333
|
+
)
|
|
334
|
+
);
|
|
335
|
+
END IF;
|
|
336
|
+
END
|
|
337
|
+
$$;
|
|
338
|
+
|
|
339
|
+
-- Baseline source: 0004_account_gate_multiple_panels.sql
|
|
340
|
+
|
|
341
|
+
ALTER TABLE account_gate_gate_panels
|
|
342
|
+
ADD COLUMN IF NOT EXISTS access_type text NOT NULL DEFAULT 'challenges';
|
|
343
|
+
|
|
344
|
+
DO $$
|
|
345
|
+
BEGIN
|
|
346
|
+
IF NOT EXISTS (
|
|
347
|
+
SELECT 1
|
|
348
|
+
FROM pg_constraint
|
|
349
|
+
WHERE conname = 'account_gate_gate_panels_access_type'
|
|
350
|
+
AND conrelid = 'account_gate_gate_panels'::regclass
|
|
351
|
+
) THEN
|
|
352
|
+
ALTER TABLE account_gate_gate_panels
|
|
353
|
+
ADD CONSTRAINT account_gate_gate_panels_access_type
|
|
354
|
+
CHECK (access_type IN ('challenges', 'password', 'emoji'));
|
|
355
|
+
END IF;
|
|
356
|
+
END
|
|
357
|
+
$$;
|
|
358
|
+
|
|
359
|
+
ALTER TABLE account_gate_gate_panels
|
|
360
|
+
DROP CONSTRAINT IF EXISTS account_gate_gate_panels_guild_id_key;
|
|
361
|
+
|
|
362
|
+
CREATE INDEX IF NOT EXISTS account_gate_gate_panels_guild_idx
|
|
363
|
+
ON account_gate_gate_panels (guild_id, created_at, panel_id);
|
|
364
|
+
|
|
365
|
+
-- Baseline source: 0005_account_gate_panel_passwords.sql
|
|
366
|
+
|
|
367
|
+
ALTER TABLE account_gate_gate_panels
|
|
368
|
+
ADD COLUMN IF NOT EXISTS password_hash text,
|
|
369
|
+
ADD COLUMN IF NOT EXISTS password_ignore_case boolean NOT NULL DEFAULT true,
|
|
370
|
+
ADD COLUMN IF NOT EXISTS password_trim_whitespace boolean NOT NULL DEFAULT true;
|
|
371
|
+
|
|
372
|
+
CREATE OR REPLACE FUNCTION account_gate_enforce_panel_limit()
|
|
373
|
+
RETURNS trigger
|
|
374
|
+
LANGUAGE plpgsql
|
|
375
|
+
AS $$
|
|
376
|
+
BEGIN
|
|
377
|
+
PERFORM pg_advisory_xact_lock(
|
|
378
|
+
hashtextextended(NEW.guild_id, 4815162342)
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
IF (
|
|
382
|
+
SELECT count(*)
|
|
383
|
+
FROM account_gate_gate_panels
|
|
384
|
+
WHERE guild_id = NEW.guild_id
|
|
385
|
+
) >= 10 THEN
|
|
386
|
+
RAISE EXCEPTION USING
|
|
387
|
+
ERRCODE = 'check_violation',
|
|
388
|
+
MESSAGE = 'Account Gate panel limit reached';
|
|
389
|
+
END IF;
|
|
390
|
+
|
|
391
|
+
RETURN NEW;
|
|
392
|
+
END
|
|
393
|
+
$$;
|
|
394
|
+
|
|
395
|
+
DROP TRIGGER IF EXISTS account_gate_gate_panels_limit
|
|
396
|
+
ON account_gate_gate_panels;
|
|
397
|
+
|
|
398
|
+
CREATE TRIGGER account_gate_gate_panels_limit
|
|
399
|
+
BEFORE INSERT ON account_gate_gate_panels
|
|
400
|
+
FOR EACH ROW
|
|
401
|
+
EXECUTE FUNCTION account_gate_enforce_panel_limit();
|
|
402
|
+
|
|
403
|
+
-- Baseline source: 0007_privacy_suppression_trigger_privileges.sql
|
|
404
|
+
|
|
405
|
+
-- Privacy erasure deletes Account Gate subject rows through a deliberately
|
|
406
|
+
-- column-scoped executor role. The deletion triggers must still be able to
|
|
407
|
+
-- write the suppression tombstone that prevents erased data being recreated.
|
|
408
|
+
CREATE OR REPLACE FUNCTION public.account_gate_install_privacy_suppression()
|
|
409
|
+
RETURNS trigger
|
|
410
|
+
LANGUAGE plpgsql
|
|
411
|
+
SECURITY DEFINER
|
|
412
|
+
SET search_path = pg_catalog, public
|
|
413
|
+
AS $$
|
|
414
|
+
BEGIN
|
|
415
|
+
INSERT INTO public.account_gate_privacy_suppressions (
|
|
416
|
+
suppression_id,
|
|
417
|
+
guild_id,
|
|
418
|
+
member_user_id,
|
|
419
|
+
suppressed_at,
|
|
420
|
+
created_at
|
|
421
|
+
)
|
|
422
|
+
VALUES (
|
|
423
|
+
OLD.guild_id || ':' || OLD.member_user_id,
|
|
424
|
+
OLD.guild_id,
|
|
425
|
+
OLD.member_user_id,
|
|
426
|
+
clock_timestamp(),
|
|
427
|
+
clock_timestamp()
|
|
428
|
+
)
|
|
429
|
+
ON CONFLICT (guild_id, member_user_id)
|
|
430
|
+
DO UPDATE SET suppressed_at = GREATEST(
|
|
431
|
+
public.account_gate_privacy_suppressions.suppressed_at,
|
|
432
|
+
EXCLUDED.suppressed_at
|
|
433
|
+
);
|
|
434
|
+
RETURN OLD;
|
|
435
|
+
END;
|
|
436
|
+
$$;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helyx/module-account-gate",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "Require configurable Discord interaction checks before new members gain access.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helyx",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"build": "tsc -b"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@helyx/module-manifest": ">=0.
|
|
52
|
-
"@helyx/sdk": ">=0.
|
|
51
|
+
"@helyx/module-manifest": ">=0.7.0 <0.8.0",
|
|
52
|
+
"@helyx/sdk": ">=0.8.4 <0.9.0"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
ALTER TABLE account_gate_gate_panels
|
|
2
|
-
ADD COLUMN IF NOT EXISTS message_body text;
|
|
3
|
-
|
|
4
|
-
UPDATE account_gate_gate_panels
|
|
5
|
-
SET message_body =
|
|
6
|
-
'Complete a short human-interaction check to access the server. Press the button below when you are ready.'
|
|
7
|
-
WHERE message_body IS NULL;
|
|
8
|
-
|
|
9
|
-
ALTER TABLE account_gate_gate_panels
|
|
10
|
-
ALTER COLUMN message_body SET DEFAULT
|
|
11
|
-
'Complete a short human-interaction check to access the server. Press the button below when you are ready.',
|
|
12
|
-
ALTER COLUMN message_body SET NOT NULL;
|
|
13
|
-
|
|
14
|
-
DO $$
|
|
15
|
-
BEGIN
|
|
16
|
-
IF NOT EXISTS (
|
|
17
|
-
SELECT 1
|
|
18
|
-
FROM pg_constraint
|
|
19
|
-
WHERE conname = 'account_gate_gate_panels_message_body_length'
|
|
20
|
-
AND conrelid = 'account_gate_gate_panels'::regclass
|
|
21
|
-
) THEN
|
|
22
|
-
ALTER TABLE account_gate_gate_panels
|
|
23
|
-
ADD CONSTRAINT account_gate_gate_panels_message_body_length
|
|
24
|
-
CHECK (char_length(message_body) BETWEEN 1 AND 1000);
|
|
25
|
-
END IF;
|
|
26
|
-
END
|
|
27
|
-
$$;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
DO $$
|
|
2
|
-
BEGIN
|
|
3
|
-
IF to_regclass('account_gate_member_summaries') IS NOT NULL THEN
|
|
4
|
-
ALTER TABLE account_gate_member_summaries
|
|
5
|
-
DROP CONSTRAINT IF EXISTS account_gate_member_summaries_last_decision_check;
|
|
6
|
-
|
|
7
|
-
ALTER TABLE account_gate_member_summaries
|
|
8
|
-
ADD CONSTRAINT account_gate_member_summaries_last_decision_check
|
|
9
|
-
CHECK (
|
|
10
|
-
last_decision IN (
|
|
11
|
-
'pending', 'finalizing', 'passed', 'failed', 'review', 'kick_pending',
|
|
12
|
-
'kicked', 'dismissed', 'revoked'
|
|
13
|
-
)
|
|
14
|
-
);
|
|
15
|
-
END IF;
|
|
16
|
-
END
|
|
17
|
-
$$;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
ALTER TABLE account_gate_gate_panels
|
|
2
|
-
ADD COLUMN IF NOT EXISTS access_type text NOT NULL DEFAULT 'challenges';
|
|
3
|
-
|
|
4
|
-
DO $$
|
|
5
|
-
BEGIN
|
|
6
|
-
IF NOT EXISTS (
|
|
7
|
-
SELECT 1
|
|
8
|
-
FROM pg_constraint
|
|
9
|
-
WHERE conname = 'account_gate_gate_panels_access_type'
|
|
10
|
-
AND conrelid = 'account_gate_gate_panels'::regclass
|
|
11
|
-
) THEN
|
|
12
|
-
ALTER TABLE account_gate_gate_panels
|
|
13
|
-
ADD CONSTRAINT account_gate_gate_panels_access_type
|
|
14
|
-
CHECK (access_type IN ('challenges', 'password', 'emoji'));
|
|
15
|
-
END IF;
|
|
16
|
-
END
|
|
17
|
-
$$;
|
|
18
|
-
|
|
19
|
-
ALTER TABLE account_gate_gate_panels
|
|
20
|
-
DROP CONSTRAINT IF EXISTS account_gate_gate_panels_guild_id_key;
|
|
21
|
-
|
|
22
|
-
CREATE INDEX IF NOT EXISTS account_gate_gate_panels_guild_idx
|
|
23
|
-
ON account_gate_gate_panels (guild_id, created_at, panel_id);
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
ALTER TABLE account_gate_gate_panels
|
|
2
|
-
ADD COLUMN IF NOT EXISTS password_hash text,
|
|
3
|
-
ADD COLUMN IF NOT EXISTS password_ignore_case boolean NOT NULL DEFAULT true,
|
|
4
|
-
ADD COLUMN IF NOT EXISTS password_trim_whitespace boolean NOT NULL DEFAULT true;
|
|
5
|
-
|
|
6
|
-
UPDATE account_gate_gate_panels AS panel
|
|
7
|
-
SET
|
|
8
|
-
password_hash = secret.encoded_hash,
|
|
9
|
-
password_ignore_case = secret.ignore_case,
|
|
10
|
-
password_trim_whitespace = secret.trim_whitespace
|
|
11
|
-
FROM account_gate_password_secrets AS secret
|
|
12
|
-
WHERE panel.guild_id = secret.guild_id
|
|
13
|
-
AND panel.access_type = 'password'
|
|
14
|
-
AND panel.password_hash IS NULL;
|
|
15
|
-
|
|
16
|
-
CREATE OR REPLACE FUNCTION account_gate_enforce_panel_limit()
|
|
17
|
-
RETURNS trigger
|
|
18
|
-
LANGUAGE plpgsql
|
|
19
|
-
AS $$
|
|
20
|
-
BEGIN
|
|
21
|
-
PERFORM pg_advisory_xact_lock(
|
|
22
|
-
hashtextextended(NEW.guild_id, 4815162342)
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
IF (
|
|
26
|
-
SELECT count(*)
|
|
27
|
-
FROM account_gate_gate_panels
|
|
28
|
-
WHERE guild_id = NEW.guild_id
|
|
29
|
-
) >= 10 THEN
|
|
30
|
-
RAISE EXCEPTION USING
|
|
31
|
-
ERRCODE = 'check_violation',
|
|
32
|
-
MESSAGE = 'Account Gate panel limit reached';
|
|
33
|
-
END IF;
|
|
34
|
-
|
|
35
|
-
RETURN NEW;
|
|
36
|
-
END
|
|
37
|
-
$$;
|
|
38
|
-
|
|
39
|
-
DROP TRIGGER IF EXISTS account_gate_gate_panels_limit
|
|
40
|
-
ON account_gate_gate_panels;
|
|
41
|
-
|
|
42
|
-
CREATE TRIGGER account_gate_gate_panels_limit
|
|
43
|
-
BEFORE INSERT ON account_gate_gate_panels
|
|
44
|
-
FOR EACH ROW
|
|
45
|
-
EXECUTE FUNCTION account_gate_enforce_panel_limit();
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
-- Privacy erasure deletes Account Gate subject rows through a deliberately
|
|
2
|
-
-- column-scoped executor role. The deletion triggers must still be able to
|
|
3
|
-
-- write the suppression tombstone that prevents erased data being recreated.
|
|
4
|
-
CREATE OR REPLACE FUNCTION public.account_gate_install_privacy_suppression()
|
|
5
|
-
RETURNS trigger
|
|
6
|
-
LANGUAGE plpgsql
|
|
7
|
-
SECURITY DEFINER
|
|
8
|
-
SET search_path = pg_catalog, public
|
|
9
|
-
AS $$
|
|
10
|
-
BEGIN
|
|
11
|
-
INSERT INTO public.account_gate_privacy_suppressions (
|
|
12
|
-
suppression_id,
|
|
13
|
-
guild_id,
|
|
14
|
-
member_user_id,
|
|
15
|
-
suppressed_at,
|
|
16
|
-
created_at
|
|
17
|
-
)
|
|
18
|
-
VALUES (
|
|
19
|
-
OLD.guild_id || ':' || OLD.member_user_id,
|
|
20
|
-
OLD.guild_id,
|
|
21
|
-
OLD.member_user_id,
|
|
22
|
-
clock_timestamp(),
|
|
23
|
-
clock_timestamp()
|
|
24
|
-
)
|
|
25
|
-
ON CONFLICT (guild_id, member_user_id)
|
|
26
|
-
DO UPDATE SET suppressed_at = GREATEST(
|
|
27
|
-
public.account_gate_privacy_suppressions.suppressed_at,
|
|
28
|
-
EXCLUDED.suppressed_at
|
|
29
|
-
);
|
|
30
|
-
RETURN OLD;
|
|
31
|
-
END;
|
|
32
|
-
$$;
|