diff --git a/keyserver/src/creators/message-creator.js b/keyserver/src/creators/message-creator.js --- a/keyserver/src/creators/message-creator.js +++ b/keyserver/src/creators/message-creator.js @@ -556,15 +556,19 @@ } } +type LatestMessagePerThread = { + +latestMessage: string, + +latestReadMessage?: string, +}; function determineLatestMessagesPerThread( preUserPushInfo: UserThreadInfo, userID: string, threadsToMessageIndices: $ReadOnlyMap>, messageInfos: $ReadOnlyArray, -) { +): $ReadOnlyMap { const { threadIDs, notFocusedThreadIDs, subthreadsCanSetToUnread } = preUserPushInfo; - const latestMessagesPerThread = new Map(); + const latestMessagesPerThread = new Map(); for (const threadID of threadIDs) { const messageIndices = threadsToMessageIndices.get(threadID); invariant(messageIndices, `indices should exist for thread ${threadID}`); diff --git a/keyserver/src/creators/report-creator.js b/keyserver/src/creators/report-creator.js --- a/keyserver/src/creators/report-creator.js +++ b/keyserver/src/creators/report-creator.js @@ -192,7 +192,7 @@ first: { +[id: string]: O }, second: { +[id: string]: O }, ): Set { - const nonMatchingIDs = new Set(); + const nonMatchingIDs = new Set(); for (const id in first) { if (!_isEqual(first[id])(second[id])) { nonMatchingIDs.add(id); diff --git a/keyserver/src/creators/update-creator.js b/keyserver/src/creators/update-creator.js --- a/keyserver/src/creators/update-creator.js +++ b/keyserver/src/creators/update-creator.js @@ -409,8 +409,10 @@ ({ currentUser }) => currentUser, ); const threadIDsNeedingFetch = viewerInfo.threadInfos - ? new Set() - : new Set(entitiesToFetch.map(({ threadID }) => threadID).filter(Boolean)); + ? new Set() + : new Set( + entitiesToFetch.map(({ threadID }) => threadID).filter(Boolean), + ); const entryIDsNeedingFetch = new Set( entitiesToFetch.map(({ entryID }) => entryID).filter(Boolean), ); diff --git a/keyserver/src/cron/update-geoip-db.js b/keyserver/src/cron/update-geoip-db.js --- a/keyserver/src/cron/update-geoip-db.js +++ b/keyserver/src/cron/update-geoip-db.js @@ -8,8 +8,9 @@ import { handleAsyncPromise } from '../responders/handlers.js'; +type GeoIPLicenseConfig = { +key: string }; async function updateGeoipDB(): Promise { - const geoipLicense = await getCommConfig({ + const geoipLicense = await getCommConfig({ folder: 'secrets', name: 'geoip_license', }); @@ -20,7 +21,7 @@ await spawnUpdater(geoipLicense); } -function spawnUpdater(geoipLicense: { key: string }): Promise { +function spawnUpdater(geoipLicense: GeoIPLicenseConfig): Promise { const spawned = childProcess.spawn(process.execPath, [ '../node_modules/geoip-lite/scripts/updatedb.js', `license_key=${geoipLicense.key}`, diff --git a/keyserver/src/database/db-config.js b/keyserver/src/database/db-config.js --- a/keyserver/src/database/db-config.js +++ b/keyserver/src/database/db-config.js @@ -49,7 +49,7 @@ dbType: assertValidDBType(process.env.COMM_DATABASE_TYPE), }; } else { - const importedDBConfig = await getCommConfig({ + const importedDBConfig = await getCommConfig({ folder: 'secrets', name: 'db_config', }); diff --git a/keyserver/src/database/migration-config.js b/keyserver/src/database/migration-config.js --- a/keyserver/src/database/migration-config.js +++ b/keyserver/src/database/migration-config.js @@ -535,7 +535,7 @@ WHERE m1.type = ${SIDEBAR_SOURCE} AND m2.type = ${TOGGLE_PIN} `); - const threadIDs = new Set(); + const threadIDs = new Set(); for (const row of result) { threadIDs.add(row.thread.toString()); } diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -200,7 +200,10 @@ uploadMediaMetadataInputValidator, } from './uploads/uploads.js'; -const ignoredArgumentValidator = t.irreducible('Ignored argument', () => true); +const ignoredArgumentValidator = t.irreducible( + 'Ignored argument', + () => true, +); const jsonEndpoints: { [id: Endpoint]: JSONResponder } = { create_account: createJSONResponder( diff --git a/keyserver/src/fetchers/message-fetchers.js b/keyserver/src/fetchers/message-fetchers.js --- a/keyserver/src/fetchers/message-fetchers.js +++ b/keyserver/src/fetchers/message-fetchers.js @@ -348,7 +348,7 @@ const messages = await parseMessageSQLResult(result, derivedMessages, viewer); const rawMessageInfos = []; - const threadToMessageCount = new Map(); + const threadToMessageCount = new Map(); for (const message of messages) { const { rawMessageInfo } = message; rawMessageInfos.push(rawMessageInfo); @@ -844,7 +844,7 @@ `; const [result] = await dbQuery(latestEditedMessageQuery); - const latestContentByID = new Map(); + const latestContentByID = new Map(); for (const row of result) { if (!row.content) { continue; diff --git a/keyserver/src/fetchers/thread-permission-fetchers.js b/keyserver/src/fetchers/thread-permission-fetchers.js --- a/keyserver/src/fetchers/thread-permission-fetchers.js +++ b/keyserver/src/fetchers/thread-permission-fetchers.js @@ -142,8 +142,8 @@ viewer: Viewer, permissionsToCheck: $ReadOnlyArray, threadIDs: $ReadOnlyArray, -) { - const threadIDsWithDisabledPermissions = new Set(); +): Promise<$ReadOnlySet> { + const threadIDsWithDisabledPermissions = new Set(); const permissionMightBeDisabled = permissionsToCheck.some(permission => permissionsDisabledByBlock.has(permission), @@ -198,6 +198,8 @@ // It doesn't matter what value we pass in, as long as it's positive. const arbitraryPositiveRole = '1'; +type ContainingStatus = 'member' | 'non-member' | 'no-containing-thread'; + type CandidateMembers = { +[key: string]: ?$ReadOnlyArray, }; @@ -216,7 +218,7 @@ ): Promise { const requireRelationship = options?.requireRelationship ?? true; - const allCandidatesSet = new Set(); + const allCandidatesSet = new Set(); for (const key in candidates) { const candidateGroup = candidates[key]; if (!candidateGroup) { @@ -248,9 +250,9 @@ })(); const memberOfContainingThreadPromise: Promise< - Map, + Map, > = (async () => { - const results = new Map(); + const results = new Map(); if (allCandidates.length === 0) { return results; } @@ -282,7 +284,7 @@ memberOfContainingThreadPromise, ]); - const ignoreMembers = new Set(); + const ignoreMembers = new Set(); for (const memberID of allCandidates) { const member = fetchedMembers[memberID]; if (!member && requireRelationship) { diff --git a/keyserver/src/push/providers.js b/keyserver/src/push/providers.js --- a/keyserver/src/push/providers.js +++ b/keyserver/src/push/providers.js @@ -27,14 +27,26 @@ return codeVersion && codeVersion >= 87 ? 'comm_fcm_config' : 'fcm_config'; } -const cachedAPNProviders = new Map(); +type APNConfig = { + +token: { + +key: string, + +keyId: string, + +teamId: string, + }, + +production: boolean, +}; + +const cachedAPNProviders = new Map(); async function getAPNProvider(profile: APNPushProfile): Promise { const provider = cachedAPNProviders.get(profile); if (provider !== undefined) { return provider; } try { - const apnConfig = await getCommConfig({ folder: 'secrets', name: profile }); + const apnConfig = await getCommConfig({ + folder: 'secrets', + name: profile, + }); invariant(apnConfig, `APN config missing for ${profile}`); if (!cachedAPNProviders.has(profile)) { cachedAPNProviders.set(profile, new apn.Provider(apnConfig)); @@ -47,14 +59,30 @@ return cachedAPNProviders.get(profile); } -const cachedFCMProviders = new Map(); +type FCMConfig = { + +type: string, + +project_id: string, + +private_key_id: string, + +private_key: string, + +client_email: string, + +client_id: string, + +auth_uri: string, + +token_uri: string, + +auth_provider_x509_cert_url: string, + +client_x509_cert_url: string, +}; + +const cachedFCMProviders = new Map(); async function getFCMProvider(profile: FCMPushProfile): Promise { const provider = cachedFCMProviders.get(profile); if (provider !== undefined) { return provider; } try { - const fcmConfig = await getCommConfig({ folder: 'secrets', name: profile }); + const fcmConfig = await getCommConfig({ + folder: 'secrets', + name: profile, + }); invariant(fcmConfig, `FCM config missed for ${profile}`); if (!cachedFCMProviders.has(profile)) { cachedFCMProviders.set( diff --git a/keyserver/src/push/rescind.js b/keyserver/src/push/rescind.js --- a/keyserver/src/push/rescind.js +++ b/keyserver/src/push/rescind.js @@ -72,7 +72,7 @@ fetchQuery.append(SQL` GROUP BY n.id, m.user`); const [fetchResult] = await dbQuery(fetchQuery); - const allDeviceTokens = new Set(); + const allDeviceTokens = new Set(); const parsedDeliveries: { [string]: $ReadOnlyArray } = {}; for (const row of fetchResult) { diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js --- a/keyserver/src/push/send.js +++ b/keyserver/src/push/send.js @@ -659,8 +659,8 @@ async function fetchInfos(pushInfo: PushInfo) { const usersToCollapsableNotifInfo = await fetchCollapsableNotifs(pushInfo); - const threadIDs = new Set(); - const threadWithChangedNamesToMessages = new Map(); + const threadIDs = new Set(); + const threadWithChangedNamesToMessages = new Map>(); const addThreadIDsFromMessageInfos = (rawMessageInfo: RawMessageInfo) => { const threadID = rawMessageInfo.threadID; threadIDs.add(threadID); @@ -756,7 +756,7 @@ serverThreadInfos: { +[threadID: string]: ServerThreadInfo }, usersToCollapsableNotifInfo: { +[userID: string]: CollapsableNotifInfo[] }, ) { - const missingUserIDs = new Set(); + const missingUserIDs = new Set(); for (const threadID in serverThreadInfos) { const serverThreadInfo = serverThreadInfos[threadID]; @@ -815,11 +815,14 @@ function getDevicesByPlatform( devices: $ReadOnlyArray, ): Map>> { - const byPlatform = new Map(); + const byPlatform = new Map< + Platform, + Map>, + >(); for (const device of devices) { let innerMap = byPlatform.get(device.platform); if (!innerMap) { - innerMap = new Map(); + innerMap = new Map>(); byPlatform.set(device.platform, innerMap); } const codeVersion: number = @@ -1520,7 +1523,7 @@ selectQuery.append(sqlCondition); const [result] = await dbQuery(selectQuery); - const userCookiePairsToInvalidDeviceTokens = new Map(); + const userCookiePairsToInvalidDeviceTokens = new Map>(); for (const row of result) { const userCookiePair = `${row.user}|${row.id}`; const existing = userCookiePairsToInvalidDeviceTokens.get(userCookiePair); diff --git a/keyserver/src/responders/comm-landing-responders.js b/keyserver/src/responders/comm-landing-responders.js --- a/keyserver/src/responders/comm-landing-responders.js +++ b/keyserver/src/responders/comm-landing-responders.js @@ -9,7 +9,7 @@ import { sendEmailSubscriptionRequestToAshoat } from '../emails/subscribe-email-updates.js'; import { checkInputValidator } from '../utils/validation-utils.js'; -const emailSubscriptionInputValidator = tShape({ +const emailSubscriptionInputValidator = tShape({ email: tEmail, }); diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js --- a/keyserver/src/responders/user-responders.js +++ b/keyserver/src/responders/user-responders.js @@ -3,7 +3,7 @@ import type { Utility as OlmUtility } from '@commapp/olm'; import invariant from 'invariant'; import { ErrorTypes, SiweMessage } from 'siwe'; -import t, { type TInterface, type TUnion } from 'tcomb'; +import t, { type TInterface, type TUnion, type TEnums } from 'tcomb'; import bcrypt from 'twin-bcrypt'; import { @@ -40,6 +40,7 @@ IdentityKeysBlob, SignedIdentityKeysBlob, } from 'lib/types/crypto-types.js'; +import type { DeviceType } from 'lib/types/device-types'; import { type CalendarQuery, rawEntryInfoValidator, @@ -221,10 +222,15 @@ return result; } -const deviceTokenUpdateRequestInputValidator = tShape({ - deviceType: t.maybe(t.enums.of(['ios', 'android'])), - deviceToken: t.String, -}); +type OldDeviceTokenUpdateRequest = { + +deviceType?: ?DeviceType, + +deviceToken: string, +}; +const deviceTokenUpdateRequestInputValidator = + tShape({ + deviceType: t.maybe(t.enums.of(['ios', 'android'])), + deviceToken: t.String, + }); export const registerRequestInputValidator: TInterface = tShape({ @@ -434,7 +440,7 @@ threadInfos: t.dict(tID, rawThreadInfoValidator), userInfos: t.list(userInfoValidator), }), - notAcknowledgedPolicies: t.maybe(t.list(policyTypeValidator)), + notAcknowledgedPolicies: t.maybe(t.list(policyTypeValidator)), }); async function logInResponder( diff --git a/keyserver/src/scripts/merge-users.js b/keyserver/src/scripts/merge-users.js --- a/keyserver/src/scripts/merge-users.js +++ b/keyserver/src/scripts/merge-users.js @@ -49,8 +49,8 @@ ({ sql: updateUserRowQuery, updateDatas } = replaceUserResult); } - const usersGettingUpdate = new Set(); - const usersNeedingUpdate = new Set(); + const usersGettingUpdate = new Set(); + const usersNeedingUpdate = new Set(); const needUserInfoUpdate = replaceUserInfo && replaceUserInfo.username; const setGettingUpdate = (threadInfo: ServerThreadInfo) => { if (!needUserInfoUpdate) { diff --git a/keyserver/src/scripts/validate-role-permissions.js b/keyserver/src/scripts/validate-role-permissions.js --- a/keyserver/src/scripts/validate-role-permissions.js +++ b/keyserver/src/scripts/validate-role-permissions.js @@ -8,6 +8,7 @@ import { configurableCommunityPermissions, userSurfacedPermissions, + type UserSurfacedPermission, } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { deepDiff, values } from 'lib/utils/objects.js'; @@ -106,8 +107,10 @@ // discrepancies that could be linked to a specific user-surfaced // permission. This could be useful in manually parsing through the // script results to 'write off' discrepancies as user role edits. - const userSurfacedExpectedPermissionsToExistingPermissions = new Set(); - const userSurfacedExistingPermissionsToExpectedPermissions = new Set(); + const userSurfacedExpectedPermissionsToExistingPermissions = + new Set(); + const userSurfacedExistingPermissionsToExpectedPermissions = + new Set(); for (const permission of values(userSurfacedPermissions)) { const permissionSet = Array.from( diff --git a/keyserver/src/socket/session-utils.js b/keyserver/src/socket/session-utils.js --- a/keyserver/src/socket/session-utils.js +++ b/keyserver/src/socket/session-utils.js @@ -423,7 +423,7 @@ const idsToFetch = Object.fromEntries( values(serverStateSyncSpecs) .filter(spec => spec.innerHashSpec?.hashKey) - .map(spec => [spec.innerHashSpec?.hashKey, new Set()]), + .map(spec => [spec.innerHashSpec?.hashKey, new Set()]), ); for (const key of invalidKeys) { const [innerHashKey, id] = key.split('|'); diff --git a/keyserver/src/socket/socket.js b/keyserver/src/socket/socket.js --- a/keyserver/src/socket/socket.js +++ b/keyserver/src/socket/socket.js @@ -94,7 +94,7 @@ } from '../utils/validation-utils.js'; const clientSocketMessageInputValidator: TUnion = t.union([ - tShape({ + tShape({ type: t.irreducible( 'clientSocketMessageTypes.INITIAL', x => x === clientSocketMessageTypes.INITIAL, @@ -114,7 +114,7 @@ clientResponses: t.list(clientResponseInputValidator), }), }), - tShape({ + tShape({ type: t.irreducible( 'clientSocketMessageTypes.RESPONSES', x => x === clientSocketMessageTypes.RESPONSES, @@ -124,14 +124,14 @@ clientResponses: t.list(clientResponseInputValidator), }), }), - tShape({ + tShape({ type: t.irreducible( 'clientSocketMessageTypes.PING', x => x === clientSocketMessageTypes.PING, ), id: t.Number, }), - tShape({ + tShape({ type: t.irreducible( 'clientSocketMessageTypes.ACK_UPDATES', x => x === clientSocketMessageTypes.ACK_UPDATES, @@ -141,7 +141,7 @@ currentAsOf: t.Number, }), }), - tShape({ + tShape({ type: t.irreducible( 'clientSocketMessageTypes.API_REQUEST', x => x === clientSocketMessageTypes.API_REQUEST, diff --git a/keyserver/src/updaters/activity-updaters.js b/keyserver/src/updaters/activity-updaters.js --- a/keyserver/src/updaters/activity-updaters.js +++ b/keyserver/src/updaters/activity-updaters.js @@ -6,6 +6,7 @@ import { localIDPrefix } from 'lib/shared/message-utils.js'; import type { + ActivityUpdate, UpdateActivityResult, UpdateActivityRequest, SetThreadUnreadStatusRequest, @@ -58,7 +59,7 @@ throw new ServerError('not_logged_in'); } - const focusUpdatesByThreadID = new Map(); + const focusUpdatesByThreadID = new Map>(); for (const activityUpdate of request.updates) { const threadID = activityUpdate.threadID; const updatesForThreadID = focusUpdatesByThreadID.get(threadID) ?? []; @@ -87,7 +88,7 @@ return { unfocusedToUnread: [] }; } - const memberThreadIDs = new Set(); + const memberThreadIDs = new Set(); const verifiedThreadIDs = []; for (const threadData of verifiedThreadsData) { if (threadData.role > 0) { @@ -317,7 +318,7 @@ `; const [result] = await dbQuery(query); - const threadsWithNewerMessages = new Set(); + const threadsWithNewerMessages = new Set(); for (const row of result) { const threadID = row.thread.toString(); const serverLatestMessage = row.latest_message; diff --git a/keyserver/src/updaters/relationship-updaters.js b/keyserver/src/updaters/relationship-updaters.js --- a/keyserver/src/updaters/relationship-updaters.js +++ b/keyserver/src/updaters/relationship-updaters.js @@ -281,7 +281,7 @@ selectQuery.append(mergeOrConditions(conditions)); const [result] = await dbQuery(selectQuery); - const existingStatuses = new Map(); + const existingStatuses = new Map(); for (const row of result) { existingStatuses.set(`${row.user1}|${row.user2}`, row.status); } diff --git a/keyserver/src/updaters/thread-permission-updaters.js b/keyserver/src/updaters/thread-permission-updaters.js --- a/keyserver/src/updaters/thread-permission-updaters.js +++ b/keyserver/src/updaters/thread-permission-updaters.js @@ -86,6 +86,11 @@ permissionsFromParent?: ?ThreadPermissionsBlob, memberOfContainingThread?: boolean, }; +type ExistingMembership = { + +oldRole: string, + +oldPermissions: ?ThreadPermissionsBlob, + +oldPermissionsForChildren: ?ThreadPermissionsBlob, +}; async function changeRole( threadID: string, userIDs: $ReadOnlyArray, @@ -156,7 +161,7 @@ depth, } = roleThreadResult; - const existingMembershipInfo = new Map(); + const existingMembershipInfo = new Map(); for (const row of membershipResults) { const userID = row.user.toString(); existingMembershipInfo.set(userID, { @@ -202,7 +207,7 @@ } const membershipRows: Array = []; - const toUpdateDescendants = new Map(); + const toUpdateDescendants = new Map(); for (const userID of userIDs) { const existingMembership = existingMembershipInfo.get(userID); const oldRole = existingMembership?.oldRole ?? '-1'; @@ -451,7 +456,7 @@ relationshipChangeset.setAllRelationshipsExist(existingMemberIDs); } - const usersForNextLayer = new Map(); + const usersForNextLayer = new Map(); for (const [userID, user] of users) { const { curRolePermissions, @@ -809,7 +814,7 @@ } const membershipRows: Array = []; - const toUpdateDescendants = new Map(); + const toUpdateDescendants = new Map(); for (const [userID, membership] of membershipInfo) { const { rolePermissions: intendedRolePermissions, permissionsFromParent } = membership; @@ -1110,7 +1115,7 @@ } const { membershipRows, relationshipChangeset } = changeset; - const membershipRowMap = new Map(); + const membershipRowMap = new Map(); for (const row of membershipRows) { const { userID, threadID } = row; changedThreadIDs.add(threadID); @@ -1144,7 +1149,7 @@ } } - const threadsToSavedUsers = new Map(); + const threadsToSavedUsers = new Map>(); for (const row of membershipRowMap.values()) { const { userID, threadID } = row; let savedUsers = threadsToSavedUsers.get(threadID); diff --git a/keyserver/src/utils/ens-cache.js b/keyserver/src/utils/ens-cache.js --- a/keyserver/src/utils/ens-cache.js +++ b/keyserver/src/utils/ens-cache.js @@ -9,9 +9,11 @@ type GetENSNames, } from 'lib/utils/ens-helpers.js'; +type AlchemyConfig = { +key: string }; + let getENSNames: ?GetENSNames; async function initENSCache() { - const alchemySecret = await getCommConfig({ + const alchemySecret = await getCommConfig({ folder: 'secrets', name: 'alchemy', }); diff --git a/keyserver/src/utils/validation-utils.test.js b/keyserver/src/utils/validation-utils.test.js --- a/keyserver/src/utils/validation-utils.test.js +++ b/keyserver/src/utils/validation-utils.test.js @@ -12,42 +12,52 @@ }); it('should redact a string inside an object', () => { - const validator = tShape({ password: tPassword }); + const validator = tShape<{ +password: string }>({ password: tPassword }); const object = { password: 'password' }; const redacted = { password: redactedString }; expect(sanitizeInput(validator, object)).toStrictEqual(redacted); }); it('should redact an optional string', () => { - const validator = tShape({ password: t.maybe(tPassword) }); + const validator = tShape<{ +password: ?string }>({ + password: t.maybe(tPassword), + }); const object = { password: 'password' }; const redacted = { password: redactedString }; expect(sanitizeInput(validator, object)).toStrictEqual(redacted); }); it('should redact a string in optional object', () => { - const validator = tShape({ obj: t.maybe(tShape({ password: tPassword })) }); + const validator = tShape<{ +obj?: ?{ +password: string } }>({ + obj: t.maybe(tShape<{ +password: string }>({ password: tPassword })), + }); const object = { obj: { password: 'password' } }; const redacted = { obj: { password: redactedString } }; expect(sanitizeInput(validator, object)).toStrictEqual(redacted); }); it('should redact a string array', () => { - const validator = tShape({ passwords: t.list(tPassword) }); + const validator = tShape<{ +passwords: $ReadOnlyArray }>({ + passwords: t.list(tPassword), + }); const object = { passwords: ['password', 'password'] }; const redacted = { passwords: [redactedString, redactedString] }; expect(sanitizeInput(validator, object)).toStrictEqual(redacted); }); it('should redact a string inside a dict', () => { - const validator = tShape({ passwords: t.dict(t.String, tPassword) }); + const validator = tShape<{ +passwords: { +[string]: string } }>({ + passwords: t.dict(t.String, tPassword), + }); const object = { passwords: { a: 'password', b: 'password' } }; const redacted = { passwords: { a: redactedString, b: redactedString } }; expect(sanitizeInput(validator, object)).toStrictEqual(redacted); }); it('should redact password dict key', () => { - const validator = tShape({ passwords: t.dict(tPassword, t.Bool) }); + const validator = tShape<{ +passwords: { +[string]: boolean } }>({ + passwords: t.dict(tPassword, t.Bool), + }); const object = { passwords: { password1: true, password2: false } }; const redacted: { +passwords: { [string]: mixed } } = { passwords: {} }; redacted.passwords[redactedString] = false; @@ -55,7 +65,9 @@ }); it('should redact a string inside a union', () => { - const validator = tShape({ + const validator = tShape<{ + +password: string | boolean, + }>({ password: t.union([tPassword, t.String, t.Bool]), }); const object = { password: 'password' }; @@ -64,7 +76,11 @@ }); it('should redact a string inside an object array', () => { - const validator = tShape({ + const validator = tShape<{ + +passwords: $ReadOnlyArray<{ + +password: string, + }>, + }>({ passwords: t.list(tShape({ password: tPassword })), }); const object = { passwords: [{ password: 'password' }] };