@helyx/bot 1.3.1 → 1.4.0

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 (83) hide show
  1. package/CHANGELOG.md +77 -28
  2. package/README.md +65 -11
  3. package/dist/cloud-connector.d.ts +11 -4
  4. package/dist/cloud-connector.js +52 -15
  5. package/dist/cloud-control-dispatcher.d.ts +8 -3
  6. package/dist/cloud-control-dispatcher.js +62 -4
  7. package/dist/control-errors.d.ts +4 -0
  8. package/dist/control-errors.js +8 -0
  9. package/dist/control-server-schemas.d.ts +84 -0
  10. package/dist/control-server-schemas.js +101 -0
  11. package/dist/control-server.d.ts +21 -7
  12. package/dist/control-server.js +88 -132
  13. package/dist/core-retention.d.ts +26 -0
  14. package/dist/core-retention.js +49 -0
  15. package/dist/creator-guild-join-service.d.ts +25 -0
  16. package/dist/creator-guild-join-service.js +218 -0
  17. package/dist/creator-invite-service.d.ts +37 -0
  18. package/dist/creator-invite-service.js +686 -0
  19. package/dist/creator-onboarding-control.d.ts +12 -0
  20. package/dist/creator-onboarding-control.js +65 -0
  21. package/dist/creator-onboarding-platform.d.ts +20 -0
  22. package/dist/creator-onboarding-platform.js +58 -0
  23. package/dist/dashboard-action-control.d.ts +2 -2
  24. package/dist/dashboard-action-control.js +2 -3
  25. package/dist/database-bootstrap.d.ts +36 -0
  26. package/dist/database-bootstrap.js +101 -0
  27. package/dist/developer-operations-module.d.ts +1 -0
  28. package/dist/developer-operations-module.js +34 -16
  29. package/dist/discord-owned-message-service.d.ts +1 -0
  30. package/dist/discord-owned-message-service.js +3 -0
  31. package/dist/guild-command-read-service.d.ts +8 -3
  32. package/dist/guild-command-read-service.js +14 -7
  33. package/dist/guild-command-release.d.ts +15 -8
  34. package/dist/guild-command-release.js +10 -3
  35. package/dist/guild-command-worker.d.ts +17 -4
  36. package/dist/guild-command-worker.js +17 -6
  37. package/dist/guilds.d.ts +24 -3
  38. package/dist/guilds.js +185 -64
  39. package/dist/hosted-server-moderation.d.ts +2 -2
  40. package/dist/hosted-server-moderation.js +10 -4
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +200 -82
  43. package/dist/interactions.js +16 -3
  44. package/dist/module-configuration.d.ts +1 -1
  45. package/dist/module-configuration.js +5 -2
  46. package/dist/module-logging.d.ts +8 -3
  47. package/dist/module-logging.js +13 -7
  48. package/dist/modules.d.ts +1 -0
  49. package/dist/modules.js +8 -1
  50. package/dist/mysql-runtime-persistence.d.ts +46 -0
  51. package/dist/mysql-runtime-persistence.js +91 -0
  52. package/dist/oauth-module.js +2 -2
  53. package/dist/oauth-service.d.ts +7 -0
  54. package/dist/oauth-service.js +3 -0
  55. package/dist/scheduled-task-runner.d.ts +9 -85
  56. package/dist/scheduled-task-runner.js +1 -1
  57. package/dist/shard-contracts.d.ts +135 -0
  58. package/dist/shard-contracts.js +13 -1
  59. package/dist/shard-router.d.ts +2 -1
  60. package/dist/shard-router.js +7 -0
  61. package/dist/shard-supervisor.js +43 -9
  62. package/dist/shard-worker.d.ts +5 -2
  63. package/dist/shard-worker.js +41 -5
  64. package/dist/supervisor-cloud-directory.d.ts +9 -3
  65. package/dist/supervisor-cloud-directory.js +8 -12
  66. package/dist/trivia-data-provider.d.ts +11 -0
  67. package/dist/trivia-data-provider.js +46 -26
  68. package/dist/verification-service.d.ts +2 -2
  69. package/dist/verification-service.js +6 -3
  70. package/documentation/README.md +5 -3
  71. package/documentation/examples/.env.example +15 -11
  72. package/documentation/guides/getting-started.md +29 -12
  73. package/documentation/guides/privacy-and-support.md +6 -5
  74. package/documentation/guides/self-hosting.md +16 -11
  75. package/documentation/modules/auto-moderation.md +7 -5
  76. package/documentation/modules/building-a-module.md +48 -2
  77. package/documentation/modules/moderation.md +5 -5
  78. package/documentation/modules/publishing-a-module.md +6 -4
  79. package/documentation/modules/testing-a-module.md +39 -4
  80. package/documentation/reference/environment.md +43 -9
  81. package/documentation/runbooks/backup-and-recovery.md +26 -7
  82. package/documentation/runbooks/installation-and-upgrades.md +97 -27
  83. package/package.json +20 -16
