0e82887ef6
Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
151 lines
4.9 KiB
TypeScript
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";
|