@minesa-org/mini-interaction 0.1.6 → 0.1.9

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/README.md CHANGED
@@ -1,7 +1,3 @@
1
1
  # Mini Interaction
2
2
 
3
3
  Mini interaction, connecting your app with Discord via HTTP-interaction (Vercel support).
4
-
5
- - Read the [Discord OAuth Linked Roles setup guide](docs/discord-oauth-setup.md)
6
- for a step-by-step walkthrough that uses `mini.discordOAuthCallback()` and a
7
- MongoDB connection configured via `MONGODB_URI`.
@@ -113,6 +113,23 @@ export type DiscordOAuthCallbackOptions = {
113
113
  successRedirect?: string | ((context: DiscordOAuthAuthorizeContext) => string | null | undefined);
114
114
  templates?: Partial<DiscordOAuthCallbackTemplates>;
115
115
  };
116
+ /** Options accepted by {@link MiniInteraction.discordOAuthVerificationPage}. */
117
+ export type DiscordOAuthVerificationPageOptions = {
118
+ oauth?: OAuthConfig;
119
+ scopes?: string[];
120
+ /**
121
+ * Path to the HTML file to load. Relative paths resolve from {@link process.cwd}.
122
+ *
123
+ * @defaultValue "index.html"
124
+ */
125
+ htmlFile?: string;
126
+ /**
127
+ * Placeholder token (with or without the `{{ }}` wrapper) that will be replaced with the generated OAuth URL.
128
+ *
129
+ * @defaultValue "OAUTH_URL"
130
+ */
131
+ placeholder?: string;
132
+ };
116
133
  /**
117
134
  * Minimal interface describing a function capable of verifying Discord interaction signatures.
118
135
  */
