fix(export): restrict provider endpoint overrides to loopback test servers

The endpoint overrides are read through `$env/dynamic/private`, so they are
evaluated per request in production, not baked in at build time. That made a
single injected environment variable enough to redirect the authorization-code
and refresh-token POSTs - which carry the OAuth client secret and the user's
refresh token - to an arbitrary host, and to redirect the user's authorize hop
to an arbitrary URL.

Overrides are now ignored unless ALLOW_PROVIDER_ENDPOINT_OVERRIDES is exactly
"true" and the value is a loopback URL. `npm run test:bdd` sets the flag;
nothing else should. resolveProviderEndpoints is pure so the refusals are unit
tested, including near-miss hosts such as http://127.0.0.1.example.

Also drops the unused `pokedex` parameter from buildCsv rather than silencing it
with `void`, and the dead hasGigantamaxed field from its fallback record.
This commit is contained in:
Josh Creek
2026-09-13 17:35:57 +01:00
parent f9b7bbdf0a
commit 92d6460765
9 changed files with 178 additions and 45 deletions
+25 -28
View File
@@ -4,7 +4,7 @@ import {
csvEscape,
sanitizeFileName,
shouldRefreshToken
} from '../../src/lib/services/PokedexExportFormatting';
} from '$lib/services/PokedexExportFormatting';
describe('Pokédex export formatting', () => {
it.each([
@@ -26,35 +26,32 @@ describe('Pokédex export formatting', () => {
});
it('builds a stable, escaped CSV with defaults for missing catch records', () => {
const csv = buildCsv(
{ _id: 'dex-1', name: 'Test' } as never,
[
{
pokedexEntry: {
_id: '25',
pokedexNumber: 25,
pokemon: 'Pikachu',
form: null
},
catchRecord: {
caught: true,
haveToEvolve: false,
inHome: true,
hasGigantamaxed: false,
personalNotes: 'Comma, and "quote"'
}
const csv = buildCsv([
{
pokedexEntry: {
_id: '25',
pokedexNumber: 25,
pokemon: 'Pikachu',
form: null
},
{
pokedexEntry: {
_id: '26',
pokedexNumber: 26,
pokemon: 'Raichu',
form: 'Alolan'
},
catchRecord: null
catchRecord: {
caught: true,
haveToEvolve: false,
inHome: true,
hasGigantamaxed: false,
personalNotes: 'Comma, and "quote"'
}
] as never
);
},
{
pokedexEntry: {
_id: '26',
pokedexNumber: 26,
pokemon: 'Raichu',
form: 'Alolan'
},
catchRecord: null
}
] as never);
expect(csv.split('\r\n')).toEqual([
'pokemonId,pokedexNumber,pokemon,form,caught,haveToEvolve,inHome,personalNotes',
'25,25,Pikachu,,true,false,true,"Comma, and ""quote"""',