@helyx/module-account-gate 1.0.12 → 1.0.15
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 +4 -4
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +30 -32
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.js +1 -1
- package/dist/index.js +103 -75
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +84 -0
- package/manifest.json +15 -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,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add manifest-backed deployment support and credential-risk policy, enforce it
|
|
8
|
+
through hosted and self-host composition, and carry it into generated build
|
|
9
|
+
manifests and self-host module labels.
|
|
10
|
+
- Updated dependencies
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
- @helyx/sdk@0.9.0
|
|
14
|
+
- @helyx/module-manifest@0.8.0
|
|
15
|
+
|
|
16
|
+
## 1.0.14
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Move Access Control commands beneath `/access control` and register them only for servers where the module is selected.
|
|
21
|
+
- Emit the declared completion and kick Logging events for automatic, staff and recovery outcomes.
|
|
22
|
+
|
|
23
|
+
## 1.0.13
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Consolidate the module database migration history into a clean one-file baseline for the planned database rebuild.
|
|
28
|
+
|
|
3
29
|
## 1.0.12
|
|
4
30
|
|
|
5
31
|
### 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.
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CommandInteractionContext, SharedCommandContribution } from "@helyx/sdk";
|
|
2
2
|
type CommandHandler = (context: CommandInteractionContext) => Promise<void>;
|
|
3
3
|
export declare function accountGateCommands(handlers: {
|
|
4
4
|
start: CommandHandler;
|
|
5
5
|
reset: CommandHandler;
|
|
6
6
|
status: CommandHandler;
|
|
7
7
|
strict: CommandHandler;
|
|
8
|
-
}): readonly
|
|
8
|
+
}): readonly SharedCommandContribution[];
|
|
9
9
|
export {};
|
|
10
10
|
//# sourceMappingURL=commands.d.ts.map
|
package/dist/commands.js
CHANGED
|
@@ -1,39 +1,37 @@
|
|
|
1
1
|
export function accountGateCommands(handlers) {
|
|
2
2
|
return [
|
|
3
3
|
{
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
family: "access",
|
|
5
|
+
path: "control.start",
|
|
6
|
+
description: "Start or resume your Access Control check",
|
|
6
7
|
acknowledgement: "deferred-ephemeral",
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
execute: handlers.strict,
|
|
35
|
-
},
|
|
36
|
-
],
|
|
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,
|
|
37
35
|
},
|
|
38
36
|
];
|
|
39
37
|
}
|
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.15";
|
|
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
|
@@ -14,6 +14,7 @@ import { gatePanelResource, getGatePanelRuntime, hasPasswordGatePanel, } from ".
|
|
|
14
14
|
import { isAccountGateWorkSuppressed } from "./privacy.js";
|
|
15
15
|
import { applyChallengeAction, createAccountGateSessionState, parseAccountGateSessionState, rerollForAccessibility, } from "./workflow.js";
|
|
16
16
|
import { accountGateCommands } from "./commands.js";
|
|
17
|
+
import { accountGateLogging, emitAccountGateCompletedLog, emitAccountGateKickLog, emitLog, successfulKickOutcome, } from "./logging.js";
|
|
17
18
|
const manifest = parseModuleManifest(JSON.parse(readFileSync(new URL("../manifest.json", import.meta.url), "utf8")));
|
|
18
19
|
const VIOLET = 0x7c3aed;
|
|
19
20
|
const GREEN = 0x16a34a;
|
|
@@ -49,7 +50,7 @@ export const accountGateModule = defineModule({
|
|
|
49
50
|
async validateActivation(context) {
|
|
50
51
|
const configuration = await getConfiguration(context.services, context.guildId);
|
|
51
52
|
if (!configuration)
|
|
52
|
-
|
|
53
|
+
return;
|
|
53
54
|
const errors = await validateAccountGateSetup(context.services, context.guildId, configuration, false, true);
|
|
54
55
|
if (errors.length > 0)
|
|
55
56
|
throw new ModuleStateValidationError(errors[0]);
|
|
@@ -82,47 +83,14 @@ export const accountGateModule = defineModule({
|
|
|
82
83
|
accountGateMemberResource,
|
|
83
84
|
serverQuestionResource,
|
|
84
85
|
],
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: "account-gate-review-created",
|
|
95
|
-
name: "Account gate review created",
|
|
96
|
-
description: "Records a staff-review request without copying challenge answers.",
|
|
97
|
-
defaultEnabled: true,
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
id: "account-gate-kicked",
|
|
101
|
-
name: "Account gate kick",
|
|
102
|
-
description: "Records the bounded enforcement and direct-message outcomes.",
|
|
103
|
-
defaultEnabled: true,
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
id: "account-gate-strict-mode-changed",
|
|
107
|
-
name: "Account gate strict mode changed",
|
|
108
|
-
description: "Records authorised strict-mode activation or automatic expiry.",
|
|
109
|
-
defaultEnabled: true,
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
id: "account-gate-resume-notice",
|
|
113
|
-
name: "Account gate resume notice",
|
|
114
|
-
description: "Records only whether private resume guidance could be delivered after a static gate.",
|
|
115
|
-
defaultEnabled: true,
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
},
|
|
86
|
+
sharedCommands: accountGateCommands({
|
|
87
|
+
start: startCheck,
|
|
88
|
+
reset: resetOwnAccess,
|
|
89
|
+
status: showStatus,
|
|
90
|
+
strict: activateStrictModeCommand,
|
|
91
|
+
}),
|
|
92
|
+
logging: accountGateLogging,
|
|
119
93
|
interactions: {
|
|
120
|
-
commands: accountGateCommands({
|
|
121
|
-
start: startCheck,
|
|
122
|
-
reset: resetOwnAccess,
|
|
123
|
-
status: showStatus,
|
|
124
|
-
strict: activateStrictModeCommand,
|
|
125
|
-
}),
|
|
126
94
|
buttons: [
|
|
127
95
|
{
|
|
128
96
|
customId: ACCOUNT_GATE_COMPONENTS.start,
|
|
@@ -167,6 +135,7 @@ export const accountGateModule = defineModule({
|
|
|
167
135
|
modals: [
|
|
168
136
|
{
|
|
169
137
|
customIdPrefix: ACCOUNT_GATE_COMPONENTS.recallSubmitPrefix,
|
|
138
|
+
acknowledgement: "deferred-ephemeral",
|
|
170
139
|
execute: submitRecallModal,
|
|
171
140
|
},
|
|
172
141
|
],
|
|
@@ -920,20 +889,9 @@ async function rerollChallenge(context) {
|
|
|
920
889
|
async function openRecallModal(context) {
|
|
921
890
|
if (!context.serverId)
|
|
922
891
|
return replyError(context, "This check is unavailable.");
|
|
923
|
-
const session = await context.services
|
|
924
|
-
.get(HELYX_SERVICE_NAMES.sessions)
|
|
925
|
-
.getFor(context.serverId, context.userId, ACCOUNT_GATE_SESSION_PURPOSE);
|
|
926
892
|
const binding = parseComponentBinding(context.customId, ACCOUNT_GATE_COMPONENTS.recallOpenPrefix);
|
|
927
|
-
|
|
928
|
-
if (!session ||
|
|
929
|
-
!state ||
|
|
930
|
-
!binding ||
|
|
931
|
-
binding.sessionId !== session.sessionId ||
|
|
932
|
-
binding.version !== session.version ||
|
|
933
|
-
binding.step !== state.step ||
|
|
934
|
-
state.chain[state.currentStep]?.type !== "modal_recall") {
|
|
893
|
+
if (!binding)
|
|
935
894
|
return replyError(context, "This recall challenge has expired.");
|
|
936
|
-
}
|
|
937
895
|
await context.showModal({
|
|
938
896
|
customId: boundComponentId(ACCOUNT_GATE_COMPONENTS.recallSubmitPrefix, binding),
|
|
939
897
|
title: "Enter the recalled code",
|
|
@@ -949,10 +907,30 @@ async function openRecallModal(context) {
|
|
|
949
907
|
},
|
|
950
908
|
],
|
|
951
909
|
});
|
|
952
|
-
|
|
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, {
|
|
953
930
|
sessionId: session.sessionId,
|
|
954
931
|
version: session.version,
|
|
955
|
-
}, { redactRecallCode: true }))
|
|
932
|
+
}, { redactRecallCode: true }))
|
|
933
|
+
.catch(() => undefined);
|
|
956
934
|
}
|
|
957
935
|
async function submitRecallModal(context) {
|
|
958
936
|
const binding = parseComponentBinding(context.customId, ACCOUNT_GATE_COMPONENTS.recallSubmitPrefix);
|
|
@@ -1478,7 +1456,15 @@ async function enforceJoinDeadline(context, task) {
|
|
|
1478
1456
|
},
|
|
1479
1457
|
});
|
|
1480
1458
|
if (["kicked", "member_missing"].includes(result.outcome)) {
|
|
1481
|
-
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
|
+
});
|
|
1482
1468
|
}
|
|
1483
1469
|
}
|
|
1484
1470
|
return { outcome: "completed" };
|
|
@@ -1699,6 +1685,11 @@ async function finalizeTerminalFinalization(services, guildId, finalizationId) {
|
|
|
1699
1685
|
const sessions = services.get(HELYX_SERVICE_NAMES.sessions);
|
|
1700
1686
|
if (row.outcome === "passed") {
|
|
1701
1687
|
if (summary.last_decision === "passed") {
|
|
1688
|
+
await emitAccountGateCompletedLog(services, guildId, {
|
|
1689
|
+
memberUserId,
|
|
1690
|
+
roleOutcome: "already_applied",
|
|
1691
|
+
idempotencyKey: `account-gate-completed:${finalizationId}`,
|
|
1692
|
+
});
|
|
1702
1693
|
await sessions.delete(stringValue(row.session_id));
|
|
1703
1694
|
await finishTerminalRecord(records, row, "completed");
|
|
1704
1695
|
return;
|
|
@@ -1728,6 +1719,11 @@ async function finalizeTerminalFinalization(services, guildId, finalizationId) {
|
|
|
1728
1719
|
await finishTerminalRecord(records, row, "cancelled");
|
|
1729
1720
|
return;
|
|
1730
1721
|
}
|
|
1722
|
+
await emitAccountGateCompletedLog(services, guildId, {
|
|
1723
|
+
memberUserId,
|
|
1724
|
+
roleOutcome: transition.outcome,
|
|
1725
|
+
idempotencyKey: `account-gate-completed:${finalizationId}`,
|
|
1726
|
+
});
|
|
1731
1727
|
}
|
|
1732
1728
|
}
|
|
1733
1729
|
else {
|
|
@@ -1761,7 +1757,7 @@ async function applyDurableFailureAction(services, configuration, guildId, membe
|
|
|
1761
1757
|
if (configuration.failureAction !== "kick")
|
|
1762
1758
|
return;
|
|
1763
1759
|
const records = services.get(HELYX_SERVICE_NAMES.records);
|
|
1764
|
-
await services
|
|
1760
|
+
const result = await services
|
|
1765
1761
|
.get(HELYX_SERVICE_NAMES.memberEnforcement)
|
|
1766
1762
|
.kick({
|
|
1767
1763
|
guildId,
|
|
@@ -1799,6 +1795,14 @@ async function applyDurableFailureAction(services, configuration, guildId, membe
|
|
|
1799
1795
|
return !(await isAccountGateWorkSuppressed(services, guildId, memberUserId, dateValue(terminal.work_created_at) ?? new Date(0)));
|
|
1800
1796
|
},
|
|
1801
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
|
+
});
|
|
1802
1806
|
}
|
|
1803
1807
|
function validateReviewDeliveryPayload(input) {
|
|
1804
1808
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
@@ -1932,6 +1936,7 @@ async function recoverReviewClaim(context, task) {
|
|
|
1932
1936
|
}
|
|
1933
1937
|
const action = stringValue(row.claim_action);
|
|
1934
1938
|
if (action === "approve") {
|
|
1939
|
+
let completionRoleOutcome = null;
|
|
1935
1940
|
if (summary.last_decision !== "passed") {
|
|
1936
1941
|
const snapshots = await context.services
|
|
1937
1942
|
.get(HELYX_SERVICE_NAMES.discordResources)
|
|
@@ -1969,8 +1974,15 @@ async function recoverReviewClaim(context, task) {
|
|
|
1969
1974
|
await finalizeReviewClaim(records, guildId, reviewId, claimRevision, "dismissed", row);
|
|
1970
1975
|
return { outcome: "completed" };
|
|
1971
1976
|
}
|
|
1977
|
+
completionRoleOutcome = transition.outcome;
|
|
1972
1978
|
}
|
|
1973
|
-
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
|
+
});
|
|
1974
1986
|
return { outcome: "completed" };
|
|
1975
1987
|
}
|
|
1976
1988
|
if (action === "retry") {
|
|
@@ -1988,6 +2000,8 @@ async function recoverReviewClaim(context, task) {
|
|
|
1988
2000
|
const snapshots = await context.services
|
|
1989
2001
|
.get(HELYX_SERVICE_NAMES.discordResources)
|
|
1990
2002
|
.resolveMemberSnapshots({ guildId, userIds: [memberUserId] });
|
|
2003
|
+
let kickOutcome = "member_missing";
|
|
2004
|
+
let directMessage = "not_requested";
|
|
1991
2005
|
if (snapshots[0]?.exists === true) {
|
|
1992
2006
|
const result = await context.services
|
|
1993
2007
|
.get(HELYX_SERVICE_NAMES.memberEnforcement)
|
|
@@ -2016,8 +2030,18 @@ async function recoverReviewClaim(context, task) {
|
|
|
2016
2030
|
delayMs: 30_000,
|
|
2017
2031
|
safeCode: "review-kick-not-complete",
|
|
2018
2032
|
};
|
|
2033
|
+
kickOutcome = successfulKickOutcome(result.outcome);
|
|
2034
|
+
directMessage = result.directMessage;
|
|
2019
2035
|
}
|
|
2020
|
-
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
|
+
});
|
|
2021
2045
|
return { outcome: "completed" };
|
|
2022
2046
|
}
|
|
2023
2047
|
await finalizeReviewClaim(records, guildId, reviewId, claimRevision, "dismissed", row);
|
|
@@ -2344,6 +2368,8 @@ async function handleReviewDecision(context, decision) {
|
|
|
2344
2368
|
return;
|
|
2345
2369
|
}
|
|
2346
2370
|
let status;
|
|
2371
|
+
let completionRoleOutcome = null;
|
|
2372
|
+
let kickResult = null;
|
|
2347
2373
|
if (decision === "approve") {
|
|
2348
2374
|
const transition = await applyRoleTransition({
|
|
2349
2375
|
guildId: context.serverId,
|
|
@@ -2372,6 +2398,7 @@ async function handleReviewDecision(context, decision) {
|
|
|
2372
2398
|
await replyError(context, "The member rejoined before approval completed; the old review was dismissed.");
|
|
2373
2399
|
return;
|
|
2374
2400
|
}
|
|
2401
|
+
completionRoleOutcome = transition.outcome;
|
|
2375
2402
|
status = "approved";
|
|
2376
2403
|
}
|
|
2377
2404
|
else if (decision === "retry") {
|
|
@@ -2436,6 +2463,10 @@ async function handleReviewDecision(context, decision) {
|
|
|
2436
2463
|
await replyError(context, `The member was not kicked (${result.outcome.replaceAll("_", " ")}). The claimed decision remains queued for safe recovery.`);
|
|
2437
2464
|
return;
|
|
2438
2465
|
}
|
|
2466
|
+
kickResult = {
|
|
2467
|
+
outcome: successfulKickOutcome(result.outcome),
|
|
2468
|
+
directMessage: result.directMessage,
|
|
2469
|
+
};
|
|
2439
2470
|
status = "kicked";
|
|
2440
2471
|
}
|
|
2441
2472
|
else {
|
|
@@ -2476,6 +2507,19 @@ async function handleReviewDecision(context, decision) {
|
|
|
2476
2507
|
await replyError(context, "This review changed in another session. Reload it before trying again.");
|
|
2477
2508
|
return;
|
|
2478
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
|
+
});
|
|
2479
2523
|
if (context.channelId) {
|
|
2480
2524
|
await context.editChannelMessage(context.channelId, context.messageId, {
|
|
2481
2525
|
allowedUserMentionIds: [],
|
|
@@ -2937,22 +2981,6 @@ function secureRandom(services) {
|
|
|
2937
2981
|
},
|
|
2938
2982
|
};
|
|
2939
2983
|
}
|
|
2940
|
-
async function emitLog(services, guildId, input) {
|
|
2941
|
-
if (!services.has(HELYX_SERVICE_NAMES.moduleLogging))
|
|
2942
|
-
return;
|
|
2943
|
-
try {
|
|
2944
|
-
await services
|
|
2945
|
-
.get(HELYX_SERVICE_NAMES.moduleLogging)
|
|
2946
|
-
.emit({
|
|
2947
|
-
guildId,
|
|
2948
|
-
moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
|
|
2949
|
-
...input,
|
|
2950
|
-
});
|
|
2951
|
-
}
|
|
2952
|
-
catch {
|
|
2953
|
-
// Customer logging is optional and must not block the gate.
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
2984
|
async function replyError(context, description) {
|
|
2957
2985
|
await context.reply({
|
|
2958
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
|
@@ -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.0.
|
|
13
|
+
"version": "1.0.15",
|
|
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",
|
|
@@ -732,7 +739,12 @@
|
|
|
732
739
|
]
|
|
733
740
|
},
|
|
734
741
|
"contributions": {
|
|
735
|
-
"commands": [
|
|
742
|
+
"commands": [
|
|
743
|
+
"access.control.start",
|
|
744
|
+
"access.control.reset",
|
|
745
|
+
"access.control.status",
|
|
746
|
+
"access.control.strict"
|
|
747
|
+
],
|
|
736
748
|
"events": [
|
|
737
749
|
"discord.guild-member-add",
|
|
738
750
|
"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.15",
|
|
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.8.0 <0.9.0",
|
|
52
|
+
"@helyx/sdk": ">=0.9.0 <0.10.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
|
-
$$;
|