@helyx/bot 0.4.42 → 0.4.43
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 +18 -0
- package/README.md +7 -0
- package/dist/control-server.js +4 -0
- package/dist/events.d.ts +2 -1
- package/dist/events.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +85 -72
- package/dist/interactions.d.ts +1 -0
- package/dist/interactions.js +40 -4
- package/dist/oauth-service.d.ts +3 -2
- package/dist/oauth-service.js +1 -1
- package/dist/shard-supervisor.js +1 -1
- package/dist/status-producer.d.ts +13 -0
- package/dist/status-producer.js +34 -0
- package/documentation/guides/self-hosting.md +18 -0
- package/documentation/modules/building-a-module.md +36 -0
- package/documentation/modules/testing-a-module.md +18 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.43
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Generalise the hosted Creator OAuth boundary with typed provider registrations,
|
|
8
|
+
neutral versioned consumer context, bounded Creator account slots and scheduled
|
|
9
|
+
cross-database orphan reconciliation without exposing credential material.
|
|
10
|
+
- Complete the Nexus redemption experience with contextual custom-request modals,
|
|
11
|
+
a dedicated protected redemption report including stored unit price, and
|
|
12
|
+
structured Achievement balance amount and currency projections.
|
|
13
|
+
- Report bounded aggregate bot readiness and keep status heartbeat preparation and delivery fail-open under timeouts and failures.
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- Updated dependencies
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @helyx/sdk@0.9.0
|
|
18
|
+
- @helyx/database@0.9.0
|
|
19
|
+
- @helyx/module-manifest@0.8.0
|
|
20
|
+
|
|
3
21
|
## 0.4.42
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -38,6 +38,13 @@ When `HELYX_BOT_CONTROL_SECRET` is configured, the bot also starts a private aut
|
|
|
38
38
|
|
|
39
39
|
Optional Helyx Cloud Connect is a separate outbound management path for self-hosted deployments. It is disabled unless `HELYX_CLOUD_CONNECT_ENABLED=true` and requires a separate local `HELYX_DATA_ENCRYPTION_KEY`. Preserve that key across every restart and deployment. If it is lost or changed, revoke the old installation in the dashboard and use a new one-use code with `/helyx-cloud connect`; the connector stops retrying until that recovery is completed. The owner-only `/helyx-cloud` command supports connection-code enrolment, status, credential rotation, disconnection and bounded support consent. The bot never sends its Discord token, database URL or connector private key to Helyx Cloud, and local Discord operation continues when Cloud is unavailable.
|
|
40
40
|
|
|
41
|
+
Production Status reporting is opt-in and disabled by default. Set
|
|
42
|
+
`HELYX_STATUS_ENABLED=true` only with the reviewed `HELYX_STATUS_URL`, the
|
|
43
|
+
bot-specific `HELYX_STATUS_HEARTBEAT_SECRET`,
|
|
44
|
+
`HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS`, and a bot-specific
|
|
45
|
+
`CF_ACCESS_CLIENT_ID`/`CF_ACCESS_CLIENT_SECRET` pair. Never reuse another
|
|
46
|
+
producer's Access identity or heartbeat secret.
|
|
47
|
+
|
|
41
48
|
Giveaways also requires the existing `HELYX_DATA_ENCRYPTION_KEY` so core can
|
|
42
49
|
resume secure weighted-selection checkpoints without exposing the selection
|
|
43
50
|
key to a module. Keep it stable across deployments; do not rotate it while a
|
package/dist/control-server.js
CHANGED
|
@@ -628,6 +628,10 @@ export class BotControlService {
|
|
|
628
628
|
});
|
|
629
629
|
}
|
|
630
630
|
async completeFirstTimeCommandSetup(input) {
|
|
631
|
+
for (const moduleIdValue of new Set(input.moduleIds)) {
|
|
632
|
+
this.#requireDashboardModule(moduleIdValue);
|
|
633
|
+
await this.#requireModuleAvailable(moduleIdValue);
|
|
634
|
+
}
|
|
631
635
|
return completeFirstTimeCommandSetup({
|
|
632
636
|
installations: this.#installations,
|
|
633
637
|
dashboardModuleIds: this.#dashboardModules.map(({ id }) => id),
|
package/dist/events.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { DiscordChannelAudienceInspection, DiscordChannelInspection, DiscordEmojiInspection, DiscordMessageInspection, DiscordGuildCapabilityInspection, DiscordGuildChannelAudienceInspection, DiscordResourceInspectionService, DiscordRoleInspection, DiscordMessageService, MemberEnforcementService, MemberRoleService, ModuleDefinition, ServiceAccess, VerificationMemberService } from "@helyx/sdk";
|
|
1
|
+
import type { DiscordChannelAudienceInspection, DiscordChannelInspection, DiscordEmojiInspection, DiscordMessageInspection, DiscordGuildCapabilityInspection, DiscordGuildChannelAudienceInspection, DiscordResourceInspectionService, DiscordRoleInspection, DiscordMessageService, InstallationService, MemberEnforcementService, MemberRoleService, ModuleDefinition, ServiceAccess, VerificationMemberService } from "@helyx/sdk";
|
|
2
2
|
import { type Client, type MessageReaction, type PartialMessageReaction } from "discord.js";
|
|
3
3
|
import { type InteractionLogger } from "./interactions.js";
|
|
4
|
+
export declare function availabilityAwareInstallations(services: ServiceAccess): Pick<InstallationService, "isModuleEnabled">;
|
|
4
5
|
export declare function installModuleEvents(client: Client, modules: readonly ModuleDefinition[], logger: InteractionLogger, services: ServiceAccess): ModuleEventHandle;
|
|
5
6
|
export interface ModuleEventHandle {
|
|
6
7
|
stop(): Promise<void>;
|
package/dist/events.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ChannelType, Events, GatewayIntentBits, GuildMemberFlags, PermissionFla
|
|
|
3
3
|
import { toReplyOptions } from "./interactions.js";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { classifyGuildMessage, unavailableGuildMessageClassification, } from "./message-classification.js";
|
|
6
|
-
function availabilityAwareInstallations(services) {
|
|
6
|
+
export function availabilityAwareInstallations(services) {
|
|
7
7
|
const installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
8
8
|
const candidate = typeof services.has === "function" &&
|
|
9
9
|
services.has(HELYX_SERVICE_NAMES.moduleAvailability)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type ModuleDefinition } from "@helyx/sdk";
|
|
2
2
|
import { GatewayIntentBits } from "discord.js";
|
|
3
|
+
import { type ShardHealthSnapshot } from "./shard-contracts.js";
|
|
3
4
|
export { loadConfiguredModulePackages } from "./modules.js";
|
|
4
5
|
export { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
5
6
|
export * from "./control-server.js";
|
|
6
7
|
export interface BotHandle {
|
|
7
8
|
shutdown(reason?: string): Promise<void>;
|
|
9
|
+
statusHealth?(): Promise<ShardHealthSnapshot>;
|
|
8
10
|
}
|
|
9
11
|
export declare function startBot(processEnvironment?: NodeJS.ProcessEnv): Promise<BotHandle>;
|
|
10
12
|
export declare function resolveBotWorkerEntryPoint(entryUrl?: string): string;
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import { HttpVerificationService } from "./verification-service.js";
|
|
|
26
26
|
import { startShardSupervisor } from "./shard-supervisor.js";
|
|
27
27
|
import { parseShardWorkerLaunchContract, startShardWorker, } from "./shard-worker.js";
|
|
28
28
|
import { createDeveloperOperationsModule } from "./developer-operations-module.js";
|
|
29
|
-
import { shardHealthSnapshotSchema } from "./shard-contracts.js";
|
|
29
|
+
import { shardHealthSnapshotSchema, } from "./shard-contracts.js";
|
|
30
30
|
import { createSupportModule } from "./support-module.js";
|
|
31
31
|
import { createOAuthModule } from "./oauth-module.js";
|
|
32
32
|
import { createPrivacyModule } from "./privacy-module.js";
|
|
@@ -42,12 +42,14 @@ import { PostgresTriviaDataProvider } from "./trivia-data-provider.js";
|
|
|
42
42
|
import { createLocalScheduledTaskRoute, createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
|
|
43
43
|
import { createLocalNumericWriteBehind } from "./numeric-write-behind.js";
|
|
44
44
|
import { discordRestOptions } from "./discord-rest.js";
|
|
45
|
+
import { discordBotStatusService } from "./status-producer.js";
|
|
45
46
|
export { loadConfiguredModulePackages } from "./modules.js";
|
|
46
47
|
export { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
47
48
|
export * from "./control-server.js";
|
|
48
49
|
const bootstrapLogger = pino({ base: { component: "bot" } });
|
|
49
50
|
const RETENTION_CLEANUP_INTERVAL_MS = 60 * 60 * 1_000;
|
|
50
51
|
export async function startBot(processEnvironment = process.env) {
|
|
52
|
+
const processStartedAt = new Date().toISOString();
|
|
51
53
|
if (processEnvironment.HELYX_SHARD_WORKER_CONTRACT !== undefined)
|
|
52
54
|
return startShardWorker(processEnvironment, parseShardWorkerLaunchContract(processEnvironment.HELYX_SHARD_WORKER_CONTRACT));
|
|
53
55
|
const environment = parseBotEnvironment(processEnvironment);
|
|
@@ -57,7 +59,6 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
57
59
|
: await startUnshardedBot(processEnvironment);
|
|
58
60
|
if (!environment.HELYX_STATUS_ENABLED)
|
|
59
61
|
return handle;
|
|
60
|
-
const processStartedAt = new Date().toISOString();
|
|
61
62
|
const reporter = new StatusHeartbeatReporter({
|
|
62
63
|
producer: "bot",
|
|
63
64
|
baseUrl: environment.HELYX_STATUS_URL,
|
|
@@ -65,21 +66,32 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
65
66
|
accessClientId: environment.CF_ACCESS_CLIENT_ID,
|
|
66
67
|
accessClientSecret: environment.CF_ACCESS_CLIENT_SECRET,
|
|
67
68
|
intervalSeconds: environment.HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS,
|
|
68
|
-
services: () =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
69
|
+
services: async () => {
|
|
70
|
+
const release = readBotVersion();
|
|
71
|
+
const health = await handle.statusHealth?.();
|
|
72
|
+
return [
|
|
73
|
+
health
|
|
74
|
+
? discordBotStatusService({ health, processStartedAt, release })
|
|
75
|
+
: {
|
|
76
|
+
key: "discord-bot",
|
|
77
|
+
state: "major_outage",
|
|
78
|
+
reasonCode: "runtime_not_ready",
|
|
79
|
+
processStartedAt,
|
|
80
|
+
release,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
key: "creator-connections",
|
|
84
|
+
state: environment.HELYX_OAUTH_ENABLED
|
|
85
|
+
? "operational"
|
|
86
|
+
: "major_outage",
|
|
87
|
+
reasonCode: environment.HELYX_OAUTH_ENABLED
|
|
88
|
+
? null
|
|
89
|
+
: "feature_disabled",
|
|
90
|
+
processStartedAt,
|
|
91
|
+
release,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
},
|
|
83
95
|
onError(error) {
|
|
84
96
|
bootstrapLogger.warn({ event: "status.heartbeat_failed", reason: error.message }, "Status heartbeat could not be delivered");
|
|
85
97
|
},
|
|
@@ -226,6 +238,60 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
226
238
|
let guildCommandWorker;
|
|
227
239
|
const unshardedSupervisorId = randomUUID();
|
|
228
240
|
const deployedCommit = deploymentReference(processEnvironment);
|
|
241
|
+
const buildHealth = async () => {
|
|
242
|
+
let databaseHealthy = true;
|
|
243
|
+
try {
|
|
244
|
+
await checkDatabaseConnection(database);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
databaseHealthy = false;
|
|
248
|
+
}
|
|
249
|
+
const ready = client.isReady();
|
|
250
|
+
const scheduler = scheduledTasks
|
|
251
|
+
? await scheduledTasks
|
|
252
|
+
.getHealth()
|
|
253
|
+
.then(schedulerOperationalHealth)
|
|
254
|
+
.catch(() => ({ status: "unavailable" }))
|
|
255
|
+
: { status: "unavailable" };
|
|
256
|
+
const observedAt = new Date().toISOString();
|
|
257
|
+
const gatewayPingMs = Number.isFinite(client.ws.ping)
|
|
258
|
+
? client.ws.ping
|
|
259
|
+
: -1;
|
|
260
|
+
return shardHealthSnapshotSchema.parse({
|
|
261
|
+
version: 1,
|
|
262
|
+
mode: "off",
|
|
263
|
+
scope: "local",
|
|
264
|
+
status: ready && databaseHealthy && scheduler.status === "available"
|
|
265
|
+
? "healthy"
|
|
266
|
+
: ready
|
|
267
|
+
? "degraded"
|
|
268
|
+
: "starting",
|
|
269
|
+
observedAt,
|
|
270
|
+
generation: "unsharded",
|
|
271
|
+
supervisorId: unshardedSupervisorId,
|
|
272
|
+
configuredShardCount: 1,
|
|
273
|
+
readyShardCount: ready ? 1 : 0,
|
|
274
|
+
coordination: {
|
|
275
|
+
redis: "not_required",
|
|
276
|
+
database: databaseHealthy ? "healthy" : "unavailable",
|
|
277
|
+
},
|
|
278
|
+
scheduler,
|
|
279
|
+
shards: [
|
|
280
|
+
{
|
|
281
|
+
shardId: 0,
|
|
282
|
+
status: ready ? "ready" : "starting",
|
|
283
|
+
guildCount: client.guilds.cache.size,
|
|
284
|
+
gatewayPingMs,
|
|
285
|
+
uptimeSeconds: process.uptime(),
|
|
286
|
+
restartCount: 0,
|
|
287
|
+
eventLoopDelayMs: 0,
|
|
288
|
+
memoryRssBytes: process.memoryUsage().rss,
|
|
289
|
+
queueDepth: 0,
|
|
290
|
+
observedAt,
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
});
|
|
294
|
+
};
|
|
229
295
|
const operationsModule = environment.DISCORD_INTERNAL_GUILD_ID
|
|
230
296
|
? createDeveloperOperationsModule({
|
|
231
297
|
internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID,
|
|
@@ -244,60 +310,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
244
310
|
processLabel: "unsharded bot process",
|
|
245
311
|
triviaData: new PostgresTriviaDataProvider(database),
|
|
246
312
|
health: {
|
|
247
|
-
getHealth:
|
|
248
|
-
let databaseHealthy = true;
|
|
249
|
-
try {
|
|
250
|
-
await checkDatabaseConnection(database);
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
databaseHealthy = false;
|
|
254
|
-
}
|
|
255
|
-
const ready = client.isReady();
|
|
256
|
-
const scheduler = scheduledTasks
|
|
257
|
-
? await scheduledTasks
|
|
258
|
-
.getHealth()
|
|
259
|
-
.then(schedulerOperationalHealth)
|
|
260
|
-
.catch(() => ({ status: "unavailable" }))
|
|
261
|
-
: { status: "unavailable" };
|
|
262
|
-
const observedAt = new Date().toISOString();
|
|
263
|
-
const gatewayPingMs = Number.isFinite(client.ws.ping)
|
|
264
|
-
? client.ws.ping
|
|
265
|
-
: -1;
|
|
266
|
-
return shardHealthSnapshotSchema.parse({
|
|
267
|
-
version: 1,
|
|
268
|
-
mode: "off",
|
|
269
|
-
scope: "local",
|
|
270
|
-
status: ready && databaseHealthy && scheduler.status === "available"
|
|
271
|
-
? "healthy"
|
|
272
|
-
: ready
|
|
273
|
-
? "degraded"
|
|
274
|
-
: "starting",
|
|
275
|
-
observedAt,
|
|
276
|
-
generation: "unsharded",
|
|
277
|
-
supervisorId: unshardedSupervisorId,
|
|
278
|
-
configuredShardCount: 1,
|
|
279
|
-
readyShardCount: ready ? 1 : 0,
|
|
280
|
-
coordination: {
|
|
281
|
-
redis: "not_required",
|
|
282
|
-
database: databaseHealthy ? "healthy" : "unavailable",
|
|
283
|
-
},
|
|
284
|
-
scheduler,
|
|
285
|
-
shards: [
|
|
286
|
-
{
|
|
287
|
-
shardId: 0,
|
|
288
|
-
status: ready ? "ready" : "starting",
|
|
289
|
-
guildCount: client.guilds.cache.size,
|
|
290
|
-
gatewayPingMs,
|
|
291
|
-
uptimeSeconds: process.uptime(),
|
|
292
|
-
restartCount: 0,
|
|
293
|
-
eventLoopDelayMs: 0,
|
|
294
|
-
memoryRssBytes: process.memoryUsage().rss,
|
|
295
|
-
queueDepth: 0,
|
|
296
|
-
observedAt,
|
|
297
|
-
},
|
|
298
|
-
],
|
|
299
|
-
});
|
|
300
|
-
},
|
|
313
|
+
getHealth: buildHealth,
|
|
301
314
|
},
|
|
302
315
|
})
|
|
303
316
|
: undefined;
|
|
@@ -561,7 +574,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
561
574
|
await client.destroy();
|
|
562
575
|
throw error;
|
|
563
576
|
}
|
|
564
|
-
return { shutdown };
|
|
577
|
+
return { shutdown, statusHealth: buildHealth };
|
|
565
578
|
}
|
|
566
579
|
catch (error) {
|
|
567
580
|
if (retentionCleanupTimer)
|
package/dist/interactions.d.ts
CHANGED
|
@@ -44,4 +44,5 @@ export declare function normalizeDiscordPermission(permission: string): string;
|
|
|
44
44
|
export declare function toReplyOptions(response: MessageResponse): InteractionReplyOptions;
|
|
45
45
|
export declare function toUpdateOptions(response: MessageResponse): InteractionUpdateOptions;
|
|
46
46
|
export declare function toModal(definition: ModalDefinition): ModalBuilder;
|
|
47
|
+
export declare function safeInteractionFailureMessage(error: unknown): string;
|
|
47
48
|
//# sourceMappingURL=interactions.d.ts.map
|
package/dist/interactions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { lookup as lookupDns } from "node:dns/promises";
|
|
2
|
-
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
2
|
+
import { HELYX_SERVICE_NAMES, NexusEconomyError, UserFacingInteractionError, } from "@helyx/sdk";
|
|
3
3
|
import { canUseCommand } from "@helyx/core";
|
|
4
4
|
import { commandRegistrationPolicy, contributionRegistrationPolicy, } from "./command-registration-policy.js";
|
|
5
5
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, EmbedBuilder, Events, GuildMember, ApplicationIntegrationType, ChannelType, InteractionContextType, LabelBuilder, MessageFlags, MentionableSelectMenuBuilder, ModalBuilder, RoleSelectMenuBuilder, Routes, StringSelectMenuBuilder, SlashCommandBuilder, TextInputBuilder, TextInputStyle, UserSelectMenuBuilder, TextDisplayBuilder, PermissionFlagsBits, escapeMarkdown, } from "discord.js";
|
|
@@ -282,7 +282,7 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
282
282
|
...interactionLogContext(interaction),
|
|
283
283
|
error: safeError(error),
|
|
284
284
|
}, "Module interaction failed");
|
|
285
|
-
await sendFailure(interaction);
|
|
285
|
+
await sendFailure(interaction, error);
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
return registrationReady;
|
|
@@ -578,6 +578,7 @@ function staleComponentResponse() {
|
|
|
578
578
|
function createContext(interaction, services) {
|
|
579
579
|
return {
|
|
580
580
|
interactionId: interaction.id,
|
|
581
|
+
interactionCreatedAt: new Date(interaction.createdTimestamp),
|
|
581
582
|
...("customId" in interaction ? { customId: interaction.customId } : {}),
|
|
582
583
|
userId: interaction.user.id,
|
|
583
584
|
userDisplayName: interaction.user.globalName ?? interaction.user.username,
|
|
@@ -739,6 +740,7 @@ function createModalContext(interaction, services) {
|
|
|
739
740
|
: undefined;
|
|
740
741
|
return {
|
|
741
742
|
interactionId: interaction.id,
|
|
743
|
+
interactionCreatedAt: new Date(interaction.createdTimestamp),
|
|
742
744
|
customId: interaction.customId,
|
|
743
745
|
userId: interaction.user.id,
|
|
744
746
|
userDisplayName: interaction.user.globalName ?? interaction.user.username,
|
|
@@ -1289,14 +1291,48 @@ function discordChannelType(type) {
|
|
|
1289
1291
|
return ChannelType.GuildCategory;
|
|
1290
1292
|
return ChannelType.GuildText;
|
|
1291
1293
|
}
|
|
1292
|
-
|
|
1294
|
+
export function safeInteractionFailureMessage(error) {
|
|
1295
|
+
if (error instanceof UserFacingInteractionError) {
|
|
1296
|
+
const messages = {
|
|
1297
|
+
nexus_exchange_slow: "The Exchange is taking too long to respond. Try again shortly.",
|
|
1298
|
+
nexus_exchange_item_unavailable: "That Exchange item is not available to you right now.",
|
|
1299
|
+
nexus_exchange_item_changed: "That Exchange item changed or is no longer available. Start again.",
|
|
1300
|
+
nexus_exchange_balance_changed: "Your Nexus balance changed after the preview. Start again.",
|
|
1301
|
+
nexus_exchange_form_expired: "This request form expired. Run /nexus redeem again.",
|
|
1302
|
+
nexus_exchange_form_replayed: "This request form was already submitted. Run /nexus redeem again.",
|
|
1303
|
+
nexus_exchange_preview_expired: "This redemption preview expired. Start the command again.",
|
|
1304
|
+
nexus_exchange_invalid_request: "This Exchange request is invalid. Start the command again.",
|
|
1305
|
+
nexus_exchange_response_required: "This custom request needs a response.",
|
|
1306
|
+
};
|
|
1307
|
+
return messages[error.code];
|
|
1308
|
+
}
|
|
1309
|
+
if (error instanceof NexusEconomyError) {
|
|
1310
|
+
const messages = {
|
|
1311
|
+
invalid_input: "This Exchange request is invalid. Start again.",
|
|
1312
|
+
module_disabled: "Nexus Economy is not enabled for this server.",
|
|
1313
|
+
item_unavailable: "That Exchange item is not available to you right now.",
|
|
1314
|
+
ineligible: "You are not eligible to redeem that Exchange item.",
|
|
1315
|
+
insufficient_stock: "There is not enough stock for this redemption.",
|
|
1316
|
+
limit_reached: "You have reached this Exchange item's redemption limit.",
|
|
1317
|
+
cooldown_active: "That Exchange item is still on cooldown for your account.",
|
|
1318
|
+
already_owned: "You already own that non-stackable Exchange item.",
|
|
1319
|
+
insufficient_balance: "Your Nexus balance is too low for this item.",
|
|
1320
|
+
operation_conflict: "This redemption was already handled or changed. Start again.",
|
|
1321
|
+
privacy_erased: "This Nexus account cannot redeem items after privacy erasure.",
|
|
1322
|
+
};
|
|
1323
|
+
return (messages[error.code] ??
|
|
1324
|
+
"Something went wrong while handling that interaction.");
|
|
1325
|
+
}
|
|
1326
|
+
return "Something went wrong while handling that interaction.";
|
|
1327
|
+
}
|
|
1328
|
+
async function sendFailure(interaction, error) {
|
|
1293
1329
|
if (interaction.isAutocomplete()) {
|
|
1294
1330
|
await interaction.respond([]).catch(() => undefined);
|
|
1295
1331
|
return;
|
|
1296
1332
|
}
|
|
1297
1333
|
if (!interaction.isRepliable())
|
|
1298
1334
|
return;
|
|
1299
|
-
const message =
|
|
1335
|
+
const message = safeInteractionFailureMessage(error);
|
|
1300
1336
|
if (interaction.replied || interaction.deferred) {
|
|
1301
1337
|
await interaction.editReply({ content: message }).catch(() => undefined);
|
|
1302
1338
|
return;
|
package/dist/oauth-service.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OAuthConnectionSummary, OAuthEligibilityResult, OAuthService } from "@helyx/sdk";
|
|
1
|
+
import type { OAuthConnectionSummary, OAuthConnectionIdentity, OAuthEligibilityResult, OAuthService } from "@helyx/sdk";
|
|
2
2
|
export declare class HttpOAuthService implements OAuthService {
|
|
3
3
|
#private;
|
|
4
4
|
private readonly internalSecret;
|
|
@@ -8,7 +8,8 @@ export declare class HttpOAuthService implements OAuthService {
|
|
|
8
8
|
}): Promise<readonly OAuthConnectionSummary[]>;
|
|
9
9
|
connectionStatus(input: {
|
|
10
10
|
discordUserId: string;
|
|
11
|
-
|
|
11
|
+
purpose?: "creator";
|
|
12
|
+
}): Promise<readonly OAuthConnectionIdentity[]>;
|
|
12
13
|
createAuthorization(input: Parameters<OAuthService["createAuthorization"]>[0]): ReturnType<OAuthService["createAuthorization"]>;
|
|
13
14
|
verifyEligibility(input: Parameters<OAuthService["verifyEligibility"]>[0]): Promise<OAuthEligibilityResult>;
|
|
14
15
|
revoke(input: Parameters<OAuthService["revoke"]>[0]): ReturnType<OAuthService["revoke"]>;
|
package/dist/oauth-service.js
CHANGED
|
@@ -11,7 +11,7 @@ export class HttpOAuthService {
|
|
|
11
11
|
}
|
|
12
12
|
async connectionStatus(input) {
|
|
13
13
|
const result = await this.#request("/v1/internal/oauth/status", input);
|
|
14
|
-
return result.
|
|
14
|
+
return result.connections;
|
|
15
15
|
}
|
|
16
16
|
createAuthorization(input) {
|
|
17
17
|
return this.#request("/v1/internal/oauth/authorizations", input);
|
package/dist/shard-supervisor.js
CHANGED
|
@@ -447,7 +447,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
447
447
|
shardCount,
|
|
448
448
|
localShardCount: claimedIds.length,
|
|
449
449
|
}, "Shard supervisor started");
|
|
450
|
-
return { shutdown };
|
|
450
|
+
return { shutdown, statusHealth: buildHealth };
|
|
451
451
|
}
|
|
452
452
|
catch (error) {
|
|
453
453
|
await shutdown("startup-failed");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StatusHeartbeatService } from "@helyx/status-client";
|
|
2
|
+
import type { ShardHealthSnapshot } from "./shard-contracts.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reduce the private shard-health contract to the bounded state accepted by the
|
|
5
|
+
* public status service. No shard IDs, guild counts, host details or errors
|
|
6
|
+
* leave the bot process.
|
|
7
|
+
*/
|
|
8
|
+
export declare function discordBotStatusService(input: {
|
|
9
|
+
health: ShardHealthSnapshot;
|
|
10
|
+
processStartedAt: string;
|
|
11
|
+
release: string;
|
|
12
|
+
}): StatusHeartbeatService;
|
|
13
|
+
//# sourceMappingURL=status-producer.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reduce the private shard-health contract to the bounded state accepted by the
|
|
3
|
+
* public status service. No shard IDs, guild counts, host details or errors
|
|
4
|
+
* leave the bot process.
|
|
5
|
+
*/
|
|
6
|
+
export function discordBotStatusService(input) {
|
|
7
|
+
const health = input.health;
|
|
8
|
+
const dependencyUnavailable = health.coordination.database === "unavailable" ||
|
|
9
|
+
health.coordination.redis === "unavailable";
|
|
10
|
+
const noReadyWorkers = health.configuredShardCount > 0 && health.readyShardCount === 0;
|
|
11
|
+
const runtimeUnavailable = health.status === "unavailable" || dependencyUnavailable || noReadyWorkers;
|
|
12
|
+
const fullyReady = health.status === "healthy" &&
|
|
13
|
+
health.readyShardCount === health.configuredShardCount &&
|
|
14
|
+
health.scheduler?.status === "available" &&
|
|
15
|
+
!dependencyUnavailable;
|
|
16
|
+
return {
|
|
17
|
+
key: "discord-bot",
|
|
18
|
+
state: runtimeUnavailable
|
|
19
|
+
? "major_outage"
|
|
20
|
+
: fullyReady
|
|
21
|
+
? "operational"
|
|
22
|
+
: "degraded",
|
|
23
|
+
reasonCode: runtimeUnavailable
|
|
24
|
+
? dependencyUnavailable
|
|
25
|
+
? "dependency_unavailable"
|
|
26
|
+
: "runtime_not_ready"
|
|
27
|
+
: fullyReady
|
|
28
|
+
? null
|
|
29
|
+
: "runtime_not_ready",
|
|
30
|
+
processStartedAt: input.processStartedAt,
|
|
31
|
+
release: input.release,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=status-producer.js.map
|
|
@@ -11,11 +11,29 @@ database, the reviewed core packages, and independently selected modules. Keep
|
|
|
11
11
|
module boundaries intact: a module must be installable, configurable, testable,
|
|
12
12
|
and removable without relying on unrelated module behaviour.
|
|
13
13
|
|
|
14
|
+
Every selectable module runs from that generated bot and its normal local
|
|
15
|
+
runtime. A self-host-compatible module does not require a provider API key,
|
|
16
|
+
OAuth application or separate third-party integration service. Provider-backed
|
|
17
|
+
features are Hosted-only and excluded from the generated project. The Helyx
|
|
18
|
+
Dashboard and optional Cloud Connect are the explicit hosted-service exceptions;
|
|
19
|
+
local Discord module work continues if either is unavailable.
|
|
20
|
+
|
|
14
21
|
Helyx supports Docker Compose and direct Node.js operation. The generated
|
|
15
22
|
project is the source of truth for its pinned public packages and module
|
|
16
23
|
catalogue. Do not copy source trees from the Helyx repository into an
|
|
17
24
|
installation.
|
|
18
25
|
|
|
26
|
+
Every implemented catalogue entry carries a manifest-backed deployment label
|
|
27
|
+
and credential-risk label. A planned entry without a canonical v0.4 manifest
|
|
28
|
+
is explicitly `unclassified`; that label makes no promise about its future
|
|
29
|
+
deployment support and it cannot be selected. The builder accepts only Live
|
|
30
|
+
modules whose
|
|
31
|
+
`deploymentSupport` is `self-hosted` or `both`, copies that policy into the
|
|
32
|
+
downloaded build manifest and resolves only eligible dependencies. The
|
|
33
|
+
dashboard shows the deployment and credential label beside each selectable
|
|
34
|
+
module. A `hosted` module cannot be selected or introduced by editing the
|
|
35
|
+
downloaded manifest.
|
|
36
|
+
|
|
19
37
|
## Secrets and access
|
|
20
38
|
|
|
21
39
|
- Store `.env` outside version control and restrict who can read it.
|
|
@@ -30,6 +30,42 @@ must exactly match executable command contributions. Declare Discord
|
|
|
30
30
|
permissions and privileged capabilities only when the module actually needs
|
|
31
31
|
them.
|
|
32
32
|
|
|
33
|
+
## Deployment and credential policy
|
|
34
|
+
|
|
35
|
+
Use manifest version `0.4` and make deployment eligibility explicit:
|
|
36
|
+
|
|
37
|
+
- `deploymentSupport` is `hosted`, `self-hosted` or `both`;
|
|
38
|
+
- `credentialRisk` is `none`, `low`, `medium`, `high` or `very-high`; and
|
|
39
|
+
- `deploymentReason` is a short internal explanation with no secret values.
|
|
40
|
+
|
|
41
|
+
The classification is a release decision, not a runtime guess. A hosted-only
|
|
42
|
+
manifest is rejected from self-host selection and generated projects. A
|
|
43
|
+
self-host-only manifest is rejected from hosted composition. The separate
|
|
44
|
+
hosted-only package list remains a defence-in-depth check and must agree with
|
|
45
|
+
the canonical manifest.
|
|
46
|
+
|
|
47
|
+
Self-host-compatible modules must be complete within the generated bot and
|
|
48
|
+
must not require a provider credential or separate integration service. If a
|
|
49
|
+
future design changes that boundary, review the deployment and credential risk
|
|
50
|
+
before implementation; do not weaken the manifest to make composition pass.
|
|
51
|
+
|
|
52
|
+
## Commands and dashboard policy
|
|
53
|
+
|
|
54
|
+
Normal customer-module commands use
|
|
55
|
+
`registrationPolicy: "enabled-guild"`, server context and guild installation.
|
|
56
|
+
Helyx projects those commands only into servers that select the module; a
|
|
57
|
+
module change may remain pending while the per-server Discord command update
|
|
58
|
+
is coalesced. Do not use the legacy `registration: "global" | "guild"` field
|
|
59
|
+
in new code. `platform-global`, `deployment` and `internal-guild` are reserved
|
|
60
|
+
for their explicit platform, deployment or Helyx operational purposes.
|
|
61
|
+
|
|
62
|
+
Use manifest-defined settings, dashboard actions and managed resources before
|
|
63
|
+
requesting a custom interface. Keep Permissions available for module-level and
|
|
64
|
+
resource access even when the module has no commands; command overrides exist
|
|
65
|
+
only for real commands. Present record details as labelled sections, fields,
|
|
66
|
+
lists and tables rather than serialized JSON, and provide bounded loading,
|
|
67
|
+
empty, disabled, conflict and failure states.
|
|
68
|
+
|
|
33
69
|
## Preserve modularity
|
|
34
70
|
|
|
35
71
|
A module owns its commands, configuration, data, migrations, health, and tests.
|
|
@@ -21,6 +21,11 @@ boundary values, wrong types, and missing required settings against the
|
|
|
21
21
|
module's configuration contribution. Each dashboard setting key must have one
|
|
22
22
|
matching validator field.
|
|
23
23
|
|
|
24
|
+
For resources and actions, test declared operations, permissions, status
|
|
25
|
+
gates, revisions, pagination and safe errors. Verify normal, empty, disabled,
|
|
26
|
+
conflict and failed states, and confirm that read-only details expose labelled
|
|
27
|
+
values rather than raw JSON.
|
|
28
|
+
|
|
24
29
|
## Slash commands
|
|
25
30
|
|
|
26
31
|
Assert that manifest command IDs and executable command names are identical.
|
|
@@ -29,6 +34,19 @@ generated application-command payload, and execute the handler with a fake SDK
|
|
|
29
34
|
context. Test acknowledgement, context, integration type, option limits, and a
|
|
30
35
|
representative response without contacting Discord.
|
|
31
36
|
|
|
37
|
+
Assert the explicit registration policy too. A normal customer-module command
|
|
38
|
+
uses `enabled-guild`; new code must not use the legacy `registration` field.
|
|
39
|
+
Test that disabled-module and denied-policy execution is rejected even while a
|
|
40
|
+
stale command may still be visible during Discord reconciliation.
|
|
41
|
+
|
|
42
|
+
## Privacy and multi-worker behaviour
|
|
43
|
+
|
|
44
|
+
Test the privacy declaration and executable provider together. A provider must
|
|
45
|
+
find matching retained records even when its module is disabled, and a missing
|
|
46
|
+
or failed provider must not be interpreted as an empty result. Stateful
|
|
47
|
+
modules also test duplicate delivery, concurrent claims, retry, restart and
|
|
48
|
+
stale-worker behaviour where those failures are possible.
|
|
49
|
+
|
|
32
50
|
## Before packaging
|
|
33
51
|
|
|
34
52
|
Run the module's build and test scripts, then inspect exactly what npm would
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helyx/bot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.43",
|
|
4
4
|
"description": "Production Discord runtime for the modular Helyx platform.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helyx",
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
"@helyx/config": ">=0.5.0 <0.6.0",
|
|
54
54
|
"@helyx/control-protocol": ">=0.4.0 <0.5.0",
|
|
55
55
|
"@helyx/core": ">=0.1.0 <0.2.0",
|
|
56
|
-
"@helyx/database": ">=0.
|
|
56
|
+
"@helyx/database": ">=0.9.0 <0.10.0",
|
|
57
57
|
"@helyx/framework": ">=0.1.0 <0.2.0",
|
|
58
|
-
"@helyx/module-manifest": ">=0.
|
|
58
|
+
"@helyx/module-manifest": ">=0.8.0 <0.9.0",
|
|
59
59
|
"@helyx/platform": ">=0.6.2 <0.7.0",
|
|
60
|
-
"@helyx/sdk": ">=0.
|
|
60
|
+
"@helyx/sdk": ">=0.9.0 <0.10.0",
|
|
61
61
|
"@helyx/status-client": ">=0.1.0 <0.2.0",
|
|
62
62
|
"@helyx/verification-contracts": ">=0.3.0 <0.4.0",
|
|
63
63
|
"discord.js": "14.27.0",
|