From 1d29b4c5b4af7a9a46ba460d0dfebbb7db528f5c Mon Sep 17 00:00:00 2001
From: Joshen Lim
Date: Mon, 13 Jul 2026 17:01:05 +0800
Subject: [PATCH] Clean up RLS Tester artifacts (#47866)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Context
As per PR title - we're pausing the development of the RLS Tester
feature preview while we re-evaluate its direction. Have also updated
the GH discussion
[here](https://github.com/orgs/supabase/discussions/45233) RE this! π
Removes the RLS Tester UI + Sandbox functionality
## Summary by CodeRabbit
* **Removed Features**
* Removed the RLS Tester feature preview, banner, and database policy
testing workflow.
* The related SQL testing, role selection, policy summaries, sandbox
management, and result views are no longer available.
* **Bug Fixes**
* Improved accessibility on the database policies page by adding a label
to the clear-filter button.
---
.../FeaturePreview/FeaturePreviewContext.tsx | 5 -
.../FeaturePreview/FeaturePreviewModal.tsx | 15 -
.../App/FeaturePreview/RLSTesterPreview.tsx | 38 --
.../App/FeaturePreview/useFeaturePreviews.ts | 10 -
.../Database/RLSTester/InferredSQLViewer.tsx | 44 --
.../Database/RLSTester/RLSTableCard.tsx | 207 ----------
.../Database/RLSTester/RLSTester.types.ts | 15 -
.../RLSTester/RLSTesterEmptyState.tsx | 13 -
.../Database/RLSTester/RLSTesterResults.tsx | 197 ---------
.../RLSTester/RLSTesterResults.utils.ts | 25 --
.../Database/RLSTester/RLSTesterSheet.tsx | 387 ------------------
.../Database/RLSTester/RoleSelector.tsx | 38 --
.../Database/RLSTester/SandboxManagement.tsx | 105 -----
.../Database/RLSTester/UserSelector.tsx | 166 --------
.../Database/RLSTester/UserSqlEditor.tsx | 28 --
.../__tests__/RLSTesterResults.test.tsx | 200 ---------
.../__tests__/RLSTesterResults.utils.test.ts | 192 ---------
.../__tests__/useTestQueryRLS.utils.test.ts | 265 ------------
.../Database/RLSTester/useTestQueryRLS.ts | 261 ------------
.../RLSTester/useTestQueryRLS.utils.ts | 56 ---
.../ui/BannerStack/BannerStackProvider.tsx | 1 -
.../BannerStack/Banners/BannerRlsTester.tsx | 113 -----
apps/studio/data/rls-tester/get-schema-ddl.ts | 180 --------
apps/studio/data/rls-tester/get-seed-data.ts | 88 ----
apps/studio/package.json | 2 -
.../pages/project/[ref]/database/policies.tsx | 34 +-
.../img/previews/rls-tester-preview.png | Bin 157676 -> 0 bytes
.../state/postgres-sandbox/pglite.worker.ts | 13 -
.../postgres-sandbox/sandbox.constants.ts | 162 --------
.../state/postgres-sandbox/sandbox.core.ts | 72 ----
.../studio/state/postgres-sandbox/sandbox.tsx | 143 -------
.../state/postgres-sandbox/sandbox.utils.ts | 210 ----------
packages/common/constants/local-storage.ts | 3 -
packages/common/telemetry-constants.ts | 14 -
pnpm-lock.yaml | 18 +-
35 files changed, 4 insertions(+), 3316 deletions(-)
delete mode 100644 apps/studio/components/interfaces/App/FeaturePreview/RLSTesterPreview.tsx
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/InferredSQLViewer.tsx
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/RLSTableCard.tsx
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/RLSTester.types.ts
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/RLSTesterEmptyState.tsx
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.tsx
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.utils.ts
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/RLSTesterSheet.tsx
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/RoleSelector.tsx
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/SandboxManagement.tsx
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/UserSelector.tsx
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/UserSqlEditor.tsx
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.test.tsx
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.utils.test.ts
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/__tests__/useTestQueryRLS.utils.test.ts
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.ts
delete mode 100644 apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.utils.ts
delete mode 100644 apps/studio/components/ui/BannerStack/Banners/BannerRlsTester.tsx
delete mode 100644 apps/studio/data/rls-tester/get-schema-ddl.ts
delete mode 100644 apps/studio/data/rls-tester/get-seed-data.ts
delete mode 100644 apps/studio/public/img/previews/rls-tester-preview.png
delete mode 100644 apps/studio/state/postgres-sandbox/pglite.worker.ts
delete mode 100644 apps/studio/state/postgres-sandbox/sandbox.constants.ts
delete mode 100644 apps/studio/state/postgres-sandbox/sandbox.core.ts
delete mode 100644 apps/studio/state/postgres-sandbox/sandbox.tsx
delete mode 100644 apps/studio/state/postgres-sandbox/sandbox.utils.ts
diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx
index 54680d64291..aa5065a4f98 100644
--- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx
+++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx
@@ -133,11 +133,6 @@ export const useIsSqlEditorManualSaveEnabled = () => {
return sqlEditorManualSaveEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]
}
-export const useIsRLSTesterEnabled = () => {
- const { flags } = useFeaturePreviewContext()
- return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER]
-}
-
export const useIsMarketplaceEnabled = () => {
const { flags } = useFeaturePreviewContext()
const isMarketplaceEnabled = useFlag('marketplaceIntegrations')
diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx
index 49ef243658d..7a03d13ea76 100644
--- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx
+++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx
@@ -34,12 +34,9 @@ import { IntegrationsLayoutPreview } from './IntegrationsLayoutPreview'
import { JitDbAccessPreview } from './JitDbAccessPreview'
import { PgDeltaDiffPreview } from './PgDeltaDiffPreview'
import { PlatformWebhooksPreview } from './PlatformWebhooksPreview'
-import { RLSTesterPreview } from './RLSTesterPreview'
import { SqlEditorManualSavePreview } from './SqlEditorManualSavePreview'
import { UnifiedLogsPreview } from './UnifiedLogsPreview'
import { FeaturePreview, useFeaturePreviews } from './useFeaturePreviews'
-import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
-import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { IS_PLATFORM } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
@@ -52,7 +49,6 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: {
[LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]: ,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]: ,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]: ,
- [LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER]: ,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]: ,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: ,
}
@@ -70,12 +66,6 @@ export const FeaturePreviewModal = () => {
const featurePreviewContext = useFeaturePreviewContext()
const track = useTrack()
- const { dismissBanner } = useBannerStack()
- const [, setIsDismissedRlsTesterBanner] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.RLS_TESTER_BANNER_DISMISSED(ref ?? ''),
- false
- )
-
const { flags, onUpdateFlag } = featurePreviewContext
const allFeaturePreviews = (
IS_PLATFORM ? featurePreviews : featurePreviews.filter((x) => !x.isPlatformOnly)
@@ -94,11 +84,6 @@ export const FeaturePreviewModal = () => {
const isEnabling = !isSelectedFeatureEnabled
- if (selectedFeature.key === LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER) {
- dismissBanner('rls-tester-banner')
- setIsDismissedRlsTesterBanner(true)
- }
-
onUpdateFlag(selectedFeature.key, isEnabling)
track(isEnabling ? 'feature_preview_enabled' : 'feature_preview_disabled', {
feature: selectedFeature.key,
diff --git a/apps/studio/components/interfaces/App/FeaturePreview/RLSTesterPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/RLSTesterPreview.tsx
deleted file mode 100644
index 01ea7cffdc5..00000000000
--- a/apps/studio/components/interfaces/App/FeaturePreview/RLSTesterPreview.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { useParams } from 'common'
-import Image from 'next/image'
-
-import { InlineLink } from '@/components/ui/InlineLink'
-import { BASE_PATH } from '@/lib/constants'
-
-export const RLSTesterPreview = () => {
- const { ref } = useParams()
-
- return (
-
-
- Verify if your RLS policies have been set up properly by running queries as a specific user.
- While role impersonation isn't a new feature on the dashboard, we've built a dedicated UI
- for this which will also show what policies are evaluated for the query.
-
-
-
-
Enabling this preview will:
-
-
- Show the "Test" button on the{' '}
-
- Database Policies page
-
-
-
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts
index c256c2ffac2..dd425cc52f5 100644
--- a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts
+++ b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts
@@ -30,16 +30,6 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
return useMemo(
() =>
[
- {
- key: LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER,
- name: 'RLS Tester',
- discussionsUrl: 'https://github.com/orgs/supabase/discussions/45233',
- enabled: true,
- isNew: true,
- isPlatformOnly: false,
- isDefaultOptIn: false,
- getRoute: (ref?: string) => `/project/${ref}/database/policies`,
- },
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS,
name: 'Updated Logs interface',
diff --git a/apps/studio/components/interfaces/Database/RLSTester/InferredSQLViewer.tsx b/apps/studio/components/interfaces/Database/RLSTester/InferredSQLViewer.tsx
deleted file mode 100644
index ecdaf4b6b80..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/InferredSQLViewer.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { UntrustedSqlFragment } from '@supabase/pg-meta'
-import { Loader2 } from 'lucide-react'
-import { Badge, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
-
-import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
-
-export const InferredSQLViewer = ({
- sql,
- isLoading = false,
-}: {
- sql: UntrustedSqlFragment | undefined
- isLoading?: boolean
-}) => {
- return (
- <>
-
-
-
Inferred SQL:
- {isLoading &&
}
-
-
-
-
- Generated
-
-
- This query is inferred from client library code with the help of the Assistant and may
- not guarantee correctness.
-
-
-
-
-
- {isLoading && !sql ? (
-
-
-
- ) : (
-
- )}
-
- >
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTableCard.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTableCard.tsx
deleted file mode 100644
index 724f8b57cf2..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTableCard.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import { Check, ChevronDown, Edit, X } from 'lucide-react'
-import { useMemo } from 'react'
-import { cn, Collapsible, CollapsibleContent, CollapsibleTrigger, WarningIcon } from 'ui'
-
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
-import { type ParseSQLQueryOperations } from '@/data/misc/parse-query-mutation'
-
-interface RLSTableCardProps {
- table: { schema: string; name: string; isRLSEnabled: boolean }
- operation: ParseSQLQueryOperations
- role?: string
- policies: Policy[]
- hasError: boolean
- handleSelectEditPolicy: (policy: Policy) => void
-}
-
-export const RLSTableCard = ({
- table,
- operation,
- role,
- policies,
- hasError,
- handleSelectEditPolicy,
-}: RLSTableCardProps) => {
- const { schema, name, isRLSEnabled } = table
- const trueOnlyPolicy = policies.find((x) => x.definition === 'true')
- const falseOnlyPolicy = policies.find((x) => x.definition === 'false')
- const noPolicies = isRLSEnabled && policies.length === 0
-
- const tableAccessDescription = useMemo(() => {
- if (!isRLSEnabled) {
- return (
-
- RLS is disabled and all data is publicly accessible. We highly recommend enabling RLS and
- adding policies to restrict access.
-
- )
- }
-
- if (noPolicies) {
- return (
-
- RLS is enabled but no policies exist for the{' '}
- {role} role on this table -{' '}
- {operation === 'SELECT'
- ? 'no data will be returned'
- : `no data will be ${operation?.toLowerCase()}${operation?.toLowerCase().endsWith('e') ? 'd' : 'ed'}`}
- .
-
- )
- }
-
- if (trueOnlyPolicy) {
- return (
- <>
-
- The policy "{trueOnlyPolicy.name}" for the{' '}
- {role} role on this table evaluates to{' '}
- true, so all data from this query is
- accessible to this user.
-
-
- >
- )
- }
-
- if (falseOnlyPolicy) {
- return (
- <>
-
- The policy "{falseOnlyPolicy.name}" for the{' '}
- {role} role on this table evaluates to{' '}
- false, so no data from this query is
- accessible to this user.
-
-
- >
- )
- }
-
- return (
- <>
-
- {policies.length} {policies.length > 1 ? 'policies apply' : 'policy applies'} for the{' '}
- {role} role on this table.{' '}
- {operation === 'SELECT'
- ? `Only rows that match ${policies.length > 1 ? 'these conditions' : 'this condition'} are returned.`
- : `The ${operation} operation will only be successful if the conditions are matched.`}
-
-
- >
- )
- }, [
- isRLSEnabled,
- noPolicies,
- trueOnlyPolicy,
- falseOnlyPolicy,
- policies,
- role,
- operation,
- handleSelectEditPolicy,
- ])
-
- return (
-
-
-
-
- {!isRLSEnabled ? (
-
- ) : (hasError && operation === 'INSERT') || noPolicies || falseOnlyPolicy ? (
-
- ) : (
-
- )}
-
- {schema}.{name}
-
-
-
-
- {operation === 'SELECT' && (
-
- {noPolicies || falseOnlyPolicy
- ? 'Returns no rows'
- : !isRLSEnabled || !!trueOnlyPolicy
- ? 'Returns all rows'
- : null}
-
- )}
-
-
-
-
- {tableAccessDescription}
-
-
- )
-}
-
-const TableAccessPolicySummary = ({
- policies,
- operation,
- handleSelectEditPolicy,
-}: {
- policies: Policy[]
- operation: ParseSQLQueryOperations
- handleSelectEditPolicy: (policy: Policy) => void
-}) => {
- return (
-
-
- {policies.length} {policies.length > 1 ? 'policies' : 'policy'} applied
-
-
- {policies.map((policy) => (
-
-
-
{policy.name}
-
- {operation === 'SELECT' ? 'Show rows' : `Allow ${operation?.toLocaleLowerCase()}s`}{' '}
- where:{' '}
-
- {policy.definition ?? policy.check}
-
-
-
- }
- className="w-7"
- tooltip={{ content: { side: 'bottom', text: 'Edit policy' } }}
- onClick={() => {
- handleSelectEditPolicy(policy)
- }}
- />
-
- ))}
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTester.types.ts b/apps/studio/components/interfaces/Database/RLSTester/RLSTester.types.ts
deleted file mode 100644
index 2948c48f477..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTester.types.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import { type User } from '@/data/auth/users-infinite-query'
-import { type ParseSQLQueryResponse } from '@/data/misc/parse-query-mutation'
-
-export type ParseQueryResults = {
- tables: {
- schema: string
- table: string
- tablePolicies: Array
- isRLSEnabled: boolean
- }[]
- operation: ParseSQLQueryResponse['operation']
- role?: string
- user?: User
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterEmptyState.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterEmptyState.tsx
deleted file mode 100644
index d6eecd60beb..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterEmptyState.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { ListTodo } from 'lucide-react'
-
-export const RLSTesterEmptyState = () => {
- return (
-
-
-
Test summary and results will be shown here
-
- Verify that the results match what your RLS policies allow
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.tsx
deleted file mode 100644
index 5391c6318f8..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-import { Badge, cn, Tabs, TabsContent, TabsList, TabsTrigger } from 'ui'
-import { Admonition } from 'ui-patterns/admonition'
-
-import { Results } from '../../SQLEditor/UtilityPanel/Results'
-import { RLSTableCard } from './RLSTableCard'
-import { ParseQueryResults } from './RLSTester.types'
-import { deriveRLSTestState } from './RLSTesterResults.utils'
-import { useTestQueryRLS } from './useTestQueryRLS'
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import { type QueryResponseError } from '@/data/sql/execute-sql-mutation'
-
-interface RLSTesterResultsProps {
- results: Object[]
- autoLimit: boolean
- parseQueryResults: ParseQueryResults
- executeSqlError: Error | QueryResponseError | null | undefined
- handleSelectEditPolicy: (policy: Policy) => void
-}
-
-export const RLSTesterResults = ({
- results,
- autoLimit,
- parseQueryResults,
- executeSqlError,
- handleSelectEditPolicy,
-}: RLSTesterResultsProps) => {
- const { limit } = useTestQueryRLS()
-
- const {
- isServiceRole,
- tableWithRLSEnabledButNoPolicies,
- tableWithRLSEnabledWithPolicyFalse,
- tableWithRLSEnabledWithPoliciesDontApply,
- noAccessToData,
- } = deriveRLSTestState(parseQueryResults)
-
- const { operation, role } = parseQueryResults
- const rlsBlockInsert = executeSqlError && operation === 'INSERT'
- const noAccess = noAccessToData || rlsBlockInsert
-
- return (
-
-
-
Summary
- {noAccess ? (
-
No access
- ) : (
-
{results.length > 0 ? 'Can access' : 'Has access'}
- )}
-
-
-
-
-
- Policies applied
-
-
- Data preview
-
-
-
- {!!parseQueryResults && (
-
-
-
Ran as
- {!parseQueryResults.role ? (
-
postgres
- ) : parseQueryResults.user ? (
-
{parseQueryResults.user.email}
- ) : parseQueryResults.role === 'anon' ? (
-
an Anonymous user
- ) : null}
-
-
- {parseQueryResults.role === 'anon' && (
-
Not logged in user
- )}
- {!!parseQueryResults.user && (
-
ID: {parseQueryResults.user.id}
- )}
-
- )}
-
-
- {!isServiceRole &&
- (!!tableWithRLSEnabledButNoPolicies ? (
-
-
- This user{' '}
- {operation === 'SELECT'
- ? 'has no access to any rows'
- : `is unable to ${operation?.toLowerCase()} any rows`}{' '}
- from this query
-
-
- The table{' '}
-
- {tableWithRLSEnabledButNoPolicies.schema}.
- {tableWithRLSEnabledButNoPolicies.table}
- {' '}
- has RLS enabled but no policies set up for the{' '}
- {parseQueryResults.role}{' '}
- role.
-
-
- ) : tableWithRLSEnabledWithPolicyFalse ? (
-
-
- This user has no access to any rows from this query
-
-
- The table{' '}
-
- {tableWithRLSEnabledWithPolicyFalse.schema}.
- {tableWithRLSEnabledWithPolicyFalse.table}
- {' '}
- has a policy that evaluates to
- false for the{' '}
- {parseQueryResults.role}{' '}
- role.
-
-
- ) : rlsBlockInsert &&
- parseQueryResults.user &&
- tableWithRLSEnabledWithPoliciesDontApply ? (
-
-
- This user is unable to {operation?.toLowerCase()} any rows from this query
-
-
- The table{' '}
-
- {tableWithRLSEnabledWithPoliciesDontApply.schema}.
- {tableWithRLSEnabledWithPoliciesDontApply.table}
- {' '}
- has a policy for the{' '}
- {parseQueryResults.role}{' '}
- role, but its condition wasn't satisfied for this specific request.
-
-
- ) : null)}
-
- {isServiceRole && (
-
-
- The postgres role has access to all rows
- for this query
-
-
- The postgres role has admin privileges and
- bypasses all RLS policies.
-
-
- )}
-
-
-
Table access
- {!isServiceRole && (
-
- {parseQueryResults?.tables.map((x) => {
- const { schema, table, tablePolicies, isRLSEnabled } = x
- return (
-
- )
- })}
-
- )}
-
-
-
-
-
-
- {results.length > 0 && (
-
- {results.length} row{results.length > 1 ? 's' : ''}
- {autoLimit && results.length >= limit && ` (Limited to only ${limit} rows)`}
-
- )}
-
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.utils.ts b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.utils.ts
deleted file mode 100644
index 5f0e43025c6..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.utils.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { ParseQueryResults } from './RLSTester.types'
-
-export function deriveRLSTestState(parseQueryResults: ParseQueryResults | undefined) {
- const isServiceRole = parseQueryResults?.role === undefined
- const tableWithRLSEnabledButNoPolicies = parseQueryResults?.tables.find(
- (x) => x.isRLSEnabled && x.tablePolicies.length === 0
- )
- const tableWithRLSEnabledWithPolicyFalse = parseQueryResults?.tables.find(
- (x) => x.isRLSEnabled && x.tablePolicies.some((y) => y.definition === 'false')
- )
- const tableWithRLSEnabledWithPoliciesDontApply = parseQueryResults?.tables.find(
- (x) => x.isRLSEnabled && x.tablePolicies.length !== 0
- )
-
- const noAccessToData =
- !isServiceRole && (!!tableWithRLSEnabledButNoPolicies || !!tableWithRLSEnabledWithPolicyFalse)
-
- return {
- isServiceRole,
- tableWithRLSEnabledButNoPolicies,
- tableWithRLSEnabledWithPolicyFalse,
- tableWithRLSEnabledWithPoliciesDontApply,
- noAccessToData,
- }
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterSheet.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterSheet.tsx
deleted file mode 100644
index d5bcc5ed0b7..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterSheet.tsx
+++ /dev/null
@@ -1,387 +0,0 @@
-import {
- acceptUntrustedSql,
- safeSql,
- type SafeSqlFragment,
- type UntrustedSqlFragment,
-} from '@supabase/pg-meta'
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from '@ui/components/shadcn/ui/select'
-import { LOCAL_STORAGE_KEYS, useFlag } from 'common'
-import { Code, ExternalLink } from 'lucide-react'
-import { useEffect, useRef, useState } from 'react'
-import {
- Button,
- DialogSectionSeparator,
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetSection,
- SheetTitle,
- SheetTrigger,
-} from 'ui'
-import { Admonition } from 'ui-patterns/admonition'
-import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
-import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
-
-import { InferredSQLViewer } from './InferredSQLViewer'
-import { type ParseQueryResults } from './RLSTester.types'
-import { RLSTesterEmptyState } from './RLSTesterEmptyState'
-import { RLSTesterResults } from './RLSTesterResults'
-import { RoleSelector } from './RoleSelector'
-import { SandboxManagement } from './SandboxManagement'
-import { UserSelector } from './UserSelector'
-import { UserSqlEditor } from './UserSqlEditor'
-import { useTestQueryRLS, type TestQueryBlockedReason } from './useTestQueryRLS'
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
-import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
-import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge'
-import { useTrack } from '@/lib/telemetry/track'
-import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
-import { PostgresSandboxProvider, usePostgresSandbox } from '@/state/postgres-sandbox/sandbox'
-import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
-import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
-
-interface RLSTesterSheetProps {
- handleSelectEditPolicy: (policy: Policy) => void
-}
-
-export const RLSTesterSheet = (props: RLSTesterSheetProps) => {
- return (
-
-
-
- )
-}
-
-const RLSTesterSheetContents = ({ handleSelectEditPolicy }: RLSTesterSheetProps) => {
- const track = useTrack()
- const aiSnap = useAiAssistantStateSnapshot()
- const { openSidebar } = useSidebarManagerSnapshot()
- const { setRole } = useRoleImpersonationStateSnapshot()
- const { startSandbox, status, isSyncing } = usePostgresSandbox()
-
- const sandboxEnabled = useFlag('rlsTesterSandbox')
- const sandboxIsStarting = status === 'loading'
-
- const [open, setOpen] = useState(false)
- const [selectedOption, setSelectedOption] = useState<'anon' | 'authenticated'>('anon')
- const [blockedReason, setBlockedReason] = useState()
-
- const [format, setFormat] = useState<'sql' | 'lib'>('sql')
- const [inferredSQL, setInferredSQL] = useState()
-
- const [value, setValue] = useState(safeSql``)
- const [results, setResults] = useState(null)
- const [autoLimit, setAutoLimit] = useState(false)
- const [parseQueryResults, setParseQueryResults] = useState()
-
- const {
- testQuery,
- inferSQLFromLib,
- isLoading,
- isInferring,
- executeSqlError,
- parseQueryError,
- parseClientCodeError,
- } = useTestQueryRLS()
- const isErrorDueToRLS =
- executeSqlError?.message.includes('violates row-level security policy') ?? false
- const mutationOperation = blockedReason?.type === 'mutation' ? blockedReason.operation : undefined
-
- const debounceRef = useRef | null>(null)
-
- const handleValueChange = (sql: SafeSqlFragment) => {
- setValue(sql)
- if (format !== 'lib') return
-
- if (debounceRef.current !== null) clearTimeout(debounceRef.current)
- if (!sql) return
-
- debounceRef.current = setTimeout(() => inferSQLFromLib(sql, setInferredSQL), 1500)
- }
-
- const executionCallbacks = {
- option: selectedOption,
- acknowledgeMutation: blockedReason?.type === 'mutation',
- onExecuteSQL: ({ result, isAutoLimit }: { result: Object[] | null; isAutoLimit: boolean }) => {
- setResults(result)
- setAutoLimit(isAutoLimit)
- },
- onParseQuery: setParseQueryResults,
- onValidationBlocked: setBlockedReason,
- }
-
- const onRunQuery = async () => {
- setBlockedReason(undefined)
-
- if (format === 'lib') {
- if (!inferredSQL) return
- const blocked = await testQuery({
- value: acceptUntrustedSql(inferredSQL),
- ...executionCallbacks,
- })
- if (!blocked) track('rls_tester_run_query_clicked', { type: 'inferred' })
- } else {
- const blocked = await testQuery({ value, ...executionCallbacks })
- if (!blocked) track('rls_tester_run_query_clicked', { type: 'raw' })
- }
- }
-
- const assistantSql = format === 'lib' && inferredSQL ? acceptUntrustedSql(inferredSQL) : value
-
- const getDebugPrompt = ({ includeSql = false }: { includeSql?: boolean } = {}) => {
- const prompt = `Help me fix my RLS policy based on the attached SQL snippet that gave the following error: \n\n${executeSqlError?.message}\n\nEvaluate if the problem might be query first, before checking my RLS policies.`
-
- return includeSql ? `${prompt}\n\nSQL Query:\n\`\`\`sql\n${assistantSql}\n\`\`\`` : prompt
- }
-
- const onDebugWithAssistant = () => {
- const prompt = getDebugPrompt()
- openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
- aiSnap.newChat({
- name: 'Debug RLS policies',
- sqlSnippets: [assistantSql],
- initialInput: prompt,
- })
- setOpen(false)
- }
-
- useEffect(() => {
- setRole({ type: 'postgrest', role: 'anon' })
- return () => {
- // Flip back to service role
- setRole(undefined)
- }
- // [Joshen] Intentional - to only reset back to service role when navigating away
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- return (
- <>
-
-
- }>
- Test
-
-
-
-
-
-
- What data can my users access?
-
-
-
- See what data a user is allowed to read or modify based on your RLS policies
-
-
-
-
- {sandboxEnabled &&
}
-
-
-
-
- {selectedOption === 'authenticated' && }
-
-
-
-
-
-
Query
-
- {
- const newFormat = x as 'sql' | 'lib'
- setFormat(newFormat)
- if (newFormat !== 'lib') {
- setInferredSQL(undefined)
- if (debounceRef.current !== null) clearTimeout(debounceRef.current)
- }
- }}
- >
-
-
-
-
-
- Query format
- SQL
- Client library
-
-
-
-
-
-
-
- {
- if (!isInferring && !isLoading) onRunQuery()
- },
- },
- }}
- />
-
-
-
- {format === 'lib' && (
-
-
-
-
- )}
-
-
-
- {blockedReason?.type === 'multiple-statements' ? (
-
- ) : blockedReason?.type === 'unsupported-operation' ? (
-
- ) : parseQueryError ? (
-
- ) : parseClientCodeError ? (
-
- ) : executeSqlError && !isErrorDueToRLS ? (
-
-
getDebugPrompt({ includeSql: true })}
- onOpenAssistant={onDebugWithAssistant}
- />,
- ]}
- />
-
- ) : isLoading ? (
-
-
-
- ) : results === null && !isErrorDueToRLS ? (
-
- ) : !!parseQueryResults ? (
-
- ) : null}
-
-
-
- }>
-
- Give feedback
-
-
-
- setOpen(false)}>
- Cancel
-
-
- Run query
-
-
-
-
-
-
- setBlockedReason(undefined)}
- alert={{
- title: `This ${mutationOperation} query will run against your actual database`,
- description: 'Your database may be directly modified as a result. Are you sure?',
- }}
- >
- {sandboxEnabled && (
- <>
-
- We highly recommend using the sandbox to set up an ephemeral database environment for
- testing insert, update, or delete queries.
-
- {
- startSandbox()
- setBlockedReason(undefined)
- }}
- >
- Set up sandbox
-
- >
- )}
-
- >
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RoleSelector.tsx b/apps/studio/components/interfaces/Database/RLSTester/RoleSelector.tsx
deleted file mode 100644
index 0d23453b36b..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RoleSelector.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { RadioGroupStacked, RadioGroupStackedItem } from 'ui'
-import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
-
-import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
-
-interface RoleSelectorProps {
- onSelectRole: (value: 'anon' | 'authenticated') => void
-}
-
-export const RoleSelector = ({ onSelectRole }: RoleSelectorProps) => {
- const { role, setRole } = useRoleImpersonationStateSnapshot()
-
- return (
-
-
- {
- onSelectRole('anon')
- setRole({ type: 'postgrest', role: 'anon' })
- }}
- />
- {
- onSelectRole('authenticated')
- }}
- />
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/SandboxManagement.tsx b/apps/studio/components/interfaces/Database/RLSTester/SandboxManagement.tsx
deleted file mode 100644
index 1200d4d361e..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/SandboxManagement.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Box, Loader2, LogOut, RefreshCw } from 'lucide-react'
-import { Badge, Button } from 'ui'
-import { Admonition } from 'ui-patterns/admonition'
-
-import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
-import { usePostgresSandbox } from '@/state/postgres-sandbox/sandbox'
-
-export const SandboxManagement = () => {
- const { status, error, isSyncing, startSandbox, destroySandbox, syncSandbox } =
- usePostgresSandbox()
-
- if (status === 'idle') {
- return (
- startSandbox()}>
- Set up sandbox
- ,
- ]}
- >
-
-
Run queries in a sandbox
-
Recommended
-
-
- Ensure that queries do not affect your actual database
-
-
- )
- }
-
- if (status === 'loading') {
- return (
-
-
-
-
-
-
Setting up sandbox
-
-
- )
- }
-
- if (status === 'error') {
- return (
- startSandbox()}>
- Retry set up
- ,
- ]}
- />
- )
- }
-
- return (
- }
- className="w-7"
- disabled={isSyncing}
- tooltip={{ content: { side: 'bottom', text: 'Exit sandbox' } }}
- onClick={() => destroySandbox()}
- />,
- }
- className="w-7"
- loading={isSyncing}
- tooltip={{ content: { side: 'bottom', text: 'Refresh schema' } }}
- onClick={() => syncSandbox()}
- />,
- ]}
- >
-
-
-
-
-
Sandbox active
-
Your database is never modified
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/UserSelector.tsx b/apps/studio/components/interfaces/Database/RLSTester/UserSelector.tsx
deleted file mode 100644
index c949c2f3fe0..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/UserSelector.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import { keepPreviousData } from '@tanstack/react-query'
-import { useDebounce } from '@uidotdev/usehooks'
-import { Check, ChevronsUpDown } from 'lucide-react'
-import { useMemo, useState } from 'react'
-import { toast } from 'sonner'
-import {
- Button,
- cn,
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- copyToClipboard,
- Popover,
- PopoverContent,
- PopoverTrigger,
- ScrollArea,
-} from 'ui'
-import { Admonition } from 'ui-patterns/admonition'
-import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
-import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
-
-import { User, useUsersInfiniteQuery } from '@/data/auth/users-infinite-query'
-import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
-import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
-import { ResponseError } from '@/types'
-
-export const UserSelector = () => {
- const { data: project } = useSelectedProjectQuery()
- const state = useRoleImpersonationStateSnapshot()
-
- const [open, setOpen] = useState(false)
- const [searchText, setSearchText] = useState('')
-
- const debouncedSearchText = useDebounce(searchText, 300)
-
- const { data, error, isSuccess, isPending, isError } = useUsersInfiniteQuery(
- {
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- keywords: debouncedSearchText.trim().toLocaleLowerCase(),
- },
- { placeholderData: keepPreviousData }
- )
- const users = useMemo(() => data?.pages.flatMap((page) => page.result) ?? [], [data?.pages])
-
- const impersonatingUser =
- state.role?.type === 'postgrest' &&
- state.role.role === 'authenticated' &&
- state.role.userType === 'native'
- ? state.role.user
- : undefined
-
- const onSelectUser = async (user: User) => {
- try {
- await state.setRole({
- type: 'postgrest',
- role: 'authenticated',
- userType: 'native',
- user,
- aal: 'aal1',
- })
- } catch (error) {
- toast.error(`Failed to impersonate user: ${(error as ResponseError).message}`)
- }
- }
-
- return (
-
- ID:{' '}
- {
- copyToClipboard(impersonatingUser?.id ?? '')
- toast('Copied ID to clipboard')
- }}
- >
- {impersonatingUser.id}
-
-
- ) : undefined
- }
- >
-
-
- }
- >
- {impersonatingUser?.email ?? 'Select a user'}
-
-
-
-
-
-
- {isError ? (
-
- Failed to fetch users: {error.message}
-
- ) : (
- No user found
- )}
-
-
- {isPending && (
-
-
-
- )}
-
- {isSuccess && (
-
- 7 ? 'h-full md:h-[210px]' : ''}>
- {users.map((user) => {
- return (
- {
- onSelectUser(user)
- setOpen(false)
- }}
- >
-
-
- {user.email}
-
- {user.id?.slice(0, 8)}
-
-
- {impersonatingUser?.id === user.id &&
}
-
-
- )
- })}
-
-
- )}
-
-
-
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/UserSqlEditor.tsx b/apps/studio/components/interfaces/Database/RLSTester/UserSqlEditor.tsx
deleted file mode 100644
index f920731f49d..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/UserSqlEditor.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { rawSql, type SafeSqlFragment } from '@supabase/pg-meta'
-import type { ComponentProps } from 'react'
-
-import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
-
-interface UserSqlEditorProps {
- id: string
- value: SafeSqlFragment
- placeholder?: SafeSqlFragment
- actions?: ComponentProps['actions']
- onChange: (sql: SafeSqlFragment) => void
-}
-
-/**
- * Wraps CodeEditor for user-authored SQL. The rawSql boundary lives here β any
- * text the user types is immediately promoted to SafeSqlFragment so callers
- * never handle plain strings.
- */
-export const UserSqlEditor = ({ value, onChange, ...props }: UserSqlEditorProps) => {
- return (
- onChange(rawSql(val ?? ''))}
- {...props}
- />
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.test.tsx b/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.test.tsx
deleted file mode 100644
index 8ee7cda9bc8..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.test.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-import type { SafeSqlFragment } from '@supabase/pg-meta'
-import { screen } from '@testing-library/react'
-import { describe, expect, it, vi } from 'vitest'
-
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import type { ParseQueryResults } from '@/components/interfaces/Database/RLSTester/RLSTester.types'
-import { RLSTesterResults } from '@/components/interfaces/Database/RLSTester/RLSTesterResults'
-import { render } from '@/tests/helpers'
-
-vi.mock('@/components/interfaces/Database/RLSTester/useTestQueryRLS', () => ({
- useTestQueryRLS: () => ({ limit: 100 }),
-}))
-
-vi.mock('@/components/interfaces/Database/RLSTester/RLSTableCard', () => ({
- RLSTableCard: () =>
,
-}))
-
-vi.mock('@/components/interfaces/SQLEditor/UtilityPanel/Results', () => ({
- Results: () =>
,
-}))
-
-const sql = (s: string) => s as unknown as SafeSqlFragment
-
-const makePolicy = (definition: string | null = null): Policy =>
- ({ definition: definition !== null ? sql(definition) : null }) as Policy
-
-const makeTable = (
- overrides?: Partial
-): ParseQueryResults['tables'][number] => ({
- schema: 'public',
- table: 'items',
- isRLSEnabled: true,
- tablePolicies: [],
- ...overrides,
-})
-
-const defaultProps = {
- results: [],
- autoLimit: false,
- executeSqlError: undefined,
- handleSelectEditPolicy: vi.fn(),
-}
-
-describe('RLSTesterResults', () => {
- describe('access badge', () => {
- it('shows "No access" badge when table has RLS enabled but no policies', () => {
- render(
-
- )
- expect(screen.getByText('No access')).toBeInTheDocument()
- })
-
- it('shows "No access" badge when a policy definition is false', () => {
- render(
-
- )
- expect(screen.getByText('No access')).toBeInTheDocument()
- })
-
- it('shows "Has access" badge when results are empty and user has access', () => {
- render(
-
- )
- expect(screen.getByText('Has access')).toBeInTheDocument()
- })
-
- it('shows "Can access" badge when results are returned', () => {
- render(
-
- )
- expect(screen.getByText('Can access')).toBeInTheDocument()
- })
- })
-
- describe('policy admonitions', () => {
- it('shows service role admonition for postgres role', () => {
- render(
-
- )
- expect(screen.getByText(/bypasses all RLS policies/)).toBeInTheDocument()
- })
-
- it('shows "no policies" admonition when RLS is enabled but no policies exist', () => {
- render(
-
- )
- expect(screen.getByText(/no policies set up/)).toBeInTheDocument()
- expect(screen.getByText(/public.profiles/)).toBeInTheDocument()
- })
-
- it('shows "policy false" admonition when a policy evaluates to false', () => {
- render(
-
- )
- expect(screen.getByText(/evaluates to/)).toBeInTheDocument()
- expect(screen.getByText(/public.secrets/)).toBeInTheDocument()
- })
- })
-
- describe('"Ran as" section', () => {
- it('shows postgres for service role', () => {
- render(
-
- )
- expect(screen.getAllByText('postgres').length).toBeGreaterThan(0)
- })
-
- it('shows "an Anonymous user" for anon role', () => {
- render(
-
- )
- expect(screen.getByText('an Anonymous user')).toBeInTheDocument()
- expect(screen.getByText('Not logged in user')).toBeInTheDocument()
- })
-
- it('shows user email and ID when a user is present', () => {
- render(
-
- )
- expect(screen.getByText('alice@example.com')).toBeInTheDocument()
- expect(screen.getByText('ID: user-123')).toBeInTheDocument()
- })
- })
-})
diff --git a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.utils.test.ts b/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.utils.test.ts
deleted file mode 100644
index 3dff5da045f..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.utils.test.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-import type { SafeSqlFragment } from '@supabase/pg-meta'
-import { describe, expect, it } from 'vitest'
-
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import type { ParseQueryResults } from '@/components/interfaces/Database/RLSTester/RLSTester.types'
-import { deriveRLSTestState } from '@/components/interfaces/Database/RLSTester/RLSTesterResults.utils'
-
-const sql = (s: string) => s as unknown as SafeSqlFragment
-
-const makePolicy = (definition: string | null = null): Policy =>
- ({ definition: definition !== null ? sql(definition) : null }) as Policy
-
-const makeTable = (
- overrides?: Partial
-): ParseQueryResults['tables'][number] => ({
- schema: 'public',
- table: 'items',
- isRLSEnabled: true,
- tablePolicies: [],
- ...overrides,
-})
-
-const makeResults = (overrides?: Partial): ParseQueryResults => ({
- tables: [],
- operation: 'SELECT',
- role: 'anon',
- ...overrides,
-})
-
-describe('deriveRLSTestState', () => {
- describe('isServiceRole', () => {
- it('is true when parseQueryResults is undefined', () => {
- const { isServiceRole } = deriveRLSTestState(undefined)
- expect(isServiceRole).toBe(true)
- })
-
- it('is true when role is undefined (postgres / service role)', () => {
- const { isServiceRole } = deriveRLSTestState(makeResults({ role: undefined }))
- expect(isServiceRole).toBe(true)
- })
-
- it('is false when role is anon', () => {
- const { isServiceRole } = deriveRLSTestState(makeResults({ role: 'anon' }))
- expect(isServiceRole).toBe(false)
- })
-
- it('is false when role is authenticated', () => {
- const { isServiceRole } = deriveRLSTestState(makeResults({ role: 'authenticated' }))
- expect(isServiceRole).toBe(false)
- })
- })
-
- describe('noAccessToData', () => {
- it('is false when parseQueryResults is undefined', () => {
- const { noAccessToData } = deriveRLSTestState(undefined)
- expect(noAccessToData).toBe(false)
- })
-
- it('is false for service role even when tables have no policies', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({ role: undefined, tables: [makeTable()] })
- )
- expect(noAccessToData).toBe(false)
- })
-
- it('is false when RLS is disabled on table', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({ tables: [makeTable({ isRLSEnabled: false, tablePolicies: [] })] })
- )
- expect(noAccessToData).toBe(false)
- })
-
- it('is true when RLS is enabled and table has no policies', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({ tables: [makeTable({ isRLSEnabled: true, tablePolicies: [] })] })
- )
- expect(noAccessToData).toBe(true)
- })
-
- it('is true when RLS is enabled and a policy definition is false', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({
- tables: [makeTable({ tablePolicies: [makePolicy('false')] })],
- })
- )
- expect(noAccessToData).toBe(true)
- })
-
- it('is false when RLS is enabled and policies are valid (not false)', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({
- tables: [makeTable({ tablePolicies: [makePolicy('auth.uid() = user_id')] })],
- })
- )
- expect(noAccessToData).toBe(false)
- })
-
- it('is false when all tables have RLS disabled regardless of policy state', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({
- tables: [
- makeTable({ isRLSEnabled: false, tablePolicies: [] }),
- makeTable({
- table: 'other',
- isRLSEnabled: false,
- tablePolicies: [makePolicy('false')],
- }),
- ],
- })
- )
- expect(noAccessToData).toBe(false)
- })
- })
-
- describe('tableWithRLSEnabledButNoPolicies', () => {
- it('is undefined when no tables', () => {
- const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState(makeResults({ tables: [] }))
- expect(tableWithRLSEnabledButNoPolicies).toBeUndefined()
- })
-
- it('is undefined when RLS disabled', () => {
- const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState(
- makeResults({ tables: [makeTable({ isRLSEnabled: false })] })
- )
- expect(tableWithRLSEnabledButNoPolicies).toBeUndefined()
- })
-
- it('is undefined when table has policies', () => {
- const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState(
- makeResults({ tables: [makeTable({ tablePolicies: [makePolicy('true')] })] })
- )
- expect(tableWithRLSEnabledButNoPolicies).toBeUndefined()
- })
-
- it('returns the matching table when RLS enabled with no policies', () => {
- const table = makeTable({ table: 'profiles', tablePolicies: [] })
- const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState(
- makeResults({ tables: [table] })
- )
- expect(tableWithRLSEnabledButNoPolicies).toEqual(table)
- })
-
- it('returns the first matching table among multiple', () => {
- const first = makeTable({ table: 'profiles', tablePolicies: [] })
- const second = makeTable({ table: 'posts', tablePolicies: [] })
- const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState(
- makeResults({ tables: [first, second] })
- )
- expect(tableWithRLSEnabledButNoPolicies).toEqual(first)
- })
- })
-
- describe('tableWithRLSEnabledWithPolicyFalse', () => {
- it('is undefined when no tables', () => {
- const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState(makeResults({ tables: [] }))
- expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined()
- })
-
- it('is undefined when RLS disabled even with false policy', () => {
- const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState(
- makeResults({
- tables: [makeTable({ isRLSEnabled: false, tablePolicies: [makePolicy('false')] })],
- })
- )
- expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined()
- })
-
- it('is undefined when no policy has definition of false', () => {
- const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState(
- makeResults({
- tables: [makeTable({ tablePolicies: [makePolicy('auth.uid() = user_id')] })],
- })
- )
- expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined()
- })
-
- it('returns the table when a policy definition is exactly "false"', () => {
- const table = makeTable({ table: 'secrets', tablePolicies: [makePolicy('false')] })
- const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState(
- makeResults({ tables: [table] })
- )
- expect(tableWithRLSEnabledWithPolicyFalse).toEqual(table)
- })
-
- it('is undefined when policy definition is null (no definition)', () => {
- const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState(
- makeResults({ tables: [makeTable({ tablePolicies: [makePolicy(null)] })] })
- )
- expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined()
- })
- })
-})
diff --git a/apps/studio/components/interfaces/Database/RLSTester/__tests__/useTestQueryRLS.utils.test.ts b/apps/studio/components/interfaces/Database/RLSTester/__tests__/useTestQueryRLS.utils.test.ts
deleted file mode 100644
index a5ccadf1c6d..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/__tests__/useTestQueryRLS.utils.test.ts
+++ /dev/null
@@ -1,265 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import {
- filterTablePolicies,
- getTestQueryBlockedReason,
-} from '@/components/interfaces/Database/RLSTester/useTestQueryRLS.utils'
-
-const makePolicy = (overrides: Partial): Policy =>
- ({
- schema: 'public',
- table: 'items',
- roles: ['anon'],
- command: 'SELECT',
- ...overrides,
- }) as Policy
-
-const base = {
- policies: [] as Policy[],
- schema: 'public',
- table: 'items',
- role: 'anon',
- operation: 'SELECT' as const,
-}
-
-describe('filterTablePolicies', () => {
- describe('schema / table matching', () => {
- it('excludes policies from a different schema', () => {
- const policy = makePolicy({ schema: 'private', table: 'items' })
- expect(filterTablePolicies({ ...base, policies: [policy] })).toHaveLength(0)
- })
-
- it('excludes policies from a different table', () => {
- const policy = makePolicy({ schema: 'public', table: 'other' })
- expect(filterTablePolicies({ ...base, policies: [policy] })).toHaveLength(0)
- })
-
- it('includes a policy matching schema and table', () => {
- const policy = makePolicy({ schema: 'public', table: 'items' })
- expect(filterTablePolicies({ ...base, policies: [policy] })).toHaveLength(1)
- })
- })
-
- describe('role matching', () => {
- it('includes policy when role is in the policy roles array', () => {
- const policy = makePolicy({ roles: ['anon', 'authenticated'] })
- expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(1)
- })
-
- it('excludes policy when role is not in the policy roles array', () => {
- const policy = makePolicy({ roles: ['authenticated'] })
- expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(0)
- })
-
- it('includes policy when the only role is "public" (applies to all roles)', () => {
- const policy = makePolicy({ roles: ['public'] })
- expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(1)
- })
-
- it('excludes "public" role shortcut when policy has multiple roles including public', () => {
- const policy = makePolicy({ roles: ['public', 'authenticated'] })
- expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(0)
- })
-
- it('handles undefined role (service role) β matches nothing unless public', () => {
- const rolePolicy = makePolicy({ roles: ['anon'] })
- const publicPolicy = makePolicy({ roles: ['public'] })
- const result = filterTablePolicies({
- ...base,
- role: undefined,
- policies: [rolePolicy, publicPolicy],
- })
- expect(result).toHaveLength(1)
- expect(result[0]).toBe(publicPolicy)
- })
- })
-
- describe('command matching', () => {
- it('includes policy when command matches the operation', () => {
- const policy = makePolicy({ command: 'SELECT' })
- expect(
- filterTablePolicies({ ...base, operation: 'SELECT', policies: [policy] })
- ).toHaveLength(1)
- })
-
- it('excludes policy when command does not match the operation', () => {
- const policy = makePolicy({ command: 'INSERT' })
- expect(
- filterTablePolicies({ ...base, operation: 'SELECT', policies: [policy] })
- ).toHaveLength(0)
- })
-
- it('includes policy with command ALL regardless of operation', () => {
- const policy = makePolicy({ command: 'ALL' })
- expect(
- filterTablePolicies({ ...base, operation: 'SELECT', policies: [policy] })
- ).toHaveLength(1)
- })
-
- it('includes ALL command policy for non-SELECT operations too', () => {
- const policy = makePolicy({ command: 'ALL' })
- expect(
- filterTablePolicies({ ...base, operation: 'INSERT', policies: [policy] })
- ).toHaveLength(1)
- })
-
- it('does not include a SELECT-only policy when operation is INSERT', () => {
- const policy = makePolicy({ command: 'SELECT' })
- expect(
- filterTablePolicies({ ...base, operation: 'INSERT', policies: [policy] })
- ).toHaveLength(0)
- })
- })
-
- describe('combined filters', () => {
- it('returns only policies that satisfy all conditions', () => {
- const match = makePolicy({
- schema: 'public',
- table: 'items',
- roles: ['anon'],
- command: 'ALL',
- })
- const wrongSchema = makePolicy({
- schema: 'private',
- table: 'items',
- roles: ['anon'],
- command: 'ALL',
- })
- const wrongRole = makePolicy({
- schema: 'public',
- table: 'items',
- roles: ['authenticated'],
- command: 'ALL',
- })
- const wrongCommand = makePolicy({
- schema: 'public',
- table: 'items',
- roles: ['anon'],
- command: 'INSERT',
- })
-
- const result = filterTablePolicies({
- ...base,
- policies: [match, wrongSchema, wrongRole, wrongCommand],
- })
- expect(result).toHaveLength(1)
- expect(result[0]).toBe(match)
- })
- })
-})
-
-describe('getTestQueryBlockedReason', () => {
- const blockedBase = {
- statementCount: 1,
- operation: 'SELECT' as const,
- hasSandbox: false,
- acknowledgeMutation: false,
- }
-
- describe('multiple statements', () => {
- it('blocks when statementCount is greater than 1', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, statementCount: 2 })).toStrictEqual({
- type: 'multiple-statements',
- })
- })
-
- it('takes priority over an unsupported operation', () => {
- expect(
- getTestQueryBlockedReason({ ...blockedBase, statementCount: 2, operation: 'DELETE' })
- ).toStrictEqual({ type: 'multiple-statements' })
- })
-
- it('takes priority over an unacknowledged mutation', () => {
- expect(
- getTestQueryBlockedReason({ ...blockedBase, statementCount: 2, operation: 'INSERT' })
- ).toStrictEqual({ type: 'multiple-statements' })
- })
-
- it('does not block when statementCount is exactly 1', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, statementCount: 1 })).toBeUndefined()
- })
-
- it('does not block when statementCount is 0', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, statementCount: 0 })).toBeUndefined()
- })
- })
-
- describe('unsupported operations', () => {
- it('blocks UPDATE', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'UPDATE' })).toStrictEqual({
- type: 'unsupported-operation',
- operation: 'UPDATE',
- })
- })
-
- it('blocks DELETE', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'DELETE' })).toStrictEqual({
- type: 'unsupported-operation',
- operation: 'DELETE',
- })
- })
-
- it('blocks UPDATE even with a sandbox available', () => {
- expect(
- getTestQueryBlockedReason({ ...blockedBase, operation: 'UPDATE', hasSandbox: true })
- ).toStrictEqual({ type: 'unsupported-operation', operation: 'UPDATE' })
- })
-
- it('blocks DELETE even when already acknowledged', () => {
- expect(
- getTestQueryBlockedReason({
- ...blockedBase,
- operation: 'DELETE',
- acknowledgeMutation: true,
- })
- ).toStrictEqual({ type: 'unsupported-operation', operation: 'DELETE' })
- })
- })
-
- describe('INSERT mutation warning', () => {
- it('blocks an unacknowledged INSERT with no sandbox', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'INSERT' })).toStrictEqual({
- type: 'mutation',
- operation: 'INSERT',
- })
- })
-
- it('does not block an INSERT when a sandbox is available', () => {
- expect(
- getTestQueryBlockedReason({ ...blockedBase, operation: 'INSERT', hasSandbox: true })
- ).toBeUndefined()
- })
-
- it('does not block an INSERT once acknowledged', () => {
- expect(
- getTestQueryBlockedReason({
- ...blockedBase,
- operation: 'INSERT',
- acknowledgeMutation: true,
- })
- ).toBeUndefined()
- })
-
- it('does not require acknowledgement when a sandbox is available', () => {
- expect(
- getTestQueryBlockedReason({
- ...blockedBase,
- operation: 'INSERT',
- hasSandbox: true,
- acknowledgeMutation: false,
- })
- ).toBeUndefined()
- })
- })
-
- describe('unblocked operations', () => {
- it('does not block SELECT', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'SELECT' })).toBeUndefined()
- })
-
- it('does not block when operation is undefined', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, operation: undefined })).toBeUndefined()
- })
- })
-})
diff --git a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.ts b/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.ts
deleted file mode 100644
index c94328db294..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.ts
+++ /dev/null
@@ -1,261 +0,0 @@
-import { safeSql, type SafeSqlFragment, type UntrustedSqlFragment } from '@supabase/pg-meta'
-import { useState } from 'react'
-import { toast } from 'sonner'
-
-import { checkIfAppendLimitRequired, suffixWithLimit } from '../../SQLEditor/SQLEditor.utils'
-import { type ParseQueryResults } from './RLSTester.types'
-import {
- filterTablePolicies,
- getTestQueryBlockedReason,
- type TestQueryBlockedReason,
-} from './useTestQueryRLS.utils'
-import { useParseClientCodeMutation } from '@/data/ai/parse-client-code-mutation'
-import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
-import { useCheckTableRLSStatusMutation } from '@/data/database/table-check-rls-mutation'
-import {
- useParseSQLQueryMutation,
- type ParseSQLQueryOperations,
-} from '@/data/misc/parse-query-mutation'
-import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
-import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
-import { wrapWithRoleImpersonation } from '@/lib/role-impersonation'
-import { usePostgresSandbox } from '@/state/postgres-sandbox/sandbox'
-import {
- isRoleImpersonationEnabled,
- useGetImpersonatedRoleState,
- useImpersonatedUser,
- useRoleImpersonationStateSnapshot,
-} from '@/state/role-impersonation-state'
-import { type ResponseError } from '@/types'
-
-const limit = 100
-
-export type { TestQueryBlockedReason }
-
-// [Joshen] Pre-requisite work for identifying UPDATE / DELETE failures due to RLS - not yet wired
-// in since those operations are currently blocked (see TestQueryBlockedReason's 'unsupported-
-// operation'). Exported so it isn't flagged as unused until the follow-up PR wires it back in.
-export const wrapReturnRowsAffected = (sql: SafeSqlFragment) => {
- return safeSql`
- DO $$
-DECLARE
- row_count integer;
-BEGIN
- ${sql}${(sql.endsWith(';') ? '' : ';') as SafeSqlFragment}
- GET DIAGNOSTICS row_count = ROW_COUNT;
- -- store it somewhere you can read back
- PERFORM set_config('rls_tester.rows_affected', row_count::text, true);
-END $$;
-
-SELECT current_setting('rls_tester.rows_affected', true);
-`
-}
-
-/**
- * [Joshen] Testing a SQL query for its RLS access involves 3 async steps
- * 0. (Optional) Inferring client library code to SQL query via the AI Assistant
- * 1. Parsing the provided SQL query to retrieve its operation type + tables involved
- * 2. Checking for tables involved if they've got RLS enabled
- * 3. Actually running the query to retrieve the results
- *
- * Errors should all be handled as part of the UI instead of toasts, hence the empty onError
- * handlers to mute the default error handlers within the react query mutationhooks
- */
-export const useTestQueryRLS = () => {
- const { data: project } = useSelectedProjectQuery()
- const { role } = useRoleImpersonationStateSnapshot()
-
- const { sandbox } = usePostgresSandbox()
- const getImpersonatedRoleState = useGetImpersonatedRoleState()
- const impersonatedRoleState = getImpersonatedRoleState()
- const user = useImpersonatedUser()
-
- const [isLoading, setIsLoading] = useState(false)
- const [sandboxError, setSandboxError] = useState()
-
- const { data: policies = [] } = useDatabasePoliciesQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
-
- const { mutateAsync: executeSql, error: executeSqlMutationError } = useExecuteSqlMutation({
- onError: () => {},
- })
- const executeSqlError = sandbox ? sandboxError : executeSqlMutationError
-
- const {
- mutateAsync: parseClientCode,
- isPending: isInferring,
- error: parseClientCodeError,
- } = useParseClientCodeMutation({
- onError: () => {},
- })
-
- const inferSQLFromLib = async (
- value: string,
- onInferSQL: (unchecked_sql: UntrustedSqlFragment) => void
- ) => {
- const { unchecked_sql, valid } = await parseClientCode({ code: value })
- if (valid && unchecked_sql != null) {
- onInferSQL(unchecked_sql)
- } else {
- toast.error('Client library code provided is not valid')
- }
- }
-
- const { mutateAsync: parseQuery, error: parseQueryError } = useParseSQLQueryMutation({
- onError: () => {},
- })
-
- const { mutateAsync: getTableRLSStatus, error: getTableRLSStatusError } =
- useCheckTableRLSStatusMutation({
- onError: () => {},
- })
-
- /**
- * Returns true if the query was blocked (multiple statements, or an unacknowledged mutation)
- * and did not run, false if it ran (successfully or not)
- */
- const testQuery = async ({
- value,
- option,
- acknowledgeMutation = false,
- onExecuteSQL,
- onParseQuery,
- onValidationBlocked,
- }: {
- value: SafeSqlFragment
- option: 'anon' | 'authenticated'
- acknowledgeMutation?: boolean
- onExecuteSQL: ({
- result,
- operation,
- isAutoLimit,
- }: {
- result: Object[] | null
- operation: ParseSQLQueryOperations
- isAutoLimit: boolean
- }) => void
- onParseQuery: (results?: ParseQueryResults) => void
- onValidationBlocked: (reason: TestQueryBlockedReason) => void
- }): Promise => {
- if (!project) {
- console.error('Project is required')
- return true
- }
-
- if (option === 'authenticated' && !user) {
- toast('Select which user to test as before running the query')
- return true
- }
-
- try {
- setIsLoading(true)
- setSandboxError(undefined)
-
- const { appendAutoLimit } = checkIfAppendLimitRequired(value, limit)
- const formattedSql = suffixWithLimit(value, limit)
- const data = await parseQuery({ sql: formattedSql })
-
- const blockedReason = getTestQueryBlockedReason({
- statementCount: data.statementCount,
- operation: data.operation,
- hasSandbox: !!sandbox,
- acknowledgeMutation,
- })
- if (blockedReason) {
- onValidationBlocked(blockedReason)
- return true
- }
-
- const formattedTables = data.tables.map((x) => {
- const [schema, table] = x.includes('.') ? x.split('.') : ['public', x]
- return { schema, table }
- })
- const response = await getTableRLSStatus({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- tables: formattedTables,
- })
-
- const tables = response
- .map(({ table, schema, rls_enabled }) => {
- const tablePolicies = filterTablePolicies({
- policies,
- schema,
- table,
- role: role?.role,
- operation: data.operation,
- })
- return {
- table,
- schema,
- isRLSEnabled: rls_enabled,
- tablePolicies,
- }
- })
- .sort((a, b) => {
- const aFirst = a.isRLSEnabled && a.tablePolicies.length === 0
- const bFirst = b.isRLSEnabled && b.tablePolicies.length === 0
- return Number(bFirst) - Number(aFirst)
- })
-
- const autoLimit = appendAutoLimit ? limit : undefined
- // UPDATE/DELETE are blocked above, so wrapReturnRowsAffected isn't wired in here yet -
- // it's kept for the follow-up PR that adds proper UPDATE/DELETE support
- const sql = wrapWithRoleImpersonation(formattedSql, impersonatedRoleState)
-
- try {
- const { result } = sandbox
- ? await sandbox.run({ sql }).catch((e) => {
- setSandboxError(e instanceof Error ? e : new Error(String(e)))
- throw e
- })
- : await executeSql({
- sql,
- autoLimit,
- projectRef: project.ref,
- connectionString: project.connectionString,
- isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRoleState.role),
- isStatementTimeoutDisabled: true,
- handleError: (e) => {
- throw e
- },
- queryKey: ['rls-tester'],
- })
-
- onExecuteSQL({ result, operation: data.operation, isAutoLimit: !!autoLimit })
- onParseQuery({ tables, operation: data.operation, role: role?.role, user })
- } catch (error) {
- const isRLSInsertError = Boolean(
- (error as ResponseError)?.message?.includes('new row violates row-level security policy')
- )
- onExecuteSQL({ result: null, operation: data.operation, isAutoLimit: false })
- if (isRLSInsertError) {
- onParseQuery({ tables, operation: data.operation, role: role?.role, user })
- } else {
- onParseQuery(undefined)
- }
- }
- } catch (error) {
- onExecuteSQL({ result: null, operation: undefined, isAutoLimit: false })
- onParseQuery(undefined)
- } finally {
- setIsLoading(false)
- }
-
- return false
- }
-
- return {
- limit,
- testQuery,
- inferSQLFromLib,
- isLoading,
- isInferring,
- executeSqlError,
- parseQueryError,
- parseClientCodeError,
- getTableRLSStatusError,
- }
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.utils.ts b/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.utils.ts
deleted file mode 100644
index b0e698dc43b..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.utils.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import type { ParseSQLQueryResponse } from '@/data/misc/parse-query-mutation'
-
-export type TestQueryBlockedReason =
- | { type: 'multiple-statements' }
- | { type: 'unsupported-operation'; operation: 'UPDATE' | 'DELETE' }
- | { type: 'mutation'; operation: 'INSERT' }
-
-/**
- * Decides whether a query should be blocked from running, and why. Checked in this order:
- * multiple statements first (regardless of operation), then unsupported operations (UPDATE/
- * DELETE aren't testable yet - RLS blocks them silently instead of raising an error), then
- * INSERT mutations against the real database that haven't been acknowledged yet.
- */
-export function getTestQueryBlockedReason({
- statementCount,
- operation,
- hasSandbox,
- acknowledgeMutation,
-}: {
- statementCount: number
- operation: ParseSQLQueryResponse['operation']
- hasSandbox: boolean
- acknowledgeMutation: boolean
-}): TestQueryBlockedReason | undefined {
- if (statementCount > 1) return { type: 'multiple-statements' }
- if (operation === 'UPDATE' || operation === 'DELETE') {
- return { type: 'unsupported-operation', operation }
- }
- if (operation === 'INSERT' && !hasSandbox && !acknowledgeMutation) {
- return { type: 'mutation', operation }
- }
- return undefined
-}
-
-export function filterTablePolicies({
- policies,
- schema,
- table,
- role,
- operation,
-}: {
- policies: Policy[]
- schema: string
- table: string
- role: string | undefined
- operation: ParseSQLQueryResponse['operation']
-}): Policy[] {
- return policies.filter(
- (x) =>
- x.schema === schema &&
- x.table === table &&
- (x.roles.includes(role ?? '') || (x.roles.length === 1 && x.roles[0] === 'public')) &&
- (x.command === 'ALL' || x.command === operation)
- )
-}
diff --git a/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx b/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx
index b8aa2e24b7a..75a441a86e9 100644
--- a/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx
+++ b/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx
@@ -5,7 +5,6 @@ export const BANNER_ID = {
INDEX_ADVISOR: 'index-advisor-banner',
TABLE_EDITOR_QUEUE_OPERATIONS: 'table-editor-queue-operations-banner',
RLS_EVENT_TRIGGER: 'rls-event-trigger-banner',
- RLS_TESTER: 'rls-tester-banner',
FREE_MICRO_UPGRADE: 'free-micro-upgrade-banner',
TOS_UPDATE: 'tos-update-banner',
UNIFIED_LOGS: 'unified-logs-banner',
diff --git a/apps/studio/components/ui/BannerStack/Banners/BannerRlsTester.tsx b/apps/studio/components/ui/BannerStack/Banners/BannerRlsTester.tsx
deleted file mode 100644
index 61b1dbca19d..00000000000
--- a/apps/studio/components/ui/BannerStack/Banners/BannerRlsTester.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { LOCAL_STORAGE_KEYS } from 'common'
-import { useParams } from 'common/hooks'
-import { AnimatePresence, motion } from 'framer-motion'
-import { Check, Loader2, Terminal } from 'lucide-react'
-import { useEffect, useState } from 'react'
-import { Badge, Button, cn } from 'ui'
-
-import { BannerCard } from '../BannerCard'
-import { useBannerStack } from '../BannerStackProvider'
-import { useFeaturePreviewModal } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
-import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
-
-const text = 'select * from colors'
-
-export const BannerRlsTester = () => {
- const { ref } = useParams()
- const { selectFeaturePreview } = useFeaturePreviewModal()
-
- const [runQueryAnimate, setRunQueryAnimate] = useState(false)
- const [showSummary, setShowSummary] = useState(false)
-
- const { dismissBanner } = useBannerStack()
- const [, setIsDismissed] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.RLS_TESTER_BANNER_DISMISSED(ref ?? ''),
- false
- )
-
- useEffect(() => {
- setTimeout(() => setRunQueryAnimate(true), 2400)
- }, [])
-
- useEffect(() => {
- if (runQueryAnimate) {
- setTimeout(() => setShowSummary(true), 1700)
- }
- }, [runQueryAnimate])
-
- return (
- {
- setIsDismissed(true)
- dismissBanner('rls-tester-banner')
- }}
- >
-
-
-
- Preview
-
-
-
-
- {runQueryAnimate && !showSummary ? (
-
- ) : (
-
- )}
-
- {text}
-
-
-
-
- {showSummary && (
-
-
-
-
- Can access public.colors
-
-
-
-
- )}
-
-
-
-
-
Row Level Security (RLS) Tester
-
- Verify your RLS policies are correct by running queries as a specific user
-
-
-
selectFeaturePreview(LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER)}
- >
- Enable feature preview
-
-
-
- )
-}
diff --git a/apps/studio/data/rls-tester/get-schema-ddl.ts b/apps/studio/data/rls-tester/get-schema-ddl.ts
deleted file mode 100644
index 815d1d51ab4..00000000000
--- a/apps/studio/data/rls-tester/get-schema-ddl.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import pgMeta, {
- getEntityDefinitionsSql,
- joinSqlFragments,
- literal,
- safeSql,
- type PGPolicy,
-} from '@supabase/pg-meta'
-import { z } from 'zod'
-
-import { executeSql } from '@/data/sql/execute-sql-mutation'
-import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas'
-
-export interface RlsTableStatus {
- schema: string
- table: string
- rls_enabled: boolean
- rls_forced: boolean
-}
-
-export interface CustomRole {
- name: string
-}
-
-export interface DatabaseSchemaDDL {
- schemas: string[]
- typeDefinitions: string[]
- entityDefinitions: string[]
- functionDefinitions: string[]
- policies: PGPolicy[]
- rlsStatuses: RlsTableStatus[]
- customRoles: CustomRole[]
-}
-
-const pgMetaRolesList = pgMeta.roles.list()
-const pgMetaFunctionsZod = pgMeta.functions.list().zod
-const pgMetaPoliciesZod = pgMeta.policies.list().zod
-const pgMetaTablesZod = pgMeta.tables.list().zod
-
-// Extension-owned / platform-specific schemas whose DDL depends on C extensions,
-// custom operators, and platform functions that PGlite cannot replicate.
-// We skip entity/function/type DDL for these but still fetch their policies β
-// those may reference user tables we do load.
-const SUPABASE_INTERNAL_SCHEMAS = new Set([...INTERNAL_SCHEMAS, '_realtime'])
-
-const SYSTEM_ROLES = new Set([
- 'postgres',
- 'anon',
- 'authenticated',
- 'service_role',
- 'supabase_admin',
- 'supabase_auth_admin',
- 'supabase_storage_admin',
- 'supabase_replication_admin',
- 'supabase_read_only_user',
- 'pg_monitor',
- 'pg_read_all_settings',
- 'pg_read_all_stats',
- 'pg_stat_scan_tables',
- 'pg_read_server_files',
- 'pg_write_server_files',
- 'pg_execute_server_program',
- 'pg_signal_backend',
- 'dashboard_user',
- 'pgbouncer',
-])
-
-function getTypeDefinitionsSql(schemas: string[]) {
- return safeSql`
- SELECT
- CASE t.typtype
- WHEN 'e' THEN
- 'CREATE TYPE ' || quote_ident(n.nspname) || '.' || quote_ident(t.typname) ||
- ' AS ENUM (' ||
- (SELECT string_agg(quote_literal(e.enumlabel), ', ' ORDER BY e.enumsortorder)
- FROM pg_enum e WHERE e.enumtypid = t.oid) ||
- ')'
- WHEN 'c' THEN
- 'CREATE TYPE ' || quote_ident(n.nspname) || '.' || quote_ident(t.typname) ||
- ' AS (' ||
- (SELECT string_agg(quote_ident(a.attname) || ' ' || pg_catalog.format_type(a.atttypid, a.atttypmod), ', ' ORDER BY a.attnum)
- FROM pg_attribute a WHERE a.attrelid = t.typrelid AND a.attnum > 0 AND NOT a.attisdropped) ||
- ')'
- WHEN 'd' THEN
- 'CREATE DOMAIN ' || quote_ident(n.nspname) || '.' || quote_ident(t.typname) ||
- ' AS ' || pg_catalog.format_type(t.typbasetype, t.typtypmod)
- END AS definition
- FROM pg_type t
- JOIN pg_namespace n ON n.oid = t.typnamespace
- LEFT JOIN pg_class c ON c.oid = t.typrelid
- LEFT JOIN pg_depend d ON d.objid = t.oid AND d.deptype = 'e'
- WHERE n.nspname IN (${joinSqlFragments(schemas.map(literal), ', ')})
- AND t.typtype IN ('e', 'c', 'd')
- AND d.objid IS NULL
- AND (t.typtype != 'c' OR c.relkind = 'c')
- ORDER BY t.typtype, n.nspname, t.typname
- `
-}
-
-type Variables = {
- projectRef?: string
- connectionString?: string | null
- schemas: string[]
-}
-
-export async function getDatabaseSchemaDDL(
- { projectRef, connectionString, schemas }: Variables,
- signal?: AbortSignal
-): Promise {
- const userSchemas = schemas.filter((s) => !SUPABASE_INTERNAL_SCHEMAS.has(s))
-
- const entitySql = getEntityDefinitionsSql({ schemas: userSchemas })
- const functionsSql = pgMeta.functions.list({ includedSchemas: userSchemas }).sql
- const policiesSql = pgMeta.policies.list({ includedSchemas: schemas }).sql
- const tablesSql = pgMeta.tables.list({ includedSchemas: userSchemas }).sql
-
- const [entityResult, policiesResult, rlsResult, rolesResult, functionsResult, typesResult] =
- await Promise.all([
- executeSql(
- { projectRef, connectionString, sql: entitySql, queryKey: ['rls-sandbox-ddl'] },
- signal
- ),
- executeSql(
- { projectRef, connectionString, sql: policiesSql, queryKey: ['rls-sandbox-policies'] },
- signal
- ),
- executeSql(
- { projectRef, connectionString, sql: tablesSql, queryKey: ['rls-sandbox-rls'] },
- signal
- ),
- executeSql(
- { projectRef, connectionString, sql: pgMetaRolesList.sql, queryKey: ['rls-sandbox-roles'] },
- signal
- ),
- executeSql(
- {
- projectRef,
- connectionString,
- sql: functionsSql,
- queryKey: ['rls-sandbox-functions'],
- },
- signal
- ),
- executeSql(
- {
- projectRef,
- connectionString,
- sql: getTypeDefinitionsSql(userSchemas),
- queryKey: ['rls-sandbox-types'],
- },
- signal
- ),
- ])
-
- const roles = (rolesResult.result as z.infer).filter(
- (r) => !SYSTEM_ROLES.has(r.name) && !r.name.startsWith('pg_') && !r.name.startsWith('supabase_')
- )
-
- const functions = (functionsResult.result as z.infer).filter(
- (f) => (f.language === 'sql' || f.language === 'plpgsql') && f.return_type !== 'trigger'
- )
-
- return {
- schemas: userSchemas,
- typeDefinitions: (typesResult.result as { definition: string }[]).map((r) => r.definition),
- entityDefinitions: (entityResult.result[0]?.data?.definitions ?? []).map(
- (d: { sql: string }) => d.sql
- ),
- functionDefinitions: functions.map((f) => f.complete_statement),
- policies: policiesResult.result as z.infer as PGPolicy[],
- rlsStatuses: (rlsResult.result as z.infer).map((t) => ({
- schema: t.schema,
- table: t.name,
- rls_enabled: t.rls_enabled,
- rls_forced: t.rls_forced,
- })),
- customRoles: roles,
- }
-}
-
-export type DatabaseSchemaDDLData = Awaited>
diff --git a/apps/studio/data/rls-tester/get-seed-data.ts b/apps/studio/data/rls-tester/get-seed-data.ts
deleted file mode 100644
index 1d4dd3318df..00000000000
--- a/apps/studio/data/rls-tester/get-seed-data.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { ident, joinSqlFragments, literal, safeSql } from '@supabase/pg-meta'
-
-import { RlsTableStatus } from './get-schema-ddl'
-import { executeSql } from '@/data/sql/execute-sql-mutation'
-
-export interface TableSeedData {
- schema: string
- table: string
- rows: Record[]
-}
-
-// Each entry can optionally restrict which columns are fetched. Used by the
-// sandbox to avoid pulling secrets (e.g. auth.users encrypted_password / tokens)
-// into the browser-side PGlite instance.
-export type SeedTableEntry = RlsTableStatus & { columns?: readonly string[] }
-
-type Variables = {
- projectRef?: string
- connectionString?: string | null
- tables: SeedTableEntry[]
- rowLimit: number
-}
-
-async function fetchTableSeed(
- {
- projectRef,
- connectionString,
- schema,
- table,
- columns,
- rowLimit,
- }: Omit & {
- schema: string
- table: string
- columns?: readonly string[]
- },
- signal?: AbortSignal
-): Promise {
- try {
- const projection =
- columns && columns.length > 0 ? joinSqlFragments(columns.map(ident), ', ') : safeSql`*`
- const { result } = await executeSql(
- {
- projectRef,
- connectionString,
- sql: safeSql`SELECT ${projection} FROM ${ident(schema)}.${ident(table)} LIMIT ${literal(Number(rowLimit))}`,
- queryKey: ['rls-sandbox-seed', schema, table],
- },
- signal
- )
- return { schema, table, rows: (result ?? []) as Record[] }
- } catch {
- return { schema, table, rows: [] }
- }
-}
-
-const SEED_CONCURRENCY = 8
-
-export async function getProjectSeedData(
- { projectRef, connectionString, tables, rowLimit }: Variables,
- signal?: AbortSignal
-): Promise {
- const results: TableSeedData[] = []
- const queue = tables.slice()
- const workers = Array.from({ length: Math.min(SEED_CONCURRENCY, queue.length) }, async () => {
- while (queue.length > 0) {
- const entry = queue.shift()
- if (!entry) break
- results.push(
- await fetchTableSeed(
- {
- projectRef,
- connectionString,
- schema: entry.schema,
- table: entry.table,
- columns: entry.columns,
- rowLimit,
- },
- signal
- )
- )
- }
- })
- await Promise.all(workers)
- return results.filter((t) => t.rows.length > 0)
-}
-
-export type ProjectSeedDataData = TableSeedData[]
diff --git a/apps/studio/package.json b/apps/studio/package.json
index 442d76c48d2..547bc0a3c54 100644
--- a/apps/studio/package.json
+++ b/apps/studio/package.json
@@ -51,8 +51,6 @@
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
- "@electric-sql/pglite": "0.4.5",
- "@electric-sql/pglite-tools": "^0.3.4",
"@graphiql/react": "^0.37.3",
"@graphiql/toolkit": "^0.11.3",
"@hcaptcha/react-hcaptcha": "^1.12.0",
diff --git a/apps/studio/pages/project/[ref]/database/policies.tsx b/apps/studio/pages/project/[ref]/database/policies.tsx
index 766d3ac6afa..34823209990 100644
--- a/apps/studio/pages/project/[ref]/database/policies.tsx
+++ b/apps/studio/pages/project/[ref]/database/policies.tsx
@@ -1,5 +1,5 @@
-import { ident, safeSql } from '@supabase/pg-meta'
import type { PGTable } from '@supabase/pg-meta'
+import { ident, safeSql } from '@supabase/pg-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { Search, X } from 'lucide-react'
@@ -21,7 +21,6 @@ import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { useIsInlineEditorEnabled } from '@/components/interfaces/Account/Preferences/useDashboardSettings'
-import { useIsRLSTesterEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { Policies } from '@/components/interfaces/Database/Policies/Policies'
import { getGeneralPolicyTemplates } from '@/components/interfaces/Database/Policies/Policies.constants'
import { PoliciesDataProvider } from '@/components/interfaces/Database/Policies/PoliciesDataContext'
@@ -30,15 +29,12 @@ import {
generatePolicyUpdateSQL,
type Policy,
} from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import { RLSTesterSheet } from '@/components/interfaces/Database/RLSTester/RLSTesterSheet'
import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { getExposedSchemas } from '@/components/layouts/ProjectNeedsSecuring/ProjectNeedsSecuring.utils'
import { AlertError } from '@/components/ui/AlertError'
import { AutoEnableRLSNotice } from '@/components/ui/AutoEnableRLSNotice'
-import { BannerRlsTester } from '@/components/ui/BannerStack/Banners/BannerRlsTester'
-import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
import { DocsButton } from '@/components/ui/DocsButton'
import { NoPermission } from '@/components/ui/NoPermission'
import { SchemaSelector } from '@/components/ui/SchemaSelector'
@@ -121,7 +117,6 @@ const DatabasePoliciesPage: NextPageWithLayout = () => {
)
const isInlineEditorEnabled = useIsInlineEditorEnabled()
- const rlsTesterEnabled = useIsRLSTesterEnabled()
const { openSidebar } = useSidebarManagerSnapshot()
const {
@@ -150,18 +145,12 @@ const DatabasePoliciesPage: NextPageWithLayout = () => {
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => setSearchString(''))
const { isSchemaLocked } = useIsProtectedSchema({ schema: schema, excludedSchemas: ['realtime'] })
- const { addBanner, dismissBanner } = useBannerStack()
const [isAutoEnableRLSMinimized] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.RLS_EVENT_TRIGGER_BANNER_DISMISSED(projectRef ?? ''),
false
)
- const [isRlsTesterBannerDismissed] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.RLS_TESTER_BANNER_DISMISSED(projectRef ?? ''),
- false
- )
-
const {
data: policies = [],
isPending: isLoadingPolicies,
@@ -249,25 +238,6 @@ const DatabasePoliciesPage: NextPageWithLayout = () => {
const handleResetSearch = useCallback(() => setSearchString(''), [setSearchString])
- useEffect(() => {
- if (rlsTesterEnabled) return
-
- if (!isRlsTesterBannerDismissed) {
- addBanner({
- id: 'rls-tester-banner',
- isDismissed: false,
- content: ,
- priority: 3,
- })
- } else {
- dismissBanner('rls-tester-banner')
- }
-
- return () => {
- dismissBanner('rls-tester-banner')
- }
- }, [addBanner, dismissBanner, isRlsTesterBannerDismissed, rlsTesterEnabled])
-
useEffect(() => {
if (selectedIdToEdit && isPoliciesSuccess && !selectedPolicyToEdit) {
toast(`Policy ID ${selectedIdToEdit} cannot be found`)
@@ -294,7 +264,6 @@ const DatabasePoliciesPage: NextPageWithLayout = () => {
{isAutoEnableRLSMinimized && }
- {rlsTesterEnabled && }
@@ -343,6 +312,7 @@ const DatabasePoliciesPage: NextPageWithLayout = () => {
}
onClick={() => setSearchString('')}
diff --git a/apps/studio/public/img/previews/rls-tester-preview.png b/apps/studio/public/img/previews/rls-tester-preview.png
deleted file mode 100644
index c21ed334276427a7062955182a3a2d6a70a2e6b2..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 157676
zcmeFY`8$+v{Qs>tSxVMKS;msc5-K}0)|4gNh>$fQJK4?HCWK~E*|JkY+4t=06fz=N
zvkivq+gQfTe6Q*K{(O$({sZnG?&JO*2gYpYbzSFqzP9JES)~CU7`*R3^rND>Bux1|bI;`JHt^yZKNB4dsfB%2|Udf4H?0?@Y_PiXY`@e6NSkdPH?~~VmQX-4~XNcGDzx>~?
z-TS{&LfJU~cb5RO@c(_oc`*Avt8*w%bEL?)V0Vl}B#8X=3N?-mb2}8jerg6hH}R*<_je!4Rd?+~Fccf+fQuU&
z6(wz3Rl)B;pM9@OY)8X&anqVrpv2q+NU^892_qIxs?f-%vD@
zl4xumA~%6kYB29#_fx`r`W^CQAT2C`N8`tsP3U)BW6A==&)oCBE~aVz*&^xE4Qm&!
zzfR30OMnJ;_&acqF7Gn+T#6#)T)|(}YM3s7z$)f(H)S`6Kj^*r>(xqZ5&2;(AuB_9
z#{$53u7L0Jk5jvd$a@g;*CRq_i!MHD4GSiD6=Eo7SR+aOL+Q55Q0}S!NTI>sOS(D=
zDg6GNgW@xX=`P3p%A?nk_nc1qJG==ITcb^rhIod*V#3%+`9viF5ZwgqYH
z?DT)d~|xI-==5Ryv}WalynY
zr~X}nyw^-7<@8}kj9S8fk13`?Sg&^7U-?m7TzuW&-R2a|51WIQGRK46s9S%zi#OL-coc{%)-mq%Gty`|TV)Vx1LXlx3lKq>yXW#{Pt^|B
zvThGT_P)!T8g{ijrgw%AR}FDuqgGxmvUG9!*ro!(3Wfb4&5zI2c~`&6&yYs>FU?QqZiF-Mz@D_v1-=VuGfMzYutw-6
zi&?4=%Mv{A%{7C*Ga?<=Oh46Kit{ZuEHrw2ESnJ83PsJbYx#>fT!}3d#At_^Jk1_%tBd)l!o3*%)ZPd?ZYkJ>%%0L}wP2K2u
zEBVl^yd{M%XfASp&6fP-^op1^N)~?hobC4h6Up>_*gqD706CPhwce$JU
z4R)uPZHahV`E4vMzLqPFEx$^l#PG7HGgTI$zj849nwZuaL-=;Jfj)=uz1q1h$sK`n
zWgF)&NfcdqwwssUlfao;jthGg6H8g%Naj`F1Wxdxp0qB1-)!UQ4feF!IA%OsT-4ZJ
zQ{vzH<)jOdOGbs
zU^OZcgXV=@SzuVmF%0oJbfeasiMv>1X{52#KvU1V&y7rMBOf+SijvfX!&=BB!rmOA
z<#5L8pat4MSvS{(?ia@$&yW()xhebs^PSIEfw|K!ZzF|-;IaaDpS8K;&T?A60zoV9
z(6@tzY9jq_t&p>}!bDn#_$gVsMw*VJZV_-2c?JBZsKA#HG{k(`@qND*>QK8>(Z$q%
zXsf!_yyqKQ+&eklpm_K#L8>K<+)f@XhbNN_2rH1fV+a{X&Lm*J3ZVu@8-MW8o_$be
zolck+fgIl}V_R;qBM(2AL5CF67rx|{cOU4_mhrpuPRai%oW=cC+K`Pzs*{cXs9B>|
zPzC&-6TpQp!Jk-)r`TTZtnlCws!4h}!)~Zj8D{%)99{oAz-n{$=|}*4_f5{gmSY9N
zF=9jEaN2J#pbEo443>g*Rd)UytSNW^)$<#8LzZp&^4f(T$hbKfT;u1323952X}G)ekZfT{u82
zgp+XL*(hr884QWs!3-bz6EO6V=ac-jwWjtui5*wsUqlw?;XEQMZGn%po-$ZgU$L?9J+NLOa%``V>
z$MS`UhI?RS)I(P>Yvpa=@}>sjz6eL!ESYq;cThChk~V>9j&-|8Y?qA~`(eKCmpzCl
z(e1*(+*dx$_NTAOkG;ac@zuc@4TB>f>!}7QQ8PAc&HM|Q(L8=(C}L-bLV%c$dpG9%
zwi1#Zu+WW-&((u{ZJIfhR3s2g%AO1C?rWPpBEFf$9kqo2mmS?JZ{F1tzYsF%^p|1*pM1TILz_VS#UQ`SjM?T!cD&
zvdcG^SN&|B0aU&R^p*A9_LWw1@kv7qEf&@@T9th%2W*UNowW#-=XCb;u&;8J`ZH5)
zw%6w*oE#Ai5;}ub$l;z7~b8q8ih-w6St_g^Wf=xUjZ4QM^
zG>SdEq-lj@?NpvrzdO(>T;tp*kuZk-i9xs`G?GWu>SLVPRD5`<4cx_X7s!KXY;K3^
zz_?5!WTLKUsL)pN(u!BMqwNr=1_c(#y|(@VreYySRNrRCa}siNoI=N+&;|ubQmfVK
zyY}q{OZ9(fU>rW#>#IoH$R`uTxzM~6&08Z6u29eM*7tL-Fg)H`
z>QDJtJW{w^$AKDMkiB~HYN?8GQYU=9jZ6v#%xIk&FYDpZh&b5i6$o+nLhSA{5Z*ZX
zO6)3WJhTx9Vrfgy?7h>$&Ml#^v{$nSV&t-=Srvzr^CTN=U{g>Vu-)D>=d`7Nn%btr
z4y4~20W!B7vcY(_S}-g1AI7ij8lNpLRblRJ9lsF0*_rDcNPdpQ2qsOImrXT51+Y
z8nq>w+0d$AEO+$I_Gcfk4*c!ksQZE+Ifv>iYFA={oNn(^vOvs6ein4jxKb-Zg9^Rp
zPK_}wz2kHxlA8Kd#R6pi&pem?*$yc5caJL6?Vu6`lZfRoxAxIyQldXBIx1jxp+19V
z5XpQ+C>5r;@P;YtEomVkJc*k9A8;MH63r)W{(y~SoUTJwAst~GZ3W}z#ffSx-68pZ
zr2a9DYFd!v^6~4mmnEsg9+jlINF!GDZ=X+1zGf?(?qnSH*=)Wq0v;4uYb_!xHsbOX7pF
zXNz1yD?;829U>E}v$*WgGM?ANID`dMteP~=hcq5!?o>xQ{^}B0P^=HSSeZrh6b%Xw
zBlX=JFRb%E9pPMeJ9)EIHK@CuO21Zo@qLTTZ!Yv@qB;b=d@JXh01qMloR@~;$!eM-
z^4q-7n&VJOkP6@}*U7uKB)u6=yCCN`iF
zOxxYSw*#wt>6k#b*4x8rn~C`-Ufz7u)=}ZcQJ0~ah-!0?43YgHL(jGTY~l}IC~@yWKT7)rcH7K;Uu
zgTUrDnzuW{_)L!nNq%1xDarVJV44nJ@Kv(S=&!A(r>gy1raK+Fe@g{Q=5yi9`FYN8
zHMT1(657dTi05G_mZ*-PC{|iL178rY=~Xr!Hh1YbOgrQZEoKia_C6?k=@!?lWi4-q
zKzY+vD`9tyUY-7RQB~9FZvS2tgKQB(j5ePp_;b!s1O$FL&3Pzij8&gyoQ0NUTWgei
zR)i&Q;q6A@NrLx_hzL>xu)e#YK@~4Y3#&hO6M`%FCc8mc8Y2mDF>!`edchkl`@^TQ
zg{!5vJ=Ys}6t;Dgh=={Qq$-}WU34g-7!l^KJf(eCYqq*oz(zI_R??QRHt
ztIZ)DDboMGg(eE|5Yfz|x9_YUsJE>t`y0q0lDgh6fUjNrQ0mU?A%1`M%8Dt{2`Q-|
zg=fuUS7)E<_tEDbzmGhyr2Zi|cmdC;&T87k_Nzyga{`Hi?-v|5T#H)>a%w^RX0>2v
z(&2VEaj8QcR)>#=^SnX0Asi%f;4l0*jb#nURlNI4_;zIfpo?$g;7}00iZOUsnCFudz~tDr*BjKL+MZqx6~ek1(;f@&&W~
zkl5&@ZH4_;nj@KwewLY+()=q8tj))7;J?2y1gDpytpk;AzN`b34rh*V9PaRg<#wRy
z4y<;DPaDXttCzVCY$d)*1INOsPK?m_Hv$Pe>wa^6Q3c2kACwiE4^!c|UNSCF(}5M6&l&Q-4N*
zUjWmmrp+yIe$C11p{-=3=$dnz@wcy)YV_I%50`$T-Om$ZTj#|Em^++koLS?lE+F#U
z4QcLGqJnwXgjk$ZlOtWGZEtp7@09xI=8&ySsZ_tcZItd$e7xFQ$8{aZiSaQtO-&Zn
zt$&R9pq?iF7R`_w9c-|Ec!Jv7+yrcr$3j~|q$uI`;1s>~gfoo=jrl%1+CiuDH^H;;Tx^jb$&;7t1A<&C^H2GI%gTH1UcLq`qQx}1n)=`Rh$ngmCQO|n(@
z$@q<5n4M!XX_nlN_RI9L<~#!9e%=$wddmb^->#V|Yg$sfUE-%`9-Pe_vz^QA_~m!I
zZ$p5I_JDuVxu<2Jl4tYRIR_2I{2K3@@B&3rV|VL&3=od%>g&WbKD}U$qSS1E9s)|v
zMq1Tt^-$U|hAZ?rTu_{P1UeV2eujEZ8^MFP1ZCV&(qI%zKjNdtNyO@^&`vPiisx`o
zF4WI>4OHLN-`(=#6FLGl_%&WMY}qy}rC9AArtK!-dx%2Cj|D}quJc>R*&(FMKb#>C
z^4ACs6qL8WHK>7f8nJM(JZ4=nK65YE^n$?UyTfZrlEJdb(}o(KkDB#&lIc`ZUz??3tx3;dI(N_nmqq4ni-sS$jaf4
zu%nq>4~zoiW!4H_?w_Ci2oXE<;o)ws^F4=a_xUQLz4XLK%)$mEGbqE1e1n9WWICw=
zX}uzSqV&bjseaLWFY8Pi@4Dz}`>|rGf9o&xnd3tdXhcRB$__;m%yb|y)HWE35|I4K
zYKi5+UuNoO`d|;%h(^i@a#cADyc6(3`ohwh=($9g3hZRx*UXJ(ibo1c&3<(lQr>*3
zyIGzFZwT_>m%s(}@HK5EX&%Osj)v&)m#6f(>F1BV@aju+w_G@JQpBEE<%VD09*z$`
z=WJ&=zQ(qsfFFY@h+%4x{(g__!Y|h6teJ&HT&>I5OOsCb%Ye^O^C8lCF#86Xw`5+*
zqc7vw@}5lN(&5-Io%ropsn%@=5|vslII#q~5_+=TcpdWrDx-Om5KTAYRP8=!fCqu_
z{Ao_17c~D>K(G1(eZVig$vOy5gdJnmUUJ7XKE|P2RED`GpN(i{6GBbduCfZsjAPmu
zHiD$0HyMxWW$o@O^vuDH5o$Fr4%xYeQUp7NS}~y$Op|x*(1$M6)WlQe8O}USb4LQh
zh5V>Qe&J2Dxt7bfy1Gk!yDH2YIcYHpn;UoIE^)l`naxOxc>~{~k9t?6_Sm+k#q{B|
zwfLta<8rOPd@;N>U7-`SDVQCN9O!v*h71e&ND;!@I<46qFAd&SG<>-sxBQ`J=%EkP
ztE$;SdHl&FXQXB6l!XDbb9iupB%<%geGKl$r6G&c6I^Ve(!DS#EIieR?+0fX$}ib5
z$~$cX7kaA9w{JepTwBY0^_lSGS2^F$=vek~Po(gqI`_nEGFqKSszDI;JAC3#@4PY3pHZ{OV?O}3EV5=mR4
zriV6}c<84YlCS*RumH^X9H6)bK&x1<+62u%59TK(-Wn90Wq&ir;Nyi6EYy3ibiMHF
z5|Qy8dxcEIV1()@^0Y}35uR)sawkGlL5m-42Xk7{iR(~Ge~^04tkD0xuY?#;@MoNw
zK;b`|PXYOIEf2Di@l^ecN1jsBtM|e(qmmp^?)`wU@U3}c-*um%|2nO8*2Ik{7L8AC
zWsTR&zZviJpG>3_i3ttGtJVDS*A{j|$UqJ1o`LjE;-4y;tU
z4nnAg^e3l6x$cPG8x`i5cc;V26A)O$D$)@G-!;t{ux&d{ma6{(E;E+t@eme!tXO85
zQI@g#*ic~}!P9Lb9R5u>^*#9rM0RB2W-uwf=Emb!Q+IxE!R2kst=3Hc3aVG{J(U6!U*#xv+QrLhA`y9$o{Mowu8T!U!Q0vA
z#BA6{wFJc8(!+ypXh1t_i(DKVWrQCG@>^Wh;`iof%)KI3BIVdZh-;LMT
z3ck-b%ojuD)?0qC6pexRRHVWNPA1S2(J&s4upZbz&HGZxw(N=xnGfP*1B)Kge2@_T
z`y9cy9ImRkH%!+xb^!sz`9#A}*@bxj*X|68`l@*u&vG(nd{><5d>13UF0qkn#0
z*;M68?n++BbiDw(BdXUUG{ywBQ|A_XwcM8On-c%MXuDL~9n!xPVl+)sxUX0k%)5bd
zyBqc@ia+&0(&pz$OJYX->1s6RkJw5^Bud3>d)TS;Ml{K`!Nq>mH6M$k9>G>YRz7W9
zuF#`s)j-2aks=fN%o>Qnxz?ZLW`~kk&n5C!AIv;w9KTgz^TICP4t377GtCjDD#zT7
z{#nWR4wI=^twMAT%>5Hza;K!CsCS`*s64?}(lm}F9?n8wn|?WKztnDPP}k*9y*PKL
zuFdy~-N|znL_jTqkYM7`PZVf8T##=&{@$F)X%i%HD6IaicFZPY3pd6Qxk4$B$L=Ld
z5(5bv^*92JNynX00sBD%`QL9pjgF&h^9|`O1@D3&@UV=Lli|P~>9_vXs79S@9G{_=
zZkBEWYI-u^eFLia4|XO&nYu0uwl}27oGe~&teY&((TXq!^k<9X?9vTZj@b2tw3|&P
zDx9FOq8NhbZZ0oPT}{GJ7NO6#+4#}maeIJ%hFob_vsMHF5U-WIFzhHulI2K4u-l4_
zL3R2l?_MKoV
zTfP4VkQpdTRs88Puu|F_pm~P8Zi*}|J`mJ=+Sgx7E7qKPPPAl?{=<=O&oz5~Gka*U
z*@Kh-;lhRYof4BBx04y?*caH&N%Kmak($@ki#K~rqavf~70Pl|T2|WBYk?xKj@muq8yQ>~p{J`$j(E+I
zbWWjATKJh0>~pGPBprvxz5t0?Fj6)8y2<{*p^O{tFp9+q_rUCpn0
zMY032XIG*a(&?KSg6W&$2@Sr4KfE|jLVUdEKX(c-A0R8XNY!jNOpBCy&YfC8G#&ck
z67<*H>0CoGUVs`;O?dOikPk5)R^ib1_WCjP#juuzcyZr2J*sazJzwZ=Oq9#;-i{=%
zmABaVznf$g(pj`is>qDUG)i_XP#vvc9nA3?R9s?7BQ2=Rg%v&uJBn~jSpBqm-e%+h
ze;aAKDNx~C?mIblQ@f2bv;B^^eKgeO@2*20NpSHs^9ao#Zrkf3>m=&L`
z;n}ZL|86^7lG-DEWPV8NvGh)#i&y|6tx#|9fE|K7#Z7%j!uN%6;ZC3`eWdK_Z$~6Gui=_JL3{PR;Ob6*T6{
z0qFDg)d6gpFw5f6Lc;==*Vcaf=Tzp1q41jMK3{NmhtMV+&*9z@*W$Ft`7I!Kdd48j
z@N@#XfX{*9CyxC;W`{gwl6E9a1f1m15%p%-V|KR|==4?`2QQ0#R5WzBVvDz`)OZ`<
zJ!8JsmDd%MB3@B!^x-8Fp!HWI`Juf|TyPNpd
zi@Pj$i>ONKNDERVK{_uLN)K#WWOVbar531?vGc6P*^)APb9#9pyS#iP!Tu)k2~CX;
zs5eba?jKeS2C-|$Gr_nl^HzC&4u50bEA~)CiZ(P^mAF5$blsn#*WfnhT57~gep!H$
zgK78>(9anqzZ#eYrZnyPgftfFHhC}8FIn&<$DbbRfOnIci4cYW>+-Mu?A6Uw2((jR=U
zL04M$XfUAa22!uYQ_)#Xxj4W4>{ORx2+;CfW@htdy%K2)96(f?Hg#g}OFCq2`yy;k3+$1Svm
z(QuT08yWGOex+HH7L)Ack89D?)L*)*pcSEcma~K2Bd;?tBF=;V?ir6ypGk?XcP5zX
z(jy|MP)pk+%g(m6fV8<--J6{poz!CD59Rv>7$2WAC~2g>n%`O35Z0m`;cWv#aQeKA
zn0oQ9;IrnPUhch%*%X#W5pmoj1yAA&Ki%0+4L?p*Y`$KG`t$G~_PT_p)zyPa;)k}O
zK!hxXPO`bMK7pT%{O!L1zLa8D=pe?VCMznJLnChHlIEhBm$wl^Nv`HrpCp7IJC+!>-mJh7tlsUu4%KD50R*o57k}KJ
z9B{L6hB~tT!^Xy9CoQ=ogi?8Jt8;hAq=}y1B^2si#S@i*hfY>rpkexqiioR-aoXXx
z5Z1S|N>6#yj@bHDA{G#=im7}A&8aMJAcLkXCEwctphvlI(X}wA
zU>{pGtsp#*xGoWOK)tF;tEQ8h%Je{)Pxy2*!qtGLTp|HqA;s`uC5PVGg`LAkqCSXO
zGuyE#D4WE8`$K&BB)E00tjRa6yP)bd>be#~n1zUG^aD4cNO5}!*p)-CQnTh1IhJKs
zi=No?@#Cbw4FF!1lHb@{llNT=a?r=d*Gn#}GJ`LwRrFT&k=(ahG{uc~zE@tt-9~H0
zIlgpMD0wwe&+0%o$=V?bdYjuPT@v$9Zo{d~%*HV&+e1iyQIq3T8Oe#%#w+7Ewo02l
zD?mchk0Dj0I!+0t1)gjDn}-m)4|UK~gZa3vVjTL~Az%XBQpl(4Gzc3~bzTN!(*eldc)nx*CuD
zuc~=1b|7{^>M~_;dk#>F*5F4V`hcPzk47@p6e)npq~(&8Q9}|5yIggaFn#me!iU
zV8o-EXz4heTEc1VbB}njpY-OR@>-zZ?zm{&otPmQ)y2rM40IDg41x4<^v;6ot`|0h
zKIgwf9r`~8jU_0w7M(1q-@TKor|n=akoK4t%89NS_$;(3V|5-0!(Sd9t~js&3SwjBXZQ-7G4ax#1aZeST7Drobe{@AXnQp?KY$
z`6S0E`FaGy-Nu;h+F!7^hM1bsC%~4jMNXMSFlSLV_X)eW)`u&`*L$eKoeTZkB_-U;
z>17za7qXGO4CckR6DukT?6pce=wRp*3qxqS|A%p=&a|{;j#|7T>>gydR@MH#>H-|8
zudQcy|Lef*D(Lw@VLT^V+6=*ZrMJ>p9B(#lE5FIE`_-a9_Ck=yPSEuSa)BRQbXq~a
zuBtw^n4-HIiAUb*Km!12nZjL;65B13_K-SBbQ(e;@$-Z5*8lO+HnjOBFO}H2F8Q0$
zbB$C4JTTWce7Gj}BmB22?~}^O+7^e9A^d$?OH|QGhP1s#*^~E{Z;K6@zBtGg=6Yai
zek3oDzVNDYD)}|nnDdxt71Nhq9Q%Ou45z8iVkP9>!Wo!{D`@;%*uIXZu#8i7mSX@I
zD-Xk0=X~dQsjqn2&2Ff$@jtf-xW+yP2&75c!==S+q_}z6z{t1Vil-0$8=Wf29{Aq>8zqByW
zAO3fUXtj%8%{zDg8~3WK&Y1Fl-v(YX$)X(Qf1X3@|A)b(yfiBLA1C_vegA&oNdNOm
zli&Yuf9;dk>$*2*sA=EZw1j-1;H161y+4PBUIoxLtyk&&nX31qFw;+;KAoJeYqD*F
zyHZH#{uIgMN&t=eIX#_O<3&wB$R?b0X$a(03VqCiBI->Vv9S;7C9;?iE)CwXY
zHq~LryU?u$o9;%u{bp*2(YoGA63P8Lz-aI7e2pF(bDQxI*W%&F#Jzv00I
z$EY=lu}AP$KR{o~+1sg(^h=p7RWNr|9%tIVRgkjRDr`;n<>|w}vsLUYO-bwL#G`e?
zey9H+q>_w}N0a`8?qVSK_K*ndF`77k-0k&v3o9a&W_WmDN?W
zW>^DK?e^q=V*eon0UIq>n|WATTJS05-*hsB77(#;N!<7<{$tH8X*z3ay9`hp}7>`u=SK*1JslhY*7ewk6Yez
zAUcu)C0RBFcnl*qVz@o;3h5vRI==3&j_kpS($e&pTshn~AR)*gt>%!fOIp=QNLtp2
zzPutUW~)31CamA~oNm}R4?i$3o}utc!FD;!d;^(^w;(@d_JzkJ${w92rUQ`gr}8#R
zp}pOqDC5-k_OwU`I_O)rMZDJxjRE88wt!;z1^jU`orDO(2Zu4tCaADg+d?$gh5_i;qpi=+fR0yO+dkN
z1%SmR+?04p?lIAA>`|bkIe1t7
z|J@K2c=?YUz$gbQ;WGVvN4qNQyM`$Q7u_$euTvBz6ubDw@P2^~>i*w!O*b&+x=hpg
zGq+*KAEaQ{RmrD&jZdS_3}#=9>up)1^t-L|_9@IksYRtAqy`4?+J3>d>k@~{pUuP4
z5W3|H!5IHvYCyKKUH$Iq0GcmU;`=*U@o*IL=k1OvfYB7<4H&myI%hI?5M&t28dGHr
zCo2IghTr(NEhm)9?%iWqXx1*Fn}RM-DD3?{@$$EybV6@x=)rRmhf>w5MI`=>#RrVQ
zm0e=wli}bVrGqLtsQxlIWL5LTgb*9pyWXtVJR*YbXnMQJzGB?jrCU96G%hudXWnwL3cW0c-q~xBwd`{z-P$xZyk0KbUQI_?}Jrm;q2~
zfIigr0d4*qwG$$lCinDy59}s`7K5%bTbys{9FZm)=*vogn=jF89cLx31~(omZrKx6
zc?GT=vTeBb|Leq6qyIA2p@|(@UmF-G8Kh;?i($P40Wsgj0#&6YWycE6zU6--~*h-*R2eSnQlM-sf=?h7
z}FXox+s*O12l(4=5$+SlC%#Ro;|K9%}q
zw4M^T!e{?95@$lUmRzq|H7z&q59=LIU$ZFEY8!3?D5Dt1#DL1yl)+lvOidn?zTW*F
zu7zftQoZNzFJWPoVUu+L9JBoA<|f0}#&)Hk3!^9NR=+T^A8OA@msx#Ib=tO5-IpTv
zRd)C)q^y8`^~hy=cQQ1hf~3mxY(XI_d8f%^&yON#Orw0KE5c5a#(s>|$l@RROx}ls
zyL~V_?Y@&!smVdi8#s%(28Xs0sdYm<1Sda+2fBub>*kZ+;r2PhG4-?XkIzUqzT%|E
z64Msm!g_al!PquM=3r#8(dtL}nV^DZcf7#V*T!x|Oyw1jV7}LQ?5V_ua>1GfMX-0b
zTl&*vSO**xP<`X1M0MdE`_wnNYB2H)JY^@YRjM`H4^_9VvbNNKgYDC8;0_TB_y-PF
zD<1(`M#k2*5vUZB$Lbi^uPp}jRLc4P`RU7X6%yzog+aFrxQdUxx41-5C_CJj9h*xj
zJ6y-oiz#kmHNn;*79gAr4}0^q!p;p*+q5=s%O)@HtXfX2K5P9Gb4&
zJ_`jtae-f0bMpwX!Aqe<#~cUE23hf!nUTtqgInyMV`tIvhl)r-yx(U&0E=C!NWuHK
zS4!YHP<3L^2|8=P1^AM6!2;={kL2Ps(oKL6?}6=T{Jz6=Xp0LO{#7;Jwr^`~zA8UC
z;JcvIQDs4kriXlLcu-mDerxJ$rEboUJkRXEc?yBji|OWfnEqHX`3%V@U5`+Apyd^M
zYwsQ3Ax`_!;(?RwMQVFS1(}HN?H8#lWrljx9!MTeI`AXaUa6PRpQY~S0fzl0H>!Nz7s-wp+GPO
z?$9Xl#%Y9)mLXU=ZYo=SuFT@W^x<3SEo4&%E!6_`rn%XhjJ
zJKJv(GApjqZmweb&OM#%meFcj2}Sz+>f#4)z1Vv%oN2qyuu;r-&GlCQ`|m$)q>N>s
z6VNDiVeX$(_f+_jy*6ikwIN~dJ=>UmT`GK@XZdLA3c{nb`P1n!dQA}nFk14NvHKVL%kToU
zNcNK~Vt=Th+O^xrRcHdvnP%g2wAyRzOSwwE8BOosGY>BVcH)uy+ov>dLxu=jTEk9$
z^ZB-CZ5{-IeLH-5Y)6Auf^kDatwJuqF5;*cPI%RCt|3GHA2en6thsvej3uv_rkI~*
zMUDNaZx$(xLG2;n^Ehi~XbhpIrQnbb1%xvAAjhO7V;JjLnWZz^Tu|Zc#7u*^3eg}O
z@D#B`p0UM++N6zq%<7mzc*7lECO`MeX8Lo%9&T+T-tOM2aXD7jQ}Ju_p_!OzkF%QnNQ*`G<-`9r?$VRmaLATar1pfxy$tOU&qwC`_P*=@7
z>%Hu}C{{X34yKBaJbub`@o{5Kq>~9RE7!O(
zpq~3}f3@RFrgo6#>glfCfJ_l^N2wJb|#qWb-c_k&7LzH?^I?jUukEo
zj-X)cj=|~16YZfzrw*^u&p^b29K7FpJoe7&(<{MRj5P=E`8x4Nzgt(vb6`g9&|aPZ
z(`sE7eVoNOTwU%08hyw5-kQQPRAi0G6!no%#mNsjsgq`pdgc4F$~O;6q!J|-vKU#{zgf|bDUXji#;ud=PM!Qc3ObcyOHOr@%MW@
zY+E056k54HJ9#Jb$DMgF*&AT;ybCV4?cvcj@?-{6ryT?``^Nd`nK8J|^%kc3#^eCl
z3F6CCd)wcBf+JO@%V}DHfEv<*#$Vi~{Z`Tt1u)CliZX94g*9zVVCqL#_3JNZYZy
zZWeT7nV4|WUhq^a;rCWbbVl8v$6y=q2hU8Xt982%^(3LlJCMFHo|DwQ!;u`)Gn;E1
z?u`2*ANM73QXAOJ+|C+V9y)7w5CAw#<9hb8_tN}cwmN{U0Y!32?On-)!k9u~G2i<~
z`vS!7FIMMx=&2N*tcZw3OazoYcZW=&Wzco(5s$)&2ggA~fNV`e>EY!^hid>ql=!r@
z_jHa9B8s`vz5`%7fhwG6zP0A%cW%BJ<6yi1P7gX!g(_%sp+-5_)|tU9UJwn=a<0X%
z$r9o=mNJ1HTW_zb@qTY@CbQHw-=Kdh8a$_(nF?h@@+ucT%iesG9klSGSwE!*qmYda
zTEJb!@uDvR!9jp${#}oc|5CE~@|rXuJ}2E8EjW~~y3dUk!Hx7wwPmJu`5!?}W67@w
z+5WqJh#Wd=9gAO-#&}o8uP03OndD5v=EJD-H@*_T$%FznvWShujbE!H|FZJ>j`@LX
zxZ?xgi9CxJ!F77U_cErRAVKguL%*cLkNbl`=7d+eP@J#z5G@i?@*Mkxcg(Ep&FT95
zs!<9UCFB??=(5GoKL+7+CUq7W|&8*3Dnv?@s)
zG=_dwO7wfRdc>>N9;wamdHng)U>DU9LXPe#ifCEB8+KB}wzPcAdQtK1BdqAp|<
zyXDf;&K_+odEtIev4jNVmzbt0YRMa%EA7$vG&dRDzU2wZPp_Q~)u
zuJ0tP*EpP|VgBdxNxZjEA*WNL%*quan&8MUGs(@NCh>;kQTiIu`o!q9zqH-`4+0(^
z+Aaq9sZW%@}VIbru$8lP7W?}4@&zxWyryBF@8
zY5t(4XQy-SX8#l0ZhT#!k?f|7CMurTW)T$!LIEns8}2*cLwrG%dC=XGt%zR{!TU=d3NLcmH$7yY
zVm0%2_sTTg=OjFLOv8Pvt2Kh1#2h4Eel+ao=swEe&;J%)x
z^$l>gVejwGSrVyZ-Y=j0w$5>7hVkYx!1@4$5PFj6-vb4R(6ylb>tm@!)1MAb)CgE>
zhhUm)mGLKaGKJmO*7JDX*&|(cEaq893^=DSGVQ?J=G5MH(+cxORJ?Mkd>Qt^FkdNS
z?`C22OTV|+clb)OMF6z2s<|P1*u1&*)21tL+oy`MwBd%@3F&yE*3|f=6X}xct|B`NH@K>80wSYFkA4b$)C)slEtZ0R
zaiYc0mtgHx4NrQ5+pCnSa>lPA&+_?5Ci?x88CsJ7yEGzF4^`)duv=Whg`AdYLw+nb
zz`Jh~_XWmA6}BAjunc;g#2!|bSr*d#tDk03Zp!SMdjO@C;n$Moq`vDzmZ8nzdgjRT
zHgb`R-$DA7E*IDbGY4I|?IeN1U4Hi1F23U?ts55XMY(sX+zE)$#zwI2J*Y)Dk>!)U
zcVeWem;u-L%g~J2GS%)VA*DsY^#oa27YB<9i|BvQ}VirKhOPj#O-s7Y$DP4+}Q_6fnH7$pJONsCJH?U!;aWj_M<
zb(|{N_ZSCFeC}BEMtCuv%24UsR!Ut~c+4Wb`S4C?(M9U$9>%`jpI;a+-W0t{U=Xsn
zH1*A=qpos;M-x%Qr~uq6+e_t0v7~iwIb6|A>GA2TV_SGe
z6TPx;s#5f&V7M@-{QgP$Mg!0p>^D{S)Bow~IdZ3ry)Sz@#n`l5%(p?DZicz#&*lE*EqBKmN|b87NR+PW-0*%PNp4b6!pQcxh!+Vy9PO)
zOwwChfT!Aby71kxbb@ym)9ll&Gq5&Uk#16i^b(6G;a?qXLhINocb6Wz(-CsuA4`(u
ze);lM@fr(!_m3bBw%n(No$V}bK)X8~$a~B?pPf>N{0MB8+QfY&C}dlmV-gBm0-DJS
z&ew>#x!rZWEscsg6L;WkxhJ>r>>EqdqVTlnjd=^lOXp9Tf>48lx#fzR6Naa!WoG)k
z{jD#G3x)m>4+&4Jle4N?)dDhzNS?e3G)@n_4uD?ejwB3mNSKBycsGM;|
z9d>0z6!6v9PJ|}^0MuGhl{GRGlMrucs_T@_8e+GiSl?_$X1Bk#g#*;<``tU{*xT&d
zP+Pvrj&7=-zgNrGP~Gq`tV2X4ZUO2_Tm5y1u7?of7ilx3SE5JJ;pF`#asAg?m*=nm
zsw{ZgL!ohg{oQJ~^7P+15DCE2?N$~NaSjiSm*VepU_2tbXXY4E#=8HOB+l^&+Y7^(
ztnni?OQ*3a4ntS&4H;>~HYvF0os$^zKC<_VRuuyBFg40M?F|FTR~?o7&Q|KS$7nhV
zgEF(UjwX(&_k~jnc@m|UG(8ukFkl{?Ty@4xmHuZHiVEh>#~+;3=h>R2$MU#VjZ=TNCP$s6$Q0J&ASOhaB$vO_MN|T
z#P45oWht%l-zm}8lyPF|X?iu|Kw{+MR<5g^t4F
zk-g5DN%_#vu_`owZ!G4EVh+mLp}@n2Qe4hv!_8LS29i<>XBSu6)jOwI%nB$!rtDDZ
zdZFg+_He38U@JH89y>6f!Z7D-AB);@DM-_mk`s=4}X_#JRWpaRN
zrF|T~j(jygyy4TL&E61pDmgZ+j^e03?!f742BxFdv=bpBjQeWmqR6g#+PUvyu?Jnk
ziTolj@gwBMOaT}d&TN}g*D&vb>9*VT
z2Fqj1yM!IxUE!}>Z8s+V=GziwCxhHUC%f-E;shI?0yUic=HmC2p_G+zHj3W4RT25N
zGp|bsIKT0$)s1O6>3=?5$cS5U=Nh6qr`m~JdvMGQ(e&qcIS*}69ge$V&3Y{tLZRyX_vBAf{|TLVgMI?!^h6%mqA@o
zWSCWCmUFzBFjGrdT;Q(g6%$4n8Ty9G6U0}zmOXQnF8HJE;~qBuhjtbmDLg%uO_jgy
z!>T(uJ1%866}pV)p0kTk5cDX@5F(ht>P12urUj}~L>ug-q4zhYhW4-;bp>mzyjdX>
zgr)ohu}&fy)%+6oQVZ!~iyrQnB9HE4RA-R#IO57+5%}wxCu%5CxH$Qcbn!@skWqcl
z>vSn%F7dAXcA&y2>*s>&I54_!!LKT$wWT&CiC`{_4+qU?e)Gx6TaCo?Ash{=?P0RZ
zWd(qerWk>FVv#Nx$7pW?_ibtSBEJOQM&=LG5}G{El8e3tuM}}Ru@7D>0h?H)U@o$j
zo_??R9mPRo@F5s_L^dAzY+o~|h;t1Tm=^NkHzq*AgvEF6=!lxILBN~Or+TV>TML$`
zH1af4A>QuS=@O13yHE4FVC8SMl@%@*DVvapwX`@A
z3d{GZ({k~#46m*t3hDMOaAP+e(ul`Ccb|hK-bR{LIa3`uL^2Qc@ZkY73eoEhZ$3la
zdg5*oo8gMqx}mJ3S|w>}xw7ZtOc3mwWLR7pKgIZ-u;JbOR22b~{r#NAUB*0VDWU+v
zXB-$%B@}w(eF<%2!&o4lgLwzyHKuX|Y~y0Ekx7~M38z{xmE$i{-DfJzux17LIrm8V
zZ`T17v%f!|#1o916%jku&^`%~;^8GKjiV`ojLh1EQ@kANYIH_Y)
zaWwLQ$M25BzuWvBoBBx^OWcmF>kjQDo>v0hxPWq?8^@9$!h%5?C|&vc{%IfHr_R$nPFG9w
zhgLFWY*2C0u5yxb`>!IIrF>v3o2vBcB&^z(Wh`OQT>B(_RT)ovuqLL6*u
z_-1$5S=1%sW4(F7K0yWrA{3+9VCZF456of;f+F_Xcw7u%x&u|4R5#01$MA4a$1vE`ToCd5#%>1D1}LWDmL(=
zZG5PP=3*#K$8*82xY$)O9SrnXIQrMzp?M1%7EGcYB4_!^Yg@;jox-#uzod
z5{++jQ^$RvwZh%BPznwWdvlc!K@b(Fw6(qQ2L8y35Hb}dxG4j=+Ir+=tkp3fGPGco
z;`Xae_p6_6S`Q);yV)N!RCgW&&9Sb-U>Wrn@2Yeu`nwo82{LROBVBtC1mmJIOXP8)
zEMNV`6Gns4R~wsk?|oaj=WIb(=(R-!r9eD%Qtp|;=z{r#;wS8-&jWM)<{_N(V?!u0
zQ6Hx{4^=9IJ%CO04Kwokr^kXqi}Rt)+G&AZ
zsJ9{Uuy-?Y?kiTMljj=@E7wbPo4LBVw%?nJh4wQYG8H+W@JA)?C*Na+5>FlyY`=45
z?=dvz_95s|JvGVaCw%sb
zY*xn>4e{!eCbhAd_X=f8xzUP#7J#aRU`w3w)zH?$yjAMt8nTi~i+6b>_neADPW(%|kOV}-K2g<2OAvt;;f&c
zaSqkNb{iXPUTta4*qwn2(?0F1AO1S~M}PfXzIT20R39Fp3Nd${Q=5@kdwkGxxt@G-
zv)bfzFo#vVVa
zC~kq>Z@DwK&RlA>m(|5Kj!>jiZ|j}DbTE=bu=
z^NQLfui?)#gZ-l*qxB9g%>f>hUH(_`?scQKUFH30^Zv2G9}(N(sdq
zU}z^Uiim~}fYy!sug-A|-Zq%iO1Ev&uOh
z1G}xmxRLxrHoiPg#jp*ZHIR9OYUMANc)L}v0q$);pzJT?GLLUvAIyFs9Ox3TLjnPP
zDoSVHLMw5%eYQb|U=hJ@9hSdJkdcQV;=uv?*7}frKvztkz`Kh`YZVp+yE^Kjx^1<|
zvi^zu@C3qH!hQNg?btbDRT$_{g8(=XvtR4Wp^REM^JGDl(pDd5-!4uHy^uEM26m6b
zK#6#obBUwviOV+|h7JA$n0IB2?>ZcNe~Th6jx*zmNoOSv7xcY!
zqv^D{$HMK0#`BZHy37u`utDH(xrJXwl?3
zqyM&2e4THk-*4)@NBY$nNBUR<144Tpe)y%0F1}5S3(d2uuPG5XN_OAH7FG883@Z+R#ULD7^y}SC_
zrgknV9|w#UXM0;FJ0z=!x{H83or-{In$K{
zJ@Iyy7FohUea^Je=`CjUjd`*U`fc_oV>z1~v{;xsKl?VO-FL;94%blOd|`ynPGNES
z@as?<{8zT!DI;2zN*pk>$3)|E;}=aV;V!Vs^7;;I7R=4cZZQ?mss2+jn37vrM=GMZ
z;$gh_hCx~MS^7v1`oykHeA~I}sUe8dSlN%@nj>0qQza$!7c9&r*hZqfo#J2IMfT+a
zb(M;VUgP><98z|c-<}V$Dc>&F{^Bac^-r_zLAmZ;9r3FaduSdrKZn+E{K#Uava$TdK%Iz}e&wJlnIm<<}B
zfhFxIL9JC}nKK9=VfP;SQdam}eFYBnyJMpE_@B6SVN>>o^sCi4Xa(f
zW;*u3yKk`8;Z$nro|tQlv@@f6$MGJ)uL--7
zYOq2R|0r`%N&%nwPBk_h2xQhZk(Uk+SU7SuxFS
z>#ojx@;2UXtqqQ4YsT}fB#-T1?{pG4f9Q1z52FT-`sTf@Q^0i5kkUAeamyo>BJhO+Hs5K)mRGijI*>Ow9a_i58^(6-5`QBadwZ`uJJ(;c~2sLkS^HIAXDDFW6iatuT05#
zn2JbOv#!4R^FrTEYRdaF#7W}(e0%S&81phs|0mBz_5#?nQTgqK1I8BF08Cijk)u{Y
zutqaUf&IynLvbRDQXTm?enk{;pG4J=ZC~h-eK*tgvO|JR7UL~(NXt&T%^>iWIyrvV>&k5nQvvMBdi^K;HNhcfCr&P!~?%6b^nsb)P
zX(+p6WbRr(hwJ)``RpP&e(<=Y63^;C>0w3uOb^E606N_`S4r@S@3j`@GE#X%H%rsa
zyA}P;ekzQlD~4?-(BScrcFJ)%^Uxk&vu?w>qv2>;myHsIw+h+dj<`49IoZXAFsHBw
zYmd2lCDE`q2AnJ!F
zBLw457XkEC9jMDHa!cmwmsw=5vU7)r!k;D4y}H=~u%Ki6`Sb3douV@tJA
z7p-u>ro?WnJMTREZnp+)Yr?7XmFf~f{PcdV3Y0mw5eUn1fGbsUSy>z}7Q1Ks%V|-P
z($+>HdB8V)kA;^hy5Sk(rASKW`B-k3SS(M^uumkdFUE*F`ezymhJrJugWvsUzYeNC
zHyIu8|4bSl9a_3rOX}i$Pt`L(J~z$>*2AMHRX5XYC}jh*lbGXs>bji<{QOp)rS;CL
z>?UK)<;Q+Z)5&!
z4fEZ!*7e-7_WJdjaFvn{+gcth
zxC&V7=ISg~G$DmwuYk}s8WYw!E5TjfgfGFx-@X4FFEup=^K>CAGn0ZuOsEJk?O|uO
z09!Gw)7VA*8~&IyVx>7irK@cx93F1-UW<)+zmA0Hg{>qG;RYhf&R#Aexh81LDNSIi
z=f*;`TRowXr+^T!
z01ewrYhsM(Jp=lt;s`8aSnUQW&<7v7aA;4-y9&?$h5b_7NS(K)G9nJCky4RZ?c6Nm
zM-HnTCa-J)DF)A&2ZDO9cKO)zvgGxF^+9;a#ph$5~8FOe8Ro
zL<&8eUYY38sb=%VF4}+|Ypf0(2L*chhRkhFrlD}SUSxDi^QsMS;bcTjA=?mBqi*5*
zSrRqV30(z?)z$1np;&bn2eq1A!i@ZsD;t_~dOy!(ZjYA|BdpmCso<=UTXJTPPHRie
z^~1F>dtM?{=MEZEA&x@b+yeU%ea+!E7IS8t=S2OHeYRI`nKZY&p(?gotm4mh08Wj_
z*ai!8$ycGw1C@U6urE<-Pe-~ctlNU|`QS5#z7P|EYwJnZqBPf@CTW=PsfPN2(~FX#
zYTF&JmK;xc=tS9>NbRZB96SLp061_rg&;mo1-NyI!0I*XL;(bnyn>PPG_eU6BEti(
zFMdQbGNIOw9r4q=>p&O4!n|J`ak~oIv$U}`e;{%dcJJ3xjFl`Ln
zG4DF>ZnA>;=BX49@4#->ZOvF)f@M{U^Q16qJfeZx>y#c4_s+{j&x|9rHT%E-7W+t@
zSG?8cA&h3uxyN*1WAi}}BabZI)RD0u)veR2*ECEM8Gr|_6!
zveXVBn#Qm7h}hV8nbQ9M
zDIcptr=&jgo`tw|2Y-fM=}i9EK}NnFO1l_QLeK1C{5-3QBZb|3yJmEg<0CTWr}BF)
ziBA(o5q7V#K|-QB;k?|4S=1OZYTi`|v-{oSB+^|>B_Q}G^svh(-U`lr_e_tzQ
z0pzW@@ee^=1?Z~*i|Hk_kJ+^7i&!J$u&&E3uf_xa&XI!yZLm_Wmj4Z8ZoT7&2rpb;
zp19!m2{Q`1uv@ya?tgp#jX~|L+Xg7~uCMb`pyS{BDE%4m&R7D|h2mND{*@&^y$TlY
z^6FjX{&PF_?xX+x<^T7>KuzcVFWZ9vb_Mm^pEpPTEdlE1-T;o&&U7@5E(@aWX(eo?PIwB{`3+_BN!{2;gQ#Wkm|0>=&$hYD_wUuC*IIoUAP^Ww-h6?x_|1
zScCfkm)q_%*MC+6h~W|dEVqW~*THU3&xM(#sHpgU&jO`^{c7(OmLz<}B8|&&sTD
zif1=Uohmg-qq7-iN|>N_KA}t
zULn8!x3g;MVFjQ&7Fzc2dwv`ylLZ<<8F$}f&-g+wRd4YNU&iQ}FciT?RX5#^3;WG;6
znS6yy&Xu8l=d$dfIR22gI3xt
z)O`H=vl6&u0CWPLcwyPEp=A(vb-ERUfkPUd>b0GZ;==s99jKp^DD5Kt2ct88EH;*Y
zeBe?@;p61y<=tqF*>bWNOsX10Ocm+Gkdu?6Y(chh1JeN>h%(9qVcF80`ac38fQsxD
zfVE+hS-PBa83rvO0I?Vk-4;QlGY>;SFRkXV@(^Q{LRI7wTr5
z4N1ugO9QqG&Kg^Q*}*tqYcd9K>s&0~cUhX5nRx*y+_CM+!V@zTb5B@E6d*`l8w(gp
zC>!<*%K}7^c?zH7#b>V-WC1OG2SXlzhRICyKSH8_-ZRC7{X%11m)qI?d_yBHWl_S}
zVY;w-O^HE6^&H!wzg;YO55W`=21?^&T-`OJY7DkBtPFO^vww-{kn}
z0n#Ct-_7+Y(5u|AhCqz`K*?gK01;dXr2{Jf(51#p8upWF%ttfdZ2_~l{@M2bNuu*W
zE)jmP{2C5#Jnmy2N7*?@M^XL;5U--E-jTSJLVOXF+ycR=3O(|*7XUwH1D*o(j<=#E
zasM9Fp_thZ*HLV~uVc!9g7~F(U_6SZ;sppExiGw^W60PnENx0c>zgW&{8ra
zTmdTMHl9gy9k_xlr51q8fhUu}l+5A#)_w$*NIu4zaHT)wS>n^H3fupNsVfz;)Fds1
zQzrpG!|D!jfiYBo=t^H)GWz$x=zOidj8_^-Eyz6w7jwGX*FLYra^zyHY#o{}*L~PW
z(~!Fs{NG<4`YiclI_J=j&pR(FFZ6TOpnA4y$JIgi;;fv2l=zET5ZfACUxzYd9k4rb
z4iN-$ECAkRUt3el>2s<4&7?(%qJW@w<2Xv9b+#+3s_)JP<1UC?Q~mS2(7`cuAHMe~
zKTv$og@scWObf^BUH7TjQ`x5;H>bIxgQe!sxfbMX%V679ID2zq
zZZGn3*e|t3l^;qLSSTOQ@^>;vp~L%hRAz%C_Tbr1)rWGV}$_oT_;ril#VJN*DNj?K67ZZxn#PL@Wn1mm7=!8I_;`puW7!3)cxA
zwZ&udYIES}vjw!r-vb;c`x5Il%6wZ)f2KV@z%RhL%F+hT{$EIOiKk)fXczGYL_
zvmWJl4MPR5^N4Kg`*hjE_j$b_VQl_ilnL8|C>^amfa&V_NRR1_Oaysc%hO7wUvKD?
z4Q5r)GqsFBuJ*K1diHg6g8-*2_!sa|hA3~C6E6vcazP-+pSC|{&qh6H!!&^esC?%U
zvgN!n^gFp_>W$t1BzA#rR1UZgL(!C*czOUg8n%|}@Nz%dN=U*uQyfIfNBM&;
zCQP7X%=wr*lZ`p2m=p064H^7rGb7P0=LU70el1#RJq|K=ToEc_G?g=~xu}>ebAUkK
z{rRYiWTOWKo5)SdDR2e|x0efh>ba3ZNxlR#6XJqg*4DgpNA70NasMh#wNdPk0CCKW#+Mio+7GBVpK~@o54W
zgyVqlocfK({e4up7zuvtji&hg<{_KfO~n%w7x(s<{r2YpO`jY797-o?D
zY=D)ur|@J(AXq`NANekU)gT^1Fg}eDpcVgoGF#pE_+VU3E=XIn4h=)7J*XFs}t!mlSMpMwl
zL4=0hpRsk;_rh8(jAaXBHCj-iJO^tED7zgcHy^90@kiYE!HwlF&-AJo&kU;W+|R}$
zGf_5b*YcG^6s{90mper|f5sxeTRHzyjqg)>Am83I#qq@c_6B)ESdSi$UR0cM6&p5W
z+6pCa*MW8r*7+0@Z)m+aG$CD%8FV|+dXqazBitrID!Ew9NM?I#60el{6TkeH%d~W@6f2(p_Ngh=XZlwA7
zUh?lhSH=g*uo`fk%LaMK{yi+I=fQZg$-z&0)z)x*z3sLed~FyCu0|UO9pUyYAcJ}u
zJ5oUg?(d}0Xrt>lcZZ0(>qC9gERpzT-9~Eifw#oBO{1GNBkLvAVgwysSn?&ot|a98}v-eJ22_@OKG?vrYEmWSB=hS&7!w>zCK{#?qOgx1rHZ==OX5)Zv
zp7zZbny)o%HG~{I_S1>L$y9DIC;EpOQ{00VUo_0@X0)qZ*M3c~?jaO-8Mna8aqGP|
zMx?E1j9ty3|J&(zgVW*y>k-uj}y`K;heqtT`Y>kG|#&zUhj(x>F>utAOqG^FP}wNbK|V#ZjHy
zDS)a|M0o~{pm7J`e$l}xeBHr;GJdxkzQ+5nm+)uZkOq*K@Y@PVU
z1CZUs)3F1p_cjSE%IwJfDBz}MB&5YjQ%s^L-OHg&{c?VvVFURC
zQ65psn{~YLpIRH>>Rr!?HmmDUnb>&ukGKp67h|H?tcrtv1*<-b=|38>u`SY~-2_j+
zRN8Ebw@%Mh@)xJnFtB>V_P91e^R3oq!;jtk&v9PxGQIeGlWzr)JXg+=LyA%VJz-un
zOf*6qQ&B1wlCb?PU34A0zZFtAGz{YLm^ieXLT)!HXY1tj6g-rd+p(2{;VzV(PKzq?
zZFEx9p0gF(=;S+S4e2F#Pr
zag%=>SHKC{|1$X`Us;l|ao)^wy8%pdW>j%EWKmT{spJ5;w*jU1(yY7*q;%(R43p{T
z?23whkH$X%RpIU64&d15<=6g9iI3(9a00qJ36OPJeFo&d4!D3*yFRE7Bhm6j@ADDz6fW)?HRaTIfccRbjv
zp^h+CiJ{8O)0Z71f;9e|ZzWwHRByExbUHMVpsHr&jjf9~KS;)iqA=9m-_*~j^BNsH
zuE&zoijx>S%Q@q1S;RbO7RV)>DjC+OQZIOl1*dw$!J3A2vt}rcTEl(u_9gtN)kIR2
z_Wo(NqE+WY!}|6I!yX%ty^(TS=i8%t@L+QPeS3K?-qHKN#zh0)wFyzDs0YqET!vz$G24yG-f;TJ<(wK1K3-8y$j+X_Pk3(l3H0&xi+_tI_^6qfY$^8to5Ze_!{6U;s<#L$&>i`40@kCCuV>c0CzRN#g
z{wpFPf}ii9KZ6Mi%?2=oPdx`}^f}(s`~4`LIv4LS|4o!@8AOq~uVNE|f{WH%3NBtM
z)dM;>W1})uzY$?&e@H`uP{P;287PIQ7<%RX57bb;BES3>P2=fA&s}ni_40;xdCUCB
z`Ft!(7E*XGFDp&fwuN5oJEheNJt5P2^h1U!d5X<2bwRI%uO)cPG~esh2(8o$jYroQ
z@ejI)<0Pco+smK96+7g_*Bbx6b-|i8oKth@-6Y^oHxHYSuz|$2cOUV0vSMTgmNe{&
z@a#FA(rgvyT7YX(fBj-M$hZCNagJ@(Y1oj1HY`fV>SgW*Ewfg{rNI7%*R)$yY2DUf
zHGdqp)wL*+$P?Et1D^Zscphriu0y|ijz?bJ&*Q(jlJF^4BRoFaJr%^s{ieyzGp1&B
zkCMy77`l<2MZ1A+@KDi&&%_Q9Wm03SyetP7xhlM56H4r((!Hu<%40O{HJ?9*t-GOcqQU0h~h
zynm}B(8C%$5Izo}9k(fMiAQzlxBzDY3UyVw_xP9PQNAp!M&&}m!Hb9m_*ktI%-MVk
zRfC|--JZ4FXyH<0vwKFuQvmZ${)tAEze9z#C=J72R#M!(6BM%RfVS+_?wl*cz=ddQ
zuFhpVh2O>5xZk_wG>3gn?B|}}`HY3j7tihdTreP3OOIKPCv+(0DPU3^%JX4Q&Tj-G
z{ub4VFbDL3@#2b6yw#h7e6`$oL}0aAgdE>B&`ZoFd;4j%Z!*#n{HDxG9;l~Iro4lz
zSCL`;O_BUu!Ql=fJgzp{H6P!VNM+X_#Whe4F4LQAfvf!LMK`}>d>v2iq4=peU=U5L
z;79ku`7iKbU{t58ovpRDD{kX!QdrSgBNHfGPz6lCRT(o|nFP3gPl%at|3ZUW>o#X=n5P&ipVB
zHWpn@JDx(Hn_DB_b_i8X8^LUvg3F!cHmmDy99K3I#@gFY^YpGYXU$1gy0?hrE00f!
zZeaV>`C0hhAR+j*`%bHEIXn)1M0s!hKn~44n?b66+zt!ZH*8yRXxfG~rbE}2*2`n3
z>se~f4cI>6a3?!#8p{LKXURdQ@`Ht5|L;_!rLOlThlMCXUFoUN(_T~?Sor`t@}Vgy
zD&GS87>f@3VR+t~7ktLXxTJ`)9jzOY9pk#dSR~?1nZ5B)0?@g~*TKkGIW!2$1$7_9
z>^IytQ^-A4uV7;m!RD|sqn0v#!P&&xpBeD^1VQsGE=%2`!Z5%nl|C^$ET%QKx?G7#
zv#q+U2r$iE-&U$lD~rA12fUq*cXTMn7croUaSUzjM8zy=do{5
z0tm5c>;fx*jBTs(%@kG+Ei_$a10`F0ZaS+q6KlBK!2J%44M&HiLj$=C6BjFkLe*kz
z`tiR{MN~uUd@sHEQ*vJ@?CI4h+R6a-rwOy8BZ*US3o_=+{nqx=0|M}`n{XexdbATwN!nhpu#mulTPw+Q&
zHeWzczMm+j6mXJE;&=IUg=o1&P((kya>aln0MA#F@dICY0|iCEmTMj4bq0Nu-&L+(
z-((y*fXz*h*;X(-;q^P=T|>wPp=8u9x$hL<>i}gR>frCT&(%h6juTo~T2LCGLpyaq
zThQg+CCUreF$Vwq`Qn8%u0%d=L~BBUQh^@OU*dV@`^MFNiMwbAkbf{WBq6Huw{k-@
zQEx8OZuwsT>PepSfkQ%x#|eyHC;{X~s_FH}m79z}x29x0Wl8LN*!0-^vM4lP;6JT|O8*4j2x#NK1t9!}H1N!eS7isfTq@Ya=mCnn2&O
zceOb)Fe%*7r|I+xPf1bXLmw^9tm|i77gT|m+aGa{o8uctlr?Vs(!?BzUd-c#p^jY{+h-+0
zD@PbNEm7&}x@UYg#z=NYvq&zc#ZNvppvKB&XPH5xjpC956Q!02BzU|}|EVco_QD9@
zICSc-2w$CMOZ;>1om78RjM_IKp~|&1ywYqpQA7
zL@)t})|Fp-rEUnb#H;~SsRAA|&m-aZ4tSoaFiJJ+jDA-6A=4}sNYjT(o0uqr)7+k5
z4}dX%5jl1WWCJ@}MpKfm3~IY>p9|(blsp>TgZ{j2Ny1vjn#oJt75(@jlc$n2%z0R`
zfbbW%+5|aO_G4SVUAfw?H<7?<*B`N7s_^TvVKSs??JM(Z!ud+Cc?29f_cJK+McmaB
zvP9_5SuyjiGMfB+C!4s}ZM&_ivSA{~tJMOzc;)B6!s5^HNGqydo}u+K@AcoeJ-CYC
z@BD7=%&q}0${}}^&v!8OvMk{F^^}*({$kvz^2Z({s|9f8^L(@f?hdN7w|YmdZ~csB
zXAH8?x6BB)$T@bSjxvxF-w#q1l=8BX@zOkGL(Ki-YLPAD|z>{`!TIWa@
z=dHsNqIOi&odWe2s$p1Z!1;|?ZBBJmRu&G}f`YFp;P=c0AXI=nMxE(tc8pSA+lvfK
zi!wF6UsZz~lwtb!dE`^Vhg+Srt`=8V*SQ)gLhd5h_h>>|czy(m0h@bCZ}1V13^
zI|bC;o^m-y4bqhk{as#6m3g86NK7U^pu^W{%-kXeP1MkJo!O=audBy=W^H^
z5SnBkKY9Aud3f}US(K0UU}>~td^!{nTVvJXU3dZyihOSH$RK##TBSgdc4AM|_7Jf*
zS6`|%@Y~36YBnZqcpRhU?O9H-_yIy~CrIB)?-A!BQ`7z=W~x$$+z#~z5x7kn`?nOy
zv639~Vjpjn5~cadPavqam(X!>+Uj9~90Z8`Y(jy97&lfs!O8l^MjpkJ&CszOB*iJR
zOb@lFUQEFJdFqR%c8}#YhQ4#MZI^u_ow=n
zG|$PMEzi_~Ka=?dYApwsNF^+qKn_~`vp~>1&`fUt4u6G$fEu=5t>%s`ai_59N9^?T
z768)@U0egbPp8l)iP6?xS*{Pnuiy3nlrMQ4h5{_JGQ_sM7m
zF3ekY*asus4tPjs+)wCjXC+|gO^OIC^Muo*Bw>wub;3V#7FACoq9#*d(5Q)_M}Bv1?N&a
zE%h?$ls2qPP*uYA;D;c*Dp2x;U?hHRKXDOc3fJJ^N6x&Le7q}7Aie~!c4gI1#ku~y
zyrYulg2*TBvZ`%~==_U9LNNws`nYOm#}WRBX6{z&q(3w<&@
zk*y=evdaBU1IXM2y2hcZ!J9E7PX{KFD9t(7Jw?UX`AdETw}X0WY2KrKhnLn-_x|6UGY0dB|ajb*dIc%cDs0Il~1l#Avi>QLH3
zgM!BZL&Y*ROX`CJ^}*=dA-=3)&PNN85{;Aqv1VRV&u+54OY>r8Lui9vaaV7?O4@?l
zCh(D!e0C#nAWq_Cq3MH{dUP@CUMx*6r??Dl+1GD@Uw`S_c(3fG3eUg;<%O20wPCAnatxQRG$iJ~|QdUbu2`D#Y^G8$BmaA?X3&Ft`4
zqKALpF>nI8chwAS^7vg>D>mwr2~t%!JSHima5r=^m|beT{XEn>(u-sW?p{FLNR~|
zQ6JRi?OW(+&!%%KtM}{_kmBCAzgT`pdv0;!EGnex44{2SGVGwJ|LBxjsSRl-tcDBptMoBMf(zn=`_EMDV
z<&>`8gztQX#^aQ5-sc3NyyOI?e#akbmTrO@?xGG~LR&ACnj95?_!5bnSn)z)p=nd&
zt6P7+9{FQZTJBgLQ`3c9iT?PoIj2h%W4y^n8P_Si6LCyT7cxbjEcChuHd9@lZr^9C
zb_-y8sy)YaODAqU|4RJEfN;EUxOz;&X1k}eQhs|P_WRK~yDpT~h@(ZXPtEZr>@fdC
z^JTWq;<5h|;>xn}_67UOE7g#I|=I@9wfI3;2CK)AP_p
z6tye|5L*+sAF(|rp!HO&c9m}^@l-v21X?<8&=dV7if`|UpBg(QpwTTu>EZb5_q#ps
zo%rL_qOzJSL=;wiA9CrpyZdv8@|lplI?H#K&>w!`(#HmTsumJv@*5TlDS;}cTar`H
znjHFHA66eEu-LeII7&@d`svYs4y2htRVp6uR+&J4HK61jl>BpyE
zEjQP0MO^XUV{<*1*^J85jw_KK5xVm3qd%uKft5-Xvf~EXGaM_>ZZ-dxjY&8L`pT-b&}cwV-DY@
zgb$t>8J6F^eqKE7w>&JAf;siAX>Y7>mWQ$gdE>cvB6I8OTyqn0Q4}FFzUMSvmryxO
zEr)Ohz#jve%vOcd-ITXWh>yU{+*uf2y#c`+{(sLF{3+F!AwmE4
z{-@YSc=Ef10`M^A63d0CMmP%e^LohuIMPKOo1P|fODO}?N#Zx~iheemg~;Xx0BSQU
zBE+)eaQOXtTdM^+512j71K_L48>2%JB(ORYc0m48v?YwJiQWgoVG4yRDrO0McR3x*
zQOJVwoVA#;P*f!KX-knvieo2)?P9A#u|uzM24nuZ3(x(;
z%DNE0H$LhbrdaQ@aICT4FqBT&NT0xFbM_o=LW?x^6pdgsoss45hG`Y~l`@?0ExoT_
zzP3LCKd!&T`yMhK9{*Ws*+XLYKewqp$#fxJ&;D0UyiPYT>z@@NGx?V`6%d$*W}+))ntyv__#%*)IOyUe
z?bgS=k1@A0B3)2}#--dg>#RYBvc{^$qcEbrOV577&bI??{tAz6e@ZV&YjCEw;_{^a
z+pPT@>)fX-`z$>wOT)J3i;fN?|4CeA+sGTB4|mXh5&GY#ifBJ6pXz;uejK15SNk_S
zHczX4tDY@LVjh5c?5}eRXq@rymUYwvUa)o8J&jJofDHQj4}rJ=y~@hj!$`~qPoOrL
z55cNGEPwv_q#9Z_yLuj6?{hse^G7|`!~>$jvV^w*|-Wljfdyhar&BPKbm{N3I4g;LWg|)8Kn-i
zedAH`Y%y=C=hKn|S2~L){TuFf^B-)2N^V;#>~DJO3=e+L4NCr}Ov0aX`|nn6XX&`g
zY_vz=H>;($gGH~Gt6wulLAOl5Hc(p6($3mJSv?1W=7onnYh_bCR;dR(LwjXCc*e9Mt$kyC6
z6;h4;?{NAQhLvd|Z4&!S)X!IswxIfo?LRdfw|eh905mHBEdUlP^8k2ZdJz4DgmouA
z3M=FJ_35(x!p}8DDYc}W=ik3y@+L#he~}E2Rwb>`9$5CvC{0#8+^KS*r#QZXvy$`I
z$S3s*J8V__=l{l^q604ce=+x!VNq`VyIV!2loA1v4v|JBBn1Xgkdkfzkp__*DJ2Ay
z5J^b^>5d@>*o34s3^E7^NRHGH1B1X>kFodvea|`9bv~Xi`(j_t%(K?B;#c>5|LC&X
z2k=2pOpenWXZwHc1O1rMeS8p=+VbP1-3^1!#U~@dQ!i^|2KlDk0-c6X^zN@~KIa+L
z58s;{`u*6@YCp73T}3Ob&bi^V3G!_-gse_s_I`?R4cC~
zOfvaedqFYD?=O&}u2R6-s^m!_Mag7OuFHIeqrtyz`LCgXIEV^(ZzJa@+_EA>>;kw5
zGq%qCcIJLxf^vj6yPug5js$JiE>II&+vK@(}XnUR!fsFMnStf1VN~
ztO$O>_C-kEu6%HB#GkYK&m;7;z(lkzk`19*|M#!nBN!6o)0epfAZGWv{B{%Q-BZW`Ir7E<=yGp*}WC)2VUj4H73k@g7hn_|b!?`D)&_zW%PS;;`=(zsFNcTJ=uGR^B-2pDW|242r
z1%k7!r|q|2^N50fvaJ8Fw_bN2OfHv8U5%cl3U$WBguA$jR$MD!S&`mx6gJ3xenZbb
z$*H27vzYh4NAWI?!@U2_dv-?bMe0QlZb|v`(frPu2ZUAVxtZZ+~c5Ehwyq
zl!vFN^^AvFzcB*w^(oMCV*I~0%O|Ufmo|x7vEz$wrG>YtrZd_#Oy47A!%Bg({vGP?
z0U`SA4@eS>nzu}g#S>@>%j*3gv@;q6J(YcmQbtT5%Y6C4XXoXbgk9yZo=BW6+v&+A
zQ0%mWU+?{TjMNVs^kbtvoIYXj1XXoWp$T0~gEZ(F=)3|d@r`REC}%S(y_e!@T^4Z2
z16p%V7Qd(NJEqiVcT!x9E|`26cmchkvj{dwo@39bv%sN4-9r4D*2GcHcc}A2uTq1`
z3~p~c4Ly^i`hDC?uNtd+aYmv^@--sHhx%5ohf&Z{@v?mzhV
zP(pu!i{kbfp&Zo3HIg8$u&OYX_WG5JdMe>)K){{6k_pR)^o<(;HQInqwReLNRCO~Z&;zyuT0a824
z4nU;Q`9yXAs;XVHl9MblyPoE~ZhQK(UK2Jhp2m4hjxnegSk>CK+;AY_rbNY0^n##n
z!~f)vPbT>mkxePQ&%PcKPgoOeO=~`sJm^+mkCeoA#WTw8cFG*P6ei3=#p&w&n0nhY
ztf_mopqb0$UH}d9eO$}NaLox2`<)raybh&)yT92+wowf>QoJ)!p6Hh+@{=bZQn$4l
z)a`FPf-j4cK~FK74*Dopmr`To+=w{x0PP$Q)q2j&wv`YD!_Cofg@A3BV*^*wV+gB)THj69y?rTC3!uP;W@QrzpNV!y
z$RGxU6F~D`rEXYo2OTv$ph~5L*;pjly`MPnt68fBQKq1;WHB%L=6_wjE6m{1(u(EM
zP7Bjgg%W2m<^}%hbti(`kL4pzppLjnX&R9?Za+sa8R6^
zS8~dw+~l`f*%jEma7KlN%k{TRSq(V3YWqwU!iTUXcR}YVMeowv<*(ot?=l
zyEFc3L+N=Um)1L!aRzJZC3F$Gv_tr4TbLnyOjIt>3GXX2zx3zP_%6Y?g)OqhTi<5qO7u4zb*>@wOsgdu4thn6#xl9uQ-M1P&M;
z=51oD1YJi`%~i6_1ZI3}u4Fcm6Oud31O1Y@hiB`3;KeJ{FaZFQ*7XR;yTO$%GNy*b
zF$~-bumX?1l5Hy!um+9=T2iUd_x#zRmZm%3M$@DhP8}-C(;u!ERras)H|@`PNRP$*
zWqT3_XH4*{KoVshZJTP>eb;L)q%So&u=5YdL(crqy`=x{k_$btI7`oI#kXu
zYpA$2N0)dB%q1-wU+fAKES4shw}8y|*#XA^!GS7p%8GrTIkn=1&k2>#?G-TybSTF~
zmh0;a_Xi6Fxg-l2&8EC7v-)8ux9v7)RxLXtXzujnv?1Y$EMJ1feJ7(-Z}nd{mTMfc
zF}UpFgUrjPBHq}tF;m{7v-7gXM=sy{MENu>cS+ET75jvp^x4qfQ&pJNG7C55u~#Z(
zOK2|j;vOi!EG{QS!-P7R56M3gMz#)MlVC*cL6BZ%Hj()-b2#O>Fqc(%UbYAL2#q+r#e+u3vR8XugbOT9{A>}^sR8~Dr9g?Rij@kJb>!j;JAJiAs84w{&!8L$2AUXfQddiLt}%&ajq9j*Zk6
z3mfLXz41NYA6`f~x^MyrH@>470~16W$WwjBQwtfV&l-FeYxdpnp0PhI6v!^0?ZH}f
z8PNm+b^Mrz&o0LiSLGKFjD?>cU>Ug2I>(%LO>WftF`esY&TXG9haJg1UWvI-HneN+
z=N7LPuWz>p;Qh-%*P7|<@1LUz3V-oo;T>0T+0zuNfV_LgoP%`N=u{{<$t1p)iKGq1
z`xUw@Yg_QL$;>PFk^|u;GaJjwV?!KU(0GQO
zHUQ>%+A~A)%Xdm#gIt!kNy5@&rnbHVgDX2;g-;5ld)o%X4~VH1CD{rN`u$*B%@=MO
z3SJgs+>3x84=}fW)mk}}VPj^C@g)j8n??0Q*EDFs0NuIKuEk0l_$pzan5>g93+&)~
zMMY&*r0Jw4kvz^LH{1@ck_uVNG+)oH|EzRiuc~%(?Gfd1fpK+HC#O7lmLVd0CltYP
zMK_u1DZE_VL*MJm)`N^E_NhjWX44fx8b`MElGibkd4W5r=i#;zR$Q}_xP_JnOQo%w
zDD?YCz4M2A3afq$yQeUv9if-O9g>OM3>I*pN(p@%=%q`ap7`F9!EHeOzV@YEUeR^I
z77W9NSEtP_f)#p_qt()5JNk55GhqH~V=lu$-No9Y)c*)G`T3<6y{y|9`J1r0j(4y+
zEp;hG*nEct`fW&Fw_yj}2uEm-`VP5o8m13@+?|jnX+W{BOA8J=}
z@7H%)d@?|hk=?2|e)K0)^@HrX0$n?y$OsGn7|E)ecLSWR#9fH)tRqk0`O{fL09%90
zn?c^CLgHiZ^n8Yq6TFBdKWJk*E`xhq4Acz0(^Xj^w4$afqy5;L>V^Dgpfiz3;8IydEHm0{JdAwFTaIgh;$>>9-)d14tcBRSFv1tB
zF~;aCbcsWo_`nEbt)@%9Ae7Hf+K6QGJHh$;Ym-vP*Gh8d);nL9$w;7xry(U90PV-7
z_?*uvq*aS76{DPs%aBOh&H&)`u4kxAYWDXf;s{kY{+
z!{{UXqcr>a564e7J`O3SDq2MsgB->S%X4u7guYQPnb*=ZaS|KCzg^fc3fWtqt}FkQ
zTH+qOdKS?dt|qMGq$ph(WWT0iF)dhBJW7-Ek|=Zjql(xWqLwEbcldfN5y;)PU0GIlZ?z1nO!cjvEw?vQ3TS^-d`~jFGCx3Zan_V9)o-uo;XT{mP5MD
zuV0knfl=Ouex?mPT9@(Y#`rUvHVr2@v2*XB&UA;!TL0hQYYMWOG%#Zq_oNQEc?QB|
z=beZ~z;Ihv=jdGLFNjtzX%!!>y>ke7`=+3snkt+d+oo-PRgN&}l{>H`aVUKi`XHV8
z6dRR!%6aDb_VSJb^D%8N;|;oCqPHcLhBwE=#%RkpKNa(Lq<1t>$aQwm8p@Q0!U8Ft
z{*ICgLBcojW-P51i&Spg>&kG;xB<5o`M0z-3@WWLj#hf&4w2}9CU29JliRG6~rpeh2AdHdOZy_^GE-Ko%5xE?QU8c8F$-DZvszA7w
z)+#RfFK++YuU<4axerrkRgQa<-y9#~nrBb~t68c$6iYc9!LJQ1c`(5skk>cydpfQT
zRKl7|6r#8!|T<($*=Vn+H>;i?YlfHBrkhTlmL
zqiwGp@EZR71@>`CY6I?Qe77qlyr!z$uDI9GK0!?c-Q&}D&Zqh0v{6N7JReVclKq&F#EScVq9bk41I373$%X5EgMU94ick1jK+f8)
zB4+;fZr9hY&5Wx-5~r_5k)G6;6;C3>1ccxICm&)j?Cz7#v!{5*W4XXO)OI_p^%aeq^T>sIHx#4(Ld(#-evqk#QMJ
z-L3nI2CAGR{2kRgPKVtplLbo!twe~`*q<91^%?Iw(1nx1!+fn
zdZLy_j^ziL|JsW$42$RF3n!N;5lNk4->*?E%Tp76S`7|xetP1d>h&dd$
z9^9B_*YumQcAZcv)r()r*+gZ?0?5NdQ7UV08Y^lEAvTSn4NB4NX%Xf4q4kzRR?OG5N+##?Q2jv=-z=xx>%F2+dg++o
zc~U2Msc_F?C(k`CHu_tW#gva?vRRW>lg_46I~>Eq)Me~>>9mJa6T1_xL(t}3eI-;OL)h#bpz`i5Wvxy
z6#7_snKonNO0Qms2FB$CmlcpFlpr+BW4iT9zQ3aY`X54G1bVQ*@X3w*^+1Jd6n)@~nfvXOa$
zF53Q+9o(Q>%AztR=f|+uSfS|{XNJMGVf}Va*+Czv`
z>m4U~u?#$8DzK9Lv7Y2c_C{CXMZPLL8{_G?k)}s=TEkvQrE}l3F0Po_w08*vId(vz
zE2Sjc;YQq&>e7OMdUGh1)|?BZG;?hm%TTJRO+;
zB%IqAq|Q0YJ8&nB#5AZe@h0!s75QC@epf9#2)g%Ti|n-=m#x02NLt2iE0iU#Be=3l
z*J$Z!7lWd#_SvomxCBwON|QbhKke&fgpbUK5eh60o3zp>@*ZyrFDn6z7?LTmkfD+#%BL&F6xfV(Qq^H;;}{>P-iI&LSITzTXrC
zo(<&@_AO=
zw5$$ZYPIptE1rH!p6Hw7+sW_Ed0|~FJ`~-Ae6RrEOS)%U(%N=43>S1i!Q#H}eMxBc
zCA1dSeD(LT==?jV5*Js@sT@b6pY3c62UKR!=(6HC{7p>_gD?t$y8}imiY+b
z2jnkDyinFiH6G`d2aE*FhM$`Q#H8xwB9;JgJ-%1%v&R7Zdmp++XU@7437{?bp
z8w(1-?y+B5WoyzCop-PrzeZHhaoDp)d!(6GLWkqBD&4AiT7Mbgcw#b!6Mk9h9pzgk
zN>M%S*ccLPUH4<5xKSlV#~&`C-%yH(xq(0KUnRjU92xd@tl#59f{gt|3VTF=9
zqa?`*HP}nV1KpvS!WiUm-n+(;U5n+j#B;x3lG1SsRuVb@K_n+#kP84U6J$Lx6-$B(|VTn
z4XsB!(P|PxcXs6GuP1EuZkFBn!SDR?f|cdq8{M&J7Fa!&PyNE-?#_;}
zNX9320n@Pd8)<&9@GEBeOx{-Wg;z*om5lryo#xPh5RC3wz(H!lSN@ErYh8Z?w(%SKs(H$6c32;npx2|zrHK&_tVlFz(c_1V!t5Dm}2EBg9%z4
zBg#OZXE{_N_Iz%v%(IoOLF7a#NREdEAy1QGAF29jwG2jE{5t57#3WYvLa)}mXAVsZ
zug#WAs`vKx>srQ{k9tLW$$wfYl{YwDS3#dn?#AP@tR}HBioFfe=aZhkQIi1oRg`Th
zdB04Hf6XM|^H;-7*N9h0t&KBdjM4}#g(#ty9v4>6^w*SUiHsh1WQ$mS$J*?iT(L)a
zpQ0G76VAVr2K!iV8up<-W_ud9$?zIG7e8}^D-OM*r4r8N&8L;2t^$&9N4@1eb7`6z
zB1caehlLFWKO3Yv;ESa5$Qq6@j30{DnW)orkGOgH3)F_!y>)rycQ@*B`>{>PDU$Gy
zs%eNQ%-F62o0^KsL6zl#qgH=?&BPDgv^`%RfzoHW;k3UBPbguFcNbkF4Lh@2`|(9I
zn_&+ygk!y#3G-vSA|`a~c(6xr$9z6jD%0n7*P!cj4*#{4e%9u9Df4(fIhc){a|X@GYlJ4)4?pZU=@LwXcp
zZc!wB&;GK+z$wAEf~sX`1Y+
za}#}DzGXMx-3IO-a7uByAH&@9VC~}*eXHFgeN3=;C!})$A%@Hgn4ePo<#jwJ_Q|z>
z`xt5>K($EtGp$XpM7yz-Bo5Fj=V%&R8{UwQAz73<
zcqMhR%L_F2I`libhDXoAjOIdbN#VkbgRF*ba>rhe?cP^t;CbD5n^
zHZmlYD!>R{DW2Zp=f6ecKNRGiyi?VH7gxJGTX!E}Ib&3gdR^=yY+iy&=8&sL5x!~{
zX^uNGY$_E9s6vfVOGs&gn@Ht-s%AXU~*yD#8WH?hKIdBi~0P#O1SAkn6+C
zlODcLDG8P(tiW>*EMRLr?)yuAN9PN_GHD>t-Mz_4!-)g=#w)vb5K;3)c|8y8r+Q{6
zuOQSF;^^AjPuc1*RLQEU#XWAs)#onHpf#s^R`4GFqd7e}3}fB9YiXIOQ!%l<2*Wgx
z$oD_4C0PCyNTEa^S3m`>h$0c5SCRR$_MUJIrwj9!>Q}Ze`SiBfVAiy{ck+pN&_jU@
z(@%8gXt$8HH}4b?t$ulSpr^OwgEaSE8+ao)E!avOVDBy!iP=}SM+z!NTA!?L3x$H4
zInkDTRZeF0jCn8@D=#0*^W0b$MwgbR49`Ls9g
zl@KNL!B