@minesa-org/mini-interaction 0.4.4 → 0.4.5

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.
@@ -1,3 +1,4 @@
1
+ import { type DiscordUser, type OAuthTokens } from "../oauth/DiscordOAuth.js";
1
2
  type TimeoutConfig = {
2
3
  initialResponseTimeout?: number;
3
4
  autoDeferSlowOperations?: boolean;
@@ -14,6 +15,17 @@ export type MiniInteractionOptions = {
14
15
  publicKey?: string;
15
16
  applicationId?: string;
16
17
  token?: string;
18
+ guildId?: string;
19
+ };
20
+ type OAuthPageTemplate = {
21
+ htmlFile: string;
22
+ };
23
+ type OAuthCallbackTemplates = {
24
+ success: OAuthPageTemplate;
25
+ missingCode: OAuthPageTemplate;
26
+ oauthError: OAuthPageTemplate;
27
+ invalidState: OAuthPageTemplate;
28
+ serverError: OAuthPageTemplate;
17
29
  };
18
30
  type NodeRequest = {
19
31
  body?: unknown;
@@ -22,6 +34,7 @@ type NodeRequest = {
22
34
  get(name: string): string | null;
23
35
  };
24
36
  method?: string;
37
+ url?: string;
25
38
  [Symbol.asyncIterator]?: () => AsyncIterableIterator<Uint8Array>;
26
39
  on?: (event: string, listener: (...args: unknown[]) => void) => void;
27
40
  };
@@ -40,6 +53,22 @@ export declare class MiniInteraction {
40
53
  private loadedModulesPromise?;
41
54
  constructor(options?: MiniInteractionOptions);
42
55
  createNodeHandler(): (req: NodeRequest, res: NodeResponse) => Promise<void>;
56
+ registerCommands(tokenOverride?: string): Promise<unknown>;
57
+ discordOAuthVerificationPage(options: {
58
+ htmlFile: string;
59
+ scopes?: string[];
60
+ }): (req: NodeRequest, res: NodeResponse) => Promise<void>;
61
+ connectedOAuthPage(htmlFile: string): OAuthPageTemplate;
62
+ failedOAuthPage(htmlFile: string): OAuthPageTemplate;
63
+ discordOAuthCallback(options: {
64
+ templates: OAuthCallbackTemplates;
65
+ onAuthorize?: (payload: {
66
+ user: DiscordUser;
67
+ tokens: OAuthTokens;
68
+ req: NodeRequest;
69
+ res: NodeResponse;
70
+ }) => Promise<void> | void;
71
+ }): (req: NodeRequest, res: NodeResponse) => Promise<void>;
43
72
  private dispatch;
44
73
  private executeCommandHandler;
45
74
  private executeComponentHandler;
@@ -54,11 +83,17 @@ export declare class MiniInteraction {
54
83
  private isImportableModule;
55
84
  private isInteractionCommand;
56
85
  private getCommandName;
86
+ private resolveCommandPayload;
57
87
  private isCustomIdHandler;
58
88
  private looksLikeModalFile;
59
89
  private readRawBody;
60
90
  private getHeader;
61
91
  private sendJson;
92
+ private loadHtmlFile;
93
+ private renderOAuthTemplate;
94
+ private sendHtml;
95
+ private getOAuthConfig;
96
+ private getCookie;
62
97
  }
63
98
  export declare const LegacyMiniInteractionAdapter: typeof MiniInteraction;
64
99
  export {};
@@ -1,4 +1,4 @@
1
- import { readdir } from "node:fs/promises";
1
+ import { readFile, readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { ApplicationCommandType, InteractionResponseType, InteractionType, } from "discord-api-types/v10";
@@ -8,6 +8,7 @@ import { createMessageComponentInteraction } from "../utils/MessageComponentInte
8
8
  import { createModalSubmitInteraction } from "../utils/ModalSubmitInteraction.js";
9
9
  import { DiscordRestClient } from "../core/http/DiscordRestClient.js";
10
10
  import { verifyAndParseInteraction } from "../core/interactions/InteractionVerifier.js";
11
+ import { generateOAuthUrl, getDiscordUser, getOAuthTokens, } from "../oauth/DiscordOAuth.js";
11
12
  export class MiniInteraction {
12
13
  options;
13
14
  projectRoot;
@@ -71,6 +72,90 @@ export class MiniInteraction {
71
72
  }
72
73
  };
73
74
  }
75
+ async registerCommands(tokenOverride) {
76
+ const modules = await this.loadModules();
77
+ const payload = modules.commands.map((command) => this.resolveCommandPayload(command));
78
+ const applicationId = this.options.applicationId ??
79
+ process.env.DISCORD_APPLICATION_ID ??
80
+ process.env.DISCORD_APP_ID;
81
+ if (!applicationId) {
82
+ throw new Error("[MiniInteraction] Missing applicationId for command registration.");
83
+ }
84
+ const token = tokenOverride ??
85
+ this.options.token ??
86
+ process.env.DISCORD_BOT_TOKEN ??
87
+ process.env.DISCORD_TOKEN;
88
+ if (!token) {
89
+ throw new Error("[MiniInteraction] Missing bot token for command registration.");
90
+ }
91
+ const rest = new DiscordRestClient({ applicationId, token });
92
+ const guildId = this.options.guildId ?? process.env.DISCORD_GUILD_ID;
93
+ const route = guildId
94
+ ? `/applications/${applicationId}/guilds/${guildId}/commands`
95
+ : `/applications/${applicationId}/commands`;
96
+ if (this.options.debug) {
97
+ console.debug(`[MiniInteraction] Registering ${payload.length} command(s) on ${guildId ? `guild ${guildId}` : "global"} scope.`);
98
+ }
99
+ return rest.request(route, {
100
+ method: "PUT",
101
+ body: JSON.stringify(payload),
102
+ });
103
+ }
104
+ discordOAuthVerificationPage(options) {
105
+ return async (_req, res) => {
106
+ const oauthConfig = this.getOAuthConfig();
107
+ const { url, state } = generateOAuthUrl(oauthConfig, options.scopes ?? [
108
+ "applications.commands",
109
+ "identify",
110
+ "guilds",
111
+ "role_connections.write",
112
+ ]);
113
+ const html = await this.loadHtmlFile(options.htmlFile);
114
+ const rendered = html.replaceAll("{{OAUTH_URL_RAW}}", url);
115
+ res.setHeader?.("Set-Cookie", `mini_oauth_state=${encodeURIComponent(state)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=900`);
116
+ this.sendHtml(res, 200, rendered);
117
+ };
118
+ }
119
+ connectedOAuthPage(htmlFile) {
120
+ return { htmlFile };
121
+ }
122
+ failedOAuthPage(htmlFile) {
123
+ return { htmlFile };
124
+ }
125
+ discordOAuthCallback(options) {
126
+ return async (req, res) => {
127
+ try {
128
+ const requestUrl = new URL(req.url ?? "/", process.env.DISCORD_REDIRECT_URI ?? "http://localhost");
129
+ const error = requestUrl.searchParams.get("error");
130
+ const code = requestUrl.searchParams.get("code");
131
+ const state = requestUrl.searchParams.get("state");
132
+ const cookieState = this.getCookie(req, "mini_oauth_state");
133
+ if (error) {
134
+ await this.renderOAuthTemplate(res, options.templates.oauthError);
135
+ return;
136
+ }
137
+ if (!code) {
138
+ await this.renderOAuthTemplate(res, options.templates.missingCode);
139
+ return;
140
+ }
141
+ if (state && cookieState && state !== cookieState) {
142
+ await this.renderOAuthTemplate(res, options.templates.invalidState);
143
+ return;
144
+ }
145
+ const tokens = await getOAuthTokens(code, this.getOAuthConfig());
146
+ const user = await getDiscordUser(tokens.access_token);
147
+ await options.onAuthorize?.({ user, tokens, req, res });
148
+ res.setHeader?.("Set-Cookie", "mini_oauth_state=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");
149
+ await this.renderOAuthTemplate(res, options.templates.success);
150
+ }
151
+ catch (error) {
152
+ if (this.options.debug) {
153
+ console.error("[MiniInteraction] discordOAuthCallback failed", error);
154
+ }
155
+ await this.renderOAuthTemplate(res, options.templates.serverError);
156
+ }
157
+ };
158
+ }
74
159
  async dispatch(interaction) {
75
160
  const modules = await this.loadModules();
76
161
  if (interaction.type === InteractionType.ApplicationCommand) {
@@ -271,6 +356,13 @@ export class MiniInteraction {
271
356
  }
272
357
  return data.name;
273
358
  }
359
+ resolveCommandPayload(command) {
360
+ const data = command.data;
361
+ if (typeof data.toJSON === "function") {
362
+ return data.toJSON();
363
+ }
364
+ return command.data;
365
+ }
274
366
  isCustomIdHandler(value) {
275
367
  return (typeof value === "object" &&
276
368
  value !== null &&
@@ -332,5 +424,44 @@ export class MiniInteraction {
332
424
  res.setHeader?.("Content-Type", "application/json; charset=utf-8");
333
425
  res.end(JSON.stringify(body));
334
426
  }
427
+ async loadHtmlFile(htmlFile) {
428
+ const absolutePath = path.resolve(this.projectRoot, htmlFile);
429
+ return readFile(absolutePath, "utf8");
430
+ }
431
+ async renderOAuthTemplate(res, template) {
432
+ const html = await this.loadHtmlFile(template.htmlFile);
433
+ this.sendHtml(res, 200, html);
434
+ }
435
+ sendHtml(res, statusCode, html) {
436
+ if (typeof res.status === "function" && typeof res.end === "function") {
437
+ res.status(statusCode);
438
+ }
439
+ res.statusCode = statusCode;
440
+ res.setHeader?.("Content-Type", "text/html; charset=utf-8");
441
+ res.end(html);
442
+ }
443
+ getOAuthConfig() {
444
+ const appId = this.options.applicationId ??
445
+ process.env.DISCORD_APPLICATION_ID ??
446
+ process.env.DISCORD_APP_ID;
447
+ const appSecret = process.env.DISCORD_CLIENT_SECRET ?? process.env.DISCORD_APPLICATION_SECRET;
448
+ const redirectUri = process.env.DISCORD_REDIRECT_URI;
449
+ if (!appId || !appSecret || !redirectUri) {
450
+ throw new Error("[MiniInteraction] Missing OAuth config. Expected DISCORD_APPLICATION_ID, DISCORD_CLIENT_SECRET and DISCORD_REDIRECT_URI.");
451
+ }
452
+ return { appId, appSecret, redirectUri };
453
+ }
454
+ getCookie(req, name) {
455
+ const cookieHeader = this.getHeader(req.headers, "cookie");
456
+ if (!cookieHeader)
457
+ return undefined;
458
+ for (const rawPart of cookieHeader.split(";")) {
459
+ const [rawKey, ...rawValue] = rawPart.trim().split("=");
460
+ if (rawKey === name) {
461
+ return decodeURIComponent(rawValue.join("="));
462
+ }
463
+ }
464
+ return undefined;
465
+ }
335
466
  }
336
467
  export const LegacyMiniInteractionAdapter = MiniInteraction;
@@ -4,7 +4,7 @@ import type { APICheckboxComponent } from "./checkbox.js";
4
4
  /** Defines a component structure for use in ActionRow builders. */
5
5
  export type ActionRowComponent = APIComponentInActionRow | APIRadioComponent | APICheckboxComponent;
6
6
  /** Defines a message component structure for use in message builders. */
7
- export type MessageActionRowComponent = APIComponentInMessageActionRow | APIRadioComponent | APICheckboxComponent;
7
+ export type MessageActionRowComponent = APIComponentInMessageActionRow;
8
8
  /** Structure for an action row containing mini-interaction components. */
9
9
  export interface MiniActionRow<T extends ActionRowComponent = ActionRowComponent> {
10
10
  type: 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Mini interaction, connecting your app with Discord via HTTP-interaction (Vercel support).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",