[website] Revert URL verification (#412)
* revert status-js * revert website
This commit is contained in:
parent
36538591ce
commit
8ec5bf3bc5
|
@ -21,17 +21,6 @@ export const ErrorPage = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
// todo!: design review, not in designs
|
|
||||||
case ERROR_CODES.UNVERIFIED_CONTENT:
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-8 bg-white text-center">
|
|
||||||
<div className="h-[160px] w-[160px] rounded-full bg-[#ffd455]" />
|
|
||||||
<Text size={27} weight="semibold">
|
|
||||||
Unverified content.
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
case ERROR_CODES.INTERNAL_SERVER_ERROR:
|
case ERROR_CODES.INTERNAL_SERVER_ERROR:
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -43,25 +43,25 @@ type Type = 'community' | 'channel' | 'profile'
|
||||||
|
|
||||||
type PreviewPageProps = {
|
type PreviewPageProps = {
|
||||||
type: Type
|
type: Type
|
||||||
unverifiedEncodedData?: string | null
|
encodedData?: string | null
|
||||||
index?: boolean
|
index?: boolean
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
type: 'community'
|
type: 'community'
|
||||||
unverifiedDecodedData?: ReturnType<typeof decodeCommunityURLData> | null
|
decodedData?: ReturnType<typeof decodeCommunityURLData> | null
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'channel'
|
type: 'channel'
|
||||||
unverifiedDecodedData?: ReturnType<typeof decodeChannelURLData> | null
|
decodedData?: ReturnType<typeof decodeChannelURLData> | null
|
||||||
channelUuid?: string
|
channelUuid?: string
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'profile'
|
type: 'profile'
|
||||||
unverifiedDecodedData?: ReturnType<typeof decodeUserURLData> | null
|
decodedData?: ReturnType<typeof decodeUserURLData> | null
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export type VerifiedData =
|
export type Data =
|
||||||
| {
|
| {
|
||||||
type: 'community'
|
type: 'community'
|
||||||
info: CommunityInfo
|
info: CommunityInfo
|
||||||
|
@ -93,26 +93,26 @@ const JOIN_BUTTON_LABEL: Record<Type, string> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PreviewPage(props: PreviewPageProps) {
|
export function PreviewPage(props: PreviewPageProps) {
|
||||||
const { type, unverifiedDecodedData, unverifiedEncodedData } = props
|
const { type, decodedData, encodedData } = props
|
||||||
|
|
||||||
const { asPath } = useRouter()
|
const { asPath } = useRouter()
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
// todo: default og image, not dynamic
|
// todo: default og image, not dynamic
|
||||||
// const ogImageUrl = getOgImageUrl(props.unverifiedDecodedData)
|
// const ogImageUrl = getOgImageUrl(props.decodedData)
|
||||||
// todo?: pass meta, info as component
|
// todo?: pass meta, info as component
|
||||||
// todo?: pass image, color as props
|
// todo?: pass image, color as props
|
||||||
|
|
||||||
const {
|
const {
|
||||||
publicKey,
|
publicKey,
|
||||||
channelUuid: urlChannelUuid,
|
channelUuid: urlChannelUuid,
|
||||||
verifiedURLData,
|
data: urlData,
|
||||||
errorCode: urlErrorCode,
|
errorCode: urlErrorCode,
|
||||||
} = useURLData(type, unverifiedDecodedData, unverifiedEncodedData)
|
} = useURLData(type, decodedData, encodedData)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: verifiedWakuData,
|
data: wakuData,
|
||||||
isLoading,
|
isLoading,
|
||||||
status,
|
status,
|
||||||
refetch,
|
refetch,
|
||||||
|
@ -120,7 +120,7 @@ export function PreviewPage(props: PreviewPageProps) {
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
queryKey: [type],
|
queryKey: [type],
|
||||||
enabled: !!publicKey,
|
enabled: !!publicKey,
|
||||||
queryFn: async function ({ queryKey }): Promise<VerifiedData | null> {
|
queryFn: async function ({ queryKey }): Promise<Data | null> {
|
||||||
const client = await getRequestClient()
|
const client = await getRequestClient()
|
||||||
|
|
||||||
switch (queryKey[0]) {
|
switch (queryKey[0]) {
|
||||||
|
@ -173,36 +173,35 @@ export function PreviewPage(props: PreviewPageProps) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (verifiedURLData) {
|
if (urlData) {
|
||||||
toast.custom('Information just updated', <InfoIcon size={20} />)
|
toast.custom('Information just updated', <InfoIcon size={20} />)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = status === 'loading' || isLoading
|
const loading = status === 'loading' || isLoading
|
||||||
const verifiedData: VerifiedData | undefined =
|
const data: Data | undefined = wakuData ?? urlData
|
||||||
verifiedWakuData ?? verifiedURLData
|
|
||||||
|
|
||||||
const { avatarURL, bannerURL } = useMemo(() => {
|
const { avatarURL, bannerURL } = useMemo(() => {
|
||||||
if (!verifiedData) {
|
if (!data) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarURL = getAvatarURL(verifiedData)
|
const avatarURL = getAvatarURL(data)
|
||||||
const bannerURL = getBannerURL(verifiedData)
|
const bannerURL = getBannerURL(data)
|
||||||
|
|
||||||
return { avatarURL, bannerURL }
|
return { avatarURL, bannerURL }
|
||||||
}, [verifiedData])
|
}, [data])
|
||||||
|
|
||||||
if (urlErrorCode) {
|
if (urlErrorCode) {
|
||||||
return <ErrorPage errorCode={urlErrorCode} />
|
return <ErrorPage errorCode={urlErrorCode} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loading && !verifiedData) {
|
if (!loading && !data) {
|
||||||
return <ErrorPage errorCode={ERROR_CODES.NOT_FOUND} />
|
return <ErrorPage errorCode={ERROR_CODES.NOT_FOUND} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((loading && !verifiedData) || !verifiedData || !publicKey) {
|
if ((loading && !data) || !data || !publicKey) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-full xl:grid xl:grid-cols-[560px,auto]">
|
<div className="h-full xl:grid xl:grid-cols-[560px,auto]">
|
||||||
|
@ -317,7 +316,7 @@ export function PreviewPage(props: PreviewPageProps) {
|
||||||
{/* todo: theme; based on user system settings */}
|
{/* todo: theme; based on user system settings */}
|
||||||
{/* todo: (system or both?) install banner */}
|
{/* todo: (system or both?) install banner */}
|
||||||
<div
|
<div
|
||||||
style={!bannerURL ? getGradientStyles(verifiedData) : undefined}
|
style={!bannerURL ? getGradientStyles(data) : undefined}
|
||||||
className="relative h-full bg-gradient-to-b from-[var(--gradient-color)] to-[#fff] to-20% xl:grid xl:grid-cols-[560px,auto]"
|
className="relative h-full bg-gradient-to-b from-[var(--gradient-color)] to-[#fff] to-20% xl:grid xl:grid-cols-[560px,auto]"
|
||||||
>
|
>
|
||||||
<div className="absolute left-0 right-0 top-0 xl:hidden">
|
<div className="absolute left-0 right-0 top-0 xl:hidden">
|
||||||
|
@ -336,54 +335,54 @@ export function PreviewPage(props: PreviewPageProps) {
|
||||||
{/* HERO */}
|
{/* HERO */}
|
||||||
<div className="mb-[32px] xl:mb-[35px]">
|
<div className="mb-[32px] xl:mb-[35px]">
|
||||||
<div className="mb-2 xl:mb-4">
|
<div className="mb-2 xl:mb-4">
|
||||||
{verifiedData.type === 'community' && (
|
{data.type === 'community' && (
|
||||||
<Avatar
|
<Avatar
|
||||||
type="community"
|
type="community"
|
||||||
name={verifiedData.info.displayName}
|
name={data.info.displayName}
|
||||||
src={avatarURL}
|
src={avatarURL}
|
||||||
size={80}
|
size={80}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{verifiedData.type === 'channel' && (
|
{data.type === 'channel' && (
|
||||||
<Avatar
|
<Avatar
|
||||||
type="channel"
|
type="channel"
|
||||||
name={verifiedData.info.displayName}
|
name={data.info.displayName}
|
||||||
emoji={verifiedData.info.emoji}
|
emoji={data.info.emoji}
|
||||||
size={80}
|
size={80}
|
||||||
// fixme: use `verifiedData.info.color` (e.g. #000000 format)
|
// fixme: use `data.info.color` (e.g. #000000 format)
|
||||||
backgroundColor="$neutral-100"
|
backgroundColor="$neutral-100"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{verifiedData.type === 'profile' && (
|
{data.type === 'profile' && (
|
||||||
<Avatar
|
<Avatar
|
||||||
type="user"
|
type="user"
|
||||||
name={verifiedData.info.displayName}
|
name={data.info.displayName}
|
||||||
src={avatarURL}
|
src={avatarURL}
|
||||||
size={80}
|
size={80}
|
||||||
colorHash={verifiedData.info.colorHash}
|
colorHash={data.info.colorHash}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="mb-3 text-[40px] font-semibold leading-[44px] xl:text-[64px] xl:leading-[68px]">
|
<h1 className="mb-3 text-[40px] font-semibold leading-[44px] xl:text-[64px] xl:leading-[68px]">
|
||||||
{verifiedData.type === 'channel' && '#'}
|
{data.type === 'channel' && '#'}
|
||||||
{verifiedData.info.displayName}
|
{data.info.displayName}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mb-4 text-[15px] text-neutral-100 xl:text-[19px]">
|
<p className="mb-4 text-[15px] text-neutral-100 xl:text-[19px]">
|
||||||
{verifiedData.info.description}
|
{data.info.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{verifiedData.type === 'community' && (
|
{data.type === 'community' && (
|
||||||
<>
|
<>
|
||||||
<div className="mb-4 flex items-center gap-1">
|
<div className="mb-4 flex items-center gap-1">
|
||||||
<MembersIcon size={20} color="$neutral-50" />
|
<MembersIcon size={20} color="$neutral-50" />
|
||||||
<Text size={15}>
|
<Text size={15}>
|
||||||
{formatNumber(verifiedData.info.membersCount)}
|
{formatNumber(data.info.membersCount)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
{verifiedData.info.tags?.length > 0 && (
|
{data.info.tags?.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-[6px] xl:gap-[11px]">
|
<div className="flex flex-wrap gap-[6px] xl:gap-[11px]">
|
||||||
{verifiedData.info.tags.map(tag => (
|
{data.info.tags.map(tag => (
|
||||||
<Tag
|
<Tag
|
||||||
key={tag.emoji + tag.text}
|
key={tag.emoji + tag.text}
|
||||||
size={32}
|
size={32}
|
||||||
|
@ -395,7 +394,7 @@ export function PreviewPage(props: PreviewPageProps) {
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{verifiedData.type === 'channel' && (
|
{data.type === 'channel' && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Text size={13} color="$neutral-40">
|
<Text size={13} color="$neutral-40">
|
||||||
Channel in
|
Channel in
|
||||||
|
@ -403,18 +402,18 @@ export function PreviewPage(props: PreviewPageProps) {
|
||||||
<ContextTag
|
<ContextTag
|
||||||
type="community"
|
type="community"
|
||||||
community={{
|
community={{
|
||||||
name: verifiedData.info.community.displayName,
|
name: data.info.community.displayName,
|
||||||
src: getAvatarURL({
|
src: getAvatarURL({
|
||||||
type: 'community',
|
type: 'community',
|
||||||
info: verifiedData.info.community as CommunityInfo,
|
info: data.info.community as CommunityInfo,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{verifiedData.type === 'profile' && (
|
{data.type === 'profile' && (
|
||||||
<p className="text-[16px] tracking-[.2em]">
|
<p className="text-[16px] tracking-[.2em]">
|
||||||
{verifiedData.info.emojiHash}
|
{data.info.emojiHash}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -497,9 +496,7 @@ export function PreviewPage(props: PreviewPageProps) {
|
||||||
!bannerURL
|
!bannerURL
|
||||||
? {
|
? {
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
'color' in verifiedData.info
|
'color' in data.info ? data.info.color : neutral[100],
|
||||||
? verifiedData.info.color
|
|
||||||
: neutral[100],
|
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
@ -527,14 +524,14 @@ const formatNumber = (n: number) => {
|
||||||
return formatter.format(n)
|
return formatter.format(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGradientStyles = (data: VerifiedData): CSSProperties => {
|
const getGradientStyles = (data: Data): CSSProperties => {
|
||||||
return {
|
return {
|
||||||
// @ts-expect-error CSSProperties do not handle inline CSS variables
|
// @ts-expect-error CSSProperties do not handle inline CSS variables
|
||||||
'--gradient-color': 'color' in data.info ? data.info.color : neutral[100],
|
'--gradient-color': 'color' in data.info ? data.info.color : neutral[100],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAvatarURL = (data: VerifiedData): string | undefined => {
|
const getAvatarURL = (data: Data): string | undefined => {
|
||||||
let avatar: Uint8Array | undefined
|
let avatar: Uint8Array | undefined
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'community':
|
case 'community':
|
||||||
|
@ -560,7 +557,7 @@ const getAvatarURL = (data: VerifiedData): string | undefined => {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBannerURL = (data: VerifiedData): string | undefined => {
|
const getBannerURL = (data: Data): string | undefined => {
|
||||||
let banner: Uint8Array | undefined
|
let banner: Uint8Array | undefined
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'community':
|
case 'community':
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
export const ERROR_CODES = {
|
export const ERROR_CODES = {
|
||||||
NOT_FOUND: 404,
|
NOT_FOUND: 404,
|
||||||
INTERNAL_SERVER_ERROR: 500,
|
INTERNAL_SERVER_ERROR: 500,
|
||||||
UNVERIFIED_CONTENT: 600,
|
|
||||||
INVALID_PUBLIC_KEY: 601,
|
INVALID_PUBLIC_KEY: 601,
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,13 +7,12 @@ import {
|
||||||
indicesToTags,
|
indicesToTags,
|
||||||
publicKeyToColorHash,
|
publicKeyToColorHash,
|
||||||
publicKeyToEmojiHash,
|
publicKeyToEmojiHash,
|
||||||
verifyEncodedURLData,
|
recoverPublicKeyFromEncodedURLData,
|
||||||
} from '@status-im/js'
|
} from '@status-im/js'
|
||||||
import { decodeVerificationURLHash } from '@status-im/js/encode-url-hash'
|
|
||||||
|
|
||||||
import { ERROR_CODES } from '@/consts/error-codes'
|
import { ERROR_CODES } from '@/consts/error-codes'
|
||||||
|
|
||||||
import type { VerifiedData } from '@/components/preview-page'
|
import type { Data } from '@/components/preview-page'
|
||||||
import type { ChannelInfo, CommunityInfo, UserInfo } from '@status-im/js'
|
import type { ChannelInfo, CommunityInfo, UserInfo } from '@status-im/js'
|
||||||
import type {
|
import type {
|
||||||
decodeChannelURLData,
|
decodeChannelURLData,
|
||||||
|
@ -23,17 +22,17 @@ import type {
|
||||||
|
|
||||||
export const useURLData = (
|
export const useURLData = (
|
||||||
type: 'community' | 'channel' | 'profile',
|
type: 'community' | 'channel' | 'profile',
|
||||||
unverifiedDecodedData:
|
decodedData:
|
||||||
| ReturnType<typeof decodeCommunityURLData>
|
| ReturnType<typeof decodeCommunityURLData>
|
||||||
| ReturnType<typeof decodeChannelURLData>
|
| ReturnType<typeof decodeChannelURLData>
|
||||||
| ReturnType<typeof decodeUserURLData>
|
| ReturnType<typeof decodeUserURLData>
|
||||||
| undefined
|
| undefined
|
||||||
| null,
|
| null,
|
||||||
unverifiedEncodedData: string | undefined | null
|
encodedData: string | undefined | null
|
||||||
) => {
|
) => {
|
||||||
const [publicKey, setPublicKey] = useState<string>()
|
const [publicKey, setPublicKey] = useState<string>()
|
||||||
const [channelUuid, setChannelUuid] = useState<string>()
|
const [channelUuid, setChannelUuid] = useState<string>()
|
||||||
const [info, setInfo] = useState<VerifiedData>()
|
const [data, setData] = useState<Data>()
|
||||||
const [error, setError] = useState<keyof typeof ERROR_CODES>()
|
const [error, setError] = useState<keyof typeof ERROR_CODES>()
|
||||||
|
|
||||||
const compressPublicKey = type !== 'profile'
|
const compressPublicKey = type !== 'profile'
|
||||||
|
@ -46,9 +45,10 @@ export const useURLData = (
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (!unverifiedDecodedData || !unverifiedEncodedData) {
|
const hash = window.location.hash.replace('#', '')
|
||||||
const hash = window.location.hash.replace('#', '')
|
|
||||||
|
|
||||||
|
// use provided public key
|
||||||
|
if (!decodedData || !encodedData) {
|
||||||
if (!hash) {
|
if (!hash) {
|
||||||
setError('NOT_FOUND')
|
setError('NOT_FOUND')
|
||||||
|
|
||||||
|
@ -61,37 +61,39 @@ export const useURLData = (
|
||||||
})
|
})
|
||||||
|
|
||||||
setPublicKey(publicKey)
|
setPublicKey(publicKey)
|
||||||
|
|
||||||
|
return
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
setError('INVALID_PUBLIC_KEY')
|
setError('INVALID_PUBLIC_KEY')
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recover public key
|
||||||
|
let deserializedPublicKey
|
||||||
|
try {
|
||||||
|
const recoveredPublicKey = recoverPublicKeyFromEncodedURLData(
|
||||||
|
encodedData,
|
||||||
|
hash
|
||||||
|
)
|
||||||
|
deserializedPublicKey = deserializePublicKey(recoveredPublicKey, {
|
||||||
|
compress: compressPublicKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
setPublicKey(deserializedPublicKey)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
setError('INVALID_PUBLIC_KEY')
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = window.location.hash.replace('#', '')
|
// map data
|
||||||
const { signature, publicKey } = decodeVerificationURLHash(hash)
|
|
||||||
|
|
||||||
if (!signature || !publicKey) {
|
|
||||||
setError('UNVERIFIED_CONTENT')
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!verifyEncodedURLData(unverifiedEncodedData, hash)) {
|
|
||||||
setError('UNVERIFIED_CONTENT')
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const deserializedPublicKey = deserializePublicKey(publicKey, {
|
|
||||||
compress: compressPublicKey,
|
|
||||||
})
|
|
||||||
|
|
||||||
const verifiedDecodedData = unverifiedDecodedData
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'community': {
|
case 'community': {
|
||||||
const data = verifiedDecodedData as Required<
|
const data = decodedData as Required<
|
||||||
ReturnType<typeof decodeCommunityURLData>
|
ReturnType<typeof decodeCommunityURLData>
|
||||||
>
|
>
|
||||||
const info: CommunityInfo = {
|
const info: CommunityInfo = {
|
||||||
|
@ -102,12 +104,12 @@ export const useURLData = (
|
||||||
tags: indicesToTags(data.tagIndices),
|
tags: indicesToTags(data.tagIndices),
|
||||||
}
|
}
|
||||||
|
|
||||||
setInfo({ type: 'community', info })
|
setData({ type: 'community', info })
|
||||||
|
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
case 'channel': {
|
case 'channel': {
|
||||||
const data = verifiedDecodedData as Required<
|
const data = decodedData as Required<
|
||||||
ReturnType<typeof decodeChannelURLData>
|
ReturnType<typeof decodeChannelURLData>
|
||||||
>
|
>
|
||||||
const info: Omit<ChannelInfo, 'community'> & {
|
const info: Omit<ChannelInfo, 'community'> & {
|
||||||
|
@ -120,13 +122,13 @@ export const useURLData = (
|
||||||
community: { displayName: data.community.displayName },
|
community: { displayName: data.community.displayName },
|
||||||
}
|
}
|
||||||
|
|
||||||
setInfo({ type: 'channel', info })
|
setData({ type: 'channel', info })
|
||||||
setChannelUuid(data.uuid)
|
setChannelUuid(data.uuid)
|
||||||
|
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
case 'profile': {
|
case 'profile': {
|
||||||
const data = verifiedDecodedData as Required<
|
const data = decodedData as Required<
|
||||||
ReturnType<typeof decodeUserURLData>
|
ReturnType<typeof decodeUserURLData>
|
||||||
>
|
>
|
||||||
const info: UserInfo = {
|
const info: UserInfo = {
|
||||||
|
@ -136,13 +138,11 @@ export const useURLData = (
|
||||||
emojiHash: publicKeyToEmojiHash(deserializedPublicKey),
|
emojiHash: publicKeyToEmojiHash(deserializedPublicKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
setInfo({ type: 'profile', info })
|
setData({ type: 'profile', info })
|
||||||
|
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setPublicKey(deserializedPublicKey)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
setError('INTERNAL_SERVER_ERROR')
|
setError('INTERNAL_SERVER_ERROR')
|
||||||
|
@ -152,7 +152,7 @@ export const useURLData = (
|
||||||
return {
|
return {
|
||||||
publicKey,
|
publicKey,
|
||||||
channelUuid,
|
channelUuid,
|
||||||
verifiedURLData: info,
|
data,
|
||||||
errorCode: error ? ERROR_CODES[error] : undefined,
|
errorCode: error ? ERROR_CODES[error] : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,8 @@ export default function CommunityPreviewPage(
|
||||||
return (
|
return (
|
||||||
<PreviewPage
|
<PreviewPage
|
||||||
type="community"
|
type="community"
|
||||||
unverifiedDecodedData={props.unverifiedDecodedData}
|
decodedData={props.decodedData}
|
||||||
unverifiedEncodedData={props.uverifiedEncodedData}
|
encodedData={props.encodedData}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,8 @@ export default function ChannelPreviewPage(
|
||||||
return (
|
return (
|
||||||
<PreviewPage
|
<PreviewPage
|
||||||
type="channel"
|
type="channel"
|
||||||
unverifiedDecodedData={props.unverifiedDecodedData}
|
decodedData={props.decodedData}
|
||||||
unverifiedEncodedData={props.uverifiedEncodedData}
|
encodedData={props.encodedData}
|
||||||
channelUuid={props.channelUuid}
|
channelUuid={props.channelUuid}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,8 +13,8 @@ export default function UserPreviewPage(
|
||||||
return (
|
return (
|
||||||
<PreviewPage
|
<PreviewPage
|
||||||
type="profile"
|
type="profile"
|
||||||
unverifiedDecodedData={props.unverifiedDecodedData}
|
decodedData={props.decodedData}
|
||||||
unverifiedEncodedData={props.uverifiedEncodedData}
|
encodedData={props.encodedData}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,17 +13,13 @@ type DecodeType =
|
||||||
|
|
||||||
export type ServerSideProps<T = ReturnType<DecodeType>> = {
|
export type ServerSideProps<T = ReturnType<DecodeType>> = {
|
||||||
/**
|
/**
|
||||||
* For verifying on client without decoding or re-encoding.
|
* For verifying on client without re-encoding.
|
||||||
*
|
|
||||||
* Verification in general is done on encoded data, so it is not
|
|
||||||
* decoded, decompressed and deserialized unnecessarily if not to be
|
|
||||||
* displayed or othwerwise needed.
|
|
||||||
*/
|
*/
|
||||||
uverifiedEncodedData: string | null
|
encodedData: string | null
|
||||||
/**
|
/**
|
||||||
* For instaneous preview even if the data is not verified yet.
|
* For instaneous preview of the content.
|
||||||
*/
|
*/
|
||||||
unverifiedDecodedData: T | null
|
decodedData: T | null
|
||||||
channelUuid?: string
|
channelUuid?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,8 +42,8 @@ export function createGetServerSideProps(decodeURLData: DecodeType) {
|
||||||
if (channelUuid) {
|
if (channelUuid) {
|
||||||
const props: ServerSideProps = {
|
const props: ServerSideProps = {
|
||||||
channelUuid: channelUuid[0],
|
channelUuid: channelUuid[0],
|
||||||
uverifiedEncodedData: null,
|
encodedData: null,
|
||||||
unverifiedDecodedData: null,
|
decodedData: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
return { props }
|
return { props }
|
||||||
|
@ -57,8 +53,8 @@ export function createGetServerSideProps(decodeURLData: DecodeType) {
|
||||||
|
|
||||||
if (!encodedData) {
|
if (!encodedData) {
|
||||||
const props: ServerSideProps = {
|
const props: ServerSideProps = {
|
||||||
uverifiedEncodedData: null,
|
encodedData: null,
|
||||||
unverifiedDecodedData: null,
|
decodedData: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
return { props }
|
return { props }
|
||||||
|
@ -66,8 +62,8 @@ export function createGetServerSideProps(decodeURLData: DecodeType) {
|
||||||
|
|
||||||
const decodedData = decodeURLData(encodedData)
|
const decodedData = decodeURLData(encodedData)
|
||||||
const props: ServerSideProps = {
|
const props: ServerSideProps = {
|
||||||
uverifiedEncodedData: encodedData,
|
encodedData,
|
||||||
unverifiedDecodedData: decodedData || null,
|
decodedData: decodedData || null,
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixme: set Cache-Control
|
// fixme: set Cache-Control
|
||||||
|
|
|
@ -22,4 +22,4 @@ export { createRequestClient } from './request-client/request-client'
|
||||||
export { deserializePublicKey } from './utils/deserialize-public-key'
|
export { deserializePublicKey } from './utils/deserialize-public-key'
|
||||||
export { publicKeyToColorHash } from './utils/public-key-to-color-hash'
|
export { publicKeyToColorHash } from './utils/public-key-to-color-hash'
|
||||||
export { publicKeyToEmojiHash } from './utils/public-key-to-emoji-hash'
|
export { publicKeyToEmojiHash } from './utils/public-key-to-emoji-hash'
|
||||||
export { verifyEncodedURLData } from './utils/sign-url-data'
|
export { recoverPublicKeyFromEncodedURLData } from './utils/sign-url-data'
|
||||||
|
|
|
@ -23,21 +23,11 @@ message User {
|
||||||
string color = 3;
|
string color = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Verification {
|
|
||||||
string signature = 1;
|
|
||||||
string public_key = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message URLData {
|
message URLData {
|
||||||
// Community, Channel, or User
|
// Community, Channel, or User
|
||||||
bytes content = 1;
|
bytes content = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message URLHash {
|
|
||||||
// Verification
|
|
||||||
bytes content = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message URLParams {
|
message URLParams {
|
||||||
string encoded_url_data = 1;
|
string encoded_url_data = 1;
|
||||||
// Signature of encoded URL data
|
// Signature of encoded URL data
|
||||||
|
|
|
@ -267,61 +267,6 @@ export class User extends Message<User> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @generated from message Verification
|
|
||||||
*/
|
|
||||||
export class Verification extends Message<Verification> {
|
|
||||||
/**
|
|
||||||
* @generated from field: string signature = 1;
|
|
||||||
*/
|
|
||||||
signature = ''
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @generated from field: string public_key = 2;
|
|
||||||
*/
|
|
||||||
publicKey = ''
|
|
||||||
|
|
||||||
constructor(data?: PartialMessage<Verification>) {
|
|
||||||
super()
|
|
||||||
proto3.util.initPartial(data, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
static readonly runtime = proto3
|
|
||||||
static readonly typeName = 'Verification'
|
|
||||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
|
||||||
{ no: 1, name: 'signature', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
|
|
||||||
{ no: 2, name: 'public_key', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
|
|
||||||
])
|
|
||||||
|
|
||||||
static fromBinary(
|
|
||||||
bytes: Uint8Array,
|
|
||||||
options?: Partial<BinaryReadOptions>
|
|
||||||
): Verification {
|
|
||||||
return new Verification().fromBinary(bytes, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJson(
|
|
||||||
jsonValue: JsonValue,
|
|
||||||
options?: Partial<JsonReadOptions>
|
|
||||||
): Verification {
|
|
||||||
return new Verification().fromJson(jsonValue, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJsonString(
|
|
||||||
jsonString: string,
|
|
||||||
options?: Partial<JsonReadOptions>
|
|
||||||
): Verification {
|
|
||||||
return new Verification().fromJsonString(jsonString, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
static equals(
|
|
||||||
a: Verification | PlainMessage<Verification> | undefined,
|
|
||||||
b: Verification | PlainMessage<Verification> | undefined
|
|
||||||
): boolean {
|
|
||||||
return proto3.util.equals(Verification, a, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message URLData
|
* @generated from message URLData
|
||||||
*/
|
*/
|
||||||
|
@ -373,57 +318,6 @@ export class URLData extends Message<URLData> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @generated from message URLHash
|
|
||||||
*/
|
|
||||||
export class URLHash extends Message<URLHash> {
|
|
||||||
/**
|
|
||||||
* Verification
|
|
||||||
*
|
|
||||||
* @generated from field: bytes content = 1;
|
|
||||||
*/
|
|
||||||
content = new Uint8Array(0)
|
|
||||||
|
|
||||||
constructor(data?: PartialMessage<URLHash>) {
|
|
||||||
super()
|
|
||||||
proto3.util.initPartial(data, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
static readonly runtime = proto3
|
|
||||||
static readonly typeName = 'URLHash'
|
|
||||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
|
||||||
{ no: 1, name: 'content', kind: 'scalar', T: 12 /* ScalarType.BYTES */ },
|
|
||||||
])
|
|
||||||
|
|
||||||
static fromBinary(
|
|
||||||
bytes: Uint8Array,
|
|
||||||
options?: Partial<BinaryReadOptions>
|
|
||||||
): URLHash {
|
|
||||||
return new URLHash().fromBinary(bytes, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJson(
|
|
||||||
jsonValue: JsonValue,
|
|
||||||
options?: Partial<JsonReadOptions>
|
|
||||||
): URLHash {
|
|
||||||
return new URLHash().fromJson(jsonValue, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJsonString(
|
|
||||||
jsonString: string,
|
|
||||||
options?: Partial<JsonReadOptions>
|
|
||||||
): URLHash {
|
|
||||||
return new URLHash().fromJsonString(jsonString, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
static equals(
|
|
||||||
a: URLHash | PlainMessage<URLHash> | undefined,
|
|
||||||
b: URLHash | PlainMessage<URLHash> | undefined
|
|
||||||
): boolean {
|
|
||||||
return proto3.util.equals(URLHash, a, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message URLParams
|
* @generated from message URLParams
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -66,7 +66,7 @@ describe('Create URLs', () => {
|
||||||
)
|
)
|
||||||
).toString()
|
).toString()
|
||||||
).toBe(
|
).toBe(
|
||||||
'https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#Co0BClhRbk8yaHc1dFZBRS1NRDVpOE1xNHNfb0dXZDByUkZtbE9iZ1JVTlFYdFVOd1AxaXhGdzkxNFk0LUJRcEYwOEtPcXBhVUxDaDdVQ3RsV1ItTzBZUDhNd0E9EjF6UTNzaFlTSHA3R29pWGFhdUpNbkRjandVMnlOamR6cFhMb3NBV2FwUFM0Q0Z4YzEx'
|
'https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#QnO2hw5tVAE-MD5i8Mq4s_oGWd0rRFmlObgRUNQXtUNwP1ixFw914Y4-BQpF08KOqpaULCh7UCtlWR-O0YP8MwA='
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ describe('Create URLs', () => {
|
||||||
)
|
)
|
||||||
).toString()
|
).toString()
|
||||||
).toBe(
|
).toBe(
|
||||||
'https://status.app/cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#Co0BClg3YWVCLU02cElidnBTVkdNNFRlSmtLV1B5YTRZUkFIYnE0YW1MMGNIbFNCcFJLbjdfbHlSNGt4RURvMmhDNGtvcVBXWWVfYWsyUjljU1ZLU2lWX25OQUE9EjF6UTNzaFlTSHA3R29pWGFhdUpNbkRjandVMnlOamR6cFhMb3NBV2FwUFM0Q0Z4YzEx'
|
'https://status.app/cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#7aeB-M6pIbvpSVGM4TeJkKWPya4YRAHbq4amL0cHlSBpRKn7_lyR4kxEDo2hC4koqPWYe_ak2R9cSVKSiV_nNAA='
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ describe('Create URLs', () => {
|
||||||
)
|
)
|
||||||
).toString()
|
).toString()
|
||||||
).toBe(
|
).toBe(
|
||||||
'https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#Co0BClhMYlFVZEpESENLb2k4RHpvWXlYODlicEtyVGpWVjNTaHFIM0U2NGJEaWZKQjJHa2VkdExCZlZLQTAyUmJVZlgwNzRwYjlpM293R3dSZFM2eF9udHhyUUE9EjF6UTNzaHdRUGhSdURKU2pWR1ZCblRqQ2RnWHk1aTlXUWFlVlBkR0pENnlUYXJKUVNq'
|
'https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#LbQUdJDHCKoi8DzoYyX89bpKrTjVV3ShqH3E64bDifJB2GkedtLBfVKA02RbUfX074pb9i3owGwRdS6x_ntxrQA='
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,14 +19,12 @@ export async function createCommunityURLWithData(
|
||||||
communityPrivateKey: Uint8Array | string
|
communityPrivateKey: Uint8Array | string
|
||||||
): Promise<URL> {
|
): Promise<URL> {
|
||||||
const encodedURLData = encodeCommunityURLData(communityData)
|
const encodedURLData = encodeCommunityURLData(communityData)
|
||||||
const encodedVerificationURLHash = await signEncodedURLData(
|
const encodedSignature = await signEncodedURLData(
|
||||||
encodedURLData,
|
encodedURLData,
|
||||||
communityPrivateKey
|
communityPrivateKey
|
||||||
)
|
)
|
||||||
|
|
||||||
return new URL(
|
return new URL(`${BASE_URL}/c/${encodedURLData}#${encodedSignature}`)
|
||||||
`${BASE_URL}/c/${encodedURLData}#${encodedVerificationURLHash}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createChannelURLWithChatKey(
|
export function createChannelURLWithChatKey(
|
||||||
|
@ -41,14 +39,12 @@ export async function createChannelURLWithData(
|
||||||
communityPrivateKey: Uint8Array | string
|
communityPrivateKey: Uint8Array | string
|
||||||
): Promise<URL> {
|
): Promise<URL> {
|
||||||
const encodedURLData = encodeChannelURLData(channelData)
|
const encodedURLData = encodeChannelURLData(channelData)
|
||||||
const encodedVerificationURLHash = await signEncodedURLData(
|
const encodedSignature = await signEncodedURLData(
|
||||||
encodedURLData,
|
encodedURLData,
|
||||||
communityPrivateKey
|
communityPrivateKey
|
||||||
)
|
)
|
||||||
|
|
||||||
return new URL(
|
return new URL(`${BASE_URL}/cc/${encodedURLData}#${encodedSignature}`)
|
||||||
`${BASE_URL}/cc/${encodedURLData}#${encodedVerificationURLHash}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createUserURLWithENS(ensName: string): URL {
|
export function createUserURLWithENS(ensName: string): URL {
|
||||||
|
@ -64,12 +60,10 @@ export async function createUserURLWithData(
|
||||||
userPrivateKey: Uint8Array | string
|
userPrivateKey: Uint8Array | string
|
||||||
): Promise<URL> {
|
): Promise<URL> {
|
||||||
const encodedURLData = encodeUserURLData(userData)
|
const encodedURLData = encodeUserURLData(userData)
|
||||||
const encodedVerificationURLHash = await signEncodedURLData(
|
const encodedSignature = await signEncodedURLData(
|
||||||
encodedURLData,
|
encodedURLData,
|
||||||
userPrivateKey
|
userPrivateKey
|
||||||
)
|
)
|
||||||
|
|
||||||
return new URL(
|
return new URL(`${BASE_URL}/u/${encodedURLData}#${encodedSignature}`)
|
||||||
`${BASE_URL}/u/${encodedURLData}#${encodedVerificationURLHash}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { describe, expect, test } from 'vitest'
|
|
||||||
|
|
||||||
import {
|
|
||||||
decodeVerificationURLHash,
|
|
||||||
encodeVerificationURLHash,
|
|
||||||
} from './encode-url-hash'
|
|
||||||
|
|
||||||
describe('Encode URL hash', () => {
|
|
||||||
test('should encode and decode verification hash', () => {
|
|
||||||
const data = {
|
|
||||||
signature:
|
|
||||||
'k-n7d-9Pcx6ht87F4riP5xAw1v7S-e1HGMRaeaO068Q3IF1Jo8xOyeMT9Yr3Wv349Z2CdBzylw8M83CgQhcMogA=', // not generated by the pk
|
|
||||||
publicKey: 'zQ3shUHp2rAM1yqBYeo6LhFbtrozG5mZeA6cRoGohsudtsieT',
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodedHash = encodeVerificationURLHash(data)
|
|
||||||
const decodedHash = decodeVerificationURLHash(encodedHash)
|
|
||||||
|
|
||||||
expect(encodedHash).toBe(
|
|
||||||
'Co0BClhrLW43ZC05UGN4Nmh0ODdGNHJpUDV4QXcxdjdTLWUxSEdNUmFlYU8wNjhRM0lGMUpvOHhPeWVNVDlZcjNXdjM0OVoyQ2RCenlsdzhNODNDZ1FoY01vZ0E9EjF6UTNzaFVIcDJyQU0xeXFCWWVvNkxoRmJ0cm96RzVtWmVBNmNSb0dvaHN1ZHRzaWVU'
|
|
||||||
)
|
|
||||||
expect(decodedHash).toEqual(data)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { base64url } from '@scure/base'
|
|
||||||
|
|
||||||
import { URLHash, Verification } from '../protos/url_pb'
|
|
||||||
|
|
||||||
import type { PlainMessage } from '@bufbuild/protobuf'
|
|
||||||
|
|
||||||
export type EncodedVerificationURLHash = string & {
|
|
||||||
_: 'EncodedVerificationURLHash'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encodeVerificationURLHash(
|
|
||||||
data: PlainMessage<Verification>
|
|
||||||
): EncodedVerificationURLHash {
|
|
||||||
return encodeURLHash(
|
|
||||||
new Verification(data).toBinary()
|
|
||||||
) as EncodedVerificationURLHash
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeVerificationURLHash(
|
|
||||||
data: string
|
|
||||||
): PlainMessage<Verification> {
|
|
||||||
const deserialized = decodeURLHash(data)
|
|
||||||
|
|
||||||
return Verification.fromBinary(
|
|
||||||
deserialized.content
|
|
||||||
).toJson() as PlainMessage<Verification>
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeURLHash(data: Uint8Array): string {
|
|
||||||
const serialized = new URLHash({ content: data }).toBinary()
|
|
||||||
const encoded = base64url.encode(serialized)
|
|
||||||
|
|
||||||
return encoded
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeURLHash(data: string): URLHash {
|
|
||||||
const decoded = base64url.decode(data)
|
|
||||||
const deserialized = URLHash.fromBinary(decoded)
|
|
||||||
|
|
||||||
return deserialized
|
|
||||||
}
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { expect, test } from 'vitest'
|
import { expect, test } from 'vitest'
|
||||||
|
|
||||||
import { encodeVerificationURLHash } from './encode-url-hash'
|
import {
|
||||||
import { signEncodedURLData, verifyEncodedURLData } from './sign-url-data'
|
recoverPublicKeyFromEncodedURLData,
|
||||||
|
signEncodedURLData,
|
||||||
|
} from './sign-url-data'
|
||||||
|
|
||||||
import type { EncodedURLData } from './encode-url-data'
|
import type { EncodedURLData } from './encode-url-data'
|
||||||
|
|
||||||
|
@ -13,50 +15,41 @@ const publicKey =
|
||||||
'0x04f9134866f2bd8f45f2bc7893c95a6b989378c370088c9a1a5a53eda2ebb8a1e8386921592b6bd56fc3573f03c46df3396cc42e2993cdc001855c858865d768a7'
|
'0x04f9134866f2bd8f45f2bc7893c95a6b989378c370088c9a1a5a53eda2ebb8a1e8386921592b6bd56fc3573f03c46df3396cc42e2993cdc001855c858865d768a7'
|
||||||
const encodedURLData =
|
const encodedURLData =
|
||||||
'G74AgK0ObFNmYT-WC_Jcc9KfSjHXAQo9THKEEbgPaJoItceMES-bUxr2Tj9efv447rRefBIUg9CEsSFyjBOFTRdZ9PH2wUOW8hVNYqIje3BC96mZ8uFogqM6k7gCCJnMHy4ulsmsgHTdeh5dAzTNNuG8m9XB8oVeildTCKlRhINnTZh4kAl5sP8SzBB4V2_I41a8PKl3mcS0z_eF5gA=' as EncodedURLData
|
'G74AgK0ObFNmYT-WC_Jcc9KfSjHXAQo9THKEEbgPaJoItceMES-bUxr2Tj9efv447rRefBIUg9CEsSFyjBOFTRdZ9PH2wUOW8hVNYqIje3BC96mZ8uFogqM6k7gCCJnMHy4ulsmsgHTdeh5dAzTNNuG8m9XB8oVeildTCKlRhINnTZh4kAl5sP8SzBB4V2_I41a8PKl3mcS0z_eF5gA=' as EncodedURLData
|
||||||
|
const encodedSignature =
|
||||||
|
'k-n7d-9Pcx6ht87F4riP5xAw1v7S-e1HGMRaeaO068Q3IF1Jo8xOyeMT9Yr3Wv349Z2CdBzylw8M83CgQhcMogA='
|
||||||
|
|
||||||
test('should verify URL data and correspoinding signature', async () => {
|
test('should sign URL data', async () => {
|
||||||
const encodedVerificationURLHash = await signEncodedURLData(
|
expect(await signEncodedURLData(encodedURLData, privateKey)).toBe(
|
||||||
encodedURLData,
|
encodedSignature
|
||||||
privateKey
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(verifyEncodedURLData(encodedURLData, encodedVerificationURLHash)).toBe(
|
|
||||||
true
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not verify URL data and random signature', async () => {
|
test('should recover original public key from URL data', async () => {
|
||||||
const randomSignature =
|
expect(
|
||||||
|
await recoverPublicKeyFromEncodedURLData(encodedURLData, encodedSignature)
|
||||||
|
).toBe(publicKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not recover original public key from same URL data but changed signature', async () => {
|
||||||
|
const changedEncodedSignature =
|
||||||
'OyOgY6Zta8S7U4l5Bv_9E_7snALhixwvjxORVAVJ-YJk-tMSGgstOy5XEEQx25TQJIAtpWf8eHnEmV8V-GmouQA='
|
'OyOgY6Zta8S7U4l5Bv_9E_7snALhixwvjxORVAVJ-YJk-tMSGgstOy5XEEQx25TQJIAtpWf8eHnEmV8V-GmouQA='
|
||||||
|
|
||||||
const encodedVerificationURLHash = encodeVerificationURLHash({
|
expect(
|
||||||
signature: randomSignature,
|
await recoverPublicKeyFromEncodedURLData(
|
||||||
publicKey,
|
encodedURLData,
|
||||||
})
|
changedEncodedSignature
|
||||||
|
)
|
||||||
expect(verifyEncodedURLData(encodedURLData, encodedVerificationURLHash)).toBe(
|
).not.toBe(publicKey)
|
||||||
false
|
|
||||||
)
|
|
||||||
// see https://github.com/paulmillr/noble-secp256k1/issues/43#issuecomment-1020214968
|
|
||||||
// expect(verifyEncodedURLData(randomSignature, encodedURLData)).toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not verify random URL data and random signature', async () => {
|
test('should not recover original public key from same signature but changed URL data', async () => {
|
||||||
const randomEncodedURLData =
|
const changedEncodedURLData =
|
||||||
'CyeACk0KHkxvcmVtIGlwc3VtIGRvbG9yIHNpdCBlZ2VzdGFzLhIYV2UgZG8gbm90IHN1cHBvcnQgQWxpY2UuGMCEPSIHIzQzNjBERioEAQIDBAM=' as EncodedURLData
|
'CyeACk0KHkxvcmVtIGlwc3VtIGRvbG9yIHNpdCBlZ2VzdGFzLhIYV2UgZG8gbm90IHN1cHBvcnQgQWxpY2UuGMCEPSIHIzQzNjBERioEAQIDBAM=' as EncodedURLData
|
||||||
const randomSignature =
|
|
||||||
'k-n7d-9Pcx6ht87F4riP5xAw1v7S-e1HGMRaeaO068Q3IF1Jo8xOyeMT9Yr3Wv349Z2CdBzylw8M83CgQhcMogA='
|
|
||||||
|
|
||||||
const encodedVerificationURLHash = encodeVerificationURLHash({
|
|
||||||
signature: randomSignature,
|
|
||||||
publicKey,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
verifyEncodedURLData(randomEncodedURLData, encodedVerificationURLHash)
|
await recoverPublicKeyFromEncodedURLData(
|
||||||
).toBe(false)
|
changedEncodedURLData,
|
||||||
// see https://github.com/paulmillr/noble-secp256k1/issues/43#issuecomment-1020214968
|
encodedSignature
|
||||||
// expect(verifyEncodedURLData(randomSignature, randomEncodedURLData)).toBe(
|
)
|
||||||
// false
|
).not.toBe(publicKey)
|
||||||
// )
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,51 +1,30 @@
|
||||||
import { base64url } from '@scure/base'
|
import { base64url } from '@scure/base'
|
||||||
import { getPublicKey } from 'ethereum-cryptography/secp256k1'
|
import { toHex, utf8ToBytes as toBytes } from 'ethereum-cryptography/utils'
|
||||||
import { bytesToHex } from 'ethereum-cryptography/utils'
|
|
||||||
|
|
||||||
import { deserializePublicKey } from './deserialize-public-key'
|
import { recoverPublicKey } from './recover-public-key'
|
||||||
import {
|
import { signData } from './sign-data'
|
||||||
decodeVerificationURLHash,
|
|
||||||
encodeVerificationURLHash,
|
|
||||||
} from './encode-url-hash'
|
|
||||||
import { serializePublicKey } from './serialize-public-key'
|
|
||||||
import { signData, verifySignedData } from './sign-data'
|
|
||||||
|
|
||||||
import type { EncodedURLData } from './encode-url-data'
|
import type { EncodedURLData } from './encode-url-data'
|
||||||
import type { EncodedVerificationURLHash } from './encode-url-hash'
|
|
||||||
|
|
||||||
export async function signEncodedURLData(
|
export async function signEncodedURLData(
|
||||||
encodedURLData: EncodedURLData,
|
encodedURLData: EncodedURLData,
|
||||||
privateKey: Uint8Array | string
|
privateKey: Uint8Array | string
|
||||||
): Promise<EncodedVerificationURLHash> {
|
): Promise<string> {
|
||||||
const signature = await signData(encodedURLData, privateKey)
|
const signature = await signData(encodedURLData, privateKey)
|
||||||
|
|
||||||
const encodedSignature = base64url.encode(signature)
|
const encodedSignature = base64url.encode(signature)
|
||||||
const serializedPublicKey = serializePublicKey(
|
|
||||||
`0x${bytesToHex(getPublicKey(privateKey))}`
|
|
||||||
)
|
|
||||||
|
|
||||||
return encodeVerificationURLHash({
|
return encodedSignature
|
||||||
signature: encodedSignature,
|
|
||||||
publicKey: serializedPublicKey,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function verifyEncodedURLData(
|
export function recoverPublicKeyFromEncodedURLData(
|
||||||
encodedURLData: string,
|
encodedURLData: string,
|
||||||
encodedVerificationURLHash: string
|
encodedSignature: string
|
||||||
): boolean {
|
): string {
|
||||||
const { signature, publicKey } = decodeVerificationURLHash(
|
const decodedSignature = base64url.decode(encodedSignature)
|
||||||
encodedVerificationURLHash
|
const recoveredPublicKey = recoverPublicKey(
|
||||||
)
|
|
||||||
|
|
||||||
const decodedSignature = base64url.decode(signature)
|
|
||||||
const deserializedPublicKey = deserializePublicKey(publicKey, {
|
|
||||||
compress: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
return verifySignedData(
|
|
||||||
decodedSignature,
|
decodedSignature,
|
||||||
encodedURLData,
|
toBytes(encodedURLData)
|
||||||
deserializedPublicKey
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return `0x${toHex(recoveredPublicKey)}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,7 @@ export default defineConfig(({ mode }) => {
|
||||||
build: {
|
build: {
|
||||||
target: 'es2020',
|
target: 'es2020',
|
||||||
lib: {
|
lib: {
|
||||||
entry: [
|
entry: ['./src/index.ts', './src/utils/encode-url-data.ts'],
|
||||||
'./src/index.ts',
|
|
||||||
'./src/utils/encode-url-data.ts',
|
|
||||||
'./src/utils/encode-url-hash.ts',
|
|
||||||
],
|
|
||||||
fileName: (format, entryName) => {
|
fileName: (format, entryName) => {
|
||||||
if (!['es'].includes(format)) {
|
if (!['es'].includes(format)) {
|
||||||
throw new Error(`Unexpected format: ${format}`)
|
throw new Error(`Unexpected format: ${format}`)
|
||||||
|
|
Loading…
Reference in New Issue