Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-badgers-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@telegram-apps/init-data-node": minor
---

Add utilities related to 3-rd party validation. Git rid of Node.js `Buffer`.
5 changes: 5 additions & 0 deletions .changeset/mean-tomatoes-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@telegram-apps/types": minor
---

Define InitData.signature.
5 changes: 5 additions & 0 deletions .changeset/perfect-icons-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@telegram-apps/transformers": minor
---

Add InitData.signature transformer.
60 changes: 59 additions & 1 deletion apps/docs/packages/telegram-apps-init-data-node.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
outline: [2, 3]
---

# @telegram-apps/init-data-node

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

### `validate3rd`

The `validate3rd` function is used to check if the passed init data was signed by Telegram. As
well as the `validate` function, this one accepts the init data in the same format.

As the second argument, it accepts the Telegram Bot identifier that was used to sign this
init data.

The third argument is an object with the following properties:

- `expiresIn` is responsible for init data expiration validation
- `test: boolean`: should be equal `true` if the init data was received in the test Telegram
environment

Here is the usage example:

```ts
const initData =
'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' +
'&chat_instance=8134722200314281151' +
'&chat_type=private' +
'&auth_date=1733584787' +
'&signature=zL-ucjNyREiHDE8aihFwpfR9aggP2xiAo3NSpfe-p7IbCisNlDKlo7Kb6G4D0Ao2mBrSgEk4maLSdv6MLIlADQ' +
'&hash=2174df5b000556d044f3f020384e879c8efcab55ddea2ced4eb752e93e7080d6';
const botId = 7342037359;

await validate3rd(initData, botId);
```

Function will throw an error in one of these cases:

- `ERR_AUTH_DATE_INVALID`: `auth_date` is empty or not found
- `ERR_SIGNATURE_MISSING`: `signature` is empty or not found
- `ERR_SIGN_INVALID`: signature is invalid
- `ERR_EXPIRED`: init data expired

> [!WARNING]
> This function uses **Web Crypto API** instead of Node.js modules.

### `isValid`

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

if (isValid('init-data')) {
if (isValid('init-data', 'my-bot-token')) {
console.log('Init data is fine');
}
```

### `isValid3rd`

Does the same as the `isValid` function, but checks if the init data was signed by Telegram:

```ts
import { isValid3rd } from '@telegram-apps/init-data-node';

if (await isValid3rd('init-data')) {
console.log('Init data is fine');
}
```

> [!WARNING]
> This function uses **Web Crypto API** instead of Node.js modules.

## Signing

There could be some cases when a developer needs to create their own init data. For instance,
Expand Down
5 changes: 3 additions & 2 deletions packages/bridge/src/env/mockTelegramEnv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,16 @@ const lp: LaunchParams = {
lastName: 'Rogue',
username: 'rogue',
},
signature: 'abc',
},
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',
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',
version: '7.2',
platform: 'tdesktop',
botInline: false,
showSettings: false
};

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';
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';

it('should save passed launch parameters in session storage', () => {
const setItem = mockSessionStorageSetItem();
Expand Down
85 changes: 0 additions & 85 deletions packages/bridge/src/launch-params/parseLaunchParams.test.ts

This file was deleted.

6 changes: 3 additions & 3 deletions packages/bridge/src/utils/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,18 @@ describe('options', () => {
expect(globalPostEvent).toHaveBeenCalledWith('web_app_request_phone', undefined);
});

it('should reject promise if postEvent threw an error', () => {
it('should reject promise if postEvent threw an error', async () => {
const promise = request('web_app_request_phone', 'phone_requested', {
postEvent() {
throw new Error('Nope!');
},
});
void expect(promise).rejects.toStrictEqual(new Error('Nope!'));
await expect(promise).rejects.toStrictEqual(new Error('Nope!'));
});
});

describe('capture', () => {
it('should capture an event in case, capture method returned true', () => {
it('should capture an event in case, capture method returned true', async () => {
const promise = request('web_app_request_phone', 'phone_requested', {
timeout: 1000,
capture: ({ status }) => status === 'allowed',
Expand Down
10 changes: 10 additions & 0 deletions packages/init-data-node/src/converters/arrayBufferToHex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Converts array buffer to hex.
* @param buffer - buffer to convert
*/
export function arrayBufferToHex(buffer: ArrayBuffer): string {
return new Uint8Array(buffer).reduce((acc, byte) => {
// Convert byte to hex and pad with zero if needed (e.g., "0a" instead of "a")
return acc + byte.toString(16).padStart(2, '0');
}, '');
}
17 changes: 17 additions & 0 deletions packages/init-data-node/src/converters/hexToArrayBuffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Converts a hex string to ArrayBuffer.
* @param hexString - value to convert.
*/
export function hexToArrayBuffer(hexString: string): ArrayBuffer {
if (hexString.length % 2 !== 0) {
throw new Error('Hex string must have an even number of characters');
}

const buffer = new ArrayBuffer(hexString.length / 2);
const uint8Array = new Uint8Array(buffer);
for (let i = 0; i < hexString.length; i += 2) {
uint8Array[i / 2] = parseInt(hexString.substring(i, i + 2), 16);
}

return buffer;
}
Loading