vpndetection 5.0.0 → 5.2.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 +38 -5
- package/dist/client.d.ts +26 -8
- package/dist/client.js +95 -87
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +37 -0
- package/dist/errors.js +58 -0
- package/dist/errors.js.map +1 -1
- package/dist/generated/index.d.ts +2 -2
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/sdk.gen.d.ts +24 -1
- package/dist/generated/sdk.gen.js +39 -0
- package/dist/generated/sdk.gen.js.map +1 -1
- package/dist/generated/types.gen.d.ts +168 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/oauth.d.ts +118 -0
- package/dist/oauth.js +240 -0
- package/dist/oauth.js.map +1 -0
- package/dist/transport.d.ts +18 -0
- package/dist/transport.js +82 -0
- package/dist/transport.js.map +1 -0
- package/package.json +2 -2
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { oauthDeviceAuthorization, oauthMetadata, oauthRevoke, oauthToken, } from './generated/sdk.gen.js';
|
|
2
|
+
import { errorFromResponse, OauthError, oauthErrorFrom, OauthExpiredTokenError, VPNDetectionError, } from './errors.js';
|
|
3
|
+
import { asError, deadline, withRetry } from './transport.js';
|
|
4
|
+
/**
|
|
5
|
+
* Signs a person in on their own machine with the OAuth device flow, so a
|
|
6
|
+
* program can be handed one of their API keys instead of asking them to paste
|
|
7
|
+
* it. Reached through `client.oauth`.
|
|
8
|
+
*
|
|
9
|
+
* Every call takes a client ID, issued on request from support@vpndetection.io.
|
|
10
|
+
* None of these requests carries the client's API key, and none needs one.
|
|
11
|
+
*/
|
|
12
|
+
export class OauthApi {
|
|
13
|
+
client;
|
|
14
|
+
retries;
|
|
15
|
+
timeoutMs;
|
|
16
|
+
// The poll's wait and its deadline, which tests replace together.
|
|
17
|
+
clock = { now: () => performance.now(), sleep: sleep };
|
|
18
|
+
constructor(client, retries, timeoutMs) {
|
|
19
|
+
this.client = client;
|
|
20
|
+
this.retries = retries;
|
|
21
|
+
this.timeoutMs = timeoutMs;
|
|
22
|
+
}
|
|
23
|
+
async metadata(options = {}) {
|
|
24
|
+
return withRetry(this.retries, async () => {
|
|
25
|
+
const res = await deadline(options.timeoutMs ?? this.timeoutMs, (signal) => oauthMetadata({
|
|
26
|
+
client: this.client, signal: signal,
|
|
27
|
+
}));
|
|
28
|
+
return decode(res, METADATA);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Start a device sign-in: show the person `user_code` and `verification_uri`,
|
|
33
|
+
* then hand the answer to `pollDeviceToken`. It consumes nothing, so it is
|
|
34
|
+
* retried like a lookup; a refusal such as `slow_down` is an `OauthError`.
|
|
35
|
+
*/
|
|
36
|
+
async deviceAuthorization(clientId, options = {}) {
|
|
37
|
+
const body = {
|
|
38
|
+
client_id: clientId,
|
|
39
|
+
scope: options.scope || undefined,
|
|
40
|
+
resource: options.resource || undefined,
|
|
41
|
+
};
|
|
42
|
+
const timeoutMs = options.timeoutMs ?? this.timeoutMs;
|
|
43
|
+
return withRetry(this.retries, async () => {
|
|
44
|
+
const res = await deadline(timeoutMs, (signal) => oauthDeviceAuthorization({
|
|
45
|
+
client: this.client, body: body, signal: signal,
|
|
46
|
+
}));
|
|
47
|
+
return decode(res, DEVICE_AUTHORIZATION);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Ask once whether the person has approved a device sign-in. Until they do,
|
|
52
|
+
* it rejects with an `OauthError` coded `authorization_pending`;
|
|
53
|
+
* `pollDeviceToken` is the loop around it.
|
|
54
|
+
*
|
|
55
|
+
* Never retried: an approved code is spent by the answer carrying the tokens,
|
|
56
|
+
* so a retry after a lost response could only lose them.
|
|
57
|
+
*/
|
|
58
|
+
async exchangeDeviceCode(clientId, deviceCode, options = {}) {
|
|
59
|
+
return this.exchange({
|
|
60
|
+
grant_type: DEVICE_CODE_GRANT, device_code: deviceCode, client_id: clientId,
|
|
61
|
+
}, options.timeoutMs);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Trade a refresh token for a new pair. The old one is spent whatever happens
|
|
65
|
+
* next, so this is never retried. A refresh names the key the person picked
|
|
66
|
+
* (`apikey_id`) but never reveals it again (`apikey`).
|
|
67
|
+
*/
|
|
68
|
+
async exchangeRefreshToken(clientId, refreshToken, options = {}) {
|
|
69
|
+
return this.exchange({
|
|
70
|
+
grant_type: 'refresh_token', refresh_token: refreshToken, client_id: clientId,
|
|
71
|
+
}, options.timeoutMs);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* End a token. A refresh token ends the whole sign-in and every token it
|
|
75
|
+
* issued, which is how a program signs the machine out; an access token ends
|
|
76
|
+
* only itself. The server answers the same for any token, known or not.
|
|
77
|
+
*/
|
|
78
|
+
async revoke(clientId, token, options = {}) {
|
|
79
|
+
await withRetry(this.retries, async () => {
|
|
80
|
+
const res = await deadline(options.timeoutMs ?? this.timeoutMs, (signal) => oauthRevoke({
|
|
81
|
+
client: this.client,
|
|
82
|
+
body: { token: token, client_id: clientId },
|
|
83
|
+
// The answer says nothing, so it is read as text and dropped rather than parsed.
|
|
84
|
+
parseAs: 'text',
|
|
85
|
+
signal: signal,
|
|
86
|
+
}));
|
|
87
|
+
throwIfFailed(res);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Wait for the person to approve a device sign-in, and return its tokens.
|
|
92
|
+
*
|
|
93
|
+
* Waits `device.interval` seconds (5 when that is below 1) before EVERY
|
|
94
|
+
* request, the first included, and 5 more for the rest of the call each time
|
|
95
|
+
* the server answers `slow_down`. Ends at the first answer that is neither:
|
|
96
|
+
* a denial rejects with `OauthAccessDeniedError`, a code that ran out with
|
|
97
|
+
* `OauthExpiredTokenError` - as does outliving `device.expires_in`, counted
|
|
98
|
+
* from this call, with no `status` - and any other failure as it came.
|
|
99
|
+
*/
|
|
100
|
+
async pollDeviceToken(clientId, device, options = {}) {
|
|
101
|
+
let interval = device.interval >= 1 ? device.interval : 5;
|
|
102
|
+
const expires = this.clock.now() + device.expires_in * 1000;
|
|
103
|
+
for (;;) {
|
|
104
|
+
await this.clock.sleep(interval * 1000, options.signal);
|
|
105
|
+
if (this.clock.now() >= expires) {
|
|
106
|
+
throw new OauthExpiredTokenError();
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return await this.exchange({
|
|
110
|
+
grant_type: DEVICE_CODE_GRANT, device_code: device.device_code, client_id: clientId,
|
|
111
|
+
}, options.timeoutMs, options.signal);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (!(err instanceof OauthError)) {
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
// RFC 8628: slow_down widens the interval for every later request, not just the next.
|
|
118
|
+
if (err.errorCode === 'slow_down') {
|
|
119
|
+
interval += 5;
|
|
120
|
+
}
|
|
121
|
+
else if (err.errorCode !== 'authorization_pending') {
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async exchange(form, timeoutMs, cancel) {
|
|
128
|
+
const res = await deadline(timeoutMs ?? this.timeoutMs, (signal) => oauthToken({
|
|
129
|
+
client: this.client, body: form, signal: signal,
|
|
130
|
+
}), cancel);
|
|
131
|
+
return decode(res, TOKEN_RESPONSE);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const DEVICE_CODE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
135
|
+
const METADATA = {
|
|
136
|
+
issuer: { type: 'string', required: true },
|
|
137
|
+
authorization_endpoint: { type: 'string', required: true },
|
|
138
|
+
token_endpoint: { type: 'string', required: true },
|
|
139
|
+
device_authorization_endpoint: { type: 'string' },
|
|
140
|
+
revocation_endpoint: { type: 'string' },
|
|
141
|
+
scopes_supported: { type: 'string[]' },
|
|
142
|
+
response_types_supported: { type: 'string[]' },
|
|
143
|
+
grant_types_supported: { type: 'string[]' },
|
|
144
|
+
code_challenge_methods_supported: { type: 'string[]' },
|
|
145
|
+
token_endpoint_auth_methods_supported: { type: 'string[]' },
|
|
146
|
+
authorization_response_iss_parameter_supported: { type: 'boolean' },
|
|
147
|
+
service_documentation: { type: 'string' },
|
|
148
|
+
};
|
|
149
|
+
const DEVICE_AUTHORIZATION = {
|
|
150
|
+
device_code: { type: 'string', required: true },
|
|
151
|
+
user_code: { type: 'string', required: true },
|
|
152
|
+
verification_uri: { type: 'string', required: true },
|
|
153
|
+
verification_uri_complete: { type: 'string' },
|
|
154
|
+
expires_in: { type: 'number', required: true },
|
|
155
|
+
interval: { type: 'number', required: true },
|
|
156
|
+
};
|
|
157
|
+
const TOKEN_RESPONSE = {
|
|
158
|
+
access_token: { type: 'string', required: true },
|
|
159
|
+
token_type: { type: 'string', required: true },
|
|
160
|
+
expires_in: { type: 'number', required: true },
|
|
161
|
+
refresh_token: { type: 'string' },
|
|
162
|
+
scope: { type: 'string' },
|
|
163
|
+
apikey_id: { type: 'string', wire: 'mslm:apikey_id' },
|
|
164
|
+
apikey: { type: 'string', wire: 'mslm:apikey' },
|
|
165
|
+
};
|
|
166
|
+
// Only the declared members are copied, on PRESENCE, so an absent one stays
|
|
167
|
+
// absent and an empty `scope` stays present. Anything else the server sends is
|
|
168
|
+
// dropped rather than surfaced untyped.
|
|
169
|
+
function decode(res, members) {
|
|
170
|
+
const status = throwIfFailed(res).status;
|
|
171
|
+
if (res.error !== undefined) {
|
|
172
|
+
// A 2xx the generated client could not parse, which reports it here.
|
|
173
|
+
throw asError(res.error);
|
|
174
|
+
}
|
|
175
|
+
const body = res.data;
|
|
176
|
+
if (!isObject(body)) {
|
|
177
|
+
throw new VPNDetectionError('server_error', 'the answer was not a JSON object', status);
|
|
178
|
+
}
|
|
179
|
+
const out = {};
|
|
180
|
+
for (const [name, member] of Object.entries(members)) {
|
|
181
|
+
const wire = member.wire ?? name;
|
|
182
|
+
const value = body[wire];
|
|
183
|
+
if (value === undefined || value === null) {
|
|
184
|
+
if (member.required) {
|
|
185
|
+
throw new VPNDetectionError('server_error', `the answer carried no ${wire}`, status);
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!hasType(value, member.type)) {
|
|
190
|
+
throw new VPNDetectionError('server_error', `the answer's ${wire} is not a ${member.type}`, status);
|
|
191
|
+
}
|
|
192
|
+
out[name] = value;
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
// Only a 4xx whose body is a JSON object with a STRING `error` is an OAuth
|
|
197
|
+
// refusal. Every 5xx, whatever it says, is the server failing, and is retried
|
|
198
|
+
// wherever the operation retries.
|
|
199
|
+
function throwIfFailed(res) {
|
|
200
|
+
if (res.response === undefined) {
|
|
201
|
+
throw new VPNDetectionError('network', 'no response from the API');
|
|
202
|
+
}
|
|
203
|
+
const { status, ok, headers } = res.response;
|
|
204
|
+
if (ok) {
|
|
205
|
+
return res.response;
|
|
206
|
+
}
|
|
207
|
+
const body = res.error;
|
|
208
|
+
if (status >= 400 && status < 500 && isObject(body) && typeof body.error === 'string') {
|
|
209
|
+
const description = typeof body.error_description === 'string' ? body.error_description : undefined;
|
|
210
|
+
throw oauthErrorFrom(body.error, description, status);
|
|
211
|
+
}
|
|
212
|
+
throw errorFromResponse(status, headers, body);
|
|
213
|
+
}
|
|
214
|
+
function isObject(value) {
|
|
215
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
216
|
+
}
|
|
217
|
+
function hasType(value, type) {
|
|
218
|
+
if (type === 'string[]') {
|
|
219
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
220
|
+
}
|
|
221
|
+
return typeof value === type;
|
|
222
|
+
}
|
|
223
|
+
function sleep(ms, signal) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
if (signal?.aborted) {
|
|
226
|
+
reject(signal.reason);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const onAbort = () => {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
reject(signal?.reason);
|
|
232
|
+
};
|
|
233
|
+
const timer = setTimeout(() => {
|
|
234
|
+
signal?.removeEventListener('abort', onAbort);
|
|
235
|
+
resolve();
|
|
236
|
+
}, ms);
|
|
237
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { VPNDetectionError } from './errors.js';
|
|
2
|
+
export interface Res {
|
|
3
|
+
data?: unknown;
|
|
4
|
+
error?: unknown;
|
|
5
|
+
response?: Response;
|
|
6
|
+
}
|
|
7
|
+
export declare function unwrap<T>(res: Res): T;
|
|
8
|
+
/**
|
|
9
|
+
* Bound one attempt, and report an expiry as our own error rather than the
|
|
10
|
+
* runtime's `TimeoutError`, whose message says nothing about which call gave up.
|
|
11
|
+
*
|
|
12
|
+
* The signal is built per call, so a retried request gets a fresh budget - the
|
|
13
|
+
* same per-attempt semantics the Go and Python clients have. Aborting `cancel`
|
|
14
|
+
* ends the attempt at once, rejecting with its reason.
|
|
15
|
+
*/
|
|
16
|
+
export declare function deadline<T>(timeoutMs: number, fn: (signal: AbortSignal) => Promise<T>, cancel?: AbortSignal): Promise<T>;
|
|
17
|
+
export declare function withRetry<T>(retries: number, fn: () => Promise<T>): Promise<T>;
|
|
18
|
+
export declare function asError(err: unknown): VPNDetectionError;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import pRetry from 'p-retry';
|
|
2
|
+
import { errorFromResponse, VPNDetectionError } from './errors.js';
|
|
3
|
+
export function unwrap(res) {
|
|
4
|
+
if (res.response === undefined) {
|
|
5
|
+
throw new VPNDetectionError('network', 'no response from the API');
|
|
6
|
+
}
|
|
7
|
+
if (!res.response.ok) {
|
|
8
|
+
throw errorFromResponse(res.response.status, res.response.headers, res.error ?? res.data);
|
|
9
|
+
}
|
|
10
|
+
return res.data;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Bound one attempt, and report an expiry as our own error rather than the
|
|
14
|
+
* runtime's `TimeoutError`, whose message says nothing about which call gave up.
|
|
15
|
+
*
|
|
16
|
+
* The signal is built per call, so a retried request gets a fresh budget - the
|
|
17
|
+
* same per-attempt semantics the Go and Python clients have. Aborting `cancel`
|
|
18
|
+
* ends the attempt at once, rejecting with its reason.
|
|
19
|
+
*/
|
|
20
|
+
export async function deadline(timeoutMs, fn, cancel) {
|
|
21
|
+
cancel?.throwIfAborted();
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
let timer;
|
|
24
|
+
let stop;
|
|
25
|
+
// Raced, not left to the signal alone: aborting releases the socket, but only
|
|
26
|
+
// a transport that HONORS the signal then settles, and a substituted `fetch`
|
|
27
|
+
// need not. Clearing the timer stops the losing side rejecting into nothing.
|
|
28
|
+
const expiry = new Promise((_, reject) => {
|
|
29
|
+
timer = setTimeout(() => {
|
|
30
|
+
controller.abort();
|
|
31
|
+
reject(new VPNDetectionError('network', `request timed out after ${timeoutMs}ms`));
|
|
32
|
+
}, timeoutMs);
|
|
33
|
+
if (cancel !== undefined) {
|
|
34
|
+
stop = () => {
|
|
35
|
+
controller.abort(cancel.reason);
|
|
36
|
+
reject(cancel.reason);
|
|
37
|
+
};
|
|
38
|
+
cancel.addEventListener('abort', stop, { once: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
return await Promise.race([fn(controller.signal), expiry]);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
if (stop !== undefined) {
|
|
47
|
+
cancel?.removeEventListener('abort', stop);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// p-retry owns the backoff schedule; the extra sleep here is what honors a
|
|
52
|
+
// server-supplied Retry-After, which p-retry has no way to know about. A 429
|
|
53
|
+
// carrying that header is the only 429 worth retrying, which is why the wait
|
|
54
|
+
// and the retry decision both key off the same field.
|
|
55
|
+
export async function withRetry(retries, fn) {
|
|
56
|
+
try {
|
|
57
|
+
return await pRetry(fn, {
|
|
58
|
+
retries: retries,
|
|
59
|
+
shouldRetry: ({ error }) => !(error instanceof VPNDetectionError) || error.retryable,
|
|
60
|
+
onFailedAttempt: async ({ error }) => {
|
|
61
|
+
const seconds = error instanceof VPNDetectionError ? error.retryAfterSeconds : undefined;
|
|
62
|
+
if (seconds !== undefined && seconds > 0) {
|
|
63
|
+
await new Promise((r) => setTimeout(r, seconds * 1000));
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
throw asError(err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function asError(err) {
|
|
73
|
+
if (err instanceof VPNDetectionError) {
|
|
74
|
+
return err;
|
|
75
|
+
}
|
|
76
|
+
const cause = err?.cause;
|
|
77
|
+
if (cause instanceof VPNDetectionError) {
|
|
78
|
+
return cause;
|
|
79
|
+
}
|
|
80
|
+
return new VPNDetectionError('network', err instanceof Error ? err.message : String(err));
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAMnE,MAAM,UAAU,MAAM,CAAI,GAAQ;IAC9B,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,iBAAiB,CAAC,SAAS,EAAE,0BAA0B,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACnB,MAAM,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9F,CAAC;IACD,OAAO,GAAG,CAAC,IAAS,CAAC;AACzB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC1B,SAAiB,EAAE,EAAuC,EAAE,MAAoB;IAEhF,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,IAAI,KAAoC,CAAC;IACzC,IAAI,IAA8B,CAAC;IACnC,8EAA8E;IAC9E,6EAA6E;IAC7E,6EAA6E;IAC7E,MAAM,MAAM,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;QAC5C,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACpB,UAAU,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,CAAC,IAAI,iBAAiB,CAAC,SAAS,EAAE,2BAA2B,SAAS,IAAI,CAAC,CAAC,CAAC;QACvF,CAAC,EAAE,SAAS,CAAC,CAAC;QACd,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACvB,IAAI,GAAG,GAAG,EAAE;gBACR,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;gBAChC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC1B,CAAC,CAAC;YACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3D,CAAC;IACL,CAAC,CAAC,CAAC;IACH,IAAI,CAAC;QACD,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/D,CAAC;YAAS,CAAC;QACP,YAAY,CAAC,KAAM,CAAC,CAAC;QACrB,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC/C,CAAC;IACL,CAAC;AACL,CAAC;AAED,2EAA2E;AAC3E,6EAA6E;AAC7E,6EAA6E;AAC7E,sDAAsD;AACtD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAI,OAAe,EAAE,EAAoB;IACpE,IAAI,CAAC;QACD,OAAO,MAAM,MAAM,CAAC,EAAE,EAAE;YACpB,OAAO,EAAE,OAAO;YAChB,WAAW,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,YAAY,iBAAiB,CAAC,IAAI,KAAK,CAAC,SAAS;YACpF,eAAe,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;gBACjC,MAAM,OAAO,GAAG,KAAK,YAAY,iBAAiB,CAAC,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC,SAAS,CAAC;gBACzF,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;oBACvC,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC;gBAC5D,CAAC;YACL,CAAC;SACJ,CAAC,CAAC;IACP,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACX,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC;AACL,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,GAAY;IAChC,IAAI,GAAG,YAAY,iBAAiB,EAAE,CAAC;QACnC,OAAO,GAAG,CAAC;IACf,CAAC;IACD,MAAM,KAAK,GAAI,GAA2B,EAAE,KAAK,CAAC;IAClD,IAAI,KAAK,YAAY,iBAAiB,EAAE,CAAC;QACrC,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,OAAO,IAAI,iBAAiB,CAAC,SAAS,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AAC9F,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vpndetection",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "Official Node.js client library for the VPNDetection API. Detect VPNs, proxies, Tor, hosting and CDN IPs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@hey-api/openapi-ts": "^0.99.0",
|
|
58
|
-
"@mslmio/eslint-config": "^1.0.
|
|
58
|
+
"@mslmio/eslint-config": "^1.0.1",
|
|
59
59
|
"@types/node": "^26.2.0",
|
|
60
60
|
"eslint": "^9.39.0",
|
|
61
61
|
"typescript": "^5.9.3"
|