From 0c8c6a5834d8fa00125837c6a09c748f1f45e171 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 --- ...-words-npm-2.0.0-5c0ece92ba-d574955cc5.zip | Bin 0 -> 14151 bytes ...random-npm-3.0.5-6946e8f8db-728b56bc3b.zip | Bin 0 -> 95136 bytes .../app/components/accounts/AccountList.tsx | 18 +- .../app/components/applications/claims.tsx | 62 +++++-- apps/passport/app/routes/authorize.tsx | 74 +++++++- .../app/routes/create/account-mask.ts | 62 +++++++ .../settings/applications/$clientId/index.tsx | 58 +++++-- .../applications/$clientId/scopes.tsx | 2 +- apps/passport/app/routes/userinfo.tsx | 3 +- apps/passport/app/utils/authorize.server.ts | 38 +++- .../ConnectedAccountsDropdown.stories.tsx | 53 ------ .../src/atoms/dropdown/DropdownSelectList.tsx | 88 ++++++++-- .../dropdown/SCWalletDropdown.stories.tsx | 44 ----- .../src/atoms/form/InputToggle.tsx | 10 +- .../design-system/src/atoms/info/Info.tsx | 11 +- .../src/atoms/pills/EmailMaskPill.tsx | 29 ++++ .../src/atoms/pills/UserPill.tsx | 24 ++- .../src/atoms/providers/Email.tsx | 4 + .../authorization/Authorization.stories.mdx | 125 ------------- .../templates/authorization/Authorization.tsx | 92 +++++++--- .../platform-middleware/inputValidators.ts | 45 ++++- packages/platform-middleware/jwt.ts | 2 + packages/security/persona.ts | 164 +++++++++++++++--- .../utils/getNormalisedConnectedAccounts.tsx | 77 ++++---- platform/account/package.json | 1 + .../src/jsonrpc/methods/deleteAccountNode.ts | 74 ++++---- .../src/jsonrpc/methods/getAccountProfile.ts | 83 +++++---- .../src/jsonrpc/methods/getMaskedAddress.ts | 45 +++++ .../src/jsonrpc/methods/getSourceAccount.ts | 40 +++++ .../src/jsonrpc/methods/setSourceAccount.ts | 51 ++++++ platform/account/src/jsonrpc/router.ts | 37 ++++ .../account/src/jsonrpc/validators/profile.ts | 7 +- platform/account/src/nodes/email.ts | 30 +++- platform/account/src/utils.ts | 1 + .../jsonrpc/methods/getAuthorizedAppScopes.ts | 7 + .../src/jsonrpc/methods/getUserInfo.ts | 6 +- .../jsonrpc/methods/revokeAppAuthorization.ts | 35 ++-- platform/core/src/context.ts | 6 +- .../edges/src/jsonrpc/methods/getEdges.ts | 1 + .../src/jsonrpc/methods/getOwnAccounts.ts | 8 +- .../src/jsonrpc/methods/getPublicAccounts.ts | 6 +- .../src/jsonrpc/validators/profile.ts | 9 +- platform/identity/src/types.ts | 3 +- yarn.lock | 17 ++ 44 files changed, 1072 insertions(+), 480 deletions(-) create mode 100644 .yarn/cache/random-words-npm-2.0.0-5c0ece92ba-d574955cc5.zip create mode 100644 .yarn/cache/seedrandom-npm-3.0.5-6946e8f8db-728b56bc3b.zip create mode 100644 apps/passport/app/routes/create/account-mask.ts delete mode 100644 packages/design-system/src/atoms/dropdown/ConnectedAccountsDropdown.stories.tsx delete mode 100644 packages/design-system/src/atoms/dropdown/SCWalletDropdown.stories.tsx create mode 100644 packages/design-system/src/atoms/pills/EmailMaskPill.tsx create mode 100644 packages/design-system/src/atoms/providers/Email.tsx delete mode 100644 packages/design-system/src/templates/authorization/Authorization.stories.mdx create mode 100644 platform/account/src/jsonrpc/methods/getMaskedAddress.ts create mode 100644 platform/account/src/jsonrpc/methods/getSourceAccount.ts create mode 100644 platform/account/src/jsonrpc/methods/setSourceAccount.ts 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 0000000000000000000000000000000000000000..17cb9b155061a666c0ef99d4584fc19e965542fe GIT binary patch literal 14151 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 Q!TdRp{)`VI)_>joAIu@nBme*a literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c2f6b0903aff937d692f3389b6a60d40fbb1a24c GIT binary patch literal 95136 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≺0+BKv9Kw_ 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 { 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..32cb1af013 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,15 @@ export const Dropdown = ({ if (!multiple) return defaultItems?.[0] as DropdownSelectListItem }) + useEffect(() => { + if (!refreshSelectedItem) return + if (!defaultItems || !defaultItems[0]) 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 +236,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" > + {defaultItems?.[0] && 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 +419,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/platform-middleware/jwt.ts b/packages/platform-middleware/jwt.ts index 8f8dce9c83..b30d199cb7 100644 --- a/packages/platform-middleware/jwt.ts +++ b/packages/platform-middleware/jwt.ts @@ -7,7 +7,9 @@ import { BaseMiddlewareFunction } from './types' export const AuthorizationTokenFromHeader: BaseMiddlewareFunction<{ req?: Request + token?: string }> = ({ ctx, next }) => { + if (ctx.token) return next(ctx) const token = ctx.req ? getAuthzTokenFromReq(ctx.req) : undefined return next({ ctx: { 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..7795802b61 --- /dev/null +++ b/platform/account/src/jsonrpc/methods/getSourceAccount.ts @@ -0,0 +1,40 @@ +import { z } from 'zod' + +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() + if (type !== EmailAccountType.Mask) return + + 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..95ad997e8e --- /dev/null +++ b/platform/account/src/jsonrpc/methods/setSourceAccount.ts @@ -0,0 +1,51 @@ +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.Apple: + 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/authorization/src/jsonrpc/methods/revokeAppAuthorization.ts b/platform/authorization/src/jsonrpc/methods/revokeAppAuthorization.ts index 494637a337..e39131605c 100644 --- a/platform/authorization/src/jsonrpc/methods/revokeAppAuthorization.ts +++ b/platform/authorization/src/jsonrpc/methods/revokeAppAuthorization.ts @@ -3,9 +3,13 @@ import { z } from 'zod' import { router } from '@proofzero/platform.core' import { Context } from '../../context' +import { generateJKU, getPrivateJWK } from '../../jwk' import { initAuthorizationNodeByName } from '../../nodes' -import { EDGE_AUTHORIZES } from '../../constants' +import { + EDGE_AUTHORIZES, + ROLLUP_INTERNAL_ACCESS_TOKEN_URN, +} from '../../constants' import { IdentityURNSpace } from '@proofzero/urns/identity' import { AuthorizationURNSpace } from '@proofzero/urns/authorization' @@ -102,19 +106,26 @@ export const revokeAppAuthorizationMethod: RevokeAppAuthorizationMethod = ctx.env.Authorization ) - // const scope = ['admin'] + const scope = ['admin'] - // const internalAccessToken = await authorizationNode.class.generateAccessToken({ - // jku: generateJKU(input.issuer), - // jwk: getPrivateJWK(ctx), - // identity: ROLLUP_INTERNAL_ACCESS_TOKEN_URN, - // clientId, - // expirationTime: '5 seconds', - // issuer: input.issuer, - // scope, - // }) + const internalAccessToken = + await authorizationNode.class.generateAccessToken({ + jku: generateJKU(input.issuer), + jwk: getPrivateJWK(ctx.env), + identity: ROLLUP_INTERNAL_ACCESS_TOKEN_URN, + clientId, + expirationTime: '5 seconds', + issuer: input.issuer, + scope, + }) + + const privilegedCaller = router.createCaller({ + ...ctx, + req: null, + token: internalAccessToken, + }) - const paymaster = await caller.starbase.getPaymaster({ + const paymaster = await privilegedCaller.starbase.getPaymaster({ clientId, }) 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"