mini-interactions-cloudflare 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,91 @@
1
+ import { InteractionHandler } from 'mini-interactions-core';
2
+
3
+ declare enum InteractionType {
4
+ Ping = 1,
5
+ ApplicationCommand = 2,
6
+ MessageComponent = 3,
7
+ ApplicationCommandAutocomplete = 4,
8
+ ModalSubmit = 5
9
+ }
10
+ declare enum InteractionResponseType {
11
+ Pong = 1,
12
+ ChannelMessageWithSource = 4,
13
+ DeferredChannelMessageWithSource = 5,
14
+ DeferredUpdateMessage = 6,
15
+ UpdateMessage = 7,
16
+ ApplicationCommandAutocompleteResult = 8,
17
+ Modal = 9
18
+ }
19
+ interface DiscordInteraction<TData = Record<string, unknown>> {
20
+ id?: string;
21
+ application_id?: string;
22
+ type: InteractionType | number;
23
+ token?: string;
24
+ version?: number;
25
+ data?: TData;
26
+ guild_id?: string;
27
+ channel_id?: string;
28
+ member?: unknown;
29
+ user?: unknown;
30
+ message?: unknown;
31
+ [key: string]: unknown;
32
+ }
33
+ interface DiscordInteractionResponse {
34
+ type: InteractionResponseType | number;
35
+ data?: unknown;
36
+ }
37
+ interface MessageComponentInteractionData extends Record<string, unknown> {
38
+ custom_id: string;
39
+ component_type: number;
40
+ values?: string[];
41
+ resolved?: unknown;
42
+ }
43
+ interface ModalSubmitInteractionData extends Record<string, unknown> {
44
+ custom_id: string;
45
+ components: unknown[];
46
+ resolved?: unknown;
47
+ }
48
+ interface CloudflareComponentContext<TData extends Record<string, unknown>> {
49
+ request: Request;
50
+ interaction: DiscordInteraction<TData>;
51
+ customId: string;
52
+ }
53
+ interface CloudflareButtonContext extends CloudflareComponentContext<MessageComponentInteractionData> {
54
+ componentType: 2;
55
+ }
56
+ interface CloudflareSelectMenuContext extends CloudflareComponentContext<MessageComponentInteractionData> {
57
+ componentType: 3 | 5 | 6 | 7 | 8;
58
+ values: readonly string[];
59
+ resolved?: unknown;
60
+ }
61
+ type CloudflareModalValue = string | boolean | readonly string[] | null;
62
+ interface CloudflareModalSubmitContext extends CloudflareComponentContext<ModalSubmitInteractionData> {
63
+ values: Readonly<Record<string, CloudflareModalValue>>;
64
+ resolved?: unknown;
65
+ }
66
+ type CloudflareInteractionResult = Response | DiscordInteractionResponse;
67
+ type CloudflareComponentHandler<TContext> = (context: TContext) => CloudflareInteractionResult | Promise<CloudflareInteractionResult>;
68
+ interface CloudflareComponentHandlers {
69
+ button?: CloudflareComponentHandler<CloudflareButtonContext>;
70
+ selectMenu?: CloudflareComponentHandler<CloudflareSelectMenuContext>;
71
+ modalSubmit?: CloudflareComponentHandler<CloudflareModalSubmitContext>;
72
+ }
73
+ interface CloudflareHandlerOptions {
74
+ /** Low-level fallback for commands, unmatched interactions, or non-interaction requests. */
75
+ handler?: InteractionHandler;
76
+ /** Discord application public key. When provided, every interaction request is verified. */
77
+ publicKey?: string;
78
+ handlers?: CloudflareComponentHandlers;
79
+ }
80
+ /** Verify Discord's Ed25519 signature using the Web Crypto API available in Workers. */
81
+ declare function verifyDiscordRequest(request: Request, publicKey: string, body?: string): Promise<boolean>;
82
+ /**
83
+ * Create a Cloudflare Worker-compatible Discord interactions handler.
84
+ *
85
+ * With only `handler`, this remains a transparent Request adapter. Supplying
86
+ * `handlers` enables parsing and routing for buttons, all select menus, and
87
+ * modal submissions. `publicKey` enables Discord signature verification.
88
+ */
89
+ declare function createCloudflareHandler(options: CloudflareHandlerOptions): InteractionHandler;
90
+
91
+ export { type CloudflareButtonContext, type CloudflareComponentContext, type CloudflareComponentHandler, type CloudflareComponentHandlers, type CloudflareHandlerOptions, type CloudflareInteractionResult, type CloudflareModalSubmitContext, type CloudflareModalValue, type CloudflareSelectMenuContext, type DiscordInteraction, type DiscordInteractionResponse, InteractionResponseType, InteractionType, type MessageComponentInteractionData, type ModalSubmitInteractionData, createCloudflareHandler, verifyDiscordRequest };
package/dist/index.js ADDED
@@ -0,0 +1,148 @@
1
+ // src/index.ts
2
+ var InteractionType = /* @__PURE__ */ ((InteractionType2) => {
3
+ InteractionType2[InteractionType2["Ping"] = 1] = "Ping";
4
+ InteractionType2[InteractionType2["ApplicationCommand"] = 2] = "ApplicationCommand";
5
+ InteractionType2[InteractionType2["MessageComponent"] = 3] = "MessageComponent";
6
+ InteractionType2[InteractionType2["ApplicationCommandAutocomplete"] = 4] = "ApplicationCommandAutocomplete";
7
+ InteractionType2[InteractionType2["ModalSubmit"] = 5] = "ModalSubmit";
8
+ return InteractionType2;
9
+ })(InteractionType || {});
10
+ var InteractionResponseType = /* @__PURE__ */ ((InteractionResponseType2) => {
11
+ InteractionResponseType2[InteractionResponseType2["Pong"] = 1] = "Pong";
12
+ InteractionResponseType2[InteractionResponseType2["ChannelMessageWithSource"] = 4] = "ChannelMessageWithSource";
13
+ InteractionResponseType2[InteractionResponseType2["DeferredChannelMessageWithSource"] = 5] = "DeferredChannelMessageWithSource";
14
+ InteractionResponseType2[InteractionResponseType2["DeferredUpdateMessage"] = 6] = "DeferredUpdateMessage";
15
+ InteractionResponseType2[InteractionResponseType2["UpdateMessage"] = 7] = "UpdateMessage";
16
+ InteractionResponseType2[InteractionResponseType2["ApplicationCommandAutocompleteResult"] = 8] = "ApplicationCommandAutocompleteResult";
17
+ InteractionResponseType2[InteractionResponseType2["Modal"] = 9] = "Modal";
18
+ return InteractionResponseType2;
19
+ })(InteractionResponseType || {});
20
+ function hexToBytes(hex, field) {
21
+ if (hex.length === 0 || hex.length % 2 !== 0 || !/^[\da-f]+$/i.test(hex)) {
22
+ throw new TypeError(`${field} must be a hexadecimal string`);
23
+ }
24
+ const bytes = new Uint8Array(new ArrayBuffer(hex.length / 2));
25
+ for (let index = 0; index < bytes.length; index += 1) {
26
+ bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
27
+ }
28
+ return bytes;
29
+ }
30
+ async function verifyDiscordRequest(request, publicKey, body) {
31
+ const signature = request.headers.get("X-Signature-Ed25519");
32
+ const timestamp = request.headers.get("X-Signature-Timestamp");
33
+ if (signature === null || timestamp === null) return false;
34
+ try {
35
+ const key = await crypto.subtle.importKey(
36
+ "raw",
37
+ hexToBytes(publicKey, "publicKey"),
38
+ { name: "Ed25519" },
39
+ false,
40
+ ["verify"]
41
+ );
42
+ const payload = new TextEncoder().encode(timestamp + (body ?? await request.clone().text()));
43
+ return await crypto.subtle.verify("Ed25519", key, hexToBytes(signature, "signature"), payload);
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+ function jsonResponse(value, status = 200) {
49
+ return new Response(JSON.stringify(value), {
50
+ status,
51
+ headers: { "Content-Type": "application/json; charset=utf-8" }
52
+ });
53
+ }
54
+ async function resolveResult(result) {
55
+ const resolved = await result;
56
+ return resolved instanceof Response ? resolved : jsonResponse(resolved);
57
+ }
58
+ function collectModalValues(components) {
59
+ const values = {};
60
+ const visit = (component) => {
61
+ if (typeof component !== "object" || component === null) return;
62
+ const entry = component;
63
+ if (typeof entry.custom_id === "string") {
64
+ if (typeof entry.value === "string" || typeof entry.value === "boolean" || entry.value === null) {
65
+ values[entry.custom_id] = entry.value;
66
+ } else if (Array.isArray(entry.values) && entry.values.every((value) => typeof value === "string")) {
67
+ values[entry.custom_id] = Object.freeze([...entry.values]);
68
+ }
69
+ }
70
+ if (Array.isArray(entry.components)) entry.components.forEach(visit);
71
+ if (entry.component !== void 0) visit(entry.component);
72
+ };
73
+ components.forEach(visit);
74
+ return Object.freeze(values);
75
+ }
76
+ function isRecord(value) {
77
+ return typeof value === "object" && value !== null && !Array.isArray(value);
78
+ }
79
+ function createCloudflareHandler(options) {
80
+ if (!options || typeof options.handler !== "function" && options.handlers === void 0) {
81
+ throw new TypeError("handler or handlers must be provided");
82
+ }
83
+ if (options.handler !== void 0 && typeof options.handler !== "function") {
84
+ throw new TypeError("handler must be a function");
85
+ }
86
+ if (options.publicKey !== void 0) hexToBytes(options.publicKey, "publicKey");
87
+ if (options.handlers === void 0 && options.publicKey === void 0) {
88
+ return (request) => options.handler(request);
89
+ }
90
+ return async (request) => {
91
+ if (request.method !== "POST") {
92
+ return options.handler?.(request) ?? new Response("Method Not Allowed", { status: 405 });
93
+ }
94
+ const body = await request.clone().text();
95
+ if (options.publicKey !== void 0 && !await verifyDiscordRequest(request, options.publicKey, body)) {
96
+ return new Response("Invalid request signature", { status: 401 });
97
+ }
98
+ let interaction;
99
+ try {
100
+ const parsed = JSON.parse(body);
101
+ if (!isRecord(parsed) || typeof parsed.type !== "number") throw new TypeError("invalid interaction");
102
+ interaction = parsed;
103
+ } catch {
104
+ return new Response("Invalid interaction payload", { status: 400 });
105
+ }
106
+ if (interaction.type === 1 /* Ping */) {
107
+ return jsonResponse({ type: 1 /* Pong */ });
108
+ }
109
+ if (interaction.type === 3 /* MessageComponent */ && isRecord(interaction.data)) {
110
+ const data = interaction.data;
111
+ if (typeof data.custom_id === "string" && data.component_type === 2 && options.handlers?.button) {
112
+ return resolveResult(options.handlers.button({
113
+ request,
114
+ interaction,
115
+ customId: data.custom_id,
116
+ componentType: 2
117
+ }));
118
+ }
119
+ if (typeof data.custom_id === "string" && (data.component_type === 3 || data.component_type === 5 || data.component_type === 6 || data.component_type === 7 || data.component_type === 8) && options.handlers?.selectMenu) {
120
+ return resolveResult(options.handlers.selectMenu({
121
+ request,
122
+ interaction,
123
+ customId: data.custom_id,
124
+ componentType: data.component_type,
125
+ values: Object.freeze([...data.values ?? []]),
126
+ ...data.resolved === void 0 ? {} : { resolved: data.resolved }
127
+ }));
128
+ }
129
+ }
130
+ if (interaction.type === 5 /* ModalSubmit */ && isRecord(interaction.data)) {
131
+ const data = interaction.data;
132
+ if (typeof data.custom_id === "string" && Array.isArray(data.components) && options.handlers?.modalSubmit) {
133
+ return resolveResult(options.handlers.modalSubmit({
134
+ request,
135
+ interaction,
136
+ customId: data.custom_id,
137
+ values: collectModalValues(data.components),
138
+ ...data.resolved === void 0 ? {} : { resolved: data.resolved }
139
+ }));
140
+ }
141
+ }
142
+ return options.handler?.(request) ?? new Response("Unhandled interaction", { status: 404 });
143
+ };
144
+ }
145
+
146
+ export { InteractionResponseType, InteractionType, createCloudflareHandler, verifyDiscordRequest };
147
+ //# sourceMappingURL=index.js.map
148
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["InteractionType","InteractionResponseType"],"mappings":"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","file":"index.js","sourcesContent":["import type { InteractionHandler } from \"mini-interactions-core\";\n\nexport enum InteractionType {\n Ping = 1,\n ApplicationCommand = 2,\n MessageComponent = 3,\n ApplicationCommandAutocomplete = 4,\n ModalSubmit = 5,\n}\n\nexport enum InteractionResponseType {\n Pong = 1,\n ChannelMessageWithSource = 4,\n DeferredChannelMessageWithSource = 5,\n DeferredUpdateMessage = 6,\n UpdateMessage = 7,\n ApplicationCommandAutocompleteResult = 8,\n Modal = 9,\n}\n\nexport interface DiscordInteraction<TData = Record<string, unknown>> {\n id?: string;\n application_id?: string;\n type: InteractionType | number;\n token?: string;\n version?: number;\n data?: TData;\n guild_id?: string;\n channel_id?: string;\n member?: unknown;\n user?: unknown;\n message?: unknown;\n [key: string]: unknown;\n}\n\nexport interface DiscordInteractionResponse {\n type: InteractionResponseType | number;\n data?: unknown;\n}\n\nexport interface MessageComponentInteractionData extends Record<string, unknown> {\n custom_id: string;\n component_type: number;\n values?: string[];\n resolved?: unknown;\n}\n\nexport interface ModalSubmitInteractionData extends Record<string, unknown> {\n custom_id: string;\n components: unknown[];\n resolved?: unknown;\n}\n\nexport interface CloudflareComponentContext<TData extends Record<string, unknown>> {\n request: Request;\n interaction: DiscordInteraction<TData>;\n customId: string;\n}\n\nexport interface CloudflareButtonContext extends CloudflareComponentContext<MessageComponentInteractionData> {\n componentType: 2;\n}\n\nexport interface CloudflareSelectMenuContext extends CloudflareComponentContext<MessageComponentInteractionData> {\n componentType: 3 | 5 | 6 | 7 | 8;\n values: readonly string[];\n resolved?: unknown;\n}\n\nexport type CloudflareModalValue = string | boolean | readonly string[] | null;\n\nexport interface CloudflareModalSubmitContext extends CloudflareComponentContext<ModalSubmitInteractionData> {\n values: Readonly<Record<string, CloudflareModalValue>>;\n resolved?: unknown;\n}\n\nexport type CloudflareInteractionResult = Response | DiscordInteractionResponse;\nexport type CloudflareComponentHandler<TContext> = (\n context: TContext,\n) => CloudflareInteractionResult | Promise<CloudflareInteractionResult>;\n\nexport interface CloudflareComponentHandlers {\n button?: CloudflareComponentHandler<CloudflareButtonContext>;\n selectMenu?: CloudflareComponentHandler<CloudflareSelectMenuContext>;\n modalSubmit?: CloudflareComponentHandler<CloudflareModalSubmitContext>;\n}\n\nexport interface CloudflareHandlerOptions {\n /** Low-level fallback for commands, unmatched interactions, or non-interaction requests. */\n handler?: InteractionHandler;\n /** Discord application public key. When provided, every interaction request is verified. */\n publicKey?: string;\n handlers?: CloudflareComponentHandlers;\n}\n\nfunction hexToBytes(hex: string, field: string): Uint8Array<ArrayBuffer> {\n if (hex.length === 0 || hex.length % 2 !== 0 || !/^[\\da-f]+$/i.test(hex)) {\n throw new TypeError(`${field} must be a hexadecimal string`);\n }\n const bytes = new Uint8Array(new ArrayBuffer(hex.length / 2));\n for (let index = 0; index < bytes.length; index += 1) {\n bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);\n }\n return bytes;\n}\n\n/** Verify Discord's Ed25519 signature using the Web Crypto API available in Workers. */\nexport async function verifyDiscordRequest(request: Request, publicKey: string, body?: string): Promise<boolean> {\n const signature = request.headers.get(\"X-Signature-Ed25519\");\n const timestamp = request.headers.get(\"X-Signature-Timestamp\");\n if (signature === null || timestamp === null) return false;\n\n try {\n const key = await crypto.subtle.importKey(\n \"raw\",\n hexToBytes(publicKey, \"publicKey\"),\n { name: \"Ed25519\" },\n false,\n [\"verify\"],\n );\n const payload = new TextEncoder().encode(timestamp + (body ?? await request.clone().text()));\n return await crypto.subtle.verify(\"Ed25519\", key, hexToBytes(signature, \"signature\"), payload);\n } catch {\n return false;\n }\n}\n\nfunction jsonResponse(value: DiscordInteractionResponse, status = 200): Response {\n return new Response(JSON.stringify(value), {\n status,\n headers: { \"Content-Type\": \"application/json; charset=utf-8\" },\n });\n}\n\nasync function resolveResult(\n result: CloudflareInteractionResult | Promise<CloudflareInteractionResult>,\n): Promise<Response> {\n const resolved = await result;\n return resolved instanceof Response ? resolved : jsonResponse(resolved);\n}\n\nfunction collectModalValues(components: unknown[]): Readonly<Record<string, CloudflareModalValue>> {\n const values: Record<string, CloudflareModalValue> = {};\n\n const visit = (component: unknown): void => {\n if (typeof component !== \"object\" || component === null) return;\n const entry = component as Record<string, unknown>;\n if (typeof entry.custom_id === \"string\") {\n if (typeof entry.value === \"string\" || typeof entry.value === \"boolean\" || entry.value === null) {\n values[entry.custom_id] = entry.value;\n }\n else if (Array.isArray(entry.values) && entry.values.every((value) => typeof value === \"string\")) {\n values[entry.custom_id] = Object.freeze([...entry.values] as string[]);\n }\n }\n if (Array.isArray(entry.components)) entry.components.forEach(visit);\n if (entry.component !== undefined) visit(entry.component);\n };\n\n components.forEach(visit);\n return Object.freeze(values);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Create a Cloudflare Worker-compatible Discord interactions handler.\n *\n * With only `handler`, this remains a transparent Request adapter. Supplying\n * `handlers` enables parsing and routing for buttons, all select menus, and\n * modal submissions. `publicKey` enables Discord signature verification.\n */\nexport function createCloudflareHandler(options: CloudflareHandlerOptions): InteractionHandler {\n if (!options || (typeof options.handler !== \"function\" && options.handlers === undefined)) {\n throw new TypeError(\"handler or handlers must be provided\");\n }\n if (options.handler !== undefined && typeof options.handler !== \"function\") {\n throw new TypeError(\"handler must be a function\");\n }\n if (options.publicKey !== undefined) hexToBytes(options.publicKey, \"publicKey\");\n\n // Preserve the original zero-overhead adapter behavior.\n if (options.handlers === undefined && options.publicKey === undefined) {\n return (request) => options.handler!(request);\n }\n\n return async (request) => {\n if (request.method !== \"POST\") {\n return options.handler?.(request) ?? new Response(\"Method Not Allowed\", { status: 405 });\n }\n\n const body = await request.clone().text();\n if (options.publicKey !== undefined && !await verifyDiscordRequest(request, options.publicKey, body)) {\n return new Response(\"Invalid request signature\", { status: 401 });\n }\n\n let interaction: DiscordInteraction;\n try {\n const parsed: unknown = JSON.parse(body);\n if (!isRecord(parsed) || typeof parsed.type !== \"number\") throw new TypeError(\"invalid interaction\");\n interaction = parsed as DiscordInteraction;\n } catch {\n return new Response(\"Invalid interaction payload\", { status: 400 });\n }\n\n if (interaction.type === InteractionType.Ping) {\n return jsonResponse({ type: InteractionResponseType.Pong });\n }\n\n if (interaction.type === InteractionType.MessageComponent && isRecord(interaction.data)) {\n const data = interaction.data as MessageComponentInteractionData;\n if (typeof data.custom_id === \"string\" && data.component_type === 2 && options.handlers?.button) {\n return resolveResult(options.handlers.button({\n request,\n interaction: interaction as DiscordInteraction<MessageComponentInteractionData>,\n customId: data.custom_id,\n componentType: 2,\n }));\n }\n if (\n typeof data.custom_id === \"string\"\n && (data.component_type === 3 || data.component_type === 5 || data.component_type === 6\n || data.component_type === 7 || data.component_type === 8)\n && options.handlers?.selectMenu\n ) {\n return resolveResult(options.handlers.selectMenu({\n request,\n interaction: interaction as DiscordInteraction<MessageComponentInteractionData>,\n customId: data.custom_id,\n componentType: data.component_type,\n values: Object.freeze([...(data.values ?? [])]),\n ...(data.resolved === undefined ? {} : { resolved: data.resolved }),\n }));\n }\n }\n\n if (interaction.type === InteractionType.ModalSubmit && isRecord(interaction.data)) {\n const data = interaction.data as ModalSubmitInteractionData;\n if (typeof data.custom_id === \"string\" && Array.isArray(data.components) && options.handlers?.modalSubmit) {\n return resolveResult(options.handlers.modalSubmit({\n request,\n interaction: interaction as DiscordInteraction<ModalSubmitInteractionData>,\n customId: data.custom_id,\n values: collectModalValues(data.components),\n ...(data.resolved === undefined ? {} : { resolved: data.resolved }),\n }));\n }\n }\n\n return options.handler?.(request) ?? new Response(\"Unhandled interaction\", { status: 404 });\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "mini-interactions-cloudflare",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "dependencies": {
17
+ "mini-interactions-core": "0.1.0"
18
+ },
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "vitest run",
24
+ "clean": "rm -rf dist"
25
+ }
26
+ }