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/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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vpndetection",
3
- "version": "5.0.0",
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.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"