@helyx/bot 1.5.1 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/command-root-catalogue.json +452 -0
  3. package/dist/cached-message-deletion-authority.d.ts +27 -0
  4. package/dist/cached-message-deletion-authority.js +288 -0
  5. package/dist/channel-audit-configuration.d.ts +11 -1
  6. package/dist/channel-audit-configuration.js +28 -6
  7. package/dist/channel-audit-control.js +42 -2
  8. package/dist/channel-audit-permissions.js +76 -3
  9. package/dist/cloud-command-policy-sync.d.ts +26 -0
  10. package/dist/cloud-command-policy-sync.js +198 -0
  11. package/dist/cloud-connector.d.ts +6 -1
  12. package/dist/cloud-connector.js +224 -53
  13. package/dist/cloud-control-dispatcher.js +18 -0
  14. package/dist/command-policy-runtime.d.ts +23 -0
  15. package/dist/command-policy-runtime.js +53 -0
  16. package/dist/command-root-catalogue.d.ts +5 -0
  17. package/dist/command-root-catalogue.js +16 -0
  18. package/dist/control-error-mapping.d.ts +8 -0
  19. package/dist/control-error-mapping.js +60 -0
  20. package/dist/control-errors.d.ts +3 -1
  21. package/dist/control-errors.js +4 -1
  22. package/dist/control-health-routes.d.ts +10 -0
  23. package/dist/control-health-routes.js +28 -0
  24. package/dist/control-server-schemas.d.ts +7 -0
  25. package/dist/control-server-schemas.js +9 -0
  26. package/dist/control-server.d.ts +27 -6
  27. package/dist/control-server.js +103 -122
  28. package/dist/creator-invite-key.d.ts +6 -0
  29. package/dist/creator-invite-key.js +16 -0
  30. package/dist/creator-invite-service.d.ts +3 -1
  31. package/dist/creator-invite-service.js +8 -7
  32. package/dist/creator-onboarding-platform.d.ts +1 -0
  33. package/dist/creator-onboarding-platform.js +5 -2
  34. package/dist/dashboard-feature-control.d.ts +2 -1
  35. package/dist/dashboard-feature-control.js +1 -0
  36. package/dist/dashboard-logging.d.ts +20 -0
  37. package/dist/dashboard-logging.js +39 -0
  38. package/dist/dashboard-module-projection.d.ts +1 -1
  39. package/dist/dashboard-module-projection.js +5 -2
  40. package/dist/dashboard-preview-import-routes.js +8 -0
  41. package/dist/database-bootstrap.d.ts +1 -1
  42. package/dist/database-bootstrap.js +4 -3
  43. package/dist/discord-channel-audit.js +1 -0
  44. package/dist/discord-user-identities.d.ts +14 -0
  45. package/dist/discord-user-identities.js +64 -0
  46. package/dist/emergency-deletion-dispatcher.d.ts +68 -0
  47. package/dist/emergency-deletion-dispatcher.js +502 -0
  48. package/dist/emergency-deletion-rest-transport.d.ts +24 -0
  49. package/dist/emergency-deletion-rest-transport.js +253 -0
  50. package/dist/events.d.ts +5 -0
  51. package/dist/events.js +53 -10
  52. package/dist/guarded-discord-dispatcher.d.ts +29 -0
  53. package/dist/guarded-discord-dispatcher.js +204 -0
  54. package/dist/guild-command-discord-client.d.ts +2 -0
  55. package/dist/guild-command-discord-client.js +7 -0
  56. package/dist/guild-command-projection.d.ts +4 -1
  57. package/dist/guild-command-projection.js +25 -0
  58. package/dist/guild-command-read-service.d.ts +10 -1
  59. package/dist/guild-command-read-service.js +37 -0
  60. package/dist/guild-command-worker.d.ts +19 -4
  61. package/dist/guild-command-worker.js +356 -26
  62. package/dist/guild-message-protection-author-roles.d.ts +7 -0
  63. package/dist/guild-message-protection-author-roles.js +93 -0
  64. package/dist/guild-message-protection-deletion.d.ts +21 -0
  65. package/dist/guild-message-protection-deletion.js +138 -0
  66. package/dist/guild-message-protection-delivery.d.ts +41 -0
  67. package/dist/guild-message-protection-delivery.js +223 -0
  68. package/dist/guild-message-update-metadata.d.ts +32 -0
  69. package/dist/guild-message-update-metadata.js +255 -0
  70. package/dist/guilds.d.ts +2 -1
  71. package/dist/guilds.js +11 -1
  72. package/dist/hosted-server-moderation.d.ts +2 -2
  73. package/dist/index.js +68 -4
  74. package/dist/interactions.d.ts +3 -1
  75. package/dist/interactions.js +77 -12
  76. package/dist/invite-rollup-maintenance.d.ts +9 -0
  77. package/dist/invite-rollup-maintenance.js +12 -0
  78. package/dist/managed-resource-authorization.d.ts +15 -0
  79. package/dist/managed-resource-authorization.js +37 -0
  80. package/dist/managed-resource-create-defaults.d.ts +21 -0
  81. package/dist/managed-resource-create-defaults.js +83 -0
  82. package/dist/managed-resource-query.d.ts +6 -0
  83. package/dist/managed-resource-query.js +15 -0
  84. package/dist/member-role-ids.d.ts +17 -0
  85. package/dist/member-role-ids.js +66 -0
  86. package/dist/message-protection-admission.d.ts +60 -0
  87. package/dist/message-protection-admission.js +279 -0
  88. package/dist/message-protection-cold-data.d.ts +42 -0
  89. package/dist/message-protection-cold-data.js +180 -0
  90. package/dist/message-protection-coordinator.d.ts +73 -0
  91. package/dist/message-protection-coordinator.js +622 -0
  92. package/dist/message-protection-settings.d.ts +40 -0
  93. package/dist/message-protection-settings.js +275 -0
  94. package/dist/message-protection-summary.d.ts +8 -0
  95. package/dist/message-protection-summary.js +98 -0
  96. package/dist/module-availability-control.js +20 -12
  97. package/dist/module-logging.d.ts +3 -2
  98. package/dist/module-logging.js +6 -1
  99. package/dist/modules.js +56 -1
  100. package/dist/mysql-runtime-persistence.d.ts +1 -0
  101. package/dist/mysql-runtime-persistence.js +4 -1
  102. package/dist/nexus-command-recovery-control-routes.d.ts +5 -0
  103. package/dist/nexus-command-recovery-control-routes.js +48 -0
  104. package/dist/nexus-command-references.d.ts +6 -0
  105. package/dist/nexus-command-references.js +33 -0
  106. package/dist/scheduled-task-runner.js +23 -4
  107. package/dist/shard-command-startup.d.ts +13 -0
  108. package/dist/shard-command-startup.js +27 -0
  109. package/dist/shard-contracts.d.ts +84 -0
  110. package/dist/shard-contracts.js +33 -0
  111. package/dist/shard-core-readiness.d.ts +5 -0
  112. package/dist/shard-core-readiness.js +19 -0
  113. package/dist/shard-router.d.ts +3 -0
  114. package/dist/shard-router.js +10 -1
  115. package/dist/shard-supervisor.d.ts +1 -1
  116. package/dist/shard-supervisor.js +131 -43
  117. package/dist/shard-worker.d.ts +1 -0
  118. package/dist/shard-worker.js +37 -15
  119. package/documentation/README.md +1 -0
  120. package/documentation/guides/self-hosting.md +54 -0
  121. package/package.json +18 -17
