Skip to content

Commit c2f694f

Browse files
authored
Merge pull request #590 from Telegram-Mini-Apps/feature/3rd-party-init-data-validation
3rd party init data validation utilities
2 parents 025d2f3 + a91a0cb commit c2f694f

30 files changed

+576
-415
lines changed

.changeset/eleven-badgers-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@telegram-apps/init-data-node": minor
3+
---
4+
5+
Add utilities related to 3-rd party validation. Git rid of Node.js `Buffer`.

.changeset/mean-tomatoes-search.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@telegram-apps/types": minor
3+
---
4+
5+
Define InitData.signature.

.changeset/perfect-icons-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@telegram-apps/transformers": minor
3+
---
4+
5+
Add InitData.signature transformer.

apps/docs/packages/telegram-apps-init-data-node.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
---
2+
outline: [2, 3]
3+
---
4+
15
# @telegram-apps/init-data-node
26

37
<p style="display: flex; gap: 8px; min-height: 20px">
@@ -133,6 +137,45 @@ duration is set to 1 day (86,400 seconds). It is recommended to always check the
133137
initialization data, as it could be stolen but still remain valid. To disable this feature,
134138
pass `{ expiresIn: 0 }` as the third argument.
135139

140+
### `validate3rd`
141+
142+
The `validate3rd` function is used to check if the passed init data was signed by Telegram. As
143+
well as the `validate` function, this one accepts the init data in the same format.
144+
145+
As the second argument, it accepts the Telegram Bot identifier that was used to sign this
146+
init data.
147+
148+
The third argument is an object with the following properties:
149+
150+
- `expiresIn` is responsible for init data expiration validation
151+
- `test: boolean`: should be equal `true` if the init data was received in the test Telegram
152+
environment
153+
154+
Here is the usage example:
155+
156+
```ts
157+
const initData =
158+
'user=%7B%22id%22%3A279058397%2C%22first_name%22%3A%22Vladislav%20%2B%20-%20%3F%20%5C%2F%22%2C%22last_name%22%3A%22Kibenko%22%2C%22username%22%3A%22vdkfrost%22%2C%22language_code%22%3A%22ru%22%2C%22is_premium%22%3Atrue%2C%22allows_write_to_pm%22%3Atrue%2C%22photo_url%22%3A%22https%3A%5C%2F%5C%2Ft.me%5C%2Fi%5C%2Fuserpic%5C%2F320%5C%2F4FPEE4tmP3ATHa57u6MqTDih13LTOiMoKoLDRG4PnSA.svg%22%7D' +
159+
'&chat_instance=8134722200314281151' +
160+
'&chat_type=private' +
161+
'&auth_date=1733584787' +
162+
'&signature=zL-ucjNyREiHDE8aihFwpfR9aggP2xiAo3NSpfe-p7IbCisNlDKlo7Kb6G4D0Ao2mBrSgEk4maLSdv6MLIlADQ' +
163+
'&hash=2174df5b000556d044f3f020384e879c8efcab55ddea2ced4eb752e93e7080d6';
164+
const botId = 7342037359;
165+
166+
await validate3rd(initData, botId);
167+
```
168+
169+
Function will throw an error in one of these cases:
170+
171+
- `ERR_AUTH_DATE_INVALID`: `auth_date` is empty or not found
172+
- `ERR_SIGNATURE_MISSING`: `signature` is empty or not found
173+
- `ERR_SIGN_INVALID`: signature is invalid
174+
- `ERR_EXPIRED`: init data expired
175+
176+
> [!WARNING]
177+
> This function uses **Web Crypto API** instead of Node.js modules.
178+
136179
### `isValid`
137180

138181
Alternatively, to check the init data validity, a developer could use the `isValid` function.
@@ -141,11 +184,26 @@ It doesn't throw an error, but returns a boolean value indicating the init data
141184
```ts
142185
import { isValid } from '@telegram-apps/init-data-node';
143186

144-
if (isValid('init-data')) {
187+
if (isValid('init-data', 'my-bot-token')) {
145188
console.log('Init data is fine');
146189
}
147190
```
148191

