Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/app-store/apps.metadata.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import horizon_workrooms_config_json from "./horizon-workrooms/config.json";
import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
import ics_feedcalendar_config_json from "./ics-feedcalendar/config.json";
import protoncalendar_config_json from "./protoncalendar/config.json";
import insihts_config_json from "./insihts/config.json";
import intercom_config_json from "./intercom/config.json";
import jelly_config_json from "./jelly/config.json";
Expand Down Expand Up @@ -183,6 +184,7 @@ export const appStoreMetadata = {
"pipedrive-crm": pipedrive_crm_config_json,
plausible: plausible_config_json,
posthog: posthog_config_json,
protoncalendar: protoncalendar_config_json,
qr_code: qr_code_config_json,
raycast: raycast_config_json,
"retell-ai": retell_ai_config_json,
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/apps.server.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const apiHandlers = {
"pipedrive-crm": import("./pipedrive-crm/api"),
plausible: import("./plausible/api"),
posthog: import("./posthog/api"),
protoncalendar: import("./protoncalendar/api"),
qr_code: import("./qr_code/api"),
riverside: import("./riverside/api"),
roam: import("./roam/api"),
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/calendar.services.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const CalendarServiceMap =
googlecalendar: import("./googlecalendar/lib/CalendarService"),
"ics-feedcalendar": import("./ics-feedcalendar/lib/CalendarService"),
larkcalendar: import("./larkcalendar/lib/CalendarService"),
protoncalendar: import("./protoncalendar/lib/CalendarService"),
office365calendar: import("./office365calendar/lib/CalendarService"),
zohocalendar: import("./zohocalendar/lib/CalendarService"),
};
3 changes: 3 additions & 0 deletions packages/app-store/protoncalendar/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Check availability from your Proton Calendar using shareable ICS feed URLs. No OAuth or API keys required — just paste your calendar's ICS link and Cal.com will check it for scheduling conflicts.

Proton Calendar is a privacy-focused calendar from the makers of Proton Mail.
90 changes: 90 additions & 0 deletions packages/app-store/protoncalendar/api/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { NextApiRequest, NextApiResponse } from "next";

import { symmetricEncrypt } from "@calcom/lib/crypto";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";

import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import appConfig from "../config.json";
import { BuildCalendarService } from "../lib";

const ALLOWED_HOSTNAMES = ["calendar.proton.me", "calendar.protonmail.com"];

function isAllowedUrl(url: string): boolean {
try {
const parsed = new URL(url);
if (parsed.protocol !== "https:") return false;
return ALLOWED_HOSTNAMES.some((host) => parsed.hostname === host || parsed.hostname.endsWith(`.${host}`));
} catch {
return false;
}
}
Comment on lines +11 to +21
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Proton URL validation logic (ALLOWED_HOSTNAMES + isAllowedUrl) is duplicated here and again in lib/CalendarService.ts (and separately in tests). This duplication risks the API accepting/rejecting different URLs than the service if one copy changes. Consider moving this into a shared utility (e.g. lib/urlValidation.ts) and importing it from both places (and tests).

Copilot uses AI. Check for mistakes.

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") {
const { urls } = req.body;

if (!Array.isArray(urls) || urls.length === 0) {
return res.status(400).json({ message: "At least one ICS feed URL is required" });
}

// Validate all URLs are from Proton domains
for (const url of urls) {
if (!isAllowedUrl(url)) {
return res.status(400).json({
message: `Invalid URL: only HTTPS URLs from ${ALLOWED_HOSTNAMES.join(" or ")} are accepted`,
});
}
}

// Get user
const user = await prisma.user.findFirstOrThrow({
where: {
id: req.session?.user?.id,
},
select: {
id: true,
email: true,
},
});

const data = {
type: appConfig.type,
key: symmetricEncrypt(JSON.stringify({ urls }), process.env.CALENDSO_ENCRYPTION_KEY || ""),
userId: user.id,
teamId: null,
appId: appConfig.slug,
invalid: false,
delegationCredentialId: null,
};

try {
const dav = BuildCalendarService({
id: 0,
...data,
user: { email: user.email },
encryptedKey: null,
});
const listedCals = await dav.listCalendars();

if (listedCals.length !== urls.length) {
throw new Error(`Listed cals and URLs mismatch: ${listedCals.length} vs. ${urls.length}`);
}

await prisma.credential.create({
data,
});
} catch (e) {
logger.error("Could not add Proton Calendar feeds", e);
return res.status(500).json({ message: "Could not add Proton Calendar feeds" });
}

return res
.status(200)
.json({ url: getInstalledAppPath({ variant: "calendar", slug: "proton-calendar" }) });
}

if (req.method === "GET") {
return res.status(200).json({ url: "/apps/proton-calendar/setup" });
}
}
1 change: 1 addition & 0 deletions packages/app-store/protoncalendar/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as add } from "./add";
15 changes: 15 additions & 0 deletions packages/app-store/protoncalendar/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "Proton Calendar",
"title": "Proton Calendar",
"slug": "proton-calendar",
"dirName": "protoncalendar",
"type": "proton-calendar_calendar",
"logo": "icon.svg",
"variant": "calendar",
"categories": ["calendar"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
"description": "Check availability from Proton Calendar using ICS feed URLs. Proton Calendar is a privacy-focused calendar from Proton.",
"isTemplate": false,
"__createdUsingCli": true
}
2 changes: 2 additions & 0 deletions packages/app-store/protoncalendar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as api from "./api";
export * as lib from "./lib";
Loading
Loading