@@ -0,0 +1,288 @@
1
+ import { ChannelType, PermissionFlagsBits, PermissionsBitField, } from "discord.js";
2
+ const snowflake = /^\d{17,20}$/u;
3
+ const messageTypes = new Set([
4
+ ChannelType.GuildText,
5
+ ChannelType.GuildAnnouncement,
6
+ ChannelType.GuildVoice,
7
+ ChannelType.GuildStageVoice,
8
+ ChannelType.PublicThread,
9
+ ChannelType.AnnouncementThread,
10
+ ChannelType.PrivateThread,
11
+ ]);
12
+ const maximumAssignedRoles = 1024; // Engineering bound, not a Discord product limit.
13
+ /** Called only by future Core exact-event capture. Cache membership alone is not event provenance. */
14
+ export function captureCachedMessageDeletionAuthority(options) {
15
+ let stopped = false, checking = false;
16
+ try {
17
+ const { client, expectedApplicationId, eligibility, isStopped } = options;
18
+ const source = { ...options.source };
19
+ const { channel } = source;
20
+ const scope = Object.freeze({
21
+ guildId: source.guildId,
22
+ moduleId: source.moduleId,
23
+ channelId: source.channelId,
24
+ messageId: source.messageId,
25
+ admissionToken: source.admissionToken,
26
+ });
27
+ if (!snowflake.test(expectedApplicationId) ||
28
+ ![scope.guildId, scope.channelId, scope.messageId].every((id) => typeof id === "string" && snowflake.test(id)) ||
29
+ typeof scope.moduleId !== "string" ||
30
+ !/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u.test(scope.moduleId) ||
31
+ scope.moduleId.length > 128 ||
32
+ typeof scope.admissionToken !== "string" ||
33
+ !/^[A-Za-z0-9._:-]{1,256}$/u.test(scope.admissionToken) ||
34
+ typeof isStopped !== "function" ||
35
+ typeof eligibility?.read !== "function" ||
36
+ typeof eligibility.isCurrent !== "function")
37
+ return undefined;
38
+ const readEligibility = eligibility.read, tokenCurrent = eligibility.isCurrent;
39
+ const hold = () => {
40
+ stopped = true;
41
+ return false;
42
+ };
43
+ const running = () => {
44
+ if (stopped)
45
+ return false;
46
+ try {
47
+ const external = isStopped();
48
+ return !stopped && external === false ? true : hold();
49
+ }
50
+ catch {
51
+ return hold();
52
+ }
53
+ };
54
+ const eligible = () => {
55
+ if (!running())
56
+ return false;
57
+ const observed = readEligibility(scope.guildId, scope.moduleId);
58
+ if (!running() ||
59
+ observed.state !== "enabled" ||
60
+ observed.generation !== scope.admissionToken)
61
+ return hold();
62
+ const current = tokenCurrent(scope.admissionToken);
63
+ return running() && current === true ? true : hold();
64
+ };
65
+ const ready = () => {
66
+ if (!running())
67
+ return false;
68
+ const current = client.isReady();
69
+ if (!running() || current !== true)
70
+ return hold();
71
+ const user = client.user, application = client.application;
72
+ return running() &&
73
+ user?.id === expectedApplicationId &&
74
+ application?.id === expectedApplicationId
75
+ ? true
76
+ : hold();
77
+ };
78
+ if (!eligible() || !ready())
79
+ return undefined;
80
+ const guild = client.guilds.cache.get(scope.guildId);
81
+ if (!running() ||
82
+ !guild ||
83
+ guild.id !== scope.guildId ||
84
+ guild.client !== client ||
85
+ guild.available !== true ||
86
+ guild.channels.cache.get(scope.channelId) !== channel ||
87
+ channel.id !== scope.channelId ||
88
+ !supported(channel))
89
+ return undefined;
90
+ const bindings = captureBindings(guild, channel, client);
91
+ if (!running() || !bindings)
92
+ return undefined;
93
+ const authority = Object.freeze({
94
+ scope,
95
+ stop: () => {
96
+ stopped = true;
97
+ },
98
+ isCurrent: () => {
99
+ if (stopped)
100
+ return false;
101
+ if (checking)
102
+ return hold();
103
+ checking = true;
104
+ try {
105
+ if (!eligible() || !ready())
106
+ return hold();
107
+ if (client.guilds.cache.get(scope.guildId) !== guild ||
108
+ guild.id !== scope.guildId ||
109
+ guild.client !== client ||
110
+ guild.available !== true ||
111
+ !running())
112
+ return hold();
113
+ for (const binding of bindings) {
114
+ if (!running() || !sameBinding(guild, binding, client))
115
+ return hold();
116
+ }
117
+ if (!running() || !supported(channel))
118
+ return hold();
119
+ const member = guild.members.cache.get(expectedApplicationId);
120
+ if (!running() ||
121
+ !member ||
122
+ member.id !== expectedApplicationId ||
123
+ member.user.id !== expectedApplicationId ||
124
+ member.guild !== guild ||
125
+ member.client !== client ||
126
+ member.partial !== false)
127
+ return hold();
128
+ if (!running() || !completeRoles(guild, member, client, running))
129
+ return hold();
130
+ if (channel.type === ChannelType.PrivateThread) {
131
+ if (!running() || !channel.isThread())
132
+ return hold();
133
+ const joined = channel.members.cache.get(expectedApplicationId);
134
+ if (!running() ||
135
+ !joined ||
136
+ joined.id !== expectedApplicationId ||
137
+ joined.thread !== channel ||
138
+ joined.client !== client ||
139
+ joined.partial !== false ||
140
+ typeof joined.joinedTimestamp !== "number" ||
141
+ !Number.isFinite(joined.joinedTimestamp) ||
142
+ joined.joinedTimestamp < 0)
143
+ return hold();
144
+ }
145
+ if (!running())
146
+ return hold();
147
+ const permissions = channel.permissionsFor(member);
148
+ if (!running() || !(permissions instanceof PermissionsBitField))
149
+ return hold();
150
+ const allowed = permissions.has([
151
+ PermissionFlagsBits.ViewChannel,
152
+ PermissionFlagsBits.ManageMessages,
153
+ ]);
154
+ if (!running() || allowed !== true)
155
+ return hold();
156
+ const administrator = permissions.has(PermissionFlagsBits.Administrator, false);
157
+ if (!running())
158
+ return hold();
159
+ const until = member.communicationDisabledUntilTimestamp;
160
+ if (!running() ||
161
+ (until !== null &&
162
+ (typeof until !== "number" ||
163
+ !Number.isFinite(until) ||
164
+ until < 0)))
165
+ return hold();
166
+ if (!administrator && until !== null) {
167
+ const now = Date.now();
168
+ if (!running() || !Number.isFinite(now) || now < 0 || until > now)
169
+ return hold();
170
+ }
171
+ if (!eligible() || !running())
172
+ return hold();
173
+ return true;
174
+ }
175
+ catch {
176
+ return hold();
177
+ }
178
+ finally {
179
+ checking = false;
180
+ }
181
+ },
182
+ });
183
+ return authority.isCurrent() ? authority : undefined;
184
+ }
185
+ catch {
186
+ stopped = true;
187
+ return undefined;
188
+ }
189
+ }
190
+ function supported(channel) {
191
+ return (messageTypes.has(channel.type) &&
192
+ channel.partial === false &&
193
+ channel.isTextBased() &&
194
+ !channel.isDMBased());
195
+ }
196
+ function sameBinding(guild, binding, client) {
197
+ const channel = binding.channel;
198
+ return (channel.id === binding.id &&
199
+ guild.channels.cache.get(binding.id) === channel &&
200
+ channel.guild === guild &&
201
+ channel.guildId === guild.id &&
202
+ channel.client === client &&
203
+ channel.type === binding.type &&
204
+ channel.parentId === binding.parentId &&
205
+ channel.partial === false);
206
+ }
207
+ function captureBindings(guild, channel, client) {
208
+ const result = [];
209
+ const bind = (value) => {
210
+ const binding = Object.freeze({
211
+ channel: value,
212
+ id: value.id,
213
+ type: value.type,
214
+ parentId: value.parentId,
215
+ });
216
+ if (!(binding.parentId === null ||
217
+ (typeof binding.parentId === "string" &&
218
+ snowflake.test(binding.parentId))) ||
219
+ !sameBinding(guild, binding, client))
220
+ return false;
221
+ result.push(binding);
222
+ return true;
223
+ };
224
+ if (!bind(channel))
225
+ return undefined;
226
+ let categoryId = channel.parentId;
227
+ if (channel.isThread()) {
228
+ if (!categoryId)
229
+ return undefined;
230
+ const parent = guild.channels.cache.get(categoryId);
231
+ const allowed = channel.type === ChannelType.AnnouncementThread
232
+ ? [ChannelType.GuildAnnouncement]
233
+ : channel.type === ChannelType.PrivateThread
234
+ ? [ChannelType.GuildText]
235
+ : [
236
+ ChannelType.GuildText,
237
+ ChannelType.GuildForum,
238
+ ChannelType.GuildMedia,
239
+ ];
240
+ if (!parent || !allowed.includes(parent.type) || !bind(parent))
241
+ return undefined;
242
+ categoryId = parent.parentId;
243
+ }
244
+ if (categoryId) {
245
+ const category = guild.channels.cache.get(categoryId);
246
+ if (!category ||
247
+ category.type !== ChannelType.GuildCategory ||
248
+ category.parentId !== null ||
249
+ !bind(category))
250
+ return undefined;
251
+ }
252
+ return Object.freeze(result);
253
+ }
254
+ function completeRoles(guild, member, client, running) {
255
+ const ids = Reflect.get(member, "_roles");
256
+ if (!running() || !Array.isArray(ids) || ids.length > maximumAssignedRoles)
257
+ return false;
258
+ const length = ids.length;
259
+ const roleIds = new Set();
260
+ for (let index = 0; index < length; index++) {
261
+ const id = ids[index];
262
+ if (!running() ||
263
+ typeof id !== "string" ||
264
+ !snowflake.test(id) ||
265
+ roleIds.has(id))
266
+ return false;
267
+ roleIds.add(id);
268
+ }
269
+ if (ids.length !== length)
270
+ return false;
271
+ roleIds.add(guild.id);
272
+ for (const id of roleIds) {
273
+ if (!running())
274
+ return false;
275
+ const role = guild.roles.cache.get(id);
276
+ if (!running() ||
277
+ !role ||
278
+ role.id !== id ||
279
+ role.guild !== guild ||
280
+ role.client !== client ||
281
+ !(role.permissions instanceof PermissionsBitField) ||
282
+ typeof role.permissions.bitfield !== "bigint" ||
283
+ role.permissions.bitfield < 0n)
284
+ return false;
285
+ }
286
+ return true;
287
+ }
288
+ //# sourceMappingURL=cached-message-deletion-authority.js.map
@@ -3,5 +3,15 @@ import type { ChannelAuditConfiguration, ChannelAuditModuleId } from "@helyx/sdk
3
3
  export declare function channelAuditConfiguration(moduleId: ChannelAuditModuleId, configuration: {
4
4
  version: number;
5
5
  value: Record<string, unknown>;
6
- } | null): ChannelAuditConfiguration;
6
+ } | null, welcome?: {
7
+ status: "loaded";
8
+ version: number | null;
9
+ enabled: boolean;
10
+ roleId: string | null;
11
+ } | {
12
+ status: "unavailable";
13
+ version: null;
14
+ enabled: null;
15
+ roleId: null;
16
+ }): ChannelAuditConfiguration;
7
17
  //# sourceMappingURL=channel-audit-configuration.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { CHANNEL_AUDIT_LIMITS, channelAuditConfigurationSchema, } from "@helyx/control-protocol/channel-audit";
