@windmc/js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/error.d.ts +5 -0
- package/dist/error.js +9 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +265 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.js +2 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WindMC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# wind-js-client
|
|
2
|
+
Официальная клиентская библиотека для работы с API Wind. Поддерживает Node.js и TypeScript.
|
|
3
|
+
|
|
4
|
+
## Установка
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @windmc/js
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Конфигурация
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { WindClient } from "@windmc/js";
|
|
14
|
+
|
|
15
|
+
const client = new WindClient({
|
|
16
|
+
clientId: "ВАШ_CLIENT_ID",
|
|
17
|
+
clientSecret: "ВАШ_CLIENT_SECRET", // для OAuth
|
|
18
|
+
redirectUri: "https://yoursite.example/callback",
|
|
19
|
+
appToken: "ВАШ_APP_TOKEN", // для Billing
|
|
20
|
+
webhookSecret: "ВАШ_WEBHOOK_SECRET", // для проверки вебхуков
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## OAuth авторизация
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// 1. Получить ссылку для входа
|
|
28
|
+
const url = client.getAuthorizeUrl(["identify", "bank"]);
|
|
29
|
+
// → https://windmc.su/oauth2/authorize?...
|
|
30
|
+
|
|
31
|
+
// 2. Обменять code на токены
|
|
32
|
+
const tokens = await client.exchangeCode(code);
|
|
33
|
+
|
|
34
|
+
// 3. Получить данные пользователя
|
|
35
|
+
const user = await client.getUser(tokens.access_token);
|
|
36
|
+
console.log(user.nickname, user.avatar);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### PKCE (для SPA и мобильных приложений)
|
|
40
|
+
|
|
41
|
+
Если ваше приложение не может безопасно хранить `clientSecret` (браузер, мобильное
|
|
42
|
+
приложение), используйте PKCE вместо секрета — `clientSecret` в конфиге при этом
|
|
43
|
+
можно не указывать:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { WindClient, generatePKCE } from "@windmc/js";
|
|
47
|
+
|
|
48
|
+
const client = new WindClient({
|
|
49
|
+
clientId: "ВАШ_CLIENT_ID",
|
|
50
|
+
redirectUri: "https://yourapp.example/callback",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 1. Сгенерировать пару и сохранить codeVerifier (например, в сессии/cookie)
|
|
54
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
55
|
+
const url = client.getAuthorizeUrl(["identify"], undefined, codeChallenge);
|
|
56
|
+
|
|
57
|
+
// 2. После редиректа обменять code на токены с помощью сохранённого codeVerifier
|
|
58
|
+
const tokens = await client.exchangeCode(code, codeVerifier);
|
|
59
|
+
|
|
60
|
+
// 3. refreshToken() и revokeToken() для PKCE-токенов работают без clientSecret
|
|
61
|
+
const refreshed = await client.refreshToken(tokens.refresh_token);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Доступные scopes
|
|
65
|
+
|
|
66
|
+
| Scope | Описание |
|
|
67
|
+
|--------------|-----------------------------------------|
|
|
68
|
+
| `identify` | Никнейм, UUID, роли |
|
|
69
|
+
| `bank` | Счета и транзакции |
|
|
70
|
+
| `bank:charge`| Списание средств (требует подтверждения)|
|
|
71
|
+
| `communities`| Список сообществ пользователя |
|
|
72
|
+
| `friends` | Список друзей |
|
|
73
|
+
|
|
74
|
+
## Методы
|
|
75
|
+
|
|
76
|
+
### OAuth / пользователь
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
client.getAuthorizeUrl(scopes, state?) // URL авторизации
|
|
80
|
+
client.exchangeCode(code) // code → tokens
|
|
81
|
+
client.refreshToken(refreshToken) // обновить токен
|
|
82
|
+
client.revokeToken(token) // отозвать токен
|
|
83
|
+
client.getUser(accessToken) // scope: identify
|
|
84
|
+
client.getBank(accessToken) // scope: bank
|
|
85
|
+
client.getBankTransactions(accessToken, limit?)// scope: bank
|
|
86
|
+
client.getCommunities(accessToken) // scope: communities
|
|
87
|
+
client.getFriends(accessToken) // scope: friends
|
|
88
|
+
client.createCharge(accessToken, payload) // scope: bank:charge
|
|
89
|
+
client.getCharge(accessToken, chargeId) // scope: bank:charge
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Billing (App Token)
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// Создать счёт — вернёт payUrl для редиректа пользователя
|
|
96
|
+
const bill = await client.createBill({
|
|
97
|
+
toAccountId: 1001,
|
|
98
|
+
amount: 500,
|
|
99
|
+
comment: "Оплата",
|
|
100
|
+
webhookUrl: "https://yoursite.example/webhooks/wind",
|
|
101
|
+
returnUrl: "https://yoursite.example/success",
|
|
102
|
+
});
|
|
103
|
+
// → { billId, payUrl, expiresAt }
|
|
104
|
+
|
|
105
|
+
// Проверить статус счёта
|
|
106
|
+
const status = await client.getBill(bill.billId);
|
|
107
|
+
// → { status: "pending" | "paid" | "expired", ... }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Вебхуки
|
|
111
|
+
|
|
112
|
+
Счёт действует 10 минут. После оплаты Wind отправляет POST-запрос на `webhookUrl` с заголовком `X-Signature: sha256=<hmac>`.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// Express: читать тело как raw buffer
|
|
116
|
+
app.use("/webhooks/wind", express.raw({ type: "application/json" }));
|
|
117
|
+
|
|
118
|
+
app.post("/webhooks/wind", (req, res) => {
|
|
119
|
+
const signature = req.headers["x-signature"];
|
|
120
|
+
|
|
121
|
+
let payload;
|
|
122
|
+
try {
|
|
123
|
+
payload = client.parseWebhook(req.body.toString(), signature);
|
|
124
|
+
} catch {
|
|
125
|
+
return res.status(401).json({ error: "Неверная подпись" });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (payload.event === "bill.paid") {
|
|
129
|
+
console.log(`Оплачен счёт ${payload.billId} на сумму ${payload.amount}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
res.status(200).json({ ok: true });
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Примеры
|
|
137
|
+
|
|
138
|
+
Готовые примеры в директории [`example/`](./example):
|
|
139
|
+
- `oauth-login` — OAuth авторизация с Express
|
|
140
|
+
- `create-bill` — Создание счёта на оплату
|
|
141
|
+
- `webhook` — Обработка вебхуков
|
|
142
|
+
|
|
143
|
+
## Сборка
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm run build
|
|
147
|
+
```
|
package/dist/error.d.ts
ADDED
package/dist/error.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Config, WindScope, AccessTokenResponse, User, BankResponse, TransactionsResponse, CommunitiesResponse, FriendsResponse, ChargeRequest, CreateChargePayload, CreateBillPayload, CreateBillResponse, BillStatusResponse, WebhookPayload } from "./types.js";
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export { WindError } from "./error.js";
|
|
4
|
+
/**
|
|
5
|
+
* Генерирует пару PKCE (code_verifier + code_challenge, метод S256).
|
|
6
|
+
* Используйте для публичных клиентов (SPA, мобильные приложения), у которых
|
|
7
|
+
* нет безопасного места для хранения clientSecret: code_verifier храните
|
|
8
|
+
* у себя (например, в сессии) до обмена кода на токен, code_challenge —
|
|
9
|
+
* передайте в {@link WindClient.getAuthorizeUrl}.
|
|
10
|
+
*/
|
|
11
|
+
export declare function generatePKCE(): {
|
|
12
|
+
codeVerifier: string;
|
|
13
|
+
codeChallenge: string;
|
|
14
|
+
};
|
|
15
|
+
export declare class WindClient {
|
|
16
|
+
private readonly config;
|
|
17
|
+
constructor(config: Config);
|
|
18
|
+
/**
|
|
19
|
+
* Возвращает URL для авторизации пользователя.
|
|
20
|
+
* @param scopes - массив запрашиваемых scopes
|
|
21
|
+
* @param state - произвольная строка для защиты от CSRF
|
|
22
|
+
* @param codeChallenge - PKCE code_challenge (см. {@link generatePKCE}); нужен для публичных
|
|
23
|
+
* клиентов (SPA, мобильные приложения), у которых нет clientSecret
|
|
24
|
+
*/
|
|
25
|
+
getAuthorizeUrl(scopes: WindScope[], state?: string, codeChallenge?: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Обменивает authorization code на токены.
|
|
28
|
+
* @param code - код из redirect_uri
|
|
29
|
+
* @param codeVerifier - PKCE code_verifier, если авторизация запускалась с codeChallenge
|
|
30
|
+
*/
|
|
31
|
+
exchangeCode(code: string, codeVerifier?: string): Promise<AccessTokenResponse>;
|
|
32
|
+
/**
|
|
33
|
+
* Обновляет access token с помощью refresh token.
|
|
34
|
+
* Для PKCE-токенов (публичный клиент без clientSecret) секрет не требуется.
|
|
35
|
+
*/
|
|
36
|
+
refreshToken(refreshToken: string): Promise<AccessTokenResponse>;
|
|
37
|
+
/**
|
|
38
|
+
* Отзывает токен.
|
|
39
|
+
* Для PKCE-токенов (публичный клиент без clientSecret) секрет не требуется.
|
|
40
|
+
*/
|
|
41
|
+
revokeToken(token: string): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Возвращает информацию о текущем пользователе.
|
|
44
|
+
* Требует scope: identify
|
|
45
|
+
*/
|
|
46
|
+
getUser(accessToken: string): Promise<User>;
|
|
47
|
+
/**
|
|
48
|
+
* Возвращает банковские счета пользователя.
|
|
49
|
+
* Требует scope: bank
|
|
50
|
+
*/
|
|
51
|
+
getBank(accessToken: string): Promise<BankResponse>;
|
|
52
|
+
/**
|
|
53
|
+
* Возвращает транзакции пользователя.
|
|
54
|
+
* Требует scope: bank
|
|
55
|
+
*/
|
|
56
|
+
getBankTransactions(accessToken: string, limit?: number): Promise<TransactionsResponse>;
|
|
57
|
+
/**
|
|
58
|
+
* Возвращает сообщества пользователя.
|
|
59
|
+
* Требует scope: communities
|
|
60
|
+
*/
|
|
61
|
+
getCommunities(accessToken: string): Promise<CommunitiesResponse>;
|
|
62
|
+
/**
|
|
63
|
+
* Возвращает список друзей пользователя.
|
|
64
|
+
* Требует scope: friends
|
|
65
|
+
*/
|
|
66
|
+
getFriends(accessToken: string): Promise<FriendsResponse>;
|
|
67
|
+
/**
|
|
68
|
+
* Создаёт запрос на списание средств.
|
|
69
|
+
* Требует scope: bank:charge
|
|
70
|
+
*/
|
|
71
|
+
createCharge(accessToken: string, payload: CreateChargePayload): Promise<ChargeRequest>;
|
|
72
|
+
/**
|
|
73
|
+
* Возвращает статус запроса на списание.
|
|
74
|
+
* Требует scope: bank:charge
|
|
75
|
+
*/
|
|
76
|
+
getCharge(accessToken: string, chargeId: string): Promise<ChargeRequest>;
|
|
77
|
+
/**
|
|
78
|
+
* Создаёт счёт на оплату. Использует App Token (X-Client-Id + Bearer).
|
|
79
|
+
* После создания пользователь перейдёт по payUrl для оплаты.
|
|
80
|
+
*/
|
|
81
|
+
createBill(payload: CreateBillPayload): Promise<CreateBillResponse>;
|
|
82
|
+
/**
|
|
83
|
+
* Возвращает информацию о счёте по его ID.
|
|
84
|
+
*/
|
|
85
|
+
getBill(billId: string): Promise<BillStatusResponse>;
|
|
86
|
+
/**
|
|
87
|
+
* Проверяет подпись входящего вебхука.
|
|
88
|
+
* @param rawBody - тело запроса как строка (до JSON.parse)
|
|
89
|
+
* @param signature - значение заголовка X-Signature
|
|
90
|
+
*/
|
|
91
|
+
verifyWebhookSignature(rawBody: string, signature: string): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Парсит тело вебхука с проверкой подписи.
|
|
94
|
+
* Бросает WindError если подпись неверна.
|
|
95
|
+
*/
|
|
96
|
+
parseWebhook(rawBody: string, signature: string): WebhookPayload;
|
|
97
|
+
private request;
|
|
98
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// WindClient © Винд 2026
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { WindError } from "./error.js";
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export { WindError } from "./error.js";
|
|
6
|
+
const DEFAULT_API_URL = "https://api.windmc.su";
|
|
7
|
+
const DEFAULT_SITE_URL = "https://windmc.su";
|
|
8
|
+
/**
|
|
9
|
+
* Генерирует пару PKCE (code_verifier + code_challenge, метод S256).
|
|
10
|
+
* Используйте для публичных клиентов (SPA, мобильные приложения), у которых
|
|
11
|
+
* нет безопасного места для хранения clientSecret: code_verifier храните
|
|
12
|
+
* у себя (например, в сессии) до обмена кода на токен, code_challenge —
|
|
13
|
+
* передайте в {@link WindClient.getAuthorizeUrl}.
|
|
14
|
+
*/
|
|
15
|
+
export function generatePKCE() {
|
|
16
|
+
const codeVerifier = crypto.randomBytes(32).toString("base64url");
|
|
17
|
+
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
18
|
+
return { codeVerifier, codeChallenge };
|
|
19
|
+
}
|
|
20
|
+
export class WindClient {
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.config = {
|
|
23
|
+
clientId: config.clientId,
|
|
24
|
+
clientSecret: config.clientSecret ?? "",
|
|
25
|
+
redirectUri: config.redirectUri ?? "",
|
|
26
|
+
appToken: config.appToken ?? "",
|
|
27
|
+
webhookSecret: config.webhookSecret ?? "",
|
|
28
|
+
apiUrl: config.apiUrl ?? DEFAULT_API_URL,
|
|
29
|
+
siteUrl: config.siteUrl ?? DEFAULT_SITE_URL,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// OAuth
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
/**
|
|
36
|
+
* Возвращает URL для авторизации пользователя.
|
|
37
|
+
* @param scopes - массив запрашиваемых scopes
|
|
38
|
+
* @param state - произвольная строка для защиты от CSRF
|
|
39
|
+
* @param codeChallenge - PKCE code_challenge (см. {@link generatePKCE}); нужен для публичных
|
|
40
|
+
* клиентов (SPA, мобильные приложения), у которых нет clientSecret
|
|
41
|
+
*/
|
|
42
|
+
getAuthorizeUrl(scopes, state, codeChallenge) {
|
|
43
|
+
const params = new URLSearchParams({
|
|
44
|
+
client_id: this.config.clientId,
|
|
45
|
+
redirect_uri: this.config.redirectUri,
|
|
46
|
+
response_type: "code",
|
|
47
|
+
scope: scopes.join(" "),
|
|
48
|
+
});
|
|
49
|
+
if (state)
|
|
50
|
+
params.set("state", state);
|
|
51
|
+
if (codeChallenge) {
|
|
52
|
+
params.set("code_challenge", codeChallenge);
|
|
53
|
+
params.set("code_challenge_method", "S256");
|
|
54
|
+
}
|
|
55
|
+
return `${this.config.siteUrl}/oauth2/authorize?${params.toString()}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Обменивает authorization code на токены.
|
|
59
|
+
* @param code - код из redirect_uri
|
|
60
|
+
* @param codeVerifier - PKCE code_verifier, если авторизация запускалась с codeChallenge
|
|
61
|
+
*/
|
|
62
|
+
async exchangeCode(code, codeVerifier) {
|
|
63
|
+
const params = {
|
|
64
|
+
grant_type: "authorization_code",
|
|
65
|
+
code,
|
|
66
|
+
client_id: this.config.clientId,
|
|
67
|
+
redirect_uri: this.config.redirectUri,
|
|
68
|
+
};
|
|
69
|
+
if (this.config.clientSecret)
|
|
70
|
+
params.client_secret = this.config.clientSecret;
|
|
71
|
+
if (codeVerifier)
|
|
72
|
+
params.code_verifier = codeVerifier;
|
|
73
|
+
return this.request("/api/oauth2/token", {
|
|
74
|
+
method: "POST",
|
|
75
|
+
body: new URLSearchParams(params),
|
|
76
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Обновляет access token с помощью refresh token.
|
|
81
|
+
* Для PKCE-токенов (публичный клиент без clientSecret) секрет не требуется.
|
|
82
|
+
*/
|
|
83
|
+
async refreshToken(refreshToken) {
|
|
84
|
+
const params = {
|
|
85
|
+
grant_type: "refresh_token",
|
|
86
|
+
refresh_token: refreshToken,
|
|
87
|
+
client_id: this.config.clientId,
|
|
88
|
+
};
|
|
89
|
+
if (this.config.clientSecret)
|
|
90
|
+
params.client_secret = this.config.clientSecret;
|
|
91
|
+
return this.request("/api/oauth2/token", {
|
|
92
|
+
method: "POST",
|
|
93
|
+
body: new URLSearchParams(params),
|
|
94
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Отзывает токен.
|
|
99
|
+
* Для PKCE-токенов (публичный клиент без clientSecret) секрет не требуется.
|
|
100
|
+
*/
|
|
101
|
+
async revokeToken(token) {
|
|
102
|
+
const params = {
|
|
103
|
+
token,
|
|
104
|
+
client_id: this.config.clientId,
|
|
105
|
+
};
|
|
106
|
+
if (this.config.clientSecret)
|
|
107
|
+
params.client_secret = this.config.clientSecret;
|
|
108
|
+
await this.request("/api/oauth2/revoke", {
|
|
109
|
+
method: "POST",
|
|
110
|
+
body: new URLSearchParams(params),
|
|
111
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// @me (OAuth user endpoints)
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
/**
|
|
118
|
+
* Возвращает информацию о текущем пользователе.
|
|
119
|
+
* Требует scope: identify
|
|
120
|
+
*/
|
|
121
|
+
async getUser(accessToken) {
|
|
122
|
+
return this.request("/api/oauth2/@me", {
|
|
123
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Возвращает банковские счета пользователя.
|
|
128
|
+
* Требует scope: bank
|
|
129
|
+
*/
|
|
130
|
+
async getBank(accessToken) {
|
|
131
|
+
return this.request("/api/oauth2/@me/bank", {
|
|
132
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Возвращает транзакции пользователя.
|
|
137
|
+
* Требует scope: bank
|
|
138
|
+
*/
|
|
139
|
+
async getBankTransactions(accessToken, limit) {
|
|
140
|
+
const query = limit ? `?limit=${limit}` : "";
|
|
141
|
+
return this.request(`/api/oauth2/@me/bank/transactions${query}`, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Возвращает сообщества пользователя.
|
|
145
|
+
* Требует scope: communities
|
|
146
|
+
*/
|
|
147
|
+
async getCommunities(accessToken) {
|
|
148
|
+
return this.request("/api/oauth2/@me/communities", {
|
|
149
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Возвращает список друзей пользователя.
|
|
154
|
+
* Требует scope: friends
|
|
155
|
+
*/
|
|
156
|
+
async getFriends(accessToken) {
|
|
157
|
+
return this.request("/api/oauth2/@me/friends", {
|
|
158
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Создаёт запрос на списание средств.
|
|
163
|
+
* Требует scope: bank:charge
|
|
164
|
+
*/
|
|
165
|
+
async createCharge(accessToken, payload) {
|
|
166
|
+
return this.request("/api/oauth2/@me/bank/charge", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: {
|
|
169
|
+
Authorization: `Bearer ${accessToken}`,
|
|
170
|
+
"Content-Type": "application/json",
|
|
171
|
+
},
|
|
172
|
+
body: JSON.stringify(payload),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Возвращает статус запроса на списание.
|
|
177
|
+
* Требует scope: bank:charge
|
|
178
|
+
*/
|
|
179
|
+
async getCharge(accessToken, chargeId) {
|
|
180
|
+
return this.request(`/api/oauth2/@me/bank/charge/${chargeId}`, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
181
|
+
}
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Billing (app token auth)
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
/**
|
|
186
|
+
* Создаёт счёт на оплату. Использует App Token (X-Client-Id + Bearer).
|
|
187
|
+
* После создания пользователь перейдёт по payUrl для оплаты.
|
|
188
|
+
*/
|
|
189
|
+
async createBill(payload) {
|
|
190
|
+
if (!this.config.appToken) {
|
|
191
|
+
throw new WindError("appToken не указан в конфигурации");
|
|
192
|
+
}
|
|
193
|
+
return this.request("/api/oauth2/billing/create", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: {
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
"X-Client-Id": this.config.clientId,
|
|
198
|
+
Authorization: `Bearer ${this.config.appToken}`,
|
|
199
|
+
},
|
|
200
|
+
body: JSON.stringify(payload),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Возвращает информацию о счёте по его ID.
|
|
205
|
+
*/
|
|
206
|
+
async getBill(billId) {
|
|
207
|
+
return this.request(`/api/oauth2/billing/${billId}`);
|
|
208
|
+
}
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Webhooks
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
/**
|
|
213
|
+
* Проверяет подпись входящего вебхука.
|
|
214
|
+
* @param rawBody - тело запроса как строка (до JSON.parse)
|
|
215
|
+
* @param signature - значение заголовка X-Signature
|
|
216
|
+
*/
|
|
217
|
+
verifyWebhookSignature(rawBody, signature) {
|
|
218
|
+
if (!this.config.webhookSecret) {
|
|
219
|
+
throw new WindError("webhookSecret не указан в конфигурации");
|
|
220
|
+
}
|
|
221
|
+
const expected = "sha256=" +
|
|
222
|
+
crypto
|
|
223
|
+
.createHmac("sha256", this.config.webhookSecret)
|
|
224
|
+
.update(rawBody)
|
|
225
|
+
.digest("hex");
|
|
226
|
+
if (expected.length !== signature.length)
|
|
227
|
+
return false;
|
|
228
|
+
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Парсит тело вебхука с проверкой подписи.
|
|
232
|
+
* Бросает WindError если подпись неверна.
|
|
233
|
+
*/
|
|
234
|
+
parseWebhook(rawBody, signature) {
|
|
235
|
+
if (!this.verifyWebhookSignature(rawBody, signature)) {
|
|
236
|
+
throw new WindError("Неверная подпись вебхука", 401);
|
|
237
|
+
}
|
|
238
|
+
return JSON.parse(rawBody);
|
|
239
|
+
}
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Internal
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
async request(path, init = {}) {
|
|
244
|
+
const url = `${this.config.apiUrl}${path}`;
|
|
245
|
+
const res = await fetch(url, init);
|
|
246
|
+
let data;
|
|
247
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
248
|
+
if (contentType.includes("application/json")) {
|
|
249
|
+
data = await res.json();
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
data = await res.text();
|
|
253
|
+
}
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
const message = typeof data === "object" &&
|
|
256
|
+
data !== null &&
|
|
257
|
+
"error" in data &&
|
|
258
|
+
typeof data.error === "string"
|
|
259
|
+
? data.error
|
|
260
|
+
: `HTTP ${res.status}`;
|
|
261
|
+
throw new WindError(message, res.status, typeof data === "object" ? data : undefined);
|
|
262
|
+
}
|
|
263
|
+
return data;
|
|
264
|
+
}
|
|
265
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret?: string;
|
|
4
|
+
redirectUri?: string;
|
|
5
|
+
appToken?: string;
|
|
6
|
+
webhookSecret?: string;
|
|
7
|
+
apiUrl?: string;
|
|
8
|
+
siteUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface AccessTokenResponse {
|
|
11
|
+
access_token: string;
|
|
12
|
+
refresh_token: string;
|
|
13
|
+
token_type: "Bearer";
|
|
14
|
+
expires_in: number;
|
|
15
|
+
scope: string;
|
|
16
|
+
}
|
|
17
|
+
export type WindScope = "identify" | "bank" | "bank:charge" | "communities" | "friends";
|
|
18
|
+
export interface User {
|
|
19
|
+
discordId: string;
|
|
20
|
+
nickname: string | null;
|
|
21
|
+
uuid: string | null;
|
|
22
|
+
discordUsername: string | null;
|
|
23
|
+
roles: string[];
|
|
24
|
+
online: boolean;
|
|
25
|
+
avatar: string | null;
|
|
26
|
+
}
|
|
27
|
+
export interface BankAccount {
|
|
28
|
+
id: number;
|
|
29
|
+
name: string;
|
|
30
|
+
balance: number;
|
|
31
|
+
frozen: boolean;
|
|
32
|
+
design: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface BankResponse {
|
|
35
|
+
accounts: BankAccount[];
|
|
36
|
+
totalBalance: number;
|
|
37
|
+
}
|
|
38
|
+
export interface Transaction {
|
|
39
|
+
id: string | number;
|
|
40
|
+
fromAccountId: number;
|
|
41
|
+
toAccountId: number;
|
|
42
|
+
amount: number;
|
|
43
|
+
type: string;
|
|
44
|
+
comment: string | null;
|
|
45
|
+
timestamp: string;
|
|
46
|
+
}
|
|
47
|
+
export interface TransactionsResponse {
|
|
48
|
+
transactions: Transaction[];
|
|
49
|
+
count: number;
|
|
50
|
+
}
|
|
51
|
+
export interface Community {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
slug: string;
|
|
55
|
+
banner: string | null;
|
|
56
|
+
description: string | null;
|
|
57
|
+
memberCount: number;
|
|
58
|
+
verified: boolean;
|
|
59
|
+
role: "owner" | "co_owner" | "member";
|
|
60
|
+
}
|
|
61
|
+
export interface CommunitiesResponse {
|
|
62
|
+
communities: Community[];
|
|
63
|
+
}
|
|
64
|
+
export interface FriendUser {
|
|
65
|
+
id: string;
|
|
66
|
+
nickname: string | null;
|
|
67
|
+
uuid: string | null;
|
|
68
|
+
discordUsername: string | null;
|
|
69
|
+
}
|
|
70
|
+
export interface Friend {
|
|
71
|
+
friendshipId: string;
|
|
72
|
+
user: FriendUser;
|
|
73
|
+
since: string;
|
|
74
|
+
}
|
|
75
|
+
export interface FriendsResponse {
|
|
76
|
+
friends: Friend[];
|
|
77
|
+
}
|
|
78
|
+
export interface ChargeRequest {
|
|
79
|
+
chargeId: string;
|
|
80
|
+
status: "pending" | "paid" | "expired";
|
|
81
|
+
amount?: number;
|
|
82
|
+
fromAccountId?: number;
|
|
83
|
+
toAccountId?: number;
|
|
84
|
+
}
|
|
85
|
+
export interface CreateChargePayload {
|
|
86
|
+
amount: number;
|
|
87
|
+
toAccountId: number;
|
|
88
|
+
fromAccountId?: number;
|
|
89
|
+
comment?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface CreateBillPayload {
|
|
92
|
+
toAccountId: number;
|
|
93
|
+
amount: number;
|
|
94
|
+
comment?: string;
|
|
95
|
+
webhookUrl?: string;
|
|
96
|
+
returnUrl?: string;
|
|
97
|
+
}
|
|
98
|
+
export interface CreateBillResponse {
|
|
99
|
+
billId: string;
|
|
100
|
+
payUrl: string;
|
|
101
|
+
expiresAt: string;
|
|
102
|
+
}
|
|
103
|
+
export interface BillApp {
|
|
104
|
+
name: string;
|
|
105
|
+
iconUrl: string | null;
|
|
106
|
+
}
|
|
107
|
+
export interface BillStatusResponse {
|
|
108
|
+
billId: string;
|
|
109
|
+
status: "pending" | "paid" | "expired";
|
|
110
|
+
amount: number;
|
|
111
|
+
comment: string | null;
|
|
112
|
+
toAccountId: number;
|
|
113
|
+
returnUrl: string | null;
|
|
114
|
+
expiresAt: string;
|
|
115
|
+
app: BillApp;
|
|
116
|
+
}
|
|
117
|
+
export interface WebhookPayload {
|
|
118
|
+
event: "bill.paid";
|
|
119
|
+
billId: string;
|
|
120
|
+
amount: number;
|
|
121
|
+
fromAccountId: number;
|
|
122
|
+
toAccountId: number;
|
|
123
|
+
payerId: string;
|
|
124
|
+
timestamp: number;
|
|
125
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@windmc/js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Wind API client",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/express": "^5.0.6",
|
|
23
|
+
"@types/node": "^25.6.0",
|
|
24
|
+
"express": "^5.2.1",
|
|
25
|
+
"typescript": "^5.9.3"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT"
|
|
31
|
+
}
|