Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/system-config-key-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'seamless-auth-api': patch
---

Starting the server no longer rewrites an env-backed `system_config` row, or warns that an admin change is being reverted, when the stored value only orders its keys differently. Postgres JSONB does not keep key order, so object values such as `lockout_policy` and `flow_rate_limits` looked changed on every boot.
6 changes: 5 additions & 1 deletion src/config/bootstrapSystemConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* See LICENSE file in the project root for full license information
*/

import { isDeepStrictEqual } from 'node:util';

import { oidcProviderConfigProblems } from '../lib/oidcProviderPolicy.js';
import { SystemConfig } from '../models/systemConfig.js';
import { SystemConfigSchema } from '../schemas/systemConfig.schema.js';
Expand Down Expand Up @@ -45,7 +47,9 @@ export async function bootstrapSystemConfig() {
envValue,
);

if (JSON.stringify(existing.value) !== JSON.stringify(parsed)) {
// Not a comparison of the serialized values: JSONB does not keep key order, so an
// object read back from the row can spell the same value in a different order.
if (!isDeepStrictEqual(existing.value, parsed)) {
logger.warn(
`Overwriting system_config "${key}" from env ${envVar}: the stored value differs and ` +
`the row is not marked admin-managed (updatedBy IS NULL). If this value was changed ` +
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/config/bootstrapSystemConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,33 @@ describe('bootstrapSystemConfig', () => {
expect(loggerWarn).not.toHaveBeenCalled();
});

it('does not rewrite a row whose JSONB value only orders its keys differently', async () => {
const { SystemConfig } = await import('../../../src/models/systemConfig');
const { parseSystemConfigEnvValue } = await import('../../../src/utils/parseEnvConfigs');
const { SystemConfigSchema } = await import('../../../src/schemas/systemConfig.schema');

const update = vi.fn();
(SystemConfig.findByPk as any).mockResolvedValue({
value: { otp: { perIp: 500 }, magicLink: { perIp: 500 } },
updatedBy: null,
update,
});

process.env.APP_NAME = 'TestApp';
process.env.RATE_LIMIT = '100';
(parseSystemConfigEnvValue as any).mockReturnValue({
magicLink: { perIp: 500 },
otp: { perIp: 500 },
});
(SystemConfigSchema.safeParse as any).mockReturnValue({ success: true, data: {} });

const { bootstrapSystemConfig } = await import('../../../src/config/bootstrapSystemConfig');
await bootstrapSystemConfig();

expect(update).not.toHaveBeenCalled();
expect(loggerWarn).not.toHaveBeenCalled();
});

it('preserves a row that was changed via the admin API (updatedBy set)', async () => {
const { SystemConfig } = await import('../../../src/models/systemConfig');
const { parseSystemConfigEnvValue } = await import('../../../src/utils/parseEnvConfigs');
Expand Down
Loading