diff --git a/.yarn/cache/random-words-npm-2.0.0-5c0ece92ba-d574955cc5.zip b/.yarn/cache/random-words-npm-2.0.0-5c0ece92ba-d574955cc5.zip new file mode 100644 index 0000000000..17cb9b1550 Binary files /dev/null and b/.yarn/cache/random-words-npm-2.0.0-5c0ece92ba-d574955cc5.zip differ diff --git a/.yarn/cache/seedrandom-npm-3.0.5-6946e8f8db-728b56bc3b.zip b/.yarn/cache/seedrandom-npm-3.0.5-6946e8f8db-728b56bc3b.zip new file mode 100644 index 0000000000..c2f6b0903a Binary files /dev/null and b/.yarn/cache/seedrandom-npm-3.0.5-6946e8f8db-728b56bc3b.zip differ diff --git a/apps/passport/app/components/accounts/AccountList.tsx b/apps/passport/app/components/accounts/AccountList.tsx index b282a39cc8..39b8c709fa 100644 --- a/apps/passport/app/components/accounts/AccountList.tsx +++ b/apps/passport/app/components/accounts/AccountList.tsx @@ -1,6 +1,8 @@ import { AccountURNSpace } from '@proofzero/urns/account' import type { AccountURN } from '@proofzero/urns/account' +import { EmailAccountType } from '@proofzero/types/account' + import { List } from '@proofzero/design-system/src/atoms/lists/List' import { Text } from '@proofzero/design-system/src/atoms/text/Text' @@ -22,13 +24,15 @@ export const AccountList = ({ }: AccountListProps) => { return accounts.length ? ( ({ - key: ali.id, - val: ali, - primary: - AccountURNSpace.decode(ali.id as AccountURN) === - AccountURNSpace.decode(primaryAccountURN), - }))} + items={accounts + .filter((ali) => ali.type !== EmailAccountType.Mask) + .map((ali) => ({ + key: ali.id, + val: ali, + primary: + AccountURNSpace.decode(ali.id as AccountURN) === + AccountURNSpace.decode(primaryAccountURN), + }))} itemRenderer={(item) => ( {connectedAccounts && (
- + {account.type === EmailAccountType.Mask ? ( + + ) : ( + + )} {account.address} + {account.type === EmailAccountType.Mask ? : null}
)} @@ -171,12 +181,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 +215,7 @@ export const ClaimsMobileView = ({ scopes }: { scopes: any[] }) => { > {appAskedFor} + {masked && } {whatsBeingShared && ( { > {appAskedFor} + {masked && } {whatsBeingShared && ( { size={20} text={scWallets ? a.title! : a.address} avatarURL={a.icon} + masked={a.type === EmailAccountType.Mask} onClick={() => setSelectedAccount(a)} className={'pointer-events-auto'} /> @@ -421,11 +436,10 @@ export const ClaimsMobileView = ({ scopes }: { scopes: any[] }) => { - } + sourceOfDataIcon={} dropdown={false} /> ) @@ -491,12 +505,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 +543,19 @@ export const ClaimsWideView = ({ scopes }: { scopes: any[] }) => { > {appAskedFor} + {masked && } ) : ( - - {appAskedFor} - +
+ + {appAskedFor} + + {masked && } +
)} @@ -636,6 +656,7 @@ export const ClaimsWideView = ({ scopes }: { scopes: any[] }) => { | { icon: string address: string + source: string type: string title?: string } @@ -675,7 +696,9 @@ export const ClaimsWideView = ({ scopes }: { scopes: any[] }) => { Wallet(s) ) : ( - a.icon)!} /> + a.icon).map((a) => a.icon)} + /> )} @@ -706,6 +729,7 @@ export const ClaimsWideView = ({ scopes }: { scopes: any[] }) => { size={20} text={scWallets ? a.title! : a.address} avatarURL={a.icon} + masked={a.type === EmailAccountType.Mask} onClick={() => setSelectedAccount(a)} className={'pointer-events-auto'} /> @@ -717,7 +741,8 @@ export const ClaimsWideView = ({ scopes }: { scopes: any[] }) => { source={ source ? source - : `${startCase(selectedAccount.type)} - ${ + : selectedAccount.source || + `${startCase(selectedAccount.type)} - ${ selectedAccount.address }` } @@ -754,11 +779,10 @@ export const ClaimsWideView = ({ scopes }: { scopes: any[] }) => { - } + masked={scope.masked} + whatsBeingShared={scope.address} + sourceOfData={scope.masked ? scope.source : scope.address} + sourceOfDataIcon={} dropdown={false} /> ) diff --git a/apps/passport/app/routes/authorize.tsx b/apps/passport/app/routes/authorize.tsx index 8ab83585d8..2160059ecb 100644 --- a/apps/passport/app/routes/authorize.tsx +++ b/apps/passport/app/routes/authorize.tsx @@ -325,9 +325,7 @@ export const action: ActionFunction = async ({ request, context }) => { const scope = (form.get('scopes') as string).split(' ') /* This stores the selection made from the user in the authorization screen; gets validated and stored for later retrieval at token generation stage */ - const personaData = JSON.parse( - form.get('personaData') as string - ) as PersonaData + let personaData = JSON.parse(form.get('personaData') as string) as PersonaData const state = form.get('state') as string const clientId = form.get('client_id') as string @@ -397,13 +395,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 +410,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 +433,54 @@ export default function Authorize() { } return selected }) + + const [maskEmail, setMaskEmail] = useState(false) + useEffect(() => { + setMaskEmailCallback() + }) + + const [loadingMaskEmail, setLoadingMaskEmail] = useState(false) + + const setMaskEmailCallback = async () => { + if (!maskEmail) return + + const accountURN = selectedEmail?.value + if (!accountURN) return + if (selectedEmail.mask) return + + setLoadingMaskEmail(true) + + const response = await fetch('/create/account-mask', { + body: JSON.stringify({ accountURN, clientId }), + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }) + + let maskAccount = selectedEmail + const [mask] = await response.json() + + setDataForScopes((state) => ({ + ...state, + connectedAccounts: connectedAccounts.map((ca) => { + if (ca.value !== accountURN) return ca + return { + ...ca, + mask, + } + }), + connectedEmails: connectedEmails.map((ce) => { + if (ce.value !== accountURN) return ce + maskAccount = { + ...ce, + mask, + } + return maskAccount + }), + })) + setSelectedEmail(maskAccount) + setLoadingMaskEmail(false) + } + const [selectedConnectedAccounts, setSelectedConnectedAccounts] = useState< Array | Array >(() => { @@ -510,7 +557,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 +570,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 +694,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 +718,9 @@ export default function Authorize() { }} selectEmailCallback={setSelectedEmail} selectedEmail={selectedEmail} + maskEmail={maskEmail} + loadingMaskEmail={loadingMaskEmail} + 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..c9947c27ec --- /dev/null +++ b/apps/passport/app/routes/create/account-mask.ts @@ -0,0 +1,62 @@ +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 { getAccountDropdownItems } from '@proofzero/utils/getNormalisedConnectedAccounts' + +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, + clientId, + } + const rc = { node_type: NodeType.Email, addr_type: EmailAccountType.Mask } + + const maskAccountURN = AccountURNSpace.componentizedUrn( + generateHashedIDRef(EmailAccountType.Mask, address), + rc, + qc + ) + + const maskAccountCoreClient = getCoreClient({ + context, + jwt, + accountURN: maskAccountURN, + }) + + await maskAccountCoreClient.account.resolveIdentity.query({ jwt }) + await maskAccountCoreClient.account.setSourceAccount.mutate(accountURN) + + const profile = + await maskAccountCoreClient.account.getAccountProfile.query() + + return json(getAccountDropdownItems([profile])) + } +) diff --git a/apps/passport/app/routes/settings/applications/$clientId/index.tsx b/apps/passport/app/routes/settings/applications/$clientId/index.tsx index fe8ebd3d18..001dd351b7 100644 --- a/apps/passport/app/routes/settings/applications/$clientId/index.tsx +++ b/apps/passport/app/routes/settings/applications/$clientId/index.tsx @@ -20,6 +20,7 @@ import { getRollupReqFunctionErrorWrapper } from '@proofzero/utils/errors' import { getCoreClient } from '~/platform.server' import { getValidatedSessionContext } from '~/session.server' import type { ScopeMeta } from '@proofzero/security/scopes' +import { EmailAccountType } from '@proofzero/types/account' export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper( async ({ request, params, context }) => { @@ -71,17 +72,27 @@ export default () => { ) continue if (scopeValue === 'email') { - const profile = connectedProfiles.find( - //There should be only one account urn provided for email - (profile) => - profile.urn === scopeValues.claimValues[scopeValue].meta.urns[0] - ) + //There should be only one account urn provided for email + const scope = scopeValues.claimValues[scopeValue] + const { meta } = scope + const masked = scope.claims.type === EmailAccountType.Mask + const urn = masked ? meta.source?.urn : meta.urns[0] + const profile = connectedProfiles.find((profile) => profile.urn === urn) + + const claim = 'email' + const address = masked ? scope.claims.email : profile.address + const { icon, type } = profile + const source = scope.meta.source?.identifier || profile.address + const sourceIcon = getDefaultIconUrl(type) + aggregator.push({ - claim: 'email', - icon: profile.icon, - address: profile.address, - type: profile.type, - sourceIcon: getDefaultIconUrl(profile.type), + claim, + icon, + address, + type, + masked, + source, + sourceIcon, }) } else if (scopeValue === 'connected_accounts') { const profiles = connectedProfiles.filter((profile) => @@ -90,11 +101,28 @@ export default () => { aggregator.push({ claim: 'connected_accounts', - accounts: profiles.map((profile) => ({ - icon: profile.icon, - address: profile.address, - type: profile.type === 'eth' ? 'blockchain' : profile.type, - })), + accounts: profiles.map((profile) => { + if (scopeValues.claimValues.email) { + const { email } = scopeValues.claimValues + const masked = + email.claims.type === EmailAccountType.Mask && + profile.urn === email.meta.urns[0] + if (masked) { + const address = email.claims.email + const source = email.meta?.source?.identifier + return { + address, + source, + type: EmailAccountType.Mask, + } + } + } + return { + icon: profile.icon, + address: profile.address, + type: profile.type === 'eth' ? 'blockchain' : profile.type, + } + }), }) } else if (scopeValue === 'profile') { aggregator.push({ 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..6fa0cab607 100644 --- a/apps/passport/app/utils/authorize.server.ts +++ b/apps/passport/app/utils/authorize.server.ts @@ -16,7 +16,11 @@ 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, +} from '@proofzero/types/account' import type { DropdownSelectListItem } from '@proofzero/design-system/src/atoms/dropdown/DropdownSelectList' import type { AccountURN } from '@proofzero/urns/account' @@ -78,14 +82,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 +100,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/ConnectedAccountsDropdown.stories.tsx b/packages/design-system/src/atoms/dropdown/ConnectedAccountsDropdown.stories.tsx deleted file mode 100644 index 21dd93bb16..0000000000 --- a/packages/design-system/src/atoms/dropdown/ConnectedAccountsDropdown.stories.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import { Dropdown, DropdownSelectListItem } from './DropdownSelectList' - -import { - OAuthAccountType, - EmailAccountType, - CryptoAccountType, -} from '@proofzero/types/account' - -import { adjustAccountTypeToDisplay } from '@proofzero/utils/getNormalisedConnectedAccounts' -export default { - title: 'Atoms/Dropdown/ConnectedAccounts', - component: Dropdown, -} - -const pickRandomAccountType = (i: number) => { - const types = [ - OAuthAccountType.Google, - OAuthAccountType.Microsoft, - EmailAccountType.Email, - CryptoAccountType.ETH, - ] - - return types[i % types.length] -} - -const accounts: DropdownSelectListItem[] = Array.from( - { length: 10 }, - (_, i) => ({ - value: `urn:proofzero:account:${i}`, - title: `Account ${i}`, - subtitle: `${adjustAccountTypeToDisplay( - pickRandomAccountType(i) - )} - Account ${i}`, - }) -) - -const Template = () => ( -
- { console.log({ val }) }} - placeholder='No connected account(s)' - ConnectButtonPhrase="Connect New Account" - ConnectButtonCallback={() => { console.log('Connect New Account') }} - multiple={true} - selectAllCheckboxTitle='All Connected Accounts' - selectAllCheckboxDescription='All current and future accounts' - /> -
-) - -export const ConnectedAccountsSelectExample = Template.bind({}) as any diff --git a/packages/design-system/src/atoms/dropdown/DropdownSelectList.tsx b/packages/design-system/src/atoms/dropdown/DropdownSelectList.tsx index 4025203c0a..d39e1a9cac 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,18 @@ 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 { EmailMaskedPill } from '@proofzero/design-system/src/atoms/pills/EmailMaskPill' export type DropdownSelectListItem = { + address: string + type: string title: string value?: string icon?: JSX.Element selected?: boolean subtitle?: string + mask?: DropdownSelectListItem } export type DropdownListboxButtonType = { @@ -103,16 +108,23 @@ export const Dropdown = ({ placeholder, ConnectButtonPhrase, ConnectButtonCallback, + switchTitles = false, + maskAccount, + refreshSelectedItem = false, onSelect, multiple = false, onSelectAll, selectAllCheckboxTitle, selectAllCheckboxDescription, + listboxOptions, DropdownListboxButton = DropdownListboxButtonDefault, disabled = false, }: { items: Array placeholder: string + switchTitles?: boolean + maskAccount?: string + refreshSelectedItem?: boolean onSelect: ( selected: Array | DropdownSelectListItem ) => void @@ -125,6 +137,9 @@ export const Dropdown = ({ onSelectAll?: (val: Array) => void selectAllCheckboxTitle?: string selectAllCheckboxDescription?: string + listboxOptions?: { + topAction: JSX.Element + } DropdownListboxButton?: ({ selectedItem, selectedItems, @@ -151,6 +166,14 @@ export const Dropdown = ({ if (!multiple) return defaultItems?.[0] as DropdownSelectListItem }) + useEffect(() => { + if (!refreshSelectedItem) return + const { value } = defaultItems?.[0] as DropdownSelectListItem + if (typeof value !== 'string') return + const item = items.find((i) => i.value === value) + setSelectedItem(item) + }) + /** * For multi select */ @@ -212,11 +235,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} - +
+ + {maskAccount === item.value && item.mask + ? switchTitles + ? item.mask.title + : item.title + : item.title} + + {maskAccount === item.value && item.mask && ( + + )} +
{item.subtitle ? ( - {item.subtitle} + {maskAccount === item.value && item.mask + ? switchTitles + ? `${adjustAccountTypeToDisplay( + item.type + )} - ${item.title}` + : `${adjustAccountTypeToDisplay( + item.type + )} - ${item.mask.address}` + : `${adjustAccountTypeToDisplay( + item.type + )} - ${item.title}`} ) : null}
@@ -368,6 +418,17 @@ export const Dropdown = ({ > {item.title} + {preselected && + maskAccount === item.value && + item.mask ? ( + + {`Masked | ${item.mask.title}`} + + ) : null} {item.subtitle ? ( { - if (string === CryptoAccountType.Wallet) { - return 'SC Wallet' - } - return string.charAt(0).toUpperCase() + string.slice(1) -} - -const accounts: DropdownSelectListItem[] = Array.from( - { length: 10 }, - (_, i) => ({ - value: `urn:rollupid:account:${i}`, - title: `Smart Contract Wallet ${i}`, - subtitle: `${modifyType( - CryptoAccountType.Wallet as string - )} - SC Wallet: ${i}`, - }) -) - -const Template = () => ( -
- { console.log({ val }) }} - placeholder='Select a Smart Contract Wallet' - ConnectButtonPhrase="New Smart Contract Wallet" - ConnectButtonCallback={() => { console.log('New Smart Contract Wallet') }} - multiple={true} - selectAllCheckboxTitle='All Smart Contract Wallets' - selectAllCheckboxDescription='All current and future SC Wallets' - /> -
-) - -export const SCWalletSelectExample = Template.bind({}) as any diff --git a/packages/design-system/src/atoms/form/InputToggle.tsx b/packages/design-system/src/atoms/form/InputToggle.tsx index afae46138b..a878cafcb6 100644 --- a/packages/design-system/src/atoms/form/InputToggle.tsx +++ b/packages/design-system/src/atoms/form/InputToggle.tsx @@ -4,7 +4,7 @@ import { Text } from '../text/Text' export type InputToggleProps = { id: string - label: string + label?: string name?: string onToggle?: (val: boolean) => void checked?: boolean @@ -33,9 +33,11 @@ export const InputToggle = ({ label && label !== '' ? 'space-x-8' : '' }`} > - - {label} - + {label && ( + + {label} + + )} @@ -33,7 +38,11 @@ export default function Info({ className="!bg-white shadow absolute z-5 w-max" placement={placement} > - {`${name} + {warning ? ( + + ) : ( + {`${name} + )} ) 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/pills/UserPill.tsx b/packages/design-system/src/atoms/pills/UserPill.tsx index 5e1191739a..2cf596e90e 100644 --- a/packages/design-system/src/atoms/pills/UserPill.tsx +++ b/packages/design-system/src/atoms/pills/UserPill.tsx @@ -1,9 +1,12 @@ import React from 'react' +import { TbShield } from 'react-icons/tb' + import { Text } from '../text/Text' export type UserPillProps = { avatarURL: string text: string + masked: boolean size?: number } & React.DetailedHTMLProps< React.HTMLAttributes, @@ -13,6 +16,7 @@ export type UserPillProps = { export default ({ avatarURL, text, + masked = false, size = 64, className, ...buttonProps @@ -21,14 +25,18 @@ export default ({ {...buttonProps} className={`min-w-0 w-fit inline-block rounded py-0.5 pr-2.5 bg-white flex flex-row items-center rounded-full gap-2 pl-1 border border-gray-200 hover:border-indigo-500 focus:border-indigo-500 focus:bg-indigo-50 ${className}`} > - + {masked ? ( + + ) : ( + + )} {text} 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.stories.mdx b/packages/design-system/src/templates/authorization/Authorization.stories.mdx deleted file mode 100644 index 1151a17795..0000000000 --- a/packages/design-system/src/templates/authorization/Authorization.stories.mdx +++ /dev/null @@ -1,125 +0,0 @@ -import { Canvas, Meta, Story } from '@storybook/addon-docs' - -import Authorization from './Authorization' - -import subtractLogo from '../../assets/subtract-logo.svg' - -import { HiOutlineEnvelope } from 'react-icons/hi2' - -import { SCOPES_JSON } from '@proofzero/security/scopes' - -import googleIcon from '@proofzero/design-system/src/atoms/providers/Google' -import microsoftIcon from '@proofzero/design-system/src/atoms/providers/Microsoft' -import appleIcon from '@proofzero/design-system/src/atoms/providers/Apple' -import { OptionType } from '@proofzero/utils/getNormalisedConnectedAccounts' - -import { - NodeType, - OAuthAccountType, - EmailAccountType, - CryptoAccountType, -} from '@proofzero/types/account' - - - - -export const Template = (args) => ( - , - }, - { - title: 'email@microsoft.com', - value: 'urn:rollupid:account/2', - icon: , - }, - { - title: 'perez@apple.com', - value: 'urn:rollupid:account/5', - icon: , - }, - { - title: 'email@yahoo.com', - value: 'urn:rollupid:account/3', - icon: , - selected: true, - }, - ]} - selectEmailCallback={() => {}} - addNewEmailCallback={() => {}} - connectedAccounts={[ - { - value: `urn:rollupid:account:0`, - title: `Account 0`, - subtitle: "SC Wallet - Account 0" - }, - { - value: `urn:rollupid:account:1`, - title: `Account 1`, - subtitle: "Github - Account 1" - }, - { - value: `urn:rollupid:account:2`, - title: `Account 2`, - subtitle: "Google - Account 2" - }, - { - value: `urn:rollupid:account:3`, - title: `Account 3`, - subtitle: "Microsoft - Account 3" - }, - ]} - selectAccountsCallback={() => {}} - disableAuthorization={true} - transitionState={'idle'} - connectedSmartContractWallets={[ - { - value: `urn:rollupid:account:0`, - title: `Account 0`, - subtitle: "SC Wallet - Account 0" - }, - { - value: `urn:rollupid:account:1`, - title: `Account 1`, - subtitle: "Github - Account 1" - }, - { - value: `urn:rollupid:account:2`, - title: `Account 2`, - subtitle: "Google - Account 2" - }, - { - value: `urn:rollupid:account:3`, - title: `Account 3`, - subtitle: "Microsoft - Account 3" - }, - ]} - selectSmartWalletCallback={() => {}} - addNewSmartWalletCallback={() => {}} - > - {' '} - -) - -# Authorization - - < Canvas > - {Template.bind({})} - diff --git a/packages/design-system/src/templates/authorization/Authorization.tsx b/packages/design-system/src/templates/authorization/Authorization.tsx index d96d21e8f2..1727207985 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,10 @@ type AuthorizationProps = { selectEmailCallback: (selected: DropdownSelectListItem) => void selectedEmail?: DropdownSelectListItem + maskEmail: boolean + loadingMaskEmail: boolean + setMaskEmail: (state: boolean) => void + connectedAccounts?: Array addNewAccountCallback: () => void selectAccountsCallback: (selected: Array) => void @@ -85,6 +94,9 @@ export default ({ addNewEmailCallback, selectEmailCallback, selectedEmail, + maskEmail, + loadingMaskEmail, + setMaskEmail, connectedAccounts, addNewAccountCallback, selectAccountsCallback, @@ -161,11 +173,18 @@ export default ({ ${open ? 'bg-gray-50 shadow-sm rounded-lg' : ''}`} >
- + {scope === 'connected_accounts' && maskEmail ? ( + + ) : ( + + )}
- - {scopeMeta.scopes[scope].name} - +
+ + {scopeMeta.scopes[scope].name} + + {scope === 'email' ? ( + maskEmail ? ( + + ) : ( + + ) + ) : null} +
{!selectedItem && !selectedItems?.length && !allItemsSelected && ( @@ -225,16 +253,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 +372,28 @@ export default ({ items={connectedEmails} defaultItems={[selectedEmail]} placeholder="Select an Email Address" - onSelect={(selectedItem: DropdownSelectListItem) => { - selectEmailCallback(selectedItem) + refreshSelectedItem={true} + maskAccount={maskEmail && selectedEmail.value} + onSelect={selectEmailCallback} + listboxOptions={{ + topAction: ( +
+ + Mask Email + + {loadingMaskEmail && } + setMaskEmail(!maskEmail)} + checked={maskEmail} + /> +
+ ), }} ConnectButtonPhrase="Connect New Email Account" ConnectButtonCallback={addNewEmailCallback} @@ -357,11 +407,9 @@ export default ({ - ) => { - selectAccountsCallback(selectedItems) - }} + switchTitles={true} + maskAccount={maskEmail && selectedEmail.value} + onSelect={selectAccountsCallback} onSelectAll={selectAllAccountsCallback} placeholder="Select at least one" ConnectButtonPhrase="Connect New Account" diff --git a/packages/platform-middleware/inputValidators.ts b/packages/platform-middleware/inputValidators.ts index 6ee901d63a..e0492368c2 100644 --- a/packages/platform-middleware/inputValidators.ts +++ b/packages/platform-middleware/inputValidators.ts @@ -8,7 +8,12 @@ import { IdentityURN, IdentityURNSpace } from '@proofzero/urns/identity' import { IdentityRefURN } from '@proofzero/urns/identity-ref' import { AnyURN, parseURN } from '@proofzero/urns' import { EdgeURN } from '@proofzero/urns/edge' -import { CryptoAccountType } from '@proofzero/types/account' +import { + CryptoAccountType, + EmailAccountType, + OAuthAccountType, + WebauthnAccountType, +} from '@proofzero/types/account' import { IdentityGroupURN, IdentityGroupURNSpace, @@ -65,6 +70,44 @@ export const CryptoAccountTypeInput = z.custom((input) => { return addrType }) +export const EmailAccountTypeInput = z.custom((input) => { + switch (input) { + case EmailAccountType.Email: + case EmailAccountType.Mask: + break + default: + throw new TypeError(`invalid email account type: ${input}`) + } + return input as EmailAccountType +}) + +export const OAuthAccountTypeInput = z.custom((input) => { + switch (input) { + case OAuthAccountType.Apple: + case OAuthAccountType.Discord: + case OAuthAccountType.GitHub: + case OAuthAccountType.Google: + case OAuthAccountType.Microsoft: + case OAuthAccountType.Twitter: + break + default: + throw new TypeError(`invalid oauth account type: ${input}`) + } + return input as OAuthAccountType +}) + +export const WebauthnAccountTypeInput = z.custom( + (input) => { + switch (input) { + case WebauthnAccountType.WebAuthN: + break + default: + throw new TypeError(`invalid webauthn account type: ${input}`) + } + return input as WebauthnAccountType + } +) + export const AnyURNInput = z.custom((input) => { parseURN(input as string) return input as AnyURN diff --git a/packages/security/persona.ts b/packages/security/persona.ts index b709126b77..833e722453 100644 --- a/packages/security/persona.ts +++ b/packages/security/persona.ts @@ -63,7 +63,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', @@ -126,10 +127,19 @@ export async function setPersonaReferences( //so we create a unique listing of them before creating the edges const uniqueAuthorizationReferences = new Set() + const coreClient = createCoreClient( + coreFetcher, + generateTraceContextHeaders(traceSpan) + ) + for (const scopeEntry of scope) { //TODO: make this more generic so it applies to all claims if (scopeEntry === 'email' && personaData.email) { uniqueAuthorizationReferences.add(personaData.email) + const sourceAccountURN = await coreClient.account.getSourceAccount.query( + personaData.email + ) + sourceAccountURN && uniqueAuthorizationReferences.add(sourceAccountURN) } else if ( scopeEntry === 'connected_accounts' && personaData.connected_accounts && @@ -153,11 +163,6 @@ export async function setPersonaReferences( } } - const coreClient = createCoreClient( - coreFetcher, - generateTraceContextHeaders(traceSpan) - ) - //TODO: The next set of 3 operations will need to be optmizied into a single //SQL transaction @@ -200,12 +205,21 @@ export type ClaimName = string export type ScopeValueName = string export type ClaimValuePairs = Record +type AccountClaim = { + urn: AccountURN + 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 +236,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 +293,33 @@ 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 urn = sourceURN as AccountURN + const type = node.rc.addr_type + const identifier = node.qc.alias + source = { urn, identifier, type } + } + } + const claimData: ClaimData = { [scopeEntry]: { claims: { - email: emailAccount, + email, + type, }, meta: { urns: [emailAccountUrn], + source, valid: true, }, }, @@ -379,6 +427,11 @@ async function erc4337ClaimsRetriever( return result } +type ConnectedAccount = { + type: string + identifier: string +} + async function connectedAccountsClaimsRetriever( scopeEntry: ScopeValueName, identityURN: IdentityURN, @@ -391,10 +444,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, }, }, @@ -408,22 +461,44 @@ async function connectedAccountsClaimsRetriever( if (personaData.connected_accounts === AuthorizationControlSelection.ALL) { //Referencable persona submission pointing to all connected accounts //at any point in time - const identityAccounts = - ( - await coreClient.identity.getAccounts.query({ - URN: identityURN, + let identityAccounts = + (await coreClient.identity.getAccounts.query({ + URN: identityURN, + })) || [] + + identityAccounts = identityAccounts.filter( + ({ rc: { addr_type } }) => addr_type !== CryptoAccountType.Wallet + ) + + if (personaData.email) { + const [emailProfile] = + await coreClient.account.getAccountProfileBatch.query([ + personaData.email, + ]) + + if ( + emailProfile.type === EmailAccountType.Mask && + 'source' in emailProfile + ) + identityAccounts = identityAccounts.filter(({ baseUrn }) => { + return baseUrn !== emailProfile.source.id }) - )?.filter( - (account) => account.rc.addr_type !== CryptoAccountType.Wallet - ) || [] - for (const accountNode of identityAccounts) { + identityAccounts = identityAccounts.filter( + ({ baseUrn, rc: { addr_type } }) => { + if (addr_type !== EmailAccountType.Mask) return true + else return baseUrn === emailProfile.id + } + ) + } + + identityAccounts.forEach((node) => { result.connected_accounts.claims.connected_accounts.push({ - type: accountNode.rc.addr_type, - identifier: accountNode.qc.alias, + type: node.rc.addr_type, + identifier: node.qc.alias, }) - result.connected_accounts.meta.urns.push(accountNode.baseUrn) - } + result.connected_accounts.meta.urns.push(node.baseUrn) + }) } else { //Static persona submission of accounts const authorizedAccounts = personaData.connected_accounts as AccountURN[] @@ -442,7 +517,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 +605,36 @@ export const userClaimsFormatter = ( } return result } + +const formatEmailScopeClaim = (scope: EmailScopeClaim) => { + if (scope.claims.type !== EmailAccountType.Mask) 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)) return + accounts + .filter((a) => a.type === EmailAccountType.Mask) + .forEach((a) => (a.type = EmailAccountType.Email)) +} + +export const maskAccountFormatter = (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..4fb1cc223b 100644 --- a/packages/utils/getNormalisedConnectedAccounts.tsx +++ b/packages/utils/getNormalisedConnectedAccounts.tsx @@ -5,7 +5,6 @@ import { OAuthAccountType, EmailAccountType, CryptoAccountType, - WebauthnAccountType, } from '@proofzero/types/account' import { HiOutlineEnvelope } from 'react-icons/hi2' @@ -14,7 +13,7 @@ import googleIcon from '@proofzero/design-system/src/atoms/providers/Google' import microsoftIcon from '@proofzero/design-system/src/atoms/providers/Microsoft' import appleIcon from '@proofzero/design-system/src/atoms/providers/Apple' -import type { Accounts } from '@proofzero/platform.identity/src/types' +import type { Account, Accounts } from '@proofzero/platform.identity/src/types' import type { AccountURN } from '@proofzero/urns/account' import type { DropdownSelectListItem } from '@proofzero/design-system/src/atoms/dropdown/DropdownSelectList' import type { GetAccountProfileResult } from '@proofzero/platform.account/src/jsonrpc/methods/getAccountProfile' @@ -49,9 +48,7 @@ export const getEmailIcon = (type: string): JSX.Element => { ) } -export const adjustAccountTypeToDisplay = ( - accountType: OAuthAccountType | EmailAccountType | CryptoAccountType | WebauthnAccountType -) => { +export const adjustAccountTypeToDisplay = (accountType: string) => { if (accountType === CryptoAccountType.Wallet) { return 'SC Wallet' } @@ -59,39 +56,58 @@ export const adjustAccountTypeToDisplay = ( } export const getEmailDropdownItems = ( - connectedAccounts?: Accounts | null + connectedAccounts?: Accounts ): 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: 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, + title: account.qc.alias, + value: account.baseUrn, + } +} + //accountDropdownItems export const getAccountDropdownItems = ( accountProfiles?: Array | null @@ -99,10 +115,13 @@ export const getAccountDropdownItems = ( if (!accountProfiles) return [] return accountProfiles.map((account) => { return { + address: account.address, title: account.title, - value: account.id as AccountURN, - subtitle: `${adjustAccountTypeToDisplay(account.type)} - ${account.address - }`, + type: account.type, + value: account.id, + subtitle: `${adjustAccountTypeToDisplay(account.type)} - ${ + account.address + }`, } }) } diff --git a/platform/account/package.json b/platform/account/package.json index ea7752cdbd..245a4c2f00 100644 --- a/platform/account/package.json +++ b/platform/account/package.json @@ -44,6 +44,7 @@ "@zerodevapp/sdk": "3.1.57", "do-proxy": "1.3.3", "jose": "4.11.0", + "random-words": "2.0.0", "remix-auth-google": "1.2.0", "zod": "3.22.4" } diff --git a/platform/account/src/jsonrpc/methods/deleteAccountNode.ts b/platform/account/src/jsonrpc/methods/deleteAccountNode.ts index 90de2d3865..a027953f74 100644 --- a/platform/account/src/jsonrpc/methods/deleteAccountNode.ts +++ b/platform/account/src/jsonrpc/methods/deleteAccountNode.ts @@ -7,7 +7,11 @@ import { RollupError, ERROR_CODES } from '@proofzero/errors' import { IdentityURNSpace } from '@proofzero/urns/identity' import { AccountURN } from '@proofzero/urns/account' +import { GetEdgesMethodOutput } from '@proofzero/platform.edges/src/jsonrpc/methods/getEdges' + import type { Context } from '../../context' +import { initAccountNodeByName } from '../../nodes' + import { getAccountLinks } from './getAccountLinks' export const DeleteAccountNodeInput = z.object({ @@ -26,14 +30,17 @@ export const deleteAccountNodeMethod = async ({ }) => { const { identityURN, forceDelete } = input - const nodeClient = ctx.account + if (!IdentityURNSpace.is(identityURN)) throw new Error('Invalid identity URN') + if (!ctx.accountURN) throw new Error('missing account URN') + if (!ctx.account) throw new Error('missing account node') const caller = router.createCaller(ctx) - const identityEdge = await caller.edges.findNode({ - baseUrn: identityURN, - }) if (!forceDelete) { + const identityEdge = await caller.edges.findNode({ + baseUrn: identityURN, + }) + const primaryAccountURN = identityEdge?.qc.primaryAccountURN if (primaryAccountURN === ctx.accountURN) { throw new RollupError({ @@ -53,38 +60,43 @@ export const deleteAccountNodeMethod = async ({ } } - // Get the account associated with the authorization header included in the request. - const account = ctx.accountURN as AccountURN + const maskAccountNodes = await caller.edges.findNodeBatch([ + { qc: { source: ctx.accountURN } }, + ]) + + const maskAccountURNs = maskAccountNodes.reduce((acc, cur) => { + cur && acc.push(cur.baseUrn as AccountURN) + return acc + }, []) - if (!IdentityURNSpace.is(identityURN)) { - throw new Error('Invalid identity URN') + let edges: GetEdgesMethodOutput['edges'] = [] + for (const baseUrn of [ctx.accountURN, ...maskAccountURNs]) { + const query = { dst: { baseUrn } } + const result = await caller.edges.getEdges({ query }) + edges = edges.concat(result.edges) } - // Remove the stored identity in the node. - await nodeClient?.class.unsetIdentity() - - const { edges: accountEdges } = await caller.edges.getEdges({ - query: { - dst: { - baseUrn: account, - }, - }, - }) - - // Remove any edge that references the account node - const edgeDeletionPromises = accountEdges.map((edge) => { - return caller.edges.removeEdge({ - src: edge.src.baseUrn, - dst: edge.dst.baseUrn, - tag: edge.tag, + await Promise.all( + edges.map((edge) => { + return caller.edges.removeEdge({ + src: edge.src.baseUrn, + dst: edge.dst.baseUrn, + tag: edge.tag, + }) }) - }) + ) - await Promise.all(edgeDeletionPromises) + await Promise.all( + [ctx.accountURN, ...maskAccountURNs].map((urn) => + caller.edges.deleteNode({ urn }) + ) + ) - await caller.edges.deleteNode({ - urn: account, - }) + const maskAccounts = maskAccountURNs.map((urn) => + initAccountNodeByName(urn, ctx.env.Account) + ) - return ctx.account?.storage.deleteAll() + await Promise.all( + [ctx.account, ...maskAccounts].map((node) => node.storage.deleteAll()) + ) } diff --git a/platform/account/src/jsonrpc/methods/getAccountProfile.ts b/platform/account/src/jsonrpc/methods/getAccountProfile.ts index 9fd454da9c..4aead4b032 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) @@ -55,7 +64,7 @@ export const getAccountProfileMethod: GetAccountProfileMethod = async ({ if (!nodeClient) throw new InternalServerError({ message: 'missing nodeClient' }) - return await getProfile(ctx, nodeClient, ctx.accountURN!) + return getProfile(ctx, nodeClient, ctx.accountURN!) } export const getAccountProfileBatchMethod = async ({ @@ -74,65 +83,77 @@ 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() +): Promise { + 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 = AccountURNSpace.getBaseURN(accountURN) + const profile = await account.getProfile() + + if (account instanceof EmailAccount && type === EmailAccountType.Mask) { + const sourceAccountURN = await account.getSourceAccount() + if (sourceAccountURN) { + const sourceAccountNode = initAccountNodeByName( + sourceAccountURN, + ctx.env.Account + ) + const source = await getProfile(ctx, sourceAccountNode, sourceAccountURN) + 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/getSourceAccount.ts b/platform/account/src/jsonrpc/methods/getSourceAccount.ts new file mode 100644 index 0000000000..285d8c2495 --- /dev/null +++ b/platform/account/src/jsonrpc/methods/getSourceAccount.ts @@ -0,0 +1,48 @@ +import { z } from 'zod' + +import { BadRequestError } from '@proofzero/errors' +import { AccountURNInput } from '@proofzero/platform-middleware/inputValidators' +import { EmailAccountType } from '@proofzero/types/account' +import { AccountURNSpace } from '@proofzero/urns/account' + +import { Context } from '../../context' +import { initAccountNodeByName } from '../../nodes' +import EmailAccount from '../../nodes/email' + +export const GetSourceAccountInput = AccountURNInput +export const GetSourceAccountOutput = AccountURNInput.optional() + +type GetSourceAccountInput = z.infer +type GetSourceAccountOutput = z.infer + +type GetSourceAccountParams = { + ctx: Context + input: GetSourceAccountInput +} + +interface GetSourceAccountMethod { + (params: GetSourceAccountParams): Promise +} + +export const getSourceAccountMethod: GetSourceAccountMethod = async ({ + ctx, + input, +}) => { + const node = initAccountNodeByName( + AccountURNSpace.getBaseURN(input), + ctx.env.Account + ) + + const type = await node.class.getType() + switch (type) { + case EmailAccountType.Mask: + break + default: + throw new BadRequestError({ + message: `invalid account type: ${type}`, + }) + } + + const email = new EmailAccount(node, ctx.env) + return email.getSourceAccount() +} diff --git a/platform/account/src/jsonrpc/methods/setSourceAccount.ts b/platform/account/src/jsonrpc/methods/setSourceAccount.ts new file mode 100644 index 0000000000..2b24853624 --- /dev/null +++ b/platform/account/src/jsonrpc/methods/setSourceAccount.ts @@ -0,0 +1,50 @@ +import { z } from 'zod' + +import { BadRequestError } from '@proofzero/errors' +import { AccountURNInput } from '@proofzero/platform-middleware/inputValidators' +import { EmailAccountType, OAuthAccountType } from '@proofzero/types/account' +import { AccountURNSpace } from '@proofzero/urns/account' + +import { Context } from '../../context' +import { initAccountNodeByName } from '../../nodes' +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, +}) => { + const node = initAccountNodeByName( + AccountURNSpace.getBaseURN(input), + ctx.env.Account + ) + + const type = await node.class.getType() + switch (type) { + case EmailAccountType.Email: + case OAuthAccountType.Google: + case OAuthAccountType.Microsoft: + break + default: + throw new BadRequestError({ + message: `invalid account type: ${type}`, + }) + } + + const email = new EmailAccount(node, ctx.env) + return email.setSourceAccount(input) +} diff --git a/platform/account/src/jsonrpc/router.ts b/platform/account/src/jsonrpc/router.ts index 1e45cee027..8ed6a10940 100644 --- a/platform/account/src/jsonrpc/router.ts +++ b/platform/account/src/jsonrpc/router.ts @@ -132,6 +132,21 @@ import { AuthorizationTokenFromHeader, ValidateJWT, } from '@proofzero/platform-middleware/jwt' +import { + getMaskedAddressMethod, + GetMaskedAddressInput, + GetMaskedAddressOutput, +} from './methods/getMaskedAddress' +import { + getSourceAccountMethod, + GetSourceAccountInput, + GetSourceAccountOutput, +} from './methods/getSourceAccount' +import { + setSourceAccountMethod, + SetSourceAccountInput, + SetSourceAccountOutput, +} from './methods/setSourceAccount' const t = initTRPC.context().create({ errorFormatter }) @@ -362,4 +377,26 @@ 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), + getSourceAccount: t.procedure + .use(LogUsage) + .use(parse3RN) + .use(Analytics) + .input(GetSourceAccountInput) + .output(GetSourceAccountOutput) + .query(getSourceAccountMethod), + setSourceAccount: t.procedure + .use(LogUsage) + .use(parse3RN) + .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..863540d46f 100644 --- a/platform/account/src/jsonrpc/validators/profile.ts +++ b/platform/account/src/jsonrpc/validators/profile.ts @@ -6,9 +6,9 @@ import { OAuthAccountType, WebauthnAccountType, } from '@proofzero/types/account' -import { WebauthnAccount } from '../../nodes' export const AccountProfileSchema = z.object({ + id: z.string(), address: z.string(), title: z.string(), icon: z.string().optional(), @@ -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..fff48c7c6f 100644 --- a/platform/account/src/nodes/email.ts +++ b/platform/account/src/nodes/email.ts @@ -1,7 +1,9 @@ import { DurableObjectStubProxy } from 'do-proxy' +import * as randomWords from 'random-words' 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 +14,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 +219,44 @@ 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 bits = generateRandomString(6) + const words = randomWords.generate(3).join('-') + const address = `${words}-${bits}@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..b6840ed6c5 100644 --- a/platform/account/src/utils.ts +++ b/platform/account/src/utils.ts @@ -53,6 +53,7 @@ export const isOAuthAccountType = (type: string | undefined) => { export const isEmailAccountType = (type: string | undefined) => { switch (type) { case EmailAccountType.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..5ab92d19d4 100644 --- a/platform/authorization/src/jsonrpc/methods/getAuthorizedAppScopes.ts +++ b/platform/authorization/src/jsonrpc/methods/getAuthorizedAppScopes.ts @@ -23,6 +23,13 @@ export const GetAuthorizedAppScopesMethodOutput = z.object({ claims: z.record(z.string(), z.any()), meta: z.object({ urns: z.array(z.string()), + source: z + .object({ + urn: inputValidators.AccountURNInput, + 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..f5780ae416 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, + maskAccountFormatter, userClaimsFormatter, } from '@proofzero/security/persona' import { PersonaData } from '@proofzero/types/application' @@ -79,6 +80,9 @@ export const getUserInfoMethod = async ({ message: 'Authorized data error. Re-authorization by user required', }) } + + const claims = userClaimsFormatter(maskAccountFormatter(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/edges/src/jsonrpc/methods/getEdges.ts b/platform/edges/src/jsonrpc/methods/getEdges.ts index 5e1ee5d51d..31e4b7f23d 100644 --- a/platform/edges/src/jsonrpc/methods/getEdges.ts +++ b/platform/edges/src/jsonrpc/methods/getEdges.ts @@ -14,6 +14,7 @@ export const GetEdgesMethodInput = z.object({ }) export const GetEdgesMethodOutput = EdgeQueryResultsOutput +export type GetEdgesMethodOutput = z.infer export type GetEdgesParams = z.infer diff --git a/platform/identity/src/jsonrpc/methods/getOwnAccounts.ts b/platform/identity/src/jsonrpc/methods/getOwnAccounts.ts index 127c60b28c..cfaf6bacd1 100644 --- a/platform/identity/src/jsonrpc/methods/getOwnAccounts.ts +++ b/platform/identity/src/jsonrpc/methods/getOwnAccounts.ts @@ -4,8 +4,9 @@ import { router } from '@proofzero/platform.core' import { inputValidators } from '@proofzero/platform-middleware' import { EDGE_ACCOUNT } from '@proofzero/platform.account/src/constants' + import { Context } from '../../context' -import { Node } from '@proofzero/platform.edges/src/jsonrpc/validators/node' +import { AccountSchema } from '../validators/profile' export const GetAccountsInput = z.object({ URN: inputValidators.AnyURNInput, @@ -18,7 +19,7 @@ export const GetAccountsInput = z.object({ export type GetAccountsParams = z.infer -export const GetAccountsOutput = z.array(Node) +export const GetAccountsOutput = z.array(AccountSchema) export type GetAccountsOutput = z.infer export const getOwnAccountsMethod = async ({ @@ -57,6 +58,5 @@ export const getOwnAccountsMethod = async ({ // nodes, filtered by account type if provided. const caller = router.createCaller(ctx) const { edges } = await caller.edges.getEdges({ query }) - - return edges.map((e) => e.dst) + return edges.map((e) => e.dst) as GetAccountsOutput } diff --git a/platform/identity/src/jsonrpc/methods/getPublicAccounts.ts b/platform/identity/src/jsonrpc/methods/getPublicAccounts.ts index 92f142e11e..22bcec5b33 100644 --- a/platform/identity/src/jsonrpc/methods/getPublicAccounts.ts +++ b/platform/identity/src/jsonrpc/methods/getPublicAccounts.ts @@ -6,7 +6,7 @@ import { inputValidators } from '@proofzero/platform-middleware' import { EDGE_ACCOUNT } from '@proofzero/platform.account/src/constants' import { Context } from '../../context' -import { Node } from '@proofzero/platform.edges/src/jsonrpc/validators/node' +import { AccountSchema } from '../validators/profile' export const GetAccountsInput = z.object({ URN: inputValidators.AnyURNInput, @@ -20,7 +20,7 @@ export const GetAccountsInput = z.object({ export type GetAccountsParams = z.infer -export const GetAccountsOutput = z.array(Node) +export const GetAccountsOutput = z.array(AccountSchema) export type GetAccountsOutput = z.infer export const getPublicAccountsMethod = async ({ @@ -53,5 +53,5 @@ export const getPublicAccountsMethod = async ({ // nodes, filtered by account type if provided. return caller.edges .getEdges({ query }) - .then((res) => res.edges.map((e) => e.dst)) + .then((res) => res.edges.map((e) => e.dst) as GetAccountsOutput) } diff --git a/platform/identity/src/jsonrpc/validators/profile.ts b/platform/identity/src/jsonrpc/validators/profile.ts index 30ca11152a..531e556775 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,10 @@ 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 AccountSchema = Node.extend({ + baseUrn: AccountURNInput, +}) +export const AccountsSchema = z.array(AccountSchema) diff --git a/platform/identity/src/types.ts b/platform/identity/src/types.ts index 79287dcb3d..0fcffa4078 100644 --- a/platform/identity/src/types.ts +++ b/platform/identity/src/types.ts @@ -1,9 +1,10 @@ import { z } from 'zod' import { AccountListSchema } from './jsonrpc/validators/accountList' import { ProfileSchema } from './jsonrpc/validators/profile' -import { AccountsSchema } from './jsonrpc/validators/profile' +import { AccountSchema, AccountsSchema } from './jsonrpc/validators/profile' // TODO: move to types packages export type AccountList = z.infer export type Profile = z.infer +export type Account = z.infer export type Accounts = z.infer diff --git a/yarn.lock b/yarn.lock index 47b29c9ab9..9a0e046f92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6465,6 +6465,7 @@ __metadata: jose: 4.11.0 npm-run-all: 4.1.5 prettier: 2.7.1 + random-words: 2.0.0 remix-auth-google: 1.2.0 typescript: 5.0.4 zod: 3.22.4 @@ -32955,6 +32956,15 @@ __metadata: languageName: node linkType: hard +"random-words@npm:2.0.0": + version: 2.0.0 + resolution: "random-words@npm:2.0.0" + dependencies: + seedrandom: ^3.0.5 + checksum: d574955cc5f38700a2394d67457673f811866db3dd818431e7f73e31f739f9e1bb2861cc679d83538f7ac87cad95f101a609dbb916019e6c2e18432db220c5eb + languageName: node + linkType: hard + "randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -35163,6 +35173,13 @@ __metadata: languageName: node linkType: hard +"seedrandom@npm:^3.0.5": + version: 3.0.5 + resolution: "seedrandom@npm:3.0.5" + checksum: 728b56bc3bc1b9ddeabd381e449b51cb31bdc0aa86e27fcd0190cea8c44613d5bcb2f6bb63ed79f78180cbe791c20b8ec31a9627f7b7fc7f476fd2bdb7e2da9f + languageName: node + linkType: hard + "select-hose@npm:^2.0.0": version: 2.0.0 resolution: "select-hose@npm:2.0.0"