Skip to content

Commit 5e93b19

Browse files
committed
fix: handle unicode redirect payloads
1 parent fce05da commit 5e93b19

3 files changed

Lines changed: 120 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@0xsequence/dapp-client': patch
3+
---
4+
5+
Fix redirect transport payload encoding so Unicode characters are handled correctly in redirect requests and responses.

packages/wallet/dapp-client/src/DappTransport.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,26 @@ import {
1414

1515
const isBrowserEnvironment = typeof window !== 'undefined' && typeof document !== 'undefined'
1616

17+
const bytesToBinaryString = (bytes: Uint8Array) => {
18+
let binary = ''
19+
const chunkSize = 0x8000
20+
for (let i = 0; i < bytes.length; i += chunkSize) {
21+
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize))
22+
}
23+
return binary
24+
}
25+
26+
const binaryStringToBytes = (value: string) => {
27+
const bytes = new Uint8Array(value.length)
28+
for (let i = 0; i < value.length; i += 1) {
29+
bytes[i] = value.charCodeAt(i)
30+
}
31+
return bytes
32+
}
33+
1734
const base64Encode = (value: string) => {
18-
if (typeof btoa !== 'undefined') {
19-
return btoa(value)
35+
if (typeof btoa !== 'undefined' && typeof TextEncoder !== 'undefined') {
36+
return btoa(bytesToBinaryString(new TextEncoder().encode(value)))
2037
}
2138
if (typeof Buffer !== 'undefined') {
2239
return Buffer.from(value, 'utf-8').toString('base64')
@@ -25,8 +42,13 @@ const base64Encode = (value: string) => {
2542
}
2643

2744
const base64Decode = (value: string) => {
28-
if (typeof atob !== 'undefined') {
29-
return atob(value)
45+
if (typeof atob !== 'undefined' && typeof TextDecoder !== 'undefined') {
46+
const decoded = atob(value)
47+
try {
48+
return new TextDecoder('utf-8', { fatal: true }).decode(binaryStringToBytes(decoded))
49+
} catch {
50+
return decoded
51+
}
3052
}
3153
if (typeof Buffer !== 'undefined') {
3254
return Buffer.from(value, 'base64').toString('utf-8')
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { DappTransport } from '../src/DappTransport.js'
4+
import { TransportMode } from '../src/types/index.js'
5+
6+
const encodeBase64Utf8 = (value: string) => {
7+
let binary = ''
8+
for (const byte of new TextEncoder().encode(value)) {
9+
binary += String.fromCharCode(byte)
10+
}
11+
return btoa(binary)
12+
}
13+
14+
const decodeBase64Utf8 = (value: string) => {
15+
const binary = atob(value)
16+
const bytes = new Uint8Array(binary.length)
17+
for (let i = 0; i < binary.length; i += 1) {
18+
bytes[i] = binary.charCodeAt(i)
19+
}
20+
return new TextDecoder().decode(bytes)
21+
}
22+
23+
const createSessionStorage = () => {
24+
const values = new Map<string, string>()
25+
return {
26+
getItem: (key: string) => values.get(key) ?? null,
27+
setItem: (key: string, value: string) => {
28+
values.set(key, value)
29+
},
30+
removeItem: (key: string) => {
31+
values.delete(key)
32+
},
33+
}
34+
}
35+
36+
describe('DappTransport redirect URLs', () => {
37+
it('encodes unicode payloads as UTF-8 base64', async () => {
38+
const transport = new DappTransport('https://wallet.example', TransportMode.REDIRECT, {}, createSessionStorage())
39+
const payload = { message: 'Sign in to Sequence 🌍' }
40+
41+
const redirectUrl = await transport.getRequestRedirectUrl('signMessage', payload, 'https://dapp.example/callback')
42+
const encodedPayload = new URL(redirectUrl).searchParams.get('payload')
43+
44+
if (!encodedPayload) {
45+
throw new Error('Expected redirect URL to include a payload')
46+
}
47+
expect(JSON.parse(decodeBase64Utf8(encodedPayload))).toEqual(payload)
48+
})
49+
50+
it('decodes unicode redirect response payloads', async () => {
51+
const storage = createSessionStorage()
52+
const transport = new DappTransport('https://wallet.example', TransportMode.REDIRECT, {}, storage)
53+
const requestUrl = await transport.getRequestRedirectUrl('signMessage', {}, 'https://dapp.example/callback')
54+
const id = new URL(requestUrl).searchParams.get('id')
55+
const payload = { message: 'Signed by Sequence 🌍' }
56+
const responseUrl = new URL('https://dapp.example/callback')
57+
58+
if (!id) {
59+
throw new Error('Expected redirect URL to include an id')
60+
}
61+
responseUrl.searchParams.set('id', id)
62+
responseUrl.searchParams.set('payload', encodeBase64Utf8(JSON.stringify(payload)))
63+
64+
await expect(transport.getRedirectResponse(false, responseUrl.toString())).resolves.toEqual({
65+
action: 'signMessage',
66+
payload,
67+
})
68+
})
69+
70+
it('decodes legacy Latin-1 redirect response payloads', async () => {
71+
const storage = createSessionStorage()
72+
const transport = new DappTransport('https://wallet.example', TransportMode.REDIRECT, {}, storage)
73+
const requestUrl = await transport.getRequestRedirectUrl('signMessage', {}, 'https://dapp.example/callback')
74+
const id = new URL(requestUrl).searchParams.get('id')
75+
const payload = { message: 'Signed by Sequence Café' }
76+
const responseUrl = new URL('https://dapp.example/callback')
77+
78+
if (!id) {
79+
throw new Error('Expected redirect URL to include an id')
80+
}
81+
responseUrl.searchParams.set('id', id)
82+
responseUrl.searchParams.set('payload', btoa(JSON.stringify(payload)))
83+
84+
await expect(transport.getRedirectResponse(false, responseUrl.toString())).resolves.toEqual({
85+
action: 'signMessage',
86+
payload,
87+
})
88+
})
89+
})

0 commit comments

Comments
 (0)