@torrow/sdk-auth 0.0.0-stage → 0.7.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 +437 -2
- package/dist/index.d.ts +543 -0
- package/dist/index.js +1317 -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-C4e90uOw.d.ts +123 -0
- package/package.json +50 -4
package/README.md
CHANGED
|
@@ -1,3 +1,438 @@
|
|
|
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 -->|раздельные OTP-запросы| flow["TorrowAuth.createOtpFlow: start / complete"]
|
|
17
|
+
kind -->|Telegram deep-link| link["TorrowAuth.loginWithLink"]
|
|
18
|
+
kind -->|токены от хоста| fromTok["TorrowAuth.fromTokens"]
|
|
19
|
+
kind -->|без входа| anon["TorrowAuth.anonymous"]
|
|
20
|
+
connect --> handle["TorrowAuthResult: client + session"]
|
|
21
|
+
login --> handle
|
|
22
|
+
link --> handle
|
|
23
|
+
fromTok --> handle
|
|
24
|
+
anon --> clientOnly["TorrowClient"]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Флоу / подсистема для агентов: **`docs/agents/auth-flow.md`**. Индекс примеров Node: **`examples/node/auth/README.md`**, **`examples/node/_shared/README.md`**.
|
|
28
|
+
|
|
29
|
+
## Целевой клиентский код
|
|
30
|
+
|
|
31
|
+
### Пошаговый OTP без пользовательской сессии
|
|
32
|
+
|
|
33
|
+
`TorrowAuth.createOtpFlow` выполняет один `sendCode` на start и один `requestToken` на complete. Он не хранит challenge или токены, не требует TokenStorage/callbacks, не делает polling, refresh, retry или revoke. Для полного входа с callbacks и сохранением используйте `loginWithOtp`.
|
|
34
|
+
|
|
35
|
+
```mermaid
|
|
36
|
+
sequenceDiagram
|
|
37
|
+
actor User as Пользователь
|
|
38
|
+
participant App as Приложение
|
|
39
|
+
participant Flow as OtpFlow (@torrow/sdk-auth)
|
|
40
|
+
participant API as Torrow API
|
|
41
|
+
|
|
42
|
+
App->>Flow: createOtpFlow(options)
|
|
43
|
+
App->>Flow: start({ account, ... }, { signal })
|
|
44
|
+
Flow->>API: POST /auth/sendcode (Basic приложения)
|
|
45
|
+
API-->>Flow: SendCodeResp
|
|
46
|
+
Flow-->>App: OtpChallenge
|
|
47
|
+
Note over App,Flow: Classic — AwaitingCode, Mobile ID — Pending.<br/>Challenge не содержит токенов или OTP.
|
|
48
|
+
App->>App: Защитить и перенести challenge через JSON
|
|
49
|
+
Note over App,Flow: complete можно вызвать на новом экземпляре flow.<br/>deviceInfo, scopes и splitTokens сохраняет приложение.
|
|
50
|
+
|
|
51
|
+
loop Пока нет TokenReady / Failed и challenge не просрочен
|
|
52
|
+
alt challenge.phase = AwaitingCode
|
|
53
|
+
App->>User: Запросить OTP / SMS-код
|
|
54
|
+
User-->>App: code
|
|
55
|
+
App->>Flow: complete({ challenge, code }, { signal })
|
|
56
|
+
else challenge.phase = Pending
|
|
57
|
+
App->>App: При повторном polling дождаться интервала
|
|
58
|
+
App->>Flow: complete({ challenge }, { signal })
|
|
59
|
+
end
|
|
60
|
+
Note right of Flow: Проверить challenge до HTTP.<br/>Pending — wire code пустой.
|
|
61
|
+
Flow->>API: POST /auth/token (Basic приложения)
|
|
62
|
+
API-->>Flow: TokenResp
|
|
63
|
+
alt status = Pending
|
|
64
|
+
Flow-->>App: Pending + challenge (phase = Pending)<br/>+ cleanup, если есть refresh
|
|
65
|
+
else status = AwaitingSmsCode
|
|
66
|
+
Flow-->>App: SMS fallback той же Mobile ID сессии<br/>AwaitingSmsCode + challenge<br/>(новая phase = AwaitingCode, usedMethod = Sms)<br/>+ cleanup, если есть refresh
|
|
67
|
+
else status = TokenReady
|
|
68
|
+
Flow-->>App: TokenReady + tokens
|
|
69
|
+
App->>App: Использовать токены и решить вопрос хранения / cleanup
|
|
70
|
+
else status = Failed
|
|
71
|
+
Flow-->>App: Failed + cleanup, если есть refresh
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Note over App,Flow: expiresAtMs не продлевается между шагами.<br/>Ошибки передаются приложению, скрытых повторов нет.<br/>Cleanup выполняет приложение с независимым AbortSignal.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { TorrowAuth, OtpFlowError, type OtpChallenge } from "@torrow/sdk-auth";
|
|
80
|
+
import { TokenStatusValues } from "@torrow/sdk-client";
|
|
81
|
+
|
|
82
|
+
const options = {
|
|
83
|
+
deviceInfo: { deviceId: "my-app-device", deviceType: "Desktop" as const },
|
|
84
|
+
scopes: ["user_api" as const],
|
|
85
|
+
splitTokens: false,
|
|
86
|
+
};
|
|
87
|
+
const challenge = await TorrowAuth.createOtpFlow(options).start({
|
|
88
|
+
account: { accountType: "Email", accountName: "user@example.com" },
|
|
89
|
+
});
|
|
90
|
+
// Между запросами приложение защищает целостность этого JSON.
|
|
91
|
+
const continuation: OtpChallenge = JSON.parse(JSON.stringify(challenge));
|
|
92
|
+
try {
|
|
93
|
+
const result = await TorrowAuth.createOtpFlow(options).complete(
|
|
94
|
+
{ challenge: continuation, code: "123456" },
|
|
95
|
+
{ signal: AbortSignal.timeout(15000) },
|
|
96
|
+
);
|
|
97
|
+
if (result.status === TokenStatusValues.TokenReady) {
|
|
98
|
+
// Использовать result.tokens.accessToken в рамках текущего запроса.
|
|
99
|
+
// Сохранение/cleanup result.tokens.refreshToken определяет приложение.
|
|
100
|
+
} else {
|
|
101
|
+
// result.cleanup?.refreshToken доступен для отдельного cleanup.
|
|
102
|
+
if ("challenge" in result) {
|
|
103
|
+
// Защитить result.challenge для следующего шага.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error instanceof OtpFlowError) {
|
|
108
|
+
// error.code — безопасная причина; error.cleanup?.refreshToken — секрет для cleanup.
|
|
109
|
+
// Cleanup выполняет приложение с независимым AbortSignal.
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Значение | Действие приложения |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `challenge.phase: AwaitingCode` | Для следующего шага требуется непустой код. Это первоначальный ввод при обычном OTP либо ввод SMS-кода после fallback в Mobile ID-сценарии. |
|
|
118
|
+
| `challenge.phase: Pending` | Продолжается ожидание результата Mobile ID. Следующий complete вызывается без кода; wire code будет пустым. |
|
|
119
|
+
| `result.status: Pending` | Сервер ещё не завершил аутентификацию. SDK возвращает challenge с фазой Pending; приложение планирует следующий запрос без кода. Возможен возврат к Pending после отправки SMS-кода. |
|
|
120
|
+
| `result.status: AwaitingSmsCode` | Сервер перевёл Mobile ID-сценарий на SMS fallback, поскольку аутентификацию через Mobile ID не удалось завершить. SDK возвращает challenge с новой фазой AwaitingCode и методом Sms; приложение запрашивает SMS-код и передаёт его в следующий complete. |
|
|
121
|
+
| `result.status: Failed` | Текущая попытка аутентификации завершилась отказом. Challenge для продолжения не возвращается; новый вход требует отдельного start. |
|
|
122
|
+
| `result.status: TokenReady` | Аутентификация завершена, токены получены и проверены SDK. Продолжение не требуется; SDK не сохраняет токены. |
|
|
123
|
+
|
|
124
|
+
`result.status` отражает серверный `TokenStatus`, а `challenge.phase` определяет следующий шаг приложения. Переход при fallback: ожидание Mobile ID (`Pending`) → серверный `AwaitingSmsCode` → ввод SMS-кода (новая фаза `AwaitingCode`, метод `Sms`). Это продолжение той же auth-сессии с новым способом подтверждения, а не продолжение фазы AwaitingCode: sessionId и deadline сохраняются, фаза меняется. При обычном Email/SMS/FlashCall/TelegramGateway-входе `start` сразу возвращает `AwaitingCode` с фактическим `usedMethod`: ввод кода требуется до первого `complete`. Статуса `AwaitingEmailCode` в текущем API нет. Предпочтительный метод `preferredMethod` является пожеланием; фактический метод определяет сервер в `usedMethod`.
|
|
125
|
+
|
|
126
|
+
Опции flow: `baseUrl`, `fetch`, `defaultHeaders`, `userAgent`, `appCredentials`, обязательный `deviceInfo`, `scopes`, `splitTokens`. SDK-default Basic и scopes такие же, как в сессии; неуказанный splitTokens не отправляется. Start принимает язык (default ru), регион (70), captcha/type, invite session и preferred/supported methods. Email/UnknownMethod поддерживается; неизвестный метод отклоняется.
|
|
127
|
+
|
|
128
|
+
**Когда использовать `splitTokens`.** Это параметр `POST /api/v1/auth/token`, который обрабатывает сервер: SDK передаёт флаг и возвращает полученные поля токенов без самостоятельного разделения или восстановления.
|
|
129
|
+
|
|
130
|
+
| Значение | Поведение сервера и сценарий |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `false` | Возвращает полные access/refresh в JSON и удаляет cookies ранее разделённых токенов. Используйте для Node, MCP и приложений, которым нужны полные токены для API-вызовов, выпуска интеграционного ключа и cleanup. |
|
|
133
|
+
| `true` | При наличии access token разделяет access/refresh: одну часть каждого токена помещает в HttpOnly cookies, другую возвращает в JSON. Используйте только для браузерного приложения, специально настроенного на этот режим авторизации. При Mobile ID без готовых токенов разделение пропускается. |
|
|
134
|
+
| Не задано | Flow не отправляет поле. В текущей реализации шлюза отсутствующий boolean имеет значение `false`; для явно выбранного режима полных токенов задавайте `false`. |
|
|
135
|
+
|
|
136
|
+
В режиме `true` поля `result.tokens` содержат части токенов, а не самостоятельные полные Bearer/refresh-токены. Последующие запросы должны передавать необходимые части и cookies для восстановления токенов шлюзом. Приложение обеспечивает работу с cookies, включая `credentials` у `fetch` и разрешения CORS при запросах между origin. SDK не добавляет cookie jar или управление cookies; стандартный Node `fetch` не сохраняет и не передаёт их автоматически между запросами. Поэтому Node-примеры задают `splitTokens: false`. Тесты SDK проверяют передачу обоих значений в HTTP-тело, а не серверное разделение или браузерную работу cookies.
|
|
137
|
+
|
|
138
|
+
Challenge: `sessionId`, `accountType`, `usedMethod`, `phase`, `expiresAtMs` и проверенные подсказки `codeLength`, `pollIntervalSeconds`, `attemptsRemaining`, `nextSendAtMs`, `availableMethods`. Даты — Unix milliseconds, пригодные для JSON. Токенов/OTP в challenge нет. Проверка структуры SDK не заменяет проверку целостности приложением. Между шагами приложение сохраняет тот же deviceInfo, scopes и splitTokens; flow не связывает их криптографически с challenge.
|
|
139
|
+
|
|
140
|
+
Срок отсчитывается от начала start: серверный sessionLifeTimeInSeconds либо локальные 180 секунд при отсутствии/null. Некорректный TTL отклоняется. Локальный срок не гарантирует действие сессии на сервере и не продлевается. Ограничение 600 секунд относится к MCP. Polling hint default — 3 секунды. Некорректные необязательные подсказки пропускаются; длина кода — корректная серверная либо Email 6, Phone/SMS fallback 4. Число попыток не уменьшается самостоятельно.
|
|
141
|
+
|
|
142
|
+
Перед проверкой token-ответа нормализуются существующие aliases: AccessToken/access_token, RefreshToken/refresh_token, Expiration/expires_in, Status. Приоритет: имя OpenAPI → PascalCase → snake_case по первому значению, отличному от null/undefined. Flow требует явный известный status и непустой access token при TokenReady. Старые методы сохраняют ответы без status, только с refresh token и прежние callback-поля через внутреннюю legacy-адаптацию общей реализации.
|
|
143
|
+
|
|
144
|
+
`OtpFlowError.code`: InvalidArguments, InvalidChallenge, InvalidResponse, UnsupportedMethod. Getter `error.cleanup` передаёт обнаруженный refresh даже при повреждённом ответе; он не включён в обычную сериализацию/диагностику ошибки. Валидные незавершённые/Failed ответы передают refresh через result.cleanup. HTTP-ошибки остаются TorrowHttpError, сеть и отмена пробрасываются. Потеря ответа не разрешает слепой повтор. Signal передаётся до fetch на обоих шагах; некорректный/просроченный challenge отклоняется до upstream.
|
|
145
|
+
|
|
146
|
+
Примеры: auth-otp-flow.ts (JSON и новый flow), auth-otp-flow-mobile-id.ts (polling приложения и SMS fallback). Выпуск интеграционного ключа, подтверждение identity и cleanup выполняются приложением.
|
|
147
|
+
|
|
148
|
+
Общая база `{ tokenStorage, deviceInfo }` (+ опционально `appCredentials`) у режимов с сессией.
|
|
149
|
+
Без `appCredentials` — **`DEFAULT_APP_CREDENTIALS`** (`sdk.torrow.net`). Своя пара перекрывает дефолт.
|
|
150
|
+
|
|
151
|
+
### 1. Restore + OTP при необходимости
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { TorrowAuth } from "@torrow/sdk-auth";
|
|
155
|
+
import { FileTokenStorage } from "@torrow/sdk-auth/node";
|
|
156
|
+
import { DeviceTypeValues } from "@torrow/sdk-client";
|
|
157
|
+
|
|
158
|
+
const base = {
|
|
159
|
+
// appCredentials опционален — иначе DEFAULT_APP_CREDENTIALS
|
|
160
|
+
tokenStorage: new FileTokenStorage("./.torrow-session.json"),
|
|
161
|
+
deviceInfo: { deviceId: "pos-terminal-1", deviceType: DeviceTypeValues.Desktop },
|
|
162
|
+
userAgent: "my-app/1.0",
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
let { client, session } = await TorrowAuth.connect(base);
|
|
166
|
+
|
|
167
|
+
if (session.status !== "authenticated") {
|
|
168
|
+
({ client, session } = await TorrowAuth.loginWithOtp({
|
|
169
|
+
...base,
|
|
170
|
+
account: { accountType: "Phone", accountName: "+79001234567" },
|
|
171
|
+
onSendCode: (d) => showHint(d.usedMethod, d.codeLength),
|
|
172
|
+
getCode: (ctx) => promptUser(ctx.codeLength),
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await client.profiles.list();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Чтобы задать свои креды приложения:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
appCredentials: { clientId: CLIENT_ID, clientSecret: CLIENT_SECRET },
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 2. Mobile ID / смена канала / rate-limit
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { OtpCodeRequestLimitExceededError } from "@torrow/sdk-auth";
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await TorrowAuth.loginWithOtp({
|
|
192
|
+
...base,
|
|
193
|
+
account: { accountType: "Phone", accountName: phone },
|
|
194
|
+
supportedMethods: ["MtsMobileId", "Sms"],
|
|
195
|
+
preferredMethod: userPickedMethod,
|
|
196
|
+
onSendCode: (d) => {
|
|
197
|
+
// MtsMobileId → UI «подтвердите push»; getCode не вызовется на happy-path
|
|
198
|
+
renderLoginUi(d);
|
|
199
|
+
},
|
|
200
|
+
getCode: (ctx) => waitCodeInput(ctx),
|
|
201
|
+
});
|
|
202
|
+
} catch (e) {
|
|
203
|
+
if (e instanceof OtpCodeRequestLimitExceededError) {
|
|
204
|
+
showRetryTimer(e.minutes, e.seconds);
|
|
205
|
+
} else {
|
|
206
|
+
throw e;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 3. Telegram deep-link
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const { client, session } = await TorrowAuth.loginWithLink({
|
|
215
|
+
...base,
|
|
216
|
+
onLink: (authUrl) => showQrOrOpen(authUrl),
|
|
217
|
+
signal: abortController.signal,
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 4. Виджет: токены от хоста
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { TorrowAuth, LocalStorageTokenStorage } from "@torrow/sdk-auth";
|
|
225
|
+
|
|
226
|
+
const { client, session } = await TorrowAuth.fromTokens({
|
|
227
|
+
accessToken: hostProvidedAccess,
|
|
228
|
+
refreshToken: hostProvidedRefresh,
|
|
229
|
+
appCredentials: hostProvidedAppCreds,
|
|
230
|
+
tokenStorage: new LocalStorageTokenStorage("torrow.widget.session"),
|
|
231
|
+
deviceInfo: { deviceId: widgetDeviceId(), deviceType: DeviceTypeValues.Web },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
session.onStatusChange((s) => {
|
|
235
|
+
if (s !== "authenticated") widgetUi.showLoginPrompt();
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 5. Аноним и выход
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const publicClient = TorrowAuth.anonymous({ userAgent: "my-widget" });
|
|
243
|
+
|
|
244
|
+
await session.logout(); // revoke refresh + clear storage + onStatusChange("unauthenticated")
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Сигналы UI
|
|
248
|
+
|
|
249
|
+
| Сигнал от SDK | Что делает App |
|
|
250
|
+
|---------------|----------------|
|
|
251
|
+
| **`onSendCode({ usedMethod: Sms, codeLength, … })`** | Форма/подсказка «код в SMS»; длина поля = `codeLength` |
|
|
252
|
+
| **`onSendCode({ usedMethod: TelegramGateway, codeLength })`** | Форма «код в Telegram» (**не** deep-link) |
|
|
253
|
+
| **`onSendCode({ usedMethod: FlashCall, codeLength })`** | Подсказка «цифры из звонка» |
|
|
254
|
+
| **`onSendCode({ accountType: Email, codeLength })`** | Форма «код на email» |
|
|
255
|
+
| **`onSendCode({ usedMethod: MtsMobileId })`** | Ожидание push **без** keypad (`codeLength` часто `0`) |
|
|
256
|
+
| **`getCode({ codeLength })`** | Дождаться ввода; валидировать по `codeLength` (экран уже из `onSendCode`) |
|
|
257
|
+
| **`onLink(authUrl)`** (`loginWithLink`) | Показать QR / открыть Telegram; **код OTP не вводить** |
|
|
258
|
+
| **`session.onStatusChange`** | Реактивный login-state (`authenticated` / `unauthenticated` / `expired`) |
|
|
259
|
+
|
|
260
|
+
## Sequence: OTP (`loginWithOtp`)
|
|
261
|
+
|
|
262
|
+
```mermaid
|
|
263
|
+
sequenceDiagram
|
|
264
|
+
participant User as User
|
|
265
|
+
participant App
|
|
266
|
+
participant SdkAuth as sdk_auth
|
|
267
|
+
participant Auth as client.auth
|
|
268
|
+
|
|
269
|
+
User->>App: телефон или email
|
|
270
|
+
App->>SdkAuth: TorrowAuth.loginWithOtp
|
|
271
|
+
SdkAuth->>Auth: sendCode
|
|
272
|
+
Auth-->>SdkAuth: usedMethod sessionId codeLength
|
|
273
|
+
SdkAuth->>App: onSendCode account usedMethod codeLength
|
|
274
|
+
|
|
275
|
+
alt usedMethod MtsMobileId
|
|
276
|
+
Note over App,User: onSendCode MtsMobileId без keypad
|
|
277
|
+
App->>User: UI ожидание push
|
|
278
|
+
loop пока Pending
|
|
279
|
+
SdkAuth->>Auth: requestToken code empty
|
|
280
|
+
Auth-->>SdkAuth: status Pending
|
|
281
|
+
end
|
|
282
|
+
opt status AwaitingSmsCode
|
|
283
|
+
Note over SdkAuth,App: показать SMS-форму
|
|
284
|
+
SdkAuth->>App: getCode codeLength
|
|
285
|
+
App->>User: форма SMS-кода
|
|
286
|
+
User->>App: ввод SMS-кода
|
|
287
|
+
App-->>SdkAuth: code
|
|
288
|
+
SdkAuth->>Auth: requestToken with code
|
|
289
|
+
end
|
|
290
|
+
else classic Sms FlashCall TelegramGateway Email
|
|
291
|
+
Note over App,User: onSendCode usedMethod в т.ч. TelegramGateway
|
|
292
|
+
App->>User: форма или подсказка под usedMethod
|
|
293
|
+
SdkAuth->>App: getCode codeLength
|
|
294
|
+
User->>App: ввод кода
|
|
295
|
+
App-->>SdkAuth: code
|
|
296
|
+
SdkAuth->>Auth: requestToken with code
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
Auth-->>SdkAuth: TokenReady
|
|
300
|
+
SdkAuth->>SdkAuth: persist TokenStorage
|
|
301
|
+
SdkAuth-->>App: client plus session
|
|
302
|
+
App->>User: вход выполнен
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Sequence: Telegram (`loginWithLink`)
|
|
306
|
+
|
|
307
|
+
```mermaid
|
|
308
|
+
sequenceDiagram
|
|
309
|
+
participant User as User
|
|
310
|
+
participant App
|
|
311
|
+
participant SdkAuth as sdk_auth
|
|
312
|
+
participant Auth as client.auth
|
|
313
|
+
participant Bot as TelegramBot
|
|
314
|
+
|
|
315
|
+
User->>App: войти через Telegram
|
|
316
|
+
App->>SdkAuth: TorrowAuth.loginWithLink
|
|
317
|
+
SdkAuth->>Auth: createSession
|
|
318
|
+
Auth-->>SdkAuth: sessionId code authUrl
|
|
319
|
+
SdkAuth->>App: onLink authUrl
|
|
320
|
+
App->>User: QR или открыть authUrl
|
|
321
|
+
User->>Bot: Start
|
|
322
|
+
Bot->>Auth: VerifyAuthSession
|
|
323
|
+
loop пока не ready
|
|
324
|
+
SdkAuth->>Auth: requestToken sessionId code
|
|
325
|
+
Auth-->>SdkAuth: not ready или TokenReady
|
|
326
|
+
end
|
|
327
|
+
SdkAuth->>SdkAuth: persist TokenStorage
|
|
328
|
+
SdkAuth-->>App: client plus session
|
|
329
|
+
App->>User: вход выполнен
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Sequence: connect / refresh / 401 / logout
|
|
333
|
+
|
|
334
|
+
```mermaid
|
|
335
|
+
sequenceDiagram
|
|
336
|
+
participant User as User
|
|
337
|
+
participant App
|
|
338
|
+
participant SdkAuth as sdk_auth
|
|
339
|
+
participant Api as TorrowAPI
|
|
340
|
+
participant Auth as client.auth
|
|
341
|
+
|
|
342
|
+
App->>SdkAuth: TorrowAuth.connect
|
|
343
|
+
SdkAuth->>SdkAuth: TokenStorage.load
|
|
344
|
+
alt access свежий
|
|
345
|
+
SdkAuth-->>App: client plus session authenticated
|
|
346
|
+
else есть refreshToken
|
|
347
|
+
SdkAuth->>Auth: refresh
|
|
348
|
+
Auth-->>SdkAuth: TokenReady
|
|
349
|
+
SdkAuth->>SdkAuth: persist
|
|
350
|
+
SdkAuth-->>App: client plus session authenticated
|
|
351
|
+
else нет токенов
|
|
352
|
+
SdkAuth-->>App: session unauthenticated
|
|
353
|
+
Note over App: далее loginWithOtp или loginWithLink
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
User->>App: действие API
|
|
357
|
+
App->>Api: Bearer request
|
|
358
|
+
alt 401 и refreshOnUnauthorized
|
|
359
|
+
Api-->>SdkAuth: 401
|
|
360
|
+
SdkAuth->>Auth: refresh
|
|
361
|
+
Auth-->>SdkAuth: TokenReady
|
|
362
|
+
SdkAuth->>Api: retry once
|
|
363
|
+
Api-->>App: 200
|
|
364
|
+
else access истекает по skew
|
|
365
|
+
SdkAuth->>Auth: proactive refresh
|
|
366
|
+
Auth-->>SdkAuth: TokenReady
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
opt logout
|
|
370
|
+
App->>SdkAuth: session.logout
|
|
371
|
+
SdkAuth->>Auth: revoke
|
|
372
|
+
SdkAuth->>SdkAuth: clear TokenStorage
|
|
373
|
+
SdkAuth->>App: onStatusChange unauthenticated
|
|
374
|
+
end
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Sequence: fromTokens + onStatusChange
|
|
378
|
+
|
|
379
|
+
```mermaid
|
|
380
|
+
sequenceDiagram
|
|
381
|
+
participant Host as HostApp
|
|
382
|
+
participant App as Widget
|
|
383
|
+
participant SdkAuth as sdk_auth
|
|
384
|
+
|
|
385
|
+
Host->>App: accessToken refreshToken optional
|
|
386
|
+
App->>SdkAuth: TorrowAuth.fromTokens
|
|
387
|
+
SdkAuth->>SdkAuth: save to TokenStorage
|
|
388
|
+
SdkAuth-->>App: client plus session
|
|
389
|
+
SdkAuth->>App: onStatusChange authenticated
|
|
390
|
+
Note over App,SdkAuth: дальше как connect refresh 401
|
|
391
|
+
opt access истёк без refresh или logout
|
|
392
|
+
SdkAuth->>App: onStatusChange expired or unauthenticated
|
|
393
|
+
App->>Host: запросить новый логин
|
|
394
|
+
end
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Storage
|
|
398
|
+
|
|
399
|
+
| Класс | Назначение |
|
|
400
|
+
|-------|------------|
|
|
401
|
+
| **`MemoryTokenStorage`** | Тесты, эфемерные процессы, без диска |
|
|
402
|
+
| **`FileTokenStorage`** | Node-only (`node:fs`): JSON-файл — **`@torrow/sdk-auth/node`** (канон для `examples/node/**`) |
|
|
403
|
+
| **`LocalStorageTokenStorage`** | Браузер / виджет: `localStorage` |
|
|
404
|
+
| custom **`TokenStorage`** | IndexedDB, SecureStore, … — `load` / `save` снаружи |
|
|
405
|
+
|
|
406
|
+
**Node vs browser:** главный вход `@torrow/sdk-auth` browser-safe. `FileTokenStorage` — только `import { FileTokenStorage } from "@torrow/sdk-auth/node"` (тянет `node:fs`).
|
|
407
|
+
|
|
408
|
+
**JWT vs integration key:** user JWT обычно с `exp` в payload; opaque/integration key — строка без JWT-формы.
|
|
409
|
+
|
|
410
|
+
- **Opaque (non-JWT-shaped):** меньше 3 сегментов через точку (не `a.b.c`); host TTL через `TorrowAuth.fromTokens({ accessExpiresAtMs })` или бессрочно без TTL
|
|
411
|
+
- **JWT-shaped undecodable (`a.b.c`):** `isAccessStillFresh` всегда false → proactive refresh на `open` / `ensureFresh`, пока есть `refreshToken`; после refresh **без** `expiration` merge сбрасывает snapshot TTL → opaque unlimited → дальнейший proactive refresh не идёт (не refresh-storm)
|
|
412
|
+
- **Valid JWT:** runtime freshness AND-ит `exp`; будущий explicit `accessExpiresAtMs` не оживит просроченный JWT
|
|
413
|
+
- **Opaque host TTL в skew-окне** (`accessTokenSkewMs`, default 30s): refresh без `expiration` сбрасывает snapshot TTL → бессрочный opaque до 401 (лучше refresh-storm)
|
|
414
|
+
|
|
415
|
+
Host: non-JWT-shaped opaque **или** валидный JWT с `exp`.
|
|
416
|
+
|
|
417
|
+
Канон examples: `new FileTokenStorage(path)` + `TorrowAuth.connect` / `loginWithOtp` — без ручного dual deviceInfo provider.
|
|
418
|
+
|
|
419
|
+
## API (кратко)
|
|
420
|
+
|
|
421
|
+
| API | Роль |
|
|
422
|
+
|-----|------|
|
|
423
|
+
| **`TorrowAuth.connect`** | restore / refresh → `{ client, session }`; **без** OTP |
|
|
424
|
+
| **`TorrowAuth.loginWithOtp`** | sendCode → Mobile ID / classic → persist |
|
|
425
|
+
| **`TorrowAuth.loginWithLink`** | Telegram deep-link → poll → persist |
|
|
426
|
+
| **`TorrowAuth.fromTokens`** | сид access[/refresh] → connect-семантика |
|
|
427
|
+
| **`TorrowAuth.anonymous`** | `TorrowClient` без Bearer |
|
|
428
|
+
| **`TokenSession`** | `status`, `onStatusChange`, `logout`, `requestTokenWithCode`, `refresh`, `client` |
|
|
429
|
+
| **`refreshOnUnauthorized`** | 401 → refresh → один retry (**default true**) |
|
|
430
|
+
| **`onRefreshFailure`** | optional; политика при ошибке refresh (`keep` / `clear` / `throw`); default: non-HTTP / network → `keep`; HTTP 429/502–504 → `keep`; other HTTP → `clear` |
|
|
431
|
+
|
|
432
|
+
`appCredentials: { clientId, clientSecret }` (опционально; иначе **`DEFAULT_APP_CREDENTIALS`**)
|
|
433
|
+
внутри auth-пакета мапится на **`AuthIdentityBasic`**
|
|
434
|
+
(`username`/`password`) → **`TorrowClientConfig.authIdentityBasic`**. Маппер **не** публичный API:
|
|
435
|
+
приложениям достаточно **`appCredentials`** или дефолта; low-level `TorrowClient` собирает Basic сам
|
|
436
|
+
(см. **`examples/node/auth/auth-send-code.ts`**).
|
|
437
|
+
|
|
438
|
+
**401-retry** вызывает только **refresh**, не OTP.
|