@mapcreator/api 5.0.0-alpha.47 → 5.0.0-alpha.49
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/cjs/api/choropleth.d.ts +6 -1
- package/cjs/api/choropleth.d.ts.map +1 -1
- package/cjs/api/choropleth.js +3 -3
- package/cjs/api/choropleth.js.map +1 -1
- package/cjs/api/jobRevision.d.ts +10 -3
- package/cjs/api/jobRevision.d.ts.map +1 -1
- package/cjs/api/jobRevision.js +54 -7
- package/cjs/api/jobRevision.js.map +1 -1
- package/cjs/api/layer.d.ts +2 -0
- package/cjs/api/layer.d.ts.map +1 -1
- package/cjs/api/layer.js.map +1 -1
- package/cjs/api/resources.d.ts +0 -1
- package/cjs/api/resources.d.ts.map +1 -1
- package/cjs/api/resources.js +3 -4
- package/cjs/api/resources.js.map +1 -1
- package/cjs/api/user.d.ts +2 -0
- package/cjs/api/user.d.ts.map +1 -1
- package/cjs/api/user.js +9 -0
- package/cjs/api/user.js.map +1 -1
- package/cjs/oauth.d.ts +8 -1
- package/cjs/oauth.d.ts.map +1 -1
- package/cjs/oauth.js +210 -4
- package/cjs/oauth.js.map +1 -1
- package/cjs/utils.d.ts.map +1 -1
- package/cjs/utils.js +5 -6
- package/cjs/utils.js.map +1 -1
- package/esm/api/choropleth.d.ts +6 -1
- package/esm/api/choropleth.d.ts.map +1 -1
- package/esm/api/choropleth.js +3 -3
- package/esm/api/choropleth.js.map +1 -1
- package/esm/api/jobRevision.d.ts +10 -3
- package/esm/api/jobRevision.d.ts.map +1 -1
- package/esm/api/jobRevision.js +53 -7
- package/esm/api/jobRevision.js.map +1 -1
- package/esm/api/layer.d.ts +2 -0
- package/esm/api/layer.d.ts.map +1 -1
- package/esm/api/layer.js.map +1 -1
- package/esm/api/resources.d.ts +0 -1
- package/esm/api/resources.d.ts.map +1 -1
- package/esm/api/resources.js +3 -4
- package/esm/api/resources.js.map +1 -1
- package/esm/api/user.d.ts +2 -0
- package/esm/api/user.d.ts.map +1 -1
- package/esm/api/user.js +8 -0
- package/esm/api/user.js.map +1 -1
- package/esm/oauth.d.ts +8 -1
- package/esm/oauth.d.ts.map +1 -1
- package/esm/oauth.js +210 -5
- package/esm/oauth.js.map +1 -1
- package/esm/utils.d.ts.map +1 -1
- package/esm/utils.js +6 -7
- package/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/api/choropleth.ts +13 -2
- package/src/api/jobRevision.ts +73 -13
- package/src/api/layer.ts +2 -0
- package/src/api/resources.ts +0 -2
- package/src/api/user.ts +14 -0
- package/src/oauth.ts +275 -5
- package/src/utils.ts +4 -10
package/src/api/jobRevision.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { type ApiLayerData, type Layer, layerRevivers } from './layer.js';
|
|
2
|
+
import { apiHost, authenticate, token } from '../oauth.js';
|
|
2
3
|
import type { ApiMapstyleSetData } from './mapstyleSet.js';
|
|
3
4
|
import type { ApiLanguageData } from './language.js';
|
|
4
5
|
import {
|
|
6
|
+
APIError,
|
|
5
7
|
type ApiCommon,
|
|
6
8
|
type ApiCommonData,
|
|
7
9
|
type ApiError,
|
|
8
10
|
type ApiSuccess,
|
|
9
11
|
type Flatten,
|
|
12
|
+
HTTPError,
|
|
13
|
+
NetworkError,
|
|
10
14
|
type Revivers,
|
|
11
15
|
defaultListHeader,
|
|
12
16
|
deletedNoneParam,
|
|
@@ -196,23 +200,79 @@ export async function listJobRevisionLayers(jobId: number): Promise<Layer[]> {
|
|
|
196
200
|
return request<ApiLayerArray, Layer>(path, null, defaultListHeader, options);
|
|
197
201
|
}
|
|
198
202
|
|
|
199
|
-
|
|
200
|
-
const pathname = `/v1/jobs/${jobId}/revisions/${lastJobRevision}/build`;
|
|
201
|
-
const path = `${pathname}?${deletedNoneParam}`;
|
|
202
|
-
|
|
203
|
-
return request<ApiJobCanBuild, JobCanBuild>(path, null, null, { method: 'POST' });
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
type OutputUrl = { url: string };
|
|
203
|
+
export type JobRevisionOutputUrl = { url: string };
|
|
207
204
|
|
|
208
|
-
type
|
|
209
|
-
data:
|
|
205
|
+
export type ApiJobRevisionOutputUrl = {
|
|
206
|
+
data: JobRevisionOutputUrl & ApiCommonData;
|
|
210
207
|
} & Omit<ApiSuccess, 'data'> | ApiError;
|
|
211
208
|
|
|
212
|
-
export async function
|
|
209
|
+
export async function getJobRevisionOutputUrl(jobId: number): Promise<JobRevisionOutputUrl> {
|
|
210
|
+
await createJobRevisionBuild(jobId);
|
|
211
|
+
|
|
213
212
|
const path = `/v1/jobs/${jobId}/revisions/${lastJobRevision}/result/output-url`;
|
|
214
213
|
|
|
215
|
-
|
|
214
|
+
return request<ApiJobRevisionOutputUrl, JobRevisionOutputUrl>(path);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export type JobRevisionOutput = { blob: Blob; filename?: string | undefined };
|
|
218
|
+
|
|
219
|
+
export async function getJobRevisionOutput(jobId: number): Promise<JobRevisionOutput> {
|
|
220
|
+
await createJobRevisionBuild(jobId);
|
|
221
|
+
|
|
222
|
+
const href = `${apiHost}/v1/jobs/${jobId}/revisions/${lastJobRevision}/result/output`;
|
|
223
|
+
const headers = { ...(token ? { Authorization: token.toString() } : null) };
|
|
224
|
+
const response = await fetch(href, { headers }).catch((error: Error) => {
|
|
225
|
+
throw new NetworkError(error?.message ?? error);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (response.ok) {
|
|
229
|
+
const blob = await response.blob().catch(() => {
|
|
230
|
+
throw new APIError({ success: false, error: { type: 'TypeError', message: 'Malformed Blob response' } });
|
|
231
|
+
});
|
|
232
|
+
const contentDisposition = response.headers.get('Content-Disposition');
|
|
233
|
+
|
|
234
|
+
if (contentDisposition) {
|
|
235
|
+
const filenameRegex = /filename\*\s*=\s*UTF-8''(.+)/i;
|
|
236
|
+
const filenameMatch = contentDisposition.match(filenameRegex);
|
|
237
|
+
|
|
238
|
+
if (filenameMatch?.[1]) {
|
|
239
|
+
return { blob, filename: filenameMatch[1] };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const fallbackRegex = /filename\s*=\s*"([^"]+)"/i;
|
|
243
|
+
const fallbackMatch = contentDisposition.match(fallbackRegex);
|
|
244
|
+
|
|
245
|
+
if (fallbackMatch?.[1]) {
|
|
246
|
+
return { blob, filename: fallbackMatch[1] };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { blob };
|
|
251
|
+
} else {
|
|
252
|
+
// eslint-disable-next-line default-case
|
|
253
|
+
switch (response.status) {
|
|
254
|
+
case 401:
|
|
255
|
+
await authenticate();
|
|
256
|
+
break; // NO-OP
|
|
257
|
+
case 403:
|
|
258
|
+
case 404:
|
|
259
|
+
case 406:
|
|
260
|
+
case 429:
|
|
261
|
+
throw new APIError(
|
|
262
|
+
(await response.json().catch(() => ({
|
|
263
|
+
success: false,
|
|
264
|
+
error: { type: 'HttpException', message: response.statusText },
|
|
265
|
+
}))) as ApiError,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
throw new HTTPError(response);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function createJobRevisionBuild(jobId: number): Promise<string> {
|
|
274
|
+
const pathname = `/v1/jobs/${jobId}/revisions/${lastJobRevision}/build`;
|
|
275
|
+
const path = `${pathname}?${deletedNoneParam}`;
|
|
216
276
|
|
|
217
|
-
return request<
|
|
277
|
+
return request<ApiCommon, string>(path, null, null, { method: 'POST' });
|
|
218
278
|
}
|
package/src/api/layer.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type Layer = {
|
|
|
4
4
|
id: number;
|
|
5
5
|
name: string;
|
|
6
6
|
description: string;
|
|
7
|
+
svg: string | null;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
export type ApiLayer = {
|
|
@@ -13,6 +14,7 @@ export type ApiLayer = {
|
|
|
13
14
|
path: string; // The path to the layer
|
|
14
15
|
description: string; // The description of the layer (searchable, sortable)
|
|
15
16
|
image: string | null; // The image related to a layer
|
|
17
|
+
svg: string | null; // The svg related to a layer
|
|
16
18
|
scale_min: number; // The minimum scale for the layer (searchable, sortable)
|
|
17
19
|
scale_max: number; // The maximum scale for the layer (searchable, sortable)
|
|
18
20
|
pangaea_ready: boolean; // Whether a layer is prepared for Pangaea (searchable, sortable)
|
package/src/api/resources.ts
CHANGED
|
@@ -56,7 +56,6 @@ export interface Resources {
|
|
|
56
56
|
dimensionSets: DimensionSet[];
|
|
57
57
|
mapstyleSets: MapstyleSet[];
|
|
58
58
|
jobTypes: JobType[];
|
|
59
|
-
hatchingSvgs: Svg[];
|
|
60
59
|
messages: Message[];
|
|
61
60
|
svgSets: SvgSet[];
|
|
62
61
|
layers: Layer[];
|
|
@@ -124,7 +123,6 @@ export async function loadResources(): Promise<Resources> {
|
|
|
124
123
|
dimensionSets: raw.dimensionSets.map(processData, getContext(dimensionSetRevivers)) as DimensionSet[],
|
|
125
124
|
mapstyleSets: raw.mapstyleSets.map(processData, getContext(mapstyleSetRevivers)) as MapstyleSet[],
|
|
126
125
|
jobTypes: raw.jobTypes.map(processData, getContext(jobTypeRevivers)) as JobType[],
|
|
127
|
-
hatchingSvgs: (raw.hatchingSvgs?.map(processData, getContext(svgRevivers)) ?? []) as Svg[],
|
|
128
126
|
messages: allMessages,
|
|
129
127
|
layers: (raw.layers?.map(processData, getContext(layerRevivers)) ?? []) as Layer[],
|
|
130
128
|
svgSets: (raw.svgSets?.map(processData, getContext(svgSetRevivers)) ?? []) as SvgSet[],
|
package/src/api/user.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ApiFontFamilyData, type FontFamily, fontFamilyRevivers } from './fontFamily.js';
|
|
2
2
|
import { type ApiSvgSetData, type SvgSet, svgSetRevivers } from './svgSet.js';
|
|
3
3
|
import { type ApiLayerData, type Layer, layerRevivers } from './layer.js';
|
|
4
|
+
import { type ApiSvgData, type Svg, svgRevivers } from './svg.js';
|
|
4
5
|
import { type JobSearchResult, listJobs } from './apiCommon.js';
|
|
5
6
|
import type { ApiJobShareArray, JobShare } from './jobShare.js';
|
|
6
7
|
import type { RequireAtLeastOne } from 'type-fest';
|
|
@@ -194,6 +195,19 @@ export async function listUserFontFamilies(): Promise<FontFamily[]> {
|
|
|
194
195
|
return request<ApiFontFamilyArray, FontFamily>(path, null, defaultListHeader, options);
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
export async function listUserHatchingSvgs(): Promise<Svg[]> {
|
|
199
|
+
const pathname = `/v1/users/${myUser}/hatching-svgs`;
|
|
200
|
+
const query = getSearchParams({ sort: 'order' });
|
|
201
|
+
const path = `${pathname}?${query}`;
|
|
202
|
+
const options = { revivers: svgRevivers };
|
|
203
|
+
|
|
204
|
+
type ApiSvgArray = {
|
|
205
|
+
data: ApiSvgData[];
|
|
206
|
+
} & Omit<ApiSuccess, 'data'> | ApiError;
|
|
207
|
+
|
|
208
|
+
return request<ApiSvgArray, Svg>(path, null, defaultListHeader, options);
|
|
209
|
+
}
|
|
210
|
+
|
|
197
211
|
export async function listUserJobs(
|
|
198
212
|
userId: number | typeof myUser,
|
|
199
213
|
title: string,
|
package/src/oauth.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
1
|
export let apiHost = '';
|
|
3
2
|
|
|
4
|
-
export
|
|
3
|
+
export let token: {
|
|
5
4
|
type: string;
|
|
6
5
|
token: string;
|
|
7
6
|
expires: Date;
|
|
@@ -13,6 +12,44 @@ let apiClientId = '';
|
|
|
13
12
|
let callbackUrl = '';
|
|
14
13
|
let oauthScopes = ['*'];
|
|
15
14
|
|
|
15
|
+
const anchorParams = ['access_token', 'token_type', 'expires_in', 'state'];
|
|
16
|
+
|
|
17
|
+
const storagePrefix = '_m4n_';
|
|
18
|
+
const statePrefix = 'oauth_state_';
|
|
19
|
+
const storageName = 'api_token';
|
|
20
|
+
|
|
21
|
+
const dummyTokenExpires = new Date('2100-01-01T01:00:00');
|
|
22
|
+
|
|
23
|
+
interface AnchorToken {
|
|
24
|
+
access_token: string;
|
|
25
|
+
token_type: string;
|
|
26
|
+
expires_in: string;
|
|
27
|
+
state: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const titleCase = (str: unknown): string => String(str).toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Setup internal structures to use dummy authentication flow
|
|
34
|
+
*
|
|
35
|
+
* @param {string} apiUrl - Full API URL
|
|
36
|
+
* @param {string} oauthToken - OAuth Token
|
|
37
|
+
*/
|
|
38
|
+
export function initDummyFlow(apiUrl: string, oauthToken: string): void {
|
|
39
|
+
const parts = oauthToken.includes(' ') ? oauthToken.split(' ', 2) : ['Bearer', oauthToken];
|
|
40
|
+
|
|
41
|
+
apiHost = apiUrl.replace(/\/+$/, '');
|
|
42
|
+
token = {
|
|
43
|
+
type: titleCase(parts[0]),
|
|
44
|
+
token: parts[1],
|
|
45
|
+
expires: dummyTokenExpires,
|
|
46
|
+
|
|
47
|
+
toString(): string {
|
|
48
|
+
return `${this.type} ${this.token}`;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
16
53
|
/**
|
|
17
54
|
* Setup internal structures to use implicit authentication flow
|
|
18
55
|
*
|
|
@@ -27,18 +64,251 @@ export function initImplicitFlow(apiUrl: string, clientId: string, redirectUrl =
|
|
|
27
64
|
apiClientId = String(clientId);
|
|
28
65
|
callbackUrl = String(redirectUrl || window.location.href.split('#')[0]);
|
|
29
66
|
oauthScopes = scopes;
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
const key = `${storagePrefix}${storageName}`;
|
|
70
|
+
const data = window.localStorage.getItem(key);
|
|
71
|
+
|
|
72
|
+
if (data) {
|
|
73
|
+
try {
|
|
74
|
+
const obj = JSON.parse(data) as { type?: unknown; token?: unknown; expires?: unknown };
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
typeof obj.type === 'string' &&
|
|
78
|
+
typeof obj.token === 'string' &&
|
|
79
|
+
typeof obj.expires === 'string' &&
|
|
80
|
+
new Date(obj.expires) > new Date()
|
|
81
|
+
) {
|
|
82
|
+
token = {
|
|
83
|
+
type: titleCase(obj.type),
|
|
84
|
+
token: obj.token,
|
|
85
|
+
expires: new Date(obj.expires),
|
|
86
|
+
|
|
87
|
+
toString(): string {
|
|
88
|
+
return `${this.type} ${this.token}`;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
} else {
|
|
92
|
+
window.localStorage.removeItem(key);
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
/* */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
const obj = getAnchorToken();
|
|
102
|
+
|
|
103
|
+
if (isAnchorToken(obj)) {
|
|
104
|
+
// We'll not go there if anchor contains error and/or message
|
|
105
|
+
// This means that anchor parameters will be preserved for the next processing
|
|
106
|
+
cleanAnchorParams();
|
|
107
|
+
|
|
108
|
+
const expires = new Date(Date.now() + Number(obj.expires_in) * 1000);
|
|
109
|
+
|
|
110
|
+
if (isValidState(obj.state) && expires > new Date()) {
|
|
111
|
+
token = {
|
|
112
|
+
type: titleCase(obj.token_type),
|
|
113
|
+
token: obj.access_token,
|
|
114
|
+
expires,
|
|
115
|
+
|
|
116
|
+
toString(): string {
|
|
117
|
+
return `${this.type} ${this.token}`;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const key = `${storagePrefix}${storageName}`;
|
|
122
|
+
const data = { type: token.type, token: token.token, expires: expires.toUTCString() };
|
|
123
|
+
|
|
124
|
+
window.localStorage.setItem(key, JSON.stringify(data));
|
|
125
|
+
} else {
|
|
126
|
+
// TODO: add some logic to handle this
|
|
127
|
+
// throw Error('Invalid state in url');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (authenticated()) {
|
|
133
|
+
const href = sessionStorage.getItem('redirect-url');
|
|
134
|
+
|
|
135
|
+
if (href) {
|
|
136
|
+
sessionStorage.removeItem('redirect-url');
|
|
137
|
+
window.history.replaceState(null, document.title, href);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
30
140
|
}
|
|
31
141
|
|
|
32
142
|
export async function authenticate(): Promise<string> | never {
|
|
33
143
|
return new Promise(() => {
|
|
34
|
-
|
|
144
|
+
if (anchorContainsError()) {
|
|
145
|
+
console.error(getError());
|
|
146
|
+
cleanAnchorParams();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
forget();
|
|
150
|
+
|
|
151
|
+
sessionStorage.setItem('redirect-url', window.location.href);
|
|
152
|
+
window.location.assign(buildRedirectUrl());
|
|
35
153
|
});
|
|
36
154
|
}
|
|
37
155
|
|
|
38
156
|
export function authenticated(): boolean {
|
|
39
|
-
return
|
|
157
|
+
return token != null && token.expires > new Date() && (
|
|
158
|
+
token.expires.valueOf() === dummyTokenExpires.valueOf() ||
|
|
159
|
+
!!window.localStorage.getItem(`${storagePrefix}${storageName}`)
|
|
160
|
+
);
|
|
40
161
|
}
|
|
41
162
|
|
|
42
163
|
export async function logout(): Promise<void> {
|
|
43
|
-
|
|
164
|
+
if (token) {
|
|
165
|
+
await fetch(`${apiHost}/oauth/logout`, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: {
|
|
168
|
+
Accept: 'application/json',
|
|
169
|
+
Authorization: token.toString(),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
forget();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function forget(): void {
|
|
178
|
+
for (let i = 0; i < window.localStorage.length; ++i) {
|
|
179
|
+
const key = window.localStorage.key(i);
|
|
180
|
+
|
|
181
|
+
if (key?.startsWith(storagePrefix)) {
|
|
182
|
+
window.localStorage.removeItem(key);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
token = null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildRedirectUrl(): string {
|
|
190
|
+
const queryParams = new URLSearchParams({
|
|
191
|
+
client_id: apiClientId,
|
|
192
|
+
redirect_uri: callbackUrl,
|
|
193
|
+
response_type: 'token',
|
|
194
|
+
scope: oauthScopes.join(' '),
|
|
195
|
+
state: generateState(),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return `${apiHost}/oauth/authorize?${queryParams}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getAnchorQuery(): string {
|
|
202
|
+
return window.location.hash.replace(/^#\/?/, '');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getAnchorParams(): Record<string, unknown> {
|
|
206
|
+
const query = getAnchorQuery();
|
|
207
|
+
// eslint-disable-next-line @stylistic/padding-line-between-statements,@typescript-eslint/no-unsafe-return
|
|
208
|
+
return Object.fromEntries(query.split('&').map(pair => pair.split('=').map(decodeURIComponent)));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getAnchorToken(): Partial<AnchorToken> {
|
|
212
|
+
const params = getAnchorParams();
|
|
213
|
+
|
|
214
|
+
return Object.fromEntries(Object.entries(params).filter(([key]) => anchorParams.includes(key)));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isAnchorToken(anchorToken: Partial<AnchorToken>): anchorToken is AnchorToken {
|
|
218
|
+
const queryKeys = Object.keys(anchorToken);
|
|
219
|
+
|
|
220
|
+
return anchorParams.every(key => queryKeys.includes(key));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function cleanAnchorParams(): void {
|
|
224
|
+
const query = window.location.hash.replace(/^#\/?/, '');
|
|
225
|
+
const targets = [...anchorParams, 'error', 'message'];
|
|
226
|
+
const newHash = query
|
|
227
|
+
.split('&')
|
|
228
|
+
.filter(pair => !targets.includes(decodeURIComponent(pair.split('=')[0])))
|
|
229
|
+
.join('&');
|
|
230
|
+
|
|
231
|
+
if (newHash) {
|
|
232
|
+
window.location.hash = newHash;
|
|
233
|
+
} else {
|
|
234
|
+
const { origin, pathname, search } = window.location;
|
|
235
|
+
|
|
236
|
+
window.history.replaceState(null, document.title, `${origin}${pathname}${search}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isValidState(state: string): boolean {
|
|
241
|
+
const key = `${storagePrefix}${statePrefix}${state}`;
|
|
242
|
+
const found = window.localStorage.getItem(key) != null;
|
|
243
|
+
|
|
244
|
+
if (found) {
|
|
245
|
+
window.localStorage.removeItem(key);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return found;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function anchorContainsError(): boolean {
|
|
252
|
+
return 'error' in getAnchorParams();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function generateState(): string {
|
|
256
|
+
// @ts-expect-error TS2365
|
|
257
|
+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
258
|
+
const state = (([1e7] + -1e3 + -4e3 + -8e3 + -1e11) as string).replace(
|
|
259
|
+
/[018]/g, // @ts-expect-error TS2362
|
|
260
|
+
c => (c ^ ((Math.random() * 256) & (0x0f >>> (c >>> 2)))).toString(16),
|
|
261
|
+
);
|
|
262
|
+
const key = `${storagePrefix}${statePrefix}${state}`;
|
|
263
|
+
|
|
264
|
+
window.localStorage.setItem(key, `${Date.now()}`);
|
|
265
|
+
|
|
266
|
+
return state;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
class OAuthError extends Error {
|
|
270
|
+
error: string;
|
|
271
|
+
|
|
272
|
+
constructor(message: string, error: unknown) {
|
|
273
|
+
super(message);
|
|
274
|
+
|
|
275
|
+
this.error = String(error);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
toString(): string {
|
|
279
|
+
let error = this.error;
|
|
280
|
+
|
|
281
|
+
if (error.includes('_')) {
|
|
282
|
+
error = error.replace('_', ' ').replace(/^./, c => c.toUpperCase());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return this.message ? `${error}: ${this.message}` : error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getError(): OAuthError {
|
|
290
|
+
const params = getAnchorParams();
|
|
291
|
+
|
|
292
|
+
return params.message
|
|
293
|
+
? new OAuthError(params.message as string, params.error)
|
|
294
|
+
: new OAuthError(titleCase(params.error), params.error);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Our goal is to support even obsolete platforms (ES2017+ / Node.js 8.10+).
|
|
299
|
+
* This is a small polyfill for possibly missing method used in our codebase.
|
|
300
|
+
*/
|
|
301
|
+
if (!Object.fromEntries) { // eslint-disable-next-line arrow-body-style
|
|
302
|
+
Object.fromEntries = <T = never>(entries: Iterable<readonly [string | number, T]>): { [k: string]: T } => {
|
|
303
|
+
return Array.from(entries).reduce<{ [k: string]: T }>(
|
|
304
|
+
(object, entry) => {
|
|
305
|
+
if (!Array.isArray(entry)) {
|
|
306
|
+
throw new TypeError(`Iterator value ${entry as unknown as string} is not an entry object.`);
|
|
307
|
+
}
|
|
308
|
+
object[`${entry[0]}`] = entry[1];
|
|
309
|
+
|
|
310
|
+
return object;
|
|
311
|
+
}, {}
|
|
312
|
+
);
|
|
313
|
+
};
|
|
44
314
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CamelCasedProperties, SnakeCasedProperties } from 'type-fest';
|
|
2
|
-
import { apiHost, authenticate } from './oauth.js';
|
|
2
|
+
import { apiHost, authenticate, token } from './oauth.js';
|
|
3
3
|
|
|
4
4
|
export type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
|
|
5
5
|
|
|
@@ -233,6 +233,7 @@ function getRequestInit<I extends ApiCommon, O extends Record<string, unknown>>(
|
|
|
233
233
|
extraHeaders?: Record<string, string> | null,
|
|
234
234
|
extraOptions?: ExtraOptions<I, O>,
|
|
235
235
|
): RequestInit {
|
|
236
|
+
const authorization = token ? { Authorization: token.toString() } : null;
|
|
236
237
|
let contentType = null as { 'Content-Type': string } | null;
|
|
237
238
|
|
|
238
239
|
if (body !== undefined) {
|
|
@@ -258,17 +259,10 @@ function getRequestInit<I extends ApiCommon, O extends Record<string, unknown>>(
|
|
|
258
259
|
}
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
const
|
|
262
|
-
.find(pair => pair.startsWith('XSRF-TOKEN'))?.split('=')[1];
|
|
262
|
+
const headers = { Accept: 'application/json', ...authorization, ...contentType, ...extraHeaders };
|
|
263
263
|
const method = extraOptions?.method ?? (body != null ? 'POST' : 'GET'); // don't touch `!=` please
|
|
264
|
-
const headers = {
|
|
265
|
-
Accept: 'application/json',
|
|
266
|
-
...contentType,
|
|
267
|
-
...extraHeaders,
|
|
268
|
-
...(method === 'POST' && token && { 'X-XSRF-Token': decodeURIComponent(token) }),
|
|
269
|
-
};
|
|
270
264
|
|
|
271
|
-
return { body, headers, method
|
|
265
|
+
return { body, headers, method } as RequestInit;
|
|
272
266
|
}
|
|
273
267
|
|
|
274
268
|
interface Context<I extends ApiCommon, O extends Record<string, unknown>> {
|