-
Notifications
You must be signed in to change notification settings - Fork 44
feat(ai-gateway): restrict custom LLMs to allowed country codes #4288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { describe, expect, it } from '@jest/globals'; | ||
| import { isCountryAllowed } from './country-access'; | ||
|
|
||
| describe('isCountryAllowed', () => { | ||
| it('allows all countries when the allow-list is absent', () => { | ||
| expect(isCountryAllowed(undefined, 'US')).toBe(true); | ||
| expect(isCountryAllowed(undefined, null)).toBe(true); | ||
| }); | ||
|
|
||
| it('allows all countries when the allow-list is empty', () => { | ||
| expect(isCountryAllowed([], 'US')).toBe(true); | ||
| expect(isCountryAllowed([], null)).toBe(true); | ||
| }); | ||
|
|
||
| it('allows a request whose country is in the allow-list', () => { | ||
| expect(isCountryAllowed(['US', 'CA'], 'US')).toBe(true); | ||
| expect(isCountryAllowed(['US', 'CA'], 'CA')).toBe(true); | ||
| }); | ||
|
|
||
| it('denies a request whose country is not in the allow-list', () => { | ||
| expect(isCountryAllowed(['US', 'CA'], 'GB')).toBe(false); | ||
| }); | ||
|
|
||
| it('compares case-insensitively', () => { | ||
| expect(isCountryAllowed(['us', 'ca'], 'US')).toBe(true); | ||
| expect(isCountryAllowed(['US', 'CA'], 'ca')).toBe(true); | ||
| }); | ||
|
|
||
| it('trims surrounding whitespace before comparing', () => { | ||
| expect(isCountryAllowed([' US ', 'CA '], 'US')).toBe(true); | ||
| expect(isCountryAllowed(['US'], ' us ')).toBe(true); | ||
| }); | ||
|
|
||
| it('fails closed when the request country cannot be determined', () => { | ||
| expect(isCountryAllowed(['US'], null)).toBe(false); | ||
| expect(isCountryAllowed(['US'], '')).toBe(false); | ||
| expect(isCountryAllowed(['US'], ' ')).toBe(false); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| /** | ||
| * Determines whether a request is allowed to access a custom LLM based on its | ||
| * `country_codes` allow-list. | ||
| * | ||
| * `country_codes` are ISO 3166-1 alpha-2 codes (e.g. "US", "GB") sourced from | ||
| * Vercel's `x-vercel-ip-country` request header. Comparison is | ||
| * case-insensitive and trims surrounding whitespace. | ||
| * | ||
| * Semantics: | ||
| * - An absent or empty `allowedCountries` list disables the restriction | ||
| * (every country is allowed). | ||
| * - When a non-empty list is configured and the request country cannot be | ||
| * determined (`requestCountry` is null or empty), access is denied | ||
| * (fail-closed) so a misconfigured or spoofed header cannot bypass the | ||
| * intended geographic restriction. | ||
| */ | ||
| export function isCountryAllowed( | ||
| allowedCountries: readonly string[] | undefined, | ||
| requestCountry: string | null | ||
| ): boolean { | ||
| if (!allowedCountries || allowedCountries.length === 0) { | ||
| return true; | ||
| } | ||
| const normalizedRequestCountry = requestCountry?.trim().toUpperCase() ?? null; | ||
| if (!normalizedRequestCountry) { | ||
| return false; | ||
| } | ||
| const normalizedAllowed = new Set(allowedCountries.map(code => code.toUpperCase())); | ||
| return normalizedAllowed.has(normalizedRequestCountry); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1516,11 +1516,20 @@ export const CustomLlmApiConfigSchema = z.object({ | |||||
|
|
||||||
| export type CustomLlmApiConfig = z.infer<typeof CustomLlmApiConfigSchema>; | ||||||
|
|
||||||
| // ISO 3166-1 alpha-2 country codes (e.g. "US", "GB") sourced from Vercel's | ||||||
| // `x-vercel-ip-country` request header. When non-empty, requests whose | ||||||
| // resolved country is not in the list are rejected at the LLM request level. | ||||||
| // Case-insensitive at enforcement; entries are validated as 2 characters. | ||||||
| export const CustomLlmCountryCodesSchema = z.array(z.string().length(2)); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: The admin JSON editor validates through this schema, so entries like
Suggested change
Reply with |
||||||
|
|
||||||
| export type CustomLlmCountryCodes = z.infer<typeof CustomLlmCountryCodesSchema>; | ||||||
|
|
||||||
| export const CustomLlmDefinitionSchema = z | ||||||
| .object({ | ||||||
| display_name: z.string(), | ||||||
| api_key: z.string(), | ||||||
| organization_ids: z.array(z.string()), | ||||||
| country_codes: CustomLlmCountryCodesSchema.optional(), | ||||||
| pricing: CustomLlmPricingSchema.optional(), | ||||||
| }) | ||||||
| .and(CustomLlmMetadataSchema) | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING: Allow-list entries are not trimmed before comparison
requestCountryis normalized withtrim().toUpperCase(), but eachcountry_codesentry is only uppercased here. A configured value like" US "or the new whitespace test added in this PR will still miss and incorrectly returnforbidden, so the implementation doesn't match the documented behavior.Reply with
@kilocode-bot fix itto have Kilo Code address this issue.