@@ -0,0 +1,218 @@
1
+ import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
+ import { DiscordAPIError, REST, RESTJSONErrorCodes, Routes, } from "discord.js";
3
+ import { discordRestOptions } from "./discord-rest.js";
4
+ import { canAccessModule } from "./premium-access.js";
5
+ const snowflake = /^\d{17,20}$/u;
6
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
7
+ /** The only onboarding boundary which can add a member. Never supplies roles or other admission overrides. */
8
+ export class DiscordCreatorGuildJoinService {
9
+ client;
10
+ services;
11
+ modules;
12
+ repository;
13
+ credentials;
14
+ #rest;
15
+ constructor(client, services, modules, repository, credentials) {
16
+ this.client = client;
17
+ this.services = services;
18
+ this.modules = modules;
19
+ this.repository = repository;
20
+ this.credentials = credentials;
21
+ }
22
+ async inspect(input) {
23
+ if (!snowflake.test(input.guildId) ||
24
+ (input.memberUserId !== undefined && !snowflake.test(input.memberUserId)))
25
+ return { joining: "unavailable", memberReadiness: "unavailable" };
26
+ const guild = this.client.isReady()
27
+ ? this.client.guilds.cache.get(input.guildId)
28
+ : undefined;
29
+ if (!guild?.available)
30
+ return { joining: "unavailable", memberReadiness: "unavailable" };
31
+ try {
32
+ if (!(await this.enabled(guild.id)))
33
+ return { joining: "unavailable", memberReadiness: "unavailable" };
34
+ await guild.fetch();
35
+ const paused = guild.features.includes("INVITES_DISABLED") ||
36
+ (guild.incidentsData?.invitesDisabledUntil?.getTime() ?? 0) >
37
+ Date.now();
38
+ return {
39
+ joining: paused ? "paused" : "open",
40
+ memberReadiness: input.memberUserId
41
+ ? await this.readiness(guild, input.memberUserId)
42
+ : "member_missing",
43
+ };
44
+ }
45
+ catch {
46
+ return { joining: "unavailable", memberReadiness: "unavailable" };
47
+ }
48
+ }
49
+ async join(input) {
50
+ if (!snowflake.test(input.guildId) ||
51
+ !snowflake.test(input.memberUserId) ||
52
+ !uuid.test(input.sessionId) ||
53
+ !/^[A-Za-z0-9_-]{43}$/u.test(input.authorisationReference))
54
+ return {
55
+ outcome: "rejected",
56
+ memberReadiness: "unavailable",
57
+ safeCode: "invalid_authorisation",
58
+ };
59
+ return this.repository.runGuildExclusive(input.guildId, async () => {
60
+ const before = await this.inspect(input);
61
+ if (before.joining !== "open")
62
+ return {
63
+ outcome: "rejected",
64
+ memberReadiness: before.memberReadiness,
65
+ safeCode: "joining_unavailable",
66
+ };
67
+ if (before.memberReadiness !== "member_missing" &&
68
+ before.memberReadiness !== "unavailable")
69
+ return {
70
+ outcome: "already_member",
71
+ memberReadiness: before.memberReadiness,
72
+ };
73
+ if (before.memberReadiness === "unavailable")
74
+ return { outcome: "uncertain", memberReadiness: "unavailable" };
75
+ let credential;
76
+ try {
77
+ credential = await this.credentials(input);
78
+ }
79
+ catch {
80
+ // No Discord write has happened, so this is a definite no-join result.
81
+ return {
82
+ outcome: "rejected",
83
+ memberReadiness: "member_missing",
84
+ safeCode: "authorisation_unavailable",
85
+ };
86
+ }
87
+ if (!credential.accessToken ||
88
+ credential.applicationId !== this.client.user?.id ||
89
+ !this.client.token)
90
+ return {
91
+ outcome: "rejected",
92
+ memberReadiness: "member_missing",
93
+ safeCode: "application_mismatch",
94
+ };
95
+ // The broker call crosses a network boundary. Check admission again just
96
+ // before the only write; never replay this PUT after an uncertain result.
97
+ const current = await this.inspect(input);
98
+ if (current.joining !== "open")
99
+ return {
100
+ outcome: "rejected",
101
+ memberReadiness: current.memberReadiness,
102
+ safeCode: "joining_unavailable",
103
+ };
104
+ if (current.memberReadiness !== "member_missing")
105
+ return current.memberReadiness === "unavailable"
106
+ ? { outcome: "uncertain", memberReadiness: "unavailable" }
107
+ : {
108
+ outcome: "already_member",
109
+ memberReadiness: current.memberReadiness,
110
+ };
111
+ this.#rest ??= new REST({
112
+ ...discordRestOptions,
113
+ retries: 0,
114
+ timeout: 15_000,
115
+ }).setToken(this.client.token);
116
+ try {
117
+ await this.#rest.put(Routes.guildMember(input.guildId, input.memberUserId), { body: { access_token: credential.accessToken } });
118
+ }
119
+ catch (error) {
120
+ const after = await this.inspect(input);
121
+ if (!["member_missing", "unavailable"].includes(after.memberReadiness)) {
122
+ await this.logJoined(input);
123
+ return { outcome: "joined", memberReadiness: after.memberReadiness };
124
+ }
125
+ return {
126
+ outcome: error instanceof DiscordAPIError &&
127
+ error.status >= 400 &&
128
+ error.status < 500 &&
129
+ error.status !== 429
130
+ ? "rejected"
131
+ : "uncertain",
132
+ memberReadiness: after.memberReadiness,
133
+ safeCode: "join_not_confirmed",
134
+ };
135
+ }
136
+ const after = await this.inspect(input);
137
+ if (!["member_missing", "unavailable"].includes(after.memberReadiness))
138
+ await this.logJoined(input);
139
+ return {
140
+ outcome: ["member_missing", "unavailable"].includes(after.memberReadiness)
141
+ ? "uncertain"
142
+ : "joined",
143
+ memberReadiness: after.memberReadiness,
144
+ };
145
+ });
146
+ }
147
+ async logJoined(input) {
148
+ // Joining is core-owned; emit the module-declared event only after a new
149
+ // admission is confirmed. Logging must never change the admission outcome.
150
+ try {
151
+ if (!this.services.has(HELYX_SERVICE_NAMES.moduleLogging))
152
+ return;
153
+ await this.services
154
+ .get(HELYX_SERVICE_NAMES.moduleLogging)
155
+ .emit({
156
+ guildId: input.guildId,
157
+ moduleId: "helyx.creator-connections",
158
+ eventId: "integrated-invite-joined",
159
+ summary: "Member joined through integrated invite",
160
+ details: [{ label: "Member", value: input.memberUserId }],
161
+ idempotencyKey: `cc-integrated-join:${input.sessionId}`,
162
+ });
163
+ }
164
+ catch {
165
+ // Optional log delivery cannot turn a successful join into an error.
166
+ }
167
+ }
168
+ async enabled(guildId) {
169
+ const module = this.modules.find((candidate) => candidate.manifest.id === "helyx.creator-connections");
170
+ return Boolean(module &&
171
+ (await this.services
172
+ .get(HELYX_SERVICE_NAMES.installations)
173
+ .isModuleEnabled(guildId, module.manifest.id)) &&
174
+ (await this.services
175
+ .get(HELYX_SERVICE_NAMES.moduleAvailability)
176
+ .isAvailable(module.manifest.id)) &&
177
+ (await canAccessModule(this.services, guildId, module)));
178
+ }
179
+ async readiness(guild, memberUserId) {
180
+ try {
181
+ const member = await guild.members.fetch({
182
+ user: memberUserId,
183
+ force: true,
184
+ });
185
+ if (member.pending)
186
+ return "screening_pending";
187
+ for (const module of this.modules) {
188
+ if (!module.admissionGate ||
189
+ !(await this.services
190
+ .get(HELYX_SERVICE_NAMES.installations)
191
+ .isModuleEnabled(guild.id, module.manifest.id)))
192
+ continue;
193
+ if (!(await this.services
194
+ .get(HELYX_SERVICE_NAMES.moduleAvailability)
195
+ .isAvailable(module.manifest.id)))
196
+ return "unavailable";
197
+ const gate = await module.admissionGate({
198
+ guildId: guild.id,
199
+ memberUserId,
200
+ roleIds: [...member.roles.cache.keys()],
201
+ services: this.services,
202
+ });
203
+ if (gate === "unavailable")
204
+ return "unavailable";
205
+ if (gate !== "ready")
206
+ return "access_pending";
207
+ }
208
+ return "ready";
209
+ }
210
+ catch (error) {
211
+ return error instanceof DiscordAPIError &&
212
+ error.code === RESTJSONErrorCodes.UnknownMember
213
+ ? "member_missing"
214
+ : "unavailable";
215
+ }
216
+ }
217
+ }
218
+ //# sourceMappingURL=creator-guild-join-service.js.map
@@ -0,0 +1,37 @@
1
+ import type { CreatorInviteRepository } from "@helyx/database";
2
+ import { type CreatorInviteService, type CreatorOwnedInvite, type ModuleDefinition, type ServiceAccess } from "@helyx/sdk";
3
+ import { type Client } from "discord.js";
4
+ /** Hosted, guild-owning core adapter. Modules never receive its encryption key/store. */
5
+ export declare class DiscordCreatorInviteService implements CreatorInviteService {
6
+ #private;
7
+ private readonly client;
8
+ private readonly repository;
9
+ private readonly services;
10
+ private readonly module;
11
+ constructor(client: Client, repository: CreatorInviteRepository, services: ServiceAccess, module: ModuleDefinition | undefined, encodedEncryptionKey: string);
12
+ selectableRoles(input: Parameters<CreatorInviteService["selectableRoles"]>[0]): ReturnType<CreatorInviteService["selectableRoles"]>;
13
+ create(input: Parameters<CreatorInviteService["create"]>[0]): Promise<CreatorOwnedInvite>;
14
+ inspect(input: Parameters<CreatorInviteService["inspect"]>[0]): Promise<CreatorOwnedInvite>;
15
+ shareUrl(input: Parameters<CreatorInviteService["shareUrl"]>[0]): Promise<{
16
+ url: string;
17
+ }>;
18
+ publicOverview(input: {
19
+ guildId: string;
20
+ reference: string;
21
+ }): Promise<{
22
+ guildName: string;
23
+ guildIconUrl: string | null;
24
+ discordInviteUrl: string;
25
+ }>;
26
+ revoke(input: Parameters<CreatorInviteService["revoke"]>[0]): ReturnType<CreatorInviteService["revoke"]>;
27
+ /** Cancels a durable intent without issuing a create POST, including rejected intents. */
28
+ cancelOperation(input: {
29
+ guildId: string;
30
+ operationId: string;
31
+ }): Promise<{
32
+ state: "revoked" | "revocation_pending" | "unknown";
33
+ }>;
34
+ /** Runs outside module enablement; only proven access/safety loss initiates revocation. */
35
+ cleanupUnavailableInvites(): Promise<void>;
36
+ }
37
+ //# sourceMappingURL=creator-invite-service.d.ts.map