-
Notifications
You must be signed in to change notification settings - Fork 232
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #304 from Telegram-Mini-Apps/feature/init-data-nod…
…e-create Add init data sign utilities
- Loading branch information
Showing
11 changed files
with
329 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@tma.js/init-data-node": minor | ||
--- | ||
|
||
Add sign utilities. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
packages/init-data-node/src/initDataToSearchParams.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { it, expect } from 'vitest'; | ||
import { initDataToSearchParams } from './initDataToSearchParams'; | ||
|
||
it('should correctly parse any set of parameters', () => { | ||
expect(initDataToSearchParams({}).toString()).toBe(''); | ||
expect(initDataToSearchParams({ hash: 'HASH' }).toString()).toBe('hash=HASH'); | ||
expect(initDataToSearchParams({ authDate: new Date(1000), hash: 'HASH' }).toString()).toBe('auth_date=1&hash=HASH'); | ||
expect( | ||
initDataToSearchParams({ | ||
authDate: new Date(1000), | ||
canSendAfter: 10000, | ||
chat: { | ||
id: 1, | ||
type: 'group', | ||
username: 'my-chat', | ||
title: 'chat-title', | ||
photoUrl: 'chat-photo', | ||
}, | ||
chatInstance: '888', | ||
chatType: 'sender', | ||
hash: '47cfa22e72b887cba90c9cb833c5ea0f599975b6ce7193741844b5c4a4228b40', | ||
queryId: 'QUERY', | ||
receiver: { | ||
addedToAttachmentMenu: false, | ||
allowsWriteToPm: true, | ||
firstName: 'receiver-first-name', | ||
id: 991, | ||
isBot: false, | ||
isPremium: true, | ||
languageCode: 'ru', | ||
lastName: 'receiver-last-name', | ||
photoUrl: 'receiver-photo', | ||
username: 'receiver-username', | ||
}, | ||
startParam: 'debug', | ||
user: { | ||
addedToAttachmentMenu: false, | ||
allowsWriteToPm: false, | ||
firstName: 'user-first-name', | ||
id: 222, | ||
isBot: true, | ||
isPremium: false, | ||
languageCode: 'en', | ||
lastName: 'user-last-name', | ||
photoUrl: 'user-photo', | ||
username: 'user-username', | ||
}, | ||
}).toString(), | ||
).toBe( | ||
'auth_date=1&can_send_after=10000&chat=%7B%22id%22%3A1%2C%22type%22%3A%22group%22%2C%22title%22%3A%22chat-title%22%2C%22photo_url%22%3A%22group%22%2C%22username%22%3A%22my-chat%22%7D&chat_instance=888&chat_type=sender&hash=47cfa22e72b887cba90c9cb833c5ea0f599975b6ce7193741844b5c4a4228b40&query_id=QUERY&receiver=%7B%22added_to_attachment_menu%22%3Afalse%2C%22allows_write_to_pm%22%3Atrue%2C%22first_name%22%3A%22receiver-first-name%22%2C%22id%22%3A991%2C%22is_bot%22%3Afalse%2C%22is_premium%22%3Atrue%2C%22language_code%22%3A%22ru%22%2C%22last_name%22%3A%22receiver-last-name%22%2C%22photo_url%22%3A%22receiver-photo%22%2C%22username%22%3A%22receiver-username%22%7D&start_param=debug&user=%7B%22added_to_attachment_menu%22%3Afalse%2C%22allows_write_to_pm%22%3Afalse%2C%22first_name%22%3A%22user-first-name%22%2C%22id%22%3A222%2C%22is_bot%22%3Atrue%2C%22is_premium%22%3Afalse%2C%22language_code%22%3A%22en%22%2C%22last_name%22%3A%22user-last-name%22%2C%22photo_url%22%3A%22user-photo%22%2C%22username%22%3A%22user-username%22%7D', | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import type { InitDataParsed, User } from '@tma.js/sdk'; | ||
|
||
import { URLSearchParams } from 'node:url'; | ||
|
||
/** | ||
* Removes undefined properties from the object. | ||
* @param object - object to remove properties from. | ||
*/ | ||
function removeUndefined(object: Record<string, string | undefined>): Record<string, string> { | ||
const result: Record<string, string> = {}; | ||
for (const key in object) { | ||
const v = object[key]; | ||
if (v !== undefined) { | ||
result[key] = v; | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
/** | ||
* Serializes a user information. | ||
* @param user - user information. | ||
*/ | ||
function serializeUser(user: User | undefined): string | undefined { | ||
return user | ||
? JSON.stringify({ | ||
added_to_attachment_menu: user.addedToAttachmentMenu, | ||
allows_write_to_pm: user.allowsWriteToPm, | ||
first_name: user.firstName, | ||
id: user.id, | ||
is_bot: user.isBot, | ||
is_premium: user.isPremium, | ||
language_code: user.languageCode, | ||
last_name: user.lastName, | ||
photo_url: user.photoUrl, | ||
username: user.username, | ||
}) | ||
: undefined; | ||
} | ||
|
||
export function initDataToSearchParams({ | ||
chat, | ||
receiver, | ||
user, | ||
...data | ||
}: Partial<InitDataParsed>): URLSearchParams { | ||
return new URLSearchParams( | ||
removeUndefined({ | ||
auth_date: data.authDate | ||
? ((+data.authDate / 1000) | 0).toString() | ||
: undefined, | ||
can_send_after: data.canSendAfter?.toString(), | ||
chat: chat | ||
? JSON.stringify({ | ||
id: chat.id, | ||
type: chat.type, | ||
title: chat.title, | ||
photo_url: chat.type, | ||
username: chat.username, | ||
}) | ||
: undefined, | ||
chat_instance: data.chatInstance, | ||
chat_type: data.chatType || undefined, | ||
hash: data.hash, | ||
query_id: data.queryId, | ||
receiver: serializeUser(receiver), | ||
start_param: data.startParam || undefined, | ||
user: serializeUser(user), | ||
}), | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { expect, it } from 'vitest'; | ||
|
||
import { sign } from './sign'; | ||
|
||
it('should correctly sign data', () => { | ||
expect( | ||
sign( | ||
{ | ||
canSendAfter: 10000, | ||
chat: { | ||
id: 1, | ||
type: 'group', | ||
username: 'my-chat', | ||
title: 'chat-title', | ||
photoUrl: 'chat-photo', | ||
}, | ||
chatInstance: '888', | ||
chatType: 'sender', | ||
queryId: 'QUERY', | ||
receiver: { | ||
addedToAttachmentMenu: false, | ||
allowsWriteToPm: true, | ||
firstName: 'receiver-first-name', | ||
id: 991, | ||
isBot: false, | ||
isPremium: true, | ||
languageCode: 'ru', | ||
lastName: 'receiver-last-name', | ||
photoUrl: 'receiver-photo', | ||
username: 'receiver-username', | ||
}, | ||
startParam: 'debug', | ||
user: { | ||
addedToAttachmentMenu: false, | ||
allowsWriteToPm: false, | ||
firstName: 'user-first-name', | ||
id: 222, | ||
isBot: true, | ||
isPremium: false, | ||
languageCode: 'en', | ||
lastName: 'user-last-name', | ||
photoUrl: 'user-photo', | ||
username: 'user-username', | ||
}, | ||
}, | ||
'5768337691:AAH5YkoiEuPk8-FZa32hStHTqXiLPtAEhx8', | ||
new Date(1000), | ||
), | ||
).toBe('auth_date=1&can_send_after=10000&chat=%7B%22id%22%3A1%2C%22type%22%3A%22group%22%2C%22title%22%3A%22chat-title%22%2C%22photo_url%22%3A%22group%22%2C%22username%22%3A%22my-chat%22%7D&chat_instance=888&chat_type=sender&query_id=QUERY&receiver=%7B%22added_to_attachment_menu%22%3Afalse%2C%22allows_write_to_pm%22%3Atrue%2C%22first_name%22%3A%22receiver-first-name%22%2C%22id%22%3A991%2C%22is_bot%22%3Afalse%2C%22is_premium%22%3Atrue%2C%22language_code%22%3A%22ru%22%2C%22last_name%22%3A%22receiver-last-name%22%2C%22photo_url%22%3A%22receiver-photo%22%2C%22username%22%3A%22receiver-username%22%7D&start_param=debug&user=%7B%22added_to_attachment_menu%22%3Afalse%2C%22allows_write_to_pm%22%3Afalse%2C%22first_name%22%3A%22user-first-name%22%2C%22id%22%3A222%2C%22is_bot%22%3Atrue%2C%22is_premium%22%3Afalse%2C%22language_code%22%3A%22en%22%2C%22last_name%22%3A%22user-last-name%22%2C%22photo_url%22%3A%22user-photo%22%2C%22username%22%3A%22user-username%22%7D&hash=47cfa22e72b887cba90c9cb833c5ea0f599975b6ce7193741844b5c4a4228b40'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { SignData } from './types.js'; | ||
import { signData } from './signData.js'; | ||
import { initDataToSearchParams } from './initDataToSearchParams.js'; | ||
|
||
/** | ||
* Signs specified init data. | ||
* @param data - init data to sign. | ||
* @param authDate - date, when this init data should be signed. | ||
* @param token - bot token. | ||
* @returns Signed init data presented as query parameters. | ||
*/ | ||
export function sign(data: SignData, token: string, authDate: Date): string { | ||
// Create search parameters, which will be signed further. | ||
const sp = initDataToSearchParams({ | ||
...data, | ||
authDate, | ||
}); | ||
|
||
// Convert search params to pairs and sort final array. | ||
const pairs = [...sp.entries()] | ||
.map(([name, value]) => `${name}=${value}`) | ||
.sort(); | ||
|
||
// Compute sign, append it to the params and return. | ||
sp.append('hash', signData(pairs.join('\n'), token)); | ||
|
||
return sp.toString(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { expect, it } from 'vitest'; | ||
import { signData } from './signData'; | ||
|
||
it( | ||
'should use HMAC-SHA256 algorithm with key, based on HMAC-SHA256 keyed with the "WebAppData" value, applied to the secret token', | ||
() => { | ||
expect(signData('abc', 'my-secret-token')) | ||
.toBe('6ecc2e9b51f30dde6877ce374ede54eb626c84e78a5d9a9dcac54d2d248f6bde'); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { createHmac } from 'node:crypto'; | ||
|
||
/** | ||
* Signs specified data with the passed token. | ||
* @param data - data to sign. | ||
* @param token - bot token. | ||
* @returns Data sign. | ||
*/ | ||
export function signData(data: string, token: string): string { | ||
return createHmac( | ||
'sha256', | ||
createHmac('sha256', 'WebAppData').update(token).digest(), | ||
) | ||
.update(data) | ||
.digest() | ||
.toString('hex'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { InitDataParsed } from '@tma.js/sdk'; | ||
|
||
export interface SignData extends Omit<InitDataParsed, 'authDate' | 'hash'> { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.