@helyx/bot 0.4.30 → 0.4.32
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/dist/control-permissions.d.ts +38 -0
- package/dist/control-permissions.js +139 -0
- package/dist/control-server.d.ts +1 -15
- package/dist/control-server.js +112 -166
- package/dist/discord-guild-health.d.ts +174 -0
- package/dist/discord-guild-health.js +370 -0
- package/dist/events.d.ts +15 -0
- package/dist/events.js +338 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +26 -7
- package/dist/interactions.js +36 -6
- package/dist/modules.d.ts +2 -0
- package/dist/modules.js +10 -1
- package/dist/oauth-module.d.ts +2 -0
- package/dist/oauth-module.js +192 -0
- package/dist/oauth-module.test.d.ts +2 -0
- package/dist/oauth-module.test.js +172 -0
- package/dist/oauth-service.d.ts +16 -0
- package/dist/oauth-service.js +40 -0
- package/dist/shard-supervisor.js +6 -0
- package/dist/shard-worker.d.ts +5 -0
- package/dist/shard-worker.js +16 -1
- package/package.json +70 -70
package/dist/modules.d.ts
CHANGED
|
@@ -12,11 +12,13 @@ export interface ConfiguredModulePackage extends LoadedDefinition {
|
|
|
12
12
|
}
|
|
13
13
|
export declare const secureRandomSelectionWorkerKeyName = "HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY";
|
|
14
14
|
interface PlatformServiceEnvironment extends Pick<BotEnvironment, "HELYX_DATA_ENCRYPTION_KEY"> {
|
|
15
|
+
HELYX_DASHBOARD_PUBLIC_URL?: string;
|
|
15
16
|
HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY?: string;
|
|
16
17
|
}
|
|
17
18
|
export declare function deriveSecureRandomSelectionKey(encodedDataEncryptionKey: string): Uint8Array;
|
|
18
19
|
export declare function platformServiceOptions(environment: PlatformServiceEnvironment, modules: readonly ModuleDefinition[]): {
|
|
19
20
|
secureRandomSelectionKey?: Uint8Array;
|
|
21
|
+
dashboardBaseUrl?: string;
|
|
20
22
|
};
|
|
21
23
|
export declare function loadConfiguredModules(specifiers: readonly string[], importer?: ModuleImporter): Promise<ModuleDefinition[]>;
|
|
22
24
|
export declare function loadConfiguredModulePackages(specifiers: readonly string[], importer?: ModuleImporter, resolvePackageJson?: PackageJsonResolver, readPackageJson?: PackageJsonReader): Promise<ConfiguredModulePackage[]>;
|
package/dist/modules.js
CHANGED
|
@@ -21,9 +21,13 @@ export function deriveSecureRandomSelectionKey(encodedDataEncryptionKey) {
|
|
|
21
21
|
.digest();
|
|
22
22
|
}
|
|
23
23
|
export function platformServiceOptions(environment, modules) {
|
|
24
|
+
const urls = environment.HELYX_DASHBOARD_PUBLIC_URL
|
|
25
|
+
? { dashboardBaseUrl: environment.HELYX_DASHBOARD_PUBLIC_URL }
|
|
26
|
+
: {};
|
|
24
27
|
const internalSelectionKey = environment.HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY;
|
|
25
28
|
if (internalSelectionKey) {
|
|
26
29
|
return {
|
|
30
|
+
...urls,
|
|
27
31
|
secureRandomSelectionKey: decodeSecureRandomSelectionKey(internalSelectionKey, secureRandomSelectionWorkerKeyName),
|
|
28
32
|
};
|
|
29
33
|
}
|
|
@@ -32,9 +36,10 @@ export function platformServiceOptions(environment, modules) {
|
|
|
32
36
|
if (modules.some(({ manifest }) => manifest.id === giveawaysModuleId)) {
|
|
33
37
|
throw new Error("HELYX_DATA_ENCRYPTION_KEY is required when Giveaways is loaded");
|
|
34
38
|
}
|
|
35
|
-
return
|
|
39
|
+
return urls;
|
|
36
40
|
}
|
|
37
41
|
return {
|
|
42
|
+
...urls,
|
|
38
43
|
secureRandomSelectionKey: deriveSecureRandomSelectionKey(encodedKey),
|
|
39
44
|
};
|
|
40
45
|
}
|
|
@@ -103,7 +108,11 @@ export function validateContributionParity(definition) {
|
|
|
103
108
|
...(definition.events?.guildVoiceSession ?? []),
|
|
104
109
|
...(definition.events?.messageReactionAdd ?? []),
|
|
105
110
|
...(definition.events?.messageReactionRemove ?? []),
|
|
111
|
+
...(definition.events?.messageReactionRemoveAll ?? []),
|
|
112
|
+
...(definition.events?.messageReactionRemoveEmoji ?? []),
|
|
113
|
+
...(definition.events?.guildMessageUpdate ?? []),
|
|
106
114
|
...(definition.events?.messageDelete ?? []),
|
|
115
|
+
...(definition.events?.guildMessageDeleteBulk ?? []),
|
|
107
116
|
...(definition.events?.guildMemberAdd ?? []),
|
|
108
117
|
...(definition.events?.guildMemberRemove ?? []),
|
|
109
118
|
...(definition.events?.guildRoleDelete ?? []),
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { defineModule, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
2
|
+
const MODULE_ID = "helyx.oauth";
|
|
3
|
+
const MODAL_ID = "helyx.oauth.revoke";
|
|
4
|
+
const SERVICE_FIELD_ID = "helyx.oauth.revoke.service";
|
|
5
|
+
const CONFIRM_FIELD_ID = "helyx.oauth.revoke.confirm";
|
|
6
|
+
const COMMENT_FIELD_ID = "helyx.oauth.revoke.comment";
|
|
7
|
+
const everyonePolicy = {
|
|
8
|
+
everyone: true,
|
|
9
|
+
requiredPermissions: [],
|
|
10
|
+
allowedRoleIds: [],
|
|
11
|
+
deniedRoleIds: [],
|
|
12
|
+
allowedUserIds: [],
|
|
13
|
+
deniedUserIds: [],
|
|
14
|
+
};
|
|
15
|
+
export function createOAuthModule() {
|
|
16
|
+
return defineModule({
|
|
17
|
+
manifest: {
|
|
18
|
+
manifestVersion: "0.1",
|
|
19
|
+
releaseStatus: "live",
|
|
20
|
+
id: MODULE_ID,
|
|
21
|
+
name: "Helyx OAuth",
|
|
22
|
+
version: "0.1.0",
|
|
23
|
+
description: "Provides self-service OAuth connection revocation.",
|
|
24
|
+
entry: "./dist/oauth-module.js",
|
|
25
|
+
framework: "^0.1.0",
|
|
26
|
+
dependencies: [],
|
|
27
|
+
permissions: [],
|
|
28
|
+
contributions: { commands: ["oauth"], events: [] },
|
|
29
|
+
},
|
|
30
|
+
defaultPermissions: {
|
|
31
|
+
module: everyonePolicy,
|
|
32
|
+
commands: { "oauth.revoke": everyonePolicy },
|
|
33
|
+
},
|
|
34
|
+
interactions: {
|
|
35
|
+
commands: [
|
|
36
|
+
{
|
|
37
|
+
name: "oauth",
|
|
38
|
+
description: "Manage OAuth access granted to Helyx.",
|
|
39
|
+
registration: "global",
|
|
40
|
+
contexts: ["guild"],
|
|
41
|
+
integrationTypes: ["guild_install"],
|
|
42
|
+
subcommands: [
|
|
43
|
+
{
|
|
44
|
+
name: "revoke",
|
|
45
|
+
description: "Revoke a service and delete the OAuth details Helyx stores for you.",
|
|
46
|
+
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
47
|
+
execute: async (context) => {
|
|
48
|
+
const oauth = context.services.get(HELYX_SERVICE_NAMES.oauth);
|
|
49
|
+
let connections;
|
|
50
|
+
try {
|
|
51
|
+
connections = await oauth.listConnections({
|
|
52
|
+
discordUserId: context.userId,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
await context.reply(unavailable());
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (connections.length === 0) {
|
|
60
|
+
await context.reply({
|
|
61
|
+
ephemeral: true,
|
|
62
|
+
componentsV2: {
|
|
63
|
+
text: [
|
|
64
|
+
{
|
|
65
|
+
content: "You do not have any OAuth services connected to Helyx.",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
await context.showModal({
|
|
73
|
+
customId: MODAL_ID,
|
|
74
|
+
title: "Revoke OAuth access",
|
|
75
|
+
items: [
|
|
76
|
+
{
|
|
77
|
+
type: "text-display",
|
|
78
|
+
content: "Choose one connection. This disconnects the service from Helyx everywhere. Any Helyx module, server or panel using it will lose access.",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: "string-select",
|
|
82
|
+
customId: SERVICE_FIELD_ID,
|
|
83
|
+
label: "Connected service",
|
|
84
|
+
placeholder: "Choose a connection",
|
|
85
|
+
required: true,
|
|
86
|
+
minValues: 1,
|
|
87
|
+
maxValues: 1,
|
|
88
|
+
options: connections.slice(0, 25).map((connection) => ({
|
|
89
|
+
label: `${providerName(connection.provider)} — ${connection.displayName}`.slice(0, 100),
|
|
90
|
+
value: connection.selectionRef,
|
|
91
|
+
description: "Remove this connection",
|
|
92
|
+
})),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: "string-select",
|
|
96
|
+
customId: CONFIRM_FIELD_ID,
|
|
97
|
+
label: "Confirm removal",
|
|
98
|
+
placeholder: "Select Remove access",
|
|
99
|
+
required: true,
|
|
100
|
+
minValues: 1,
|
|
101
|
+
maxValues: 1,
|
|
102
|
+
options: [
|
|
103
|
+
{
|
|
104
|
+
label: "Remove access",
|
|
105
|
+
value: "remove_access",
|
|
106
|
+
description: "Revoke access and delete my stored details",
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
customId: COMMENT_FIELD_ID,
|
|
112
|
+
label: "Reason or comment (optional)",
|
|
113
|
+
description: "Stored only in Helyx's restricted security audit.",
|
|
114
|
+
style: "paragraph",
|
|
115
|
+
required: false,
|
|
116
|
+
maxLength: 250,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
fields: [],
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
modals: [
|
|
127
|
+
{
|
|
128
|
+
customId: MODAL_ID,
|
|
129
|
+
execute: async (context) => {
|
|
130
|
+
const selectionRef = context.getStringSelectValues(SERVICE_FIELD_ID)[0];
|
|
131
|
+
const confirmation = context.getStringSelectValues(CONFIRM_FIELD_ID)[0];
|
|
132
|
+
if (!selectionRef || confirmation !== "remove_access") {
|
|
133
|
+
await context.reply({
|
|
134
|
+
ephemeral: true,
|
|
135
|
+
componentsV2: {
|
|
136
|
+
text: [{ content: "Access was not removed." }],
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const comment = context.getField(COMMENT_FIELD_ID).trim();
|
|
142
|
+
await context.deferReply?.(true);
|
|
143
|
+
try {
|
|
144
|
+
const result = await context.services
|
|
145
|
+
.get(HELYX_SERVICE_NAMES.oauth)
|
|
146
|
+
.revoke({
|
|
147
|
+
discordUserId: context.userId,
|
|
148
|
+
selectionRef,
|
|
149
|
+
confirmation: "remove_access",
|
|
150
|
+
...(comment ? { comment } : {}),
|
|
151
|
+
correlationId: context.interactionId,
|
|
152
|
+
});
|
|
153
|
+
await context.reply({
|
|
154
|
+
ephemeral: true,
|
|
155
|
+
componentsV2: {
|
|
156
|
+
text: [
|
|
157
|
+
{
|
|
158
|
+
content: result.providerRevocation === "completed"
|
|
159
|
+
? `${providerName(result.provider)} access has been removed from Helyx. Local credentials were deleted.`
|
|
160
|
+
: `${providerName(result.provider)} access has been removed from Helyx and local credentials were deleted. Helyx could not confirm provider-side revocation, so you may also remove Helyx from your ${providerName(result.provider)} connected-app settings.`,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
await context.reply(unavailable());
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
start: () => Promise.resolve(),
|
|
174
|
+
stop: () => Promise.resolve(),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function providerName(provider) {
|
|
178
|
+
return provider === "youtube" ? "YouTube" : "Twitch";
|
|
179
|
+
}
|
|
180
|
+
function unavailable() {
|
|
181
|
+
return {
|
|
182
|
+
ephemeral: true,
|
|
183
|
+
componentsV2: {
|
|
184
|
+
text: [
|
|
185
|
+
{
|
|
186
|
+
content: "OAuth access could not be updated right now. Nothing new was retained; please try again.",
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=oauth-module.js.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createOAuthModule } from "./oauth-module.js";
|
|
4
|
+
describe("global OAuth command", () => {
|
|
5
|
+
it("is global, guild-scoped and available without management permissions", () => {
|
|
6
|
+
const module = createOAuthModule();
|
|
7
|
+
const command = module.interactions?.commands?.[0];
|
|
8
|
+
const revoke = command?.subcommands?.[0];
|
|
9
|
+
expect(command).toMatchObject({
|
|
10
|
+
name: "oauth",
|
|
11
|
+
registration: "global",
|
|
12
|
+
contexts: ["guild"],
|
|
13
|
+
integrationTypes: ["guild_install"],
|
|
14
|
+
});
|
|
15
|
+
expect(revoke).toMatchObject({
|
|
16
|
+
name: "revoke",
|
|
17
|
+
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
18
|
+
});
|
|
19
|
+
expect(module.manifest.permissions).toEqual([]);
|
|
20
|
+
expect(module.defaultPermissions?.commands["oauth.revoke"]).toMatchObject({
|
|
21
|
+
everyone: true,
|
|
22
|
+
requiredPermissions: [],
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
it("does not open a modal when the acting user has no connections", async () => {
|
|
26
|
+
const oauth = oauthService({ connections: [] });
|
|
27
|
+
const showModal = vi.fn();
|
|
28
|
+
const replies = [];
|
|
29
|
+
const command = createOAuthModule().interactions?.commands?.[0]?.subcommands?.[0];
|
|
30
|
+
await command?.execute(commandContext(oauth, {
|
|
31
|
+
showModal,
|
|
32
|
+
reply: (response) => {
|
|
33
|
+
replies.push(response);
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
expect(showModal).not.toHaveBeenCalled();
|
|
38
|
+
expect(replies).toEqual([expect.objectContaining({ ephemeral: true })]);
|
|
39
|
+
expect(replies[0]?.componentsV2?.text?.[0]?.content).toContain("do not have any OAuth services");
|
|
40
|
+
});
|
|
41
|
+
it("uses opaque selections and requires explicit removal confirmation", async () => {
|
|
42
|
+
const oauth = oauthService({
|
|
43
|
+
connections: [
|
|
44
|
+
{
|
|
45
|
+
selectionRef: "opaque-selection-reference-123",
|
|
46
|
+
provider: "youtube",
|
|
47
|
+
displayName: "Example creator",
|
|
48
|
+
connectedAt: new Date().toISOString(),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
const showModal = vi.fn();
|
|
53
|
+
const command = createOAuthModule().interactions?.commands?.[0]?.subcommands?.[0];
|
|
54
|
+
await command?.execute(commandContext(oauth, { showModal }));
|
|
55
|
+
const modal = showModal.mock.calls[0]?.[0];
|
|
56
|
+
expect(modal?.title).toBe("Revoke OAuth access");
|
|
57
|
+
const service = modal?.items?.find((item) => "customId" in item && item.customId.endsWith("service"));
|
|
58
|
+
const confirmation = modal?.items?.find((item) => "customId" in item && item.customId.endsWith("confirm"));
|
|
59
|
+
expect(service).toMatchObject({
|
|
60
|
+
type: "string-select",
|
|
61
|
+
required: true,
|
|
62
|
+
options: [
|
|
63
|
+
expect.objectContaining({ value: "opaque-selection-reference-123" }),
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
expect(confirmation).toMatchObject({
|
|
67
|
+
type: "string-select",
|
|
68
|
+
required: true,
|
|
69
|
+
options: [expect.objectContaining({ value: "remove_access" })],
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it("revokes only for the acting user and returns an ephemeral advisory", async () => {
|
|
73
|
+
const revoke = vi.fn().mockResolvedValue({
|
|
74
|
+
provider: "twitch",
|
|
75
|
+
outcome: "completed",
|
|
76
|
+
providerRevocation: "failed",
|
|
77
|
+
});
|
|
78
|
+
const oauth = oauthService({ connections: [], revoke });
|
|
79
|
+
const replies = [];
|
|
80
|
+
const modal = createOAuthModule().interactions?.modals?.[0];
|
|
81
|
+
await modal?.execute(modalContext(oauth, {
|
|
82
|
+
reply: (response) => {
|
|
83
|
+
replies.push(response);
|
|
84
|
+
return Promise.resolve();
|
|
85
|
+
},
|
|
86
|
+
}));
|
|
87
|
+
expect(revoke).toHaveBeenCalledWith({
|
|
88
|
+
discordUserId: "12345678901234567",
|
|
89
|
+
selectionRef: "opaque-selection-reference-123",
|
|
90
|
+
confirmation: "remove_access",
|
|
91
|
+
comment: "No longer needed",
|
|
92
|
+
correlationId: "interaction-1",
|
|
93
|
+
});
|
|
94
|
+
expect(replies[0]).toMatchObject({ ephemeral: true });
|
|
95
|
+
expect(replies[0]?.componentsV2?.text?.[0]?.content).toContain("connected-app settings");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
function oauthService(input) {
|
|
99
|
+
return {
|
|
100
|
+
connectionStatus: () => Promise.resolve([]),
|
|
101
|
+
listConnections: () => Promise.resolve(input.connections),
|
|
102
|
+
createAuthorization: () => Promise.resolve({
|
|
103
|
+
authorizationUrl: "https://example.com",
|
|
104
|
+
expiresAt: "",
|
|
105
|
+
}),
|
|
106
|
+
verifyEligibility: () => Promise.resolve({
|
|
107
|
+
provider: "youtube",
|
|
108
|
+
status: "unknown",
|
|
109
|
+
connected: false,
|
|
110
|
+
checkedAt: new Date().toISOString(),
|
|
111
|
+
}),
|
|
112
|
+
revoke: input.revoke ??
|
|
113
|
+
(() => Promise.resolve({
|
|
114
|
+
provider: "youtube",
|
|
115
|
+
outcome: "completed",
|
|
116
|
+
providerRevocation: "completed",
|
|
117
|
+
})),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function services(oauth) {
|
|
121
|
+
return {
|
|
122
|
+
has: (name) => name === HELYX_SERVICE_NAMES.oauth,
|
|
123
|
+
get: (name) => {
|
|
124
|
+
if (name !== HELYX_SERVICE_NAMES.oauth)
|
|
125
|
+
throw new Error("unknown service");
|
|
126
|
+
return oauth;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function commandContext(oauth, overrides = {}) {
|
|
131
|
+
return {
|
|
132
|
+
...interactionBase(oauth),
|
|
133
|
+
showModal: () => Promise.resolve(),
|
|
134
|
+
getStringOption: () => null,
|
|
135
|
+
getBooleanOption: () => null,
|
|
136
|
+
getUserOption: () => null,
|
|
137
|
+
getChannelOption: () => null,
|
|
138
|
+
getRoleOption: () => null,
|
|
139
|
+
...overrides,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function modalContext(oauth, overrides = {}) {
|
|
143
|
+
return {
|
|
144
|
+
...interactionBase(oauth),
|
|
145
|
+
getField: (customId) => customId.endsWith("comment") ? "No longer needed" : "",
|
|
146
|
+
getStringSelectValues: (customId) => customId.endsWith("service")
|
|
147
|
+
? ["opaque-selection-reference-123"]
|
|
148
|
+
: ["remove_access"],
|
|
149
|
+
getSelectedUserIds: () => [],
|
|
150
|
+
getSelectedRoleIds: () => [],
|
|
151
|
+
getSelectedMentionables: () => ({ userIds: [], roleIds: [] }),
|
|
152
|
+
getSelectedChannelIds: () => [],
|
|
153
|
+
...overrides,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function interactionBase(oauth) {
|
|
157
|
+
return {
|
|
158
|
+
interactionId: "interaction-1",
|
|
159
|
+
userId: "12345678901234567",
|
|
160
|
+
userDisplayName: "Member",
|
|
161
|
+
userRoleIds: [],
|
|
162
|
+
isAdministrator: false,
|
|
163
|
+
serverId: "12345678901234568",
|
|
164
|
+
services: services(oauth),
|
|
165
|
+
reply: () => Promise.resolve(),
|
|
166
|
+
sendToChannel: () => Promise.resolve({ messageId: "message-1" }),
|
|
167
|
+
deleteChannelMessage: () => Promise.resolve(),
|
|
168
|
+
editChannelMessage: () => Promise.resolve(),
|
|
169
|
+
addChannelMessageReaction: () => Promise.resolve(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=oauth-module.test.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { OAuthConnectionSummary, OAuthEligibilityResult, OAuthService } from "@helyx/sdk";
|
|
2
|
+
export declare class HttpOAuthService implements OAuthService {
|
|
3
|
+
#private;
|
|
4
|
+
private readonly internalSecret;
|
|
5
|
+
constructor(baseUrl: string, internalSecret: string);
|
|
6
|
+
listConnections(input: {
|
|
7
|
+
discordUserId: string;
|
|
8
|
+
}): Promise<readonly OAuthConnectionSummary[]>;
|
|
9
|
+
connectionStatus(input: {
|
|
10
|
+
discordUserId: string;
|
|
11
|
+
}): Promise<readonly ("youtube" | "twitch")[]>;
|
|
12
|
+
createAuthorization(input: Parameters<OAuthService["createAuthorization"]>[0]): ReturnType<OAuthService["createAuthorization"]>;
|
|
13
|
+
verifyEligibility(input: Parameters<OAuthService["verifyEligibility"]>[0]): Promise<OAuthEligibilityResult>;
|
|
14
|
+
revoke(input: Parameters<OAuthService["revoke"]>[0]): ReturnType<OAuthService["revoke"]>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=oauth-service.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class HttpOAuthService {
|
|
2
|
+
internalSecret;
|
|
3
|
+
#baseUrl;
|
|
4
|
+
constructor(baseUrl, internalSecret) {
|
|
5
|
+
this.internalSecret = internalSecret;
|
|
6
|
+
this.#baseUrl = new URL(baseUrl);
|
|
7
|
+
}
|
|
8
|
+
async listConnections(input) {
|
|
9
|
+
const result = await this.#request("/v1/internal/oauth/connections", input);
|
|
10
|
+
return result.connections;
|
|
11
|
+
}
|
|
12
|
+
async connectionStatus(input) {
|
|
13
|
+
const result = await this.#request("/v1/internal/oauth/status", input);
|
|
14
|
+
return result.providers;
|
|
15
|
+
}
|
|
16
|
+
createAuthorization(input) {
|
|
17
|
+
return this.#request("/v1/internal/oauth/authorizations", input);
|
|
18
|
+
}
|
|
19
|
+
verifyEligibility(input) {
|
|
20
|
+
return this.#request("/v1/internal/oauth/eligibility", input);
|
|
21
|
+
}
|
|
22
|
+
revoke(input) {
|
|
23
|
+
return this.#request("/v1/internal/oauth/revocations", input);
|
|
24
|
+
}
|
|
25
|
+
async #request(path, body) {
|
|
26
|
+
const response = await fetch(new URL(path, this.#baseUrl), {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
authorization: `Bearer ${this.internalSecret}`,
|
|
30
|
+
"content-type": "application/json",
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
signal: AbortSignal.timeout(6_000),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok)
|
|
36
|
+
throw new Error(`oauth_service_http_${response.status}`);
|
|
37
|
+
return (await response.json());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=oauth-service.js.map
|
package/dist/shard-supervisor.js
CHANGED
|
@@ -22,6 +22,7 @@ import { ShardControlRouter } from "./shard-router.js";
|
|
|
22
22
|
import { createDeveloperOperationsModule } from "./developer-operations-module.js";
|
|
23
23
|
import { SupervisorCloudDiscordDirectory } from "./supervisor-cloud-directory.js";
|
|
24
24
|
import { createSupportModule } from "./support-module.js";
|
|
25
|
+
import { createOAuthModule } from "./oauth-module.js";
|
|
25
26
|
import { createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
|
|
26
27
|
import { numericWriteBehindRegistrations } from "./numeric-write-behind.js";
|
|
27
28
|
import { executeRedisNumericOperation, RedisNumericWriteBehindService, } from "./numeric-write-behind-redis.js";
|
|
@@ -187,10 +188,14 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
187
188
|
})
|
|
188
189
|
: undefined;
|
|
189
190
|
const supportModule = createSupportModule(cloudConnector);
|
|
191
|
+
const oauthModule = environment.HELYX_OAUTH_ENABLED
|
|
192
|
+
? createOAuthModule()
|
|
193
|
+
: undefined;
|
|
190
194
|
const publicModules = [
|
|
191
195
|
managementModule,
|
|
192
196
|
permissionsModule,
|
|
193
197
|
supportModule,
|
|
198
|
+
...(oauthModule ? [oauthModule] : []),
|
|
194
199
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
195
200
|
...featureModules,
|
|
196
201
|
];
|
|
@@ -210,6 +215,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
210
215
|
managementModule,
|
|
211
216
|
permissionsModule,
|
|
212
217
|
supportModule,
|
|
218
|
+
...(oauthModule ? [oauthModule] : []),
|
|
213
219
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
214
220
|
].map(({ manifest }) => manifest),
|
|
215
221
|
modules: featureModules.map(({ manifest }) => manifest),
|
package/dist/shard-worker.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
34
34
|
APPROVED_ROLES: string[];
|
|
35
35
|
HELYX_MODULE_PACKAGES: string[];
|
|
36
36
|
HELYX_MODULE_CATALOGUE_PATH: string | undefined;
|
|
37
|
+
HELYX_DASHBOARD_PUBLIC_URL: string;
|
|
37
38
|
HELYX_SUPPORT_STAFF_CHANNEL_ID: string | undefined;
|
|
38
39
|
HELYX_VERIFICATION_ENABLED: boolean;
|
|
39
40
|
HELYX_VERIFICATION_SIGNING_PUBLIC_KEYS: Record<string, string>;
|
|
@@ -41,10 +42,12 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
41
42
|
HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS: number;
|
|
42
43
|
HELYX_PRIVACY_ENABLED: boolean;
|
|
43
44
|
HELYX_MESSAGE_CONTENT_ENABLED: boolean;
|
|
45
|
+
HELYX_GUILD_MEMBERS_ENABLED: boolean;
|
|
44
46
|
HELYX_CLOUD_CONNECT_ENABLED: boolean;
|
|
45
47
|
HELYX_CLOUD_ENROLMENT_TOKEN: string | undefined;
|
|
46
48
|
HELYX_CLOUD_ENROLMENT_FILE: string;
|
|
47
49
|
HELYX_CLOUD_API_URL: string;
|
|
50
|
+
HELYX_OAUTH_ENABLED: boolean;
|
|
48
51
|
HELYX_CLOUD_ENDPOINT: string;
|
|
49
52
|
HELYX_OWNER_USER_IDS: string[];
|
|
50
53
|
HELYX_SHARD_MODE: "off" | "auto" | "fixed";
|
|
@@ -60,6 +63,7 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
60
63
|
HELYX_VERIFICATION_INTERNAL_SECRET?: string | undefined;
|
|
61
64
|
HELYX_PRIVACY_EXECUTOR_DATABASE_URL?: string | undefined;
|
|
62
65
|
HELYX_PRIVACY_EXECUTOR_DATABASE_SSL?: "disable" | "require" | "verify-full" | undefined;
|
|
66
|
+
HELYX_OAUTH_INTERNAL_SECRET?: string | undefined;
|
|
63
67
|
HELYX_SHARD_COUNT?: number | undefined;
|
|
64
68
|
HELYX_SHARD_REDIS_URL?: string | undefined;
|
|
65
69
|
HELYX_SHARD_REDIS_NAMESPACE?: string | undefined;
|
|
@@ -71,5 +75,6 @@ export declare function shardWorkerCommandRegistration(input: {
|
|
|
71
75
|
prepare: NonNullable<CommandRegistration["prepare"]>;
|
|
72
76
|
operationsModuleId?: string;
|
|
73
77
|
supportModuleId: string;
|
|
78
|
+
oauthModuleId?: string;
|
|
74
79
|
}): CommandRegistration;
|
|
75
80
|
//# sourceMappingURL=shard-worker.d.ts.map
|
package/dist/shard-worker.js
CHANGED
|
@@ -20,11 +20,14 @@ import { loadConfiguredModulePackages, platformServiceOptions, secureRandomSelec
|
|
|
20
20
|
import { createPermissionsModule } from "./permissions-module.js";
|
|
21
21
|
import { WorkerCloudConnectorProxy } from "./shard-cloud-proxy.js";
|
|
22
22
|
import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
|
|
23
|
+
import { DiscordGuildHealthInspection } from "./discord-guild-health.js";
|
|
23
24
|
import { CoreResourceAuthorizationService } from "./resource-authorization.js";
|
|
24
25
|
import { createDeveloperOperationsModule, WorkerShardHealthProvider, } from "./developer-operations-module.js";
|
|
25
26
|
import { assertShardMessageSize, controlOperationGuildId, executeShardControlOperation, guildIdToShardId, SHARD_LEASE_HEARTBEAT_TIMEOUT_MS, shardIdentitySchema, supervisorMessageSchema, } from "./shard-contracts.js";
|
|
26
27
|
import { gatewayIntentsForModules, readBotVersion, } from "./index.js";
|
|
27
28
|
import { createSupportModule } from "./support-module.js";
|
|
29
|
+
import { createOAuthModule } from "./oauth-module.js";
|
|
30
|
+
import { HttpOAuthService } from "./oauth-service.js";
|
|
28
31
|
import { createLocalScheduledTaskRoute, } from "./scheduled-task-runner.js";
|
|
29
32
|
import { createLocalNumericWriteBehind } from "./numeric-write-behind.js";
|
|
30
33
|
import { WorkerNumericWriteBehindProxy } from "./numeric-write-behind-proxy.js";
|
|
@@ -111,7 +114,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
111
114
|
const modulePackages = await loadConfiguredModulePackages(moduleSpecifiers);
|
|
112
115
|
const featureModules = modulePackages.map(({ definition }) => definition);
|
|
113
116
|
client = new Client({
|
|
114
|
-
intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED),
|
|
117
|
+
intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED),
|
|
115
118
|
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
|
116
119
|
allowedMentions: { parse: [] },
|
|
117
120
|
shards: [identity.shardId],
|
|
@@ -130,10 +133,14 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
130
133
|
})
|
|
131
134
|
: undefined;
|
|
132
135
|
const supportModule = createSupportModule(cloudProxy);
|
|
136
|
+
const oauthModule = environment.HELYX_OAUTH_ENABLED
|
|
137
|
+
? createOAuthModule()
|
|
138
|
+
: undefined;
|
|
133
139
|
const publicModules = [
|
|
134
140
|
managementModule,
|
|
135
141
|
permissionsModule,
|
|
136
142
|
supportModule,
|
|
143
|
+
...(oauthModule ? [oauthModule] : []),
|
|
137
144
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
138
145
|
...featureModules,
|
|
139
146
|
];
|
|
@@ -152,6 +159,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
152
159
|
managementModule,
|
|
153
160
|
permissionsModule,
|
|
154
161
|
supportModule,
|
|
162
|
+
...(oauthModule ? [oauthModule] : []),
|
|
155
163
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
156
164
|
].map(({ manifest }) => manifest),
|
|
157
165
|
modules: featureModules.map(({ manifest }) => manifest),
|
|
@@ -182,6 +190,9 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
182
190
|
}));
|
|
183
191
|
platformServices.set(HELYX_SERVICE_NAMES.verificationMembers, new DiscordVerificationMemberService(client));
|
|
184
192
|
}
|
|
193
|
+
if (environment.HELYX_OAUTH_ENABLED &&
|
|
194
|
+
environment.HELYX_OAUTH_INTERNAL_SECRET)
|
|
195
|
+
platformServices.set(HELYX_SERVICE_NAMES.oauth, new HttpOAuthService(environment.HELYX_CLOUD_API_URL, environment.HELYX_OAUTH_INTERNAL_SECRET));
|
|
185
196
|
numericWriteBehind =
|
|
186
197
|
identity.scope === "coordinated"
|
|
187
198
|
? new WorkerNumericWriteBehindProxy(identity)
|
|
@@ -193,6 +204,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
193
204
|
platformServices.set(HELYX_SERVICE_NAMES.memberEnforcement, new DiscordMemberEnforcementService(client));
|
|
194
205
|
platformServices.set(HELYX_SERVICE_NAMES.memberRoles, new DiscordMemberRoleService(client));
|
|
195
206
|
platformServices.set(HELYX_SERVICE_NAMES.discordResources, new DiscordResourceInspection(client));
|
|
207
|
+
platformServices.set(HELYX_SERVICE_NAMES.discordGuildHealth, new DiscordGuildHealthInspection(client, publicModules, serviceAccess));
|
|
196
208
|
if (featureModules.some(({ manifest }) => manifest.id === "helyx.logging"))
|
|
197
209
|
platformServices.set(HELYX_SERVICE_NAMES.moduleLogging, new DiscordModuleLoggingService(client, database, publicModules, logger));
|
|
198
210
|
const scheduledTaskRoute = createLocalScheduledTaskRoute({
|
|
@@ -205,6 +217,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
205
217
|
managementModule.manifest.id,
|
|
206
218
|
permissionsModule.manifest.id,
|
|
207
219
|
supportModule.manifest.id,
|
|
220
|
+
...(oauthModule ? [oauthModule.manifest.id] : []),
|
|
208
221
|
...(cloudManagementModule ? [cloudManagementModule.manifest.id] : []),
|
|
209
222
|
...(operationsModule ? [operationsModule.manifest.id] : []),
|
|
210
223
|
]);
|
|
@@ -226,6 +239,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
226
239
|
? { operationsModuleId: operationsModule.manifest.id }
|
|
227
240
|
: {}),
|
|
228
241
|
supportModuleId: supportModule.manifest.id,
|
|
242
|
+
...(oauthModule ? { oauthModuleId: oauthModule.manifest.id } : {}),
|
|
229
243
|
}), logger, serviceAccess);
|
|
230
244
|
moduleEvents = installModuleEvents(client, publicModules, logger, serviceAccess);
|
|
231
245
|
const controlService = new BotControlService(database, publicModules, guildResources, serviceAccess, undefined, environment.DISCORD_INTERNAL_GUILD_ID &&
|
|
@@ -419,6 +433,7 @@ export function shardWorkerCommandRegistration(input) {
|
|
|
419
433
|
synchronise: false,
|
|
420
434
|
selfAuthorizingGuildModuleIds: new Set([
|
|
421
435
|
input.supportModuleId,
|
|
436
|
+
...(input.oauthModuleId ? [input.oauthModuleId] : []),
|
|
422
437
|
...(input.operationsModuleId ? [input.operationsModuleId] : []),
|
|
423
438
|
]),
|
|
424
439
|
};
|