feat: add encryptedKey column to Credential table for calendar integrations (#27154)

Adds encrypted credential storage using a new keyring system:
  - New `encryptedKey` column with AES-256-GCM encryption
  - Decryption in getCalendarsEvents with fallback to legacy `key`
  - buildCredentialCreateData service for credential creation
  - Phase 1: Google Calendar only, other integrations follow
This commit is contained in:
Alex van Andel
2026-01-30 09:15:13 -03:00
committed by GitHub
parent 80e4b1af1f
commit fc9c26e8dd
36 changed files with 308 additions and 34 deletions
@@ -83,6 +83,7 @@ type UserCredentialType = {
teamId: number | null;
key: Prisma.JsonValue;
invalid: boolean | null;
encryptedKey: string | null;
};
export async function patchHandler(req: NextApiRequest) {
@@ -120,6 +120,7 @@ export class AppleCalendarService implements CredentialSyncCalendarApp {
appId: APPLE_CALENDAR_ID,
invalid: false,
delegationCredentialId: null,
encryptedKey: null,
};
const dav = BuildCalendarService({
@@ -40,6 +40,7 @@ export class IcsFeedService implements ICSFeedCalendarApp {
appId: ICS_CALENDAR,
invalid: false,
delegationCredentialId: null,
encryptedKey: null,
};
try {
@@ -88,6 +88,7 @@ export class CredentialsRepository {
id: true,
type: true,
key: true,
encryptedKey: true,
userId: true,
teamId: true,
appId: true,
+15 -13
View File
@@ -7,6 +7,7 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
import { buildCredentialCreateData } from "@calcom/features/credentials/services/CredentialDataService";
import { DelegationCredentialRepository } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
@@ -70,19 +71,20 @@ export async function handleCreateCredentials() {
`Creating credentials for ${toProcessMembers.length} members out of ${delegatedMembers.length} delegated members in organization ${organization.id}`
);
const credentialCreationPromises = toProcessMembers.map(async (member) => {
try {
await CredentialRepository.create({
type: "google_calendar",
key: {
// The in-memory credential that we create for Delegation Credential has access_token as "NOOP_UNUSED_DELEGATION_TOKEN. This is to mark it is in DB, in case we need to identify that.
access_token: "NOOP_UNUSED_DELEGATION_TOKEN_DB",
},
userId: member.userId,
appId: "google-calendar",
delegationCredentialId: delegationCredential.id,
});
log.info(`Created credential for member ${member.userId}`);
const credentialCreationPromises = toProcessMembers.map(async (member) => {
try {
const credentialData = buildCredentialCreateData({
type: "google_calendar",
key: {
// The in-memory credential that we create for Delegation Credential has access_token as "NOOP_UNUSED_DELEGATION_TOKEN. This is to mark it is in DB, in case we need to identify that.
access_token: "NOOP_UNUSED_DELEGATION_TOKEN_DB",
},
userId: member.userId,
appId: "google-calendar",
delegationCredentialId: delegationCredential.id,
});
await CredentialRepository.create(credentialData);
log.info(`Created credential for member ${member.userId}`);
} catch (error) {
log.error(`Error creating credential for member ${member.userId}:`, safeStringify(error));
throw error;
@@ -10,6 +10,7 @@ type MockCredential = {
userId: number | null;
user: { email: string; name: string | null } | null;
key: { access_token: string };
encryptedKey: string | null;
invalid: boolean | null;
teamId: number | null;
team: { name: string } | null;
@@ -28,6 +29,7 @@ const createMockCredential = (overrides: Partial<MockCredential> = {}): MockCred
team: null,
user: { email: "test@example.com", name: "Test User" },
key: { access_token: "mock_token_123" },
encryptedKey: null,
delegationCredentialId: "delegation-123",
appId: "google-calendar",
invalid: false,
@@ -61,6 +61,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...data,
user: { email: user.email },
delegationCredentialId: null,
encryptedKey: null,
});
await dav?.listCalendars();
await prisma.credential.upsert({
@@ -39,6 +39,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: 0,
...data,
user: { email: user.email },
encryptedKey: null,
});
await dav?.listCalendars();
await prisma.credential.create({
@@ -91,6 +91,7 @@ export const FAKE_DAILY_CREDENTIAL: CredentialForCalendarService & { invalid: bo
appId: "daily-video",
invalid: false,
teamId: null,
encryptedKey: null,
delegatedToId: null,
delegatedTo: null,
delegationCredentialId: null,
@@ -126,6 +126,7 @@ const buildDelegationCredential = (overrides = {}) => ({
delegatedTo: {
serviceAccountKey: mockServiceAccountKey,
},
encryptedKey: null,
...overrides,
});
@@ -158,6 +159,7 @@ const buildRegularCredential = (overrides = {}): CredentialForCalendarService =>
delegatedTo: null,
// Regular credentials have it set to null always
delegationCredentialId: null,
encryptedKey: null,
...overrides,
});
@@ -92,6 +92,7 @@ const _buildCommonUserCredential = ({
key: {
access_token: "NOOP_UNUSED_DELEGATION_TOKEN",
},
encryptedKey: null,
invalid: false,
teamId: null,
team: null,
@@ -46,6 +46,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
id: 0,
...data,
user: { email: user.email },
encryptedKey: null,
});
await dav?.listCalendars();
await prisma.credential.create({
@@ -46,6 +46,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
id: 0,
user: { email: user.email },
...data,
encryptedKey: null,
});
await dav?.listCalendars();
await prisma.credential.create({
@@ -38,7 +38,7 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
};
try {
const service = BuildCalendarService({ id: 0, user: { email: session.user.email || "" }, ...data });
const service = BuildCalendarService({ id: 0, user: { email: session.user.email || "" }, ...data, encryptedKey: null });
await service?.listCalendars();
await prisma.credential.create({ data });
} catch (reason) {
@@ -4,6 +4,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createGoogleCalendarServiceWithGoogleType } from "@calcom/app-store/googlecalendar/lib/CalendarService";
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
import { buildCredentialCreateData } from "@calcom/features/credentials/services/CredentialDataService";
import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar";
import {
GOOGLE_CALENDAR_SCOPES,
@@ -71,12 +72,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
oAuth2Client.setCredentials(key);
const gcalCredential = await CredentialRepository.create({
const gcalCredentialData = buildCredentialCreateData({
userId: req.session.user.id,
key,
appId: "google-calendar",
type: "google_calendar",
});
const gcalCredential = await CredentialRepository.create(gcalCredentialData);
const gCalService = createGoogleCalendarServiceWithGoogleType({
...gcalCredential,
@@ -169,12 +171,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
}
// Create a new google meet credential
await CredentialRepository.create({
const googleMeetCredentialData = buildCredentialCreateData({
userId: req.session.user.id,
type: "google_video",
key: {},
appId: "google-meet",
});
await CredentialRepository.create(googleMeetCredentialData);
res.redirect(
getSafeRedirectUrl(`${WEBAPP_URL}/apps/installed/conferencing?hl=google-meet`) ??
getInstalledAppPath({ variant: "conferencing", slug: "google-meet" })
@@ -52,6 +52,7 @@ const mockCredential: CredentialForCalendarServiceWithEmail = {
delegatedTo: null,
invalid: false,
teamId: null,
encryptedKey: null,
};
describe("getAvailability", () => {
@@ -191,6 +191,7 @@ describe("HubspotCalendarService", () => {
email: "test-user@example.com",
},
delegationCredentialId: null,
encryptedKey: null,
};
service = BuildCrmService(mockCredential, {}) as CRM & { getAppOptions: () => AppOptions };
@@ -37,6 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: 0,
...data,
user: { email: user.email },
encryptedKey: null,
});
const listedCals = await dav.listCalendars();
@@ -56,6 +56,7 @@ const testCredential = {
teamId: 1,
delegatedTo: null,
delegationCredentialId: null,
encryptedKey: null,
};
describe("createMeeting", () => {
@@ -120,6 +120,7 @@ describe("SalesforceCRMService", () => {
email: "test-user@example.com",
},
delegationCredentialId: null,
encryptedKey: null,
};
service = createSalesforceCrmServiceWithSalesforceType(mockCredential, {}, true);
@@ -86,6 +86,7 @@ const createMockCredential = (options?: { tokenLifetime?: number; issuedAt?: str
appId: "test_app_id",
invalid: false,
delegationCredentialId: null,
encryptedKey: null,
};
};
+1
View File
@@ -21,6 +21,7 @@ describe("sanitizeAppForViewer", () => {
delegatedToId: null,
delegationCredentialId: null,
team: null,
encryptedKey: null,
};
const mockApp: App & {
+1
View File
@@ -64,6 +64,7 @@ function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?
teamId: null,
appId: appMeta.slug,
invalid: false,
encryptedKey: null,
delegatedTo: null,
delegatedToId: null,
delegationCredentialId: null,
@@ -17,6 +17,7 @@ import {
import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing";
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
import { buildCredentialCreateData } from "@calcom/features/credentials/services/CredentialDataService";
import type { TrackingData } from "@calcom/lib/tracking";
import { DeploymentRepository } from "@calcom/features/ee/deployment/repositories/DeploymentRepository";
import createUsersAndConnectToOrg from "@calcom/features/ee/dsync/lib/users/createUsersAndConnectToOrg";
@@ -730,12 +731,13 @@ export const getOptions = ({
token_type: account.token_type,
expires_at: account.expires_at,
};
const gcalCredential = await CredentialRepository.create({
const gcalCredentialData = buildCredentialCreateData({
userId: Number(user.id),
key: credentialkey,
appId: "google-calendar",
type: "google_calendar",
});
const gcalCredential = await CredentialRepository.create(gcalCredentialData);
const gCalService = createGoogleCalendarServiceWithGoogleType({
...gcalCredential,
user: null,
@@ -748,12 +750,13 @@ export const getOptions = ({
type: "google_video",
}))
) {
await CredentialRepository.create({
const googleMeetCredentialData = buildCredentialCreateData({
type: "google_video",
key: {},
userId: Number(user.id),
appId: "google-meet",
});
await CredentialRepository.create(googleMeetCredentialData);
}
const oAuth2Client = new OAuth2Client(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET);
@@ -939,6 +939,7 @@ export default class EventManager {
invalid: credentialFromDB.invalid,
appId: credentialFromDB.appId,
user: credentialFromDB.user,
encryptedKey: credentialFromDB.encryptedKey,
delegatedToId: credentialFromDB.delegatedToId,
delegatedTo: credentialFromDB.delegatedTo,
delegationCredentialId: credentialFromDB.delegationCredentialId,
@@ -1155,6 +1156,7 @@ export default class EventManager {
invalid: credentialFromDB.invalid,
appId: credentialFromDB.appId,
user: credentialFromDB.user,
encryptedKey: credentialFromDB.encryptedKey,
delegatedToId: credentialFromDB.delegatedToId,
delegatedTo: credentialFromDB.delegatedTo,
delegationCredentialId: credentialFromDB.delegationCredentialId,
@@ -1,13 +1,14 @@
import process from "node:process";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { decryptSecret } from "@calcom/lib/crypto/keyring";
import { isDelegationCredential } from "@calcom/lib/delegationCredential";
import logger from "@calcom/lib/logger";
import { getPiiFreeSelectedCalendar, getPiiFreeCredential } from "@calcom/lib/piiFreeData";
import { getPiiFreeCredential, getPiiFreeSelectedCalendar } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { performance } from "@calcom/lib/server/perfObserver";
import type { CalendarFetchMode, EventBusyDate, SelectedCalendar } from "@calcom/types/Calendar";
import type { CredentialForCalendarService } from "@calcom/types/Credential";
import { normalizeTimezone } from "./timezone-conversion";
const log = logger.getSubLogger({ prefix: ["getCalendarsEvents"] });
@@ -101,7 +102,33 @@ const getCalendarsEvents = async (
const calendarAndCredentialPairs = await Promise.all(
calendarCredentials.map(async (credential) => {
const calendar = await getCalendar(credential, mode);
let key: typeof credential.key;
try {
if (credential.encryptedKey) {
key = JSON.parse(
decryptSecret({
envelope: JSON.parse(credential.encryptedKey),
aad: { type: credential.type },
})
);
} else {
key = credential.key;
}
} catch {
log.warn("Failed to decrypt credential key, falling back to legacy key", {
credentialId: credential.id,
});
key = credential.key;
}
const calendar = await getCalendar(
{
...credential,
// use encrypted secret to get unencrypted calendar creds
key,
},
mode
);
return [calendar, credential] as const;
})
);
@@ -180,6 +180,7 @@ const createMeetingWithCalVideo = async (calEvent: CalendarEvent) => {
user: { email: "" },
teamId: null,
key: dailyAppKeys,
encryptedKey: null,
invalid: false,
delegationCredentialId: null,
},
@@ -203,6 +204,7 @@ export const createInstantMeetingWithCalVideo = async (endTime: string) => {
user: { email: "" },
teamId: null,
key: dailyAppKeys,
encryptedKey: null,
invalid: false,
delegationCredentialId: null,
},
@@ -229,6 +231,7 @@ const getRecordingsOfCalVideoByRoomName = async (
user: { email: "" },
teamId: null,
key: dailyAppKeys,
encryptedKey: null,
invalid: false,
delegationCredentialId: null,
},
@@ -255,6 +258,7 @@ const getDownloadLinkOfCalVideoByRecordingId = async (
user: { email: "" },
teamId: null,
key: dailyAppKeys,
encryptedKey: null,
invalid: false,
delegationCredentialId: null,
},
@@ -279,6 +283,7 @@ const getAllTranscriptsAccessLinkFromRoomName = async (roomName: string) => {
user: { email: "" },
teamId: null,
key: dailyAppKeys,
encryptedKey: null,
invalid: false,
delegationCredentialId: null,
},
@@ -303,6 +308,7 @@ const getAllTranscriptsAccessLinkFromMeetingId = async (meetingId: string) => {
user: { email: "" },
teamId: null,
key: dailyAppKeys,
encryptedKey: null,
invalid: false,
delegationCredentialId: null,
},
@@ -327,6 +333,7 @@ const submitBatchProcessorTranscriptionJob = async (recordingId: string) => {
user: { email: "" },
teamId: null,
key: dailyAppKeys,
encryptedKey: null,
invalid: false,
delegationCredentialId: null,
},
@@ -363,6 +370,7 @@ const getTranscriptsAccessLinkFromRecordingId = async (recordingId: string) => {
user: { email: "" },
teamId: null,
key: dailyAppKeys,
encryptedKey: null,
invalid: false,
delegationCredentialId: null,
},
@@ -388,6 +396,7 @@ const checkIfRoomNameMatchesInRecording = async (roomName: string, recordingId:
user: { email: "" },
teamId: null,
key: dailyAppKeys,
encryptedKey: null,
invalid: false,
delegationCredentialId: null,
},
@@ -413,6 +422,7 @@ const getCalVideoMeetingSessionsByRoomName = async (roomName: string) => {
user: { email: "" },
teamId: null,
key: dailyAppKeys,
encryptedKey: null,
invalid: false,
delegationCredentialId: null,
},
@@ -13,6 +13,7 @@ type CredentialCreateInput = {
userId: number;
appId: string;
delegationCredentialId?: string | null;
encryptedKey?: string | null;
};
type CredentialUpdateInput = {
@@ -45,7 +46,9 @@ export class CredentialRepository {
}
static async create(data: CredentialCreateInput) {
const credential = await prisma.credential.create({ data: { ...data } });
const credential = await prisma.credential.create({
data,
});
return buildNonDelegationCredential(credential);
}
static async findByAppIdAndUserId({
@@ -81,7 +84,7 @@ export class CredentialRepository {
static async findFirstByIdWithKeyAndUser({ id }: { id: number }) {
const credential = await prisma.credential.findUnique({
where: { id },
select: { ...safeCredentialSelect, key: true },
select: { ...safeCredentialSelect, key: true, encryptedKey: true },
});
return buildNonDelegationCredential(credential);
}
@@ -276,15 +279,17 @@ export class CredentialRepository {
type,
key,
appId,
encryptedKey,
}: {
userId: number;
delegationCredentialId: string;
type: string;
key: Prisma.InputJsonValue;
appId: string;
encryptedKey?: string | null;
}) {
return prisma.credential.create({
data: { userId, delegationCredentialId, type, key, appId },
data: { userId, delegationCredentialId, type, key, appId, ...(encryptedKey && { encryptedKey }) },
});
}
@@ -0,0 +1,43 @@
import { encryptSecret } from "@calcom/lib/crypto/keyring";
export type CredentialCreateData = {
type: string;
key: object;
userId: number;
appId: string;
delegationCredentialId?: string | null;
encryptedKey?: string | null;
};
/**
* Builds the data object for creating a credential, including the encrypted key.
* This service handles the encryption logic so the repository stays focused on data access.
*
* @param data The credential data without encryptedKey
* @returns The credential data with encryptedKey populated if encryption key is available
*/
export function buildCredentialCreateData(data: {
type: string;
key: object;
userId: number;
appId: string;
delegationCredentialId?: string | null;
}): CredentialCreateData {
const aad = {
type: data.type,
};
let encryptedKey: ReturnType<typeof encryptSecret> | null = null;
try {
encryptedKey = encryptSecret({ ring: "CREDENTIALS", plaintext: JSON.stringify(data.key), aad });
} catch {
// Encryption keyring not configured - this is expected in tests and some environments
// The encryptedKey will be null, which is fine for backwards compatibility
}
return {
...data,
key: data.key,
...(encryptedKey && { encryptedKey: JSON.stringify(encryptedKey) }),
};
}
+149
View File
@@ -0,0 +1,149 @@
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
import process from "node:process";
type KeyringName = "CREDENTIALS";
type SecretEnvelopeV1 = {
v: 1;
alg: "AES-256-GCM";
ring: KeyringName;
kid: string;
nonce: string;
ct: string;
tag: string;
};
type AAD = Record<string, string | number | boolean | null> | Array<string | number | boolean | null>;
function stableJson(value: unknown): string {
if (value === null || value === undefined) return "null";
if (typeof value !== "object") return JSON.stringify(value);
if (Array.isArray(value)) {
return `[${value.map((v) => stableJson(v)).join(",")}]`;
}
const keys = Object.keys(value).sort();
const entries = keys.map(
(k) => `${JSON.stringify(k)}:${stableJson((value as Record<string, unknown>)[k])}`
);
return `{${entries.join(",")}}`;
}
function aadToBuffer(aad: AAD): Buffer {
const stable = stableJson(aad);
return Buffer.from(stable, "utf8");
}
function b64url(buf: Buffer): string {
return buf.toString("base64url");
}
function unb64url(s: string): Buffer {
return Buffer.from(s, "base64url");
}
const envKeyringPrefix = (ring: KeyringName): string => {
// enforce all caps ring names to match your convention (optional)
// if you want to allow any casing, remove this check.
if (ring !== ring.toUpperCase()) {
throw new Error(`Keyring name must be ALL CAPS. Got: ${ring}`);
}
return `CALCOM_KEYRING_${ring}_`;
};
const getCurrentKid = (ring: KeyringName): string => {
const prefix = envKeyringPrefix(ring);
const current = process.env[`${prefix}CURRENT`];
if (!current) throw new Error(`Missing env var ${prefix}CURRENT`);
return current;
};
export function getKeyMaterial(ring: KeyringName, kid: string): Buffer {
const prefix = envKeyringPrefix(ring);
const raw = process.env[`${prefix}${kid.toUpperCase()}`]; // e.g. CALCOM_KEYRING_CREDENTIALS_K1
if (!raw) throw new Error(`Unknown kid for ring=${ring}: missing env var ${prefix}${kid.toUpperCase()}`);
const key = Buffer.from(raw, "base64url");
if (key.length !== 32) {
throw new Error(`Invalid key length for ring=${ring} kid=${kid}. Expected 32 bytes, got ${key.length}`);
}
return key;
}
export const encryptSecret = ({
ring,
plaintext,
aad,
}: {
ring: KeyringName;
plaintext: string;
aad: AAD;
}): SecretEnvelopeV1 => {
const kid = getCurrentKid(ring);
const key = getKeyMaterial(ring, kid);
const nonce = randomBytes(12); // 96-bit nonce for GCM
const aadBuf = aadToBuffer(aad);
const cipher = createCipheriv("aes-256-gcm", key, nonce);
cipher.setAAD(aadBuf);
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return {
v: 1,
alg: "AES-256-GCM",
ring,
kid,
nonce: b64url(nonce),
ct: b64url(ct),
tag: b64url(tag),
};
};
export const decryptSecret = ({ envelope, aad }: { envelope: SecretEnvelopeV1; aad: AAD }): string => {
if (envelope.v !== 1) {
throw new Error(`Unsupported envelope version: ${envelope.v}`);
}
if (envelope.alg !== "AES-256-GCM") {
throw new Error(`Unsupported envelope algorithm: ${envelope.alg}`);
}
const key = getKeyMaterial(envelope.ring, envelope.kid);
const nonce = unb64url(envelope.nonce);
const ct = unb64url(envelope.ct);
const tag = unb64url(envelope.tag);
const aadBuf = aadToBuffer(aad);
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
decipher.setAAD(aadBuf);
decipher.setAuthTag(tag);
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
return pt.toString("utf8");
};
export const decryptAndMaybeReencrypt = ({
aad,
envelope,
}: {
envelope: SecretEnvelopeV1;
aad: AAD;
}): { plaintext: string; updatedEnvelope: SecretEnvelopeV1 | null } => {
const plaintext = decryptSecret({ envelope, aad });
const currentKid = getCurrentKid(envelope.ring);
if (envelope.kid === currentKid) {
return { plaintext, updatedEnvelope: null };
}
const updatedEnvelope = encryptSecret({
ring: envelope.ring,
plaintext,
aad,
});
return { plaintext, updatedEnvelope };
};
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "public"."Credential" ADD COLUMN "encryptedKey" TEXT;
+10 -9
View File
@@ -300,17 +300,18 @@ model EventType {
}
model Credential {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
// @@type is deprecated
type String
key Json
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int?
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
type String
key Json
encryptedKey String?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int?
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
// How to make it a required column?
appId String?
appId String?
// paid apps
subscriptionId String?
+2
View File
@@ -12,6 +12,7 @@ export const credentialForCalendarServiceSelect = {
},
teamId: true,
key: true,
encryptedKey: true,
invalid: true,
delegationCredentialId: true,
} satisfies Prisma.CredentialSelect;
@@ -21,6 +22,7 @@ export const safeCredentialSelect = {
type: true,
/** Omitting to avoid frontend leaks */
// key: true,
// encryptedKey: true,
userId: true,
user: {
select: {
@@ -41,6 +41,7 @@ export const calendarOverlayHandler = async ({ ctx, input }: ListOptions) => {
id: true,
type: true,
key: true,
encryptedKey: true,
userId: true,
teamId: true,
appId: true,
@@ -127,6 +127,7 @@ export const getHostsWithLocationOptionsHandler = async ({
credentials: host.user.credentials.map((cred) => ({
...cred,
key: {} as Prisma.JsonValue,
encryptedKey: null,
})),
}));
+2 -1
View File
@@ -55,7 +55,8 @@ export type Office365CredentialPayload = CredentialPayload & {
} | null;
};
export type CredentialFrontendPayload = Omit<CredentialPayload, "key"> & {
export type CredentialFrontendPayload = Omit<CredentialPayload, "key" | "encryptedKey"> & {
/** We should type error if keys are leaked to the frontend */
key?: never;
encryptedKey?: never;
};