diff --git a/.env b/.env
deleted file mode 100644
index 28b258a..0000000
--- a/.env
+++ /dev/null
@@ -1,2 +0,0 @@
-COLLABLAND_ACTION_PUBLIC_KEY="7fe0a458caca2901f62e029a9da29f65b0915fc837eedc6db902be3aaceaffcc"
-SKIP_VERIFICATION=true
\ No newline at end of file
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..4e6f0e3
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+SKIP_VERIFICATION=
+NODE_ENV=
\ No newline at end of file
diff --git a/.eslintrc.js b/.eslintrc.js
index c268e32..0de68f8 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -17,4 +17,5 @@ module.exports = {
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
root: true,
+ ignorePatterns: ["dist/**"],
};
diff --git a/.gitignore b/.gitignore
index bea514d..0ce2085 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
node_modules
.idea
dist
-.DS_Store
\ No newline at end of file
+.DS_Store
+.env
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..ed4d133
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "dotenv.enableAutocloaking": false
+}
diff --git a/README.md b/README.md
index 356311b..3af94c3 100644
--- a/README.md
+++ b/README.md
@@ -78,9 +78,10 @@ The repository serves as a Express.js template for implementing Collab.Land acti
## **Contributing** 🫶
-- Please go through the following article [[**Link**]()] to understand the deep technical details regarding building on the Collab.Land actions platform.
+- Please go through the following article [[**Link**](https://dev.collab.land/docs/upstream-integrations/build-a-custom-action)] to understand the deep technical details regarding building on the Collab.Land actions platform.
- In order to change the slash commands for the actions, try editing the `MiniAppManifest` models mentioned in the metadata route handlers [[Here 👀]](src/routes/hello-action.ts#L86)
- In order to change the logic which runs on the slash commands, try changing the `handle()` function mentioned in the interactions route handlers [[Here 👀]](src/routes/hello-action.ts#L23)
---
+
Built with ❤️ and 🤝 by Collab.Land
diff --git a/package-lock.json b/package-lock.json
index 315ee4c..a9a0fc6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@collabland/models": "^0.22.1",
"@discordjs/rest": "^1.5.0",
"body-parser": "^1.20.1",
+ "bs58": "^5.0.0",
"cookie-parser": "~1.4.4",
"discord.js": "^14.7.1",
"dotenv": "^16.0.3",
diff --git a/package.json b/package.json
index 2d85466..fb77734 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"@collabland/models": "^0.22.1",
"@discordjs/rest": "^1.5.0",
"body-parser": "^1.20.1",
+ "bs58": "^5.0.0",
"cookie-parser": "~1.4.4",
"discord.js": "^14.7.1",
"dotenv": "^16.0.3",
diff --git a/src/bin/www.ts b/src/bin/www.ts
index cf18ab0..4441644 100755
--- a/src/bin/www.ts
+++ b/src/bin/www.ts
@@ -4,6 +4,7 @@ import app from "../app";
import * as http from "http";
import * as dotenv from "dotenv"; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
import debug from "debug";
+import { SignatureVerifier } from "../helpers/verify";
dotenv.config();
/**
@@ -26,12 +27,16 @@ app.set("port", port);
const server = http.createServer(app);
/**
- * Listen on provided port, on all network interfaces.
+ * Initialize all services, listen on provided port, on all network interfaces.
*/
-server.listen(port);
-server.on("error", onError);
-server.on("listening", onListening);
+Promise.all([SignatureVerifier.initVerifier()])
+ .then((_) => {
+ server.listen(port);
+ server.on("error", onError);
+ server.on("listening", onListening);
+ })
+ .catch((e) => onError(e));
/**
* Normalize a port into a number, string, or false.
@@ -76,6 +81,7 @@ function onError(error: any) {
break;
default:
throw error;
+ process.exit(1);
}
}
diff --git a/src/helpers/verify.ts b/src/helpers/verify.ts
index ec87457..9be036d 100644
--- a/src/helpers/verify.ts
+++ b/src/helpers/verify.ts
@@ -1,49 +1,98 @@
import { utils, Wallet } from "ethers";
import nacl from "tweetnacl";
import { Request, Response } from "express";
+import { decode } from "bs58";
import {
ActionEcdsaSignatureHeader,
ActionEd25519SignatureHeader,
ActionSignatureTimestampHeader,
} from "../constants";
-import { debugFactory } from "@collabland/common";
+import {
+ AnyType,
+ debugFactory,
+ getFetch,
+ handleFetchResponse,
+ HttpErrors,
+} from "@collabland/common";
+const fetch = getFetch();
const debug = debugFactory("SignatureVerifier");
+type CollabLandConfig = {
+ jwtPublicKey: string;
+ discordClientId: string;
+ actionEcdsaPublicKey: string;
+ actionEd25519PublicKey: string;
+};
+
export class SignatureVerifier {
+ private static ECDSAPublicKey: string;
+ private static ED25519PublicKey: string;
+
+ static async initVerifier() {
+ const apiUrl = `https://api${
+ process.env.NODE_ENV === "production" ? "" : "-qa"
+ }.collab.land/config`;
+ const keysResponse = await fetch(apiUrl);
+ const keys = await handleFetchResponse(
+ keysResponse,
+ 200,
+ {
+ customErrorMessage: `Error in fetching collab.land config from URL: ${apiUrl}`,
+ }
+ );
+ SignatureVerifier.ECDSAPublicKey = keys.actionEcdsaPublicKey;
+ SignatureVerifier.ED25519PublicKey = Buffer.from(
+ decode(keys.actionEd25519PublicKey)
+ ).toString("hex");
+ debug("API URL for Collab.Land Config:", apiUrl);
+ debug("SingatureVerifier Initialized");
+ }
verify(req: Request, res: Response) {
if (!process.env.SKIP_VERIFICATION) {
- const ecdsaSignature = req.header(ActionEcdsaSignatureHeader);
- const ed25519Signature = req.header(ActionEd25519SignatureHeader);
- const signatureTimestamp: number = parseInt(
- req.header(ActionSignatureTimestampHeader) ?? "0"
- );
- const body = JSON.stringify(req.body);
- const publicKey = this.getPublicKey();
- const signature = ecdsaSignature ?? ed25519Signature;
- if (!signature) {
- res.status(401);
- res.send({
- message: `${ActionEcdsaSignatureHeader} or ${ActionEd25519SignatureHeader} header is required`,
- });
- return;
- }
- if (!publicKey) {
- res.status(401);
- res.send({
- message: `Public key is not set.`,
- });
- return;
+ try {
+ debug("Verifying signature...");
+ const ecdsaSignature = req.header(ActionEcdsaSignatureHeader);
+ const ed25519Signature = req.header(ActionEd25519SignatureHeader);
+ const signatureTimestamp: number = parseInt(
+ req.header(ActionSignatureTimestampHeader) ?? "0"
+ );
+ const body = JSON.stringify(req.body);
+ const signature = ecdsaSignature ?? ed25519Signature;
+ if (!signature) {
+ throw new HttpErrors[401](
+ `${ActionEcdsaSignatureHeader} or ${ActionEd25519SignatureHeader} header is required`
+ );
+ }
+ const signatureType =
+ signature === ecdsaSignature ? "ecdsa" : "ed25519";
+ const publicKey = this.getPublicKey(signatureType);
+ if (!publicKey) {
+ throw new HttpErrors[401](`Public key is not set.`);
+ }
+ this.verifyRequest(
+ body,
+ signatureTimestamp,
+ signature,
+ publicKey,
+ signatureType
+ );
+ return true;
+ } catch (err) {
+ if (HttpErrors.isHttpError(err)) {
+ res.status(err.statusCode).json({
+ message: err.message,
+ });
+ return false;
+ } else {
+ res.status(403).json({
+ message: "Unauthorized",
+ });
+ return false;
+ }
}
- const signatureType = signature === ecdsaSignature ? "ecdsa" : "ed25519";
-
- this.verifyRequest(
- body,
- signatureTimestamp,
- signature,
- publicKey,
- signatureType
- );
+ } else {
+ return true;
}
}
@@ -63,8 +112,10 @@ export class SignatureVerifier {
};
}
- private getPublicKey() {
- return process.env.COLLABLAND_ACTION_PUBLIC_KEY;
+ private getPublicKey(signatureType: "ecdsa" | "ed25519") {
+ return signatureType === "ecdsa"
+ ? SignatureVerifier.ECDSAPublicKey
+ : SignatureVerifier.ED25519PublicKey;
}
private verifyRequest(
@@ -76,7 +127,9 @@ export class SignatureVerifier {
) {
const delta = Math.abs(Date.now() - signatureTimestamp);
if (delta >= 5 * 60 * 1000) {
- throw new Error("Invalid request - signature timestamp is expired.");
+ throw new HttpErrors[403](
+ "Invalid request - signature timestamp is expired."
+ );
}
const msg = signatureTimestamp + body;
if (signatureType === "ed25519") {
@@ -109,13 +162,13 @@ export class SignatureVerifier {
Buffer.from(publicKey, "hex")
);
debug("Signature verified: %s", verified);
- } catch (err: any) {
+ } catch (err: AnyType) {
verified = false;
debug(err.message);
}
if (!verified) {
- throw new Error(
+ throw new HttpErrors[403](
"Invalid request - Ed25519 signature cannot be verified."
);
}
@@ -148,7 +201,9 @@ export class SignatureVerifier {
if (!verified) {
debug("Invalid signature: %s, body: %s", signature, body);
- throw new Error("Invalid request - Ecdsa signature cannot be verified.");
+ throw new HttpErrors[403](
+ "Invalid request - Ecdsa signature cannot be verified."
+ );
}
return verified;
}
diff --git a/src/routes/button-action.ts b/src/routes/button-action.ts
index 2cb8d26..4ac4b47 100644
--- a/src/routes/button-action.ts
+++ b/src/routes/button-action.ts
@@ -68,6 +68,7 @@ router.get("/metadata", function (req, res) {
version: { name: "0.0.1" },
website: "https://collab.land",
description: "An example Collab.Land action",
+ shortDescription: "A short description for the miniapp card"
});
const metadata: DiscordActionMetadata = {
/**
@@ -112,9 +113,11 @@ router.get("/metadata", function (req, res) {
router.post("/interactions", async function (req, res) {
const verifier = new SignatureVerifier();
- verifier.verify(req, res);
- const result = await handle(req.body);
- res.send(result);
+ const verified = verifier.verify(req, res);
+ if (verified) {
+ const result = await handle(req.body);
+ res.send(result);
+ }
});
export default router;
diff --git a/src/routes/hello-action.ts b/src/routes/hello-action.ts
index e396251..7a430f1 100644
--- a/src/routes/hello-action.ts
+++ b/src/routes/hello-action.ts
@@ -139,9 +139,11 @@ router.get("/metadata", function (req, res) {
router.post("/interactions", async function (req, res) {
const verifier = new SignatureVerifier();
- verifier.verify(req, res);
- const result = await handle(req.body);
- res.send(result);
+ const verified = verifier.verify(req, res);
+ if (verified) {
+ const result = await handle(req.body);
+ res.send(result);
+ }
});
export default router;
diff --git a/src/routes/index.ts b/src/routes/index.ts
index 0f5d355..8dfad5d 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -2,4 +2,4 @@ import helloAction from "./hello-action";
import buttonAction from "./button-action";
import popupAction from "./popup-action";
-export default {helloAction, buttonAction, popupAction};
+export default { helloAction, buttonAction, popupAction };
diff --git a/src/routes/popup-action.ts b/src/routes/popup-action.ts
index ff26b41..92f6677 100644
--- a/src/routes/popup-action.ts
+++ b/src/routes/popup-action.ts
@@ -120,9 +120,11 @@ router.get("/metadata", function (req, res) {
router.post("/interactions", async function (req, res) {
const verifier = new SignatureVerifier();
- verifier.verify(req, res);
- const result = await handle(req.body);
- res.send(result);
+ const verified = verifier.verify(req, res);
+ if (verified) {
+ const result = await handle(req.body);
+ res.send(result);
+ }
});
export default router;