@torrow/sdk-auth 0.0.0-stage → 0.6.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/README.md +319 -2
- package/dist/index.d.ts +350 -0
- package/dist/index.js +1077 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +28 -0
- package/dist/node.js +52 -0
- package/dist/node.js.map +1 -0
- package/dist/types-LCuPy3dY.d.ts +128 -0
- package/package.json +50 -4
package/README.md
CHANGED
|
@@ -1,3 +1,320 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@torrow/sdk-auth`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Сессия токенов поверх **`@torrow/sdk-client`**: storage, проактивный refresh, single-flight, **`refreshOnUnauthorized`** (default **true**), фасад **`TorrowAuth`**.
|
|
4
|
+
|
|
5
|
+
В snapshot файла/storage: access / refresh / срок access / **`deviceId`**. Тип устройства (`deviceType`) задаёт приложение — в snapshot не пишется.
|
|
6
|
+
|
|
7
|
+
IMAP / stdin / captcha-UI / чтение `process.env` — **не** в пакете (слой app / **`examples/node/_shared`**).
|
|
8
|
+
|
|
9
|
+
## Выбор режима
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
flowchart TD
|
|
13
|
+
need[Нужен клиент] --> kind{Сценарий}
|
|
14
|
+
kind -->|restore| connect["TorrowAuth.connect"]
|
|
15
|
+
kind -->|OTP login| login["TorrowAuth.loginWithOtp"]
|
|
16
|
+
kind -->|Telegram deep-link| link["TorrowAuth.loginWithLink"]
|
|
17
|
+
kind -->|токены от хоста| fromTok["TorrowAuth.fromTokens"]
|
|
18
|
+
kind -->|без входа| anon["TorrowAuth.anonymous"]
|
|
19
|
+
connect --> handle["TorrowAuthResult: client + session"]
|
|
20
|
+
login --> handle
|
|
21
|
+
link --> handle
|
|
22
|
+
fromTok --> handle
|
|
23
|
+
anon --> clientOnly["TorrowClient"]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Флоу / подсистема для агентов: **`docs/agents/auth-flow.md`**. Индекс примеров Node: **`examples/node/auth/README.md`**, **`examples/node/_shared/README.md`**.
|
|
27
|
+
|
|
28
|
+
## Целевой клиентский код
|
|
29
|
+
|
|
30
|
+
Общая база `{ tokenStorage, deviceInfo }` (+ опционально `appCredentials`) у режимов с сессией.
|
|
31
|
+
Без `appCredentials` — **`DEFAULT_APP_CREDENTIALS`** (`sdk.torrow.net`). Своя пара перекрывает дефолт.
|
|
32
|
+
|
|
33
|
+
### 1. Restore + OTP при необходимости
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { TorrowAuth } from "@torrow/sdk-auth";
|
|
37
|
+
import { FileTokenStorage } from "@torrow/sdk-auth/node";
|
|
38
|
+
import { DeviceTypeValues } from "@torrow/sdk-client";
|
|
39
|
+
|
|
40
|
+
const base = {
|
|
41
|
+
// appCredentials опционален — иначе DEFAULT_APP_CREDENTIALS
|
|
42
|
+
tokenStorage: new FileTokenStorage("./.torrow-session.json"),
|
|
43
|
+
deviceInfo: { deviceId: "pos-terminal-1", deviceType: DeviceTypeValues.Desktop },
|
|
44
|
+
userAgent: "my-app/1.0",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let { client, session } = await TorrowAuth.connect(base);
|
|
48
|
+
|
|
49
|
+
if (session.status !== "authenticated") {
|
|
50
|
+
({ client, session } = await TorrowAuth.loginWithOtp({
|
|
51
|
+
...base,
|
|
52
|
+
account: { accountType: "Phone", accountName: "+79001234567" },
|
|
53
|
+
onSendCode: (d) => showHint(d.usedMethod, d.codeLength),
|
|
54
|
+
getCode: (ctx) => promptUser(ctx.codeLength),
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await client.profiles.list();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Чтобы задать свои креды приложения:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
appCredentials: { clientId: CLIENT_ID, clientSecret: CLIENT_SECRET },
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Mobile ID / смена канала / rate-limit
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { OtpCodeRequestLimitExceededError } from "@torrow/sdk-auth";
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await TorrowAuth.loginWithOtp({
|
|
74
|
+
...base,
|
|
75
|
+
account: { accountType: "Phone", accountName: phone },
|
|
76
|
+
supportedMethods: ["MtsMobileId", "Sms"],
|
|
77
|
+
preferredMethod: userPickedMethod,
|
|
78
|
+
onSendCode: (d) => {
|
|
79
|
+
// MtsMobileId → UI «подтвердите push»; getCode не вызовется на happy-path
|
|
80
|
+
renderLoginUi(d);
|
|
81
|
+
},
|
|
82
|
+
getCode: (ctx) => waitCodeInput(ctx),
|
|
83
|
+
});
|
|
84
|
+
} catch (e) {
|
|
85
|
+
if (e instanceof OtpCodeRequestLimitExceededError) {
|
|
86
|
+
showRetryTimer(e.minutes, e.seconds);
|
|
87
|
+
} else {
|
|
88
|
+
throw e;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3. Telegram deep-link
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const { client, session } = await TorrowAuth.loginWithLink({
|
|
97
|
+
...base,
|
|
98
|
+
onLink: (authUrl) => showQrOrOpen(authUrl),
|
|
99
|
+
signal: abortController.signal,
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 4. Виджет: токены от хоста
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { TorrowAuth, LocalStorageTokenStorage } from "@torrow/sdk-auth";
|
|
107
|
+
|
|
108
|
+
const { client, session } = await TorrowAuth.fromTokens({
|
|
109
|
+
accessToken: hostProvidedAccess,
|
|
110
|
+
refreshToken: hostProvidedRefresh,
|
|
111
|
+
appCredentials: hostProvidedAppCreds,
|
|
112
|
+
tokenStorage: new LocalStorageTokenStorage("torrow.widget.session"),
|
|
113
|
+
deviceInfo: { deviceId: widgetDeviceId(), deviceType: DeviceTypeValues.Web },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
session.onStatusChange((s) => {
|
|
117
|
+
if (s !== "authenticated") widgetUi.showLoginPrompt();
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 5. Аноним и выход
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const publicClient = TorrowAuth.anonymous({ userAgent: "my-widget" });
|
|
125
|
+
|
|
126
|
+
await session.logout(); // revoke refresh + clear storage + onStatusChange("unauthenticated")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Сигналы UI
|
|
130
|
+
|
|
131
|
+
| Сигнал от SDK | Что делает App |
|
|
132
|
+
|---------------|----------------|
|
|
133
|
+
| **`onSendCode({ usedMethod: Sms, codeLength, … })`** | Форма/подсказка «код в SMS»; длина поля = `codeLength` |
|
|
134
|
+
| **`onSendCode({ usedMethod: TelegramGateway, codeLength })`** | Форма «код в Telegram» (**не** deep-link) |
|
|
135
|
+
| **`onSendCode({ usedMethod: FlashCall, codeLength })`** | Подсказка «цифры из звонка» |
|
|
136
|
+
| **`onSendCode({ accountType: Email, codeLength })`** | Форма «код на email» |
|
|
137
|
+
| **`onSendCode({ usedMethod: MtsMobileId })`** | Ожидание push **без** keypad (`codeLength` часто `0`) |
|
|
138
|
+
| **`getCode({ codeLength })`** | Дождаться ввода; валидировать по `codeLength` (экран уже из `onSendCode`) |
|
|
139
|
+
| **`onLink(authUrl)`** (`loginWithLink`) | Показать QR / открыть Telegram; **код OTP не вводить** |
|
|
140
|
+
| **`session.onStatusChange`** | Реактивный login-state (`authenticated` / `unauthenticated` / `expired`) |
|
|
141
|
+
|
|
142
|
+
## Sequence: OTP (`loginWithOtp`)
|
|
143
|
+
|
|
144
|
+
```mermaid
|
|
145
|
+
sequenceDiagram
|
|
146
|
+
participant User as User
|
|
147
|
+
participant App
|
|
148
|
+
participant SdkAuth as sdk_auth
|
|
149
|
+
participant Auth as client.auth
|
|
150
|
+
|
|
151
|
+
User->>App: телефон или email
|
|
152
|
+
App->>SdkAuth: TorrowAuth.loginWithOtp
|
|
153
|
+
SdkAuth->>Auth: sendCode
|
|
154
|
+
Auth-->>SdkAuth: usedMethod sessionId codeLength
|
|
155
|
+
SdkAuth->>App: onSendCode account usedMethod codeLength
|
|
156
|
+
|
|
157
|
+
alt usedMethod MtsMobileId
|
|
158
|
+
Note over App,User: onSendCode MtsMobileId без keypad
|
|
159
|
+
App->>User: UI ожидание push
|
|
160
|
+
loop пока Pending
|
|
161
|
+
SdkAuth->>Auth: requestToken code empty
|
|
162
|
+
Auth-->>SdkAuth: status Pending
|
|
163
|
+
end
|
|
164
|
+
opt status AwaitingSmsCode
|
|
165
|
+
Note over SdkAuth,App: показать SMS-форму
|
|
166
|
+
SdkAuth->>App: getCode codeLength
|
|
167
|
+
App->>User: форма SMS-кода
|
|
168
|
+
User->>App: ввод SMS-кода
|
|
169
|
+
App-->>SdkAuth: code
|
|
170
|
+
SdkAuth->>Auth: requestToken with code
|
|
171
|
+
end
|
|
172
|
+
else classic Sms FlashCall TelegramGateway Email
|
|
173
|
+
Note over App,User: onSendCode usedMethod в т.ч. TelegramGateway
|
|
174
|
+
App->>User: форма или подсказка под usedMethod
|
|
175
|
+
SdkAuth->>App: getCode codeLength
|
|
176
|
+
User->>App: ввод кода
|
|
177
|
+
App-->>SdkAuth: code
|
|
178
|
+
SdkAuth->>Auth: requestToken with code
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
Auth-->>SdkAuth: TokenReady
|
|
182
|
+
SdkAuth->>SdkAuth: persist TokenStorage
|
|
183
|
+
SdkAuth-->>App: client plus session
|
|
184
|
+
App->>User: вход выполнен
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Sequence: Telegram (`loginWithLink`)
|
|
188
|
+
|
|
189
|
+
```mermaid
|
|
190
|
+
sequenceDiagram
|
|
191
|
+
participant User as User
|
|
192
|
+
participant App
|
|
193
|
+
participant SdkAuth as sdk_auth
|
|
194
|
+
participant Auth as client.auth
|
|
195
|
+
participant Bot as TelegramBot
|
|
196
|
+
|
|
197
|
+
User->>App: войти через Telegram
|
|
198
|
+
App->>SdkAuth: TorrowAuth.loginWithLink
|
|
199
|
+
SdkAuth->>Auth: createSession
|
|
200
|
+
Auth-->>SdkAuth: sessionId code authUrl
|
|
201
|
+
SdkAuth->>App: onLink authUrl
|
|
202
|
+
App->>User: QR или открыть authUrl
|
|
203
|
+
User->>Bot: Start
|
|
204
|
+
Bot->>Auth: VerifyAuthSession
|
|
205
|
+
loop пока не ready
|
|
206
|
+
SdkAuth->>Auth: requestToken sessionId code
|
|
207
|
+
Auth-->>SdkAuth: not ready или TokenReady
|
|
208
|
+
end
|
|
209
|
+
SdkAuth->>SdkAuth: persist TokenStorage
|
|
210
|
+
SdkAuth-->>App: client plus session
|
|
211
|
+
App->>User: вход выполнен
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Sequence: connect / refresh / 401 / logout
|
|
215
|
+
|
|
216
|
+
```mermaid
|
|
217
|
+
sequenceDiagram
|
|
218
|
+
participant User as User
|
|
219
|
+
participant App
|
|
220
|
+
participant SdkAuth as sdk_auth
|
|
221
|
+
participant Api as TorrowAPI
|
|
222
|
+
participant Auth as client.auth
|
|
223
|
+
|
|
224
|
+
App->>SdkAuth: TorrowAuth.connect
|
|
225
|
+
SdkAuth->>SdkAuth: TokenStorage.load
|
|
226
|
+
alt access свежий
|
|
227
|
+
SdkAuth-->>App: client plus session authenticated
|
|
228
|
+
else есть refreshToken
|
|
229
|
+
SdkAuth->>Auth: refresh
|
|
230
|
+
Auth-->>SdkAuth: TokenReady
|
|
231
|
+
SdkAuth->>SdkAuth: persist
|
|
232
|
+
SdkAuth-->>App: client plus session authenticated
|
|
233
|
+
else нет токенов
|
|
234
|
+
SdkAuth-->>App: session unauthenticated
|
|
235
|
+
Note over App: далее loginWithOtp или loginWithLink
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
User->>App: действие API
|
|
239
|
+
App->>Api: Bearer request
|
|
240
|
+
alt 401 и refreshOnUnauthorized
|
|
241
|
+
Api-->>SdkAuth: 401
|
|
242
|
+
SdkAuth->>Auth: refresh
|
|
243
|
+
Auth-->>SdkAuth: TokenReady
|
|
244
|
+
SdkAuth->>Api: retry once
|
|
245
|
+
Api-->>App: 200
|
|
246
|
+
else access истекает по skew
|
|
247
|
+
SdkAuth->>Auth: proactive refresh
|
|
248
|
+
Auth-->>SdkAuth: TokenReady
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
opt logout
|
|
252
|
+
App->>SdkAuth: session.logout
|
|
253
|
+
SdkAuth->>Auth: revoke
|
|
254
|
+
SdkAuth->>SdkAuth: clear TokenStorage
|
|
255
|
+
SdkAuth->>App: onStatusChange unauthenticated
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Sequence: fromTokens + onStatusChange
|
|
260
|
+
|
|
261
|
+
```mermaid
|
|
262
|
+
sequenceDiagram
|
|
263
|
+
participant Host as HostApp
|
|
264
|
+
participant App as Widget
|
|
265
|
+
participant SdkAuth as sdk_auth
|
|
266
|
+
|
|
267
|
+
Host->>App: accessToken refreshToken optional
|
|
268
|
+
App->>SdkAuth: TorrowAuth.fromTokens
|
|
269
|
+
SdkAuth->>SdkAuth: save to TokenStorage
|
|
270
|
+
SdkAuth-->>App: client plus session
|
|
271
|
+
SdkAuth->>App: onStatusChange authenticated
|
|
272
|
+
Note over App,SdkAuth: дальше как connect refresh 401
|
|
273
|
+
opt access истёк без refresh или logout
|
|
274
|
+
SdkAuth->>App: onStatusChange expired or unauthenticated
|
|
275
|
+
App->>Host: запросить новый логин
|
|
276
|
+
end
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Storage
|
|
280
|
+
|
|
281
|
+
| Класс | Назначение |
|
|
282
|
+
|-------|------------|
|
|
283
|
+
| **`MemoryTokenStorage`** | Тесты, эфемерные процессы, без диска |
|
|
284
|
+
| **`FileTokenStorage`** | Node-only (`node:fs`): JSON-файл — **`@torrow/sdk-auth/node`** (канон для `examples/node/**`) |
|
|
285
|
+
| **`LocalStorageTokenStorage`** | Браузер / виджет: `localStorage` |
|
|
286
|
+
| custom **`TokenStorage`** | IndexedDB, SecureStore, … — `load` / `save` снаружи |
|
|
287
|
+
|
|
288
|
+
**Node vs browser:** главный вход `@torrow/sdk-auth` browser-safe. `FileTokenStorage` — только `import { FileTokenStorage } from "@torrow/sdk-auth/node"` (тянет `node:fs`).
|
|
289
|
+
|
|
290
|
+
**JWT vs integration key:** user JWT обычно с `exp` в payload; opaque/integration key — строка без JWT-формы.
|
|
291
|
+
|
|
292
|
+
- **Opaque (non-JWT-shaped):** меньше 3 сегментов через точку (не `a.b.c`); host TTL через `TorrowAuth.fromTokens({ accessExpiresAtMs })` или бессрочно без TTL
|
|
293
|
+
- **JWT-shaped undecodable (`a.b.c`):** `isAccessStillFresh` всегда false → proactive refresh на `open` / `ensureFresh`, пока есть `refreshToken`; после refresh **без** `expiration` merge сбрасывает snapshot TTL → opaque unlimited → дальнейший proactive refresh не идёт (не refresh-storm)
|
|
294
|
+
- **Valid JWT:** runtime freshness AND-ит `exp`; будущий explicit `accessExpiresAtMs` не оживит просроченный JWT
|
|
295
|
+
- **Opaque host TTL в skew-окне** (`accessTokenSkewMs`, default 30s): refresh без `expiration` сбрасывает snapshot TTL → бессрочный opaque до 401 (лучше refresh-storm)
|
|
296
|
+
|
|
297
|
+
Host: non-JWT-shaped opaque **или** валидный JWT с `exp`.
|
|
298
|
+
|
|
299
|
+
Канон examples: `new FileTokenStorage(path)` + `TorrowAuth.connect` / `loginWithOtp` — без ручного dual deviceInfo provider.
|
|
300
|
+
|
|
301
|
+
## API (кратко)
|
|
302
|
+
|
|
303
|
+
| API | Роль |
|
|
304
|
+
|-----|------|
|
|
305
|
+
| **`TorrowAuth.connect`** | restore / refresh → `{ client, session }`; **без** OTP |
|
|
306
|
+
| **`TorrowAuth.loginWithOtp`** | sendCode → Mobile ID / classic → persist |
|
|
307
|
+
| **`TorrowAuth.loginWithLink`** | Telegram deep-link → poll → persist |
|
|
308
|
+
| **`TorrowAuth.fromTokens`** | сид access[/refresh] → connect-семантика |
|
|
309
|
+
| **`TorrowAuth.anonymous`** | `TorrowClient` без Bearer |
|
|
310
|
+
| **`TokenSession`** | `status`, `onStatusChange`, `logout`, `requestTokenWithCode`, `refresh`, `client` |
|
|
311
|
+
| **`refreshOnUnauthorized`** | 401 → refresh → один retry (**default true**) |
|
|
312
|
+
| **`onRefreshFailure`** | optional; политика при ошибке refresh (`keep` / `clear` / `throw`); default: non-HTTP / network → `keep`; HTTP 429/502–504 → `keep`; other HTTP → `clear` |
|
|
313
|
+
|
|
314
|
+
`appCredentials: { clientId, clientSecret }` (опционально; иначе **`DEFAULT_APP_CREDENTIALS`**)
|
|
315
|
+
внутри auth-пакета мапится на **`AuthIdentityBasic`**
|
|
316
|
+
(`username`/`password`) → **`TorrowClientConfig.authIdentityBasic`**. Маппер **не** публичный API:
|
|
317
|
+
приложениям достаточно **`appCredentials`** или дефолта; low-level `TorrowClient` собирает Basic сам
|
|
318
|
+
(см. **`examples/node/auth/auth-send-code.ts`**).
|
|
319
|
+
|
|
320
|
+
**401-retry** вызывает только **refresh**, не OTP.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { T as TokenStorage, A as AuthTokenSnapshot, a as TokenSessionOptions, b as TokenSessionStatus, c as TokenSessionStatusListener } from './types-LCuPy3dY.js';
|
|
2
|
+
export { d as AppCredentials, D as DEFAULT_APP_CREDENTIALS, R as RefreshFailureAction } from './types-LCuPy3dY.js';
|
|
3
|
+
import { TorrowClient, DeviceInfo, TokenResp, AuthScope, VerificationMethod, SendCodeAccountType, AuthCaptchaType, AccountType } from '@torrow/sdk-client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-memory реализация {@link TokenStorage}.
|
|
7
|
+
*
|
|
8
|
+
* Тесты, эфемерные процессы, `TorrowAuth.fromTokens` без диска.
|
|
9
|
+
* Для Node-примеров обычно {@link FileTokenStorage}; для браузера —
|
|
10
|
+
* {@link LocalStorageTokenStorage}.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** In-memory хранилище токенов сессии. */
|
|
14
|
+
declare class MemoryTokenStorage implements TokenStorage {
|
|
15
|
+
private snapshot;
|
|
16
|
+
constructor(initial?: AuthTokenSnapshot);
|
|
17
|
+
load(): Promise<AuthTokenSnapshot>;
|
|
18
|
+
save(snapshot: AuthTokenSnapshot): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Persist snapshot в браузерный **`localStorage`** — реализация {@link TokenStorage}.
|
|
23
|
+
*
|
|
24
|
+
* Для виджетов / SPA (`TorrowAuth.fromTokens` / `connect`). В Node без
|
|
25
|
+
* `globalThis.localStorage` конструктор бросит. Не путать с {@link FileTokenStorage}.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type LocalStorageLike = {
|
|
29
|
+
getItem(key: string): string | null;
|
|
30
|
+
setItem(key: string, value: string): void;
|
|
31
|
+
removeItem?(key: string): void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Persist snapshot токенов в браузерный `localStorage` (виджет / SPA).
|
|
35
|
+
*/
|
|
36
|
+
declare class LocalStorageTokenStorage implements TokenStorage {
|
|
37
|
+
readonly key: string;
|
|
38
|
+
private readonly store;
|
|
39
|
+
constructor(key: string, store?: LocalStorageLike);
|
|
40
|
+
load(): Promise<AuthTokenSnapshot>;
|
|
41
|
+
save(snapshot: AuthTokenSnapshot): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Сессия auth ещё не подтверждена (например Telegram до Start у бота). */
|
|
45
|
+
declare class AuthSessionNotReadyError extends Error {
|
|
46
|
+
readonly name = "AuthSessionNotReadyError";
|
|
47
|
+
readonly cause?: unknown;
|
|
48
|
+
constructor(message?: string, cause?: unknown);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ядро пользовательской сессии: persist + проактивный refresh + single-flight + 401-retry.
|
|
53
|
+
*
|
|
54
|
+
* Low-level API: {@link TokenSession.open}. Канон для приложений —
|
|
55
|
+
* **`TorrowAuth.connect` / `login*` / `fromTokens`** (см. `torrow-auth.ts`).
|
|
56
|
+
*
|
|
57
|
+
* Логин снаружи: `TorrowAuth.loginWithOtp` / `loginWithLink`, либо
|
|
58
|
+
* `requestTokenWithCode` / `applyTokenResp` на уже открытой сессии.
|
|
59
|
+
* Не путать статус `"unauthenticated"` с `TorrowAuth.anonymous` (клиент без сессии).
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Пользовательская сессия (класс). См. file overview выше.
|
|
64
|
+
*/
|
|
65
|
+
declare class TokenSession {
|
|
66
|
+
private readonly opts;
|
|
67
|
+
private tokenSnapshot;
|
|
68
|
+
private ensureFreshMutex;
|
|
69
|
+
private _client;
|
|
70
|
+
private readonly statusListeners;
|
|
71
|
+
private closed;
|
|
72
|
+
/** Set in refreshWithFailurePolicy catch; not cleared on success (see public refresh). */
|
|
73
|
+
private lastRefreshError;
|
|
74
|
+
private constructor();
|
|
75
|
+
static open(opts: TokenSessionOptions): Promise<TokenSession>;
|
|
76
|
+
/** Persist, переданный в `TokenSession.open` / `TorrowAuth.*`. */
|
|
77
|
+
get tokenStorage(): TokenStorage;
|
|
78
|
+
get client(): TorrowClient;
|
|
79
|
+
/** Текущий статус сессии. */
|
|
80
|
+
get status(): TokenSessionStatus;
|
|
81
|
+
/** Подписка на смену статуса; возвращает unsubscribe. Сразу вызывает listener с текущим статусом. */
|
|
82
|
+
onStatusChange(listener: TokenSessionStatusListener): () => void;
|
|
83
|
+
/** Текущий снимок токенов сессии (копия {@link AuthTokenSnapshot}). */
|
|
84
|
+
getTokenSnapshot(): AuthTokenSnapshot;
|
|
85
|
+
/** `deviceInfo` из options open или fallback Desktop + deviceId из tokenSnapshot. */
|
|
86
|
+
getDeviceInfo(): DeviceInfo;
|
|
87
|
+
getAccessToken(): Promise<string | undefined>;
|
|
88
|
+
/**
|
|
89
|
+
* Сохранить ответ `requestToken` / `refresh` (только TokenReady).
|
|
90
|
+
* Сериализован с `ensureFresh` / `logout` через mutex. Дефолт merge намеренно
|
|
91
|
+
* (refresh-shaped); логин — `{ replace: true }`. После `logout` — новая сессия.
|
|
92
|
+
*/
|
|
93
|
+
applyTokenResp(tokens: TokenResp, opts?: {
|
|
94
|
+
replace?: boolean;
|
|
95
|
+
}): Promise<void>;
|
|
96
|
+
clearTokens(): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Revoke refresh (если есть) + очистка storage.
|
|
99
|
+
* Единственное публичное имя выхода из сессии.
|
|
100
|
+
*/
|
|
101
|
+
logout(): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Получить токены по **id auth-сессии** (`sessionId` из `sendCode` / `createSession`)
|
|
104
|
+
* и **OTP-коду доступа**, затем сохранить их в storage сессии.
|
|
105
|
+
*
|
|
106
|
+
* Один вызов `POST /api/v1/auth/token` (`requestToken`) с `{ sessionId, code }`.
|
|
107
|
+
* При успехе (`TokenReady`) — persist; если сессия ещё не готова —
|
|
108
|
+
* {@link AuthSessionNotReadyError} (поллинг для deep-link — внутри `TorrowAuth.loginWithLink`).
|
|
109
|
+
*
|
|
110
|
+
* HTTP вне mutex; persist сериализован через {@link applyTokenResp} (не оборачивать
|
|
111
|
+
* весь метод в `runExclusive` — вложенный mutex с публичным `applyTokenResp`).
|
|
112
|
+
*/
|
|
113
|
+
requestTokenWithCode(args: {
|
|
114
|
+
sessionId: string;
|
|
115
|
+
code: string;
|
|
116
|
+
scopes?: AuthScope[];
|
|
117
|
+
signal?: AbortSignal;
|
|
118
|
+
}): Promise<TokenResp>;
|
|
119
|
+
/**
|
|
120
|
+
* Явный refresh (single-flight через общий mutex сессии).
|
|
121
|
+
* Сериализован с `logout` / `ensureFresh` / `applyTokenResp`.
|
|
122
|
+
* При гонке с `logout()`: promise может resolve с `TokenResp`, но snapshot уже очищен —
|
|
123
|
+
* не используй return value; смотри `session.status` / `getTokenSnapshot()`.
|
|
124
|
+
*/
|
|
125
|
+
refresh(): Promise<TokenResp>;
|
|
126
|
+
ensureFresh(): Promise<void>;
|
|
127
|
+
private assertNotClosedForWrite;
|
|
128
|
+
/** Caller must hold `runExclusive`. Do not call public `applyTokenResp` from here. */
|
|
129
|
+
private applyTokenRespUnlocked;
|
|
130
|
+
private clearTokensUnlocked;
|
|
131
|
+
/** Caller must hold `runExclusive`. `refreshWithFailurePolicy` skips when no refreshToken; guard here is intentional defense-in-depth. */
|
|
132
|
+
private refreshUnlocked;
|
|
133
|
+
/** Caller must hold `runExclusive`. */
|
|
134
|
+
private refreshWithFailurePolicy;
|
|
135
|
+
private runExclusive;
|
|
136
|
+
private ensureFreshImpl;
|
|
137
|
+
private isAccessStillFresh;
|
|
138
|
+
private computeStatus;
|
|
139
|
+
private emitStatusChange;
|
|
140
|
+
private resolveDeviceInfo;
|
|
141
|
+
/** Identity-клиент с Basic приложения, без Bearer пользователя. */
|
|
142
|
+
identityClient(): TorrowClient;
|
|
143
|
+
private buildClient;
|
|
144
|
+
private persist;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Внутренний оркестратор OTP-логина (не публичный export целиком; типы UI — да).
|
|
149
|
+
*
|
|
150
|
+
* **`sendCode`** → ветка по **`usedMethod`** (Mobile ID poll | classic код) →
|
|
151
|
+
* **`TokenResp`**. UI снаружи: **`onSendCode`** + **`getCode`**.
|
|
152
|
+
* IMAP / stdin / env — в app / `examples/node/_shared`, не здесь.
|
|
153
|
+
*
|
|
154
|
+
* Вызывается из `TorrowAuth.loginWithOtp`; Mobile ID — `mobile-id.ts`.
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Аргумент **`onSendCode`**: снимок **после** успешного `sendCode` (в основном поля **`SendCodeResp`**).
|
|
159
|
+
* Не путать с телом запроса: в `sendCode` уходят `supportedMethods` / `preferredMethod`
|
|
160
|
+
* (см. {@link RunOtpLoginOptions}); здесь — что сервер **выбрал** и что ещё доступно.
|
|
161
|
+
*
|
|
162
|
+
* Типичный UI: по `usedMethod` показать «ждём SMS» / «код на email» / «подтвердите push»;
|
|
163
|
+
* по `codeLength` — маску поля ввода.
|
|
164
|
+
*/
|
|
165
|
+
type OtpSendCodeResult = {
|
|
166
|
+
/** Аккаунт из `SendCodeReq` (телефон / email). */
|
|
167
|
+
accountName: string;
|
|
168
|
+
/** `Phone` \| `Email` из запроса (не путать с `usedMethod`). */
|
|
169
|
+
accountType: SendCodeAccountType;
|
|
170
|
+
/** Id auth-сессии для последующего `requestToken` / Mobile ID poll. */
|
|
171
|
+
sessionId: string;
|
|
172
|
+
/**
|
|
173
|
+
* Фактический канал из **`SendCodeResp.usedMethod`** — ради этого и нужен `onSendCode`.
|
|
174
|
+
* Sms / Email / FlashCall / TelegramGateway / MtsMobileId / …
|
|
175
|
+
*/
|
|
176
|
+
usedMethod?: VerificationMethod;
|
|
177
|
+
/** Длина кода для input/маски; у Mobile ID часто `0` (keypad не нужен). */
|
|
178
|
+
codeLength?: number;
|
|
179
|
+
/** Интервал poll при Mobile ID (`SendCodeResp.pollIntervalSeconds`). */
|
|
180
|
+
pollIntervalSeconds?: number;
|
|
181
|
+
/**
|
|
182
|
+
* Из **`SendCodeResp`** (не тело `sendCode`): методы, доступные для этого аккаунта
|
|
183
|
+
* (UX «другой способ»). В запрос передаётся {@link RunOtpLoginOptions.supportedMethods}.
|
|
184
|
+
*/
|
|
185
|
+
availableMethods?: VerificationMethod[] | null;
|
|
186
|
+
/** TTL auth-сессии `sendCode` (сек); для Mobile ID — ориентир max wait. */
|
|
187
|
+
sessionLifeTimeInSeconds?: number;
|
|
188
|
+
/** Когда снова можно дернуть `sendCode` (rate-limit hint из ответа). */
|
|
189
|
+
nextAvailableUsageTime?: Date | null;
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Аргумент **`getCode`**: только когда нужны цифры (stdin / UI / IMAP снаружи).
|
|
193
|
+
*
|
|
194
|
+
* UI-экран уже выбран в **`onSendCode`** (`usedMethod`). Здесь — собрать код
|
|
195
|
+
* (длина из `codeLength`). На happy-path Mobile ID (push) **`getCode` не вызывается**;
|
|
196
|
+
* SMS-fallback после poll — тот же колбэк, без отдельного `reason`.
|
|
197
|
+
*
|
|
198
|
+
* `accountName` / `accountType` не дублируются — они в options и в `onSendCode`.
|
|
199
|
+
*/
|
|
200
|
+
type OtpGetCodeContext = {
|
|
201
|
+
sessionId: string;
|
|
202
|
+
/** Эффективная длина для валидации (Mobile ID SMS fallback: при `0` с сервера → 4). */
|
|
203
|
+
codeLength?: number;
|
|
204
|
+
/** Канал из `sendCode` (опциональная подсказка; экран уже из `onSendCode`). */
|
|
205
|
+
usedMethod?: VerificationMethod;
|
|
206
|
+
};
|
|
207
|
+
/** Превышен лимит `sendCode` (`CODE_REQUEST_LIMIT_EXCEEDED`). */
|
|
208
|
+
declare class OtpCodeRequestLimitExceededError extends Error {
|
|
209
|
+
readonly name = "OtpCodeRequestLimitExceededError";
|
|
210
|
+
readonly minutes?: string;
|
|
211
|
+
readonly seconds?: string;
|
|
212
|
+
readonly traceUrl?: string;
|
|
213
|
+
readonly apiMessage?: string;
|
|
214
|
+
readonly body: unknown;
|
|
215
|
+
constructor(body: unknown);
|
|
216
|
+
}
|
|
217
|
+
/** Нет Basic на `/auth/token` (`AUTH_HEADER_MISSING`). */
|
|
218
|
+
declare class OtpAuthHeaderMissingError extends Error {
|
|
219
|
+
readonly name = "OtpAuthHeaderMissingError";
|
|
220
|
+
readonly body: unknown;
|
|
221
|
+
readonly systemMessage?: string;
|
|
222
|
+
constructor(body: unknown);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Публичный фасад аутентификации **`TorrowAuth`**.
|
|
227
|
+
*
|
|
228
|
+
* Точка входа приложения: `connect` / `loginWithOtp` / `loginWithLink` /
|
|
229
|
+
* `fromTokens` / `anonymous`. Возвращает {@link TorrowAuthResult}
|
|
230
|
+
* (`{ client, session }`), кроме `anonymous` (только `TorrowClient`).
|
|
231
|
+
*
|
|
232
|
+
* Оркестрация OTP/Telegram — в `otp-login.ts` / `link-login.ts`;
|
|
233
|
+
* persist и refresh — в {@link TokenSession}.
|
|
234
|
+
*/
|
|
235
|
+
|
|
236
|
+
/** Результат `TorrowAuth.connect` / `loginWithOtp` / `loginWithLink` / `fromTokens`: API-клиент + сессия. */
|
|
237
|
+
type TorrowAuthResult = {
|
|
238
|
+
client: TorrowClient;
|
|
239
|
+
session: TokenSession;
|
|
240
|
+
};
|
|
241
|
+
/**
|
|
242
|
+
* Общая база опций `TorrowAuth.connect` / `loginWithOtp` / `loginWithLink` / `fromTokens`.
|
|
243
|
+
*
|
|
244
|
+
* Производная от {@link TokenSessionOptions}: те же HTTP-кнопки + storage + app creds,
|
|
245
|
+
* но `deviceInfo` **обязателен**; `onRefreshFailure` пробрасывается в `TokenSession.open`.
|
|
246
|
+
*
|
|
247
|
+
* **Карта:** `TorrowClientConfig` → (Pick HTTP) → `TokenSessionOptions` → **эта база** →
|
|
248
|
+
* `TorrowAuthConnectOptions` / `TorrowAuthLoginWithOtpOptions` / …
|
|
249
|
+
* Приложение обычно передаёт именно `TorrowAuth*Options`, не `TorrowClientConfig`.
|
|
250
|
+
*
|
|
251
|
+
* `appCredentials` опционален: без него — дефолт SDK (`DEFAULT_APP_CREDENTIALS`).
|
|
252
|
+
*/
|
|
253
|
+
type TorrowAuthSessionBase = Omit<TokenSessionOptions, "deviceInfo"> & {
|
|
254
|
+
deviceInfo: DeviceInfo;
|
|
255
|
+
};
|
|
256
|
+
/** Опции {@link TorrowAuth.connect}: restore / refresh из `tokenStorage`. */
|
|
257
|
+
type TorrowAuthConnectOptions = TorrowAuthSessionBase;
|
|
258
|
+
/** Аккаунт для OTP `sendCode` — плоско, как OpenAPI `SendCodeReq`. */
|
|
259
|
+
type TorrowAuthAccount = {
|
|
260
|
+
/** `Phone` \| `Email` → `SendCodeReq.type`. */
|
|
261
|
+
accountType: SendCodeAccountType;
|
|
262
|
+
/** Телефон или email → `SendCodeReq.accountName`. */
|
|
263
|
+
accountName: string;
|
|
264
|
+
};
|
|
265
|
+
/** Опции {@link TorrowAuth.loginWithOtp}: connect-база + OTP-колбэки / поля `sendCode`. */
|
|
266
|
+
type TorrowAuthLoginWithOtpOptions = TorrowAuthSessionBase & {
|
|
267
|
+
account: TorrowAuthAccount;
|
|
268
|
+
/**
|
|
269
|
+
* Сигнал UI после `sendCode` (`usedMethod`, `codeLength`, …).
|
|
270
|
+
* До Mobile ID poll и до classic `getCode`. См. {@link OtpSendCodeResult}.
|
|
271
|
+
*/
|
|
272
|
+
onSendCode: (result: OtpSendCodeResult) => void | Promise<void>;
|
|
273
|
+
/**
|
|
274
|
+
* Вернуть одноразовый код. Не зовётся на happy-path Mobile ID (push без SMS).
|
|
275
|
+
* См. {@link OtpGetCodeContext}.
|
|
276
|
+
*/
|
|
277
|
+
getCode: (ctx: OtpGetCodeContext) => Promise<string>;
|
|
278
|
+
/** → `TokenReq.scopes`. По умолчанию `DefaultAuthScopes`. */
|
|
279
|
+
scopes?: AuthScope[];
|
|
280
|
+
/** → `SendCodeReq.language`. По умолчанию `"ru"`. */
|
|
281
|
+
language?: string;
|
|
282
|
+
/** → `SendCodeReq.regionStorageId`. По умолчанию `70`. */
|
|
283
|
+
regionStorageId?: number;
|
|
284
|
+
/** → `SendCodeReq.captcha` (если шлюз требует). */
|
|
285
|
+
captcha?: string;
|
|
286
|
+
/** → `SendCodeReq.captchaType`. */
|
|
287
|
+
captchaType?: AuthCaptchaType;
|
|
288
|
+
/** → `SendCodeReq.inviteSessionId`. */
|
|
289
|
+
inviteSessionId?: string;
|
|
290
|
+
/**
|
|
291
|
+
* Hint серверу → `SendCodeReq.preferredMethod`.
|
|
292
|
+
* Факт смотри в `onSendCode.usedMethod`.
|
|
293
|
+
*/
|
|
294
|
+
preferredMethod?: VerificationMethod;
|
|
295
|
+
/**
|
|
296
|
+
* Capability клиента → `SendCodeReq.supportedMethods`.
|
|
297
|
+
* Без `MtsMobileId` сервер обычно не выберет Mobile ID.
|
|
298
|
+
*/
|
|
299
|
+
supportedMethods?: VerificationMethod[];
|
|
300
|
+
/** Отмена `sendCode` / poll / `requestToken`. */
|
|
301
|
+
signal?: AbortSignal;
|
|
302
|
+
};
|
|
303
|
+
/** Опции {@link TorrowAuth.loginWithLink} (Telegram deep-link). */
|
|
304
|
+
type TorrowAuthLoginWithLinkOptions = TorrowAuthSessionBase & {
|
|
305
|
+
/** Показать deep-link / QR (`CreateAuthSessionResp.authUrl`). */
|
|
306
|
+
onLink: (authUrl: string) => void | Promise<void>;
|
|
307
|
+
/** По умолчанию `Telegram`. */
|
|
308
|
+
accountType?: AccountType;
|
|
309
|
+
language?: string;
|
|
310
|
+
regionStorageId?: number;
|
|
311
|
+
captcha?: string;
|
|
312
|
+
inviteSessionId?: string;
|
|
313
|
+
scopes?: AuthScope[];
|
|
314
|
+
/** Интервал poll `requestToken` (мс). По умолчанию 2000. */
|
|
315
|
+
pollIntervalMs?: number;
|
|
316
|
+
/** Max wait poll (сек). Иначе TTL сессии из ответа или 180. */
|
|
317
|
+
pollTimeoutSec?: number;
|
|
318
|
+
signal?: AbortSignal;
|
|
319
|
+
};
|
|
320
|
+
/** Опции {@link TorrowAuth.fromTokens}: сид токенов от хоста в `tokenStorage` → connect. */
|
|
321
|
+
type TorrowAuthFromTokensOptions = TorrowAuthSessionBase & {
|
|
322
|
+
accessToken: string;
|
|
323
|
+
/** Без него refresh недоступен (даже если задан `appCredentials`). */
|
|
324
|
+
refreshToken?: string;
|
|
325
|
+
/**
|
|
326
|
+
* Абсолютное истечение access (мс). Для opaque/integration keys без JWT `exp`.
|
|
327
|
+
* Приоритет **записи в snapshot**: explicit (`> 0` finite) → JWT `exp` → `undefined` (бессрочный ключ).
|
|
328
|
+
* Runtime freshness AND-ит JWT `exp`: будущий explicit TTL не оживит просроченный JWT.
|
|
329
|
+
* Opaque без JWT-формы: explicit TTL или бессрочно без TTL.
|
|
330
|
+
*/
|
|
331
|
+
accessExpiresAtMs?: number;
|
|
332
|
+
};
|
|
333
|
+
/**
|
|
334
|
+
* Фасад аутентификации для приложений и виджетов.
|
|
335
|
+
*
|
|
336
|
+
* - {@link TorrowAuth.connect} — restore / refresh
|
|
337
|
+
* - {@link TorrowAuth.loginWithOtp} — OTP (Phone/Email, Mobile ID / classic)
|
|
338
|
+
* - {@link TorrowAuth.loginWithLink} — Telegram deep-link
|
|
339
|
+
* - {@link TorrowAuth.fromTokens} — сид токенов от хоста
|
|
340
|
+
* - {@link TorrowAuth.anonymous} — клиент без Bearer
|
|
341
|
+
*/
|
|
342
|
+
declare const TorrowAuth: {
|
|
343
|
+
readonly connect: (options: TorrowAuthConnectOptions) => Promise<TorrowAuthResult>;
|
|
344
|
+
readonly loginWithOtp: (options: TorrowAuthLoginWithOtpOptions) => Promise<TorrowAuthResult>;
|
|
345
|
+
readonly loginWithLink: (options: TorrowAuthLoginWithLinkOptions) => Promise<TorrowAuthResult>;
|
|
346
|
+
readonly fromTokens: (options: TorrowAuthFromTokensOptions) => Promise<TorrowAuthResult>;
|
|
347
|
+
readonly anonymous: (config?: Pick<TokenSessionOptions, "baseUrl" | "fetch" | "defaultHeaders" | "userAgent">) => TorrowClient;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
export { AuthSessionNotReadyError, AuthTokenSnapshot, LocalStorageTokenStorage, MemoryTokenStorage, OtpAuthHeaderMissingError, OtpCodeRequestLimitExceededError, type OtpGetCodeContext, type OtpSendCodeResult, TokenSession, TokenSessionOptions, TokenSessionStatus, TokenSessionStatusListener, TokenStorage, TorrowAuth, type TorrowAuthAccount, type TorrowAuthConnectOptions, type TorrowAuthFromTokensOptions, type TorrowAuthLoginWithLinkOptions, type TorrowAuthLoginWithOtpOptions, type TorrowAuthResult, type TorrowAuthSessionBase };
|