Files
Benny JooandDevin AI ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

132 lines
3.9 KiB
TypeScript

import { Dub } from "dub-package";
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
import logger from "@calcom/lib/logger";
import type { AnalyticsService, SendEventProps } from "@calcom/types/AnalyticsService";
import type { CredentialPayload } from "@calcom/types/Credential";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
import type { DubOAuthToken } from "./type";
class DubService implements AnalyticsService {
private dubClient?: Dub;
private client_id = "";
private client_secret = "";
private log = logger.getSubLogger({ prefix: ["[[lib]] dub"] });
private credential: CredentialPayload;
constructor(credential: CredentialPayload) {
this.credential = credential;
this.client_id = "";
this.client_secret = "";
}
private async initClient() {
const appKeys = await getAppKeysFromSlug("dub");
const { client_id, client_secret } = appKeys;
if (!client_id || !client_secret) {
this.log.error("Dub.co app keys missing!");
return;
}
this.client_id = client_id as string;
this.client_secret = client_secret as string;
let token = this.credential.key as unknown as DubOAuthToken | undefined;
if (!token) return;
const isTokenExpired = (token: DubOAuthToken) => {
if (!token || !token.access_token) return true;
if (token.expiry_date) {
return token.expiry_date < Date.now();
}
return false;
};
if (isTokenExpired(token)) {
token = await this.refreshAccessToken(token.refresh_token);
if (!token) return;
}
this.dubClient = new Dub({ token: token.access_token });
}
private async refreshAccessToken(refreshToken: string): Promise<DubOAuthToken | undefined> {
try {
if (!refreshToken) return;
const newToken: DubOAuthToken = await refreshOAuthTokens(
async () => {
const response = await fetch(`https://api.dub.co/oauth/token`, {
method: "POST",
body: new URLSearchParams({
client_id: this.client_id,
client_secret: this.client_secret,
grant_type: "refresh_token",
refresh_token: refreshToken,
}).toString(),
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
if (!response.ok) {
const res = await response.json();
if (response.status === 401) {
await CredentialRepository.updateCredentialById({
id: this.credential.id,
data: {
invalid: true,
},
});
}
throw new Error(`Error refreshing dub token: ${res?.error?.message ?? response.statusText}`);
}
return await response.json();
},
"dub",
this.credential.userId
);
newToken.expiry_date = Date.now() + newToken.expires_in * 1000;
await CredentialRepository.updateCredentialById({
id: this.credential.id,
data: { key: newToken as any },
});
return newToken;
} catch (err) {
this.log.error(err);
throw err;
}
}
async sendEvent({ name, email, eventName, id, externalId }: SendEventProps): Promise<void> {
await this.initClient();
if (!this.dubClient) return;
await this.dubClient.track.lead({
clickId: id,
customerName: name,
customerEmail: email,
externalId: externalId ?? email,
eventName: eventName ?? "Cal.diy lead",
});
}
}
/**
* Factory function that creates a Dub Analytics service instance.
* This is exported instead of the class to prevent SDK types (Dub)
* from leaking into the emitted .d.ts file.
*/
export default function BuildAnalyticsService(credential: CredentialPayload): AnalyticsService {
return new DubService(credential);
}