@quiltt/core 3.5.5 → 3.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/CHANGELOG.md +24 -10
- package/README.md +1 -1
- package/dist/{SubscriptionLink-client-B5Tmyqw7.js → SubscriptionLink-client-Bf6n9gzT.js} +70 -61
- package/dist/{index-client-BY-d8Msy.js → index-client-C87sSU05.js} +1 -1
- package/dist/index.d.ts +190 -179
- package/dist/index.js +122 -86
- package/package.json +13 -14
- package/src/api/graphql/links/ActionCableLink.ts +1 -1
- package/src/api/graphql/links/AuthLink.ts +1 -1
- package/src/api/graphql/links/BatchHttpLink.ts +4 -2
- package/src/api/graphql/links/ErrorLink.ts +1 -1
- package/src/api/graphql/links/HttpLink.ts +4 -2
- package/src/api/rest/{AuthAPI.ts → auth.ts} +39 -26
- package/src/api/rest/fetchWithRetry.ts +73 -0
- package/src/api/rest/index.ts +1 -1
- package/src/configuration.ts +33 -19
- package/src/index.ts +2 -2
- package/src/{Storage → storage}/Local.ts +0 -2
- package/src/{Storage → storage}/Memory.ts +0 -2
- package/src/{Storage/index.ts → storage/Storage.ts} +0 -3
- package/src/storage/index.ts +3 -0
- package/src/api/rest/axios.ts +0 -52
package/dist/index.js
CHANGED
|
@@ -1,55 +1,12 @@
|
|
|
1
|
-
import { G as GlobalStorage, e as endpointGraphQL, v as version, d as debugging, S as SubscriptionLink, a as endpointAuth } from './SubscriptionLink-client-B5Tmyqw7.js';
|
|
2
|
-
export { L as LocalStorage, M as MemoryStorage, O as Observable, f as Storage, c as cdnBase, b as endpointWebsockets } from './SubscriptionLink-client-B5Tmyqw7.js';
|
|
3
1
|
import { ApolloLink, ApolloClient } from '@apollo/client/index.js';
|
|
4
2
|
export { InMemoryCache, gql, useMutation, useQuery, useSubscription } from '@apollo/client/index.js';
|
|
3
|
+
import { G as GlobalStorage, e as endpointGraphQL, v as version, d as debugging, S as SubscriptionLink, a as endpointAuth } from './SubscriptionLink-client-Bf6n9gzT.js';
|
|
4
|
+
export { L as LocalStorage, M as MemoryStorage, O as Observable, b as Storage, c as cdnBase, f as endpointWebsockets, g as getEnv } from './SubscriptionLink-client-Bf6n9gzT.js';
|
|
5
5
|
import { BatchHttpLink as BatchHttpLink$1 } from '@apollo/client/link/batch-http/index.js';
|
|
6
|
-
import
|
|
6
|
+
import crossfetch from 'cross-fetch';
|
|
7
7
|
import { onError } from '@apollo/client/link/error/index.js';
|
|
8
8
|
import { HttpLink as HttpLink$1 } from '@apollo/client/link/http/index.js';
|
|
9
9
|
import { RetryLink as RetryLink$1 } from '@apollo/client/link/retry/index.js';
|
|
10
|
-
import Axios from 'axios';
|
|
11
|
-
|
|
12
|
-
const MATCHER = /^(?:[\w-]+\.){2}[\w-]+$/;
|
|
13
|
-
const JsonWebTokenParse = (token)=>{
|
|
14
|
-
if (typeof token === 'undefined' || token === null) return token;
|
|
15
|
-
if (!MATCHER.test(token)) {
|
|
16
|
-
console.error(`Invalid Session Token: ${token}`);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
const [_header, payload, _signature] = token.split('.');
|
|
20
|
-
try {
|
|
21
|
-
return {
|
|
22
|
-
token: token,
|
|
23
|
-
claims: JSON.parse(atob(payload))
|
|
24
|
-
};
|
|
25
|
-
} catch (error) {
|
|
26
|
-
console.error(`Invalid Session Token: ${error}`);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* This is designed to support singletons to timeouts that can broadcast
|
|
32
|
-
* to any observers, preventing race conditions with multiple timeouts.
|
|
33
|
-
*/ class Timeoutable {
|
|
34
|
-
constructor(){
|
|
35
|
-
this.observers = [];
|
|
36
|
-
this.set = (callback, delay)=>{
|
|
37
|
-
if (this.timeout) {
|
|
38
|
-
clearTimeout(this.timeout);
|
|
39
|
-
}
|
|
40
|
-
this.observers.push(callback);
|
|
41
|
-
this.timeout = setTimeout(this.broadcast.bind(this), delay);
|
|
42
|
-
};
|
|
43
|
-
this.clear = (observer)=>{
|
|
44
|
-
this.observers = this.observers.filter((callback)=>callback !== observer);
|
|
45
|
-
};
|
|
46
|
-
// Only sends to the 1st listener, but ensures that someone is notified
|
|
47
|
-
this.broadcast = ()=>{
|
|
48
|
-
if (this.observers.length === 0) return;
|
|
49
|
-
this.observers[0](undefined);
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
10
|
|
|
54
11
|
var ConnectorSDKEventType;
|
|
55
12
|
(function(ConnectorSDKEventType) {
|
|
@@ -81,9 +38,11 @@ var ConnectorSDKEventType;
|
|
|
81
38
|
}
|
|
82
39
|
}
|
|
83
40
|
|
|
41
|
+
// Use `cross-fetch` only if `fetch` is not available on the `globalThis` object
|
|
42
|
+
const effectiveFetch$2 = typeof fetch === 'undefined' ? crossfetch : fetch;
|
|
84
43
|
const BatchHttpLink = new BatchHttpLink$1({
|
|
85
44
|
uri: endpointGraphQL,
|
|
86
|
-
fetch
|
|
45
|
+
fetch: effectiveFetch$2
|
|
87
46
|
});
|
|
88
47
|
|
|
89
48
|
const ErrorLink = onError(({ graphQLErrors, networkError })=>{
|
|
@@ -104,9 +63,11 @@ const ErrorLink = onError(({ graphQLErrors, networkError })=>{
|
|
|
104
63
|
|
|
105
64
|
const ForwardableLink = new ApolloLink((operation, forward)=>forward(operation));
|
|
106
65
|
|
|
66
|
+
// Use `cross-fetch` only if `fetch` is not available on the `globalThis` object
|
|
67
|
+
const effectiveFetch$1 = typeof fetch === 'undefined' ? crossfetch : fetch;
|
|
107
68
|
const HttpLink = new HttpLink$1({
|
|
108
69
|
uri: endpointGraphQL,
|
|
109
|
-
fetch
|
|
70
|
+
fetch: effectiveFetch$1
|
|
110
71
|
});
|
|
111
72
|
|
|
112
73
|
const RetryLink = new RetryLink$1({
|
|
@@ -152,35 +113,51 @@ class QuilttClient extends ApolloClient {
|
|
|
152
113
|
}
|
|
153
114
|
}
|
|
154
115
|
|
|
116
|
+
// Use `cross-fetch` only if `fetch` is not available on the `globalThis` object
|
|
117
|
+
const effectiveFetch = typeof fetch === 'undefined' ? crossfetch : fetch;
|
|
155
118
|
const RETRY_DELAY = 150 // ms
|
|
156
119
|
;
|
|
157
120
|
const RETRIES = 10 // 150, 300, 450, 600, 750, 900, 1050, 1200, 1350, 1500 = 8.250s
|
|
158
121
|
;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
122
|
+
/**
|
|
123
|
+
* A wrapper around the native `fetch` function that adds automatic retries on failure, including network errors and HTTP 429 responses.
|
|
124
|
+
* Now treats any response with status < 500 as valid.
|
|
125
|
+
*/ const fetchWithRetry = async (url, options = {
|
|
126
|
+
retry: false
|
|
127
|
+
})=>{
|
|
128
|
+
const { retry, retriesRemaining, validateStatus = (status)=>status >= 200 && status < 300, ...fetchOptions } = options;
|
|
129
|
+
try {
|
|
130
|
+
const response = await effectiveFetch(url, fetchOptions);
|
|
131
|
+
const isResponseOk = validateStatus(response.status);
|
|
132
|
+
if (isResponseOk) {
|
|
133
|
+
return {
|
|
134
|
+
data: await response.json().catch(()=>null),
|
|
135
|
+
status: response.status,
|
|
136
|
+
statusText: response.statusText,
|
|
137
|
+
headers: response.headers,
|
|
138
|
+
ok: isResponseOk
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// If validateStatus fails, and retry is enabled, prepare to retry for eligible status codes
|
|
142
|
+
if (retry && (response.status >= 500 || response.status === 429)) {
|
|
143
|
+
throw new Error('Retryable failure');
|
|
144
|
+
}
|
|
145
|
+
throw new Error('HTTP error with status ' + response.status);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (retry) {
|
|
148
|
+
const currentRetriesRemaining = retriesRemaining !== undefined ? retriesRemaining : RETRIES;
|
|
149
|
+
if (currentRetriesRemaining > 0) {
|
|
150
|
+
const delayTime = RETRY_DELAY * (RETRIES - currentRetriesRemaining);
|
|
151
|
+
await new Promise((resolve)=>setTimeout(resolve, delayTime));
|
|
152
|
+
return fetchWithRetry(url, {
|
|
153
|
+
...options,
|
|
154
|
+
retriesRemaining: currentRetriesRemaining - 1
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
175
158
|
return Promise.reject(error);
|
|
176
|
-
} else {
|
|
177
|
-
config.retriesRemaining -= 1;
|
|
178
159
|
}
|
|
179
|
-
|
|
180
|
-
setTimeout(()=>resolve(), RETRY_DELAY * (RETRIES - config.retriesRemaining));
|
|
181
|
-
});
|
|
182
|
-
return delay.then(()=>axios(config));
|
|
183
|
-
});
|
|
160
|
+
};
|
|
184
161
|
|
|
185
162
|
var AuthStrategies;
|
|
186
163
|
(function(AuthStrategies) {
|
|
@@ -194,47 +171,64 @@ class AuthAPI {
|
|
|
194
171
|
* Response Statuses:
|
|
195
172
|
* - 200: OK -> Session is Valid
|
|
196
173
|
* - 401: Unauthorized -> Session is Invalid
|
|
197
|
-
*/ this.ping = (token)=>{
|
|
198
|
-
|
|
174
|
+
*/ this.ping = async (token)=>{
|
|
175
|
+
const response = await fetchWithRetry(endpointAuth, {
|
|
176
|
+
method: 'GET',
|
|
177
|
+
...this.config(token)
|
|
178
|
+
});
|
|
179
|
+
return response;
|
|
199
180
|
};
|
|
200
181
|
/**
|
|
201
182
|
* Response Statuses:
|
|
202
183
|
* - 201: Created -> Profile Created, New Session Returned
|
|
203
184
|
* - 202: Accepted -> Profile Found, MFA Code Sent for `authenticate`
|
|
204
185
|
* - 422: Unprocessable Entity -> Invalid Payload
|
|
205
|
-
*/ this.identify = (payload)=>{
|
|
206
|
-
|
|
186
|
+
*/ this.identify = async (payload)=>{
|
|
187
|
+
const response = await fetchWithRetry(endpointAuth, {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
body: JSON.stringify(this.body(payload)),
|
|
190
|
+
...this.config()
|
|
191
|
+
});
|
|
192
|
+
return response;
|
|
207
193
|
};
|
|
208
194
|
/**
|
|
209
195
|
* Response Statuses:
|
|
210
196
|
* - 201: Created -> MFA Validated, New Session Returned
|
|
211
197
|
* - 401: Unauthorized -> MFA Invalid
|
|
212
198
|
* - 422: Unprocessable Entity -> Invalid Payload
|
|
213
|
-
*/ this.authenticate = (payload)=>{
|
|
214
|
-
|
|
199
|
+
*/ this.authenticate = async (payload)=>{
|
|
200
|
+
const response = await fetchWithRetry(endpointAuth, {
|
|
201
|
+
method: 'PUT',
|
|
202
|
+
body: JSON.stringify(this.body(payload)),
|
|
203
|
+
...this.config()
|
|
204
|
+
});
|
|
205
|
+
return response;
|
|
215
206
|
};
|
|
216
207
|
/**
|
|
217
208
|
* Response Statuses:
|
|
218
209
|
* - 204: No Content -> Session Revoked
|
|
219
210
|
* - 401: Unauthorized -> Session Not Found
|
|
220
|
-
*/ this.revoke = (token)=>{
|
|
221
|
-
|
|
211
|
+
*/ this.revoke = async (token)=>{
|
|
212
|
+
const response = await fetchWithRetry(endpointAuth, {
|
|
213
|
+
method: 'DELETE',
|
|
214
|
+
...this.config(token)
|
|
215
|
+
});
|
|
216
|
+
return response;
|
|
222
217
|
};
|
|
223
218
|
this.config = (token)=>{
|
|
224
|
-
const headers =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
};
|
|
219
|
+
const headers = new Headers();
|
|
220
|
+
headers.set('Content-Type', 'application/json');
|
|
221
|
+
headers.set('Accept', 'application/json');
|
|
228
222
|
if (token) {
|
|
229
|
-
headers.Authorization
|
|
223
|
+
headers.set('Authorization', `Bearer ${token}`);
|
|
230
224
|
}
|
|
231
225
|
return {
|
|
232
|
-
headers
|
|
226
|
+
headers,
|
|
233
227
|
validateStatus: this.validateStatus,
|
|
234
228
|
retry: true
|
|
235
229
|
};
|
|
236
230
|
};
|
|
237
|
-
this.validateStatus = (status)=>status < 500;
|
|
231
|
+
this.validateStatus = (status)=>status < 500 && status !== 429;
|
|
238
232
|
this.body = (payload)=>{
|
|
239
233
|
if (!this.clientId) {
|
|
240
234
|
console.error('Quiltt Client ID is not set. Unable to identify & authenticate');
|
|
@@ -250,4 +244,46 @@ class AuthAPI {
|
|
|
250
244
|
}
|
|
251
245
|
}
|
|
252
246
|
|
|
247
|
+
const MATCHER = /^(?:[\w-]+\.){2}[\w-]+$/;
|
|
248
|
+
const JsonWebTokenParse = (token)=>{
|
|
249
|
+
if (typeof token === 'undefined' || token === null) return token;
|
|
250
|
+
if (!MATCHER.test(token)) {
|
|
251
|
+
console.error(`Invalid Session Token: ${token}`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const [_header, payload, _signature] = token.split('.');
|
|
255
|
+
try {
|
|
256
|
+
return {
|
|
257
|
+
token: token,
|
|
258
|
+
claims: JSON.parse(atob(payload))
|
|
259
|
+
};
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error(`Invalid Session Token: ${error}`);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* This is designed to support singletons to timeouts that can broadcast
|
|
267
|
+
* to any observers, preventing race conditions with multiple timeouts.
|
|
268
|
+
*/ class Timeoutable {
|
|
269
|
+
constructor(){
|
|
270
|
+
this.observers = [];
|
|
271
|
+
this.set = (callback, delay)=>{
|
|
272
|
+
if (this.timeout) {
|
|
273
|
+
clearTimeout(this.timeout);
|
|
274
|
+
}
|
|
275
|
+
this.observers.push(callback);
|
|
276
|
+
this.timeout = setTimeout(this.broadcast.bind(this), delay);
|
|
277
|
+
};
|
|
278
|
+
this.clear = (observer)=>{
|
|
279
|
+
this.observers = this.observers.filter((callback)=>callback !== observer);
|
|
280
|
+
};
|
|
281
|
+
// Only sends to the 1st listener, but ensures that someone is notified
|
|
282
|
+
this.broadcast = ()=>{
|
|
283
|
+
if (this.observers.length === 0) return;
|
|
284
|
+
this.observers[0](undefined);
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
253
289
|
export { AuthAPI, AuthLink, AuthStrategies, BatchHttpLink, ConnectorSDKEventType, ErrorLink, ForwardableLink, GlobalStorage, HttpLink, JsonWebTokenParse, QuilttClient, RetryLink, SubscriptionLink, TerminatingLink, Timeoutable, VersionLink, debugging, endpointAuth, endpointGraphQL, version };
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quiltt/core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Javascript API client and utilities for Quiltt",
|
|
5
|
-
"repository": {
|
|
6
|
-
"type": "git",
|
|
7
|
-
"url": "https://github.com/quiltt/quiltt-public.git",
|
|
8
|
-
"directory": "packages/core"
|
|
9
|
-
},
|
|
10
|
-
"homepage": "https://github.com/quiltt/quiltt-public/tree/main/packages/core#readme",
|
|
11
5
|
"keywords": [
|
|
12
6
|
"quiltt",
|
|
13
7
|
"typescript"
|
|
14
8
|
],
|
|
9
|
+
"homepage": "https://github.com/quiltt/quiltt-js/tree/main/packages/core#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/quiltt/quiltt-js.git",
|
|
13
|
+
"directory": "packages/core"
|
|
14
|
+
},
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"sideEffects": [
|
|
17
17
|
"./src/Storage/Local.ts",
|
|
@@ -33,16 +33,15 @@
|
|
|
33
33
|
"CHANGELOG.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@apollo/client": "^3.
|
|
37
|
-
"
|
|
38
|
-
"cross-fetch": "^3.1.8",
|
|
36
|
+
"@apollo/client": "^3.9.9",
|
|
37
|
+
"cross-fetch": "^4.0.0",
|
|
39
38
|
"graphql": "^16.8.1",
|
|
40
|
-
"graphql-ruby-client": "^1.
|
|
39
|
+
"graphql-ruby-client": "^1.13.3"
|
|
41
40
|
},
|
|
42
41
|
"devDependencies": {
|
|
43
42
|
"@trivago/prettier-plugin-sort-imports": "4.1.1",
|
|
44
|
-
"@types/node": "20.
|
|
45
|
-
"@types/react": "18.2.
|
|
43
|
+
"@types/node": "20.12.2",
|
|
44
|
+
"@types/react": "18.2.73",
|
|
46
45
|
"@typescript-eslint/eslint-plugin": "5.60.1",
|
|
47
46
|
"@typescript-eslint/parser": "5.60.1",
|
|
48
47
|
"bunchee": "4.4.8",
|
|
@@ -51,7 +50,7 @@
|
|
|
51
50
|
"eslint-plugin-prettier": "4.2.1",
|
|
52
51
|
"prettier": "2.8.8",
|
|
53
52
|
"rimraf": "5.0.5",
|
|
54
|
-
"typescript": "5.4.
|
|
53
|
+
"typescript": "5.4.3"
|
|
55
54
|
},
|
|
56
55
|
"publishConfig": {
|
|
57
56
|
"access": "public"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GlobalStorage } from '@/
|
|
1
|
+
import { GlobalStorage } from '@/storage'
|
|
2
2
|
import type { FetchResult, NextLink, Operation } from '@apollo/client/core/index.js'
|
|
3
3
|
import { ApolloLink, Observable } from '@apollo/client/core/index.js'
|
|
4
4
|
import { print } from 'graphql'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FetchResult, NextLink, Observable, Operation } from '@apollo/client/index.js'
|
|
2
2
|
import { ApolloLink } from '@apollo/client/index.js'
|
|
3
3
|
|
|
4
|
-
import { GlobalStorage } from '@/
|
|
4
|
+
import { GlobalStorage } from '@/storage'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* unauthorizedCallback only triggers in the event the token is present, and
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { BatchHttpLink as ApolloHttpLink } from '@apollo/client/link/batch-http/index.js'
|
|
2
|
+
import crossfetch from 'cross-fetch'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
// Use `cross-fetch` only if `fetch` is not available on the `globalThis` object
|
|
5
|
+
const effectiveFetch = typeof fetch === 'undefined' ? crossfetch : fetch
|
|
4
6
|
|
|
5
7
|
import { endpointGraphQL } from '../../../configuration'
|
|
6
8
|
|
|
7
9
|
export const BatchHttpLink = new ApolloHttpLink({
|
|
8
10
|
uri: endpointGraphQL,
|
|
9
|
-
fetch,
|
|
11
|
+
fetch: effectiveFetch,
|
|
10
12
|
})
|
|
11
13
|
|
|
12
14
|
export default BatchHttpLink
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { HttpLink as ApolloHttpLink } from '@apollo/client/link/http/index.js'
|
|
2
|
+
import crossfetch from 'cross-fetch'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
// Use `cross-fetch` only if `fetch` is not available on the `globalThis` object
|
|
5
|
+
const effectiveFetch = typeof fetch === 'undefined' ? crossfetch : fetch
|
|
4
6
|
|
|
5
7
|
import { endpointGraphQL } from '../../../configuration'
|
|
6
8
|
|
|
7
9
|
export const HttpLink = new ApolloHttpLink({
|
|
8
10
|
uri: endpointGraphQL,
|
|
9
|
-
fetch,
|
|
11
|
+
fetch: effectiveFetch,
|
|
10
12
|
})
|
|
11
13
|
|
|
12
14
|
export default HttpLink
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { endpointAuth } from '../../configuration'
|
|
1
|
+
import { endpointAuth } from '@/configuration'
|
|
2
|
+
import type { FetchResponse } from './fetchWithRetry'
|
|
3
|
+
import { fetchWithRetry } from './fetchWithRetry'
|
|
5
4
|
|
|
6
5
|
export enum AuthStrategies {
|
|
7
6
|
Email = 'email',
|
|
@@ -22,7 +21,7 @@ export type UsernamePayload = EmailInput | PhoneInput
|
|
|
22
21
|
export type PasscodePayload = UsernamePayload & { passcode: string }
|
|
23
22
|
|
|
24
23
|
type SessionData = { token: string }
|
|
25
|
-
type NoContentData =
|
|
24
|
+
type NoContentData = null
|
|
26
25
|
type UnauthorizedData = { message: string; instruction: string }
|
|
27
26
|
export type UnprocessableData = { [attribute: string]: Array<string> }
|
|
28
27
|
|
|
@@ -31,8 +30,8 @@ type Identify = SessionData | NoContentData | UnprocessableData
|
|
|
31
30
|
type Authenticate = SessionData | UnauthorizedData | UnprocessableData
|
|
32
31
|
type Revoke = NoContentData | UnauthorizedData
|
|
33
32
|
|
|
34
|
-
export type SessionResponse =
|
|
35
|
-
export type UnprocessableResponse =
|
|
33
|
+
export type SessionResponse = FetchResponse<SessionData>
|
|
34
|
+
export type UnprocessableResponse = FetchResponse<UnprocessableData>
|
|
36
35
|
|
|
37
36
|
// https://www.quiltt.dev/api-reference/rest/auth#
|
|
38
37
|
export class AuthAPI {
|
|
@@ -47,8 +46,12 @@ export class AuthAPI {
|
|
|
47
46
|
* - 200: OK -> Session is Valid
|
|
48
47
|
* - 401: Unauthorized -> Session is Invalid
|
|
49
48
|
*/
|
|
50
|
-
ping = (token: string) => {
|
|
51
|
-
|
|
49
|
+
ping = async (token: string) => {
|
|
50
|
+
const response = await fetchWithRetry<Ping>(endpointAuth, {
|
|
51
|
+
method: 'GET',
|
|
52
|
+
...this.config(token),
|
|
53
|
+
})
|
|
54
|
+
return response
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
/**
|
|
@@ -57,8 +60,13 @@ export class AuthAPI {
|
|
|
57
60
|
* - 202: Accepted -> Profile Found, MFA Code Sent for `authenticate`
|
|
58
61
|
* - 422: Unprocessable Entity -> Invalid Payload
|
|
59
62
|
*/
|
|
60
|
-
identify = (payload: UsernamePayload) => {
|
|
61
|
-
|
|
63
|
+
identify = async (payload: UsernamePayload) => {
|
|
64
|
+
const response = await fetchWithRetry<Identify>(endpointAuth, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
body: JSON.stringify(this.body(payload)),
|
|
67
|
+
...this.config(),
|
|
68
|
+
})
|
|
69
|
+
return response
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
/**
|
|
@@ -67,8 +75,13 @@ export class AuthAPI {
|
|
|
67
75
|
* - 401: Unauthorized -> MFA Invalid
|
|
68
76
|
* - 422: Unprocessable Entity -> Invalid Payload
|
|
69
77
|
*/
|
|
70
|
-
authenticate = (payload: PasscodePayload) => {
|
|
71
|
-
|
|
78
|
+
authenticate = async (payload: PasscodePayload): Promise<FetchResponse<Authenticate>> => {
|
|
79
|
+
const response = await fetchWithRetry<Authenticate>(endpointAuth, {
|
|
80
|
+
method: 'PUT',
|
|
81
|
+
body: JSON.stringify(this.body(payload)),
|
|
82
|
+
...this.config(),
|
|
83
|
+
})
|
|
84
|
+
return response
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
/**
|
|
@@ -76,28 +89,30 @@ export class AuthAPI {
|
|
|
76
89
|
* - 204: No Content -> Session Revoked
|
|
77
90
|
* - 401: Unauthorized -> Session Not Found
|
|
78
91
|
*/
|
|
79
|
-
revoke = (token: string) => {
|
|
80
|
-
|
|
92
|
+
revoke = async (token: string): Promise<FetchResponse<Revoke>> => {
|
|
93
|
+
const response = await fetchWithRetry<Revoke>(endpointAuth, {
|
|
94
|
+
method: 'DELETE',
|
|
95
|
+
...this.config(token),
|
|
96
|
+
})
|
|
97
|
+
return response
|
|
81
98
|
}
|
|
82
99
|
|
|
83
|
-
private config = (token?: string)
|
|
84
|
-
const headers
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
100
|
+
private config = (token?: string) => {
|
|
101
|
+
const headers = new Headers()
|
|
102
|
+
headers.set('Content-Type', 'application/json')
|
|
103
|
+
headers.set('Accept', 'application/json')
|
|
88
104
|
|
|
89
105
|
if (token) {
|
|
90
|
-
headers.Authorization
|
|
106
|
+
headers.set('Authorization', `Bearer ${token}`)
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
return {
|
|
94
|
-
headers
|
|
110
|
+
headers,
|
|
95
111
|
validateStatus: this.validateStatus,
|
|
96
112
|
retry: true,
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
|
-
|
|
100
|
-
private validateStatus = (status: number) => status < 500
|
|
115
|
+
private validateStatus = (status: number) => status < 500 && status !== 429
|
|
101
116
|
|
|
102
117
|
private body = (payload: any) => {
|
|
103
118
|
if (!this.clientId) {
|
|
@@ -112,5 +127,3 @@ export class AuthAPI {
|
|
|
112
127
|
}
|
|
113
128
|
}
|
|
114
129
|
}
|
|
115
|
-
|
|
116
|
-
export default AuthAPI
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import crossfetch from 'cross-fetch'
|
|
2
|
+
|
|
3
|
+
// Use `cross-fetch` only if `fetch` is not available on the `globalThis` object
|
|
4
|
+
const effectiveFetch = typeof fetch === 'undefined' ? crossfetch : fetch
|
|
5
|
+
|
|
6
|
+
type FetchWithRetryOptions = RequestInit & {
|
|
7
|
+
retry?: boolean
|
|
8
|
+
retriesRemaining?: number
|
|
9
|
+
validateStatus?: (status: number) => boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type FetchResponse<T> = {
|
|
13
|
+
data: T
|
|
14
|
+
status: number
|
|
15
|
+
statusText: string
|
|
16
|
+
headers: Headers
|
|
17
|
+
ok: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const RETRY_DELAY = 150 // ms
|
|
21
|
+
const RETRIES = 10 // 150, 300, 450, 600, 750, 900, 1050, 1200, 1350, 1500 = 8.250s
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A wrapper around the native `fetch` function that adds automatic retries on failure, including network errors and HTTP 429 responses.
|
|
25
|
+
* Now treats any response with status < 500 as valid.
|
|
26
|
+
*/
|
|
27
|
+
export const fetchWithRetry = async <T>(
|
|
28
|
+
url: RequestInfo,
|
|
29
|
+
options: FetchWithRetryOptions = { retry: false }
|
|
30
|
+
): Promise<FetchResponse<T>> => {
|
|
31
|
+
const {
|
|
32
|
+
retry,
|
|
33
|
+
retriesRemaining,
|
|
34
|
+
validateStatus = (status) => status >= 200 && status < 300, // Default to success for 2xx responses
|
|
35
|
+
...fetchOptions
|
|
36
|
+
} = options
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const response = await effectiveFetch(url, fetchOptions)
|
|
40
|
+
|
|
41
|
+
const isResponseOk = validateStatus(response.status)
|
|
42
|
+
|
|
43
|
+
if (isResponseOk) {
|
|
44
|
+
return {
|
|
45
|
+
data: await response.json().catch(() => null),
|
|
46
|
+
status: response.status,
|
|
47
|
+
statusText: response.statusText,
|
|
48
|
+
headers: response.headers,
|
|
49
|
+
ok: isResponseOk,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If validateStatus fails, and retry is enabled, prepare to retry for eligible status codes
|
|
54
|
+
if (retry && (response.status >= 500 || response.status === 429)) {
|
|
55
|
+
throw new Error('Retryable failure')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new Error('HTTP error with status ' + response.status)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (retry) {
|
|
61
|
+
const currentRetriesRemaining = retriesRemaining !== undefined ? retriesRemaining : RETRIES
|
|
62
|
+
if (currentRetriesRemaining > 0) {
|
|
63
|
+
const delayTime = RETRY_DELAY * (RETRIES - currentRetriesRemaining)
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, delayTime))
|
|
65
|
+
return fetchWithRetry(url, {
|
|
66
|
+
...options,
|
|
67
|
+
retriesRemaining: currentRetriesRemaining - 1,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return Promise.reject(error)
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/api/rest/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './auth'
|
package/src/configuration.ts
CHANGED
|
@@ -1,32 +1,46 @@
|
|
|
1
1
|
import { name as packageName, version as packageVersion } from '../package.json'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Retrieves the environment variable by key, with fallback and type conversion,
|
|
5
|
+
* supporting Node.js, Vite, and potentially other runtime environments.
|
|
6
|
+
*/
|
|
7
|
+
export const getEnv = (key: string, fallback: any = undefined): any => {
|
|
4
8
|
try {
|
|
5
|
-
|
|
6
|
-
} catch {
|
|
7
|
-
return undefined
|
|
8
|
-
}
|
|
9
|
-
})()
|
|
9
|
+
let value: string | undefined
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Check if running under Node.js and use process.env
|
|
12
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
13
|
+
value = process.env[key]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Return the value after type conversion if necessary or use fallback
|
|
17
|
+
if (value === undefined || value === null) {
|
|
18
|
+
return fallback
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Convert to boolean if the value is 'true' or 'false'
|
|
22
|
+
if (value === 'true' || value === 'false') {
|
|
23
|
+
return value === 'true'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Convert to number if it's numeric
|
|
27
|
+
if (!isNaN(Number(value))) {
|
|
28
|
+
return Number(value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return value
|
|
32
|
+
} catch (error) {
|
|
15
33
|
return undefined
|
|
16
34
|
}
|
|
17
|
-
}
|
|
35
|
+
}
|
|
18
36
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} catch {
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
})()
|
|
37
|
+
const QUILTT_API_INSECURE = getEnv('QUILTT_API_INSECURE', false)
|
|
38
|
+
const QUILTT_API_DOMAIN = getEnv('QUILTT_API_DOMAIN', 'quiltt.io')
|
|
39
|
+
const QUILTT_DEBUG = getEnv('QUILTT_DEBUG', process?.env?.NODE_ENV !== 'production')
|
|
26
40
|
|
|
27
41
|
const domain = QUILTT_API_DOMAIN || 'quiltt.io'
|
|
28
42
|
const protocolHttp = `http${QUILTT_API_INSECURE ? '' : 's'}`
|
|
29
|
-
const protocolWebsockets = `ws${
|
|
43
|
+
const protocolWebsockets = `ws${QUILTT_API_INSECURE ? '' : 's'}`
|
|
30
44
|
|
|
31
45
|
export const debugging = QUILTT_DEBUG
|
|
32
46
|
export const version = `${packageName}: v${packageVersion}`
|