parent
98741fb49c
commit
f9af5e0216
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@status-im/js': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
validate encoded data
|
|
@ -32,7 +32,34 @@ describe('Encode URL data', () => {
|
||||||
expect(decodedData).toEqual(data)
|
expect(decodedData).toEqual(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should encode and decode channel', () => {
|
test('should throw for invalid community data', () => {
|
||||||
|
expect(() => {
|
||||||
|
const encodedData = 'Ow=='
|
||||||
|
decodeCommunityURLData(encodedData)
|
||||||
|
}).toThrowError()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw for unsupported data length', () => {
|
||||||
|
expect(() => {
|
||||||
|
const encodedData =
|
||||||
|
'G2QBQJwFdqwxrBnNb57kP0irrJpuouIjS1WZqHS6A2txojsUHidyu3evaAO3GQQku5NCQXiwAYchBIMNyptts=MD9bZAwoTasraIMkjbS1uAD7oxsAQ53OAmQWCefyBuuXlAu6J7eKQRQhgg5tan75fFp9jwGIjBLbGhnyUht2qj5GWlSBp7_OXsHxgnr21xA2HgR9VGYYikQJA4tcQHDrQzg_ARC9KiOVDD6vgTCM9_CN0HJ1zxwP3w6nzgkDTNuvDCFD3Clqo6Cf_UNY2cNRlKTqj86G4gC2dUNSApwiq72BdGTtrleiRFPUhCbTRbmEG4YwFOs4EjBdJHHRiqjS5GYGc1dAdgcGr2BQ==============================================================================================================================================='
|
||||||
|
decodeCommunityURLData(encodedData)
|
||||||
|
}).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"too_big\\",
|
||||||
|
\\"maximum\\": 500,
|
||||||
|
\\"type\\": \\"string\\",
|
||||||
|
\\"inclusive\\": true,
|
||||||
|
\\"exact\\": false,
|
||||||
|
\\"message\\": \\"String must contain at most 500 character(s)\\",
|
||||||
|
\\"path\\": []
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should encode and decode channel', () => {
|
||||||
const data = {
|
const data = {
|
||||||
emoji: '🏴',
|
emoji: '🏴',
|
||||||
displayName: 'lorem-ipsum-dolore-nulla',
|
displayName: 'lorem-ipsum-dolore-nulla',
|
||||||
|
@ -56,6 +83,70 @@ describe('Encode URL data', () => {
|
||||||
expect(decodedData).toEqual(data)
|
expect(decodedData).toEqual(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should throw for invalid channel data', () => {
|
||||||
|
expect(() => {
|
||||||
|
const encodedData = 'Ow=='
|
||||||
|
decodeChannelURLData(encodedData)
|
||||||
|
}).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"displayName\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"description\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"emoji\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"color\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"object\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"community\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"uuid\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
test('should encode and decode user', () => {
|
test('should encode and decode user', () => {
|
||||||
const data = {
|
const data = {
|
||||||
displayName: 'Lorem ipsum dolore nulla',
|
displayName: 'Lorem ipsum dolore nulla',
|
||||||
|
@ -72,4 +163,68 @@ describe('Encode URL data', () => {
|
||||||
)
|
)
|
||||||
expect(decodedData).toEqual(data)
|
expect(decodedData).toEqual(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should throw for invalid user data', () => {
|
||||||
|
expect(() => {
|
||||||
|
const encodedData = 'Ow=='
|
||||||
|
decodeChannelURLData(encodedData)
|
||||||
|
}).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"displayName\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"description\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"emoji\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"color\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"object\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"community\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"uuid\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { base64url } from '@scure/base'
|
import { base64url } from '@scure/base'
|
||||||
import { brotliCompressSync, brotliDecompressSync } from 'zlib'
|
import { brotliCompressSync, brotliDecompressSync } from 'zlib'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
import { Channel, Community, URLData, User } from '../protos/url_pb'
|
import { Channel, Community, URLData, User } from '../protos/url_pb'
|
||||||
|
|
||||||
|
@ -7,6 +8,17 @@ import type { PlainMessage } from '@bufbuild/protobuf'
|
||||||
|
|
||||||
export type EncodedURLData = string & { _: 'EncodedURLData' }
|
export type EncodedURLData = string & { _: 'EncodedURLData' }
|
||||||
|
|
||||||
|
const colorSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/)
|
||||||
|
const communityDisplayName = z.string().max(30).nonempty()
|
||||||
|
|
||||||
|
const communitySchema = z.object({
|
||||||
|
displayName: communityDisplayName,
|
||||||
|
description: z.string().max(140).nonempty(),
|
||||||
|
membersCount: z.number().nonnegative(),
|
||||||
|
color: colorSchema,
|
||||||
|
tagIndices: z.number().nonnegative().array(),
|
||||||
|
})
|
||||||
|
|
||||||
export function encodeCommunityURLData(
|
export function encodeCommunityURLData(
|
||||||
data: PlainMessage<Community>
|
data: PlainMessage<Community>
|
||||||
): EncodedURLData {
|
): EncodedURLData {
|
||||||
|
@ -17,25 +29,48 @@ export function encodeCommunityURLData(
|
||||||
export function decodeCommunityURLData(data: string): PlainMessage<Community> {
|
export function decodeCommunityURLData(data: string): PlainMessage<Community> {
|
||||||
const deserialized = decodeURLData(data)
|
const deserialized = decodeURLData(data)
|
||||||
|
|
||||||
return Community.fromBinary(
|
const community = Community.fromBinary(deserialized.content).toJson()
|
||||||
deserialized.content
|
|
||||||
).toJson() as PlainMessage<Community>
|
return communitySchema.parse(community)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const channelSchema = z.object({
|
||||||
|
displayName: z.string().max(24).nonempty(),
|
||||||
|
description: z.string().max(140).nonempty(),
|
||||||
|
emoji: z.string().emoji(),
|
||||||
|
color: colorSchema,
|
||||||
|
community: z.object({
|
||||||
|
displayName: communityDisplayName,
|
||||||
|
}),
|
||||||
|
uuid: z.string().uuid(),
|
||||||
|
})
|
||||||
|
|
||||||
export function encodeChannelURLData(
|
export function encodeChannelURLData(
|
||||||
data: PlainMessage<Channel>
|
data: PlainMessage<Channel>
|
||||||
): EncodedURLData {
|
): EncodedURLData {
|
||||||
return encodeURLData(new Channel(data).toBinary()) as EncodedURLData
|
return encodeURLData(new Channel(data).toBinary()) as EncodedURLData
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeChannelURLData(data: string): PlainMessage<Channel> {
|
export function decodeChannelURLData(data: string): Omit<
|
||||||
|
PlainMessage<Channel>,
|
||||||
|
'community'
|
||||||
|
> & {
|
||||||
|
community: Pick<PlainMessage<Community>, 'displayName'>
|
||||||
|
} {
|
||||||
const deserialized = decodeURLData(data)
|
const deserialized = decodeURLData(data)
|
||||||
|
|
||||||
return Channel.fromBinary(
|
const channel = Channel.fromBinary(deserialized.content).toJson()
|
||||||
deserialized.content
|
|
||||||
).toJson() as PlainMessage<Channel>
|
return channelSchema.parse(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userSchema = z.object({
|
||||||
|
displayName: z.string().max(24).nonempty(),
|
||||||
|
description: z.string().max(240).nonempty(),
|
||||||
|
// fixme: await integration in native platforms
|
||||||
|
color: colorSchema.optional().default('#ffffff'),
|
||||||
|
})
|
||||||
|
|
||||||
export function encodeUserURLData(data: PlainMessage<User>): EncodedURLData {
|
export function encodeUserURLData(data: PlainMessage<User>): EncodedURLData {
|
||||||
return encodeURLData(new User(data).toBinary()) as EncodedURLData
|
return encodeURLData(new User(data).toBinary()) as EncodedURLData
|
||||||
}
|
}
|
||||||
|
@ -43,7 +78,9 @@ export function encodeUserURLData(data: PlainMessage<User>): EncodedURLData {
|
||||||
export function decodeUserURLData(data: string): PlainMessage<User> {
|
export function decodeUserURLData(data: string): PlainMessage<User> {
|
||||||
const deserialized = decodeURLData(data)
|
const deserialized = decodeURLData(data)
|
||||||
|
|
||||||
return User.fromBinary(deserialized.content).toJson() as PlainMessage<User>
|
const user = User.fromBinary(deserialized.content).toJson()
|
||||||
|
|
||||||
|
return userSchema.parse(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeURLData(data: Uint8Array): string {
|
function encodeURLData(data: Uint8Array): string {
|
||||||
|
@ -57,6 +94,11 @@ function encodeURLData(data: Uint8Array): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeURLData(data: string): URLData {
|
function decodeURLData(data: string): URLData {
|
||||||
|
// note: https://github.com/status-im/status-web/pull/345#discussion_r1113129396 observed lengths
|
||||||
|
// note?: https://docs.google.com/spreadsheets/d/1JD4kp0aUm90piUZ7FgM_c2NGe2PdN8BFB11wmt5UZIY/view#gid=1260088614 limit for url path segmets not split by ";" or "_"
|
||||||
|
// fixme: set to 301 per url path segment when the above mentioned splitting is implemented
|
||||||
|
z.string().max(500).parse(data) // default max in order not to compute arbitrary values
|
||||||
|
|
||||||
const decoded = base64url.decode(data)
|
const decoded = base64url.decode(data)
|
||||||
const decompressed = brotliDecompressSync(decoded)
|
const decompressed = brotliDecompressSync(decoded)
|
||||||
const deserialized = URLData.fromBinary(decompressed)
|
const deserialized = URLData.fromBinary(decompressed)
|
||||||
|
|
Loading…
Reference in New Issue