mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-16 19:46:58 +01:00
feat(integration): add github app authentication (#3968)
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
IconCode,
|
||||
IconGrid3x3,
|
||||
IconKey,
|
||||
IconMessage,
|
||||
IconPassword,
|
||||
IconPasswordUser,
|
||||
IconPlug,
|
||||
IconServer,
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
@@ -21,4 +23,7 @@ export const integrationSecretIcons = {
|
||||
topic: IconMessage,
|
||||
opnsenseApiKey: IconKey,
|
||||
opnsenseApiSecret: IconPassword,
|
||||
githubAppId: IconCode,
|
||||
githubInstallationId: IconPlug,
|
||||
privateKey: IconKey,
|
||||
} satisfies Record<IntegrationSecretKind, TablerIcon>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ChangeEventHandler, FocusEventHandler } from "react";
|
||||
import { PasswordInput, TextInput } from "@mantine/core";
|
||||
import { PasswordInput, Textarea, TextInput } from "@mantine/core";
|
||||
|
||||
import { integrationSecretKindObject } from "@homarr/definitions";
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
@@ -14,9 +14,9 @@ interface IntegrationSecretInputProps {
|
||||
label?: string;
|
||||
kind: IntegrationSecretKind;
|
||||
value?: string;
|
||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||
onFocus?: FocusEventHandler<HTMLInputElement>;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||
onChange: ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||
onFocus?: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,19 @@ export const IntegrationSecretInput = (props: IntegrationSecretInputProps) => {
|
||||
const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
||||
const t = useI18n();
|
||||
const Icon = integrationSecretIcons[kind];
|
||||
const { multiline } = integrationSecretKindObject[kind];
|
||||
if (multiline) {
|
||||
return (
|
||||
<Textarea
|
||||
{...props}
|
||||
label={props.label ?? t(`integration.secrets.kind.${kind}.label`)}
|
||||
w="100%"
|
||||
leftSection={<Icon size={20} stroke={1.5} />}
|
||||
autosize
|
||||
minRows={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
@@ -45,6 +58,21 @@ const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
||||
const PrivateSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
||||
const t = useI18n();
|
||||
const Icon = integrationSecretIcons[kind];
|
||||
const { multiline } = integrationSecretKindObject[kind];
|
||||
|
||||
if (multiline) {
|
||||
return (
|
||||
<Textarea
|
||||
{...props}
|
||||
label={props.label ?? t(`integration.secrets.kind.${kind}.label`)}
|
||||
description={t("integration.secrets.secureNotice")}
|
||||
w="100%"
|
||||
leftSection={<Icon size={20} stroke={1.5} />}
|
||||
autosize
|
||||
minRows={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PasswordInput
|
||||
|
||||
@@ -25,16 +25,6 @@ export const testConnectionAsync = async (
|
||||
integrationUrl: integration.url,
|
||||
});
|
||||
|
||||
const formSecrets = integration.secrets
|
||||
.filter((secret) => secret.value !== null)
|
||||
.map((secret) => ({
|
||||
...secret,
|
||||
// We ensured above that the value is not null
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
value: secret.value!,
|
||||
source: "form" as const,
|
||||
}));
|
||||
|
||||
const decryptedDbSecrets = dbSecrets
|
||||
.map((secret) => {
|
||||
try {
|
||||
@@ -55,6 +45,15 @@ export const testConnectionAsync = async (
|
||||
})
|
||||
.filter((secret) => secret !== null);
|
||||
|
||||
const formSecrets = integration.secrets
|
||||
.map((secret) => ({
|
||||
...secret,
|
||||
// If the value is not defined in the form (because we only changed other values) we use the existing value from the db if it exists
|
||||
value: secret.value ?? decryptedDbSecrets.find((dbSecret) => dbSecret.kind === secret.kind)?.value ?? null,
|
||||
source: "form" as const,
|
||||
}))
|
||||
.filter((secret): secret is SourcedIntegrationSecret<"form"> => secret.value !== null);
|
||||
|
||||
const sourcedSecrets = [...formSecrets, ...decryptedDbSecrets];
|
||||
const secretKinds = getSecretKindOption(integration.kind, sourcedSecrets);
|
||||
|
||||
@@ -89,10 +88,10 @@ export const testConnectionAsync = async (
|
||||
return result;
|
||||
};
|
||||
|
||||
interface SourcedIntegrationSecret {
|
||||
interface SourcedIntegrationSecret<TSource extends string = "db" | "form"> {
|
||||
kind: IntegrationSecretKind;
|
||||
value: string;
|
||||
source: "db" | "form";
|
||||
source: TSource;
|
||||
}
|
||||
|
||||
const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedIntegrationSecret[]) => {
|
||||
@@ -111,7 +110,9 @@ const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedInteg
|
||||
}
|
||||
|
||||
const onlyFormSecretsKindOptions = matchingSecretKindOptions.filter((secretKinds) =>
|
||||
sourcedSecrets.filter((secret) => secretKinds.includes(secret.kind)).every((secret) => secret.source === "form"),
|
||||
secretKinds.every((secretKind) =>
|
||||
sourcedSecrets.find((secret) => secret.kind === secretKind && secret.source === "form"),
|
||||
),
|
||||
);
|
||||
|
||||
if (onlyFormSecretsKindOptions.length >= 1) {
|
||||
|
||||
@@ -265,4 +265,77 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("with input of existing github app", async () => {
|
||||
// Arrange
|
||||
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
|
||||
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||
factorySpy.mockReturnValue(
|
||||
Promise.resolve({
|
||||
testConnectionAsync: async () => await Promise.resolve({ success: true }),
|
||||
} as homarrIntegrations.PiHoleIntegrationV6),
|
||||
);
|
||||
optionsSpy.mockReturnValue([[], ["githubAppId", "githubInstallationId", "privateKey"]]);
|
||||
|
||||
const integration = {
|
||||
id: "new",
|
||||
name: "GitHub",
|
||||
url: "https://api.github.com",
|
||||
kind: "github" as const,
|
||||
secrets: [
|
||||
{
|
||||
kind: "githubAppId" as const,
|
||||
value: "345",
|
||||
},
|
||||
{
|
||||
kind: "githubInstallationId" as const,
|
||||
value: "456",
|
||||
},
|
||||
{
|
||||
kind: "privateKey" as const,
|
||||
value: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dbSecrets = [
|
||||
{
|
||||
kind: "githubAppId" as const,
|
||||
value: "123.encrypted" as const,
|
||||
},
|
||||
{
|
||||
kind: "githubInstallationId" as const,
|
||||
value: "234.encrypted" as const,
|
||||
},
|
||||
{
|
||||
kind: "privateKey" as const,
|
||||
value: "privateKey.encrypted" as const,
|
||||
},
|
||||
];
|
||||
|
||||
// Act
|
||||
await testConnectionAsync(integration, dbSecrets);
|
||||
|
||||
// Assert
|
||||
expect(factorySpy).toHaveBeenCalledWith({
|
||||
id: "new",
|
||||
name: "GitHub",
|
||||
url: "https://api.github.com",
|
||||
kind: "github" as const,
|
||||
decryptedSecrets: [
|
||||
expect.objectContaining({
|
||||
kind: "githubAppId",
|
||||
value: "345",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "githubInstallationId",
|
||||
value: "456",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "privateKey",
|
||||
value: "privateKey",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"dayjs": "^1.11.18",
|
||||
"dns-caching": "^0.2.5",
|
||||
"next": "15.5.2",
|
||||
"octokit": "^5.0.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"undici": "7.15.0",
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./fetch-http-error-handler";
|
||||
export * from "./ofetch-http-error-handler";
|
||||
export * from "./axios-http-error-handler";
|
||||
export * from "./tsdav-http-error-handler";
|
||||
export * from "./octokit-http-error-handler";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { RequestError as OctokitRequestError } from "octokit";
|
||||
|
||||
import type { AnyRequestError } from "../request-error";
|
||||
import { ResponseError } from "../response-error";
|
||||
import { HttpErrorHandler } from "./http-error-handler";
|
||||
|
||||
export class OctokitHttpErrorHandler extends HttpErrorHandler {
|
||||
/**
|
||||
* I wasn't able to get a request error triggered. Therefore we ignore them for now
|
||||
* and just forward them as unknown errors
|
||||
*/
|
||||
handleRequestError(_: unknown): AnyRequestError | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
handleResponseError(error: unknown): ResponseError | undefined {
|
||||
if (!(error instanceof OctokitRequestError)) return undefined;
|
||||
|
||||
return new ResponseError({
|
||||
status: error.status,
|
||||
url: error.response?.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,19 @@ import type { AtLeastOneOf } from "@homarr/common/types";
|
||||
import { createDocumentationLink } from "./docs";
|
||||
|
||||
export const integrationSecretKindObject = {
|
||||
apiKey: { isPublic: false },
|
||||
username: { isPublic: true },
|
||||
password: { isPublic: false },
|
||||
tokenId: { isPublic: true },
|
||||
realm: { isPublic: true },
|
||||
personalAccessToken: { isPublic: false },
|
||||
topic: { isPublic: true },
|
||||
opnsenseApiKey: { isPublic: false },
|
||||
opnsenseApiSecret: { isPublic: false },
|
||||
} satisfies Record<string, { isPublic: boolean }>;
|
||||
apiKey: { isPublic: false, multiline: false },
|
||||
username: { isPublic: true, multiline: false },
|
||||
password: { isPublic: false, multiline: false },
|
||||
tokenId: { isPublic: true, multiline: false },
|
||||
realm: { isPublic: true, multiline: false },
|
||||
personalAccessToken: { isPublic: false, multiline: false },
|
||||
topic: { isPublic: true, multiline: false },
|
||||
opnsenseApiKey: { isPublic: false, multiline: false },
|
||||
opnsenseApiSecret: { isPublic: false, multiline: false },
|
||||
privateKey: { isPublic: false, multiline: true },
|
||||
githubAppId: { isPublic: true, multiline: false },
|
||||
githubInstallationId: { isPublic: true, multiline: false },
|
||||
} satisfies Record<string, { isPublic: boolean; multiline: boolean }>;
|
||||
|
||||
export const integrationSecretKinds = objectKeys(integrationSecretKindObject);
|
||||
|
||||
@@ -211,7 +214,7 @@ export const integrationDefs = {
|
||||
},
|
||||
github: {
|
||||
name: "Github",
|
||||
secretKinds: [[], ["personalAccessToken"]],
|
||||
secretKinds: [[], ["personalAccessToken"], ["githubAppId", "githubInstallationId", "privateKey"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
|
||||
category: ["releasesProvider"],
|
||||
defaultUrl: "https://api.github.com",
|
||||
@@ -259,7 +262,7 @@ export const integrationDefs = {
|
||||
},
|
||||
gitHubContainerRegistry: {
|
||||
name: "GitHub Container Registry",
|
||||
secretKinds: [[], ["personalAccessToken"]],
|
||||
secretKinds: [[], ["personalAccessToken"], ["githubAppId", "githubInstallationId", "privateKey"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
|
||||
category: ["releasesProvider"],
|
||||
defaultUrl: "https://api.github.com",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@jellyfin/sdk": "^0.11.0",
|
||||
"@octokit/auth-app": "^8.1.0",
|
||||
"maria2": "^0.4.1",
|
||||
"node-ical": "^0.20.1",
|
||||
"octokit": "^5.0.3",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
AxiosHttpErrorHandler,
|
||||
FetchHttpErrorHandler,
|
||||
OctokitHttpErrorHandler,
|
||||
OFetchHttpErrorHandler,
|
||||
TsdavHttpErrorHandler,
|
||||
} from "@homarr/common/server";
|
||||
@@ -11,3 +12,4 @@ export const integrationFetchHttpErrorHandler = new IntegrationHttpErrorHandler(
|
||||
export const integrationOFetchHttpErrorHandler = new IntegrationHttpErrorHandler(new OFetchHttpErrorHandler());
|
||||
export const integrationAxiosHttpErrorHandler = new IntegrationHttpErrorHandler(new AxiosHttpErrorHandler());
|
||||
export const integrationTsdavHttpErrorHandler = new IntegrationHttpErrorHandler(new TsdavHttpErrorHandler());
|
||||
export const integrationOctokitHttpErrorHandler = new IntegrationHttpErrorHandler(new OctokitHttpErrorHandler());
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { Octokit, RequestError } from "octokit";
|
||||
import type { fetch } from "undici";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { HandleIntegrationErrors } from "../base/errors/decorator";
|
||||
import { integrationOctokitHttpErrorHandler } from "../base/errors/http";
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
@@ -18,23 +21,21 @@ import type {
|
||||
|
||||
const localLogger = logger.child({ module: "GitHubContainerRegistryIntegration" });
|
||||
|
||||
@HandleIntegrationErrors([integrationOctokitHttpErrorHandler])
|
||||
export class GitHubContainerRegistryIntegration extends Integration implements ReleasesProviderIntegration {
|
||||
private static readonly userAgent = "Homarr-Lab/Homarr:GitHubContainerRegistryIntegration";
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const headers: RequestInit["headers"] = {
|
||||
"User-Agent": GitHubContainerRegistryIntegration.userAgent,
|
||||
};
|
||||
const api = this.getApi(input.fetchAsync);
|
||||
|
||||
if (this.hasSecretValue("personalAccessToken"))
|
||||
headers.Authorization = `Bearer ${this.getSecretValue("personalAccessToken")}`;
|
||||
|
||||
const response = await input.fetchAsync(this.url("/octocat"), {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult(response);
|
||||
if (this.hasSecretValue("personalAccessToken")) {
|
||||
await api.rest.users.getAuthenticated();
|
||||
} else if (this.hasSecretValue("githubAppId")) {
|
||||
await api.rest.apps.getInstallation({
|
||||
installation_id: Number(this.getSecretValue("githubInstallationId")),
|
||||
});
|
||||
} else {
|
||||
await api.request("GET /octocat");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -131,15 +132,38 @@ export class GitHubContainerRegistryIntegration extends Integration implements R
|
||||
}
|
||||
}
|
||||
|
||||
private getApi() {
|
||||
private getAuthProperties(): Pick<OctokitOptions, "auth" | "authStrategy"> {
|
||||
if (this.hasSecretValue("personalAccessToken"))
|
||||
return {
|
||||
auth: this.getSecretValue("personalAccessToken"),
|
||||
};
|
||||
|
||||
if (this.hasSecretValue("githubAppId"))
|
||||
return {
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: this.getSecretValue("githubAppId"),
|
||||
installationId: this.getSecretValue("githubInstallationId"),
|
||||
privateKey: this.getSecretValue("privateKey"),
|
||||
} satisfies Parameters<typeof createAppAuth>[0],
|
||||
};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private getApi(customFetch?: typeof fetch) {
|
||||
return new Octokit({
|
||||
baseUrl: this.url("/").origin,
|
||||
request: {
|
||||
fetch: fetchWithTrustedCertificatesAsync,
|
||||
fetch: customFetch ?? fetchWithTrustedCertificatesAsync,
|
||||
},
|
||||
userAgent: GitHubContainerRegistryIntegration.userAgent,
|
||||
throttle: { enabled: false }, // Disable throttling for this integration, Octokit will retry by default after a set time, thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
|
||||
...(this.hasSecretValue("personalAccessToken") ? { auth: this.getSecretValue("personalAccessToken") } : {}),
|
||||
// Disable throttling for this integration, Octokit will retry by default after a set time,
|
||||
// thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
|
||||
throttle: { enabled: false },
|
||||
...this.getAuthProperties(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type OctokitOptions = Exclude<ConstructorParameters<typeof Octokit>[0], undefined>;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Octokit, RequestError } from "octokit";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { Octokit, RequestError as OctokitRequestError } from "octokit";
|
||||
import type { fetch } from "undici";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { HandleIntegrationErrors } from "../base/errors/decorator";
|
||||
import { integrationOctokitHttpErrorHandler } from "../base/errors/http";
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
@@ -18,23 +21,21 @@ import type {
|
||||
|
||||
const localLogger = logger.child({ module: "GithubIntegration" });
|
||||
|
||||
@HandleIntegrationErrors([integrationOctokitHttpErrorHandler])
|
||||
export class GithubIntegration extends Integration implements ReleasesProviderIntegration {
|
||||
private static readonly userAgent = "Homarr-Lab/Homarr:GithubIntegration";
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const headers: RequestInit["headers"] = {
|
||||
"User-Agent": GithubIntegration.userAgent,
|
||||
};
|
||||
const api = this.getApi(input.fetchAsync);
|
||||
|
||||
if (this.hasSecretValue("personalAccessToken"))
|
||||
headers.Authorization = `Bearer ${this.getSecretValue("personalAccessToken")}`;
|
||||
|
||||
const response = await input.fetchAsync(this.url("/octocat"), {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult(response);
|
||||
if (this.hasSecretValue("personalAccessToken")) {
|
||||
await api.rest.users.getAuthenticated();
|
||||
} else if (this.hasSecretValue("githubAppId")) {
|
||||
await api.rest.apps.getInstallation({
|
||||
installation_id: Number(this.getSecretValue("githubInstallationId")),
|
||||
});
|
||||
} else {
|
||||
await api.request("GET /octocat");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -91,7 +92,7 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
|
||||
|
||||
return getLatestRelease(releasesProviderResponse, repository, details);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof RequestError ? error.message : String(error);
|
||||
const errorMessage = error instanceof OctokitRequestError ? error.message : String(error);
|
||||
|
||||
localLogger.warn(`Failed to get releases for ${owner}\\${name} with Github integration`, {
|
||||
owner,
|
||||
@@ -131,21 +132,44 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
|
||||
localLogger.warn(`Failed to get details for ${owner}\\${name} with Github integration`, {
|
||||
owner,
|
||||
name,
|
||||
error: error instanceof RequestError ? error.message : String(error),
|
||||
error: error instanceof OctokitRequestError ? error.message : String(error),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getApi() {
|
||||
private getAuthProperties(): Pick<OctokitOptions, "auth" | "authStrategy"> {
|
||||
if (this.hasSecretValue("personalAccessToken"))
|
||||
return {
|
||||
auth: this.getSecretValue("personalAccessToken"),
|
||||
};
|
||||
|
||||
if (this.hasSecretValue("githubAppId"))
|
||||
return {
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: this.getSecretValue("githubAppId"),
|
||||
installationId: this.getSecretValue("githubInstallationId"),
|
||||
privateKey: this.getSecretValue("privateKey"),
|
||||
} satisfies Parameters<typeof createAppAuth>[0],
|
||||
};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private getApi(customFetch?: typeof fetch) {
|
||||
return new Octokit({
|
||||
baseUrl: this.url("/").origin,
|
||||
request: {
|
||||
fetch: fetchWithTrustedCertificatesAsync,
|
||||
fetch: customFetch ?? fetchWithTrustedCertificatesAsync,
|
||||
},
|
||||
userAgent: GithubIntegration.userAgent,
|
||||
throttle: { enabled: false }, // Disable throttling for this integration, Octokit will retry by default after a set time, thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
|
||||
...(this.hasSecretValue("personalAccessToken") ? { auth: this.getSecretValue("personalAccessToken") } : {}),
|
||||
// Disable throttling for this integration, Octokit will retry by default after a set time,
|
||||
// thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
|
||||
throttle: { enabled: false },
|
||||
...this.getAuthProperties(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type OctokitOptions = Exclude<ConstructorParameters<typeof Octokit>[0], undefined>;
|
||||
|
||||
@@ -952,6 +952,18 @@
|
||||
"opnsenseApiSecret": {
|
||||
"label": "API Key (Secret)",
|
||||
"newLabel": "New API Key (Secret)"
|
||||
},
|
||||
"githubAppId": {
|
||||
"label": "App Id",
|
||||
"newLabel": "New App Id"
|
||||
},
|
||||
"githubInstallationId": {
|
||||
"label": "Installation Id",
|
||||
"newLabel": "New Installation Id"
|
||||
},
|
||||
"privateKey": {
|
||||
"label": "Private key",
|
||||
"newLabel": "New private key"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -870,6 +870,9 @@ importers:
|
||||
next:
|
||||
specifier: 15.5.2
|
||||
version: 15.5.2(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.92.1)
|
||||
octokit:
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
react:
|
||||
specifier: 19.1.1
|
||||
version: 19.1.1
|
||||
@@ -1462,6 +1465,9 @@ importers:
|
||||
'@jellyfin/sdk':
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0(axios@1.11.0)
|
||||
'@octokit/auth-app':
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0
|
||||
maria2:
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1
|
||||
@@ -3471,12 +3477,12 @@ packages:
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@octokit/app@16.0.1':
|
||||
resolution: {integrity: sha512-kgTeTsWmpUX+s3Fs4EK4w1K+jWCDB6ClxLSWUWTyhlw7+L3jHtuXDR4QtABu2GsmCMdk67xRhruiXotS3ay3Yw==}
|
||||
'@octokit/app@16.1.0':
|
||||
resolution: {integrity: sha512-OdKHnm0CYLk8Setr47CATT4YnRTvWkpTYvE+B/l2B0mjszlfOIit3wqPHVslD2jfc1bD4UbO7Mzh6gjCuMZKsA==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/auth-app@8.0.1':
|
||||
resolution: {integrity: sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg==}
|
||||
'@octokit/auth-app@8.1.0':
|
||||
resolution: {integrity: sha512-6bWhyvLXqCSfHiqlwzn9pScLZ+Qnvh/681GR/UEEPCMIVwfpRDBw0cCzy3/t2Dq8B7W2X/8pBgmw6MOiyE0DXQ==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/auth-oauth-app@9.0.1':
|
||||
@@ -11828,9 +11834,9 @@ snapshots:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.17.1
|
||||
|
||||
'@octokit/app@16.0.1':
|
||||
'@octokit/app@16.1.0':
|
||||
dependencies:
|
||||
'@octokit/auth-app': 8.0.1
|
||||
'@octokit/auth-app': 8.1.0
|
||||
'@octokit/auth-unauthenticated': 7.0.1
|
||||
'@octokit/core': 7.0.2
|
||||
'@octokit/oauth-app': 8.0.1
|
||||
@@ -11838,7 +11844,7 @@ snapshots:
|
||||
'@octokit/types': 14.1.0
|
||||
'@octokit/webhooks': 14.0.0
|
||||
|
||||
'@octokit/auth-app@8.0.1':
|
||||
'@octokit/auth-app@8.1.0':
|
||||
dependencies:
|
||||
'@octokit/auth-oauth-app': 9.0.1
|
||||
'@octokit/auth-oauth-user': 6.0.0
|
||||
@@ -11885,7 +11891,7 @@ snapshots:
|
||||
'@octokit/graphql': 9.0.1
|
||||
'@octokit/request': 10.0.2
|
||||
'@octokit/request-error': 7.0.0
|
||||
'@octokit/types': 14.0.0
|
||||
'@octokit/types': 14.1.0
|
||||
before-after-hook: 4.0.0
|
||||
universal-user-agent: 7.0.2
|
||||
|
||||
@@ -11944,13 +11950,13 @@ snapshots:
|
||||
dependencies:
|
||||
'@octokit/core': 7.0.2
|
||||
'@octokit/request-error': 7.0.0
|
||||
'@octokit/types': 14.0.0
|
||||
'@octokit/types': 14.1.0
|
||||
bottleneck: 2.19.5
|
||||
|
||||
'@octokit/plugin-throttling@11.0.1(@octokit/core@7.0.2)':
|
||||
dependencies:
|
||||
'@octokit/core': 7.0.2
|
||||
'@octokit/types': 14.0.0
|
||||
'@octokit/types': 14.1.0
|
||||
bottleneck: 2.19.5
|
||||
|
||||
'@octokit/request-error@7.0.0':
|
||||
@@ -17406,7 +17412,7 @@ snapshots:
|
||||
|
||||
octokit@5.0.3:
|
||||
dependencies:
|
||||
'@octokit/app': 16.0.1
|
||||
'@octokit/app': 16.1.0
|
||||
'@octokit/core': 7.0.2
|
||||
'@octokit/oauth-app': 8.0.1
|
||||
'@octokit/plugin-paginate-graphql': 6.0.0(@octokit/core@7.0.2)
|
||||
|
||||
Reference in New Issue
Block a user