Implement Labrinth Canary API flag (#5531)

This commit is contained in:
aecsocket
2026-03-11 15:28:09 +00:00
committed by GitHub
parent 086508be23
commit 3b21944a75
7 changed files with 71 additions and 1 deletions

View File

@@ -43,6 +43,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
hidePreviewBanner: false, hidePreviewBanner: false,
i18nDebug: false, i18nDebug: false,
showDiscoverProjectButtons: false, showDiscoverProjectButtons: false,
labrinthApiCanary: false,
} as const) } as const)
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS

View File

@@ -5,6 +5,7 @@
let cachedRateLimitKey = undefined let cachedRateLimitKey = undefined
let rateLimitKeyPromise = undefined let rateLimitKeyPromise = undefined
const LABRINTH_CANARY_COOKIE = 'labrinth-canary=always'
async function getRateLimitKey(config) { async function getRateLimitKey(config) {
if (config.rateLimitKey) return config.rateLimitKey if (config.rateLimitKey) return config.rateLimitKey
@@ -38,6 +39,15 @@ export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
options.headers['x-ratelimit-key'] = await getRateLimitKey(config) options.headers['x-ratelimit-key'] = await getRateLimitKey(config)
} }
if (useFeatureFlags().value.labrinthApiCanary) {
const existingCookie = options.headers.cookie
if (!existingCookie?.split('; ').includes(LABRINTH_CANARY_COOKIE)) {
options.headers.cookie = existingCookie
? `${existingCookie}; ${LABRINTH_CANARY_COOKIE}`
: LABRINTH_CANARY_COOKIE
}
}
if (!skipAuth) { if (!skipAuth) {
const auth = await useAuth() const auth = await useAuth()

View File

@@ -2,7 +2,9 @@ import {
type AbstractFeature, type AbstractFeature,
type AuthConfig, type AuthConfig,
AuthFeature, AuthFeature,
CanaryCookieFeature,
CircuitBreakerFeature, CircuitBreakerFeature,
LABRINTH_CANARY_COOKIE,
NodeAuthFeature, NodeAuthFeature,
nodeAuthState, nodeAuthState,
NuxtCircuitBreakerStorage, NuxtCircuitBreakerStorage,
@@ -28,7 +30,11 @@ export function createModrinthClient(
auth: Ref<{ token: string | undefined }>, auth: Ref<{ token: string | undefined }>,
config: { apiBaseUrl: string; archonBaseUrl: string; rateLimitKey?: string }, config: { apiBaseUrl: string; archonBaseUrl: string; rateLimitKey?: string },
): NuxtModrinthClient { ): NuxtModrinthClient {
const flags = useFeatureFlags()
const optionalFeatures = [ const optionalFeatures = [
new CanaryCookieFeature({
getCookie: () => (flags.value.labrinthApiCanary ? LABRINTH_CANARY_COOKIE : undefined),
}) as AbstractFeature,
import.meta.dev ? (new VerboseLoggingFeature() as AbstractFeature) : undefined, import.meta.dev ? (new VerboseLoggingFeature() as AbstractFeature) : undefined,
].filter(Boolean) as AbstractFeature[] ].filter(Boolean) as AbstractFeature[]

View File

@@ -25,7 +25,11 @@ export default defineNuxtRouteMiddleware(async (to) => {
const queryClient = useAppQueryClient() const queryClient = useAppQueryClient()
const authToken = useCookie('auth-token') const authToken = useCookie('auth-token')
const client = useServerModrinthClient({ authToken: authToken.value || undefined }) const flags = useFeatureFlags()
const client = useServerModrinthClient({
authToken: authToken.value || undefined,
canaryCookie: flags.value.labrinthApiCanary,
})
const tags = useGeneratedState() const tags = useGeneratedState()
const projectId = to.params.id as string const projectId = to.params.id as string

View File

@@ -1,7 +1,9 @@
import { import {
type AuthConfig, type AuthConfig,
AuthFeature, AuthFeature,
CanaryCookieFeature,
type FeatureConfig, type FeatureConfig,
LABRINTH_CANARY_COOKIE,
type NuxtClientConfig, type NuxtClientConfig,
NuxtModrinthClient, NuxtModrinthClient,
} from '@modrinth/api-client' } from '@modrinth/api-client'
@@ -20,6 +22,7 @@ async function getRateLimitKeyFromSecretsStore(): Promise<string | undefined> {
export interface ServerModrinthClientOptions { export interface ServerModrinthClientOptions {
event?: H3Event event?: H3Event
authToken?: string authToken?: string
canaryCookie?: boolean
} }
export function useServerModrinthClient(options?: ServerModrinthClientOptions): NuxtModrinthClient { export function useServerModrinthClient(options?: ServerModrinthClientOptions): NuxtModrinthClient {
@@ -37,6 +40,10 @@ export function useServerModrinthClient(options?: ServerModrinthClientOptions):
) )
} }
if (options?.canaryCookie) {
features.push(new CanaryCookieFeature({ getCookie: () => LABRINTH_CANARY_COOKIE }))
}
const clientConfig: NuxtClientConfig = { const clientConfig: NuxtClientConfig = {
labrinthBaseUrl: apiBaseUrl, labrinthBaseUrl: apiBaseUrl,
rateLimitKey: config.rateLimitKey || getRateLimitKeyFromSecretsStore, rateLimitKey: config.rateLimitKey || getRateLimitKeyFromSecretsStore,

View File

@@ -0,0 +1,37 @@
import { AbstractFeature, type FeatureConfig } from '../core/abstract-feature'
export const LABRINTH_CANARY_COOKIE = 'labrinth-canary=always'
export interface CanaryCookieConfig extends FeatureConfig {
getCookie?: () => string | undefined | Promise<string | undefined>
}
export class CanaryCookieFeature extends AbstractFeature {
declare protected config: CanaryCookieConfig
constructor(config?: CanaryCookieConfig) {
super(config)
}
shouldApply(context: Parameters<AbstractFeature['shouldApply']>[0]): boolean {
return super.shouldApply(context) && context.options.api === 'labrinth'
}
async execute<T>(next: () => Promise<T>, context: Parameters<AbstractFeature['execute']>[1]) {
const cookie = this.config.getCookie ? await this.config.getCookie() : LABRINTH_CANARY_COOKIE
if (!cookie) {
return next()
}
const headers = { ...(context.options.headers ?? {}) }
const existingCookie = headers.cookie ?? headers.Cookie
if (!existingCookie?.split('; ').includes(cookie)) {
headers.cookie = existingCookie ? `${existingCookie}; ${cookie}` : cookie
delete headers.Cookie
context.options.headers = headers
}
return next()
}
}

View File

@@ -9,6 +9,11 @@ export {
} from './core/abstract-websocket' } from './core/abstract-websocket'
export { ModrinthApiError, ModrinthServerError } from './core/errors' export { ModrinthApiError, ModrinthServerError } from './core/errors'
export { type AuthConfig, AuthFeature } from './features/auth' export { type AuthConfig, AuthFeature } from './features/auth'
export {
type CanaryCookieConfig,
CanaryCookieFeature,
LABRINTH_CANARY_COOKIE,
} from './features/canary-cookie'
export { export {
type CircuitBreakerConfig, type CircuitBreakerConfig,
CircuitBreakerFeature, CircuitBreakerFeature,