@@ -123,7 +140,6 @@ type VerifyKeyFunction = (message: string | Uint8Array, signature: string, times
123
140
  export declare class MiniInteraction {
124
141
  readonly applicationId: string;
125
142
  readonly publicKey: string;
126
- private readonly baseUrl;
127
143
  private readonly fetchImpl;
128
144
  private readonly verifyKeyImpl;
129
145
  private readonly commandsDirectory;
@@ -213,25 +229,32 @@ export declare class MiniInteraction {
213
229
  */
214
230
  handleRequest(request: MiniInteractionRequest): Promise<MiniInteractionHandlerResult>;
215
231
  /**
216
- * Creates a Node.js style request handler that validates and processes interactions.
232
+ * Creates a Node.js style request handler compatible with Express, Next.js API routes,
233
+ * Vercel serverless functions, and any runtime that expects a `(req, res)` listener.
217
234
  */
218
235
  createNodeHandler(): MiniInteractionNodeHandler;
219
236
  /**
220
- * Alias for {@link createNodeHandler} for frameworks expecting a listener function.
221
- */
222
- createNodeListener(): MiniInteractionNodeHandler;
223
- /**
224
- * Convenience alias for {@link createNodeHandler} tailored to Vercel serverless functions.
237
+ * Generates a lightweight verification handler that serves an HTML page with an embedded OAuth link.
238
+ *
239
+ * This is primarily used when Discord asks for a verification URL while setting up Linked Roles.
240
+ * Provide an HTML file that contains the `{{OAUTH_URL}}` placeholder (or a custom placeholder defined in options)
241
+ * and this helper will replace the token with a freshly generated OAuth link on every request.
242
+ *
243
+ * Available placeholders:
244
+ * - `{{OAUTH_URL}}` - HTML-escaped OAuth URL for links/buttons.
245
+ * - `{{OAUTH_URL_RAW}}` - raw OAuth URL, ideal for `<script>` usage.
246
+ * - `{{OAUTH_STATE}}` - HTML-escaped OAuth state value.
247
+ * - Custom placeholder name (from {@link DiscordOAuthVerificationPageOptions.placeholder}) and its `_RAW` variant.
225
248
  */
226
- createVercelHandler(): MiniInteractionNodeHandler;
249
+ discordOAuthVerificationPage(options?: DiscordOAuthVerificationPageOptions): MiniInteractionNodeHandler;
227
250
  /**
228
251
  * Loads an HTML file and returns a success template that replaces useful placeholders.
229
252
  *
230
253
  * The following placeholders are available in the HTML file:
231
254
  * - `{{username}}`, `{{discriminator}}`, `{{user_id}}`, `{{user_tag}}`
232
255
  * - `{{access_token}}`, `{{refresh_token}}`, `{{token_type}}`, `{{scope}}`, `{{expires_at}}`
233
- * - `{{state}}`
234
- */
256
+ * - `{{state}}`
257
+ */
235
258
  connectedOAuthPage(filePath: string): DiscordOAuthCallbackTemplates["success"];
236
259
  /**
237
260
  * Loads an HTML file and returns an error template that can be reused for all failure cases.
@@ -247,8 +270,12 @@ export declare class MiniInteraction {
247
270
  private loadHtmlTemplate;
248
271
  /**
249
272
  * Replaces placeholder tokens in a template with escaped HTML values.
250
- */
273
+ */
251
274
  private renderHtmlTemplate;
275
+ /**
276
+ * Normalizes placeholder tokens to the bare key the HTML renderer expects.
277
+ */
278
+ private normalizeTemplateKey;
252
279
  /**
253
280
  * Creates a minimal Discord OAuth callback handler that renders helpful HTML responses.
254
281
  *
@@ -9,7 +9,7 @@ import { createCommandInteraction } from "../utils/CommandInteractionOptions.js"
9
9
  import { createMessageComponentInteraction, } from "../utils/MessageComponentInteraction.js";
10
10
  import { createModalSubmitInteraction, } from "../utils/ModalSubmitInteraction.js";
11
11
  import { createUserContextMenuInteraction, createMessageContextMenuInteraction, } from "../utils/ContextMenuInteraction.js";
12
- import { getOAuthTokens, getDiscordUser, } from "../oauth/DiscordOAuth.js";
12
+ import { generateOAuthUrl, getOAuthTokens, getDiscordUser, } from "../oauth/DiscordOAuth.js";
13
13
  /** File extensions that are treated as loadable modules when auto-loading. */
14
14
  const SUPPORTED_MODULE_EXTENSIONS = new Set([
15
15
  ".js",
@@ -25,7 +25,6 @@ const SUPPORTED_MODULE_EXTENSIONS = new Set([
25
25
  export class MiniInteraction {
26
26
  applicationId;
27
27
  publicKey;
28
- baseUrl;
29
28
  fetchImpl;
30
29
  verifyKeyImpl;
31
30
  commandsDirectory;
@@ -54,7 +53,6 @@ export class MiniInteraction {
54
53
  }
55
54
  this.applicationId = applicationId;
56
55
  this.publicKey = publicKey;
57
- this.baseUrl = DISCORD_BASE_URL;
58
56
  this.fetchImpl = fetchImpl;
59
57
  this.verifyKeyImpl = verifyKeyImplementation ?? verifyKey;
60
58
  this.commandsDirectory =
@@ -271,7 +269,7 @@ export class MiniInteraction {
271
269
  if (!Array.isArray(resolvedCommands) || resolvedCommands.length === 0) {
272
270
  throw new Error("[MiniInteraction] commands must be a non-empty array payload");
273
271
  }
274
- const url = `${this.baseUrl}/applications/${this.applicationId}/commands`;
272
+ const url = `${DISCORD_BASE_URL}/applications/${this.applicationId}/commands`;
275
273
  const response = await this.fetchImpl(url, {
276
274
  method: "PUT",
277
275
  headers: {
@@ -299,7 +297,7 @@ export class MiniInteraction {
299
297
  if (!Array.isArray(metadata) || metadata.length === 0) {
300
298
  throw new Error("[MiniInteraction] metadata must be a non-empty array payload");
301
299
  }
302
- const url = `${this.baseUrl}/applications/${this.applicationId}/role-connections/metadata`;
300
+ const url = `${DISCORD_BASE_URL}/applications/${this.applicationId}/role-connections/metadata`;
303
301
  const response = await this.fetchImpl(url, {
304
302
  method: "PUT",
305
303
  headers: {
@@ -372,7 +370,8 @@ export class MiniInteraction {
372
370
  };
373
371
  }
374
372
  /**
375
- * Creates a Node.js style request handler that validates and processes interactions.
373
+ * Creates a Node.js style request handler compatible with Express, Next.js API routes,
374
+ * Vercel serverless functions, and any runtime that expects a `(req, res)` listener.
376
375
  */
377
376
  createNodeHandler() {
378
377
  return (request, response) => {
@@ -426,16 +425,49 @@ export class MiniInteraction {
426
425
  };
427
426
  }
428
427
  /**
429
- * Alias for {@link createNodeHandler} for frameworks expecting a listener function.
430
- */
431
- createNodeListener() {
432
- return this.createNodeHandler();
433
- }
434
- /**
435
- * Convenience alias for {@link createNodeHandler} tailored to Vercel serverless functions.
428
+ * Generates a lightweight verification handler that serves an HTML page with an embedded OAuth link.
429
+ *
430
+ * This is primarily used when Discord asks for a verification URL while setting up Linked Roles.
431
+ * Provide an HTML file that contains the `{{OAUTH_URL}}` placeholder (or a custom placeholder defined in options)
432
+ * and this helper will replace the token with a freshly generated OAuth link on every request.
433
+ *
434
+ * Available placeholders:
435
+ * - `{{OAUTH_URL}}` - HTML-escaped OAuth URL for links/buttons.
436
+ * - `{{OAUTH_URL_RAW}}` - raw OAuth URL, ideal for `<script>` usage.
437
+ * - `{{OAUTH_STATE}}` - HTML-escaped OAuth state value.
438
+ * - Custom placeholder name (from {@link DiscordOAuthVerificationPageOptions.placeholder}) and its `_RAW` variant.
436
439
  */
437
- createVercelHandler() {
438
- return this.createNodeHandler();
440
+ discordOAuthVerificationPage(options = {}) {
441
+ const scopes = options.scopes ?? ["identify", "role_connections.write"];
442
+ const htmlFile = options.htmlFile ?? "index.html";
443
+ const placeholderKey = this.normalizeTemplateKey(options.placeholder ?? "OAUTH_URL");
444
+ const template = this.loadHtmlTemplate(htmlFile);
445
+ const oauthConfig = resolveOAuthConfig(options.oauth);
446
+ return (_request, response) => {
447
+ try {
448
+ const { url, state } = generateOAuthUrl(oauthConfig, scopes);
449
+ const values = {
450
+ OAUTH_URL: url,
451
+ OAUTH_URL_RAW: url,
452
+ OAUTH_STATE: state,
453
+ };
454
+ const rawKeys = new Set(["OAUTH_URL_RAW"]);
455
+ if (placeholderKey !== "OAUTH_URL") {
456
+ values[placeholderKey] = url;
457
+ const rawVariant = `${placeholderKey}_RAW`;
458
+ values[rawVariant] = url;
459
+ rawKeys.add(rawVariant);
460
+ }
461
+ const html = this.renderHtmlTemplate(template, values, {
462
+ rawKeys,
463
+ });
464
+ sendHtml(response, html);
465
+ }
466
+ catch (error) {
467
+ console.error("[MiniInteraction] Failed to render OAuth verification page:", error);
468
+ sendHtml(response, DEFAULT_VERIFICATION_ERROR_HTML, 500);
469
+ }
470
+ };
439
471
  }
440
472
  /**
441
473
  * Loads an HTML file and returns a success template that replaces useful placeholders.
@@ -443,8 +475,8 @@ export class MiniInteraction {
443
475
  * The following placeholders are available in the HTML file:
444
476
  * - `{{username}}`, `{{discriminator}}`, `{{user_id}}`, `{{user_tag}}`
445
477
  * - `{{access_token}}`, `{{refresh_token}}`, `{{token_type}}`, `{{scope}}`, `{{expires_at}}`
446
- * - `{{state}}`
447
- */
478
+ * - `{{state}}`
479
+ */
448
480
  connectedOAuthPage(filePath) {
449
481
  const template = this.loadHtmlTemplate(filePath);
450
482
  return ({ user, tokens, state }) => {
@@ -501,16 +533,34 @@ export class MiniInteraction {
501
533
  }
502
534
  /**
503
535
  * Replaces placeholder tokens in a template with escaped HTML values.
504
- */
505
- renderHtmlTemplate(template, values) {
536
+ */
537
+ renderHtmlTemplate(template, values, options) {
538
+ const rawKeys = options?.rawKeys instanceof Set
539
+ ? options.rawKeys
540
+ : new Set(options?.rawKeys ?? []);
506
541
  return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => {
507
542
  const value = values[key];
508
543
  if (value === undefined || value === null) {
509
544
  return "";
510
545
  }
511
- return escapeHtml(String(value));
546
+ const stringValue = String(value);
547
+ return rawKeys.has(key) ? stringValue : escapeHtml(stringValue);
512
548
  });
513
549
  }
550
+ /**
551
+ * Normalizes placeholder tokens to the bare key the HTML renderer expects.
552
+ */
553
+ normalizeTemplateKey(token) {
554
+ if (!token) {
555
+ return "OAUTH_URL";
556
+ }
557
+ const trimmed = token.trim();
558
+ const match = trimmed.match(/^\{\{\s*(\w+)\s*\}\}$/);
559
+ if (match) {
560
+ return match[1];
561
+ }
562
+ return trimmed || "OAUTH_URL";
563
+ }
514
564
  /**
515
565
  * Creates a minimal Discord OAuth callback handler that renders helpful HTML responses.
516
566
  *
@@ -1224,6 +1274,22 @@ const DEFAULT_DISCORD_OAUTH_TEMPLATES = {
1224
1274
  </body>
1225
1275
  </html>`,
1226
1276
  };
1277
+ const DEFAULT_VERIFICATION_ERROR_HTML = `<!DOCTYPE html>
1278
+ <html>
1279
+ <head>
1280
+ <meta charset="utf-8" />
1281
+ <title>Server Error</title>
1282
+ <style>
1283
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; text-align: center; }
1284
+ .error { color: #d32f2f; background: #ffebee; padding: 15px; border-radius: 5px; display: inline-block; }
1285
+ </style>
1286
+ </head>
1287
+ <body>
1288
+ <div class="error">
1289
+ <p>We were unable to load the Discord verification page. Please try again later.</p>
1290
+ </div>
1291
+ </body>
1292
+ </html>`;
1227
1293
  function sendHtml(response, body, statusCode = 200) {
1228
1294
  if (response.headersSent || response.writableEnded) {
1229
1295
  return;
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export type { AttachmentOptionBuilder, ChannelOptionBuilder, MentionableOptionBu
7
7
  export { CommandInteractionOptionResolver, createCommandInteraction, } from "./utils/CommandInteractionOptions.js";
8
8
  export type { CommandInteraction, MentionableOption, ResolvedUserOption, } from "./utils/CommandInteractionOptions.js";
9
9
  export type { UserContextMenuInteraction, MessageContextMenuInteraction, } from "./utils/ContextMenuInteraction.js";
10
- export type { MiniInteractionFetchHandler, MiniInteractionNodeHandler, MiniInteractionHandlerResult, MiniInteractionRequest, MiniInteractionOptions, DiscordOAuthAuthorizeContext, DiscordOAuthCallbackOptions, DiscordOAuthCallbackTemplates, DiscordOAuthErrorTemplateContext, DiscordOAuthServerErrorTemplateContext, DiscordOAuthStateTemplateContext, DiscordOAuthSuccessTemplateContext, } from "./clients/MiniInteraction.js";
10
+ export type { MiniInteractionFetchHandler, MiniInteractionNodeHandler, MiniInteractionHandlerResult, MiniInteractionRequest, MiniInteractionOptions, DiscordOAuthAuthorizeContext, DiscordOAuthCallbackOptions, DiscordOAuthCallbackTemplates, DiscordOAuthErrorTemplateContext, DiscordOAuthServerErrorTemplateContext, DiscordOAuthStateTemplateContext, DiscordOAuthSuccessTemplateContext, DiscordOAuthVerificationPageOptions, } from "./clients/MiniInteraction.js";
11
11
  export type { MiniInteractionCommand, SlashCommandHandler, UserCommandHandler, MessageCommandHandler, CommandHandler, } from "./types/Commands.js";
12
12
  export type { MiniInteractionComponent, MiniInteractionButtonHandler, MiniInteractionStringSelectHandler, MiniInteractionRoleSelectHandler, MiniInteractionUserSelectHandler, MiniInteractionChannelSelectHandler, MiniInteractionMentionableSelectHandler, MiniInteractionComponentHandler, MiniInteractionModal, MiniInteractionModalHandler, MiniInteractionHandler, } from "./clients/MiniInteraction.js";
13
13
  export type { MessageComponentInteraction, ButtonInteraction, StringSelectInteraction, RoleSelectInteraction, UserSelectInteraction, ChannelSelectInteraction, MentionableSelectInteraction, ResolvedUserOption as ComponentResolvedUserOption, ResolvedMentionableOption as ComponentResolvedMentionableOption, } from "./utils/MessageComponentInteraction.js";
@@ -1,16 +1,2 @@
1
- import "dotenv/config";
2
- /** Discord application's public key used for request signature verification. */
3
- declare const DISCORD_APP_PUBLIC_KEY: string;
4
- /** Discord application identifier used for REST requests. */
5
- declare const DISCORD_APPLICATION_ID: string;
6
- /** Bot token used when registering commands against Discord's API. */
7
- declare const DISCORD_BOT_TOKEN: string;
8
- /** Guild identifier used for guild-scoped command registration. */
9
- declare const DISCORD_GUILD_ID: string;
10
- /** Whether commands should be registered globally instead of per guild. */
11
- declare const DISCORD_GLOBAL: boolean;
12
- /** Local development port for the example interaction server. */
13
- declare const DISCORD_APP_PORT: string;
14
1
  /** Discord REST API base URL used for all network requests. */
15
- declare const DISCORD_BASE_URL = "https://discord.com/api/v10";
16
- export { DISCORD_APPLICATION_ID, DISCORD_APP_PORT, DISCORD_APP_PUBLIC_KEY, DISCORD_BASE_URL, DISCORD_BOT_TOKEN, DISCORD_GLOBAL, DISCORD_GUILD_ID, };
2
+ export declare const DISCORD_BASE_URL: "https://discord.com/api/v10";
@@ -1,16 +1,2 @@
1
- import "dotenv/config";
2
- /** Discord application's public key used for request signature verification. */
3
- const DISCORD_APP_PUBLIC_KEY = process.env.DISCORD_APP_PUBLIC_KEY;
4
- /** Discord application identifier used for REST requests. */
5
- const DISCORD_APPLICATION_ID = process.env.DISCORD_APPLICATION_ID;
6
- /** Bot token used when registering commands against Discord's API. */
7
- const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
8
- /** Guild identifier used for guild-scoped command registration. */
9
- const DISCORD_GUILD_ID = process.env.DISCORD_GUILD_ID;
10
- /** Whether commands should be registered globally instead of per guild. */
11
- const DISCORD_GLOBAL = (process.env.DISCORD_GLOBAL ?? "false").toLowerCase() === "true";
12
- /** Local development port for the example interaction server. */
13
- const DISCORD_APP_PORT = process.env.DISCORD_PORT;
14
1
  /** Discord REST API base URL used for all network requests. */
15
- const DISCORD_BASE_URL = "https://discord.com/api/v10";
16
- export { DISCORD_APPLICATION_ID, DISCORD_APP_PORT, DISCORD_APP_PUBLIC_KEY, DISCORD_BASE_URL, DISCORD_BOT_TOKEN, DISCORD_GLOBAL, DISCORD_GUILD_ID, };
2
+ export const DISCORD_BASE_URL = "https://discord.com/api/v10";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.1.6",
3
+ "version": "0.1.9",
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",
@@ -41,8 +41,7 @@
41
41
  "homepage": "https://github.com/minesa-org/mini-interaction#readme",
42
42
  "dependencies": {
43
43
  "discord-api-types": "^0.38.32",
44
- "discord-interactions": "^4.4.0",
45
- "dotenv": "^17.2.3"
44
+ "discord-interactions": "^4.4.0"
46
45
  },
47
46
  "devDependencies": {
48
47
  "@types/node": "^24.10.0",