diff --git a/lib/flow-typed/npm/tcomb_v3.x.x.js b/lib/flow-typed/npm/tcomb_v3.x.x.js index a367c2fb9..d1f62efc5 100644 --- a/lib/flow-typed/npm/tcomb_v3.x.x.js +++ b/lib/flow-typed/npm/tcomb_v3.x.x.js @@ -1,139 +1,139 @@ // flow-typed signature: f712ee1961974c799331608650bc7eb2 // flow-typed version: <>/tcomb_v3.2.29/flow_v0.137.0 declare module 'tcomb' { declare class TBaseType<+T> { (val: T): this; is(val: mixed): boolean; displayName: string; +t: T; } declare export type TType<+T> = | TIrreducible | TMaybe | TList | TDict | TUnion | TEnums | TRefinement | TInterface; declare export class TIrreducible<+T> extends TBaseType { meta: { +kind: 'irreducible', +name: string, +identity: boolean, +predicate: mixed => boolean, }; } declare export class TMaybe<+T> extends TBaseType { meta: { +kind: 'maybe', +name: string, +identity: boolean, +type: TType, }; } declare export class TList<+T> extends TBaseType { meta: { +kind: 'list', +name: string, +identity: boolean, +type: TType, }; } declare export class TDict<+T> extends TBaseType { meta: { +kind: 'dict', +name: string, +identity: boolean, +domain: TType, +codomain: TType, }; } declare export class TUnion<+T> extends TBaseType { meta: { +kind: 'union', +name: string, +identity: boolean, +types: Array>, }; } - declare export class TEnums extends TBaseType { + declare export class TEnums extends TBaseType { meta: { +kind: 'enums', +name: string, +identity: boolean, +map: Object, }; } declare export class TRefinement<+T> extends TBaseType { meta: { +kind: 'subtype', +name: string, +identity: boolean, +type: TType, +predicate: mixed => boolean, }; } declare type TypeToValidator = (v: V) => TType; declare export type TStructProps<+T> = $ObjMap; declare type TStructOptions = { name?: string, strict?: boolean, defaultProps?: Object, }; declare export class TInterface<+T> extends TBaseType { meta: { +kind: 'interface', +name: string, +identity: boolean, +props: TStructProps, +strict: boolean, }; } declare export default { +Nil: TIrreducible, +Bool: TIrreducible, +Boolean: TIrreducible, +String: TIrreducible, +Number: TIrreducible, +Object: TIrreducible, maybe(type: TType, name?: string): TMaybe, list(type: TType, name?: string): TList>, dict( domain: TType, codomain: TType, name?: string, ): TDict<{ [key: S]: T }>, union<+T>(types: $ReadOnlyArray>, name?: string): TUnion, +enums: { - of(enums: $ReadOnlyArray, name?: string): TEnums, + of(enums: $ReadOnlyArray | $ReadOnlyArray, name?: string): TEnums, }, irreducible( name: string, predicate: (mixed) => boolean, ): TIrreducible, refinement( type: TType, predicate: (T) => boolean, name?: string, ): TRefinement, interface( props: TStructProps, options?: string | TStructOptions, ): TInterface, ... }; } diff --git a/lib/reducers/aux-user-reducer.js b/lib/reducers/aux-user-reducer.js index 246b46057..00512b262 100644 --- a/lib/reducers/aux-user-reducer.js +++ b/lib/reducers/aux-user-reducer.js @@ -1,150 +1,151 @@ // @flow import { setAuxUserFIDsActionType, addAuxUserFIDsActionType, clearAuxUserFIDsActionType, setPeerDeviceListsActionType, } from '../actions/aux-user-actions.js'; import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { auxUserStoreOpsHandlers, type AuxUserStoreOperation, type ReplaceAuxUserInfoOperation, } from '../ops/aux-user-store-ops.js'; import type { AuxUserStore } from '../types/aux-user-types.js'; import type { BaseAction } from '../types/redux-types'; const { processStoreOperations: processStoreOps } = auxUserStoreOpsHandlers; function reduceAuxUserStore( state: AuxUserStore, action: BaseAction, ): { +auxUserStore: AuxUserStore, +auxUserStoreOperations: $ReadOnlyArray, } { if (action.type === setAuxUserFIDsActionType) { const toUpdateUserIDs = new Set( action.payload.farcasterUsers.map(farcasterUser => farcasterUser.userID), ); const replaceOperations: ReplaceAuxUserInfoOperation[] = []; for (const userID in state.auxUserInfos) { if ( state.auxUserInfos[userID].fid !== null && !toUpdateUserIDs.has(userID) ) { replaceOperations.push({ type: 'replace_aux_user_info', payload: { id: userID, auxUserInfo: { ...state.auxUserInfos[userID], fid: null, }, }, }); } } for (const farcasterUser of action.payload.farcasterUsers) { replaceOperations.push({ type: 'replace_aux_user_info', payload: { id: farcasterUser.userID, auxUserInfo: { ...state.auxUserInfos[farcasterUser.userID], fid: farcasterUser.farcasterID, }, }, }); } return { auxUserStore: processStoreOps(state, replaceOperations), auxUserStoreOperations: replaceOperations, }; } else if (action.type === addAuxUserFIDsActionType) { const replaceOperations: ReplaceAuxUserInfoOperation[] = []; for (const farcasterUser of action.payload.farcasterUsers) { replaceOperations.push({ type: 'replace_aux_user_info', payload: { id: farcasterUser.userID, auxUserInfo: { ...state.auxUserInfos[farcasterUser.userID], fid: farcasterUser.farcasterID, }, }, }); } return { auxUserStore: processStoreOps(state, replaceOperations), auxUserStoreOperations: replaceOperations, }; } else if (action.type === clearAuxUserFIDsActionType) { const replaceOperations: ReplaceAuxUserInfoOperation[] = []; for (const userID in state.auxUserInfos) { if (state.auxUserInfos[userID].fid !== null) { replaceOperations.push({ type: 'replace_aux_user_info', payload: { id: userID, auxUserInfo: { ...state.auxUserInfos[userID], fid: null, }, }, }); } } return { auxUserStore: processStoreOps(state, replaceOperations), auxUserStoreOperations: replaceOperations, }; } else if (action.type === setClientDBStoreActionType) { const newAuxUserInfos = action.payload.auxUserInfos; if (!newAuxUserInfos) { return { auxUserStore: state, auxUserStoreOperations: [], }; } const newAuxUserStore: AuxUserStore = { ...state, auxUserInfos: newAuxUserInfos, }; return { auxUserStore: newAuxUserStore, auxUserStoreOperations: [], }; } else if (action.type === setPeerDeviceListsActionType) { const replaceOperations: ReplaceAuxUserInfoOperation[] = []; for (const userID in action.payload.deviceLists) { replaceOperations.push({ type: 'replace_aux_user_info', payload: { id: userID, auxUserInfo: { ...state.auxUserInfos[userID], fid: state.auxUserInfos[userID]?.fid ?? null, deviceList: action.payload.deviceLists[userID], + devicesPlatformDetails: action.payload.usersPlatformDetails[userID], }, }, }); } return { auxUserStore: processStoreOps(state, replaceOperations), auxUserStoreOperations: replaceOperations, }; } return { auxUserStore: state, auxUserStoreOperations: [], }; } export { reduceAuxUserStore }; diff --git a/lib/reducers/aux-user-reducer.test.js b/lib/reducers/aux-user-reducer.test.js index 7ee2b263b..3af01c10b 100644 --- a/lib/reducers/aux-user-reducer.test.js +++ b/lib/reducers/aux-user-reducer.test.js @@ -1,132 +1,165 @@ // @flow import { reduceAuxUserStore } from './aux-user-reducer.js'; import { addAuxUserFIDsActionType, setAuxUserFIDsActionType, setPeerDeviceListsActionType, } from '../actions/aux-user-actions.js'; -import type { RawDeviceList } from '../types/identity-service-types.js'; +import { + type RawDeviceList, + type IdentityPlatformDetails, + identityDeviceTypes, +} from '../types/identity-service-types.js'; jest.mock('../utils/config.js'); describe('reduceAuxUserStore', () => { it('should update aux user store with farcaster data for user2', () => { const oldAuxUserStore = { auxUserInfos: { userID_1: { fid: 'farcasterID_1', }, }, }; const updateAuxUserInfosAction = { type: addAuxUserFIDsActionType, payload: { farcasterUsers: [ { userID: 'userID_2', username: 'username_2', farcasterID: 'farcasterID_2', }, ], }, }; expect( reduceAuxUserStore(oldAuxUserStore, updateAuxUserInfosAction) .auxUserStore, ).toEqual({ auxUserInfos: { userID_1: { fid: 'farcasterID_1', }, userID_2: { fid: 'farcasterID_2', }, }, }); }); it('should set aux user store user1 fid to null and add user2', () => { const oldAuxUserStore = { auxUserInfos: { userID_1: { fid: 'farcasterID_1', }, }, }; const updateAuxUserInfosAction = { type: setAuxUserFIDsActionType, payload: { farcasterUsers: [ { userID: 'userID_2', username: 'username_2', farcasterID: 'farcasterID_2', }, ], }, }; expect( reduceAuxUserStore(oldAuxUserStore, updateAuxUserInfosAction) .auxUserStore, ).toEqual({ auxUserInfos: { userID_1: { fid: null, }, userID_2: { fid: 'farcasterID_2', }, }, }); }); it('should update aux user store with device lists for users', () => { const oldAuxUserStore = { auxUserInfos: { userID_1: { fid: 'farcasterID_1', }, }, }; const deviceList1: RawDeviceList = { devices: ['D1', 'D2'], timestamp: 1, }; + const devicesPlatformDetails1: { + +[deviceID: string]: IdentityPlatformDetails, + } = { + D1: { + deviceType: identityDeviceTypes.ANDROID, + codeVersion: 350, + stateVersion: 75, + }, + D2: { + deviceType: identityDeviceTypes.WINDOWS, + codeVersion: 80, + stateVersion: 75, + }, + }; const deviceList2: RawDeviceList = { devices: ['D3'], timestamp: 1, }; + const devicesPlatformDetails2: { + +[deviceID: string]: IdentityPlatformDetails, + } = { + D3: { + deviceType: identityDeviceTypes.IOS, + codeVersion: 350, + stateVersion: 75, + }, + }; const updateAuxUserInfosAction = { type: setPeerDeviceListsActionType, payload: { deviceLists: { userID_1: deviceList1, userID_2: deviceList2, }, + usersPlatformDetails: { + userID_1: devicesPlatformDetails1, + userID_2: devicesPlatformDetails2, + }, }, }; expect( reduceAuxUserStore(oldAuxUserStore, updateAuxUserInfosAction) .auxUserStore, ).toEqual({ auxUserInfos: { userID_1: { fid: 'farcasterID_1', deviceList: deviceList1, + devicesPlatformDetails: devicesPlatformDetails1, }, userID_2: { fid: null, deviceList: deviceList2, + devicesPlatformDetails: devicesPlatformDetails2, }, }, }); }); }); diff --git a/lib/types/aux-user-types.js b/lib/types/aux-user-types.js index 2233b8321..fa0a0b1ed 100644 --- a/lib/types/aux-user-types.js +++ b/lib/types/aux-user-types.js @@ -1,27 +1,35 @@ // @flow import type { FarcasterUser, RawDeviceList, UsersRawDeviceLists, + IdentityPlatformDetails, } from './identity-service-types.js'; -export type AuxUserInfo = { +fid: ?string, deviceList?: RawDeviceList }; +export type AuxUserInfo = { + +fid: ?string, + +deviceList?: RawDeviceList, + +devicesPlatformDetails?: { +[deviceID: string]: IdentityPlatformDetails }, +}; export type AuxUserInfos = { +[userID: string]: AuxUserInfo }; export type AuxUserStore = { +auxUserInfos: AuxUserInfos, }; export type SetAuxUserFIDsPayload = { +farcasterUsers: $ReadOnlyArray, }; export type AddAuxUserFIDsPayload = { +farcasterUsers: $ReadOnlyArray, }; export type SetPeerDeviceListsPayload = { +deviceLists: UsersRawDeviceLists, + +usersPlatformDetails: { + +[userID: string]: { +[deviceID: string]: IdentityPlatformDetails }, + }, }; diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index c797686d6..dc952d030 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,324 +1,344 @@ // @flow -import t, { type TInterface, type TList, type TDict } from 'tcomb'; +import t, { type TInterface, type TList, type TDict, type TEnums } from 'tcomb'; import { identityKeysBlobValidator, type IdentityKeysBlob, signedPrekeysValidator, type SignedPrekeys, type OneTimeKeysResultValues, } from './crypto-types.js'; import { type OlmSessionInitializationInfo, olmSessionInitializationInfoValidator, } from './request-types.js'; import { currentUserInfoValidator, type CurrentUserInfo, } from './user-types.js'; +import { values } from '../utils/objects.js'; import { tUserID, tShape } from '../utils/validation-utils.js'; export type UserAuthMetadata = { +userID: string, +accessToken: string, }; // This type should not be altered without also updating OutboundKeyInfoResponse // in native/native_rust_library/src/identity/x3dh.rs export type OutboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +oneTimeContentPrekey: ?string, +oneTimeNotifPrekey: ?string, }; // This type should not be altered without also updating InboundKeyInfoResponse // in native/native_rust_library/src/identity/x3dh.rs export type InboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +username?: ?string, +walletAddress?: ?string, }; export type DeviceOlmOutboundKeys = { +identityKeysBlob: IdentityKeysBlob, +contentInitializationInfo: OlmSessionInitializationInfo, +notifInitializationInfo: OlmSessionInitializationInfo, +payloadSignature: string, }; export const deviceOlmOutboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, contentInitializationInfo: olmSessionInitializationInfoValidator, notifInitializationInfo: olmSessionInitializationInfoValidator, payloadSignature: t.String, }); export type UserDevicesOlmOutboundKeys = { +deviceID: string, +keys: ?DeviceOlmOutboundKeys, }; export type DeviceOlmInboundKeys = { +identityKeysBlob: IdentityKeysBlob, +signedPrekeys: SignedPrekeys, +payloadSignature: string, }; export const deviceOlmInboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, signedPrekeys: signedPrekeysValidator, payloadSignature: t.String, }); export type UserDevicesOlmInboundKeys = { +keys: { +[deviceID: string]: ?DeviceOlmInboundKeys, }, +username?: ?string, +walletAddress?: ?string, }; // This type should not be altered without also updating FarcasterUser in // keyserver/addons/rust-node-addon/src/identity_client/get_farcaster_users.rs export type FarcasterUser = { +userID: string, +username: string, +farcasterID: string, }; export const farcasterUserValidator: TInterface = tShape({ userID: tUserID, username: t.String, farcasterID: t.String, }); export const farcasterUsersValidator: TList> = t.list( farcasterUserValidator, ); export const userDeviceOlmInboundKeysValidator: TInterface = tShape({ keys: t.dict(t.String, t.maybe(deviceOlmInboundKeysValidator)), username: t.maybe(t.String), walletAddress: t.maybe(t.String), }); export interface IdentityServiceClient { // Only a primary device can initiate account deletion, and web cannot be a // primary device +deleteWalletUser?: () => Promise; // Only a primary device can initiate account deletion, and web cannot be a // primary device +deletePasswordUser?: (password: string) => Promise; +logOut: () => Promise; +logOutSecondaryDevice: () => Promise; +getKeyserverKeys: string => Promise; // Users cannot register from web +registerPasswordUser?: ( username: string, password: string, fid: ?string, ) => Promise; // Users cannot register from web +registerReservedPasswordUser?: ( username: string, password: string, keyserverMessage: string, keyserverSignature: string, ) => Promise; +logInPasswordUser: ( username: string, password: string, ) => Promise; +getOutboundKeysForUser: ( userID: string, ) => Promise; +getInboundKeysForUser: ( userID: string, ) => Promise; +uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise; +generateNonce: () => Promise; // Users cannot register from web +registerWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: string, fid: ?string, ) => Promise; // Users cannot register from web +registerReservedWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: string, keyserverMessage: string, keyserverSignature: string, ) => Promise; +logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise; // on native, publishing prekeys to Identity is called directly from C++, // there is no need to expose it to JS +publishWebPrekeys?: (prekeys: SignedPrekeys) => Promise; +getDeviceListHistoryForUser: ( userID: string, sinceTimestamp?: number, ) => Promise<$ReadOnlyArray>; +getDeviceListsForUsers: ( userIDs: $ReadOnlyArray, ) => Promise; // updating device list is possible only on Native // web cannot be a primary device, so there's no need to expose it to JS +updateDeviceList?: (newDeviceList: SignedDeviceList) => Promise; +uploadKeysForRegisteredDeviceAndLogIn: ( userID: string, signedNonce: SignedNonce, ) => Promise; +getFarcasterUsers: ( farcasterIDs: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>; +linkFarcasterAccount: (farcasterID: string) => Promise; +unlinkFarcasterAccount: () => Promise; +findUserIdentities: (userIDs: $ReadOnlyArray) => Promise; } export type IdentityServiceAuthLayer = { +userID: string, +deviceID: string, +commServicesAccessToken: string, }; export type IdentityAuthResult = { +userID: string, +accessToken: string, +username: string, +preRequestUserState?: ?CurrentUserInfo, }; export const identityAuthResultValidator: TInterface = tShape({ userID: tUserID, accessToken: t.String, username: t.String, preRequestUserState: t.maybe(currentUserInfoValidator), }); export type IdentityNewDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +contentOneTimeKeys: $ReadOnlyArray, +notifOneTimeKeys: $ReadOnlyArray, }; export type IdentityExistingDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, }; // Device list types export type RawDeviceList = { +devices: $ReadOnlyArray, +timestamp: number, }; export const rawDeviceListValidator: TInterface = tShape({ devices: t.list(t.String), timestamp: t.Number, }); export type UsersRawDeviceLists = { +[userID: string]: RawDeviceList, }; // User Identity types export type EthereumIdentity = { walletAddress: string, siweMessage: string, siweSignature: string, }; export type Identity = { +username: string, +ethIdentity: ?EthereumIdentity, +farcasterID: ?string, }; export type Identities = { +[userID: string]: Identity, }; export const ethereumIdentityValidator: TInterface = tShape({ walletAddress: t.String, siweMessage: t.String, siweSignature: t.String, }); export const identityValidator: TInterface = tShape({ username: t.String, ethIdentity: t.maybe(ethereumIdentityValidator), farcasterID: t.maybe(t.String), }); export const identitiesValidator: TDict = t.dict( t.String, identityValidator, ); export type SignedDeviceList = { // JSON-stringified RawDeviceList +rawDeviceList: string, // Current primary device signature. Absent for Identity Service generated // device lists. +curPrimarySignature?: string, // Previous primary device signature. Present only if primary device // has changed since last update. +lastPrimarySignature?: string, }; export const signedDeviceListValidator: TInterface = tShape({ rawDeviceList: t.String, curPrimarySignature: t.maybe(t.String), lastPrimarySignature: t.maybe(t.String), }); export const signedDeviceListHistoryValidator: TList> = t.list(signedDeviceListValidator); export type UsersSignedDeviceLists = { +[userID: string]: SignedDeviceList, }; export const usersSignedDeviceListsValidator: TDict = t.dict(t.String, signedDeviceListValidator); export type SignedNonce = { +nonce: string, +nonceSignature: string, }; export const ONE_TIME_KEYS_NUMBER = 10; export const identityDeviceTypes = Object.freeze({ KEYSERVER: 0, WEB: 1, IOS: 2, ANDROID: 3, WINDOWS: 4, MAC_OS: 5, }); + +export type IdentityDeviceType = $Values; +export const identityDeviceTypeValidator: TEnums = t.enums.of( + values(identityDeviceTypes), +); + +export type IdentityPlatformDetails = { + +deviceType: IdentityDeviceType, + +codeVersion: number, + +stateVersion?: number, + +majorDesktopVersion?: number, +}; +export const identityPlatformDetailsValidator: TInterface = + tShape({ + deviceType: identityDeviceTypeValidator, + codeVersion: t.Number, + stateVersion: t.maybe(t.Number), + majorDesktopVersion: t.maybe(t.Number), + });