diff --git a/app/EmojiService.ts b/app/EmojiService.ts index 1644667ad0..9af70d6b43 100644 --- a/app/EmojiService.ts +++ b/app/EmojiService.ts @@ -9,6 +9,7 @@ import LRU from 'lru-cache'; import type { OptionalResourceService } from './OptionalResourceService'; import { SignalService as Proto } from '../ts/protobuf'; +import { parseUnknown } from '../ts/util/schemas'; const MANIFEST_PATH = join(__dirname, '..', 'build', 'jumbomoji.json'); @@ -64,8 +65,9 @@ export class EmojiService { public static async create( resourceService: OptionalResourceService ): Promise { - const json = await readFile(MANIFEST_PATH, 'utf8'); - const manifest = manifestSchema.parse(JSON.parse(json)); + const contents = await readFile(MANIFEST_PATH, 'utf8'); + const json: unknown = JSON.parse(contents); + const manifest = parseUnknown(manifestSchema, json); return new EmojiService(resourceService, manifest); } diff --git a/app/OptionalResourceService.ts b/app/OptionalResourceService.ts index f832a78519..3b2931f044 100644 --- a/app/OptionalResourceService.ts +++ b/app/OptionalResourceService.ts @@ -17,6 +17,7 @@ import { OptionalResourcesDictSchema } from '../ts/types/OptionalResource'; import * as log from '../ts/logging/log'; import { getGotOptions } from '../ts/updater/got'; import { drop } from '../ts/util/drop'; +import { parseUnknown } from '../ts/util/schemas'; const RESOURCES_DICT_PATH = join( __dirname, @@ -106,8 +107,10 @@ export class OptionalResourceService { return; } - const json = JSON.parse(await readFile(RESOURCES_DICT_PATH, 'utf8')); - this.maybeDeclaration = OptionalResourcesDictSchema.parse(json); + const json: unknown = JSON.parse( + await readFile(RESOURCES_DICT_PATH, 'utf8') + ); + this.maybeDeclaration = parseUnknown(OptionalResourcesDictSchema, json); // Clean unknown resources let subPaths: Array; diff --git a/app/attachment_channel.ts b/app/attachment_channel.ts index 24442560d5..1cf9ca48fe 100644 --- a/app/attachment_channel.ts +++ b/app/attachment_channel.ts @@ -56,6 +56,7 @@ import { isVideoTypeSupported, } from '../ts/util/GoogleChrome'; import { decryptAttachmentV2ToSink } from '../ts/AttachmentCrypto'; +import { parseLoose } from '../ts/util/schemas'; let initialized = false; @@ -471,7 +472,7 @@ export async function handleAttachmentRequest(req: Request): Promise { let disposition: z.infer = 'attachment'; const dispositionParam = url.searchParams.get('disposition'); if (dispositionParam != null) { - disposition = dispositionSchema.parse(dispositionParam); + disposition = parseLoose(dispositionSchema, dispositionParam); } strictAssert(attachmentsDir != null, 'not initialized'); diff --git a/app/crashReports.ts b/app/crashReports.ts index 643cf582ba..4fdc276d73 100644 --- a/app/crashReports.ts +++ b/app/crashReports.ts @@ -12,6 +12,7 @@ import * as Errors from '../ts/types/errors'; import { isProduction } from '../ts/util/version'; import { isNotNil } from '../ts/util/isNotNil'; import OS from '../ts/util/os/osMain'; +import { parseUnknown } from '../ts/util/schemas'; // See https://github.com/rust-minidump/rust-minidump/blob/main/minidump-processor/json-schema.md const dumpString = z.string().or(z.null()).optional(); @@ -120,9 +121,8 @@ export function setup( pendingDumps.map(async fullPath => { const content = await readFile(fullPath); try { - const dump = dumpSchema.parse( - JSON.parse(dumpToJSONString(content)) - ); + const json: unknown = JSON.parse(dumpToJSONString(content)); + const dump = parseUnknown(dumpSchema, json); if (dump.crash_info?.type !== 'Simulated Exception') { return fullPath; } @@ -173,7 +173,8 @@ export function setup( const content = await readFile(fullPath); const { mtime } = await stat(fullPath); - const dump = dumpSchema.parse(JSON.parse(dumpToJSONString(content))); + const json: unknown = JSON.parse(dumpToJSONString(content)); + const dump = parseUnknown(dumpSchema, json); if (dump.crash_info?.type === 'Simulated Exception') { return undefined; diff --git a/app/dns-fallback.ts b/app/dns-fallback.ts index bf689782cc..18ce401c66 100644 --- a/app/dns-fallback.ts +++ b/app/dns-fallback.ts @@ -5,6 +5,7 @@ import { join } from 'path'; import { readFile } from 'fs/promises'; import { DNSFallbackSchema } from '../ts/types/DNSFallback'; import type { DNSFallbackType } from '../ts/types/DNSFallback'; +import { parseUnknown } from '../ts/util/schemas'; let cached: DNSFallbackType | undefined; @@ -25,9 +26,9 @@ export async function getDNSFallback(): Promise { return cached; } - const json = JSON.parse(str); + const json: unknown = JSON.parse(str); - const result = DNSFallbackSchema.parse(json); + const result = parseUnknown(DNSFallbackSchema, json); cached = result; return result; } diff --git a/app/locale.ts b/app/locale.ts index 5e7daab588..2a959da4d0 100644 --- a/app/locale.ts +++ b/app/locale.ts @@ -13,6 +13,7 @@ import type { LoggerType } from '../ts/types/Logging'; import type { HourCyclePreference, LocaleMessagesType } from '../ts/types/I18N'; import type { LocalizerType } from '../ts/types/Util'; import * as Errors from '../ts/types/errors'; +import { parseUnknown } from '../ts/util/schemas'; const TextInfoSchema = z.object({ direction: z.enum(['ltr', 'rtl']), @@ -70,16 +71,18 @@ function getLocaleDirection( try { // @ts-expect-error -- TS doesn't know about this method if (typeof locale.getTextInfo === 'function') { - return TextInfoSchema.parse( + return parseUnknown( + TextInfoSchema, // @ts-expect-error -- TS doesn't know about this method - locale.getTextInfo() + locale.getTextInfo() as unknown ).direction; } // @ts-expect-error -- TS doesn't know about this property if (typeof locale.textInfo === 'object') { - return TextInfoSchema.parse( + return parseUnknown( + TextInfoSchema, // @ts-expect-error -- TS doesn't know about this property - locale.textInfo + locale.textInfo as unknown ).direction; } } catch (error) { diff --git a/app/main.ts b/app/main.ts index 77ccc49be4..be6bd572bf 100644 --- a/app/main.ts +++ b/app/main.ts @@ -123,6 +123,7 @@ import { ZoomFactorService } from '../ts/services/ZoomFactorService'; import { SafeStorageBackendChangeError } from '../ts/types/SafeStorageBackendChangeError'; import { LINUX_PASSWORD_STORE_FLAGS } from '../ts/util/linuxPasswordStoreFlags'; import { getOwn } from '../ts/util/getOwn'; +import { safeParseLoose, safeParseUnknown } from '../ts/util/schemas'; const animationSettings = systemPreferences.getAnimationSettings(); @@ -436,7 +437,8 @@ export const windowConfigSchema = z.object({ type WindowConfigType = z.infer; let windowConfig: WindowConfigType | undefined; -const windowConfigParsed = windowConfigSchema.safeParse( +const windowConfigParsed = safeParseUnknown( + windowConfigSchema, windowFromEphemeral || windowFromUserConfig ); if (windowConfigParsed.success) { @@ -2692,7 +2694,7 @@ ipc.on('delete-all-data', () => { ipc.on('get-config', async event => { const theme = await getResolvedThemeSetting(); - const directoryConfig = directoryConfigSchema.safeParse({ + const directoryConfig = safeParseLoose(directoryConfigSchema, { directoryUrl: config.get('directoryUrl') || undefined, directoryMRENCLAVE: config.get('directoryMRENCLAVE') || undefined, @@ -2705,7 +2707,7 @@ ipc.on('get-config', async event => { ); } - const parsed = rendererConfigSchema.safeParse({ + const parsed = safeParseLoose(rendererConfigSchema, { name: packageJson.productName, availableLocales: getResolvedMessagesLocale().availableLocales, resolvedTranslationsLocale: getResolvedMessagesLocale().name, diff --git a/patches/zod+3.22.3.patch b/patches/zod+3.22.3.patch new file mode 100644 index 0000000000..df96bd01c5 --- /dev/null +++ b/patches/zod+3.22.3.patch @@ -0,0 +1,14 @@ +diff --git a/node_modules/zod/lib/types.d.ts b/node_modules/zod/lib/types.d.ts +index 0ece6e8..57bbe86 100644 +--- a/node_modules/zod/lib/types.d.ts ++++ b/node_modules/zod/lib/types.d.ts +@@ -56,7 +56,9 @@ export declare abstract class ZodType; + _parseAsync(input: ParseInput): AsyncParseReturnType; ++ /** @deprecated (Signal Desktop: Use ts/util/schema.ts instead) */ + parse(data: unknown, params?: Partial): Output; ++ /** @deprecated (Signal Desktop: Use ts/util/schema.ts instead) */ + safeParse(data: unknown, params?: Partial): SafeParseReturnType; + parseAsync(data: unknown, params?: Partial): Promise; + safeParseAsync(data: unknown, params?: Partial): Promise>; diff --git a/ts/SignalProtocolStore.ts b/ts/SignalProtocolStore.ts index 7f175c1eea..72e9efde8f 100644 --- a/ts/SignalProtocolStore.ts +++ b/ts/SignalProtocolStore.ts @@ -67,6 +67,7 @@ import { SIGNED_PRE_KEY_ID_KEY, } from './textsecure/AccountManager'; import { formatGroups, groupWhile } from './util/groupWhile'; +import { parseUnknown } from './util/schemas'; const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds const LOW_KEYS_THRESHOLD = 25; @@ -99,7 +100,7 @@ const identityKeySchema = z.object({ function validateIdentityKey(attrs: unknown): attrs is IdentityKeyType { // We'll throw if this doesn't match - identityKeySchema.parse(attrs); + parseUnknown(identityKeySchema, attrs); return true; } /* diff --git a/ts/badges/parseBadgesFromServer.ts b/ts/badges/parseBadgesFromServer.ts index a35c7fd272..90fafdb9a7 100644 --- a/ts/badges/parseBadgesFromServer.ts +++ b/ts/badges/parseBadgesFromServer.ts @@ -9,6 +9,7 @@ import * as log from '../logging/log'; import type { BadgeType, BadgeImageType } from './types'; import { parseBadgeCategory } from './BadgeCategory'; import { BadgeImageTheme, parseBadgeImageTheme } from './BadgeImageTheme'; +import { safeParseUnknown } from '../util/schemas'; const MAX_BADGES = 1000; @@ -40,7 +41,7 @@ export function parseBoostBadgeListFromServer( ): Record { const result: Record = {}; - const parseResult = boostBadgesFromServerSchema.safeParse(value); + const parseResult = safeParseUnknown(boostBadgesFromServerSchema, value); if (!parseResult.success) { log.warn( 'parseBoostBadgeListFromServer: server response was invalid:', @@ -73,7 +74,7 @@ export function parseBadgeFromServer( value: unknown, updatesUrl: string ): BadgeType | undefined { - const parseResult = badgeFromServerSchema.safeParse(value); + const parseResult = safeParseUnknown(badgeFromServerSchema, value); if (!parseResult.success) { log.warn( 'parseBadgeFromServer: badge was invalid:', diff --git a/ts/components/EditNicknameAndNoteModal.tsx b/ts/components/EditNicknameAndNoteModal.tsx index b7e6638868..59b323c9b1 100644 --- a/ts/components/EditNicknameAndNoteModal.tsx +++ b/ts/components/EditNicknameAndNoteModal.tsx @@ -15,6 +15,7 @@ import { Input } from './Input'; import { AutoSizeTextArea } from './AutoSizeTextArea'; import { Button, ButtonVariant } from './Button'; import { strictAssert } from '../util/assert'; +import { safeParsePartial } from '../util/schemas'; const formSchema = z.object({ nickname: z @@ -67,7 +68,7 @@ export function EditNicknameAndNoteModal({ const familyNameValue = toOptionalStringValue(familyName); const noteValue = toOptionalStringValue(note); const hasEitherName = givenNameValue != null || familyNameValue != null; - return formSchema.safeParse({ + return safeParsePartial(formSchema, { nickname: hasEitherName ? { givenName: givenNameValue, familyName: familyNameValue } : null, diff --git a/ts/jobs/AttachmentDownloadManager.ts b/ts/jobs/AttachmentDownloadManager.ts index 055661fcea..286f28e553 100644 --- a/ts/jobs/AttachmentDownloadManager.ts +++ b/ts/jobs/AttachmentDownloadManager.ts @@ -45,6 +45,7 @@ import type { MIMEType } from '../types/MIME'; import { AttachmentDownloadSource } from '../sql/Interface'; import { drop } from '../util/drop'; import { getAttachmentCiphertextLength } from '../AttachmentCrypto'; +import { safeParsePartial } from '../util/schemas'; export enum AttachmentDownloadUrgency { IMMEDIATE = 'immediate', @@ -175,7 +176,7 @@ export class AttachmentDownloadManager extends JobManager { } protected parseData(data: unknown): CallLinkRefreshJobData { - return callLinkRefreshJobDataSchema.parse(data); + return parseUnknown(callLinkRefreshJobDataSchema, data); } protected async run( diff --git a/ts/jobs/conversationJobQueue.ts b/ts/jobs/conversationJobQueue.ts index fba565bd90..67decfe4c3 100644 --- a/ts/jobs/conversationJobQueue.ts +++ b/ts/jobs/conversationJobQueue.ts @@ -50,6 +50,7 @@ import { drop } from '../util/drop'; import { isInPast } from '../util/timestamp'; import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary'; import { FIBONACCI } from '../util/BackOff'; +import { parseUnknown } from '../util/schemas'; // Note: generally, we only want to add to this list. If you do need to change one of // these values, you'll likely need to write a database migration. @@ -415,7 +416,7 @@ export class ConversationJobQueue extends JobQueue { } protected parseData(data: unknown): ConversationQueueJobData { - return conversationQueueJobDataSchema.parse(data); + return parseUnknown(conversationQueueJobDataSchema, data); } protected override getInMemoryQueue({ diff --git a/ts/jobs/groupAvatarJobQueue.ts b/ts/jobs/groupAvatarJobQueue.ts index 9df6b257b2..17f605b87f 100644 --- a/ts/jobs/groupAvatarJobQueue.ts +++ b/ts/jobs/groupAvatarJobQueue.ts @@ -10,6 +10,7 @@ import { DataWriter } from '../sql/Client'; import type { JOB_STATUS } from './JobQueue'; import { JobQueue } from './JobQueue'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; +import { parseUnknown } from '../util/schemas'; const groupAvatarJobDataSchema = z.object({ conversationId: z.string(), @@ -20,7 +21,7 @@ export type GroupAvatarJobData = z.infer; export class GroupAvatarJobQueue extends JobQueue { protected parseData(data: unknown): GroupAvatarJobData { - return groupAvatarJobDataSchema.parse(data); + return parseUnknown(groupAvatarJobDataSchema, data); } protected async run( diff --git a/ts/jobs/removeStorageKeyJobQueue.ts b/ts/jobs/removeStorageKeyJobQueue.ts index c6c3e23a9f..574c782d71 100644 --- a/ts/jobs/removeStorageKeyJobQueue.ts +++ b/ts/jobs/removeStorageKeyJobQueue.ts @@ -7,6 +7,7 @@ import type { JOB_STATUS } from './JobQueue'; import { JobQueue } from './JobQueue'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; +import { parseUnknown } from '../util/schemas'; const removeStorageKeyJobDataSchema = z.object({ key: z.enum([ @@ -22,7 +23,7 @@ type RemoveStorageKeyJobData = z.infer; export class RemoveStorageKeyJobQueue extends JobQueue { protected parseData(data: unknown): RemoveStorageKeyJobData { - return removeStorageKeyJobDataSchema.parse(data); + return parseUnknown(removeStorageKeyJobDataSchema, data); } protected async run({ diff --git a/ts/jobs/reportSpamJobQueue.ts b/ts/jobs/reportSpamJobQueue.ts index c189400b03..1fdc155c83 100644 --- a/ts/jobs/reportSpamJobQueue.ts +++ b/ts/jobs/reportSpamJobQueue.ts @@ -17,6 +17,7 @@ import { parseIntWithFallback } from '../util/parseIntWithFallback'; import type { WebAPIType } from '../textsecure/WebAPI'; import { HTTPError } from '../textsecure/Errors'; import { sleeper } from '../util/sleeper'; +import { parseUnknown } from '../util/schemas'; const RETRY_WAIT_TIME = durations.MINUTE; const RETRYABLE_4XX_FAILURE_STATUSES = new Set([ @@ -44,7 +45,7 @@ export class ReportSpamJobQueue extends JobQueue { } protected parseData(data: unknown): ReportSpamJobData { - return reportSpamJobDataSchema.parse(data); + return parseUnknown(reportSpamJobDataSchema, data); } protected async run( diff --git a/ts/jobs/singleProtoJobQueue.ts b/ts/jobs/singleProtoJobQueue.ts index 4208650e18..52d9762640 100644 --- a/ts/jobs/singleProtoJobQueue.ts +++ b/ts/jobs/singleProtoJobQueue.ts @@ -24,6 +24,7 @@ import { } from './helpers/handleMultipleSendErrors'; import { isConversationUnregistered } from '../util/isConversationUnregistered'; import { isConversationAccepted } from '../util/isConversationAccepted'; +import { parseUnknown } from '../util/schemas'; const MAX_RETRY_TIME = DAY; const MAX_PARALLEL_JOBS = 5; @@ -43,7 +44,7 @@ export class SingleProtoJobQueue extends JobQueue { } protected parseData(data: unknown): SingleProtoJobData { - return singleProtoJobDataSchema.parse(data); + return parseUnknown(singleProtoJobDataSchema, data); } protected async run( diff --git a/ts/logging/uploadDebugLog.ts b/ts/logging/uploadDebugLog.ts index bd162a96b7..650ec7003e 100644 --- a/ts/logging/uploadDebugLog.ts +++ b/ts/logging/uploadDebugLog.ts @@ -11,6 +11,7 @@ import { getUserAgent } from '../util/getUserAgent'; import { maybeParseUrl } from '../util/url'; import * as durations from '../util/durations'; import type { LoggerType } from '../types/Logging'; +import { parseUnknown } from '../util/schemas'; const BASE_URL = 'https://debuglogs.org'; @@ -26,7 +27,7 @@ const tokenBodySchema = z const parseTokenBody = ( rawBody: unknown ): { fields: Record; url: string } => { - const body = tokenBodySchema.parse(rawBody); + const body = parseUnknown(tokenBodySchema, rawBody); const parsedUrl = maybeParseUrl(body.url); if (!parsedUrl) { diff --git a/ts/scripts/build-localized-display-names.ts b/ts/scripts/build-localized-display-names.ts index 0e8df6ca8a..d7fc6948cd 100644 --- a/ts/scripts/build-localized-display-names.ts +++ b/ts/scripts/build-localized-display-names.ts @@ -5,6 +5,7 @@ import { parse } from 'csv-parse'; import fs from 'fs/promises'; import { z } from 'zod'; import { _getAvailableLocales } from '../../app/locale'; +import { parseUnknown } from '../util/schemas'; const type = process.argv[2]; if (type !== 'countries' && type !== 'locales') { @@ -119,7 +120,7 @@ function assertValuesForAllCountries(result: LocaleDisplayNamesResult) { async function main() { const contents = await fs.readFile(localeDisplayNamesDataPath, 'utf-8'); const records = await parseCsv(contents); - const data = LocaleDisplayNames.parse(records); + const data = parseUnknown(LocaleDisplayNames, records as unknown); const result = convertData(data); if (type === 'locales') { assertValuesForAllLocales(result); diff --git a/ts/scripts/get-emoji-locales.ts b/ts/scripts/get-emoji-locales.ts index a5ac63c91d..993042c37e 100644 --- a/ts/scripts/get-emoji-locales.ts +++ b/ts/scripts/get-emoji-locales.ts @@ -9,6 +9,7 @@ import prettier from 'prettier'; import type { OptionalResourceType } from '../types/OptionalResource'; import { OptionalResourcesDictSchema } from '../types/OptionalResource'; +import { parseUnknown } from '../util/schemas'; const MANIFEST_URL = 'https://updates.signal.org/dynamic/android/emoji/search/manifest.json'; @@ -29,7 +30,7 @@ async function fetchJSON(url: string): Promise { } async function main(): Promise { - const manifest = ManifestSchema.parse(await fetchJSON(MANIFEST_URL)); + const manifest = parseUnknown(ManifestSchema, await fetchJSON(MANIFEST_URL)); // eslint-disable-next-line dot-notation manifest.languageToSmartlingLocale['zh_TW'] = 'zh-Hant'; @@ -75,8 +76,9 @@ async function main(): Promise { 'build', 'optional-resources.json' ); - const resources = OptionalResourcesDictSchema.parse( - JSON.parse(await readFile(resourcesPath, 'utf8')) + const resources = parseUnknown( + OptionalResourcesDictSchema, + JSON.parse(await readFile(resourcesPath, 'utf8')) as unknown ); for (const [locale, resource] of extraResources) { diff --git a/ts/scripts/get-jumbomoji.ts b/ts/scripts/get-jumbomoji.ts index 2197bd4880..aef02473a0 100644 --- a/ts/scripts/get-jumbomoji.ts +++ b/ts/scripts/get-jumbomoji.ts @@ -9,6 +9,7 @@ import prettier from 'prettier'; import type { OptionalResourceType } from '../types/OptionalResource'; import { OptionalResourcesDictSchema } from '../types/OptionalResource'; +import { parseUnknown } from '../util/schemas'; const VERSION = 10; @@ -28,7 +29,10 @@ async function fetchJSON(url: string): Promise { } async function main(): Promise { - const { jumbomoji } = ManifestSchema.parse(await fetchJSON(MANIFEST_URL)); + const { jumbomoji } = parseUnknown( + ManifestSchema, + await fetchJSON(MANIFEST_URL) + ); const extraResources = new Map(); @@ -68,8 +72,9 @@ async function main(): Promise { 'build', 'optional-resources.json' ); - const resources = OptionalResourcesDictSchema.parse( - JSON.parse(await readFile(resourcesPath, 'utf8')) + const resources = parseUnknown( + OptionalResourcesDictSchema, + JSON.parse(await readFile(resourcesPath, 'utf8')) as unknown ); for (const [sheet, resource] of extraResources) { diff --git a/ts/scripts/test-electron.ts b/ts/scripts/test-electron.ts index 76738a6b55..c893d71afa 100644 --- a/ts/scripts/test-electron.ts +++ b/ts/scripts/test-electron.ts @@ -13,6 +13,7 @@ import logSymbols from 'log-symbols'; import { explodePromise } from '../util/explodePromise'; import { missingCaseError } from '../util/missingCaseError'; import { SECOND } from '../util/durations'; +import { parseUnknown } from '../util/schemas'; const ROOT_DIR = join(__dirname, '..', '..'); @@ -137,7 +138,10 @@ async function launchElectron( return; } - const event = eventSchema.parse(JSON.parse(match[1])); + const event = parseUnknown( + eventSchema, + JSON.parse(match[1]) as unknown + ); if (event.type === 'pass') { pass += 1; diff --git a/ts/services/senderCertificate.ts b/ts/services/senderCertificate.ts index 395118a347..a17c43bf9c 100644 --- a/ts/services/senderCertificate.ts +++ b/ts/services/senderCertificate.ts @@ -17,9 +17,10 @@ import type { WebAPIType } from '../textsecure/WebAPI'; import { SignalService as Proto } from '../protobuf'; import SenderCertificate = Proto.SenderCertificate; +import { safeParseUnknown } from '../util/schemas'; function isWellFormed(data: unknown): data is SerializedCertificateType { - return serializedCertificateSchema.safeParse(data).success; + return safeParseUnknown(serializedCertificateSchema, data).success; } // In case your clock is different from the server's, we "fake" expire certificates early. diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 538b9cb5d0..592ba1252d 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -207,6 +207,7 @@ import { } from '../types/AttachmentBackup'; import { redactGenericText } from '../util/privacy'; import { getAttachmentCiphertextLength } from '../AttachmentCrypto'; +import { parseStrict, parseUnknown, safeParseUnknown } from '../util/schemas'; type ConversationRow = Readonly<{ json: string; @@ -3664,7 +3665,7 @@ function getCallHistory( return; } - return callHistoryDetailsSchema.parse(row); + return parseUnknown(callHistoryDetailsSchema, row as unknown); } const SEEN_STATUS_UNSEEN = sqlConstant(SeenStatus.Unseen); @@ -3746,7 +3747,7 @@ function getCallHistoryForCallLogEventTarget( return null; } - return callHistoryDetailsSchema.parse(row); + return parseUnknown(callHistoryDetailsSchema, row as unknown); } function getConversationIdForCallHistory( @@ -4110,7 +4111,7 @@ function getCallHistoryGroupsCount( return 0; } - return countSchema.parse(result); + return parseUnknown(countSchema, result as unknown); } const groupsDataSchema = z.array( @@ -4135,7 +4136,8 @@ function getCallHistoryGroups( // getCallHistoryGroupData creates a temporary table and thus requires // write access. const writable = toUnsafeWritableDB(db, 'only temp table use'); - const groupsData = groupsDataSchema.parse( + const groupsData = parseUnknown( + groupsDataSchema, getCallHistoryGroupData(writable, false, filter, pagination) ); @@ -4145,8 +4147,9 @@ function getCallHistoryGroups( .map(groupData => { return { ...groupData, - possibleChildren: possibleChildrenSchema.parse( - JSON.parse(groupData.possibleChildren) + possibleChildren: parseUnknown( + possibleChildrenSchema, + JSON.parse(groupData.possibleChildren) as unknown ), inPeriod: new Set(groupData.inPeriod.split(',')), }; @@ -4167,7 +4170,7 @@ function getCallHistoryGroups( } } - return callHistoryGroupSchema.parse({ ...rest, type, children }); + return parseStrict(callHistoryGroupSchema, { ...rest, type, children }); }) .reverse(); } @@ -4788,14 +4791,14 @@ function getAttachmentDownloadJob( function removeAllBackupAttachmentDownloadJobs(db: WritableDB): void { const [query, params] = sql` - DELETE FROM attachment_downloads + DELETE FROM attachment_downloads WHERE source = ${AttachmentDownloadSource.BACKUP_IMPORT};`; db.prepare(query).run(params); } function getSizeOfPendingBackupAttachmentDownloadJobs(db: ReadableDB): number { const [query, params] = sql` - SELECT SUM(ciphertextSize) FROM attachment_downloads + SELECT SUM(ciphertextSize) FROM attachment_downloads WHERE source = ${AttachmentDownloadSource.BACKUP_IMPORT};`; return db.prepare(query).pluck().get(params); } @@ -4842,7 +4845,7 @@ function getNextAttachmentDownloadJobs( }) AND messageId IN (${sqlJoin(prioritizeMessageIds)}) - AND + AND ${sourceWhereFragment} -- for priority messages, let's load them oldest first; this helps, e.g. for stories where we -- want the oldest one first @@ -4862,7 +4865,7 @@ function getNextAttachmentDownloadJobs( active = 0 AND (retryAfter is NULL OR retryAfter <= ${timestamp}) - AND + AND ${sourceWhereFragment} ORDER BY receivedAt DESC LIMIT ${numJobsRemaining} @@ -4876,14 +4879,14 @@ function getNextAttachmentDownloadJobs( try { return allJobs.map(row => { try { - return attachmentDownloadJobSchema.parse({ + return parseUnknown(attachmentDownloadJobSchema, { ...row, active: Boolean(row.active), attachment: jsonToObject(row.attachmentJson), ciphertextSize: row.ciphertextSize || getAttachmentCiphertextLength(row.attachment.size), - }); + } as unknown); } catch (error) { logger.error( `getNextAttachmentDownloadJobs: Error with job for message ${row.messageId}, deleting.` @@ -5040,11 +5043,11 @@ function getNextAttachmentBackupJobs( const rows = db.prepare(query).all(params); return rows .map(row => { - const parseResult = attachmentBackupJobSchema.safeParse({ + const parseResult = safeParseUnknown(attachmentBackupJobSchema, { ...row, active: Boolean(row.active), data: jsonToObject(row.data), - }); + } as unknown); if (!parseResult.success) { const redactedMediaName = redactGenericText(row.mediaName); logger.error( diff --git a/ts/sql/migrations/1040-undownloaded-backed-up-media.ts b/ts/sql/migrations/1040-undownloaded-backed-up-media.ts index d8062bc7de..1b3b24dcdb 100644 --- a/ts/sql/migrations/1040-undownloaded-backed-up-media.ts +++ b/ts/sql/migrations/1040-undownloaded-backed-up-media.ts @@ -12,6 +12,7 @@ import { import type { AttachmentType } from '../../types/Attachment'; import { jsonToObject, objectToJSON, sql } from '../util'; import { AttachmentDownloadSource } from '../Interface'; +import { parsePartial } from '../../util/schemas'; export const version = 1040; @@ -68,7 +69,7 @@ export function updateToSchemaVersion1040( attempts INTEGER NOT NULL, retryAfter INTEGER, lastAttemptTimestamp INTEGER, - + PRIMARY KEY (messageId, attachmentType, digest) ) STRICT; `); @@ -84,7 +85,7 @@ export function updateToSchemaVersion1040( // 5. Add new index on active & receivedAt. For most queries when there are lots of // jobs (like during backup restore), many jobs will match the the WHERE clause, so // the ORDER BY on receivedAt is probably the most expensive part. - db.exec(` + db.exec(` CREATE INDEX attachment_downloads_active_receivedAt ON attachment_downloads ( active, receivedAt @@ -94,7 +95,7 @@ export function updateToSchemaVersion1040( // 6. Add new index on active & messageId. In order to prioritize visible messages, // we'll also query for rows with a matching messageId. For these, the messageId // matching is likely going to be the most expensive part. - db.exec(` + db.exec(` CREATE INDEX attachment_downloads_active_messageId ON attachment_downloads ( active, messageId @@ -103,7 +104,7 @@ export function updateToSchemaVersion1040( // 7. Add new index just on messageId, for the ON DELETE CASCADE foreign key // constraint - db.exec(` + db.exec(` CREATE INDEX attachment_downloads_messageId ON attachment_downloads ( messageId @@ -139,7 +140,7 @@ export function updateToSchemaVersion1040( ciphertextSize: 0, }; - const parsed = attachmentDownloadJobSchema.parse(updatedJob); + const parsed = parsePartial(attachmentDownloadJobSchema, updatedJob); rowsToTransfer.push(parsed as AttachmentDownloadJobType); } catch { @@ -160,13 +161,13 @@ export function updateToSchemaVersion1040( ( messageId, attachmentType, - receivedAt, + receivedAt, sentAt, digest, contentType, size, attachmentJson, - active, + active, attempts, retryAfter, lastAttemptTimestamp @@ -181,7 +182,7 @@ export function updateToSchemaVersion1040( ${row.contentType}, ${row.size}, ${objectToJSON(row.attachment)}, - ${row.active ? 1 : 0}, + ${row.active ? 1 : 0}, ${row.attempts}, ${row.retryAfter}, ${row.lastAttemptTimestamp} diff --git a/ts/sql/migrations/89-call-history.ts b/ts/sql/migrations/89-call-history.ts index a095ce3a38..7e07bdbc27 100644 --- a/ts/sql/migrations/89-call-history.ts +++ b/ts/sql/migrations/89-call-history.ts @@ -23,6 +23,7 @@ import type { WritableDB, MessageType, ConversationType } from '../Interface'; import { strictAssert } from '../../util/assert'; import { missingCaseError } from '../../util/missingCaseError'; import { isAciString } from '../../util/isAciString'; +import { safeParseStrict } from '../../util/schemas'; // Legacy type for calls that never had a call id type DirectCallHistoryDetailsType = { @@ -177,7 +178,7 @@ function convertLegacyCallDetails( endedTimestamp: null, }; - const result = callHistoryDetailsSchema.safeParse(callHistory); + const result = safeParseStrict(callHistoryDetailsSchema, callHistory); if (result.success) { return result.data; } diff --git a/ts/sql/server/callLinks.ts b/ts/sql/server/callLinks.ts index 9b00dbcae5..cb7d1e68e5 100644 --- a/ts/sql/server/callLinks.ts +++ b/ts/sql/server/callLinks.ts @@ -21,6 +21,7 @@ import { prepare } from '../Server'; import { sql } from '../util'; import { strictAssert } from '../../util/assert'; import { CallStatusValue } from '../../types/CallDisposition'; +import { parseStrict, parseUnknown } from '../../util/schemas'; export function callLinkExists(db: ReadableDB, roomId: string): boolean { const [query, params] = sql` @@ -58,7 +59,7 @@ export function getCallLinkRecordByRoomId( return undefined; } - return callLinkRecordSchema.parse(row); + return parseUnknown(callLinkRecordSchema, row as unknown); } export function getAllCallLinks(db: ReadableDB): ReadonlyArray { @@ -68,7 +69,9 @@ export function getAllCallLinks(db: ReadableDB): ReadonlyArray { return db .prepare(query) .all() - .map(item => callLinkFromRecord(callLinkRecordSchema.parse(item))); + .map((item: unknown) => + callLinkFromRecord(parseUnknown(callLinkRecordSchema, item)) + ); } function _insertCallLink(db: WritableDB, callLink: CallLinkType): void { @@ -142,7 +145,10 @@ export function updateCallLinkState( callLinkState: CallLinkStateType ): CallLinkType { const { name, restrictions, expiration, revoked } = callLinkState; - const restrictionsValue = callLinkRestrictionsSchema.parse(restrictions); + const restrictionsValue = parseStrict( + callLinkRestrictionsSchema, + restrictions + ); const [query, params] = sql` UPDATE callLinks SET @@ -153,9 +159,9 @@ export function updateCallLinkState( WHERE roomId = ${roomId} RETURNING *; `; - const row = db.prepare(query).get(params); + const row: unknown = db.prepare(query).get(params); strictAssert(row, 'Expected row to be returned'); - return callLinkFromRecord(callLinkRecordSchema.parse(row)); + return callLinkFromRecord(parseUnknown(callLinkRecordSchema, row)); } export function updateCallLinkAdminKeyByRoomId( @@ -302,7 +308,7 @@ export function getAllCallLinkRecordsWithAdminKey( return db .prepare(query) .all() - .map(item => callLinkRecordSchema.parse(item)); + .map((item: unknown) => parseUnknown(callLinkRecordSchema, item)); } export function getAllMarkedDeletedCallLinkRoomIds( diff --git a/ts/sql/server/groupSendEndorsements.ts b/ts/sql/server/groupSendEndorsements.ts index 7b556117df..272371e23b 100644 --- a/ts/sql/server/groupSendEndorsements.ts +++ b/ts/sql/server/groupSendEndorsements.ts @@ -8,7 +8,6 @@ import type { } from '../../types/GroupSendEndorsements'; import { groupSendEndorsementExpirationSchema, - groupSendCombinedEndorsementSchema, groupSendMemberEndorsementSchema, groupSendEndorsementsDataSchema, } from '../../types/GroupSendEndorsements'; @@ -17,6 +16,7 @@ import type { ReadableDB, WritableDB } from '../Interface'; import { sql } from '../util'; import type { AciString } from '../../types/ServiceId'; import { strictAssert } from '../../util/assert'; +import { parseLoose, parseUnknown } from '../../util/schemas'; /** * We don't need to store more than one endorsement per group or per member. @@ -110,7 +110,7 @@ export function getGroupSendCombinedEndorsementExpiration( if (value == null) { return null; } - return groupSendEndorsementExpirationSchema.parse(value); + return parseUnknown(groupSendEndorsementExpirationSchema, value as unknown); } export function getGroupSendEndorsementsData( @@ -128,24 +128,21 @@ export function getGroupSendEndorsementsData( WHERE groupId IS ${groupId} `; - const combinedEndorsement = groupSendCombinedEndorsementSchema - .optional() - .parse( - prepare>(db, selectCombinedEndorsement).get( - selectCombinedEndorsementParams - ) - ); + const combinedEndorsement: unknown = prepare>( + db, + selectCombinedEndorsement + ).get(selectCombinedEndorsementParams); if (combinedEndorsement == null) { return null; } - const memberEndorsements = prepare>( + const memberEndorsements: Array = prepare>( db, selectMemberEndorsements ).all(selectMemberEndorsementsParams); - return groupSendEndorsementsDataSchema.parse({ + return parseLoose(groupSendEndorsementsDataSchema, { combinedEndorsement, memberEndorsements, }); @@ -168,5 +165,5 @@ export function getGroupSendMemberEndorsement( if (row == null) { return null; } - return groupSendMemberEndorsementSchema.parse(row); + return parseUnknown(groupSendMemberEndorsementSchema, row as unknown); } diff --git a/ts/test-node/jobs/JobQueue_test.ts b/ts/test-node/jobs/JobQueue_test.ts index 6632ad8217..a2b55b13b3 100644 --- a/ts/test-node/jobs/JobQueue_test.ts +++ b/ts/test-node/jobs/JobQueue_test.ts @@ -20,6 +20,7 @@ import type { JOB_STATUS } from '../../jobs/JobQueue'; import { JobQueue } from '../../jobs/JobQueue'; import type { ParsedJob, StoredJob, JobQueueStore } from '../../jobs/types'; import { sleep } from '../../util/sleep'; +import { parseUnknown } from '../../util/schemas'; describe('JobQueue', () => { describe('end-to-end tests', () => { @@ -36,7 +37,7 @@ describe('JobQueue', () => { class Queue extends JobQueue { parseData(data: unknown): TestJobData { - return testJobSchema.parse(data); + return parseUnknown(testJobSchema, data); } async run({ @@ -86,7 +87,7 @@ describe('JobQueue', () => { class Queue extends JobQueue { parseData(data: unknown): number { - return z.number().parse(data); + return parseUnknown(z.number(), data); } async run(): Promise { @@ -137,7 +138,7 @@ describe('JobQueue', () => { class Queue extends JobQueue { parseData(data: unknown): number { - return z.number().parse(data); + return parseUnknown(z.number(), data); } protected override getInMemoryQueue( @@ -180,7 +181,7 @@ describe('JobQueue', () => { class TestQueue extends JobQueue { parseData(data: unknown): string { - return z.string().parse(data); + return parseUnknown(z.string(), data); } async run(): Promise { @@ -248,7 +249,7 @@ describe('JobQueue', () => { class TestQueue extends JobQueue { parseData(data: unknown): string { - return z.string().parse(data); + return parseUnknown(z.string(), data); } async run(): Promise { @@ -353,7 +354,6 @@ describe('JobQueue', () => { // Chai's `assert.instanceOf` doesn't tell TypeScript anything, so we do it here. if (!(booErr instanceof JobError)) { assert.fail('Expected error to be a JobError'); - return; } assert.include(booErr.message, 'bar job always fails in this test'); @@ -367,7 +367,7 @@ describe('JobQueue', () => { class TestQueue extends JobQueue { parseData(data: unknown): string { - return z.string().parse(data); + return parseUnknown(z.string(), data); } async run( @@ -412,7 +412,7 @@ describe('JobQueue', () => { class TestQueue extends JobQueue { parseData(data: unknown): number { - return z.number().parse(data); + return parseUnknown(z.number(), data); } async run( @@ -490,7 +490,6 @@ describe('JobQueue', () => { // Chai's `assert.instanceOf` doesn't tell TypeScript anything, so we do it here. if (!(jobError instanceof JobError)) { assert.fail('Expected error to be a JobError'); - return; } assert.include( jobError.message, @@ -740,7 +739,7 @@ describe('JobQueue', () => { while (true) { // eslint-disable-next-line no-await-in-loop const [job] = await once(this.eventEmitter, 'drip'); - yield storedJobSchema.parse(job); + yield parseUnknown(storedJobSchema, job as unknown); } } @@ -766,7 +765,7 @@ describe('JobQueue', () => { class TestQueue extends JobQueue { parseData(data: unknown): number { - return z.number().parse(data); + return parseUnknown(z.number(), data); } async run({ diff --git a/ts/test-node/sql/migration_89_test.ts b/ts/test-node/sql/migration_89_test.ts index 4e34d4a577..36f5b6eb3c 100644 --- a/ts/test-node/sql/migration_89_test.ts +++ b/ts/test-node/sql/migration_89_test.ts @@ -22,6 +22,7 @@ import { getCallIdFromEra } from '../../util/callDisposition'; import { isValidUuid } from '../../util/isValidUuid'; import { createDB, updateToVersion } from './helpers'; import type { WritableDB, MessageType } from '../../sql/Interface'; +import { parsePartial } from '../../util/schemas'; describe('SQL/updateToSchemaVersion89', () => { let db: WritableDB; @@ -152,8 +153,8 @@ describe('SQL/updateToSchemaVersion89', () => { return db .prepare(selectHistoryQuery) .all() - .map(row => { - return callHistoryDetailsSchema.parse({ + .map((row: object) => { + return parsePartial(callHistoryDetailsSchema, { ...row, // Not present at the time of migration, but required by zod diff --git a/ts/test-node/util/schemas_test.ts b/ts/test-node/util/schemas_test.ts new file mode 100644 index 0000000000..c83be79a0e --- /dev/null +++ b/ts/test-node/util/schemas_test.ts @@ -0,0 +1,242 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { z } from 'zod'; +import assert from 'node:assert/strict'; +import { + parseLoose, + parsePartial, + parseStrict, + parseUnknown, + SchemaParseError, +} from '../../util/schemas'; + +describe('schemas', () => { + const schema = z.object({ prop: z.literal('value') }); + + it('rejects invalid inputs', () => { + function assertThrows(fn: () => void) { + assert.throws(fn, SchemaParseError); + } + + const input = { prop: 42 }; + // @ts-expect-error: not unknown + assertThrows(() => parseUnknown(schema, input)); + // @ts-expect-error: invalid type + assertThrows(() => parseStrict(schema, input)); + assertThrows(() => parseLoose(schema, input)); + // @ts-expect-error: invalid type + assertThrows(() => parsePartial(schema, input)); + }); + + it('accepts valid inputs', () => { + const valid = { prop: 'value' }; + + function assertShape(value: { prop: 'value' }) { + assert.deepEqual(value, valid); + } + + // unknown + { + const input = valid as unknown; + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + // @ts-expect-error: not loose + assertShape(parseLoose(schema, input)); + // @ts-expect-error: not partial + assertShape(parsePartial(schema, input)); + } + // any + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const input = valid as unknown as any; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + // @ts-expect-error: not loose + assertShape(parseLoose(schema, input)); + // @ts-expect-error: not partial + assertShape(parsePartial(schema, input)); + } + // {} + { + // eslint-disable-next-line @typescript-eslint/ban-types + const input = valid as unknown as {}; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + // @ts-expect-error: not loose + assertShape(parseLoose(schema, input)); + // @ts-expect-error: not partial + assertShape(parsePartial(schema, input)); + } + // never + { + const input = valid as unknown as never; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + // @ts-expect-error: not loose + assertShape(parseLoose(schema, input)); + // @ts-expect-error: not partial + assertShape(parsePartial(schema, input)); + } + // { prop: "value" } + { + const input = valid as { prop: 'value' }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + assertShape(parseStrict(schema, input)); + assertShape(parseLoose(schema, input)); + assertShape(parsePartial(schema, input)); + } + // { prop?: "value" } + { + const input = valid as { prop?: 'value' }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + // @ts-expect-error: not loose + assertShape(parseLoose(schema, input)); + assertShape(parsePartial(schema, input)); + } + // { prop: "value" | void } + { + const input = valid as { prop: 'value' | void }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + assertShape(parseLoose(schema, input)); + assertShape(parsePartial(schema, input)); + } + // { prop: "value" | undefined } + { + const input = valid as { prop: 'value' | undefined }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + assertShape(parseLoose(schema, input)); + assertShape(parsePartial(schema, input)); + } + // { prop: "value" | null } + { + const input = valid as { prop: 'value' | null }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + assertShape(parseLoose(schema, input)); + assertShape(parsePartial(schema, input)); + } + + // { prop: string } + { + const input = valid as { prop: string }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + assertShape(parseLoose(schema, input)); + // @ts-expect-error: not partial + assertShape(parsePartial(schema, input)); + } + // { prop?: string } + { + const input = valid as { prop?: string }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + // @ts-expect-error: not loose + assertShape(parseLoose(schema, input)); + // @ts-expect-error: not partial + assertShape(parsePartial(schema, input)); + } + // { prop: void } + { + const input = valid as unknown as { prop: void }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + assertShape(parseLoose(schema, input)); + assertShape(parsePartial(schema, input)); + } + // { prop: undefined } + { + const input = valid as unknown as { prop: undefined }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + assertShape(parseLoose(schema, input)); + assertShape(parsePartial(schema, input)); + } + // { prop: null } + { + const input = valid as unknown as { prop: null }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + assertShape(parseLoose(schema, input)); + assertShape(parsePartial(schema, input)); + } + // { prop: unknown } + { + const input = valid as { prop: unknown }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + assertShape(parseLoose(schema, input)); + // @ts-expect-error: not partial + assertShape(parsePartial(schema, input)); + } + // { prop: any } + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const input = valid as { prop: any }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // (ideally not allowed) + assertShape(parseStrict(schema, input)); + // (ideally not allowed) + assertShape(parseLoose(schema, input)); + // (ideally not allowed) + assertShape(parsePartial(schema, input)); + } + // { prop: {} } + { + // eslint-disable-next-line @typescript-eslint/ban-types + const input = valid as { prop: {} }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // @ts-expect-error: not strict + assertShape(parseStrict(schema, input)); + // (ideally not allowed) + assertShape(parseLoose(schema, input)); + // @ts-expect-error: not partial + assertShape(parsePartial(schema, input)); + } + // { prop: never } + { + const input = valid as { prop: never }; + // @ts-expect-error: not unknown + assertShape(parseUnknown(schema, input)); + // (ideally not allowed) + assertShape(parseStrict(schema, input)); + // (ideally not allowed) + assertShape(parseLoose(schema, input)); + // (ideally not allowed) + assertShape(parsePartial(schema, input)); + } + }); +}); diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index 2f294e3a8c..8ac6c93379 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -73,6 +73,7 @@ import { safeParseNumber } from '../util/numbers'; import { isStagingServer } from '../util/isStagingServer'; import type { IWebSocketResource } from './WebsocketResources'; import type { GroupSendToken } from '../types/GroupSendEndorsements'; +import { parseUnknown, safeParseUnknown } from '../util/schemas'; // Note: this will break some code that expects to be able to use err.response when a // web request fails, because it will force it to text. But it is very useful for @@ -1948,7 +1949,7 @@ export function initialize({ responseType: 'json', }); - return whoamiResultZod.parse(response); + return parseUnknown(whoamiResultZod, response); } async function sendChallengeResponse(challengeResponse: ChallengeType) { @@ -2027,7 +2028,7 @@ export function initialize({ httpType: 'GET', responseType: 'json', }); - const res = remoteConfigResponseZod.parse(rawRes); + const res = parseUnknown(remoteConfigResponseZod, rawRes); return { ...res, @@ -2157,7 +2158,7 @@ export function initialize({ responseType: 'json', }); - const result = verifyServiceIdResponse.safeParse(res); + const result = safeParseUnknown(verifyServiceIdResponse, res); if (result.success) { return result.data; @@ -2223,7 +2224,8 @@ export function initialize({ hash, }: GetAccountForUsernameOptionsType) { const hashBase64 = toWebSafeBase64(Bytes.toBase64(hash)); - return getAccountForUsernameResultZod.parse( + return parseUnknown( + getAccountForUsernameResultZod, await _ajax({ call: 'username', httpType: 'GET', @@ -2251,7 +2253,7 @@ export function initialize({ return; } - return uploadAvatarHeadersZod.parse(res); + return parseUnknown(uploadAvatarHeadersZod, res as unknown); } async function getProfileUnauth( @@ -2389,7 +2391,7 @@ export function initialize({ abortSignal, }); - return reserveUsernameResultZod.parse(response); + return parseUnknown(reserveUsernameResultZod, response); } async function confirmUsername({ hash, @@ -2408,14 +2410,15 @@ export function initialize({ responseType: 'json', abortSignal, }); - return confirmUsernameResultZod.parse(response); + return parseUnknown(confirmUsernameResultZod, response); } async function replaceUsernameLink({ encryptedUsername, keepLinkHandle, }: ReplaceUsernameLinkOptionsType): Promise { - return replaceUsernameLinkResultZod.parse( + return parseUnknown( + replaceUsernameLinkResultZod, await _ajax({ call: 'usernameLink', httpType: 'PUT', @@ -2440,7 +2443,8 @@ export function initialize({ async function resolveUsernameLink( serverId: string ): Promise { - return resolveUsernameLinkResultZod.parse( + return parseUnknown( + resolveUsernameLinkResultZod, await _ajax({ httpType: 'GET', call: 'usernameLink', @@ -2475,7 +2479,8 @@ export function initialize({ transport: VerificationTransport ) { // Create a new blank session using just a E164 - let session = verificationSessionZod.parse( + let session = parseUnknown( + verificationSessionZod, await _ajax({ call: 'verificationSession', httpType: 'POST', @@ -2490,7 +2495,8 @@ export function initialize({ ); // Submit a captcha solution to the session - session = verificationSessionZod.parse( + session = parseUnknown( + verificationSessionZod, await _ajax({ call: 'verificationSession', httpType: 'PATCH', @@ -2511,7 +2517,8 @@ export function initialize({ } // Request an SMS or Voice confirmation - session = verificationSessionZod.parse( + session = parseUnknown( + verificationSessionZod, await _ajax({ call: 'verificationSession', httpType: 'POST', @@ -2618,7 +2625,8 @@ export function initialize({ aciPqLastResortPreKey, pniPqLastResortPreKey, }: CreateAccountOptionsType) { - const session = verificationSessionZod.parse( + const session = parseUnknown( + verificationSessionZod, await _ajax({ isRegistration: true, call: 'verificationSession', @@ -2676,7 +2684,7 @@ export function initialize({ jsonData, }); - return createAccountResultZod.parse(responseJson); + return parseUnknown(createAccountResultZod, responseJson); } ); } @@ -2726,7 +2734,7 @@ export function initialize({ jsonData, }); - return linkDeviceResultZod.parse(responseJson); + return parseUnknown(linkDeviceResultZod, responseJson); } ); } @@ -2842,7 +2850,7 @@ export function initialize({ responseType: 'json', }); - return getBackupInfoResponseSchema.parse(res); + return parseUnknown(getBackupInfoResponseSchema, res); } async function getBackupStream({ @@ -2880,7 +2888,7 @@ export function initialize({ responseType: 'json', }); - return attachmentUploadFormResponse.parse(res); + return parseUnknown(attachmentUploadFormResponse, res); } function createFetchForAttachmentUpload({ @@ -2932,7 +2940,7 @@ export function initialize({ responseType: 'json', }); - return attachmentUploadFormResponse.parse(res); + return parseUnknown(attachmentUploadFormResponse, res); } async function refreshBackup(headers: BackupPresentationHeadersType) { @@ -2961,7 +2969,7 @@ export function initialize({ responseType: 'json', }); - return getBackupCredentialsResponseSchema.parse(res); + return parseUnknown(getBackupCredentialsResponseSchema, res); } async function getBackupCDNCredentials({ @@ -2979,7 +2987,7 @@ export function initialize({ responseType: 'json', }); - return getBackupCDNCredentialsResponseSchema.parse(res); + return parseUnknown(getBackupCDNCredentialsResponseSchema, res); } async function setBackupId({ @@ -3051,7 +3059,7 @@ export function initialize({ }, }); - return backupMediaBatchResponseSchema.parse(res); + return parseUnknown(backupMediaBatchResponseSchema, res); } async function backupDeleteMedia({ @@ -3099,7 +3107,7 @@ export function initialize({ urlParameters: `?${params.join('&')}`, }); - return backupListMediaResponseSchema.parse(res); + return parseUnknown(backupListMediaResponseSchema, res); } async function callLinkCreateAuth( @@ -3111,7 +3119,7 @@ export function initialize({ responseType: 'json', jsonData: { createCallLinkCredentialRequest: requestBase64 }, }); - return callLinkCreateAuthResponseSchema.parse(response); + return parseUnknown(callLinkCreateAuthResponseSchema, response); } async function setPhoneNumberDiscoverability(newValue: boolean) { @@ -3354,7 +3362,10 @@ export function initialize({ accessKey: accessKeys != null ? Bytes.toBase64(accessKeys) : undefined, groupSendToken, }); - const parseResult = multiRecipient200ResponseSchema.safeParse(response); + const parseResult = safeParseUnknown( + multiRecipient200ResponseSchema, + response + ); if (parseResult.success) { return parseResult.data; } @@ -3490,8 +3501,10 @@ export function initialize({ urlParameters: `/${encryptedStickers.length}`, }); - const { packId, manifest, stickers } = - StickerPackUploadFormSchema.parse(formJson); + const { packId, manifest, stickers } = parseUnknown( + StickerPackUploadFormSchema, + formJson + ); // Upload manifest const manifestParams = makePutParams(manifest, encryptedManifest); @@ -3718,7 +3731,8 @@ export function initialize({ } async function getAttachmentUploadForm() { - return attachmentUploadFormResponse.parse( + return parseUnknown( + attachmentUploadFormResponse, await _ajax({ call: 'attachmentUploadForm', httpType: 'GET', diff --git a/ts/textsecure/WebsocketResources.ts b/ts/textsecure/WebsocketResources.ts index b7f4e59c7b..8223abcb36 100644 --- a/ts/textsecure/WebsocketResources.ts +++ b/ts/textsecure/WebsocketResources.ts @@ -62,6 +62,7 @@ import { ToastType } from '../types/Toast'; import { AbortableProcess } from '../util/AbortableProcess'; import type { WebAPICredentials } from './Types'; import { NORMAL_DISCONNECT_CODE } from './SocketManager'; +import { parseUnknown } from '../util/schemas'; const THIRTY_SECONDS = 30 * durations.SECOND; @@ -107,7 +108,7 @@ export namespace AggregatedStats { try { const json = localStorage.getItem(key); return json != null - ? AggregatedStatsSchema.parse(JSON.parse(json)) + ? parseUnknown(AggregatedStatsSchema, JSON.parse(json) as unknown) : createEmpty(); } catch (error) { log.warn( diff --git a/ts/types/CallLink.ts b/ts/types/CallLink.ts index d7862241b0..584b87557a 100644 --- a/ts/types/CallLink.ts +++ b/ts/types/CallLink.ts @@ -6,6 +6,7 @@ import type { ConversationType } from '../state/ducks/conversations'; import { safeParseInteger } from '../util/numbers'; import { byteLength } from '../Bytes'; import type { StorageServiceFieldsType } from '../sql/Interface'; +import { parsePartial } from '../util/schemas'; export enum CallLinkUpdateSyncType { Update = 'Update', @@ -44,7 +45,10 @@ export const callLinkRestrictionsSchema = z.nativeEnum(CallLinkRestrictions); export function toCallLinkRestrictions( restrictions: number | string ): CallLinkRestrictions { - return callLinkRestrictionsSchema.parse(safeParseInteger(restrictions)); + return parsePartial( + callLinkRestrictionsSchema, + safeParseInteger(restrictions) + ); } /** diff --git a/ts/types/GroupSendEndorsements.ts b/ts/types/GroupSendEndorsements.ts index 271f4d1cd8..fc04fdcaad 100644 --- a/ts/types/GroupSendEndorsements.ts +++ b/ts/types/GroupSendEndorsements.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { aciSchema, type AciString } from './ServiceId'; import * as Bytes from '../Bytes'; +import { parseStrict } from '../util/schemas'; const GROUPV2_ID_LENGTH = 32; // 32 bytes @@ -94,5 +95,5 @@ export const groupSendTokenSchema = z export type GroupSendToken = z.infer; export function toGroupSendToken(token: Uint8Array): GroupSendToken { - return groupSendTokenSchema.parse(token); + return parseStrict(groupSendTokenSchema, token); } diff --git a/ts/util/callDisposition.ts b/ts/util/callDisposition.ts index c42d4bf506..6bd7633b14 100644 --- a/ts/util/callDisposition.ts +++ b/ts/util/callDisposition.ts @@ -68,6 +68,7 @@ import { drop } from './drop'; import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync'; import { storageServiceUploadJob } from '../services/storage'; import { CallLinkDeleteManager } from '../jobs/CallLinkDeleteManager'; +import { parsePartial, parseStrict } from './schemas'; // utils // ----- @@ -200,7 +201,7 @@ export function getCallEventForProto( callEventProto: Proto.SyncMessage.ICallEvent, eventSource: string ): CallEventDetails { - const callEvent = callEventNormalizeSchema.parse(callEventProto); + const callEvent = parsePartial(callEventNormalizeSchema, callEventProto); const { callId, peerId, timestamp } = callEvent; let type: CallType; @@ -251,7 +252,7 @@ export function getCallEventForProto( throw new TypeError(`Unknown call event ${callEvent.event}`); } - return callEventDetailsSchema.parse({ + return parseStrict(callEventDetailsSchema, { callId, peerId, ringerId: null, @@ -279,7 +280,10 @@ const callLogEventFromProto: Partial< export function getCallLogEventForProto( callLogEventProto: Proto.SyncMessage.ICallLogEvent ): CallLogEventDetails { - const callLogEvent = callLogEventNormalizeSchema.parse(callLogEventProto); + const callLogEvent = parsePartial( + callLogEventNormalizeSchema, + callLogEventProto + ); const type = callLogEventFromProto[callLogEvent.type]; if (type == null) { @@ -496,7 +500,7 @@ export function getCallDetailsFromDirectCall( call: Call ): CallDetails { const ringerId = call.isIncoming ? call.remoteUserId : null; - return callDetailsSchema.parse({ + return parseStrict(callDetailsSchema, { callId: Long.fromValue(call.callId).toString(), peerId, ringerId, @@ -518,7 +522,7 @@ export function getCallDetailsFromEndedDirectCall( wasVideoCall: boolean, timestamp: number ): CallDetails { - return callDetailsSchema.parse({ + return parseStrict(callDetailsSchema, { callId, peerId, ringerId, @@ -535,7 +539,7 @@ export function getCallDetailsFromGroupCallMeta( peerId: AciString | string, groupCallMeta: GroupCallMeta ): CallDetails { - return callDetailsSchema.parse({ + return parseStrict(callDetailsSchema, { callId: groupCallMeta.callId, peerId, ringerId: groupCallMeta.ringerId, @@ -552,7 +556,7 @@ export function getCallDetailsForAdhocCall( peerId: AciString | string, callId: string ): CallDetails { - return callDetailsSchema.parse({ + return parseStrict(callDetailsSchema, { callId, peerId, ringerId: null, @@ -575,7 +579,11 @@ export function getCallEventDetails( event: LocalCallEvent, eventSource: string ): CallEventDetails { - return callEventDetailsSchema.parse({ ...callDetails, event, eventSource }); + return parseStrict(callEventDetailsSchema, { + ...callDetails, + event, + eventSource, + }); } // transitions @@ -646,7 +654,7 @@ export function transitionCallHistory( `transitionCallHistory: Transitioned call history timestamp (before: ${callHistory?.timestamp}, after: ${timestamp})` ); - return callHistoryDetailsSchema.parse({ + return parseStrict(callHistoryDetailsSchema, { callId, peerId, ringerId, diff --git a/ts/util/callLinksRingrtc.ts b/ts/util/callLinksRingrtc.ts index 20dfdcee63..3e950350fb 100644 --- a/ts/util/callLinksRingrtc.ts +++ b/ts/util/callLinksRingrtc.ts @@ -33,6 +33,7 @@ import { getKeyFromCallLink, toAdminKeyBytes, } from './callLinks'; +import { parseStrict } from './schemas'; /** * RingRTC conversions @@ -56,7 +57,7 @@ const RingRTCCallLinkRestrictionsSchema = z.nativeEnum( export function callLinkRestrictionsToRingRTC( restrictions: CallLinkRestrictions ): RingRTCCallLinkRestrictions { - return RingRTCCallLinkRestrictionsSchema.parse(restrictions); + return parseStrict(RingRTCCallLinkRestrictionsSchema, restrictions); } export function getRoomIdFromRootKey(rootKey: CallLinkRootKey): string { @@ -152,7 +153,7 @@ export function callLinkToRecord(callLink: CallLinkType): CallLinkRecord { const adminKey = callLink.adminKey ? toAdminKeyBytes(callLink.adminKey) : null; - return callLinkRecordSchema.parse({ + return parseStrict(callLinkRecordSchema, { roomId: callLink.roomId, rootKey, adminKey, diff --git a/ts/util/groupSendEndorsements.ts b/ts/util/groupSendEndorsements.ts index 3dc16213de..56d71c1da7 100644 --- a/ts/util/groupSendEndorsements.ts +++ b/ts/util/groupSendEndorsements.ts @@ -27,6 +27,7 @@ import { ToastType } from '../types/Toast'; import * as Errors from '../types/errors'; import { isTestOrMockEnvironment } from '../environment'; import { isAlpha } from './version'; +import { parseStrict } from './schemas'; export function decodeGroupSendEndorsementResponse({ groupId, @@ -91,7 +92,7 @@ export function decodeGroupSendEndorsementResponse({ `decodeGroupSendEndorsementResponse: Received endorsements (group: ${idForLogging}, expiration: ${expiration}, members: ${groupMembers.length})` ); - const groupEndorsementsData: GroupSendEndorsementsData = { + return parseStrict(groupSendEndorsementsDataSchema, { combinedEndorsement: { groupId, expiration, @@ -110,9 +111,7 @@ export function decodeGroupSendEndorsementResponse({ endorsement: endorsement.getContents(), }; }), - }; - - return groupSendEndorsementsDataSchema.parse(groupEndorsementsData); + }); } const TWO_DAYS = DurationInSeconds.fromDays(2); diff --git a/ts/util/retryPlaceholders.ts b/ts/util/retryPlaceholders.ts index 9440b104db..bbbc894ac7 100644 --- a/ts/util/retryPlaceholders.ts +++ b/ts/util/retryPlaceholders.ts @@ -5,6 +5,7 @@ import { z } from 'zod'; import { groupBy } from 'lodash'; import * as log from '../logging/log'; import { aciSchema } from '../types/ServiceId'; +import { safeParseStrict } from './schemas'; const retryItemSchema = z .object({ @@ -53,7 +54,8 @@ export class RetryPlaceholders { ); } - const parsed = retryItemListSchema.safeParse( + const parsed = safeParseStrict( + retryItemListSchema, window.storage.get(STORAGE_KEY, new Array()) ); if (!parsed.success) { @@ -104,7 +106,7 @@ export class RetryPlaceholders { // Basic data management async add(item: RetryItemType): Promise { - const parsed = retryItemSchema.safeParse(item); + const parsed = safeParseStrict(retryItemSchema, item); if (!parsed.success) { throw new Error( `RetryPlaceholders.add: Item did not match schema ${JSON.stringify( diff --git a/ts/util/schemas.ts b/ts/util/schemas.ts new file mode 100644 index 0000000000..9eb8206423 --- /dev/null +++ b/ts/util/schemas.ts @@ -0,0 +1,179 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { + IfAny, + IfEmptyObject, + IfNever, + IfUnknown, + IsLiteral, + LiteralToPrimitive, + Primitive, +} from 'type-fest'; +import type { SafeParseReturnType, ZodError, ZodType, ZodTypeDef } from 'zod'; + +type Schema = ZodType; +type SafeResult = SafeParseReturnType; + +type LooseInput = + IsLiteral extends true ? LiteralToPrimitive : Record; + +type PartialInput = T extends Primitive + ? T | null | void + : { [Key in keyof T]?: T[Key] | null | void }; + +export class SchemaParseError extends TypeError { + constructor(schema: Schema, error: ZodError) { + let message = 'zod: issues found when parsing with schema'; + if (schema.description) { + message += ` (${schema.description})`; + } + message += ':'; + for (const issue of error.issues) { + message += `\n - ${issue.path.join('.')}: ${issue.message}`; + } + super(message); + } +} + +function parse( + schema: Schema, + input: unknown +): Output { + const result = schema.safeParse(input); + if (result.success) { + return result.data; + } + throw new SchemaParseError(schema, result.error); +} + +function safeParse( + schema: Schema, + input: unknown +): SafeResult { + return schema.safeParse(input); +} + +/** + * This uses type-fest to validate that the data being passed into parse() and + * safeParse() is not types like `any`, `{}`, `never`, or an unexpected `unknown`. + * + * `never` is hard to prevent from being passed in, so instead we make the function + * arguments themselves not constructable using an intersection with a warning. + */ + +// Must be exactly `unknown` +type UnknownArgs = + IfAny extends true + ? [data: Data] & 'Unexpected input `any` must be `unknown`' + : IfNever extends true + ? [data: Data] & 'Unexpected input `never` must be `unknown`' + : IfEmptyObject extends true + ? [data: Data] & 'Unexpected input `{}` must be `unknown`' + : IfUnknown extends true + ? [data: Data] + : [data: Data] & 'Unexpected input type must be `unknown`'; + +type TypedArgs = + IfAny extends true + ? [data: Data] & 'Unexpected input `any` must be typed' + : IfNever extends true + ? [data: Data] & 'Unexpected input `never` must be typed' + : IfEmptyObject extends true + ? [data: Data] & 'Unexpected input `{}` must be typed' + : IfUnknown extends true + ? [data: Data] & 'Unexpected input `unknown` must be typed' + : [data: Data]; + +// prettier-ignore +type ParseUnknown = (schema: Schema, ...args: UnknownArgs) => Output; +// prettier-ignore +type SafeParseUnknown = (schema: Schema, ...args: UnknownArgs) => SafeResult; +// prettier-ignore +type ParseStrict = (schema: Schema, ...args: TypedArgs) => Output; +// prettier-ignore +type SafeParseStrict = (schema: Schema, ...args: TypedArgs) => SafeResult; +// prettier-ignore +type ParseLoose = >(schema: Schema, ...args: TypedArgs) => Output; +// prettier-ignore +type SafeParseLoose = >(schema: Schema, ...args: TypedArgs) => SafeResult; +// prettier-ignore +type ParsePartial = >(schema: Schema, ...args: TypedArgs) => Output; +// prettier-ignore +type SafeParsePartial = >(schema: Schema, ...args: TypedArgs) => SafeResult; + +/** + * Parse an *unknown* value with a zod schema. + * ```ts + * type Input = unknown // unknown + * type Output = { prop: string } + * ``` + * @throws {SchemaParseError} + */ +export const parseUnknown: ParseUnknown = parse; + +/** + * Safely parse an *unknown* value with a zod schema. + * ```ts + * type Input = unknown // unknown + * type Output = { success: true, error: null, data: { prop: string } } + * ``` + */ +export const safeParseUnknown: SafeParseUnknown = safeParse; + +/** + * Parse a *strict* value with a zod schema. + * ```ts + * type Input = { prop: string } // strict + * type Output = { prop: string } + * ``` + * @throws {SchemaParseError} + */ +export const parseStrict: ParseStrict = parse; + +/** + * Safely parse a *strict* value with a zod schema. + * ```ts + * type Input = { prop: string } // strict + * type Output = { success: true, error: null, data: { prop: string } } + * ``` + */ +export const safeParseStrict: SafeParseStrict = safeParse; + +/** + * Parse a *loose* value with a zod schema. + * ```ts + * type Input = { prop: unknown } // loose + * type Output = { prop: string } + * ``` + * @throws {SchemaParseError} + */ +export const parseLoose: ParseLoose = parse; + +/** + * Safely parse a *loose* value with a zod schema. + * ```ts + * type Input = { prop: unknown } // loose + * type Output = { success: true, error: null, data: { prop: string } } + * ``` + */ +export const safeParseLoose: SafeParseLoose = safeParse; + +/** + * Parse a *partial* value with a zod schema. + * ```ts + * type Input = { prop?: string | null | undefined } // partial + * type Output = { prop: string } + * ``` + * @throws {SchemaParseError} + */ +export const parsePartial: ParsePartial = parse; + +/** + * Safely parse a *partial* value with a zod schema. + * ```ts + * type Input = { prop?: string | null | undefined } // partial + * type Output = { success: true, error: null, data: { prop: string } } + * ``` + */ +export const safeParsePartial: SafeParsePartial = safeParse; diff --git a/ts/util/sendToGroup.ts b/ts/util/sendToGroup.ts index 3964c9b0cf..0dec04728d 100644 --- a/ts/util/sendToGroup.ts +++ b/ts/util/sendToGroup.ts @@ -73,6 +73,7 @@ import { import { maybeUpdateGroup } from '../groups'; import type { GroupSendToken } from '../types/GroupSendEndorsements'; import { isAciString } from './isAciString'; +import { safeParseStrict, safeParseUnknown } from './schemas'; const UNKNOWN_RECIPIENT = 404; const INCORRECT_AUTH_KEY = 401; @@ -603,7 +604,7 @@ export async function sendToGroupViaSenderKey( { online, story, urgent } ); - const parsed = multiRecipient200ResponseSchema.safeParse(result); + const parsed = safeParseStrict(multiRecipient200ResponseSchema, result); if (parsed.success) { const { uuids404 } = parsed.data; if (uuids404 && uuids404.length > 0) { @@ -1022,7 +1023,10 @@ async function handle409Response( error: HTTPError ) { const logId = sendTarget.idForLogging(); - const parsed = multiRecipient409ResponseSchema.safeParse(error.response); + const parsed = safeParseUnknown( + multiRecipient409ResponseSchema, + error.response + ); if (parsed.success) { await waitForAll({ tasks: parsed.data.map(item => async () => { @@ -1068,7 +1072,10 @@ async function handle410Response( ) { const logId = sendTarget.idForLogging(); - const parsed = multiRecipient410ResponseSchema.safeParse(error.response); + const parsed = safeParseUnknown( + multiRecipient410ResponseSchema, + error.response + ); if (parsed.success) { await waitForAll({ tasks: parsed.data.map(item => async () => { diff --git a/ts/util/signalRoutes.ts b/ts/util/signalRoutes.ts index 65d4786e14..2a357210bb 100644 --- a/ts/util/signalRoutes.ts +++ b/ts/util/signalRoutes.ts @@ -8,6 +8,7 @@ import { z } from 'zod'; import { strictAssert } from './assert'; import * as log from '../logging/log'; import * as Errors from '../types/errors'; +import { parsePartial, parseUnknown, safeParseUnknown } from './schemas'; function toUrl(input: URL | string): URL | null { if (input instanceof URL) { @@ -164,7 +165,10 @@ function _route( ); return null; } - const parseResult = config.schema.safeParse(rawArgs); + const parseResult = safeParseUnknown( + config.schema, + rawArgs as unknown + ); if (parseResult.success) { const args = parseResult.data; return { @@ -183,13 +187,13 @@ function _route( }, toWebUrl(args) { if (config.toWebUrl) { - return config.toWebUrl(config.schema.parse(args)); + return config.toWebUrl(parseUnknown(config.schema, args as unknown)); } throw new Error('Route does not support web URLs'); }, toAppUrl(args) { if (config.toAppUrl) { - return config.toAppUrl(config.schema.parse(args)); + return config.toAppUrl(parseUnknown(config.schema, args as unknown)); } throw new Error('Route does not support app URLs'); }, @@ -219,7 +223,7 @@ export const contactByPhoneNumberRoute = _route('contactByPhoneNumber', { }), parse(result) { return { - phoneNumber: paramSchema.parse(result.hash.groups.phoneNumber), + phoneNumber: parsePartial(paramSchema, result.hash.groups.phoneNumber), }; }, toWebUrl(args) { diff --git a/ts/util/syncTasks.ts b/ts/util/syncTasks.ts index 4a5a003d3b..ec1764dd12 100644 --- a/ts/util/syncTasks.ts +++ b/ts/util/syncTasks.ts @@ -30,6 +30,7 @@ import { onSync as onViewSync, viewSyncTaskSchema, } from '../messageModifiers/ViewSyncs'; +import { safeParseUnknown } from './schemas'; const syncTaskDataSchema = z.union([ deleteMessageSchema, @@ -86,7 +87,7 @@ export async function queueSyncTasks( await removeSyncTaskById(id); return; } - const parseResult = syncTaskDataSchema.safeParse(data); + const parseResult = safeParseUnknown(syncTaskDataSchema, data); if (!parseResult.success) { log.error( `${innerLogId}: Failed to parse. Deleting. Error: ${parseResult.error}` diff --git a/ts/windows/minimalContext.ts b/ts/windows/minimalContext.ts index 98341aaa2c..0301f7e3f7 100644 --- a/ts/windows/minimalContext.ts +++ b/ts/windows/minimalContext.ts @@ -20,6 +20,7 @@ import { } from '../context/localeMessages'; import { waitForSettingsChange } from '../context/waitForSettingsChange'; import { isTestOrMockEnvironment } from '../environment'; +import { parseUnknown } from '../util/schemas'; const emojiListCache = new Map(); @@ -55,8 +56,8 @@ export const MinimalSignalContext: MinimalSignalContextType = { 'OptionalResourceService:getData', `emoji-index-${locale}.json` ); - const json = JSON.parse(Buffer.from(buf).toString()); - const result = LocaleEmojiListSchema.parse(json); + const json: unknown = JSON.parse(Buffer.from(buf).toString()); + const result = parseUnknown(LocaleEmojiListSchema, json); emojiListCache.set(locale, result); return result; },