diff --git a/backend/package-lock.json b/backend/package-lock.json index c6ac0b147..db4e9e7dd 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -83,6 +83,7 @@ "ioredis": "^5.3.2", "isomorphic-dompurify": "^2.22.0", "jmespath": "^0.16.0", + "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", "jsrp": "^0.2.4", "jwks-rsa": "^3.1.0", @@ -143,6 +144,7 @@ "@smithy/types": "^4.3.1", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", + "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.5", "@types/jsrp": "^0.2.6", "@types/libsodium-wrappers": "^0.7.13", @@ -13160,6 +13162,13 @@ "integrity": "sha512-pegh49FtNsC389Flyo9y8AfkVIZn9MMPE9yJrO9svhq6Fks2MwymULWjZqySuxmctd3ZH4/n7Mr98D+1Qo5vGA==", "dev": true }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -20452,6 +20461,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, diff --git a/backend/package.json b/backend/package.json index 0c8464eaf..8fece4ebe 100644 --- a/backend/package.json +++ b/backend/package.json @@ -87,6 +87,7 @@ "@smithy/types": "^4.3.1", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", + "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.5", "@types/jsrp": "^0.2.6", "@types/libsodium-wrappers": "^0.7.13", @@ -203,6 +204,7 @@ "ioredis": "^5.3.2", "isomorphic-dompurify": "^2.22.0", "jmespath": "^0.16.0", + "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", "jsrp": "^0.2.4", "jwks-rsa": "^3.1.0", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 8ca4288b5..d075d21a8 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -114,6 +114,7 @@ import { TSlackServiceFactory } from "@app/services/slack/slack-service"; import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service"; import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; import { TTotpServiceFactory } from "@app/services/totp/totp-service"; +import { TUpgradePathService } from "@app/services/upgrade-path/upgrade-path-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; @@ -312,6 +313,7 @@ declare module "fastify" { identityAuthTemplate: TIdentityAuthTemplateServiceFactory; notification: TNotificationServiceFactory; offlineUsageReport: TOfflineUsageReportServiceFactory; + upgradePath: TUpgradePathService; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index eccad2956..6a4c1d995 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -309,6 +309,7 @@ import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry- import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; import { totpConfigDALFactory } from "@app/services/totp/totp-config-dal"; import { totpServiceFactory } from "@app/services/totp/totp-service"; +import { upgradePathServiceFactory } from "@app/services/upgrade-path/upgrade-path-service"; import { userDALFactory } from "@app/services/user/user-dal"; import { userServiceFactory } from "@app/services/user/user-service"; import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; @@ -759,6 +760,8 @@ export const registerRoutes = async ( userAliasDAL }); + const upgradePathService = upgradePathServiceFactory({ keyStore }); + const totpService = totpServiceFactory({ totpConfigDAL, userDAL, @@ -2174,7 +2177,8 @@ export const registerRoutes = async ( reminder: reminderService, bus: eventBusService, sse: sseService, - notification: notificationService + notification: notificationService, + upgradePath: upgradePathService }); const cronJobs: CronJob[] = []; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 6108be32b..78ed50a76 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -51,6 +51,7 @@ import { registerSecretRequestsRouter } from "./secret-requests-router"; import { registerSecretSharingRouter } from "./secret-sharing-router"; import { registerSecretTagRouter } from "./secret-tag-router"; import { registerSlackRouter } from "./slack-router"; +import { registerUpgradePathRouter } from "./upgrade-path-router"; import { registerSsoRouter } from "./sso-router"; import { registerUserActionRouter } from "./user-action-router"; import { registerUserEngagementRouter } from "./user-engagement-router"; @@ -188,4 +189,5 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { ); await server.register(registerEventRouter, { prefix: "/events" }); + await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" }); }; diff --git a/backend/src/server/routes/v1/upgrade-path-router.ts b/backend/src/server/routes/v1/upgrade-path-router.ts new file mode 100644 index 000000000..30b5e9561 --- /dev/null +++ b/backend/src/server/routes/v1/upgrade-path-router.ts @@ -0,0 +1,143 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { BadRequestError } from "@app/lib/errors"; +import { publicEndpointLimit } from "@app/server/config/rateLimiter"; + +const versionSchema = z + .string() + .min(1) + .max(50) + .regex(new RE2(/^[a-zA-Z0-9._/-]+$/), "Invalid version format"); +const booleanSchema = z.boolean().default(false); +const queryBooleanSchema = z + .union([z.boolean(), z.string()]) + .transform((val) => { + if (typeof val === "string") { + return val === "true" || val === "1"; + } + return val; + }) + .default(false); + +export const registerUpgradePathRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/versions", + config: { + rateLimit: publicEndpointLimit + }, + schema: { + querystring: z.object({ + includePrerelease: queryBooleanSchema + }), + response: { + 200: z.object({ + versions: z.array( + z.object({ + tagName: z.string(), + name: z.string(), + publishedAt: z.string(), + prerelease: z.boolean(), + draft: z.boolean() + }) + ) + }) + } + }, + handler: async (req) => { + try { + const { includePrerelease } = req.query; + const versions = await req.server.services.upgradePath.getGitHubReleases(includePrerelease); + + return { + versions + }; + } catch (error) { + req.log.error(error, "Failed to fetch versions"); + if (error instanceof z.ZodError) { + throw new BadRequestError({ message: "Invalid query parameters" }); + } + throw new BadRequestError({ message: "Failed to fetch GitHub releases" }); + } + } + }); + + server.route({ + method: "POST", + url: "/calculate", + config: { + rateLimit: publicEndpointLimit + }, + schema: { + body: z.object({ + fromVersion: versionSchema, + toVersion: versionSchema, + includePrerelease: booleanSchema + }), + response: { + 200: z.object({ + path: z.array( + z.object({ + version: z.string(), + name: z.string(), + publishedAt: z.string(), + prerelease: z.boolean() + }) + ), + breakingChanges: z.array( + z.object({ + version: z.string(), + changes: z.array( + z.object({ + title: z.string(), + description: z.string(), + action: z.string() + }) + ) + }) + ), + features: z.array( + z.object({ + version: z.string(), + name: z.string(), + body: z.string(), + publishedAt: z.string() + }) + ), + hasDbMigration: z.boolean(), + config: z.record(z.unknown()) + }) + } + }, + handler: async (req) => { + try { + const { fromVersion, toVersion, includePrerelease } = req.body; + + req.log.info({ fromVersion, toVersion, includePrerelease }, "Calculating upgrade path"); + + const result = await req.server.services.upgradePath.calculateUpgradePath( + fromVersion, + toVersion, + includePrerelease + ); + + req.log.info( + { pathLength: result.path.length, hasBreaking: result.breakingChanges.length > 0 }, + "Upgrade path calculated" + ); + + return result; + } catch (error) { + req.log.error(error, "Failed to calculate upgrade path"); + if (error instanceof z.ZodError) { + throw new BadRequestError({ message: `Invalid input: ${error.errors.map((e) => e.message).join(", ")}` }); + } + if (error instanceof Error) { + throw new BadRequestError({ message: error.message }); + } + throw new BadRequestError({ message: "Failed to calculate upgrade path" }); + } + } + }); +}; diff --git a/backend/src/services/upgrade-path/github-client.ts b/backend/src/services/upgrade-path/github-client.ts new file mode 100644 index 000000000..2982e297a --- /dev/null +++ b/backend/src/services/upgrade-path/github-client.ts @@ -0,0 +1,187 @@ +/* eslint-disable no-await-in-loop */ +import RE2 from "re2"; + +import { FormattedRelease, GitHubApiError, GitHubRelease } from "./types"; + +interface GitHubClientConfig { + token?: string; + timeout: number; + maxRetries: number; + retryDelay: number; + maxPagesPerRequest: number; + perPage: number; +} + +interface RateLimitInfo { + remaining: number; + reset: Date; + used: number; + limit: number; +} + +const getDefaultConfig = (): GitHubClientConfig => ({ + token: process.env.GITHUB_TOKEN, + timeout: 30000, + maxRetries: 3, + retryDelay: 1000, + maxPagesPerRequest: 10, + perPage: 100 +}); + +const getHeaders = (token?: string): Record => { + const headers: Record = { + Accept: "application/vnd.github.v3+json", + "User-Agent": "Infisical-Upgrade-Path-Tool/1.0", + "X-GitHub-Api-Version": "2022-11-28" + }; + + if (token) { + headers.Authorization = `token ${token}`; + } + + return headers; +}; + +const delay = (ms: number): Promise => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}; + +const isMainInfisicalRelease = (tagName: string): boolean => { + if ( + tagName.startsWith("infisical-cli/") || + tagName.startsWith("infisical-k8-operator/") || + tagName.startsWith("infisical-k8s-operator/") + ) { + return false; + } + return tagName.startsWith("v") || tagName.startsWith("infisical/v") || new RE2(/^\d+\.\d+\.\d+/).test(tagName); +}; + +const normalizeVersion = (tagName: string): string => { + const versionMatch = tagName.match(new RE2(/(\d+\.\d+\.\d+(?:\.\d+)?)/)); + if (versionMatch) { + return `v${versionMatch[1]}`; + } + + if (tagName.startsWith("infisical/")) { + const withoutPrefix = tagName.replace(new RE2(/^infisical\//), ""); + return withoutPrefix.replace(new RE2(/-[a-zA-Z]+$/), ""); + } + return tagName.replace(new RE2(/-[a-zA-Z]+$/), ""); +}; + +const makeRequest = async ( + url: string, + config: GitHubClientConfig, + retryCount = 0 +): Promise<{ data: T; rateLimit: RateLimitInfo }> => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), config.timeout); + + try { + const response = await fetch(url, { + headers: getHeaders(config.token), + signal: controller.signal + }); + + clearTimeout(timeout); + + const rateLimit: RateLimitInfo = { + remaining: parseInt(response.headers.get("X-RateLimit-Remaining") || "0", 10), + reset: new Date(parseInt(response.headers.get("X-RateLimit-Reset") || "0", 10) * 1000), + used: parseInt(response.headers.get("X-RateLimit-Used") || "0", 10), + limit: parseInt(response.headers.get("X-RateLimit-Limit") || "5000", 10) + }; + + if (!response.ok) { + const error: GitHubApiError = new Error(`GitHub API error: ${response.status}`); + error.status = response.status; + error.headers = response.headers; + + if (response.status === 403) { + const resetTime = rateLimit.reset.toISOString(); + error.message = `GitHub API rate limit exceeded. Remaining: ${rateLimit.remaining}, Reset at: ${resetTime}. ${ + !config.token ? "Consider setting GITHUB_TOKEN environment variable." : "" + }`; + } + + if (retryCount < config.maxRetries && (response.status >= 500 || response.status === 403)) { + await delay(config.retryDelay * 2 ** retryCount); + return await makeRequest(url, config, retryCount + 1); + } + + throw error; + } + + const data = (await response.json()) as T; + return { data, rateLimit }; + } catch (error) { + clearTimeout(timeout); + + if (error instanceof Error && error.name === "AbortError") { + throw new Error(`Request timeout after ${config.timeout}ms`); + } + + if (retryCount < config.maxRetries && !(error as GitHubApiError).status) { + await delay(config.retryDelay * 2 ** retryCount); + return await makeRequest(url, config, retryCount + 1); + } + + throw error; + } +}; + +export const fetchReleases = async (includePrerelease = false): Promise => { + const config = getDefaultConfig(); + const allReleases: GitHubRelease[] = []; + let page = 1; + let hasMorePages = true; + + const maxConcurrentRequests = Math.min(3, config.maxPagesPerRequest); + + while (hasMorePages && page <= config.maxPagesPerRequest) { + const requests: Promise<{ data: GitHubRelease[]; rateLimit: RateLimitInfo }>[] = []; + + for (let i = 0; i < maxConcurrentRequests && page <= config.maxPagesPerRequest; i += 1, page += 1) { + const url = `https://api.github.com/repos/Infisical/infisical/releases?page=${page}&per_page=${config.perPage}`; + requests.push(makeRequest(url, config)); + } + + const results = await Promise.allSettled(requests); + let hasData = false; + + for (const result of results) { + if (result.status === "fulfilled") { + const { data } = result.value; + if (data.length > 0) { + allReleases.push(...data); + hasData = true; + } + } + } + + if (!hasData || results.every((r) => r.status === "fulfilled" && r.value.data.length < config.perPage)) { + hasMorePages = false; + } + } + + const formattedReleases = allReleases + .filter((release) => !release.draft) + .filter((release) => isMainInfisicalRelease(release.tag_name)) + .map( + (release): FormattedRelease => ({ + tagName: release.tag_name, + normalizedTagName: normalizeVersion(release.tag_name), + name: release.name, + body: release.body, + publishedAt: release.published_at, + prerelease: release.prerelease, + draft: release.draft + }) + ) + .sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()); + + return formattedReleases.filter((release) => includePrerelease || !release.prerelease); +}; diff --git a/backend/src/services/upgrade-path/index.ts b/backend/src/services/upgrade-path/index.ts new file mode 100644 index 000000000..1000e2bfa --- /dev/null +++ b/backend/src/services/upgrade-path/index.ts @@ -0,0 +1,2 @@ +export type { TUpgradePathService, TUpgradePathServiceFactory } from "./upgrade-path-service"; +export { upgradePathServiceFactory } from "./upgrade-path-service"; diff --git a/backend/src/services/upgrade-path/types.ts b/backend/src/services/upgrade-path/types.ts new file mode 100644 index 000000000..83d3d1546 --- /dev/null +++ b/backend/src/services/upgrade-path/types.ts @@ -0,0 +1,66 @@ +export interface GitHubRelease { + tag_name: string; + name: string; + body: string; + published_at: string; + prerelease: boolean; + draft: boolean; +} + +export interface FormattedRelease { + tagName: string; + normalizedTagName: string; + name: string; + body: string; + publishedAt: string; + prerelease: boolean; + draft: boolean; +} + +export interface BreakingChange { + title: string; + description: string; + action: string; +} + +export interface VersionConfig { + breaking_changes?: BreakingChange[]; + db_schema_changes?: string; + notes?: string; +} + +export interface UpgradePathConfig { + versions?: Record; +} + +export interface UpgradePathResult { + path: Array<{ + version: string; + name: string; + publishedAt: string; + prerelease: boolean; + }>; + breakingChanges: Array<{ + version: string; + changes: BreakingChange[]; + }>; + features: Array<{ + version: string; + name: string; + body: string; + publishedAt: string; + }>; + hasDbMigration: boolean; + config: Record; +} + +export interface GitHubApiError extends Error { + status?: number; + headers?: Headers; +} + +export interface CacheEntry { + data: T; + timestamp: number; + ttl: number; +} diff --git a/backend/src/services/upgrade-path/upgrade-path-service.ts b/backend/src/services/upgrade-path/upgrade-path-service.ts new file mode 100644 index 000000000..05bd44e25 --- /dev/null +++ b/backend/src/services/upgrade-path/upgrade-path-service.ts @@ -0,0 +1,270 @@ +import { readFile } from "fs/promises"; +import * as yaml from "js-yaml"; +import * as path from "path"; +import RE2 from "re2"; +import { z } from "zod"; + +import { TKeyStoreFactory } from "@app/keystore/keystore"; + +import { fetchReleases } from "./github-client"; +import { BreakingChange, FormattedRelease, UpgradePathConfig, UpgradePathResult, VersionConfig } from "./types"; + +export type TUpgradePathServiceFactory = { + keyStore: TKeyStoreFactory; +}; +export type TUpgradePathService = ReturnType; + +const versionSchema = z + .string() + .min(1) + .max(50) + .regex(new RE2(/^[a-zA-Z0-9._/-]+$/), "Invalid version format"); +const booleanSchema = z.boolean().default(false); + +interface CalculateUpgradePathParams { + fromVersion: string; + toVersion: string; + includePrerelease?: boolean; +} + +export const upgradePathServiceFactory = ({ keyStore }: TUpgradePathServiceFactory) => { + const getGitHubReleases = async (includePrerelease = false): Promise => { + const cacheKey = `upgrade-path:releases:${includePrerelease}`; + + try { + const cached = await keyStore.getItem(cacheKey); + if (cached) return JSON.parse(cached) as FormattedRelease[]; + } catch (error) { + // Cache miss, continue to fetch from source + } + + try { + const releases = await fetchReleases(booleanSchema.parse(includePrerelease)); + + const filteredReleases = releases.filter((v) => !v.tagName.includes("nightly")); + + await keyStore.setItemWithExpiry(cacheKey, 24 * 60 * 60, JSON.stringify(filteredReleases)); + return filteredReleases; + } catch (error) { + throw new Error(`GitHub releases unavailable: ${error instanceof Error ? error.message : "Unknown error"}`); + } + }; + + const getUpgradePathConfig = async (): Promise> => { + const cacheKey = "upgrade-path:config"; + + try { + const cached = await keyStore.getItem(cacheKey); + if (cached) return JSON.parse(cached) as Record; + } catch (error) { + // Cache miss, continue to fetch from source + } + + try { + const yamlPath = path.join(__dirname, "..", "..", "..", "upgrade-path.yaml"); + const yamlContent = await readFile(yamlPath, "utf8"); + + if (yamlContent.length > 1024 * 1024) { + throw new Error("Config file too large"); + } + + const config = yaml.load(yamlContent) as UpgradePathConfig; + const versionConfig = config?.versions || {}; + + await keyStore.setItemWithExpiry(cacheKey, 24 * 60 * 60, JSON.stringify(versionConfig)); + return versionConfig; + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") { + const empty = {}; + await keyStore.setItemWithExpiry(cacheKey, 24 * 60 * 60, JSON.stringify(empty)); + return empty; + } + throw new Error(`Config load failed: ${error instanceof Error ? error.message : "Unknown error"}`); + } + }; + + const normalizeVersion = (version: string): string => { + // Extract just the X.X.X.X part from any version format + const versionMatch = version.match(/(\d+\.\d+\.\d+(?:\.\d+)?)/); + if (versionMatch) { + return versionMatch[1]; + } + + // Handle legacy version formats + if (version.startsWith("infisical/")) { + return version.replace(new RE2(/^infisical\/v?/), "").replace(new RE2(/-[a-zA-Z]+$/), ""); + } + return version.replace(new RE2(/^v/), "").replace(new RE2(/-[a-zA-Z]+$/), ""); + }; + + const findBreakingChangesForVersion = ( + version: FormattedRelease, + config: Record + ): BreakingChange[] => { + // Check multiple key variations for breaking changes configuration + const versionNumber = normalizeVersion(version.tagName); + const possibleKeys = [ + version.tagName, + version.normalizedTagName, + versionNumber, + `v${versionNumber}`, + version.tagName.replace(new RE2(/^infisical\//), ""), + version.tagName.replace(new RE2(/^infisical\/v?/), "").replace(new RE2(/-[a-zA-Z]+$/), "") + ]; + + for (const key of possibleKeys) { + const versionConfig = config[key]; + if (versionConfig?.breaking_changes?.length) { + return versionConfig.breaking_changes; + } + } + return []; + }; + + const validateParams = (params: CalculateUpgradePathParams) => { + const { fromVersion, toVersion, includePrerelease = false } = params; + + versionSchema.parse(fromVersion); + versionSchema.parse(toVersion); + + if (fromVersion === toVersion) { + throw new Error("Versions cannot be identical"); + } + + if (fromVersion.includes("nightly") || toVersion.includes("nightly")) { + throw new Error("Nightly releases are not supported for upgrade path calculation"); + } + + return { fromVersion, toVersion, includePrerelease: booleanSchema.parse(includePrerelease) }; + }; + + const calculateUpgradePath = async (params: CalculateUpgradePathParams): Promise => { + const { fromVersion, toVersion, includePrerelease } = validateParams(params); + const cacheKey = `upgrade-path:${fromVersion}:${toVersion}:${includePrerelease}`; + + try { + const cached = await keyStore.getItem(cacheKey); + if (cached) return JSON.parse(cached) as UpgradePathResult; + } catch (error) { + // Cache miss, continue to fetch from source + } + + const [releases, config] = await Promise.all([getGitHubReleases(includePrerelease), getUpgradePathConfig()]); + + const cleanFrom = normalizeVersion(fromVersion); + const cleanTo = normalizeVersion(toVersion); + + const fromIdx = releases.findIndex((r) => normalizeVersion(r.normalizedTagName) === cleanFrom); + const toIdx = releases.findIndex((r) => normalizeVersion(r.normalizedTagName) === cleanTo); + + if (fromIdx === -1) throw new Error(`Version ${fromVersion} not found`); + if (toIdx === -1) throw new Error(`Version ${toVersion} not found`); + if (fromIdx <= toIdx) throw new Error("Invalid version order"); + + const upgradePath = releases.slice(toIdx, fromIdx + 1).reverse(); + const [first, last] = [upgradePath[0], upgradePath[upgradePath.length - 1]]; + + // Find all versions with breaking changes in the upgrade path + const withBreakingChanges = upgradePath.filter((version) => { + const breakingChanges = findBreakingChangesForVersion(version, config); + return breakingChanges.length > 0; + }); + + // Build the filtered path with breaking change versions + const filteredPath = [first]; + + // Get intermediate versions with breaking changes (excluding first and last) + const allIntermediateWithBreaking = withBreakingChanges + .filter((v) => v !== first && v !== last) + .sort((a, b) => new Date(a.publishedAt).getTime() - new Date(b.publishedAt).getTime()); + + // Limit intermediate steps to avoid overly complex upgrade paths + const maxIntermediateSteps = 8; + const intermediate = + allIntermediateWithBreaking.length > maxIntermediateSteps + ? allIntermediateWithBreaking.slice(-maxIntermediateSteps) + : allIntermediateWithBreaking; + + filteredPath.push(...intermediate); + if (last !== first) filteredPath.push(last); + + const breakingChanges: Array<{ version: string; changes: BreakingChange[] }> = []; + const features: Array<{ version: string; name: string; body: string; publishedAt: string }> = []; + let hasDbMigration = false; + + // Process versions in upgrade path, excluding starting version + for (let i = 1; i < upgradePath.length; i += 1) { + const version = upgradePath[i]; + const isFromVersion = normalizeVersion(version.normalizedTagName) === cleanFrom; + + // Process breaking changes for intermediate versions only + if (!isFromVersion) { + const versionBreakingChanges = findBreakingChangesForVersion(version, config); + if (versionBreakingChanges.length > 0) { + breakingChanges.push({ + version: version.tagName, + changes: versionBreakingChanges + }); + } + } + + // Process database migrations for intermediate versions only + if (!isFromVersion) { + const versionNumber = normalizeVersion(version.tagName); + const possibleKeys = [ + version.tagName, + version.normalizedTagName, + versionNumber, + `v${versionNumber}`, + version.tagName.replace(new RE2(/^infisical\//), ""), + version.tagName.replace(new RE2(/^infisical\/v?/), "").replace(new RE2(/-[a-zA-Z]+$/), "") + ]; + + for (const key of possibleKeys) { + const versionConfig = config[key]; + if ( + versionConfig?.db_schema_changes && + typeof versionConfig.db_schema_changes === "string" && + versionConfig.db_schema_changes.trim() + ) { + hasDbMigration = true; + break; + } + } + } + + // Collect release notes and features + if (version.body) { + features.push({ + version: version.tagName, + name: version.name, + body: version.body, + publishedAt: version.publishedAt + }); + } + } + + const result: UpgradePathResult = { + path: filteredPath.map((r) => ({ + version: r.tagName, + name: r.name, + publishedAt: r.publishedAt, + prerelease: r.prerelease + })), + breakingChanges, + features, + hasDbMigration, + config + }; + + await keyStore.setItemWithExpiry(cacheKey, 60 * 60, JSON.stringify(result)); + return result; + }; + + return { + getGitHubReleases, + getUpgradePathConfig, + calculateUpgradePath: (fromVersion: string, toVersion: string, includePrerelease = false) => + calculateUpgradePath({ fromVersion, toVersion, includePrerelease }) + }; +}; diff --git a/backend/upgrade-path.yaml b/backend/upgrade-path.yaml new file mode 100644 index 000000000..87aa5a53f --- /dev/null +++ b/backend/upgrade-path.yaml @@ -0,0 +1,78 @@ +versions: + "infisical/v0.130.0-postgres": + breaking_changes: + - title: "API Key Authentication Deprecation" + description: "Legacy API key authentication method has been deprecated and will be removed in future versions" + action: "Migrate all integrations to use Machine Identity authentication with JWT tokens. Update your CI/CD pipelines and automation scripts" + impact: "high" + - title: "Environment Variable Structure Changes" + description: "Environment variable naming convention has changed from INFISICAL_ prefix to INF_ for better compatibility" + action: "Update all environment variable references in your deployment configurations, Docker files, and Kubernetes manifests" + impact: "medium" + - title: "RBAC Permission Model Updates" + description: "Role-based access control has been restructured with new permission granularity affecting existing role assignments" + action: "Review and reassign user roles and permissions. Test access to sensitive resources before production deployment" + impact: "high" + db_schema_changes: "Extensive database schema restructuring for authentication and RBAC systems. Requires table reorganization and reindexing which may cause extended downtime." + notes: "Critical authentication and permission system overhaul. Database migration is extensive and may cause extended downtime. Plan maintenance window accordingly and ensure health checks are adjusted for longer migration time." + + + "infisical/v0.131.0-postgres": + breaking_changes: + - title: "Webhook Payload Format Changes" + description: "Webhook event payloads now use a new standardized format that is incompatible with previous versions" + action: "Update all webhook consumers to handle the new payload structure. Test webhook integrations with Slack, Discord, and custom endpoints" + impact: "high" + - title: "Secret Versioning API Breaking Changes" + description: "Secret versioning endpoints have changed from /api/v2/secrets to /api/v3/secrets with modified request/response schemas" + action: "Update all API clients and SDKs to use the new v3 endpoints. Modify any custom integrations or scripts" + impact: "medium" + - title: "CLI Authentication Method Changes" + description: "Infisical CLI now requires explicit authentication method specification and no longer supports legacy token formats" + action: "Update CLI installation in all environments. Re-authenticate CLI instances using 'infisical login' command" + impact: "medium" + db_schema_changes: "Major database schema changes for API restructuring. Includes reindexing large tables and webhook payload modifications which significantly impact performance during migration." + notes: "Major API restructure with extensive database changes. Migration involves reindexing large tables and may significantly impact instance performance. Health checks will likely fail during migration. Schedule during lowest traffic period." + + "v0.147.0": + breaking_changes: + - title: "Docker Tag Format Changes" + description: "Docker tags no longer contain the -postgres suffix. This affects deployment configurations" + action: "Update all deployment scripts, Docker Compose files, and Kubernetes manifests to use new tag format without -postgres suffix" + impact: "high" + - title: "Release Channel System Introduction" + description: "Formal release channels introduced with breaking changes to update mechanisms" + action: "Review release channel documentation and update your deployment strategy to align with new release channels" + impact: "medium" + db_schema_changes: "Database schema updates for release channel system implementation. Adds new tables for channel tracking and version management." + notes: "Docker tag format change requires deployment configuration updates. Review release channel documentation." + + "0.147.0": + breaking_changes: + - title: "Docker Tag Format Changes" + description: "Docker tags no longer contain the -postgres suffix. This affects deployment configurations" + action: "Update all deployment scripts, Docker Compose files, and Kubernetes manifests to use new tag format without -postgres suffix" + impact: "high" + - title: "Release Channel System Introduction" + description: "Formal release channels introduced with breaking changes to update mechanisms" + action: "Review release channel documentation and update your deployment strategy to align with new release channels" + impact: "medium" + db_schema_changes: "Database schema updates for release channel system implementation. Adds new tables for channel tracking and version management." + notes: "Docker tag format change requires deployment configuration updates. Review release channel documentation." + + "v0.148.0": + breaking_changes: + - title: "Secret Overview Page Removal" + description: "Secret overview page has been removed and replaced with revamped secret dashboard" + action: "Update any bookmarks, documentation, or automation that references the old overview page URLs" + impact: "medium" + - title: "Universal Auth Login Lockout" + description: "New lockout mechanism for Universal Auth that may affect existing authentication flows" + action: "Review and test authentication flows. Update monitoring and alerting for lockout scenarios" + impact: "high" + - title: "SAML Duplicate Account Handling Changes" + description: "Changes to how duplicate SAML accounts are handled during first-time sign-in" + action: "Test SAML authentication flows and ensure proper account linking procedures are in place" + impact: "medium" + db_schema_changes: "Database schema changes for authentication system improvements and UI restructuring. Includes new lockout mechanism tables and SAML account handling modifications." + notes: "UI changes and authentication flow updates. Test all authentication methods thoroughly." diff --git a/docs/images/self-hosting/helper/upgrade-path-tool.png b/docs/images/self-hosting/helper/upgrade-path-tool.png new file mode 100644 index 000000000..a8a538aaf Binary files /dev/null and b/docs/images/self-hosting/helper/upgrade-path-tool.png differ diff --git a/docs/self-hosting/guides/upgrading-infisical.mdx b/docs/self-hosting/guides/upgrading-infisical.mdx index 60c6edbff..f91b904b5 100644 --- a/docs/self-hosting/guides/upgrading-infisical.mdx +++ b/docs/self-hosting/guides/upgrading-infisical.mdx @@ -41,9 +41,16 @@ Now, migrations run automatically during boot-up. This improvement streamlines t - Ensure you have a complete backup of your Postgres database. - Verify that your backup is current and accessible. -2. **Select the Upgrade Version:** - - Visit the [Infisical releases page](https://github.com/Infisical/infisical/releases) for a list of available versions. - - Look for releases with the prefix `infisical/` as there are other releases that are not related to the Infisical instance. +2. **Plan Your Upgrade Path:** + - Use our [Upgrade Path Tool](https://app.infisical.com/upgrade-path) to analyze your upgrade path between your current version and target version. + - The tool will show you: + - **Breaking changes** that require action before upgrading + - **Database migrations** that may require additional settings or precautions + - **Step-by-step upgrade path** with intermediate versions if needed + - Review any breaking changes and plan necessary configuration updates before proceeding. + - Visit the [Infisical releases page](https://github.com/Infisical/infisical/releases) for a complete list of available versions. + +![Upgrade Path Tool showing breaking changes and migration information](/images/self-hosting/helper/upgrade-path-tool.png) 3. **Start the Upgrade Process:** - Launch the new version of Infisical. During startup, the application will automatically compare the current database schema with the updated schema in the code. diff --git a/frontend/src/hooks/api/upgradePath/index.ts b/frontend/src/hooks/api/upgradePath/index.ts new file mode 100644 index 000000000..73c6d1254 --- /dev/null +++ b/frontend/src/hooks/api/upgradePath/index.ts @@ -0,0 +1,7 @@ +export type { + CalculateUpgradePathParams, + GetUpgradePathVersionsParams, + GitHubVersion, + UpgradePathResult +} from "./queries"; +export { useCalculateUpgradePath, useGetUpgradePathVersions } from "./queries"; diff --git a/frontend/src/hooks/api/upgradePath/queries.tsx b/frontend/src/hooks/api/upgradePath/queries.tsx new file mode 100644 index 000000000..aa4475c74 --- /dev/null +++ b/frontend/src/hooks/api/upgradePath/queries.tsx @@ -0,0 +1,83 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +export interface GitHubVersion { + tagName: string; + name: string; + publishedAt: string; + prerelease: boolean; + draft: boolean; +} + +export interface UpgradePathResult { + path: Array<{ + version: string; + name: string; + publishedAt: string; + prerelease: boolean; + }>; + breakingChanges: Array<{ + version: string; + changes: Array<{ + title: string; + description: string; + action: string; + }>; + }>; + features: Array<{ + version: string; + name: string; + body: string; + publishedAt: string; + }>; + hasDbMigration: boolean; + config: Record; +} + +export interface GetUpgradePathVersionsParams { + includePrerelease?: boolean; +} + +export interface CalculateUpgradePathParams { + fromVersion: string; + toVersion: string; + includePrerelease?: boolean; +} + +const upgradePathKeys = { + all: ["upgrade-path"] as const, + versions: (params: GetUpgradePathVersionsParams) => + [...upgradePathKeys.all, "versions", params] as const, + calculate: (params: CalculateUpgradePathParams) => + [...upgradePathKeys.all, "calculate", params] as const +}; + +export const useGetUpgradePathVersions = ( + params: GetUpgradePathVersionsParams, + options?: Omit, "queryKey" | "queryFn"> +) => { + return useQuery({ + queryKey: upgradePathKeys.versions(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ versions: GitHubVersion[] }>( + "/api/v1/upgrade-path/versions", + { + params + } + ); + return data; + }, + ...options + }); +}; + +export const useCalculateUpgradePath = () => { + return async (params: CalculateUpgradePathParams): Promise => { + const { data } = await apiRequest.post( + "/api/v1/upgrade-path/calculate", + params + ); + return data; + }; +}; diff --git a/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx b/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx new file mode 100644 index 000000000..a8a8551f2 --- /dev/null +++ b/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx @@ -0,0 +1,671 @@ +/* eslint-disable no-nested-ternary */ +import { useEffect, useMemo, useRef, useState } from "react"; +import { Helmet } from "react-helmet"; +import { faExternalLink, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useMutation } from "@tanstack/react-query"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input } from "@app/components/v2"; + +import { + useCalculateUpgradePath, + useGetUpgradePathVersions +} from "../../../hooks/api/upgradePath/queries"; + +interface UpgradeResult { + path: Array<{ + version: string; + name: string; + publishedAt: string; + prerelease: boolean; + }>; + breakingChanges: Array<{ + version: string; + changes: Array<{ + title: string; + description: string; + action: string; + }>; + }>; + features: Array<{ + version: string; + name: string; + body: string; + publishedAt: string; + }>; + hasDbMigration: boolean; + config: Record; +} + +export const UpgradePathPage = () => { + const [fromVersion, setFromVersion] = useState(""); + const [toVersion, setToVersion] = useState(""); + const [fromSearch, setFromSearch] = useState(""); + const [toSearch, setToSearch] = useState(""); + const [showFromDropdown, setShowFromDropdown] = useState(false); + const [showToDropdown, setShowToDropdown] = useState(false); + const [upgradeResult, setUpgradeResult] = useState(null); + const [debouncedFromSearch, setDebouncedFromSearch] = useState(""); + const [debouncedToSearch, setDebouncedToSearch] = useState(""); + + const fromDropdownRef = useRef(null); + const toDropdownRef = useRef(null); + const fromSearchTimeoutRef = useRef(); + const toSearchTimeoutRef = useRef(); + + useEffect(() => { + if (fromSearchTimeoutRef.current) { + clearTimeout(fromSearchTimeoutRef.current); + } + fromSearchTimeoutRef.current = setTimeout(() => { + setDebouncedFromSearch(fromSearch); + }, 300); + + return () => { + if (fromSearchTimeoutRef.current) { + clearTimeout(fromSearchTimeoutRef.current); + } + }; + }, [fromSearch]); + + useEffect(() => { + if (toSearchTimeoutRef.current) { + clearTimeout(toSearchTimeoutRef.current); + } + toSearchTimeoutRef.current = setTimeout(() => { + setDebouncedToSearch(toSearch); + }, 300); + + return () => { + if (toSearchTimeoutRef.current) { + clearTimeout(toSearchTimeoutRef.current); + } + }; + }, [toSearch]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (fromDropdownRef.current && !fromDropdownRef.current.contains(event.target as Node)) { + setShowFromDropdown(false); + } + if (toDropdownRef.current && !toDropdownRef.current.contains(event.target as Node)) { + setShowToDropdown(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + const { + data: versions, + isLoading: versionsLoading, + isFetching: versionsFetching + } = useGetUpgradePathVersions( + { + includePrerelease: false + }, + { + enabled: true, + staleTime: 24 * 60 * 60 * 1000, + refetchOnWindowFocus: false + } + ); + + const calculateMutation = useMutation({ + mutationFn: useCalculateUpgradePath(), + onSuccess: (data) => { + setUpgradeResult(data); + }, + onError: (error: unknown) => { + createNotification({ + text: (error as any)?.response?.data?.message || "Failed to calculate upgrade path", + type: "error" + }); + } + }); + + const filteredFromVersions = useMemo(() => { + if (!versions?.versions) return []; + + const filtered = versions.versions + .filter((version) => !version.tagName.includes("nightly")) + .filter((version) => { + if (!debouncedFromSearch) return true; + const searchTerm = debouncedFromSearch.toLowerCase(); + return version.tagName.toLowerCase().includes(searchTerm); + }); + + return filtered.slice(0, 25); + }, [versions?.versions, debouncedFromSearch]); + + const filteredToVersions = useMemo(() => { + if (!versions?.versions) return []; + + const filtered = versions.versions + .filter((version) => !version.tagName.includes("nightly")) + .filter((version) => { + if (!debouncedToSearch) return true; + const searchTerm = debouncedToSearch.toLowerCase(); + return version.tagName.toLowerCase().includes(searchTerm); + }); + + return filtered.slice(0, 25); + }, [versions?.versions, debouncedToSearch]); + + const handleFromVersionSelect = (version: string) => { + setFromVersion(version); + setFromSearch(version); + setShowFromDropdown(false); + }; + + const handleToVersionSelect = (version: string) => { + setToVersion(version); + setToSearch(version); + setShowToDropdown(false); + }; + + const handleCalculate = () => { + if (!fromVersion || !toVersion) { + createNotification({ + text: "Please select both from and to versions", + type: "error" + }); + return; + } + + if (fromVersion === toVersion) { + createNotification({ + text: "From and To versions cannot be the same", + type: "error" + }); + return; + } + + calculateMutation.mutate({ + fromVersion, + toVersion, + includePrerelease: false + }); + }; + + return ( + <> + + Infisical Upgrade Path Tool | Infisical + + + + + +
+
+
+
+ {/* Header */} +
+
+ + Infisical logo + +
+

+ Upgrade your Infisical Version +

+
+ + {/* Calculator Card */} +
+

Calculate Upgrade Path

+
+
+ {/* From Version Selector */} +
+ +
+ { + setFromSearch(e.target.value); + setFromVersion(""); + setShowFromDropdown(true); + }} + onFocus={() => setShowFromDropdown(true)} + placeholder="Search or select version..." + className="border-mineshaft-600 bg-mineshaft-900" + leftIcon={} + isDisabled={versionsLoading || versionsFetching} + /> + {showFromDropdown && ( +
+ {(() => { + if (versionsLoading || versionsFetching) { + return ( +
+
+ + Loading versions... + +
+ ); + } + if (filteredFromVersions.length > 0) { + return filteredFromVersions.slice(0, 8).map((version) => ( + + )); + } + return ( +
+ {debouncedFromSearch + ? `No versions found matching "${debouncedFromSearch}"` + : "No versions available"} +
+ ); + })()} +
+ )} +
+ +
+ + {/* To Version Selector */} +
+ +
+ { + setToSearch(e.target.value); + setToVersion(""); + setShowToDropdown(true); + }} + onFocus={() => setShowToDropdown(true)} + placeholder="Search or select version..." + className="border-mineshaft-600 bg-mineshaft-900" + leftIcon={} + isDisabled={versionsLoading || versionsFetching} + /> + {showToDropdown && ( +
+ {(() => { + if (versionsLoading || versionsFetching) { + return ( +
+
+ + Loading versions... + +
+ ); + } + if (filteredToVersions.length > 0) { + return filteredToVersions.slice(0, 8).map((version) => ( + + )); + } + return ( +
+ {debouncedToSearch + ? `No versions found matching "${debouncedToSearch}"` + : "No versions available"} +
+ ); + })()} +
+ )} +
+ +
+
+ + +
+
+ + {/* Results Section */} + {upgradeResult && ( +
+ {/* Action Required Banner */} + {(() => { + const versionsWithBreakingChanges = upgradeResult.breakingChanges + .filter((bc) => bc.changes.length > 0) + .map((bc) => bc.version); + + const versionsWithDbMigrations = upgradeResult.path + .filter((step, index) => { + const isStartingVersion = index === 0; + if (isStartingVersion) return false; + + const versionConfig = upgradeResult.config as Record; + + const possibleKeys = [ + step.version, + step.version.replace(/^v/, ""), + step.version.replace(/^infisical\/v?/, ""), + step.version.replace(/^infisical\/v?/, "").replace(/-[a-zA-Z]+$/, "") + ]; + + const dbSchemaChanges = possibleKeys + .map((key) => versionConfig?.[key]?.db_schema_changes) + .find((changes) => changes); + + return ( + dbSchemaChanges && + (typeof dbSchemaChanges === "string" + ? dbSchemaChanges.trim() + : dbSchemaChanges) + ); + }) + .map((step) => step.version); + + const allConflictVersions = [ + ...new Set([...versionsWithBreakingChanges, ...versionsWithDbMigrations]) + ]; + const hasIssues = allConflictVersions.length > 0; + + return ( +
+
+
+ {hasIssues ? ( + + + + ) : ( + + + + )} +
+
+

+ {hasIssues ? "Action Required:" : "Ready to Upgrade:"} +

+

+ {hasIssues + ? `Your upgrade path contains conflicts in the following versions: ${allConflictVersions.join(", ")}. Please review and resolve each item before proceeding to the next version.` + : "Your upgrade path is clear with no breaking changes or conflicts. You can proceed with the upgrade."} +

+
+
+
+ ); + })()} + + {/* Upgrade Steps */} +
+
+ + + +

Upgrade Steps

+
+ +
+ {upgradeResult.path.map((step, index) => { + const isFirst = index === 0; + const isLast = index === upgradeResult.path.length - 1; + + const versionChanges = upgradeResult.breakingChanges.find((bc) => { + if (bc.version === step.version) return true; + + const normalizeVersion = (v: string) => { + return v.replace(/^(infisical\/)?v?/, "").replace(/-[a-zA-Z]+$/, ""); + }; + + const normalizedStep = normalizeVersion(step.version); + const normalizedBC = normalizeVersion(bc.version); + + return normalizedStep === normalizedBC; + }); + + const versionConfig = upgradeResult.config as Record; + + const possibleKeys = [ + step.version, + step.version.replace(/^v/, ""), + step.version.replace(/^infisical\/v?/, ""), + step.version.replace(/^infisical\/v?/, "").replace(/-[a-zA-Z]+$/, "") + ]; + + const dbMigrationDescription = possibleKeys + .map((key) => versionConfig?.[key]?.db_schema_changes) + .find((changes) => changes); + + const hasDbMigration = + !isFirst && + dbMigrationDescription && + (typeof dbMigrationDescription === "string" + ? dbMigrationDescription.trim() + : dbMigrationDescription); + + const hasBreakingChanges = + versionChanges && versionChanges.changes.length > 0; + + return ( +
+ {/* Timeline Column */} +
+ {/* Timeline Circle */} +
+ {isFirst || isLast ? ( +
+ ) : hasBreakingChanges || hasDbMigration ? ( + + + + ) : ( +
+ )} +
+ + {/* Timeline Line */} + {!isLast &&
} +
+ + {/* Content Column */} +
+ {/* Version Header */} +
+

{step.version}

+ {isFirst && ( + + Starting Version + + )} + {isLast && ( + + Target Version + + )} + + + View Changelog + +
+ + {/* Version Notes */} + {(() => { + if (isFirst) return null; + + const notes = possibleKeys + .map((key) => versionConfig?.[key]?.notes) + .find((note) => note); + + if (!notes) return null; + + return ( +
+
{notes}
+
+ ); + })()} + + {/* Database Schema Changes */} + {hasDbMigration && ( +
+
+ Database Schema Changes Required +
+
+ {typeof dbMigrationDescription === "string" + ? dbMigrationDescription + : "This version includes database schema changes that require migrations."} +
+
+ Action:{" "} + + Make sure to backup your database before proceeding + +
+
+ )} + + {/* Breaking Changes */} + {hasBreakingChanges && ( +
+
+ + + + + Breaking Changes ({versionChanges.changes.length}) + +
+ {versionChanges.changes.map((change) => ( +
+
+ {change.title} +
+
+ {change.description} +
+
+ Action:{" "} + {change.action} +
+
+ ))} +
+ )} +
+
+ ); + })} +
+
+
+ )} +
+
+

+ Made with ❤️ by{" "} + + Infisical + +
+ 235 2nd st, San Francisco, California, 94105, United States. 🇺🇸 +

+
+
+
+ + ); +}; diff --git a/frontend/src/pages/public/UpgradePathPage/route.tsx b/frontend/src/pages/public/UpgradePathPage/route.tsx new file mode 100644 index 000000000..cc91b74e4 --- /dev/null +++ b/frontend/src/pages/public/UpgradePathPage/route.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { UpgradePathPage } from "./UpgradePathPage"; + +export const Route = createFileRoute("/upgrade-path")({ + component: UpgradePathPage +}); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 1f62ba7b9..d4ecc024d 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { createFileRoute } from '@tanstack/react-router' import { Route as rootRoute } from './pages/root' import { Route as middlewaresRestrictLoginSignupImport } from './pages/middlewares/restrict-login-signup' import { Route as middlewaresAuthenticateImport } from './pages/middlewares/authenticate' +import { Route as publicUpgradePathPageRouteImport } from './pages/public/UpgradePathPage/route' import { Route as publicShareSecretPageRouteImport } from './pages/public/ShareSecretPage/route' import { Route as authCliRedirectPageRouteImport } from './pages/auth/CliRedirectPage/route' import { Route as indexImport } from './pages/index' @@ -318,6 +319,14 @@ const middlewaresAuthenticateRoute = middlewaresAuthenticateImport.update({ getParentRoute: () => rootRoute, } as any) +const publicUpgradePathPageRouteRoute = publicUpgradePathPageRouteImport.update( + { + id: '/upgrade-path', + path: '/upgrade-path', + getParentRoute: () => rootRoute, + } as any, +) + const publicShareSecretPageRouteRoute = publicShareSecretPageRouteImport.update( { id: '/share-secret', @@ -2037,6 +2046,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof publicShareSecretPageRouteImport parentRoute: typeof rootRoute } + '/upgrade-path': { + id: '/upgrade-path' + path: '/upgrade-path' + fullPath: '/upgrade-path' + preLoaderRoute: typeof publicUpgradePathPageRouteImport + parentRoute: typeof rootRoute + } '/_authenticate': { id: '/_authenticate' path: '' @@ -4600,6 +4616,7 @@ export interface FileRoutesByFullPath { '/': typeof indexRoute '/cli-redirect': typeof authCliRedirectPageRouteRoute '/share-secret': typeof publicShareSecretPageRouteRoute + '/upgrade-path': typeof publicUpgradePathPageRouteRoute '': typeof organizationLayoutRouteWithChildren '/password-setup': typeof authPasswordSetupPageRouteRoute '/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute @@ -4821,6 +4838,7 @@ export interface FileRoutesByTo { '/': typeof indexRoute '/cli-redirect': typeof authCliRedirectPageRouteRoute '/share-secret': typeof publicShareSecretPageRouteRoute + '/upgrade-path': typeof publicUpgradePathPageRouteRoute '': typeof organizationLayoutRouteWithChildren '/password-setup': typeof authPasswordSetupPageRouteRoute '/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute @@ -5029,6 +5047,7 @@ export interface FileRoutesById { '/': typeof indexRoute '/cli-redirect': typeof authCliRedirectPageRouteRoute '/share-secret': typeof publicShareSecretPageRouteRoute + '/upgrade-path': typeof publicUpgradePathPageRouteRoute '/_authenticate': typeof middlewaresAuthenticateRouteWithChildren '/_restrict-login-signup': typeof middlewaresRestrictLoginSignupRouteWithChildren '/_authenticate/password-setup': typeof authPasswordSetupPageRouteRoute @@ -5262,6 +5281,7 @@ export interface FileRouteTypes { | '/' | '/cli-redirect' | '/share-secret' + | '/upgrade-path' | '' | '/password-setup' | '/email-not-verified' @@ -5482,6 +5502,7 @@ export interface FileRouteTypes { | '/' | '/cli-redirect' | '/share-secret' + | '/upgrade-path' | '' | '/password-setup' | '/email-not-verified' @@ -5688,6 +5709,7 @@ export interface FileRouteTypes { | '/' | '/cli-redirect' | '/share-secret' + | '/upgrade-path' | '/_authenticate' | '/_restrict-login-signup' | '/_authenticate/password-setup' @@ -5920,6 +5942,7 @@ export interface RootRouteChildren { indexRoute: typeof indexRoute authCliRedirectPageRouteRoute: typeof authCliRedirectPageRouteRoute publicShareSecretPageRouteRoute: typeof publicShareSecretPageRouteRoute + publicUpgradePathPageRouteRoute: typeof publicUpgradePathPageRouteRoute middlewaresAuthenticateRoute: typeof middlewaresAuthenticateRouteWithChildren middlewaresRestrictLoginSignupRoute: typeof middlewaresRestrictLoginSignupRouteWithChildren publicViewSecretRequestByIDPageRouteRoute: typeof publicViewSecretRequestByIDPageRouteRoute @@ -5930,6 +5953,7 @@ const rootRouteChildren: RootRouteChildren = { indexRoute: indexRoute, authCliRedirectPageRouteRoute: authCliRedirectPageRouteRoute, publicShareSecretPageRouteRoute: publicShareSecretPageRouteRoute, + publicUpgradePathPageRouteRoute: publicUpgradePathPageRouteRoute, middlewaresAuthenticateRoute: middlewaresAuthenticateRouteWithChildren, middlewaresRestrictLoginSignupRoute: middlewaresRestrictLoginSignupRouteWithChildren, @@ -5952,6 +5976,7 @@ export const routeTree = rootRoute "/", "/cli-redirect", "/share-secret", + "/upgrade-path", "/_authenticate", "/_restrict-login-signup", "/secret-request/secret/$secretRequestId", @@ -5967,6 +5992,9 @@ export const routeTree = rootRoute "/share-secret": { "filePath": "public/ShareSecretPage/route.tsx" }, + "/upgrade-path": { + "filePath": "public/UpgradePathPage/route.tsx" + }, "/_authenticate": { "filePath": "middlewares/authenticate.tsx", "children": [ diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 70c34edc6..1db098b71 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -373,6 +373,7 @@ export const routes = rootRoute("root.tsx", [ route("/shared/secret/$secretId", "public/ViewSharedSecretByIDPage/route.tsx"), route("/secret-request/secret/$secretRequestId", "public/ViewSecretRequestByIDPage/route.tsx"), route("/share-secret", "public/ShareSecretPage/route.tsx"), + route("/upgrade-path", "public/UpgradePathPage/route.tsx"), route("/cli-redirect", "auth/CliRedirectPage/route.tsx"), middleware("restrict-login-signup.tsx", [ route("/admin/signup", "admin/SignUpPage/route.tsx"), diff --git a/package-lock.json b/package-lock.json index 4d72220af..6e37ffeab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "license": "ISC", "dependencies": { "@radix-ui/react-radio-group": "^1.1.3", + "js-yaml": "^4.1.0", "secrets.js-grempe": "^2.0.0" }, "devDependencies": { @@ -561,7 +562,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/balanced-match": { @@ -1104,7 +1104,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -1891,8 +1890,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "balanced-match": { "version": "1.0.2", @@ -2284,7 +2282,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" } diff --git a/package.json b/package.json index db15de3fb..2efd0def8 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@radix-ui/react-radio-group": "^1.1.3", + "js-yaml": "^4.1.0", "secrets.js-grempe": "^2.0.0" } }