From e0f5e452c629a73309100b4292f80457bd2e949d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?So=CC=88nmez=20Kartal?= Date: Tue, 10 Oct 2023 17:06:28 +0300 Subject: [PATCH] feat(passport): account mask address --- .../app/components/applications/claims.tsx | 42 ++++--- apps/passport/app/routes/authorize.tsx | 79 ++++++++++-- .../app/routes/create/account-mask.ts | 61 +++++++++ apps/passport/app/routes/settings.tsx | 12 +- .../applications/$clientId/scopes.tsx | 2 +- apps/passport/app/routes/userinfo.tsx | 3 +- apps/passport/app/utils/authorize.server.ts | 39 ++++-- .../src/atoms/dropdown/DropdownSelectList.tsx | 88 ++++++++++--- .../src/atoms/form/InputToggle.tsx | 2 +- .../src/atoms/pills/EmailMaskPill.tsx | 29 +++++ .../src/atoms/providers/Email.tsx | 4 + .../templates/authorization/Authorization.tsx | 72 ++++++++--- packages/security/package.json | 1 + packages/security/persona.ts | 116 ++++++++++++++++-- .../utils/getNormalisedConnectedAccounts.tsx | 75 +++++++---- .../src/jsonrpc/methods/getAccountProfile.ts | 81 +++++++----- .../src/jsonrpc/methods/getMaskedAddress.ts | 45 +++++++ .../src/jsonrpc/methods/setSourceAccount.ts | 43 +++++++ platform/account/src/jsonrpc/router.ts | 26 ++++ .../account/src/jsonrpc/validators/profile.ts | 5 + platform/account/src/nodes/email.ts | 27 +++- platform/account/src/utils.ts | 2 + .../jsonrpc/methods/getAuthorizedAppScopes.ts | 6 + .../src/jsonrpc/methods/getUserInfo.ts | 11 +- platform/core/src/context.ts | 6 +- .../src/jsonrpc/validators/profile.ts | 6 +- yarn.lock | 1 + 27 files changed, 735 insertions(+), 149 deletions(-) create mode 100644 apps/passport/app/routes/create/account-mask.ts create mode 100644 packages/design-system/src/atoms/pills/EmailMaskPill.tsx create mode 100644 packages/design-system/src/atoms/providers/Email.tsx create mode 100644 platform/account/src/jsonrpc/methods/getMaskedAddress.ts create mode 100644 platform/account/src/jsonrpc/methods/setSourceAccount.ts diff --git a/apps/passport/app/components/applications/claims.tsx b/apps/passport/app/components/applications/claims.tsx index e0d25d5fd5..501951164b 100644 --- a/apps/passport/app/components/applications/claims.tsx +++ b/apps/passport/app/components/applications/claims.tsx @@ -10,12 +10,14 @@ import { Disclosure } from '@headlessui/react' import { useState } from 'react' import passportLogoURL from '~/assets/PassportIcon.svg' +import { HiOutlineMail } from 'react-icons/hi' import { TbCrown } from 'react-icons/tb' import { Modal } from '@proofzero/design-system/src/molecules/modal/Modal' import warningImg from '~/assets/warning.svg' import InputText from '~/components/inputs/InputText' import { startCase } from 'lodash' import { HiOutlineExternalLink, HiOutlineX } from 'react-icons/hi' +import { EmailMaskedPill } from '@proofzero/design-system/src/atoms/pills/EmailMaskPill' export const ConfirmRevocationModal = ({ title, @@ -171,12 +173,14 @@ export const ClaimsMobileView = ({ scopes }: { scopes: any[] }) => { const RowView = ({ account, appAskedFor, + masked = false, whatsBeingShared, sourceOfData, sourceOfDataIcon, dropdown = true, }: { appAskedFor: string + masked: boolean sourceOfData: string sourceOfDataIcon: JSX.Element dropdown?: boolean @@ -203,6 +207,7 @@ export const ClaimsMobileView = ({ scopes }: { scopes: any[] }) => { > {appAskedFor} + {masked && } {whatsBeingShared && ( { > {appAskedFor} + {masked && } {whatsBeingShared && ( { - } + sourceOfDataIcon={} dropdown={false} /> ) @@ -491,12 +496,14 @@ export const ClaimsWideView = ({ scopes }: { scopes: any[] }) => { const RowView = ({ account, appAskedFor, + masked = false, whatsBeingShared, sourceOfData, sourceOfDataIcon, dropdown = true, }: { appAskedFor: string + masked: boolean sourceOfData: string sourceOfDataIcon: JSX.Element dropdown?: boolean @@ -527,15 +534,19 @@ export const ClaimsWideView = ({ scopes }: { scopes: any[] }) => { > {appAskedFor} + {masked && } ) : ( - - {appAskedFor} - +
+ + {appAskedFor} + + {masked && } +
)} @@ -754,11 +765,12 @@ export const ClaimsWideView = ({ scopes }: { scopes: any[] }) => { + masked={scope.masked} + whatsBeingShared={ + scope.masked ? scope.address : scope.source.address } + sourceOfData={scope.address} + sourceOfDataIcon={} dropdown={false} /> ) diff --git a/apps/passport/app/routes/authorize.tsx b/apps/passport/app/routes/authorize.tsx index 8ab83585d8..ed1ef74d54 100644 --- a/apps/passport/app/routes/authorize.tsx +++ b/apps/passport/app/routes/authorize.tsx @@ -38,7 +38,10 @@ import type { PersonaData } from '@proofzero/types/application' import Authorization from '@proofzero/design-system/src/templates/authorization/Authorization' import { getRollupReqFunctionErrorWrapper } from '@proofzero/utils/errors' -import { getEmailIcon } from '@proofzero/utils/getNormalisedConnectedAccounts' +import { + decorateAccountDropdownItem, + getEmailIcon, +} from '@proofzero/utils/getNormalisedConnectedAccounts' import { ThemeContext } from '@proofzero/design-system/src/contexts/theme' import { AuthenticationScreenDefaults } from '@proofzero/design-system/src/templates/authentication/Authentication' import { Helmet } from 'react-helmet' @@ -397,13 +400,13 @@ export const action: ActionFunction = async ({ request, context }) => { } export default function Authorize() { + const loaderData = useLoaderData() const { clientId, appProfile, scopeMeta, state, redirectOverride, - dataForScopes, redirectUri, profile, prompt, @@ -412,10 +415,11 @@ export default function Authorize() { const userProfile = profile as UserProfile + const [dataForScopes, setDataForScopes] = useState(loaderData.dataForScopes) const { - connectedEmails, personaData, requestedScope, + connectedEmails, connectedAccounts, connectedSmartContractWallets, } = dataForScopes as DataForScopes @@ -434,6 +438,51 @@ export default function Authorize() { } return selected }) + + const [maskEmail, setMaskEmail] = useState(false) + useEffect(() => { + if (!maskEmail) return + if (selectedEmail?.mask) return + setMaskEmailCallback() + }, [maskEmail, selectedEmail]) + + const setMaskEmailCallback = async () => { + if (!maskEmail) return + + const accountURN = selectedEmail?.value + if (!accountURN) return + if (selectedEmail.mask) return + + const response = await fetch('/create/account-mask', { + body: JSON.stringify({ accountURN, clientId }), + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }) + + let maskedAccount = selectedEmail + const maskAccount = await response.json() + + setDataForScopes((state) => ({ + ...state, + connectedAccounts: connectedAccounts.map((ca) => { + if (ca.value !== accountURN) return ca + return { + ...ca, + mask: decorateAccountDropdownItem(maskAccount), + } + }), + connectedEmails: connectedEmails.map((ce) => { + if (ce.value !== accountURN) return ce + maskedAccount = { + ...ce, + mask: decorateAccountDropdownItem(maskAccount), + } + return maskedAccount + }), + })) + setSelectedEmail(maskedAccount) + } + const [selectedConnectedAccounts, setSelectedConnectedAccounts] = useState< Array | Array >(() => { @@ -441,8 +490,8 @@ export default function Authorize() { return [AuthorizationControlSelection.ALL] } else { return connectedAccounts?.length - ? connectedAccounts.filter((acc) => - persona.connected_accounts?.includes(acc.value) + ? connectedAccounts.filter( + (acc) => persona.connected_accounts?.includes(acc.value) ) : [] } @@ -454,8 +503,8 @@ export default function Authorize() { return [AuthorizationControlSelection.ALL] } else { return connectedSmartContractWallets?.length - ? connectedSmartContractWallets.filter((acc) => - persona.erc_4337?.includes(acc.value) + ? connectedSmartContractWallets.filter( + (acc) => persona.erc_4337?.includes(acc.value) ) : [] } @@ -510,7 +559,9 @@ export default function Authorize() { } if (requestedScope.includes('email') && selectedEmail) { - personaData.email = selectedEmail.value + personaData.email = maskEmail + ? selectedEmail.mask?.value + : selectedEmail.value } if ( @@ -521,7 +572,12 @@ export default function Authorize() { personaData.connected_accounts = AuthorizationControlSelection.ALL } else { personaData.connected_accounts = selectedConnectedAccounts.map( - (account) => (account as DropdownSelectListItem).value + (account) => { + const item = account as DropdownSelectListItem + if (!maskEmail) return item.value + if (item.value === selectedEmail?.value) return item.mask?.value + return item.value + } ) } } @@ -640,10 +696,13 @@ export default function Authorize() { // Substituting subtitle with icon // on the client side return { + address: email.address, + type: email.type, icon: getEmailIcon(email.subtitle!), title: email.title, selected: email.selected, value: email.value, + mask: email.mask, } }) ?? [] } @@ -661,6 +720,8 @@ export default function Authorize() { }} selectEmailCallback={setSelectedEmail} selectedEmail={selectedEmail} + maskEmail={maskEmail} + setMaskEmail={setMaskEmail} connectedAccounts={connectedAccounts ?? []} selectedConnectedAccounts={selectedConnectedAccounts} addNewAccountCallback={() => { diff --git a/apps/passport/app/routes/create/account-mask.ts b/apps/passport/app/routes/create/account-mask.ts new file mode 100644 index 0000000000..f65058019c --- /dev/null +++ b/apps/passport/app/routes/create/account-mask.ts @@ -0,0 +1,61 @@ +import { json } from '@remix-run/cloudflare' +import type { ActionFunction } from '@remix-run/cloudflare' + +import { EmailAccountType, NodeType } from '@proofzero/types/account' +import { type AccountURN, AccountURNSpace } from '@proofzero/urns/account' +import { generateHashedIDRef } from '@proofzero/urns/idref' +import { getRollupReqFunctionErrorWrapper } from '@proofzero/utils/errors' + +import { getCoreClient } from '~/platform.server' +import { getValidatedSessionContext } from '~/session.server' +import { BadRequestError } from '@proofzero/errors' + +export const action: ActionFunction = getRollupReqFunctionErrorWrapper( + async ({ request, context }) => { + const { jwt } = await getValidatedSessionContext( + request, + context.authzQueryParams, + context.env, + context.traceSpan + ) + + const { accountURN, clientId } = await request.json<{ + accountURN: AccountURN + clientId: string + }>() + + if (typeof accountURN !== 'string') + throw new BadRequestError({ message: 'missing account urn' }) + + const coreClient = getCoreClient({ context, jwt, accountURN }) + const address = await coreClient.account.getMaskedAddress.query({ + clientId, + }) + const qc = { + alias: address, + source: accountURN, + } + const rc = { node_type: NodeType.Email, addr_type: EmailAccountType.Mask } + + const maskedAccountURN = AccountURNSpace.componentizedUrn( + generateHashedIDRef(EmailAccountType.Mask, address), + rc, + qc + ) + + const maskedAccountCoreClient = getCoreClient({ + context, + jwt, + accountURN: maskedAccountURN, + }) + + await maskedAccountCoreClient.account.resolveIdentity.query({ jwt }) + await maskedAccountCoreClient.account.setSourceAccount.mutate(accountURN) + + return json({ + baseUrn: AccountURNSpace.getBaseURN(maskedAccountURN), + qc, + rc, + }) + } +) diff --git a/apps/passport/app/routes/settings.tsx b/apps/passport/app/routes/settings.tsx index 3229187b8f..7edc7b5fcf 100644 --- a/apps/passport/app/routes/settings.tsx +++ b/apps/passport/app/routes/settings.tsx @@ -18,7 +18,7 @@ import noImg from '~/assets/noImg.svg' import { getCoreClient } from '~/platform.server' import type { AccountURN } from '@proofzero/urns/account' -import type { NodeType } from '@proofzero/types/account' +import { EmailAccountType, type NodeType } from '@proofzero/types/account' import type { LoaderFunction, MetaFunction, @@ -68,10 +68,12 @@ export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper( identity: identityURN, }) - const accountTypeUrns = identityProfile?.accounts.map((a) => ({ - urn: a.baseUrn, - nodeType: a.rc.node_type, - })) as { urn: AccountURN; nodeType: NodeType }[] + const accountTypeUrns = identityProfile?.accounts + .filter((a) => a.rc.addr_type !== EmailAccountType.Mask) + .map((a) => ({ + urn: a.baseUrn, + nodeType: a.rc.node_type, + })) as { urn: AccountURN; nodeType: NodeType }[] const accounts = accountTypeUrns.map((atu) => atu.urn) diff --git a/apps/passport/app/routes/settings/applications/$clientId/scopes.tsx b/apps/passport/app/routes/settings/applications/$clientId/scopes.tsx index 321e6c9c8e..01cd168e2e 100644 --- a/apps/passport/app/routes/settings/applications/$clientId/scopes.tsx +++ b/apps/passport/app/routes/settings/applications/$clientId/scopes.tsx @@ -19,7 +19,7 @@ export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper( } const coreClient = getCoreClient({ context }) - return await coreClient.authorization.getAuthorizedAppScopes.query({ + return coreClient.authorization.getAuthorizedAppScopes.query({ clientId, identityURN, }) diff --git a/apps/passport/app/routes/userinfo.tsx b/apps/passport/app/routes/userinfo.tsx index 096fa503e8..b9e3aee93b 100644 --- a/apps/passport/app/routes/userinfo.tsx +++ b/apps/passport/app/routes/userinfo.tsx @@ -14,10 +14,9 @@ export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper( const { origin: issuer } = new URL(request.url) const coreClient = getCoreClient({ context }) - const result = await coreClient.authorization.getUserInfo.query({ + return coreClient.authorization.getUserInfo.query({ access_token, issuer, }) - return result } ) diff --git a/apps/passport/app/utils/authorize.server.ts b/apps/passport/app/utils/authorize.server.ts index e739dc09a6..1d9264a758 100644 --- a/apps/passport/app/utils/authorize.server.ts +++ b/apps/passport/app/utils/authorize.server.ts @@ -16,7 +16,12 @@ import { import type { IdentityURN } from '@proofzero/urns/identity' import type { PersonaData } from '@proofzero/types/application' import { redirect } from '@remix-run/cloudflare' -import { CryptoAccountType, NodeType } from '@proofzero/types/account' +import { + CryptoAccountType, + EmailAccountType, + NodeType, + OAuthAccountType, +} from '@proofzero/types/account' import type { DropdownSelectListItem } from '@proofzero/design-system/src/atoms/dropdown/DropdownSelectList' import type { AccountURN } from '@proofzero/urns/account' @@ -78,14 +83,16 @@ export const getDataForScopes = async ( } if (requestedScope.includes(Symbol.keyFor(SCOPE_CONNECTED_ACCOUNTS)!)) { const accounts = connectedAccounts - .filter((ca) => { - return ( - (ca.rc.node_type === NodeType.OAuth || - ca.rc.node_type === NodeType.Email || - ca.rc.node_type === NodeType.Crypto || - ca.rc.node_type === NodeType.WebAuthN) && - ca.rc.addr_type !== CryptoAccountType.Wallet - ) + .filter(({ rc: { addr_type, node_type } }) => { + switch (node_type) { + case NodeType.Email: + return addr_type === EmailAccountType.Email + case NodeType.Crypto: + return addr_type !== CryptoAccountType.Wallet + case NodeType.OAuth: + case NodeType.WebAuthN: + return true + } }) .map((ca) => { return ca.baseUrn as AccountURN @@ -94,6 +101,20 @@ export const getDataForScopes = async ( const accountProfiles = await coreClient.account.getAccountProfileBatch.query(accounts) connectedAddresses = getAccountDropdownItems(accountProfiles) + + if (connectedEmails.length) { + connectedAddresses = connectedAddresses.map((ca) => { + const emailAccount = connectedEmails.find( + (ce) => ca.value === ce.value + ) + if (!emailAccount) return ca + if (!emailAccount.mask) return ca + return { + ...ca, + mask: emailAccount.mask, + } + }) + } } if (requestedScope.includes(Symbol.keyFor(SCOPE_SMART_CONTRACT_WALLETS)!)) { const accounts = connectedAccounts diff --git a/packages/design-system/src/atoms/dropdown/DropdownSelectList.tsx b/packages/design-system/src/atoms/dropdown/DropdownSelectList.tsx index 4025203c0a..ee7681ae85 100644 --- a/packages/design-system/src/atoms/dropdown/DropdownSelectList.tsx +++ b/packages/design-system/src/atoms/dropdown/DropdownSelectList.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useState } from 'react' +import React, { Fragment, useEffect, useState } from 'react' import { Listbox, Transition } from '@headlessui/react' import { Text } from '../text/Text' import { @@ -10,13 +10,19 @@ import { Button } from '../buttons/Button' import { TbCirclePlus } from 'react-icons/tb' import { BadRequestError } from '@proofzero/errors' import { AuthorizationControlSelection } from '@proofzero/types/application' +import { adjustAccountTypeToDisplay } from '@proofzero/utils/getNormalisedConnectedAccounts' +import { EmailAccountType, OAuthAccountType } from '@proofzero/types/account' +import { EmailMaskedPill } from '@proofzero/design-system/src/atoms/pills/EmailMaskPill' export type DropdownSelectListItem = { + address: string + type: EmailAccountType | OAuthAccountType title: string value?: string icon?: JSX.Element selected?: boolean subtitle?: string + mask?: DropdownSelectListItem } export type DropdownListboxButtonType = { @@ -103,16 +109,23 @@ export const Dropdown = ({ placeholder, ConnectButtonPhrase, ConnectButtonCallback, + switchTitles = false, + maskedAccount, + refreshSelectedItem = false, onSelect, multiple = false, onSelectAll, selectAllCheckboxTitle, selectAllCheckboxDescription, + listboxOptions, DropdownListboxButton = DropdownListboxButtonDefault, disabled = false, }: { items: Array placeholder: string + switchTitles: boolean + maskedAccount?: string + refreshSelectedItem: boolean onSelect: ( selected: Array | DropdownSelectListItem ) => void @@ -125,6 +138,9 @@ export const Dropdown = ({ onSelectAll?: (val: Array) => void selectAllCheckboxTitle?: string selectAllCheckboxDescription?: string + listboxOptions?: { + topAction: JSX.Element + } DropdownListboxButton?: ({ selectedItem, selectedItems, @@ -147,8 +163,12 @@ export const Dropdown = ({ */ const [selectedItem, setSelectedItem] = useState< DropdownSelectListItem | undefined - >(() => { - if (!multiple) return defaultItems?.[0] as DropdownSelectListItem + >(!multiple && (defaultItems?.[0] as DropdownSelectListItem)) + + useEffect(() => { + if (!refreshSelectedItem) return + const item = items.find((i) => i.value === defaultItems?.[0].value) + setSelectedItem(item) }) /** @@ -212,11 +232,19 @@ export const Dropdown = ({ className="border border-gray-300 shadow-lg rounded-lg absolute w-full mt-1 bg-white pt-3 space-y-3 z-10 dark:bg-[#1F2937] dark:border-gray-600" > + {listboxOptions?.topAction && ( + <> + {listboxOptions.topAction} +
+ + )} + {items?.length ? ( multiple ? ( /** * Multi select */ + <>
- - {item.title} - +
+ + {maskedAccount === item.value && item.mask + ? switchTitles + ? item.mask.title + : item.title + : item.title} + + {maskedAccount === item.value && item.mask && ( + + )} +
{item.subtitle ? ( - {item.subtitle} + {maskedAccount === item.value && item.mask + ? switchTitles + ? `${adjustAccountTypeToDisplay( + item.type + )} - ${item.title}` + : `${adjustAccountTypeToDisplay( + item.type + )} - ${item.mask.address}` + : `${adjustAccountTypeToDisplay( + item.type + )} - ${item.title}`} ) : null}
@@ -368,6 +415,17 @@ export const Dropdown = ({ > {item.title} + {preselected && + maskedAccount === item.value && + item.mask ? ( + + {`Masked | ${item.mask.title}`} + + ) : null} {item.subtitle ? ( void checked?: boolean diff --git a/packages/design-system/src/atoms/pills/EmailMaskPill.tsx b/packages/design-system/src/atoms/pills/EmailMaskPill.tsx new file mode 100644 index 0000000000..f3ae8ac75a --- /dev/null +++ b/packages/design-system/src/atoms/pills/EmailMaskPill.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { Text } from '../text/Text' +import { Pill } from './Pill' + +import { TbShield, TbShieldOff } from 'react-icons/tb' + +type IconType = typeof TbShield | typeof TbShieldOff + +type BaseEmailPillProps = { + title: string + IconComponent: IconType +} + +const BaseEmailPill = ({ title, IconComponent }: BaseEmailPillProps) => ( + + + + {title} + + +) + +export const EmailMaskedPill = () => ( + +) + +export const EmailUnmaskedPill = () => ( + +) diff --git a/packages/design-system/src/atoms/providers/Email.tsx b/packages/design-system/src/atoms/providers/Email.tsx new file mode 100644 index 0000000000..a00266094a --- /dev/null +++ b/packages/design-system/src/atoms/providers/Email.tsx @@ -0,0 +1,4 @@ +import React from 'react' +import { HiOutlineMail } from 'react-icons/hi' + +export const WrappedSVG = diff --git a/packages/design-system/src/templates/authorization/Authorization.tsx b/packages/design-system/src/templates/authorization/Authorization.tsx index d96d21e8f2..7be76733d6 100644 --- a/packages/design-system/src/templates/authorization/Authorization.tsx +++ b/packages/design-system/src/templates/authorization/Authorization.tsx @@ -1,5 +1,10 @@ import React from 'react' +import { + EmailMaskedPill, + EmailUnmaskedPill, +} from '@proofzero/design-system/src/atoms/pills/EmailMaskPill' import { Avatar } from '../../atoms/profile/avatar/Avatar' +import { InputToggle } from '../../atoms/form/InputToggle' import { Text } from '../../atoms/text/Text' import authorizeCheck from './authorize-check.svg' import subtractLogo from '../../assets/subtract-logo.svg' @@ -55,6 +60,9 @@ type AuthorizationProps = { selectEmailCallback: (selected: DropdownSelectListItem) => void selectedEmail?: DropdownSelectListItem + maskEmail: boolean + setMaskEmail: (state: boolean) => void + connectedAccounts?: Array addNewAccountCallback: () => void selectAccountsCallback: (selected: Array) => void @@ -85,6 +93,8 @@ export default ({ addNewEmailCallback, selectEmailCallback, selectedEmail, + maskEmail, + setMaskEmail, connectedAccounts, addNewAccountCallback, selectAccountsCallback, @@ -197,13 +207,22 @@ export default ({
- - {scopeMeta.scopes[scope].name} - +
+ + {scopeMeta.scopes[scope].name} + + {scope === 'email' ? ( + maskEmail ? ( + + ) : ( + + ) + ) : null} +
{!selectedItem && !selectedItems?.length && !allItemsSelected && ( @@ -225,16 +244,18 @@ export default ({ {selectedItem?.title?.length && ( - {selectedItem.title} + {maskEmail && selectedItem.mask + ? selectedItem.mask.title + : selectedItem.title} )} {selectedItems?.length > 1 && !allItemsSelected && ( {selectedItems?.length} items selected @@ -342,8 +363,27 @@ export default ({ items={connectedEmails} defaultItems={[selectedEmail]} placeholder="Select an Email Address" - onSelect={(selectedItem: DropdownSelectListItem) => { - selectEmailCallback(selectedItem) + refreshSelectedItem={true} + maskedAccount={maskEmail && selectedEmail.value} + onSelect={selectEmailCallback} + listboxOptions={{ + topAction: ( +
+ + Mask Email + + setMaskEmail(!maskEmail)} + checked={maskEmail} + /> +
+ ), }} ConnectButtonPhrase="Connect New Email Account" ConnectButtonCallback={addNewEmailCallback} @@ -357,11 +397,9 @@ export default ({ - ) => { - selectAccountsCallback(selectedItems) - }} + switchTitles={true} + maskedAccount={maskEmail && selectedEmail.value} + onSelect={selectAccountsCallback} onSelectAll={selectAllAccountsCallback} placeholder="Select at least one" ConnectButtonPhrase="Connect New Account" diff --git a/packages/security/package.json b/packages/security/package.json index 6a1a82fa6f..72cd97ad42 100644 --- a/packages/security/package.json +++ b/packages/security/package.json @@ -28,6 +28,7 @@ "dependencies": { "@types/psl": "1.1.0", "cross-env": "7.0.3", + "lodash": "4.17.21", "psl": "1.9.0" }, "packageManager": "yarn@3.2.4", diff --git a/packages/security/persona.ts b/packages/security/persona.ts index b709126b77..24256e61df 100644 --- a/packages/security/persona.ts +++ b/packages/security/persona.ts @@ -1,3 +1,5 @@ +import uniqWith from 'lodash/uniqWith' + import { AuthorizationURN, AuthorizationURNSpace, @@ -63,7 +65,8 @@ export async function validatePersonaData( accountProfile.type !== OAuthAccountType.Google && accountProfile.type !== OAuthAccountType.Microsoft && accountProfile.type !== OAuthAccountType.Apple && - accountProfile.type !== EmailAccountType.Email + accountProfile.type !== EmailAccountType.Email && + accountProfile.type !== EmailAccountType.Mask ) throw new BadRequestError({ message: 'Account provided is not an email-compatible account', @@ -200,12 +203,20 @@ export type ClaimName = string export type ScopeValueName = string export type ClaimValuePairs = Record +type AccountClaim = { + type: string + identifier: string +} + +export type ClaimMeta = { + urns: AnyURN[] + source?: AccountClaim + valid: boolean +} + export type ScopeClaimsResponse = { claims: ClaimValuePairs - meta: { - urns: AnyURN[] - valid: boolean - } + meta: ClaimMeta } export type ClaimData = { @@ -222,6 +233,21 @@ export type ScopeClaimRetrieverFunction = ( traceSpan: TraceSpan ) => Promise +type EmailScopeClaim = { + claims: { + type: EmailAccountType + email: string + } + meta: ClaimMeta +} + +type ConnectedAccountsScopeClaim = { + claims: { + connected_accounts: Array + } + meta: ClaimMeta +} + function createInvalidClaimDataObject(scopeEntry: ScopeValueName): ClaimData { return { [scopeEntry]: { @@ -264,14 +290,32 @@ async function emailClaimRetriever( tag: EDGE_HAS_REFERENCE_TO, }, }) - const emailAccount = edgesResults.edges[0].dst.qc.alias + + const { addr_type: type } = edgesResults.edges[0].dst.rc + const { alias: email, source: sourceURN } = edgesResults.edges[0].dst.qc + + let source + + if (sourceURN) { + const node = await coreClient.edges.findNode.query({ + baseUrn: sourceURN as AccountURN, + }) + if (node) { + const type = node.rc.addr_type + const identifier = node.qc.alias + source = { type, identifier } + } + } + const claimData: ClaimData = { [scopeEntry]: { claims: { - email: emailAccount, + email, + type, }, meta: { urns: [emailAccountUrn], + source, valid: true, }, }, @@ -379,6 +423,11 @@ async function erc4337ClaimsRetriever( return result } +type ConnectedAccount = { + type: string + identifier: string +} + async function connectedAccountsClaimsRetriever( scopeEntry: ScopeValueName, identityURN: IdentityURN, @@ -391,10 +440,10 @@ async function connectedAccountsClaimsRetriever( const result = { connected_accounts: { claims: { - connected_accounts: new Array(), + connected_accounts: new Array(), }, meta: { - urns: new Array(), + urns: new Array(), valid: true, }, }, @@ -442,7 +491,9 @@ async function connectedAccountsClaimsRetriever( type: accountNode.rc.addr_type, identifier: accountNode.qc.alias, }) - result.connected_accounts.meta.urns.push(accountNode.baseUrn) + result.connected_accounts.meta.urns.push( + accountNode.baseUrn as AccountURN + ) }) } return result @@ -528,3 +579,48 @@ export const userClaimsFormatter = ( } return result } + +const formatEmailScopeClaim = (scope: EmailScopeClaim) => { + if (!scope.claims.email.endsWith('.gate@rollup.email')) return + if (!scope.meta.source) return + scope.claims.type = EmailAccountType.Email +} + +const formatConnectedAccounts = ( + connectedAccounts: ConnectedAccountsScopeClaim, + email: EmailScopeClaim +) => { + if (!email.meta.source) return + const { connected_accounts: accounts } = connectedAccounts.claims + if (Array.isArray(accounts)) { + for (const a of accounts) { + if (a.type === EmailAccountType.Mask) { + a.type = EmailAccountType.Email + } + if (a.identifier === email.meta.source.identifier) { + a.type = EmailAccountType.Email + a.identifier = email.claims.email + } + } + + connectedAccounts.claims.connected_accounts = uniqWith( + accounts, + (a, b) => a.type === b.type && a.identifier === b.identifier + ) + } +} + +export const maskedAccountFormatter = (claims: ClaimData) => { + if (claims.email) { + const emailScope = claims.email as unknown as EmailScopeClaim + formatEmailScopeClaim(emailScope) + + if (claims.connected_accounts) { + const connectedAccountsScope = + claims.connected_accounts as ConnectedAccountsScopeClaim + formatConnectedAccounts(connectedAccountsScope, emailScope) + } + } + + return claims +} diff --git a/packages/utils/getNormalisedConnectedAccounts.tsx b/packages/utils/getNormalisedConnectedAccounts.tsx index 3b6c03037b..d995f941ec 100644 --- a/packages/utils/getNormalisedConnectedAccounts.tsx +++ b/packages/utils/getNormalisedConnectedAccounts.tsx @@ -50,7 +50,11 @@ export const getEmailIcon = (type: string): JSX.Element => { } export const adjustAccountTypeToDisplay = ( - accountType: OAuthAccountType | EmailAccountType | CryptoAccountType | WebauthnAccountType + accountType: + | OAuthAccountType + | EmailAccountType + | CryptoAccountType + | WebauthnAccountType ) => { if (accountType === CryptoAccountType.Wallet) { return 'SC Wallet' @@ -63,35 +67,57 @@ export const getEmailDropdownItems = ( ): Array => { if (!connectedAccounts) return [] + const emailAddressTypes = [EmailAccountType.Email] + const oauthAddressTypes = [ + OAuthAccountType.Apple, + OAuthAccountType.Google, + OAuthAccountType.Microsoft, + ] + const filteredEmailsFromConnectedAccounts = connectedAccounts.filter( - (account) => { - return ( - (account.rc.node_type === NodeType.OAuth && - (account.rc.addr_type === OAuthAccountType.Google || - account.rc.addr_type === OAuthAccountType.Microsoft || - account.rc.addr_type === OAuthAccountType.Apple)) || - (account.rc.node_type === NodeType.Email && - account.rc.addr_type === EmailAccountType.Email) - ) + ({ rc: { addr_type, node_type } }) => { + switch (node_type) { + case NodeType.Email: + return emailAddressTypes.includes(addr_type as EmailAccountType) + case NodeType.OAuth: { + return oauthAddressTypes.includes(addr_type as OAuthAccountType) + } + } } ) - return filteredEmailsFromConnectedAccounts.map((account, i) => { + const maskEmailAccounts = connectedAccounts.filter( + ({ rc: { addr_type } }) => addr_type === EmailAccountType.Mask + ) + + return filteredEmailsFromConnectedAccounts.map((account) => { + const maskAccount = maskEmailAccounts.find( + (a) => a.qc.source === account.baseUrn + ) return { - // There's a problem when passing icon down to client (since icon is a JSX.Element) - // My guess is that it should be rendered on the client side only. - // that's why I'm passing type (as subtitle) instead of icon and then substitute it - // with icon on the client side - subtitle: account.rc.addr_type as - | OAuthAccountType - | EmailAccountType - | CryptoAccountType, - title: account.qc.alias, - value: account.baseUrn as AccountURN, + ...decorateAccountDropdownItem(account), + mask: maskAccount ? decorateAccountDropdownItem(maskAccount) : undefined, } }) } +export const decorateAccountDropdownItem = (account) => { + return { + address: account.qc.alias, + type: account.rc.addr_type, + // There's a problem when passing icon down to client (since icon is a JSX.Element) + // My guess is that it should be rendered on the client side only. + // that's why I'm passing type (as subtitle) instead of icon and then substitute it + // with icon on the client side + subtitle: account.rc.addr_type as + | OAuthAccountType + | EmailAccountType + | CryptoAccountType, + title: account.qc.alias, + value: account.baseUrn as AccountURN, + } +} + //accountDropdownItems export const getAccountDropdownItems = ( accountProfiles?: Array | null @@ -99,10 +125,13 @@ export const getAccountDropdownItems = ( if (!accountProfiles) return [] return accountProfiles.map((account) => { return { + address: account.address, title: account.title, + type: account.type, value: account.id as AccountURN, - subtitle: `${adjustAccountTypeToDisplay(account.type)} - ${account.address - }`, + subtitle: `${adjustAccountTypeToDisplay(account.type)} - ${ + account.address + }`, } }) } diff --git a/platform/account/src/jsonrpc/methods/getAccountProfile.ts b/platform/account/src/jsonrpc/methods/getAccountProfile.ts index 9fd454da9c..1bcbd49654 100644 --- a/platform/account/src/jsonrpc/methods/getAccountProfile.ts +++ b/platform/account/src/jsonrpc/methods/getAccountProfile.ts @@ -14,6 +14,7 @@ import { AccountURNInput } from '@proofzero/platform-middleware/inputValidators' import type { Context } from '../../context' import { + AccountNode, AppleAccount, CryptoAccount, DiscordAccount, @@ -27,13 +28,21 @@ import { WebauthnAccount, } from '../../nodes' -import { AccountProfileSchema } from '../validators/profile' +import { + AccountProfileSchema, + MaskAccountProfileSchema, +} from '../validators/profile' import OAuthAccount from '../../nodes/oauth' import { AccountURN, AccountURNSpace } from '@proofzero/urns/account' -export const GetAccountProfileOutput = AccountProfileSchema.extend({ - id: AccountURNInput, -}) +export const GetAccountProfileOutput = z.union([ + MaskAccountProfileSchema.extend({ + id: AccountURNInput, + }), + AccountProfileSchema.extend({ + id: AccountURNInput, + }), +]) export const GetAccountProfileBatchInput = z.array(AccountURNInput) export const GetAccountProfileBatchOutput = z.array(GetAccountProfileOutput) @@ -74,65 +83,79 @@ export const getAccountProfileBatchMethod = async ({ const nodeClient = initAccountNodeByName(baseURN, ctx.env.Account) resultPromises.push(getProfile(ctx, nodeClient, accountURN)) } - return await Promise.all(resultPromises) + return Promise.all(resultPromises) } async function getProfile( ctx: Context, - nodeClient: ReturnType, + node: AccountNode, accountURN: AccountURN ) { - const address = await nodeClient?.class.getAddress() - const type = await nodeClient?.class.getType() + const address = await node.class.getAddress() + const type = await node.class.getType() if (!address || !type) { throw new InternalServerError({ message: 'missing address or type' }) } if (!accountURN) throw new BadRequestError({ message: 'missing accountURN' }) - const getProfileNode = (): - | ContractAccount - | CryptoAccount - | EmailAccount - | WebauthnAccount - | OAuthAccount - | undefined => { + const getAccount = (node: AccountNode) => { switch (type) { case CryptoAccountType.ETH: - return new CryptoAccount(nodeClient) + return new CryptoAccount(node) case CryptoAccountType.Wallet: - return new ContractAccount(nodeClient) + return new ContractAccount(node) + case EmailAccountType.Mask: case EmailAccountType.Email: - return new EmailAccount(nodeClient, ctx.env) + return new EmailAccount(node, ctx.env) case WebauthnAccountType.WebAuthN: - return new WebauthnAccount(nodeClient) + return new WebauthnAccount(node) case OAuthAccountType.Apple: - return new AppleAccount(nodeClient, ctx.env) + return new AppleAccount(node, ctx.env) case OAuthAccountType.Discord: - return new DiscordAccount(nodeClient, ctx.env) + return new DiscordAccount(node, ctx.env) case OAuthAccountType.GitHub: - return new GithubAccount(nodeClient) + return new GithubAccount(node) case OAuthAccountType.Google: - return new GoogleAccount(nodeClient, ctx.env) + return new GoogleAccount(node, ctx.env) case OAuthAccountType.Microsoft: - return new MicrosoftAccount(nodeClient, ctx.hashedIdref!, ctx.env) + return new MicrosoftAccount(node, ctx.hashedIdref!, ctx.env) case OAuthAccountType.Twitter: - return new TwitterAccount(nodeClient) + return new TwitterAccount(node) } } - const node = getProfileNode() - if (!node) { + const account = getAccount(node) + if (!account) { throw new InternalServerError({ message: 'unsupported account type', cause: { type }, }) } - const profile = await node.getProfile() + const id = accountURN + + const profile = await account.getProfile() + + if (account instanceof EmailAccount && type === EmailAccountType.Mask) { + const sourceAccountURN = await account.getSourceAccount() + if (sourceAccountURN) { + const sourceAccount = getAccount( + initAccountNodeByName(sourceAccountURN, ctx.env.Account) + ) + if (sourceAccount) { + const source = await sourceAccount.getProfile() + return { + ...profile, + id, + source, + } + } + } + } return { - id: accountURN, ...profile, + id, } } diff --git a/platform/account/src/jsonrpc/methods/getMaskedAddress.ts b/platform/account/src/jsonrpc/methods/getMaskedAddress.ts new file mode 100644 index 0000000000..964b0e33ab --- /dev/null +++ b/platform/account/src/jsonrpc/methods/getMaskedAddress.ts @@ -0,0 +1,45 @@ +import { z } from 'zod' + +import { BadRequestError } from '@proofzero/errors' +import { EmailAccountType, OAuthAccountType } from '@proofzero/types/account' + +import { Context } from '../../context' +import EmailAccount from '../../nodes/email' + +export const GetMaskedAddressInput = z.object({ + clientId: z.string(), +}) +export const GetMaskedAddressOutput = z.string() + +type GetMaskedAddressInput = z.infer +type GetMaskedAddressOutput = z.infer + +type GetMaskedAddressParams = { + ctx: Context + input: GetMaskedAddressInput +} + +interface GetMaskedAddressMethod { + (params: GetMaskedAddressParams): Promise +} + +export const getMaskedAddressMethod: GetMaskedAddressMethod = async ({ + ctx, + input, +}) => { + if (!ctx.account) throw new BadRequestError({ message: 'missing account' }) + + const accountType = await ctx.account.class.getType() + switch (accountType) { + case EmailAccountType.Email: + case OAuthAccountType.Apple: + case OAuthAccountType.Google: + case OAuthAccountType.Microsoft: + break + default: + throw new BadRequestError({ message: 'invalid account type' }) + } + + const node = new EmailAccount(ctx.account, ctx.env) + return node.getMaskedAddress(input.clientId) +} diff --git a/platform/account/src/jsonrpc/methods/setSourceAccount.ts b/platform/account/src/jsonrpc/methods/setSourceAccount.ts new file mode 100644 index 0000000000..d43379e7f7 --- /dev/null +++ b/platform/account/src/jsonrpc/methods/setSourceAccount.ts @@ -0,0 +1,43 @@ +import { z } from 'zod' + +import { BadRequestError } from '@proofzero/errors' +import { AccountURNInput } from '@proofzero/platform-middleware/inputValidators' +import { EmailAccountType, OAuthAccountType } from '@proofzero/types/account' + +import { Context } from '../../context' +import EmailAccount from '../../nodes/email' + +export const SetSourceAccountInput = AccountURNInput +export const SetSourceAccountOutput = z.void() + +type SetSourceAccountInput = z.infer +type SetSourceAccountOutput = z.infer + +type SetSourceAccountParams = { + ctx: Context + input: SetSourceAccountInput +} + +interface SetSourceAccountMethod { + (params: SetSourceAccountParams): Promise +} + +export const setSourceAccountMethod: SetSourceAccountMethod = async ({ + ctx, + input, +}) => { + if (!ctx.account) throw new BadRequestError({ message: 'missing account' }) + + const accountType = await ctx.account.class.getType() + switch (accountType) { + case EmailAccountType.Mask: + break + default: + throw new BadRequestError({ + message: `invalid account type: ${accountType}`, + }) + } + + const node = new EmailAccount(ctx.account, ctx.env) + return node.setSourceAccount(input) +} diff --git a/platform/account/src/jsonrpc/router.ts b/platform/account/src/jsonrpc/router.ts index ac9b0ea2a8..e8229a3b94 100644 --- a/platform/account/src/jsonrpc/router.ts +++ b/platform/account/src/jsonrpc/router.ts @@ -128,6 +128,16 @@ import { ConnectIdentityGroupEmailOutputSchema, connectIdentityGroupEmail, } from './methods/identity-groups/connectIdentityGroupEmail' +import { + getMaskedAddressMethod, + GetMaskedAddressInput, + GetMaskedAddressOutput, +} from './methods/getMaskedAddress' +import { + setSourceAccountMethod, + SetSourceAccountInput, + SetSourceAccountOutput, +} from './methods/setSourceAccount' const t = initTRPC.context().create({ errorFormatter }) @@ -356,4 +366,20 @@ export const appRouter = t.router({ .input(ConnectIdentityGroupEmailInputSchema) .output(ConnectIdentityGroupEmailOutputSchema) .mutation(connectIdentityGroupEmail), + getMaskedAddress: t.procedure + .use(LogUsage) + .use(parse3RN) + .use(setAccountNodeClient) + .use(Analytics) + .input(GetMaskedAddressInput) + .output(GetMaskedAddressOutput) + .query(getMaskedAddressMethod), + setSourceAccount: t.procedure + .use(LogUsage) + .use(parse3RN) + .use(setAccountNodeClient) + .use(Analytics) + .input(SetSourceAccountInput) + .output(SetSourceAccountOutput) + .mutation(setSourceAccountMethod), }) diff --git a/platform/account/src/jsonrpc/validators/profile.ts b/platform/account/src/jsonrpc/validators/profile.ts index 46c2f17e90..586072d21c 100644 --- a/platform/account/src/jsonrpc/validators/profile.ts +++ b/platform/account/src/jsonrpc/validators/profile.ts @@ -16,6 +16,7 @@ export const AccountProfileSchema = z.object({ type: z.union([ z.literal(CryptoAccountType.ETH), z.literal(CryptoAccountType.Wallet), + z.literal(EmailAccountType.Mask), z.literal(EmailAccountType.Email), z.literal(WebauthnAccountType.WebAuthN), z.literal(OAuthAccountType.Apple), @@ -26,3 +27,7 @@ export const AccountProfileSchema = z.object({ z.literal(OAuthAccountType.Twitter), ]), }) + +export const MaskAccountProfileSchema = AccountProfileSchema.extend({ + source: AccountProfileSchema, +}) diff --git a/platform/account/src/nodes/email.ts b/platform/account/src/nodes/email.ts index 1fa4a71164..1226ee1e38 100644 --- a/platform/account/src/nodes/email.ts +++ b/platform/account/src/nodes/email.ts @@ -2,6 +2,7 @@ import { DurableObjectStubProxy } from 'do-proxy' import { BadRequestError, InternalServerError } from '@proofzero/errors' import { EmailAccountType, NodeType } from '@proofzero/types/account' +import { type AccountURN } from '@proofzero/urns/account' import generateRandomString from '@proofzero/utils/generateRandomString' import type { Environment } from '@proofzero/platform.core' @@ -12,7 +13,7 @@ import { EMAIL_VERIFICATION_OPTIONS } from '../constants' import { AccountNode } from '.' import Account from './account' -type EmailAccountProfile = AccountProfile +type EmailAccountProfile = AccountProfile type VerificationPayload = { state: string @@ -217,22 +218,42 @@ export default class EmailAccount { } async getProfile(): Promise { - const [nickname, gradient, address] = await Promise.all([ + const [nickname, gradient, address, type] = await Promise.all([ this.node.class.getNickname(), this.node.class.getGradient(), this.node.class.getAddress(), + this.node.class.getType(), ]) if (!address) throw new InternalServerError({ message: 'Cannot load profile for email account node', cause: 'missing account', }) + return { address, + type: type as EmailAccountType, title: nickname ?? address, icon: gradient, - type: EmailAccountType.Email, } } + + async getSourceAccount() { + return this.node.storage.get('source-account') + } + + async setSourceAccount(accountURN: AccountURN) { + await this.node.storage.put('source-account', accountURN) + } + + async getMaskedAddress(clientId: string): Promise { + const key = `masked-address/${clientId}` + const stored = await this.node.storage.get(key) + if (stored) return stored + const address = `${generateRandomString(6)}.gate@rollup.email` + await this.node.storage.put(key, address) + return address + } } + export type EmailAccountProxyStub = DurableObjectStubProxy diff --git a/platform/account/src/utils.ts b/platform/account/src/utils.ts index 5b0ce4697f..69949f9f01 100644 --- a/platform/account/src/utils.ts +++ b/platform/account/src/utils.ts @@ -54,6 +54,8 @@ export const isEmailAccountType = (type: string | undefined) => { switch (type) { case EmailAccountType.Email: return NodeType.Email + case EmailAccountType.Mask: + return NodeType.Email default: return false } diff --git a/platform/authorization/src/jsonrpc/methods/getAuthorizedAppScopes.ts b/platform/authorization/src/jsonrpc/methods/getAuthorizedAppScopes.ts index fde299fded..747759a027 100644 --- a/platform/authorization/src/jsonrpc/methods/getAuthorizedAppScopes.ts +++ b/platform/authorization/src/jsonrpc/methods/getAuthorizedAppScopes.ts @@ -23,6 +23,12 @@ export const GetAuthorizedAppScopesMethodOutput = z.object({ claims: z.record(z.string(), z.any()), meta: z.object({ urns: z.array(z.string()), + source: z + .object({ + type: z.string(), + identifier: z.string(), + }) + .optional(), valid: z.boolean(), }), }) diff --git a/platform/authorization/src/jsonrpc/methods/getUserInfo.ts b/platform/authorization/src/jsonrpc/methods/getUserInfo.ts index 5caa910715..b246a0a8cc 100644 --- a/platform/authorization/src/jsonrpc/methods/getUserInfo.ts +++ b/platform/authorization/src/jsonrpc/methods/getUserInfo.ts @@ -12,6 +12,7 @@ import { initAuthorizationNodeByName } from '../../nodes' import { getClaimValues, + maskedAccountFormatter, userClaimsFormatter, } from '@proofzero/security/persona' import { PersonaData } from '@proofzero/types/application' @@ -61,9 +62,8 @@ export const getUserInfoMethod = async ({ if (error) throw getErrorCause(error) - const personaData = await authorizationNode.storage.get( - 'personaData' - ) + const personaData = + await authorizationNode.storage.get('personaData') const claimValues = await getClaimValues( identityURN, clientId, @@ -79,6 +79,9 @@ export const getUserInfoMethod = async ({ message: 'Authorized data error. Re-authorization by user required', }) } + + const claims = userClaimsFormatter(maskedAccountFormatter(claimValues)) + //`sub` is a mandatory field in the userinfo result - return { ...userClaimsFormatter(claimValues), sub: jwt.sub } + return { ...claims, sub: jwt.sub } } diff --git a/platform/core/src/context.ts b/platform/core/src/context.ts index 84090e6bda..e7802934f2 100644 --- a/platform/core/src/context.ts +++ b/platform/core/src/context.ts @@ -14,7 +14,8 @@ import { generateTraceSpan, } from '@proofzero/platform-middleware/trace' -import { +import type { Account } from '@proofzero/platform.account/src' +import type { Authorization, ExchangeCode, } from '@proofzero/platform.authorization/src' @@ -23,7 +24,6 @@ import type { Identity } from '@proofzero/platform.identity' import * as db from '@proofzero/platform.edges/src/db' import type { Environment } from './types' -import type { AccountNode } from '@proofzero/platform.account/src/nodes' export const GeoContext = 'com.kubelt.geo/location' @@ -50,7 +50,7 @@ export interface CreateInnerContextOptions authorizationNode?: DurableObjectStubProxy identityNode?: DurableObjectStubProxy - account?: AccountNode + account?: DurableObjectStubProxy account3RN?: AccountURN accountURN?: AccountURN alias?: string diff --git a/platform/identity/src/jsonrpc/validators/profile.ts b/platform/identity/src/jsonrpc/validators/profile.ts index 30ca11152a..bef121e038 100644 --- a/platform/identity/src/jsonrpc/validators/profile.ts +++ b/platform/identity/src/jsonrpc/validators/profile.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { inputValidators } from '@proofzero/platform-middleware' +import { AccountURNInput } from '@proofzero/platform-middleware/inputValidators' import { Node } from '../../../../edges/src/jsonrpc/validators/node' export const ProfileSchema = z.object({ @@ -10,7 +10,7 @@ export const ProfileSchema = z.object({ isToken: z.boolean().optional(), }) .optional(), - primaryAccountURN: inputValidators.AccountURNInput.optional(), + primaryAccountURN: AccountURNInput.optional(), }) -export const AccountsSchema = z.array(Node) +export const AccountsSchema = z.array(Node.extend({ baseUrn: AccountURNInput })) diff --git a/yarn.lock b/yarn.lock index 47b29c9ab9..f431b4f29c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6751,6 +6751,7 @@ __metadata: "@types/psl": 1.1.0 cross-env: 7.0.3 eslint: 8.28.0 + lodash: 4.17.21 npm-run-all: 4.1.5 prettier: 2.7.1 psl: 1.9.0