Files
kandev/apps/web/components/github/github-app-onboarding-model.ts
T
João Salavisa 0e82887ef6 feat(i18n): localize Settings → Integrations → GitHub connection and auth (#2155)
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 14:16:26 +02:00

151 lines
4.9 KiB
TypeScript

import { ApiError } from "@/lib/api/client";
import type {
GitHubAppManifestOwnerType,
GitHubAppOwnerType,
GitHubAppRegistrationErrorBody,
GitHubAppVisibility,
} from "@/lib/types/github";
import { t } from "@/lib/i18n";
const ownerPattern = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
const slugPattern = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,98}[A-Za-z0-9])?$/;
export type AppSetupErrors = Partial<
Record<
| "displayName"
| "ownerLogin"
| "publicBaseUrl"
| "appId"
| "clientId"
| "clientSecret"
| "privateKey"
| "webhookSecret"
| "slug",
string
>
>;
export function normalizePublicBaseUrl(rawValue: string, errors: AppSetupErrors) {
let url: URL;
try {
url = new URL(rawValue.trim());
} catch {
errors.publicBaseUrl = t("github:enterAPublicHttpsOrigin");
return null;
}
if (url.protocol !== "https:" || url.username || url.password) {
errors.publicBaseUrl = t("github:enterAPublicHttpsOrigin");
return null;
}
if (url.hostname === "localhost" || url.hostname.endsWith(".localhost")) {
errors.publicBaseUrl = t("github:enterAPublicHostNotLocalhost");
return null;
}
if ((url.pathname && url.pathname !== "/") || url.search || url.hash) {
errors.publicBaseUrl = t("github:enterAnOriginWithoutAPath");
return null;
}
return url.origin;
}
export function validateRegistrationBasics(input: {
displayName: string;
ownerType: GitHubAppManifestOwnerType | GitHubAppOwnerType;
ownerLogin: string;
publicBaseUrl: string;
}) {
const errors: AppSetupErrors = {};
const displayName = input.displayName.trim();
const ownerLogin = input.ownerLogin.trim();
if (!displayName || displayName.length > 100)
errors.displayName = t("github:enterANameUpTo100Characters");
if (!ownerPattern.test(ownerLogin)) errors.ownerLogin = t("github:enterAValidGithubAccountLogin");
const publicBaseUrl = normalizePublicBaseUrl(input.publicBaseUrl, errors);
return { displayName, ownerLogin, publicBaseUrl, errors };
}
export function validateImportSecrets(input: {
appId: string;
clientId: string;
clientSecret: string;
privateKey: string;
webhookSecret: string;
slug: string;
}) {
const errors: AppSetupErrors = {};
const appId = Number(input.appId);
if (!Number.isSafeInteger(appId) || appId <= 0)
errors.appId = t("github:enterTheNumericGithubAppId");
if (!input.clientId.trim()) errors.clientId = t("github:enterTheAppClientId");
if (!input.clientSecret) errors.clientSecret = t("github:enterTheAppClientSecret");
if (!input.privateKey.trim()) errors.privateKey = t("github:pasteTheAppPrivateKey");
if (!input.webhookSecret) errors.webhookSecret = t("github:enterTheWebhookSecret");
if (!slugPattern.test(input.slug.trim())) errors.slug = t("github:enterTheGithubAppSlug");
return { appId, errors };
}
export function submitManifestToGitHub(registrationUrl: string, manifest: unknown) {
const form = document.createElement("form");
form.method = "POST";
form.action = registrationUrl;
const input = document.createElement("input");
input.type = "hidden";
input.name = "manifest";
input.value = JSON.stringify(manifest);
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
export type CallbackNotice = {
tone: "success" | "info" | "error";
title: string;
description: string;
};
export function callbackNotice(code: string): CallbackNotice {
if (code === "app_registered")
return {
tone: "success",
title: t("github:githubAppAdded"),
description: t("github:theAppIsReadyToSelect"),
};
if (code === "app_connected")
return {
tone: "success",
title: t("github:githubAppConnected"),
description: t("github:workspaceAutomationNowUsesTheVerified"),
};
if (code === "personal_connected")
return {
tone: "success",
title: t("github:githubIdentityConnected"),
description: t("github:myGithubNowUsesYourVerified"),
};
if (code === "github_app_registration_cancelled")
return {
tone: "info",
title: t("github:githubAppSetupCancelled"),
description: t("github:theExistingWorkspaceConnectionWasNot"),
};
return {
tone: "error",
title: t("github:githubSetupWasNotCompleted"),
description: t("github:theGithubResponseCouldNotBe"),
};
}
export function appRegistrationError(error: unknown) {
if (!(error instanceof ApiError) || !error.body || typeof error.body !== "object") return null;
return error.body as GitHubAppRegistrationErrorBody;
}
export function githubAppSettingsURL(ownerType: GitHubAppOwnerType, owner: string, slug: string) {
if (!owner || !slug) return "https://github.com/settings/apps";
return ownerType === "Organization"
? `https://github.com/organizations/${encodeURIComponent(owner)}/settings/apps/${encodeURIComponent(slug)}`
: `https://github.com/settings/apps/${encodeURIComponent(slug)}`;
}
export const defaultVisibility: GitHubAppVisibility = "private";