Files
supabase/apps/studio/lib/server/configcat.ts
Charis 13de8189c9 feat(studio): assistant get_active_incidents on status page endpoint (#50994)
## Summary

* Adds a second branch to the assistant's `get_active_incidents` tool
(`apps/studio/lib/ai/tools/incident-tools.ts`) that reads
`/api/status-page` when the global ConfigCat flag `incidentIoStatusPage`
is on, instead of the legacy `/api/incident-status` endpoint.
* Filters on `visible`, ignores `show_banner` (only the global banner
respects it), and concatenates ongoing incidents + in-progress
maintenances (not scheduled maintenances).
* Adds `isServerFlagEnabled` to `lib/server/configcat.ts` for reading
global, non-user-targeted ConfigCat flags server-side, and threads the
flag through `getTools` from `pages/api/ai/sql/generate-v4.ts`.

This is PR 3 of 6 in the
[FE-4057](https://linear.app/supabase/issue/FE-4057/frontend-bannerbot-reconfigured)
stack — stacked on `charis/fe-4057-pr2-support-form`. Behind the
`incidentIoStatusPage` flag (default off), so this ships no user-visible
change on its own. See Linear
[FE-4057](https://linear.app/supabase/issue/FE-4057/frontend-bannerbot-reconfigured)
for full context.

## Test plan

- [X] `pnpm --filter studio run typecheck`
- [X] `pnpm --filter studio run lint:ratchet`
- [X] `pnpm knip --workspace apps/studio`
- [X] `pnpm test:prettier`
- [X] `pnpm --filter studio exec vitest run
lib/ai/tools/incident-tools.test.ts` (19/19 passing, including 7 new
tests for the status-page branch)
2026-09-29 15:28:49 -04:00

82 lines
2.4 KiB
TypeScript

import { getClient, PollingMode, User } from '@configcat/sdk/node'
import { IS_PLATFORM } from 'common'
let serverClient: ReturnType<typeof getClient>
export type TrustedUserEmail = string & { readonly __trustedUserEmailBrand: never }
// Promotes an email to TrustedUserEmail. Only call this with an email whose origin is already
// verified — e.g. `claims.email` from a JWT that apiAuthenticate has confirmed — never with a
// raw request body/query param.
export function trustedUserEmail(email: string | undefined): TrustedUserEmail | undefined {
return email as TrustedUserEmail | undefined
}
const STAFF_EMAIL_DOMAINS = ['supabase.com', 'supabase.io']
function isStaffEmail(email: string): boolean {
const domain = email.slice(email.lastIndexOf('@') + 1).toLowerCase()
return STAFF_EMAIL_DOMAINS.includes(domain)
}
function buildUser(userEmail?: TrustedUserEmail, customAttributes?: Record<string, string>) {
const _customAttributes = {
...customAttributes,
is_staff: (!!userEmail && isStaffEmail(userEmail)).toString(),
}
return new User(userEmail ?? 'anonymous', undefined, undefined, _customAttributes)
}
function getServerClient() {
if (serverClient) return serverClient
const proxyUrl = process.env.NEXT_PUBLIC_CONFIGCAT_PROXY_URL
const sdkKey = process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY
if (!proxyUrl && !sdkKey) {
console.log('Skipping server ConfigCat set up as env vars are not present')
return undefined
}
try {
if (proxyUrl) {
serverClient = getClient('configcat-proxy/frontend-v2', PollingMode.LazyLoad, {
baseUrl: proxyUrl,
})
return serverClient
}
if (sdkKey) {
serverClient = getClient(sdkKey, PollingMode.LazyLoad)
return serverClient
}
return undefined
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(`Failed to get server ConfigCat client: ${message}`)
return undefined
}
}
export async function getServerFlags(
userEmail?: TrustedUserEmail,
customAttributes?: Record<string, string>
) {
const client = getServerClient()
if (!client) {
return []
}
return client.getAllValuesAsync(buildUser(userEmail, customAttributes))
}
export async function isServerFlagEnabled(name: string): Promise<boolean> {
if (!IS_PLATFORM) return false
const flags = await getServerFlags()
return flags.some((flag) => flag.settingKey === name && flag.settingValue === true)
}