192+
### `isValid3rd`
193+
194+
Does the same as the `isValid` function, but checks if the init data was signed by Telegram:
195+
196+
```ts
197+
import { isValid3rd } from '@telegram-apps/init-data-node';
198+
199+
if (await isValid3rd('init-data')) {
200+
console.log('Init data is fine');
201+
}
202+
```
203+
204+
> [!WARNING]
205+
> This function uses **Web Crypto API** instead of Node.js modules.
206+
149207
## Signing
150208

151209
There could be some cases when a developer needs to create their own init data. For instance,

packages/bridge/src/env/mockTelegramEnv.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,16 @@ const lp: LaunchParams = {
4343
lastName: 'Rogue',
4444
username: 'rogue',
4545
},
46+
signature: 'abc',
4647
},
47-
initDataRaw: 'user=%7B%22id%22%3A99281932%2C%22first_name%22%3A%22Andrew%22%2C%22last_name%22%3A%22Rogue%22%2C%22username%22%3A%22rogue%22%2C%22language_code%22%3A%22en%22%2C%22is_premium%22%3Atrue%2C%22allows_write_to_pm%22%3Atrue%7D&chat_instance=8428209589180549439&chat_type=sender&start_param=debug&auth_date=1716922846&hash=89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31',
48+
initDataRaw: 'user=%7B%22id%22%3A99281932%2C%22first_name%22%3A%22Andrew%22%2C%22last_name%22%3A%22Rogue%22%2C%22username%22%3A%22rogue%22%2C%22language_code%22%3A%22en%22%2C%22is_premium%22%3Atrue%2C%22allows_write_to_pm%22%3Atrue%7D&chat_instance=8428209589180549439&chat_type=sender&start_param=debug&auth_date=1716922846&hash=89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31&signature=abc',
4849
version: '7.2',
4950
platform: 'tdesktop',
5051
botInline: false,
5152
showSettings: false
5253
};
5354

54-
const lpString = 'tgWebAppPlatform=tdesktop&tgWebAppThemeParams=%7B%22accent_text_color%22%3A%22%236ab2f2%22%2C%22bg_color%22%3A%22%2317212b%22%2C%22button_color%22%3A%22%235288c1%22%2C%22button_text_color%22%3A%22%23ffffff%22%2C%22destructive_text_color%22%3A%22%23ec3942%22%2C%22header_bg_color%22%3A%22%2317212b%22%2C%22hint_color%22%3A%22%23708499%22%2C%22link_color%22%3A%22%236ab3f3%22%2C%22secondary_bg_color%22%3A%22%23232e3c%22%2C%22section_bg_color%22%3A%22%2317212b%22%2C%22section_header_text_color%22%3A%22%236ab3f3%22%2C%22subtitle_text_color%22%3A%22%23708499%22%2C%22text_color%22%3A%22%23f5f5f5%22%7D&tgWebAppVersion=7.2&tgWebAppData=user%3D%257B%2522id%2522%253A99281932%252C%2522first_name%2522%253A%2522Andrew%2522%252C%2522last_name%2522%253A%2522Rogue%2522%252C%2522username%2522%253A%2522rogue%2522%252C%2522language_code%2522%253A%2522en%2522%252C%2522is_premium%2522%253Atrue%252C%2522allows_write_to_pm%2522%253Atrue%257D%26chat_instance%3D8428209589180549439%26chat_type%3Dsender%26start_param%3Ddebug%26auth_date%3D1716922846%26hash%3D89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31&tgWebAppShowSettings=0&tgWebAppBotInline=0';
55+
const lpString = 'tgWebAppPlatform=tdesktop&tgWebAppThemeParams=%7B%22accent_text_color%22%3A%22%236ab2f2%22%2C%22bg_color%22%3A%22%2317212b%22%2C%22button_color%22%3A%22%235288c1%22%2C%22button_text_color%22%3A%22%23ffffff%22%2C%22destructive_text_color%22%3A%22%23ec3942%22%2C%22header_bg_color%22%3A%22%2317212b%22%2C%22hint_color%22%3A%22%23708499%22%2C%22link_color%22%3A%22%236ab3f3%22%2C%22secondary_bg_color%22%3A%22%23232e3c%22%2C%22section_bg_color%22%3A%22%2317212b%22%2C%22section_header_text_color%22%3A%22%236ab3f3%22%2C%22subtitle_text_color%22%3A%22%23708499%22%2C%22text_color%22%3A%22%23f5f5f5%22%7D&tgWebAppVersion=7.2&tgWebAppData=user%3D%257B%2522id%2522%253A99281932%252C%2522first_name%2522%253A%2522Andrew%2522%252C%2522last_name%2522%253A%2522Rogue%2522%252C%2522username%2522%253A%2522rogue%2522%252C%2522language_code%2522%253A%2522en%2522%252C%2522is_premium%2522%253Atrue%252C%2522allows_write_to_pm%2522%253Atrue%257D%26chat_instance%3D8428209589180549439%26chat_type%3Dsender%26start_param%3Ddebug%26auth_date%3D1716922846%26hash%3D89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31%26signature%3Dabc&tgWebAppShowSettings=0&tgWebAppBotInline=0';
5556

