From 91163e30d939a281c37e13f443091f2461daaf68 Mon Sep 17 00:00:00 2001 From: Seailz Date: Sat, 14 Jan 2023 01:20:34 +0000 Subject: [PATCH] feature: Added support for HTTP-only bots --- README.md | 104 ++++++++++++-- pom.xml | 21 +++ .../java/com/seailz/discordjv/DiscordJv.java | 37 ++++- .../discordjv/command/CommandDispatcher.java | 1 - .../discordjv/http/HttpOnlyApplication.java | 18 +++ .../discordjv/http/HttpOnlyManager.java | 133 ++++++++++++++++++ .../discordjv/http/SecurityManager.java | 17 +++ .../model/interaction/Interaction.java | 10 +- .../seailz/discordjv/utils/HTTPOnlyInfo.java | 18 +++ 9 files changed, 338 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/seailz/discordjv/http/HttpOnlyApplication.java create mode 100644 src/main/java/com/seailz/discordjv/http/HttpOnlyManager.java create mode 100644 src/main/java/com/seailz/discordjv/http/SecurityManager.java create mode 100644 src/main/java/com/seailz/discordjv/utils/HTTPOnlyInfo.java diff --git a/README.md b/README.md index e06243a2..869f36b2 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,106 @@ ![discord.jv banner](https://cdn.discordapp.com/attachments/1045606909195071498/1045608115074252800/AB1CEB6F-A4D9-496E-9520-821EB3BA0FCC-removebg-preview.png) [![seailz - discord.jv](https://img.shields.io/static/v1?label=seailz&message=discord.jv&color=blue&logo=github)](https://github.com/seailz/discord.jv "Go to GitHub repo") [![stars - discord.jv](https://img.shields.io/github/stars/seailz/discord.jv?style=social)](https://github.com/seailz/discord.jv) [![forks - discord.jv](https://img.shields.io/github/forks/seailz/discord.jv?style=social)](https://github.com/seailz/discord.jv) [![License](https://img.shields.io/badge/License-GNU_General_Public_License_v3.0-blue)](#license) [![issues - discord.jv](https://img.shields.io/github/issues/seailz/discord.jv)](https://github.com/seailz/discord.jv/issues) + # discord.jv - a clean Java wrapper for Discord -discord.jv [![loc - discord.jv](https://sloc.xyz/github/seailz/discord.jv)](https://github.com/seailz/discord.jv) is a **work in progress** Java wrapper for the [Discord API](https://discord.com/developers/docs/intro). -Everything that needs doing can be found in the [Issues](https://github.com/seailz/discord.jv/issues) tab, so if you're interested in helping out it would be greatly appreciated! + +discord.jv [![loc - discord.jv](https://sloc.xyz/github/seailz/discord.jv)](https://github.com/seailz/discord.jv) is +a **work in progress** Java wrapper for the [Discord API](https://discord.com/developers/docs/intro). +Everything that needs doing can be found in the [Issues](https://github.com/seailz/discord.jv/issues) tab, so if you're +interested in helping out it would be greatly appreciated! + +## Getting Started + +### Prerequisites + +You'll need to add discord.jv to your project's dependencies. We are currently using +jitpack to host our builds. See tutorials for your dependency management +system [here](https://jitpack.io/#discord-jv/discord.jv/-SNAPSHOT). + +A Discord bot token is required to use the API. You can get one by creating a bot +account [here](https://discord.com/developers/applications). + +### Creating a Bot + +To initialize a bot that uses the gateway (is able to receive events), you can use the following code: + +```java +new DiscordJv("token"); +``` + +You can specify intents to use with the gateway by using the following code: + +```java +new DiscordJv("token",EnumSet.of(Intents.GUILDS,Intents.GUILD_MESSAGES)); +``` + +Note: You can use the `Intents.ALL` constant to specify all intents. This does not include privileged intents. + +### Creating an HTTP-Only bot + +To make your bot an HTTP only bot, +you'll need to specify a couple more parameters. + +```java +new DiscordJv("token",EnumSet.of(Intents.GUILDS,Intents.GUILD_MESSAGES),true, + new HTTPOnlyInfo( + "interactions", + "EXAMPLE_APPLICATION_PUBLIC_KEY", // this cxan be found in your application's page in the dev panel + )); +``` + +You should set `"interactions"` to whatever endpoint you want to use to receive post requests from Discord. This will be +the endpoint you set in the Discord Developer Portal. + +This WILL break some methods and is only recommended to be used if you know what you are doing. +Otherwise, making a normal bot is recommended. +HTTP-only bots (or Interaction-only bots) are bots that do not connect to the gateway, and therefore cannot receive +events. +They receive interactions through POST requests to a specified endpoint of your bot. +This is useful if you want to make a bot that only uses slash commands. +

+Voice will not work, neither will setting your status & most gateway events. +
Interaction-based events will still be delivered as usual. +

+To change the port of your web server, create a new file in the running directory called `application.properties` and add the following line: + +``` +server.port=8081 +``` + +#### Getting it set up in the Discord Developer Portal + +1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) +2. You'll want to start your bot if you haven't already. +3. Select your bot. +4. Go to the "General Information" tab. +5. Scroll down to "INTERACTIONS ENDPOINT URL" and set it to the URL of your bot's endpoint. For example, if my bot's IP + was 123, the port was 321, and the endpoint was "interactions", I would set it to `http://123:321/interactions`. + +If it fails to save after that, please contact `Seailz#0001` on Discord. ## Documentation -There is currently no documentation, but it will be avaliable [here](https://discord-jv.gitbook.io/discord.jv-documentation/) when it's ready. Although there is no documentation, there are still examples which are avaliable [here](https://github.com/discord-jv/discord.jv/tree/main/examples) + +There is currently no documentation (excluding the tiny bit above), but it will be +available [here](https://discord-jv.gitbook.io/discord.jv-documentation/) when it's ready. Although there is no +documentation, there are still examples which are +avaliable [here](https://github.com/discord-jv/discord.jv/tree/main/examples) Javadocs can be found [here](https://discord-jv.github.io/discord.jv). ## Contributing -If you want to contribute to this project, feel free to do so! Everything that needs doing can be found in the [Issues](https://github.com/seailz/discord.jv/issues) tab, - and if an issue there has the **avaliable** tag you are free to make a PR fixing/adding it! :) - - Make sure you first check the [active PRs](https://github.com/seailz/discord.jv/pulls) and branches for the feature/bug you're fixing/adding. - If you make a PR for an issue with the **claimed** tag and you are not the one who claimed it, (or in other words if there is a PR open for the issue you're looking to fix/add), then your PR will be closed. - - If you've opened a PR but it's not yet finished, please mark it as a draft PR. - Branches should be named either: + +If you want to contribute to this project, feel free to do so! Everything that needs doing can be found in +the [Issues](https://github.com/seailz/discord.jv/issues) tab, +and if an issue there has the **available** tag you are free to make a PR fixing/adding it! :) + +Make sure you first check the [active PRs](https://github.com/seailz/discord.jv/pulls) and branches for the feature/bug +you're fixing/adding. +If you make a PR for an issue with the **claimed** tag, and you are not the one who claimed it, (or in other words if +there is a PR open for the issue you're looking to fix/add), then your PR will be closed. + +If you've opened a PR, but it's not yet finished, please mark it as a draft PR. +Branches should be named either: `feature/[feature]` or `bug/[bugfix]` diff --git a/pom.xml b/pom.xml index 731e7e5a..db9acb99 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,16 @@ shade + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + false @@ -51,6 +61,17 @@ + + commons-codec + commons-codec + 1.12 + + + + software.pando.crypto + salty-coffee + 1.1.0 + org.json diff --git a/src/main/java/com/seailz/discordjv/DiscordJv.java b/src/main/java/com/seailz/discordjv/DiscordJv.java index 2a0d9bc1..45569889 100644 --- a/src/main/java/com/seailz/discordjv/DiscordJv.java +++ b/src/main/java/com/seailz/discordjv/DiscordJv.java @@ -16,6 +16,7 @@ import com.seailz.discordjv.events.DiscordListener; import com.seailz.discordjv.events.EventDispatcher; import com.seailz.discordjv.gateway.GatewayFactory; +import com.seailz.discordjv.http.HttpOnlyApplication; import com.seailz.discordjv.model.application.Application; import com.seailz.discordjv.model.application.Intent; import com.seailz.discordjv.model.channel.Category; @@ -28,6 +29,7 @@ import com.seailz.discordjv.model.status.Status; import com.seailz.discordjv.model.user.User; import com.seailz.discordjv.utils.Checker; +import com.seailz.discordjv.utils.HTTPOnlyInfo; import com.seailz.discordjv.utils.URLS; import com.seailz.discordjv.utils.cache.Cache; import com.seailz.discordjv.utils.cache.JsonCache; @@ -63,7 +65,7 @@ public class DiscordJv { /** * Used to manage the gateway connection */ - private final GatewayFactory gatewayFactory; + private GatewayFactory gatewayFactory; /** * Stores the logger */ @@ -107,17 +109,30 @@ public class DiscordJv { */ private JsonCache selfUserCache; + public DiscordJv(String token, EnumSet intents, APIVersion version) throws ExecutionException, InterruptedException { + this(token, intents, version, false, null); + } + /** * Creates a new instance of the DiscordJv class * This will start the connection to the Discord gateway, set caches, set the event dispatcher, set the logger, set up eliminate handling, and initiates no shutdown * - * @param token The token of the bot - * @param intents The intents the bot will use - * @param version The version of the Discord API the bot will use + * @param token The token of the bot + * @param intents The intents the bot will use + * @param version The version of the Discord API the bot will use + * @param httpOnly Makes your bot an HTTP only bot. This WILL + * break some methods and is only recommended to be set to true if you know what you are doing. Otherwise, leave it to false or don't set it. + * HTTP-only bots (or Interaction-only bots) are bots that do not connect to the gateway, and therefore cannot receive events. They receive + * interactions through POST requests to a specified endpoint of your bot. This is useful if you want to make a bot that only uses slash commands. + * Voice will not work, neither will {@link #setStatus(Status)} & most gateway events. + * Interaction-based events will still be delivered as usual. + * For a full tutorial, see the README.md file. + * @param httpOnlyInfo The information needed to make your bot HTTP only. This is only needed if you set httpOnly to true, otherwise set to null. + * See the above parameter for more information. * @throws ExecutionException If an error occurs while connecting to the gateway * @throws InterruptedException If an error occurs while connecting to the gateway */ - public DiscordJv(String token, EnumSet intents, APIVersion version) throws ExecutionException, InterruptedException { + public DiscordJv(String token, EnumSet intents, APIVersion version, boolean httpOnly, HTTPOnlyInfo httpOnlyInfo) throws ExecutionException, InterruptedException { System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); this.token = token; this.intents = intents; @@ -126,7 +141,7 @@ public DiscordJv(String token, EnumSet intents, APIVersion version) thro this.commandDispatcher = new CommandDispatcher(); this.rateLimits = new HashMap<>(); this.queuedRequests = new ArrayList<>(); - this.gatewayFactory = new GatewayFactory(this); + if (!httpOnly) this.gatewayFactory = new GatewayFactory(this); this.guildCache = new Cache<>(this, Guild.class, new DiscordRequest( new JSONObject(), @@ -158,6 +173,12 @@ public DiscordJv(String token, EnumSet intents, APIVersion version) thro this.eventDispatcher = new EventDispatcher(this); new RequestQueueHandler(this); + if (httpOnly) { + if (httpOnlyInfo == null) + throw new IllegalArgumentException("httpOnlyInfo cannot be null if httpOnly is true!"); + HttpOnlyApplication.init(this, httpOnlyInfo.endpoint(), httpOnlyInfo.applicationPublicKey()); + } + initiateNoShutdown(); initiateShutdownHooks(); } @@ -193,7 +214,7 @@ protected void initiateShutdownHooks() { } catch (InterruptedException e) { throw new RuntimeException(e); } - gatewayFactory.close(); + if (gatewayFactory != null) gatewayFactory.close(); })); } @@ -204,6 +225,8 @@ protected void initiateShutdownHooks() { * @throws IOException If an error occurs while setting the status */ public void setStatus(@NotNull Status status) throws IOException { + if (gatewayFactory == null) + throw new IllegalStateException("Cannot set status on an HTTP-only bot. See the constructor for more information."); JSONObject json = new JSONObject(); json.put("d", status.compile()); json.put("op", 3); diff --git a/src/main/java/com/seailz/discordjv/command/CommandDispatcher.java b/src/main/java/com/seailz/discordjv/command/CommandDispatcher.java index e945d648..5e2197c8 100644 --- a/src/main/java/com/seailz/discordjv/command/CommandDispatcher.java +++ b/src/main/java/com/seailz/discordjv/command/CommandDispatcher.java @@ -52,7 +52,6 @@ public void dispatch(String name, CommandInteractionEvent event) { .get(subListeners.values().stream().toList().indexOf(detailsList)); if (Objects.equals(name, top.getClass().getAnnotation(SlashCommandInfo.class).name())) { - System.out.println("found top command " + name); details.listener().onCommand(event); } return; diff --git a/src/main/java/com/seailz/discordjv/http/HttpOnlyApplication.java b/src/main/java/com/seailz/discordjv/http/HttpOnlyApplication.java new file mode 100644 index 00000000..15a253a7 --- /dev/null +++ b/src/main/java/com/seailz/discordjv/http/HttpOnlyApplication.java @@ -0,0 +1,18 @@ +package com.seailz.discordjv.http; + +import com.seailz.discordjv.DiscordJv; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class HttpOnlyApplication { + + public HttpOnlyApplication() { + } + + public static void init(DiscordJv discordJv, String endpoint, String applicationPublicKey) { + HttpOnlyManager.init(discordJv, endpoint, applicationPublicKey); + SpringApplication.run(HttpOnlyApplication.class); + } + +} diff --git a/src/main/java/com/seailz/discordjv/http/HttpOnlyManager.java b/src/main/java/com/seailz/discordjv/http/HttpOnlyManager.java new file mode 100644 index 00000000..bf47a724 --- /dev/null +++ b/src/main/java/com/seailz/discordjv/http/HttpOnlyManager.java @@ -0,0 +1,133 @@ +package com.seailz.discordjv.http; + +import com.seailz.discordjv.DiscordJv; +import com.seailz.discordjv.command.CommandType; +import com.seailz.discordjv.events.model.interaction.button.ButtonInteractionEvent; +import com.seailz.discordjv.events.model.interaction.command.CommandInteractionEvent; +import com.seailz.discordjv.events.model.interaction.command.MessageContextCommandInteractionEvent; +import com.seailz.discordjv.events.model.interaction.command.SlashCommandInteractionEvent; +import com.seailz.discordjv.events.model.interaction.command.UserContextCommandInteractionEvent; +import com.seailz.discordjv.events.model.interaction.modal.ModalInteractionEvent; +import com.seailz.discordjv.events.model.interaction.select.StringSelectMenuInteractionEvent; +import com.seailz.discordjv.events.model.interaction.select.entity.ChannelSelectMenuInteractionEvent; +import com.seailz.discordjv.events.model.interaction.select.entity.RoleSelectMenuInteractionEvent; +import com.seailz.discordjv.events.model.interaction.select.entity.UserSelectMenuInteractionEvent; +import com.seailz.discordjv.gateway.GatewayFactory; +import com.seailz.discordjv.model.component.ComponentType; +import com.seailz.discordjv.model.interaction.Interaction; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.codec.DecoderException; +import org.json.JSONObject; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +@RestController +public class HttpOnlyManager { + + private static DiscordJv discordJv; + private static String endpoint; + private static String applicationPublicKey; + + public static void init(DiscordJv discordJv, String endpoint, String applicationPublicKey) { + HttpOnlyManager.discordJv = discordJv; + HttpOnlyManager.endpoint = endpoint; + HttpOnlyManager.applicationPublicKey = applicationPublicKey; + } + + @PostMapping("/*") + public ResponseEntity get(HttpServletRequest request) throws IOException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, NoSuchAlgorithmException, InvalidKeyException, SignatureException, DecoderException { + String path = request.getRequestURI(); + if (!path.endsWith(endpoint)) { + return ResponseEntity.notFound().build(); + } + + // Retrieve the signature, timestamp, and body from the headers + String signatureFromHeaders = request.getHeader("X-Signature-Ed25519"); + String timestampFromHeaders = request.getHeader("X-Signature-Timestamp"); + String body = request.getReader().lines().collect(Collectors.joining()); + + boolean verified = SecurityManager.verify(applicationPublicKey, signatureFromHeaders, timestampFromHeaders, body); + + if (!verified) { + // The signature is invalid + return ResponseEntity.status(401).build(); + } + + // the signature is valid, continue. + + + // handle interaction request + Interaction interaction = Interaction.decompile(new JSONObject(body), discordJv); + switch (interaction.type()) { + case PING -> { + return ResponseEntity.ok("{\"type\": 1}"); + } + case APPLICATION_COMMAND -> { + CommandInteractionEvent event = null; + + switch (CommandType.fromCode(new JSONObject(interaction.raw()).getJSONObject("data").getInt("type"))) { + case SLASH_COMMAND -> { + event = new SlashCommandInteractionEvent(discordJv, GatewayFactory.getLastSequence(), new JSONObject().put("d", new JSONObject(body))); + } + case USER -> + event = new UserContextCommandInteractionEvent(discordJv, GatewayFactory.getLastSequence(), new JSONObject().put("d", new JSONObject(body))); + case MESSAGE -> + event = new MessageContextCommandInteractionEvent(discordJv, GatewayFactory.getLastSequence(), new JSONObject().put("d", new JSONObject(body))); + } + + discordJv.getCommandDispatcher().dispatch(new JSONObject(interaction.raw()).getJSONObject("data").getString("name"), event); + } + case MESSAGE_COMPONENT -> { + switch (ComponentType.getType(new JSONObject().put("d", new JSONObject(body)).getJSONObject("d").getJSONObject("data").getInt("component_type"))) { + case BUTTON -> { + ButtonInteractionEvent event = new ButtonInteractionEvent(discordJv, 0L, new JSONObject().put("d", new JSONObject(body))); + discordJv.getEventDispatcher().dispatchEvent(event, ButtonInteractionEvent.class, discordJv); + } + case STRING_SELECT -> { + StringSelectMenuInteractionEvent event = new StringSelectMenuInteractionEvent(discordJv, 0L, new JSONObject().put("d", new JSONObject(body))); + discordJv.getEventDispatcher().dispatchEvent(event, StringSelectMenuInteractionEvent.class, discordJv); + } + case ROLE_SELECT -> { + RoleSelectMenuInteractionEvent event = new RoleSelectMenuInteractionEvent(discordJv, 0L, new JSONObject().put("d", new JSONObject(body))); + discordJv.getEventDispatcher().dispatchEvent(event, RoleSelectMenuInteractionEvent.class, discordJv); + } + case USER_SELECT -> { + UserSelectMenuInteractionEvent event = new UserSelectMenuInteractionEvent(discordJv, 0L, new JSONObject().put("d", new JSONObject(body))); + discordJv.getEventDispatcher().dispatchEvent(event, UserSelectMenuInteractionEvent.class, discordJv); + } + case CHANNEL_SELECT -> { + ChannelSelectMenuInteractionEvent event = new ChannelSelectMenuInteractionEvent(discordJv, 0L, new JSONObject().put("d", new JSONObject(body))); + discordJv.getEventDispatcher().dispatchEvent(event, ChannelSelectMenuInteractionEvent.class, discordJv); + } + + } + } + case APPLICATION_COMMAND_AUTOCOMPLETE -> { + + } + case MODAL_SUBMIT -> { + ModalInteractionEvent event = new ModalInteractionEvent(discordJv, 0L, new JSONObject().put("d", new JSONObject(body))); + discordJv.getEventDispatcher().dispatchEvent(event, ModalInteractionEvent.class, discordJv); + } + case UNKNOWN -> { + Logger.getLogger("DispatchedEvents").warning( + "[DISCORD.JV] Unknown interaction type: " + new JSONObject().put("d", new JSONObject(body)).getJSONObject("d").getInt("type") + ". This is usually because of an outdated framework version. Please update discord.jv"); + return null; + } + } + + + return ResponseEntity.ok("{\"type\": 1}"); + } + +} + diff --git a/src/main/java/com/seailz/discordjv/http/SecurityManager.java b/src/main/java/com/seailz/discordjv/http/SecurityManager.java new file mode 100644 index 00000000..d40d8bb9 --- /dev/null +++ b/src/main/java/com/seailz/discordjv/http/SecurityManager.java @@ -0,0 +1,17 @@ +package com.seailz.discordjv.http; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import software.pando.crypto.nacl.Crypto; + +import java.nio.charset.StandardCharsets; + +public class SecurityManager { + + public static boolean verify(String publicKey, String signature, String timestamp, String body) throws DecoderException { + return Crypto.signVerify( + Crypto.signingPublicKey(Hex.decodeHex(publicKey)), + (timestamp + body).getBytes(StandardCharsets.UTF_8), + Hex.decodeHex(signature)); + } +} \ No newline at end of file diff --git a/src/main/java/com/seailz/discordjv/model/interaction/Interaction.java b/src/main/java/com/seailz/discordjv/model/interaction/Interaction.java index c3e3c0cb..e079abdf 100644 --- a/src/main/java/com/seailz/discordjv/model/interaction/Interaction.java +++ b/src/main/java/com/seailz/discordjv/model/interaction/Interaction.java @@ -51,8 +51,9 @@ public class Interaction implements Compilerable { private final String locale; // Guild's preferred locale, if invoked in a guild private final String guildLocale; + private final String raw; - public Interaction(String id, Application application, InteractionType type, InteractionData data, Guild guild, Channel channel, Member member, User user, String token, int version, Message message, String appPermissions, String locale, String guildLocale) { + public Interaction(String id, Application application, InteractionType type, InteractionData data, Guild guild, Channel channel, Member member, User user, String token, int version, Message message, String appPermissions, String locale, String guildLocale, String raw) { this.id = id; this.application = application; this.type = type; @@ -67,6 +68,7 @@ public Interaction(String id, Application application, InteractionType type, Int this.appPermissions = appPermissions; this.locale = locale; this.guildLocale = guildLocale; + this.raw = raw; } public String id() { @@ -125,6 +127,10 @@ public String guildLocale() { return guildLocale; } + public String raw() { + return raw; + } + @Override public JSONObject compile() { Class dataClass = data.getClass(); @@ -174,7 +180,7 @@ public static Interaction decompile(JSONObject json, DiscordJv discordJv) throws String locale = json.has("locale") ? json.getString("locale") : null; String guildLocale = json.has("guildLocale") ? json.getString("guildLocale") : null; - return new Interaction(id, application, type, data, guild, channel, member, user, token, version, message, appPermissions, locale, guildLocale); + return new Interaction(id, application, type, data, guild, channel, member, user, token, version, message, appPermissions, locale, guildLocale, json.toString()); } diff --git a/src/main/java/com/seailz/discordjv/utils/HTTPOnlyInfo.java b/src/main/java/com/seailz/discordjv/utils/HTTPOnlyInfo.java new file mode 100644 index 00000000..d4c56af1 --- /dev/null +++ b/src/main/java/com/seailz/discordjv/utils/HTTPOnlyInfo.java @@ -0,0 +1,18 @@ +package com.seailz.discordjv.utils; + +import com.seailz.discordjv.DiscordJv; +import com.seailz.discordjv.utils.version.APIVersion; + +import java.util.EnumSet; + +/** + * POJO that contains information for discord.jv about + *
initializing an HTTP-Only (Interaction-only) bot. + *

+ * See {@link DiscordJv#DiscordJv(String, EnumSet, APIVersion, boolean, HTTPOnlyInfo)} for more information. + */ +public record HTTPOnlyInfo( + String endpoint, + String applicationPublicKey +) { +}