2
2
  /** Deliberate allowlist: never return provider state, answers or the saved settings object. */
3
- export function channelAuditConfiguration(moduleId, configuration) {
3
+ export function channelAuditConfiguration(moduleId, configuration, welcome) {
4
4
  const value = configuration?.value ?? {};
5
5
  const mappedFields = [];
6
6
  const id = (field) => {
@@ -13,7 +13,7 @@ export function channelAuditConfiguration(moduleId, configuration) {
13
13
  const entryChannelId = id(moduleId === "helyx.account-gate"
14
14
  ? "gateChannelId"
15
15
  : "verificationChannelId");
16
- const afterRoleId = id("verifiedRoleId");
16
+ const configuredAfterRoleId = id("verifiedRoleId");
17
17
  const reviewChannelId = moduleId === "helyx.account-gate" ? id("reviewChannelId") : null;
18
18
  const configuredRoles = moduleId === "helyx.account-gate" && Array.isArray(value.temporaryRoleIds)
19
19
  ? [
@@ -23,18 +23,40 @@ export function channelAuditConfiguration(moduleId, configuration) {
23
23
  if (configuredRoles.length)
24
24
  mappedFields.push("temporaryRoleIds");
25
25
  const suggested = moduleId === "helyx.verification" ? id("roleToRemoveId") : null;
26
- const beforeRoleIds = suggested
27
- ? [suggested]
28
- : configuredRoles.slice(0, CHANNEL_AUDIT_LIMITS.beforeRoles);
26
+ const configuredRemovedRoleIds = suggested ? [suggested] : configuredRoles;
27
+ // An existing invalid save can grant and remove the same role. Do not
28
+ // project that as a usable journey: require the owner to select the grant
29
+ // role again while preserving the configured removal evidence.
30
+ const afterRoleId = configuredRemovedRoleIds.includes(configuredAfterRoleId ?? "")
31
+ ? null
32
+ : configuredAfterRoleId;
33
+ const welcomeRoleId = welcome?.status === "loaded" && welcome.enabled ? welcome.roleId : null;
34
+ const reserveWelcomeSlot = welcomeRoleId !== null &&
35
+ !configuredRemovedRoleIds
36
+ .slice(0, CHANNEL_AUDIT_LIMITS.beforeRoles)
37
+ .includes(welcomeRoleId);
38
+ const boundedRemovedRoleIds = configuredRemovedRoleIds.slice(0, CHANNEL_AUDIT_LIMITS.beforeRoles - (reserveWelcomeSlot ? 1 : 0));
39
+ const removedRoleIds = welcomeRoleId &&
40
+ configuredRemovedRoleIds.includes(welcomeRoleId) &&
41
+ !boundedRemovedRoleIds.includes(welcomeRoleId)
42
+ ? [...boundedRemovedRoleIds, welcomeRoleId]
43
+ : boundedRemovedRoleIds;
44
+ const beforeRoleIds = [
45
+ ...new Set([...removedRoleIds, ...(welcomeRoleId ? [welcomeRoleId] : [])]),
46
+ ].slice(0, CHANNEL_AUDIT_LIMITS.beforeRoles);
29
47
  return channelAuditConfigurationSchema.parse({
30
48
  moduleId,
31
49
  version: configuration?.version ?? null,
32
50
  entryChannelId,
33
51
  beforeRoleIds,
52
+ removedRoleIds,
34
53
  afterRoleId,
35
54
  reviewChannelId,
55
+ ...(welcome ? { welcome } : {}),
36
56
  mappedFields,
37
- truncated: configuredRoles.length > CHANNEL_AUDIT_LIMITS.beforeRoles,
57
+ truncated: configuredRemovedRoleIds.length > removedRoleIds.length ||
58
+ new Set([...removedRoleIds, ...(welcomeRoleId ? [welcomeRoleId] : [])])
59
+ .size > CHANNEL_AUDIT_LIMITS.beforeRoles,
38
60
  });
39
61
  }
40
62
  //# sourceMappingURL=channel-audit-configuration.js.map
@@ -25,17 +25,57 @@ export function createChannelAuditControl(input) {
25
25
  if (!installed.some((item) => item.moduleId === context.moduleId))
26
26
  throw new BotControlNotFoundError("This module is not installed for this server");
27
27
  await input.requireAccessible(context.guildId, context.moduleId);
28
+ return { actor, guildAccess, installed };
29
+ };
30
+ const unavailableWelcome = () => ({
31
+ status: "unavailable",
32
+ version: null,
33
+ enabled: null,
34
+ roleId: null,
35
+ });
36
+ const readWelcome = async (context, authority) => {
37
+ const moduleId = "helyx.welcome";
38
+ const installation = authority.installed.find((item) => item.moduleId === moduleId);
39
+ const module = input.modules.get(moduleId);
40
+ if (!installation || !module?.manifest.dashboard)
41
+ return unavailableWelcome();
42
+ try {
43
+ const resolvedPolicy = await input.permissions.getPolicy(context.guildId, moduleId);
44
+ if (!canAccessGuildDashboard(authority.actor, authority.guildAccess) ||
45
+ !canUseCommand(authority.actor, resolvedPolicy.policy, authority.guildAccess))
46
+ return unavailableWelcome();
47
+ await input.requireAccessible(context.guildId, moduleId);
48
+ }
49
+ catch {
50
+ return unavailableWelcome();
51
+ }
52
+ try {
53
+ const configuration = await input.configurations.get(context.guildId, moduleId);
54
+ const candidate = configuration?.value.roleId;
55
+ return {
56
+ status: "loaded",
57
+ version: configuration ? safeVersion(configuration.version) : null,
58
+ enabled: installation.enabled,
59
+ roleId: typeof candidate === "string" && /^\d{17,20}$/u.test(candidate)
60
+ ? candidate
61
+ : null,
62
+ };
63
+ }
64
+ catch {
65
+ return unavailableWelcome();
66
+ }
28
67
  };
29
68
  return {
30
69
  readChannelAuditConfiguration: async (context) => {
31
- await authorize(context);
70
+ const authority = await authorize(context);
32
71
  const configuration = await input.configurations.get(context.guildId, context.moduleId);
72
+ const welcome = await readWelcome(context, authority);
33
73
  return channelAuditConfiguration(context.moduleId, configuration
34
74
  ? {
35
75
  version: safeVersion(configuration.version),
36
76
  value: configuration.value,
37
77
  }
38
- : null);
78
+ : null, welcome);
39
79
  },
40
80
  checkChannelAudit: async (context) => {
41
81
  const assumptions = channelAuditInputSchema.parse(context.assumptions);
@@ -93,6 +93,9 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
93
93
  const channels = [];
94
94
  const add = (finding) => findings.push(finding);
95
95
  const roles = new Map(snapshot.roles.map((role) => [role.id, role]));
96
+ const removedRoleIds = assumptions.removedRoleIds ?? assumptions.beforeRoleIds;
97
+ const retainedBeforeRoleIds = assumptions.beforeRoleIds.filter((roleId) => !removedRoleIds.includes(roleId));
98
+ const afterRoleIds = [assumptions.afterRoleId, ...retainedBeforeRoleIds];
96
99
  const requiredRoles = new Set([
97
100
  ...assumptions.beforeRoleIds,
98
101
  assumptions.afterRoleId,
@@ -124,7 +127,7 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
124
127
  detail: afterRole.assignmentPolicyReason,
125
128
  roleId: afterRole.id,
126
129
  });
127
- for (const roleId of assumptions.beforeRoleIds) {
130
+ for (const roleId of removedRoleIds) {
128
131
  const beforeRole = roles.get(roleId);
129
132
  if (beforeRole?.assignmentPolicyReason)
130
133
  add({
@@ -198,6 +201,7 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
198
201
  const selectedIds = new Set([
199
202
  assumptions.entryChannelId,
200
203
  ...assumptions.allowedChannelIds,
204
+ ...(assumptions.requiredAfterChannelIds ?? []),
201
205
  ...(assumptions.reviewChannelId ? [assumptions.reviewChannelId] : []),
202
206
  ]);
203
207
  const ordered = [...snapshot.channels].sort((left, right) => Number(selectedIds.has(right.id)) - Number(selectedIds.has(left.id)) ||
@@ -220,7 +224,7 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
220
224
  }
221
225
  const before = effectiveAuditPermissions(snapshot.guildId, snapshot.roles, assumptions.beforeRoleIds, channel);
222
226
  const everyone = effectiveAuditPermissions(snapshot.guildId, snapshot.roles, [], channel);
223
- const after = effectiveAuditPermissions(snapshot.guildId, snapshot.roles, [assumptions.afterRoleId], channel);
227
+ const after = effectiveAuditPermissions(snapshot.guildId, snapshot.roles, afterRoleIds, channel);
224
228
  const retained = effectiveAuditPermissions(snapshot.guildId, snapshot.roles, [...assumptions.beforeRoleIds, assumptions.afterRoleId], channel);
225
229
  const row = {
226
230
  id: channel.id,
@@ -291,6 +295,47 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
291
295
  channelId: channel.id,
292
296
  });
293
297
  }
298
+ if (assumptions.entryAfter && assumptions.entryAfter !== "either") {
299
+ if (after === null)
300
+ add({
301
+ code: "entry_after_unknown",
302
+ category: "unknown",
303
+ title: "Could not verify entrance visibility after the check",
304
+ detail: "One or more roles needed for the after-check view are unavailable. This is not evidence that the expected transition works.",
305
+ channelId: channel.id,
306
+ });
307
+ else {
308
+ const visible = has(after, AUDIT_PERMISSIONS.ViewChannel);
309
+ const expectedVisible = assumptions.entryAfter === "visible";
310
+ add(visible === expectedVisible
311
+ ? {
312
+ code: expectedVisible
313
+ ? "entry_after_visible"
314
+ : "entry_after_hidden",
315
+ category: "working",
316
+ title: expectedVisible
317
+ ? "Entrance remains visible after the check"
318
+ : "Entrance is hidden after the check",
319
+ detail: expectedVisible
320
+ ? "The granted role plus every before role not selected for removal can view the entrance."
321
+ : "The granted role plus every before role not selected for removal cannot view the entrance.",
322
+ channelId: channel.id,
323
+ }
324
+ : {
325
+ code: expectedVisible
326
+ ? "entry_after_not_visible"
327
+ : "entry_after_still_visible",
328
+ category: "blocked",
329
+ title: expectedVisible
330
+ ? "Entrance is not visible after the check"
331
+ : "Entrance remains visible after the check",
332
+ detail: expectedVisible
333
+ ? "The configured after-check role set cannot view the entrance."
334
+ : "The granted role or a retained before role keeps the entrance visible after the transition.",
335
+ channelId: channel.id,
336
+ });
337
+ }
338
+ }
294
339
  const publishing = AUDIT_PERMISSIONS.ViewChannel | AUDIT_PERMISSIONS.SendMessages;
295
340
  if (channel.botPermissions === null)
296
341
  add({
@@ -321,6 +366,32 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
321
366
  detail: "Review this explicitly selected staff-review destination separately from entrance visibility.",
322
367
  channelId: channel.id,
323
368
  });
369
+ if ((assumptions.requiredAfterChannelIds ?? []).includes(channel.id)) {
370
+ if (after === null)
371
+ add({
372
+ code: "required_after_channel_unknown",
373
+ category: "unknown",
374
+ title: "Required after-check channel could not be verified",
375
+ detail: "One or more roles needed for the after-check view are unavailable. This required channel is not counted as passing.",
376
+ channelId: channel.id,
377
+ });
378
+ else if (has(after, AUDIT_PERMISSIONS.ViewChannel))
379
+ add({
380
+ code: "required_after_channel_visible",
381
+ category: "working",
382
+ title: "Required after-check channel is visible",
383
+ detail: "The granted role plus every before role not selected for removal can view this required channel.",
384
+ channelId: channel.id,
385
+ });
386
+ else
387
+ add({
388
+ code: "required_after_channel_hidden",
389
+ category: "blocked",
390
+ title: "Required after-check channel is not visible",
391
+ detail: "The configured after-check role set cannot view this required channel.",
392
+ channelId: channel.id,
393
+ });
394
+ }
324
395
  }
325
396
  for (const channelId of selectedIds) {
326
397
  const invalid = snapshot.invalidSelectedChannels?.find((item) => item.id === channelId);
@@ -373,7 +444,9 @@ export function buildChannelAuditReport(snapshot, moduleId, assumptions, observe
373
444
  moduleId === "helyx.account-gate"
374
445
  ? "Access Control grants the access role, then attempts removal of its configured temporary roles. Selected before roles are assumptions, and delivery can be partial."
375
446
  : "Verification grants the verified role and removes its configured removal role when present. It does not assign the before roles, and other member roles can remain.",
376
- "After-check view assumes selected before roles were removed; the retained view shows their combination with the after role. Actual transitions can differ or partially complete.",
447
+ assumptions.removedRoleIds
448
+ ? "After-check view grants the selected after role, removes only the explicitly selected removal roles, and retains every other before role. Actual transitions can differ or partially complete."
449
+ : "Legacy after-check view assumes every selected before role was removed; the retained view shows their combination with the after role. Actual transitions can differ or partially complete.",
377
450
  "The retained view includes all selected before roles; it does not enumerate every partially retained role combination.",
378
451
  ...snapshot.fetchLimitations,
379
452
  ];
@@ -0,0 +1,26 @@
1
+ import { type CommandPolicyDeliverySession, type CommandPolicyOwnerToken, type CommandPolicyPull } from "@helyx/database-contracts";
2
+ import type { CommandPolicyDeliveryService } from "@helyx/platform";
3
+ /** One authenticated socket, one bounded transfer, no customer policy authority. */
4
+ export declare class CloudCommandPolicySynchronizer {
5
+ #private;
6
+ private readonly input;
7
+ constructor(input: {
8
+ applicationId: string;
9
+ owner: CommandPolicyOwnerToken;
10
+ session: CommandPolicyDeliverySession;
11
+ delivery: CommandPolicyDeliveryService;
12
+ send(correlationId: string, pull: CommandPolicyPull): void;
13
+ isOwnerCurrent?(): Promise<boolean>;
14
+ onHeld(): void;
15
+ intervalMs?: number;
16
+ timeoutMs?: number;
17
+ });
18
+ start(): Promise<void>;
19
+ runOnce(): Promise<void>;
20
+ /** Caller must first pass the current accepted envelope's sequence/epoch guard. */
21
+ accept(correlationId: string, payload: unknown, expiresAt: string): void;
22
+ /** Stop local activity before an atomic receipt-first identity transition. */
23
+ cancel(): void;
24
+ stop(): Promise<void>;
25
+ }
26
+ //# sourceMappingURL=cloud-command-policy-sync.d.ts.map