From f53fb3d7aa3c1af6bb41150946d97912cae5d2f2 Mon Sep 17 00:00:00 2001 From: WhoamiI00 Date: Thu, 4 Jun 2026 00:12:31 +0530 Subject: [PATCH 1/2] fix: harden auth/security route against missing or null policy entries The /auth/security page assumes every policy in the listPolicies() response is non-null and has a $id, and that the policies it indexes into the result map always exist. After a 1.8.1 -> 1.9.0 upgrade, listPolicies may return entries that are absent for policies not yet present in the migrated project, which causes: - +page.ts: TypeError when .map((policy) => [policy.$id, policy]) encounters a null entry. - passwordPolicies.svelte: TypeError accessing .total / .enabled on an undefined policy in onMount and in the hasChanges derived. Changes: - +page.ts: filter out null entries before building policiesById. - passwordPolicies.svelte: type the three password-policy props as | undefined to match the (already-possible) runtime shape, and use optional chaining + nullish-coalescing fallbacks everywhere the policies are read. Related to #3076. This addresses the same class of null-safety bug in the same load path; the exact failing call site reported in #3076 points into minified JS without source maps, so this is defensive hardening rather than a confirmed root-cause fix. --- .../auth/security/+page.ts | 4 +++- .../auth/security/passwordPolicies.svelte | 20 +++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/+page.ts b/src/routes/(console)/project-[region]-[project]/auth/security/+page.ts index b8fb7a234f..c56e931d0c 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/security/+page.ts @@ -27,7 +27,9 @@ export const load: PageLoad = async ({ depends, params }) => { const { policies } = await sdk.forProject(params.region, params.project).project.listPolicies(); const policiesById = Object.fromEntries( - (policies as ProjectPolicy[]).map((policy) => [policy.$id, policy]) + (policies as ProjectPolicy[]) + .filter((policy) => policy != null && policy.$id != null) + .map((policy) => [policy.$id, policy]) ) as Partial>; return { diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte index caa766d3ca..8ff1868465 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte @@ -17,9 +17,9 @@ personalDataPolicy }: { project: Models.Project; - dictionaryPolicy: Models.PolicyPasswordDictionary; - historyPolicy: Models.PolicyPasswordHistory; - personalDataPolicy: Models.PolicyPasswordPersonalData; + dictionaryPolicy: Models.PolicyPasswordDictionary | undefined; + historyPolicy: Models.PolicyPasswordHistory | undefined; + personalDataPolicy: Models.PolicyPasswordPersonalData | undefined; } = $props(); let lastValidLimit = $state(5); @@ -30,15 +30,15 @@ onMount(() => { // update initial states here in onMount. - const historyValue = historyPolicy.total; + const historyValue = historyPolicy?.total; if (historyValue && historyValue > 0) { passwordHistory = historyValue; lastValidLimit = historyValue; } passwordHistoryEnabled = (historyValue ?? 0) !== 0; - passwordDictionary = dictionaryPolicy.enabled; - authPersonalDataCheck = personalDataPolicy.enabled; + passwordDictionary = dictionaryPolicy?.enabled ?? false; + authPersonalDataCheck = personalDataPolicy?.enabled ?? false; }); $effect(() => { @@ -49,11 +49,11 @@ }); const hasChanges = $derived.by(() => { - const dictChanged = passwordDictionary !== dictionaryPolicy.enabled; - const dataCheckChanged = authPersonalDataCheck !== personalDataPolicy.enabled; - const historyChanged = passwordHistoryEnabled !== (historyPolicy.total !== 0); + const dictChanged = passwordDictionary !== (dictionaryPolicy?.enabled ?? false); + const dataCheckChanged = authPersonalDataCheck !== (personalDataPolicy?.enabled ?? false); + const historyChanged = passwordHistoryEnabled !== ((historyPolicy?.total ?? 0) !== 0); const limitChanged = - passwordHistoryEnabled && Number(passwordHistory) !== historyPolicy.total; + passwordHistoryEnabled && Number(passwordHistory) !== (historyPolicy?.total ?? 0); return historyChanged || dictChanged || dataCheckChanged || limitChanged; }); From c9bfd4d5d13f5d1499a91a9435940df5fe7d339b Mon Sep 17 00:00:00 2001 From: WhoamiI00 Date: Thu, 4 Jun 2026 00:27:43 +0530 Subject: [PATCH 2/2] fix(auth/security): extend null-safety to sibling components Addresses Greptile review on PR #3077: the prior commit only hardened +page.ts and passwordPolicies.svelte, leaving five sibling components that read policy fields in top-level $state / $derived initializers which crash before render when a policy is absent. Changes: - +page.ts: widen the eight cast-as policy return types to `... | undefined` so downstream components see the real runtime shape. The three email-policy return values keep their fallback defaults and remain non-nullable. - sessionSecurity.svelte, updateUsersLimit.svelte, updateSessionLength.svelte, updateSessionsLimit.svelte, updateMembershipPrivacy.svelte: type the `policy` prop as `... | undefined`, replace direct field reads with optional chaining + nullish-coalescing defaults (false for booleans, 0 for numeric totals/durations). This makes the entire /auth/security page tolerant of a partially migrated 1.8.1 -> 1.9.0 backend that returns only a subset of policies. Verification: `bun run check` 0 errors, `bun run lint` clean on the security folder, `bun run test:unit` 235/235 pass. Related to #3076. --- .../auth/security/+page.ts | 48 +++++++++++-------- .../auth/security/sessionSecurity.svelte | 13 ++--- .../security/updateMembershipPrivacy.svelte | 22 ++++----- .../auth/security/updateSessionLength.svelte | 7 +-- .../auth/security/updateSessionsLimit.svelte | 7 +-- .../auth/security/updateUsersLimit.svelte | 9 ++-- 6 files changed, 58 insertions(+), 48 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/+page.ts b/src/routes/(console)/project-[region]-[project]/auth/security/+page.ts index c56e931d0c..9003396db7 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/security/+page.ts @@ -33,27 +33,33 @@ export const load: PageLoad = async ({ depends, params }) => { ) as Partial>; return { - membershipPrivacyPolicy: policiesById[ - ProjectPolicyId.Membershipprivacy - ] as Models.PolicyMembershipPrivacy, - passwordDictionaryPolicy: policiesById[ - ProjectPolicyId.Passworddictionary - ] as Models.PolicyPasswordDictionary, - passwordHistoryPolicy: policiesById[ - ProjectPolicyId.Passwordhistory - ] as Models.PolicyPasswordHistory, - passwordPersonalDataPolicy: policiesById[ - ProjectPolicyId.Passwordpersonaldata - ] as Models.PolicyPasswordPersonalData, - sessionAlertPolicy: policiesById[ProjectPolicyId.Sessionalert] as Models.PolicySessionAlert, - sessionDurationPolicy: policiesById[ - ProjectPolicyId.Sessionduration - ] as Models.PolicySessionDuration, - sessionInvalidationPolicy: policiesById[ - ProjectPolicyId.Sessioninvalidation - ] as Models.PolicySessionInvalidation, - sessionLimitPolicy: policiesById[ProjectPolicyId.Sessionlimit] as Models.PolicySessionLimit, - userLimitPolicy: policiesById[ProjectPolicyId.Userlimit] as Models.PolicyUserLimit, + membershipPrivacyPolicy: policiesById[ProjectPolicyId.Membershipprivacy] as + | Models.PolicyMembershipPrivacy + | undefined, + passwordDictionaryPolicy: policiesById[ProjectPolicyId.Passworddictionary] as + | Models.PolicyPasswordDictionary + | undefined, + passwordHistoryPolicy: policiesById[ProjectPolicyId.Passwordhistory] as + | Models.PolicyPasswordHistory + | undefined, + passwordPersonalDataPolicy: policiesById[ProjectPolicyId.Passwordpersonaldata] as + | Models.PolicyPasswordPersonalData + | undefined, + sessionAlertPolicy: policiesById[ProjectPolicyId.Sessionalert] as + | Models.PolicySessionAlert + | undefined, + sessionDurationPolicy: policiesById[ProjectPolicyId.Sessionduration] as + | Models.PolicySessionDuration + | undefined, + sessionInvalidationPolicy: policiesById[ProjectPolicyId.Sessioninvalidation] as + | Models.PolicySessionInvalidation + | undefined, + sessionLimitPolicy: policiesById[ProjectPolicyId.Sessionlimit] as + | Models.PolicySessionLimit + | undefined, + userLimitPolicy: policiesById[ProjectPolicyId.Userlimit] as + | Models.PolicyUserLimit + | undefined, denyAliasedEmailPolicy: (policiesById[ProjectEmailPolicyId.DenyAliasedEmail] as EnabledPolicy) ?? getDefaultEnabledPolicy(ProjectEmailPolicyId.DenyAliasedEmail), diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/sessionSecurity.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/sessionSecurity.svelte index ef395b3ef6..6f35dc22b1 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/sessionSecurity.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/sessionSecurity.svelte @@ -16,21 +16,22 @@ sessionInvalidationPolicy }: { project: Models.Project; - sessionAlertPolicy: Models.PolicySessionAlert; - sessionInvalidationPolicy: Models.PolicySessionInvalidation; + sessionAlertPolicy: Models.PolicySessionAlert | undefined; + sessionInvalidationPolicy: Models.PolicySessionInvalidation | undefined; } = $props(); let authSessionAlerts = $state(false); let sessionInvalidation = $state(false); onMount(() => { - authSessionAlerts = sessionAlertPolicy.enabled; - sessionInvalidation = sessionInvalidationPolicy.enabled; + authSessionAlerts = sessionAlertPolicy?.enabled ?? false; + sessionInvalidation = sessionInvalidationPolicy?.enabled ?? false; }); const hasChanges = $derived.by(() => { - const alertsChanged = authSessionAlerts !== sessionAlertPolicy.enabled; - const invalidationChanged = sessionInvalidation !== sessionInvalidationPolicy.enabled; + const alertsChanged = authSessionAlerts !== (sessionAlertPolicy?.enabled ?? false); + const invalidationChanged = + sessionInvalidation !== (sessionInvalidationPolicy?.enabled ?? false); return alertsChanged || invalidationChanged; }); diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte index dd16a25a4b..d25ad4c343 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte @@ -14,21 +14,21 @@ policy }: { project: Models.Project; - policy: Models.PolicyMembershipPrivacy; + policy: Models.PolicyMembershipPrivacy | undefined; } = $props(); - let authMembershipsMfa = $state(policy.userMFA); - let authMembershipsUserId = $state(policy.userId); - let authMembershipsUserName = $state(policy.userName); - let authMembershipsUserEmail = $state(policy.userEmail); - let authMembershipsUserPhone = $state(policy.userPhone); + let authMembershipsMfa = $state(policy?.userMFA ?? false); + let authMembershipsUserId = $state(policy?.userId ?? false); + let authMembershipsUserName = $state(policy?.userName ?? false); + let authMembershipsUserEmail = $state(policy?.userEmail ?? false); + let authMembershipsUserPhone = $state(policy?.userPhone ?? false); const isSubmitDisabled = $derived( - authMembershipsUserId === policy.userId && - authMembershipsUserName === policy.userName && - authMembershipsUserEmail === policy.userEmail && - authMembershipsUserPhone === policy.userPhone && - authMembershipsMfa === policy.userMFA + authMembershipsUserId === (policy?.userId ?? false) && + authMembershipsUserName === (policy?.userName ?? false) && + authMembershipsUserEmail === (policy?.userEmail ?? false) && + authMembershipsUserPhone === (policy?.userPhone ?? false) && + authMembershipsMfa === (policy?.userMFA ?? false) ); async function updateMembershipsPrivacy() { diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte index 5ffef790ee..be90c0cb69 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte @@ -15,10 +15,11 @@ policy }: { project: Models.Project; - policy: Models.PolicySessionDuration; + policy: Models.PolicySessionDuration | undefined; } = $props(); - const { value, unit, baseValue, units } = $derived(createTimeUnitPair(policy.duration)); + const policyDuration = $derived(policy?.duration ?? 0); + const { value, unit, baseValue, units } = $derived(createTimeUnitPair(policyDuration)); const options = $derived(units.map((v) => ({ label: v.name, value: v.name }))); async function updateSessionLength() { @@ -53,7 +54,7 @@ - diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte index f169a9cab5..623e0940ce 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte @@ -14,10 +14,11 @@ policy }: { project: Models.Project; - policy: Models.PolicySessionLimit; + policy: Models.PolicySessionLimit | undefined; } = $props(); - let maxSessions = $state(policy.total); + const policyTotal = $derived(policy?.total ?? 0); + let maxSessions = $state(policyTotal); async function updateSessionsLimit() { try { @@ -56,7 +57,7 @@ bind:value={maxSessions} /> - + diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte index dc64a2dc78..ebe38cfbb9 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte @@ -15,17 +15,18 @@ policy }: { project: Models.Project; - policy: Models.PolicyUserLimit; + policy: Models.PolicyUserLimit | undefined; } = $props(); let maxUsersInputField: HTMLInputElement | null = $state(null); - let value = $state(policy.total !== 0 ? 'limited' : 'unlimited'); - let newLimit = $state(policy.total !== 0 ? policy.total : 100); + const policyTotal = $derived(policy?.total ?? 0); + let value = $state(policyTotal !== 0 ? 'limited' : 'unlimited'); + let newLimit = $state(policyTotal !== 0 ? policyTotal : 100); const isLimited = $derived(value === 'limited'); const btnDisabled = $derived.by(() => { - return (!isLimited && policy.total === 0) || (isLimited && policy.total === newLimit); + return (!isLimited && policyTotal === 0) || (isLimited && policyTotal === newLimit); }); async function updateLimit() {