5657
it('should save passed launch parameters in session storage', () => {
5758
const setItem = mockSessionStorageSetItem();

packages/bridge/src/launch-params/parseLaunchParams.test.ts

Lines changed: 0 additions & 85 deletions
This file was deleted.

packages/bridge/src/utils/request.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,18 @@ describe('options', () => {
8585
expect(globalPostEvent).toHaveBeenCalledWith('web_app_request_phone', undefined);
8686
});
8787

88-
it('should reject promise if postEvent threw an error', () => {
88+
it('should reject promise if postEvent threw an error', async () => {
8989
const promise = request('web_app_request_phone', 'phone_requested', {
9090
postEvent() {
9191
throw new Error('Nope!');
9292
},
9393
});
94-
void expect(promise).rejects.toStrictEqual(new Error('Nope!'));
94+
await expect(promise).rejects.toStrictEqual(new Error('Nope!'));
9595
});
9696
});
9797

9898
describe('capture', () => {
99-
it('should capture an event in case, capture method returned true', () => {
99+
it('should capture an event in case, capture method returned true', async () => {
100100
const promise = request('web_app_request_phone', 'phone_requested', {
101101
timeout: 1000,
102102
capture: ({ status }) => status === 'allowed',
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Converts array buffer to hex.
3+
* @param buffer - buffer to convert
4+
*/
5+
export function arrayBufferToHex(buffer: ArrayBuffer): string {
6+
return new Uint8Array(buffer).reduce((acc, byte) => {
7+
// Convert byte to hex and pad with zero if needed (e.g., "0a" instead of "a")
8+
return acc + byte.toString(16).padStart(2, '0');
9+
}, '');
10+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Converts a hex string to ArrayBuffer.
3+
* @param hexString - value to convert.
4+
*/
5+
export function hexToArrayBuffer(hexString: string): ArrayBuffer {
6+
if (hexString.length % 2 !== 0) {
7+
throw new Error('Hex string must have an even number of characters');
8+
}
9+
10+
const buffer = new ArrayBuffer(hexString.length / 2);
11+
const uint8Array = new Uint8Array(buffer);
12+
for (let i = 0; i < hexString.length; i += 2) {
13+
uint8Array[i / 2] = parseInt(hexString.substring(i, i + 2), 16);
14+
}
15+
16+
return buffer;
17+
}

0 commit comments

Comments
 (0)