diff --git a/src/manifest/manifest_test.ts b/src/manifest/manifest_test.ts index 6dbbfa62..aa3db938 100644 --- a/src/manifest/manifest_test.ts +++ b/src/manifest/manifest_test.ts @@ -3,7 +3,7 @@ import { ISlackManifestRunOnSlack, SlackManifestType, } from "./types.ts"; -import { Manifest, SlackManifest } from "./mod.ts"; +import { Manifest, SlackManifest, validateOutgoingDomains } from "./mod.ts"; import { DefineDatastore, DefineEvent, @@ -26,6 +26,7 @@ import { import { DefineConnector } from "../functions/mod.ts"; import { InternalSlackTypes } from "../schema/slack/types/custom/mod.ts"; import { DuplicateCallbackIdError, DuplicateNameError } from "./errors.ts"; +import { assertThrows } from "https://deno.land/std@0.152.0/testing/asserts.ts"; Deno.test("SlackManifestType correctly resolves to a Hosted App when runOnSlack = true", () => { const definition: SlackManifestType = { @@ -1172,3 +1173,66 @@ Deno.test("Manifest throws error when CustomEvents with duplicate name are added assertStringIncludes(error.message, "CustomEvent"); } }); + +Deno.test("Manifest correctly parses valid domains", () => { + const def = { + outgoingDomains: [ + "https://slack.com", + "http://salesforce.com", + "https://api.slack.com", + ], + }; + const expected = ["slack.com", "salesforce.com", "api.slack.com"]; + + const definition: SlackManifestType = { + runOnSlack: false, + name: "test", + description: "description", + backgroundColor: "#FFF", + longDescription: + "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", + displayName: "displayName", + icon: "icon.png", + botScopes: ["channels:history", "chat:write", "commands"], + }; + const manifest = Manifest(definition); + + manifest.outgoing_domains = validateOutgoingDomains( + def.outgoingDomains || [], + ); + + assertEquals(manifest.outgoing_domains, expected); +}); + +Deno.test("Manifest throws error for invalid domains", () => { + const def = { + outgoingDomains: [ + "slack", + "htt*ps://api.slack.com", + "https://api slack.com", + ], + }; + + assertThrows( + () => { + const definition: SlackManifestType = { + runOnSlack: false, + name: "test", + description: "description", + backgroundColor: "#FFF", + longDescription: + "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", + displayName: "displayName", + icon: "icon.png", + botScopes: ["channels:history", "chat:write", "commands"], + }; + const manifest = Manifest(definition); + + manifest.outgoing_domains = validateOutgoingDomains( + def.outgoingDomains || [], + ); + }, + Error, + "Invalid outgoing domain", + ); +}); diff --git a/src/manifest/mod.ts b/src/manifest/mod.ts index 5677e7f7..abc69029 100644 --- a/src/manifest/mod.ts +++ b/src/manifest/mod.ts @@ -32,6 +32,30 @@ export const Manifest = ( return manifest.export(); }; +export const validateOutgoingDomains = ( + domains: string[], +) => { + return domains.map((domain) => { + try { + const url = domain.includes("://") ? domain : `http://${domain}`; + return new URL(url).hostname; + } catch (e) { + if (isValidDomain(domain)) { + return domain; + } else { + throw new Error( + `Invalid outgoing domain: ${domain}, error ${e}`, + ); + } + } + }); +}; + +const isValidDomain = (domain: string) => { + const domainPattern = /^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$/; + return domainPattern.test(domain); +}; + export class SlackManifest { constructor(private definition: SlackManifestType) { this.registerFeatures(); @@ -132,7 +156,9 @@ export class SlackManifest { ); } - manifest.outgoing_domains = def.outgoingDomains || []; + manifest.outgoing_domains = validateOutgoingDomains( + def.outgoingDomains || [], + ); // Assign remote hosted app properties if (manifest.settings.function_runtime === "slack") {