@ocenkamobi/om-oidc-client 0.1.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 +136 -0
- package/dist/axios.d.ts +9 -0
- package/dist/axios.js +13 -0
- package/dist/cache.d.ts +11 -0
- package/dist/cache.js +53 -0
- package/dist/got.d.ts +10 -0
- package/dist/got.js +22 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +46 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# @ocenkamobi/om-oidc-client
|
|
2
|
+
|
|
3
|
+
Клиент сервисных аккаунтов auth.ocenka.mobi для серверов на Node 20+. Делает
|
|
4
|
+
две вещи: аутентифицирует запрос к `/token` через `client_secret_basic` и
|
|
5
|
+
кэширует ответ — одинаковый запрос получает тот же токен, пока тот не начнёт
|
|
6
|
+
истекать.
|
|
7
|
+
|
|
8
|
+
Библиотека отвечает за авторизацию сервисных аккаунтов — про реализацию грантов
|
|
9
|
+
в [auth.ocenka.mobi](https://gitlab.com/ocenkamobi/auth.ocenka.mobi/-/blob/main/docs/README.md)
|
|
10
|
+
ничего не знает.
|
|
11
|
+
|
|
12
|
+
Для CommonJS и Node 12 —
|
|
13
|
+
[`@ocenkamobi/om-oidc-client-cjs`](https://gitlab.com/om-misc/om-oidc-client-cjs).
|
|
14
|
+
|
|
15
|
+
## Подключение
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm i @ocenkamobi/om-oidc-client
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createOmOidcClient } from '@ocenkamobi/om-oidc-client'
|
|
23
|
+
|
|
24
|
+
export const oidc = createOmOidcClient({
|
|
25
|
+
authority: 'https://auth.ocenka.mobi',
|
|
26
|
+
clientId: process.env.CLIENT_ID!,
|
|
27
|
+
clientSecret: process.env.CLIENT_SECRET!,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const { access_token } = await oidc.token({
|
|
31
|
+
grant_type: 'client_credentials',
|
|
32
|
+
resource: 'https://express.ocenka.mobi',
|
|
33
|
+
scope: 'openid profile org express',
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Клиент создаётся один на каждый сервисный аккаунт и держит отдельный кэш.
|
|
38
|
+
|
|
39
|
+
| опция | по умолчанию | значение |
|
|
40
|
+
| -------------- | ------------ | --------------------------------------------------------------- |
|
|
41
|
+
| `authority` | — | сервер авторизации, токены запрашиваются у `${authority}/token` |
|
|
42
|
+
| `clientId` | — | `client_id` сервисного аккаунта |
|
|
43
|
+
| `clientSecret` | — | `client_secret` сервисного аккаунта |
|
|
44
|
+
| `minTtl` | `60` | сколько секунд осталось в токене, чтобы его взяли из кэша |
|
|
45
|
+
| `maxEntries` | `500` | сколько записей помещается в кэш |
|
|
46
|
+
|
|
47
|
+
## Кэш
|
|
48
|
+
|
|
49
|
+
- Ключ — тело запроса. Порядок полей и скоупов в `scope` значения не имеет,
|
|
50
|
+
пустые поля не учитываются.
|
|
51
|
+
- Токен отдаётся из кэша, пока ему осталось жить не меньше `minTtl` секунд.
|
|
52
|
+
Срок берётся из `expires_in`.
|
|
53
|
+
- Одновременные одинаковые запросы делают один сетевой вызов.
|
|
54
|
+
- Из кэша вытесняются токены, которыми не пользуются: каждое чтение токена из
|
|
55
|
+
кэша поднимает его.
|
|
56
|
+
- Ошибки не кэшируются.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
await oidc.token(body, { minTtl: 300 }) // токен нужен на долгую операцию
|
|
60
|
+
await oidc.token(body, { force: true }) // мимо кэша
|
|
61
|
+
oidc.invalidate(body)
|
|
62
|
+
oidc.clear()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Всё тело вызова `await oidc.token(body)` — это ключ для кэша. Все ключи
|
|
66
|
+
хэшируются.
|
|
67
|
+
|
|
68
|
+
[Отложенный токен](https://gitlab.com/ocenkamobi/auth.ocenka.mobi/-/blob/main/docs/deferred-token.md)
|
|
69
|
+
(`requested_token_type`) выдаётся как любой другой ответ и кэшируется на свои
|
|
70
|
+
7 суток, но bearer-токеном не является: плагины его в заголовок не поставят, а
|
|
71
|
+
кинут ошибку. Обменивать его на access-токен нужно самому — обычным запросом.
|
|
72
|
+
|
|
73
|
+
## Ошибки
|
|
74
|
+
|
|
75
|
+
Отказ сервера — `OmOidcTokenError` с полями `status`, `error`,
|
|
76
|
+
`error_description`. Ретраев нет. Запрос к `/token` прерывается через 10 секунд.
|
|
77
|
+
|
|
78
|
+
## axios
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import axios from 'axios'
|
|
82
|
+
import { attachToken } from '@ocenkamobi/om-oidc-client/axios'
|
|
83
|
+
|
|
84
|
+
const api = axios.create()
|
|
85
|
+
attachToken(api, oidc)
|
|
86
|
+
|
|
87
|
+
await api.get('https://express.ocenka.mobi/api/reports', {
|
|
88
|
+
oidc: { grant_type: 'client_credentials', resource: 'https://express.ocenka.mobi' },
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Токен ставится только запросам с `oidc` — без него заголовок не появится.
|
|
93
|
+
|
|
94
|
+
При необходимости параметры можно задать один раз: axios сливает конфиг инстанса
|
|
95
|
+
с конфигом запроса, включая `oidc`.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const express = axios.create({
|
|
99
|
+
oidc: { grant_type: 'client_credentials', resource: 'https://express.ocenka.mobi' },
|
|
100
|
+
})
|
|
101
|
+
attachToken(express, oidc)
|
|
102
|
+
|
|
103
|
+
await express.get('https://express.ocenka.mobi/api/orders') // с токеном
|
|
104
|
+
await express.get('https://express.ocenka.mobi/api/health', { oidc: false }) // без токена
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`attachToken` возвращает функцию, снимающую интерцептор.
|
|
108
|
+
|
|
109
|
+
## got
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import got from 'got'
|
|
113
|
+
import { tokenHooks, withToken } from '@ocenkamobi/om-oidc-client/got'
|
|
114
|
+
|
|
115
|
+
const api = got.extend(tokenHooks(oidc))
|
|
116
|
+
|
|
117
|
+
await api.get('https://express.ocenka.mobi/api/reports', withToken({
|
|
118
|
+
grant_type: 'client_credentials',
|
|
119
|
+
resource: 'https://express.ocenka.mobi',
|
|
120
|
+
}))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`withToken()` кладёт параметры в `context` запроса. Без него токен не ставится.
|
|
124
|
+
|
|
125
|
+
Дефолт для инстанса задаётся тем же `withToken()` вторым аргументом `extend`, а
|
|
126
|
+
снимается `withToken(false)`:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const express = got.extend(
|
|
130
|
+
tokenHooks(oidc),
|
|
131
|
+
withToken({ grant_type: 'client_credentials', resource: 'https://express.ocenka.mobi' }),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
await express.get('https://express.ocenka.mobi/api/orders')
|
|
135
|
+
await express.get('https://express.ocenka.mobi/api/health', withToken(false))
|
|
136
|
+
```
|
package/dist/axios.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AxiosInstance } from 'axios';
|
|
2
|
+
import type { OmOidcClient, OmOidcTokenRequest } from './index.js';
|
|
3
|
+
declare module 'axios' {
|
|
4
|
+
interface AxiosRequestConfig {
|
|
5
|
+
/** Параметры запроса токена: с ними запрос уходит с `Authorization: Bearer …` */
|
|
6
|
+
oidc?: OmOidcTokenRequest | false;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export declare function attachToken(instance: AxiosInstance, client: OmOidcClient): () => void;
|
package/dist/axios.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function attachToken(instance, client) {
|
|
2
|
+
const id = instance.interceptors.request.use(async (config) => {
|
|
3
|
+
if (config.oidc) {
|
|
4
|
+
const { access_token, token_type } = await client.token(config.oidc);
|
|
5
|
+
if (token_type !== 'Bearer')
|
|
6
|
+
throw new Error(`Not Bearer token, token_type: ${token_type}`);
|
|
7
|
+
config.headers.set('Authorization', `Bearer ${access_token}`);
|
|
8
|
+
}
|
|
9
|
+
return config;
|
|
10
|
+
});
|
|
11
|
+
/** Возвращает функцию, убирающую интерцептор */
|
|
12
|
+
return () => instance.interceptors.request.eject(id);
|
|
13
|
+
}
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface TokenCache<T> {
|
|
2
|
+
key(request: Record<string, string | undefined>): string;
|
|
3
|
+
get(key: string, minTtl: number): T | undefined;
|
|
4
|
+
issue(key: string, load: () => Promise<T>): Promise<T>;
|
|
5
|
+
delete(key: string): void;
|
|
6
|
+
clear(): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function createTokenCache<T extends {
|
|
9
|
+
expires_in: number;
|
|
10
|
+
}>(maxEntries?: number): TokenCache<T>;
|
|
11
|
+
export {};
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const DEFAULT_MAX_ENTRIES = 500;
|
|
3
|
+
export function createTokenCache(maxEntries = DEFAULT_MAX_ENTRIES) {
|
|
4
|
+
const entries = new Map();
|
|
5
|
+
const pending = new Map();
|
|
6
|
+
return {
|
|
7
|
+
key(request) {
|
|
8
|
+
const canonical = Object.entries(request)
|
|
9
|
+
.filter((entry) => !!entry[1])
|
|
10
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
11
|
+
.map(([name, value]) => [name, name === 'scope' ? value.split(/\s+/).filter(Boolean).sort().join(' ') : value]);
|
|
12
|
+
return createHash('sha256').update(JSON.stringify(canonical)).digest('hex');
|
|
13
|
+
},
|
|
14
|
+
get(key, minTtl) {
|
|
15
|
+
const entry = entries.get(key);
|
|
16
|
+
if (!entry)
|
|
17
|
+
return undefined;
|
|
18
|
+
const remaining = entry.expiresAt - Date.now();
|
|
19
|
+
if (remaining <= 0) {
|
|
20
|
+
entries.delete(key);
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
if (remaining < minTtl * 1000)
|
|
24
|
+
return undefined;
|
|
25
|
+
// Обращение возвращает запись в конец очереди
|
|
26
|
+
entries.delete(key);
|
|
27
|
+
entries.set(key, entry);
|
|
28
|
+
return entry.value;
|
|
29
|
+
},
|
|
30
|
+
issue(key, load) {
|
|
31
|
+
let promise = pending.get(key);
|
|
32
|
+
if (!promise) {
|
|
33
|
+
promise = load()
|
|
34
|
+
.then(value => {
|
|
35
|
+
entries.delete(key);
|
|
36
|
+
if (entries.size >= maxEntries)
|
|
37
|
+
entries.delete(entries.keys().next().value);
|
|
38
|
+
entries.set(key, { value, expiresAt: Date.now() + value.expires_in * 1000 });
|
|
39
|
+
return value;
|
|
40
|
+
})
|
|
41
|
+
.finally(() => pending.delete(key));
|
|
42
|
+
pending.set(key, promise);
|
|
43
|
+
}
|
|
44
|
+
return promise;
|
|
45
|
+
},
|
|
46
|
+
delete(key) {
|
|
47
|
+
entries.delete(key);
|
|
48
|
+
},
|
|
49
|
+
clear() {
|
|
50
|
+
entries.clear();
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
package/dist/got.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ExtendOptions } from 'got';
|
|
2
|
+
import type { OmOidcClient, OmOidcTokenRequest } from './index.js';
|
|
3
|
+
/** Хуки для `got.extend()` */
|
|
4
|
+
export declare function tokenHooks(client: OmOidcClient): ExtendOptions;
|
|
5
|
+
/** Опции запроса got с параметрами токена */
|
|
6
|
+
export declare function withToken(request: OmOidcTokenRequest | false): {
|
|
7
|
+
context: {
|
|
8
|
+
oidc: OmOidcTokenRequest | false;
|
|
9
|
+
};
|
|
10
|
+
};
|
package/dist/got.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Хуки для `got.extend()` */
|
|
2
|
+
export function tokenHooks(client) {
|
|
3
|
+
return {
|
|
4
|
+
hooks: {
|
|
5
|
+
beforeRequest: [
|
|
6
|
+
async (options) => {
|
|
7
|
+
const request = options.context.oidc;
|
|
8
|
+
if (request) {
|
|
9
|
+
const { access_token, token_type } = await client.token(request);
|
|
10
|
+
if (token_type !== 'Bearer')
|
|
11
|
+
throw new Error(`Not Bearer token, token_type: ${token_type}`);
|
|
12
|
+
options.headers.authorization = `Bearer ${access_token}`;
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Опции запроса got с параметрами токена */
|
|
20
|
+
export function withToken(request) {
|
|
21
|
+
return { context: { oidc: request } };
|
|
22
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { LiteralUnion } from 'type-fest';
|
|
2
|
+
export interface OmOidcClientOptions {
|
|
3
|
+
/** Сервер авторизации, токены запрашиваются у `${authority}/token` */
|
|
4
|
+
authority: string;
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
/** Минимальное время жизни токена для использования из кэша, в секундах. По умолчанию 60 */
|
|
8
|
+
minTtl?: number;
|
|
9
|
+
/** Максимально количество записей в кэше. По умолчанию 500 */
|
|
10
|
+
maxEntries?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Гранты сервисных аккаунтов auth.ocenka.mobi; значение свободное — сервер знает и другие */
|
|
13
|
+
export type OmOidcGrantType = LiteralUnion<'client_credentials' | 'urn:ietf:params:oauth:grant-type:token-exchange' | 'urn:ocenkamobi:params:oauth:grant-type:client-credentials-act' | 'urn:ocenkamobi:params:oauth:grant-type:impersonation', string>;
|
|
14
|
+
/** Идентификаторы типов токенов по RFC 8693 §3; значение свободное */
|
|
15
|
+
export type OmOidcTokenType = LiteralUnion<'urn:ietf:params:oauth:token-type:access_token' | 'urn:ocenkamobi:params:oauth:token-type:deferred_token', string>;
|
|
16
|
+
/** Тело запроса к `/token` как есть: грант и параметры задаёт вызывающий */
|
|
17
|
+
export interface OmOidcTokenRequest {
|
|
18
|
+
grant_type: OmOidcGrantType;
|
|
19
|
+
resource?: string;
|
|
20
|
+
audience?: string;
|
|
21
|
+
scope?: string;
|
|
22
|
+
subject?: string;
|
|
23
|
+
subject_token?: string;
|
|
24
|
+
subject_token_type?: OmOidcTokenType;
|
|
25
|
+
actor_token?: string;
|
|
26
|
+
actor_token_type?: OmOidcTokenType;
|
|
27
|
+
requested_token_type?: OmOidcTokenType;
|
|
28
|
+
[param: string]: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
export interface OmOidcTokenResponse {
|
|
31
|
+
access_token: string;
|
|
32
|
+
token_type: string;
|
|
33
|
+
expires_in: number;
|
|
34
|
+
scope?: string;
|
|
35
|
+
issued_token_type?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface OmOidcTokenOptions {
|
|
38
|
+
/** Переопределяет `minTtl` клиента для этого вызова */
|
|
39
|
+
minTtl?: number;
|
|
40
|
+
/** Запросить новый токен, минуя кэш */
|
|
41
|
+
force?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface OmOidcClient {
|
|
44
|
+
token(request: OmOidcTokenRequest, options?: OmOidcTokenOptions): Promise<OmOidcTokenResponse>;
|
|
45
|
+
invalidate(request: OmOidcTokenRequest): void;
|
|
46
|
+
clear(): void;
|
|
47
|
+
}
|
|
48
|
+
export declare class OmOidcTokenError extends Error {
|
|
49
|
+
readonly status: number;
|
|
50
|
+
readonly error: string;
|
|
51
|
+
readonly error_description?: string | undefined;
|
|
52
|
+
constructor(status: number, error: string, error_description?: string | undefined);
|
|
53
|
+
}
|
|
54
|
+
export declare function createOmOidcClient({ authority, clientId, clientSecret, minTtl, maxEntries }: OmOidcClientOptions): OmOidcClient;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createTokenCache } from './cache.js';
|
|
2
|
+
export class OmOidcTokenError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
error;
|
|
5
|
+
error_description;
|
|
6
|
+
constructor(status, error, error_description) {
|
|
7
|
+
super(error_description ? `${error}: ${error_description}` : error);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.error = error;
|
|
10
|
+
this.error_description = error_description;
|
|
11
|
+
this.name = 'OmOidcTokenError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const TIMEOUT = 10_000;
|
|
15
|
+
export function createOmOidcClient({ authority, clientId, clientSecret, minTtl = 60, maxEntries }) {
|
|
16
|
+
const endpoint = `${authority.replace(/\/+$/, '')}/token`;
|
|
17
|
+
// RFC 6749 §2.3.1: части кодируются как form-urlencoded до base64
|
|
18
|
+
const credentials = Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`);
|
|
19
|
+
const headers = {
|
|
20
|
+
authorization: `Basic ${credentials.toString('base64')}`,
|
|
21
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
22
|
+
};
|
|
23
|
+
const cache = createTokenCache(maxEntries);
|
|
24
|
+
async function request(req) {
|
|
25
|
+
const body = new URLSearchParams(Object.entries(req).filter((entry) => !!entry[1]));
|
|
26
|
+
const res = await fetch(endpoint, { method: 'POST', headers, body, signal: AbortSignal.timeout(TIMEOUT) });
|
|
27
|
+
const data = (await res.json().catch(() => ({})));
|
|
28
|
+
if (!res.ok || !data.access_token) {
|
|
29
|
+
throw new OmOidcTokenError(res.status, data.error ?? 'invalid_response', data.error_description);
|
|
30
|
+
}
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
async token(req, options = {}) {
|
|
35
|
+
const key = cache.key(req);
|
|
36
|
+
const cached = options.force ? undefined : cache.get(key, options.minTtl ?? minTtl);
|
|
37
|
+
return cached ?? cache.issue(key, () => request(req));
|
|
38
|
+
},
|
|
39
|
+
invalidate(req) {
|
|
40
|
+
cache.delete(cache.key(req));
|
|
41
|
+
},
|
|
42
|
+
clear() {
|
|
43
|
+
cache.clear();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ocenkamobi/om-oidc-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Клиент сервисных аккаунтов auth.ocenka.mobi с кэшем токенов",
|
|
5
|
+
"homepage": "https://gitlab.com/om-misc/om-oidc-client#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://gitlab.com/om-misc/om-oidc-client/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+ssh://git@gitlab.com/om-misc/om-oidc-client.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "UNLICENSED",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "@isjs",
|
|
16
|
+
"email": "stas@isjs.ru"
|
|
17
|
+
},
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"type": "module",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./axios": {
|
|
29
|
+
"types": "./dist/axios.d.ts",
|
|
30
|
+
"import": "./dist/axios.js"
|
|
31
|
+
},
|
|
32
|
+
"./got": {
|
|
33
|
+
"types": "./dist/got.d.ts",
|
|
34
|
+
"import": "./dist/got.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"ts": "tsc --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"axios": "^1",
|
|
47
|
+
"got": ">=12"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"axios": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"got": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"packageManager": "pnpm@11.11.0",
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^22.20.2",
|
|
60
|
+
"axios": "^1.20.0",
|
|
61
|
+
"got": "^14.6.6",
|
|
62
|
+
"typescript": "^6.0.3",
|
|
63
|
+
"vitest": "^4.1.11"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"type-fest": "^5.9.0"
|
|
67
|
+
}
|
|
68
|
+
}
|