@medplum/core 0.3.0 → 0.4.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 +4 -2
- package/dist/cjs/index.js +1769 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/index.min.js +16 -0
- package/dist/cjs/index.min.js.map +1 -0
- package/dist/esm/index.js +1721 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index.min.js +16 -0
- package/dist/esm/index.min.js.map +1 -0
- package/dist/{cache.d.ts → types/cache.d.ts} +0 -0
- package/dist/{client.d.ts → types/client.d.ts} +50 -8
- package/dist/{crypto.d.ts → types/crypto.d.ts} +0 -0
- package/dist/{eventtarget.d.ts → types/eventtarget.d.ts} +0 -0
- package/dist/{format.d.ts → types/format.d.ts} +0 -0
- package/dist/{index.d.ts → types/index.d.ts} +0 -0
- package/dist/types/jwt.d.ts +5 -0
- package/dist/{outcomes.d.ts → types/outcomes.d.ts} +6 -1
- package/dist/{search.d.ts → types/search.d.ts} +3 -0
- package/dist/{searchparams.d.ts → types/searchparams.d.ts} +0 -0
- package/dist/{storage.d.ts → types/storage.d.ts} +0 -0
- package/dist/{types.d.ts → types/types.d.ts} +17 -3
- package/dist/{utils.d.ts → types/utils.d.ts} +0 -0
- package/package.json +11 -5
- package/rollup.config.js +36 -0
- package/dist/cache.js +0 -39
- package/dist/cache.js.map +0 -1
- package/dist/client.js +0 -572
- package/dist/client.js.map +0 -1
- package/dist/crypto.js +0 -33
- package/dist/crypto.js.map +0 -1
- package/dist/eventtarget.js +0 -38
- package/dist/eventtarget.js.map +0 -1
- package/dist/format.js +0 -56
- package/dist/format.js.map +0 -1
- package/dist/index.js +0 -20
- package/dist/index.js.map +0 -1
- package/dist/jwt.d.ts +0 -5
- package/dist/jwt.js +0 -28
- package/dist/jwt.js.map +0 -1
- package/dist/outcomes.js +0 -154
- package/dist/outcomes.js.map +0 -1
- package/dist/search.js +0 -120
- package/dist/search.js.map +0 -1
- package/dist/searchparams.js +0 -128
- package/dist/searchparams.js.map +0 -1
- package/dist/storage.js +0 -90
- package/dist/storage.js.map +0 -1
- package/dist/types.js +0 -171
- package/dist/types.js.map +0 -1
- package/dist/utils.js +0 -239
- package/dist/utils.js.map +0 -1
package/dist/client.js
DELETED
|
@@ -1,572 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// PKCE auth ased on:
|
|
3
|
-
// https://aws.amazon.com/blogs/security/how-to-add-authentication-single-page-web-application-with-amazon-cognito-oauth2-implementation/
|
|
4
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
5
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
6
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
7
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
8
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
9
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
10
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
11
|
-
});
|
|
12
|
-
};
|
|
13
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.MedplumClient = void 0;
|
|
15
|
-
const cache_1 = require("./cache");
|
|
16
|
-
const crypto_1 = require("./crypto");
|
|
17
|
-
const eventtarget_1 = require("./eventtarget");
|
|
18
|
-
const jwt_1 = require("./jwt");
|
|
19
|
-
const outcomes_1 = require("./outcomes");
|
|
20
|
-
const search_1 = require("./search");
|
|
21
|
-
const storage_1 = require("./storage");
|
|
22
|
-
const types_1 = require("./types");
|
|
23
|
-
const utils_1 = require("./utils");
|
|
24
|
-
const DEFAULT_BASE_URL = 'https://api.medplum.com/';
|
|
25
|
-
const DEFAULT_SCOPE = 'launch/patient openid fhirUser offline_access user/*.*';
|
|
26
|
-
const DEFAULT_RESOURCE_CACHE_SIZE = 1000;
|
|
27
|
-
const JSON_CONTENT_TYPE = 'application/json';
|
|
28
|
-
const FHIR_CONTENT_TYPE = 'application/fhir+json';
|
|
29
|
-
const PATCH_CONTENT_TYPE = 'application/json-patch+json';
|
|
30
|
-
class MedplumClient extends eventtarget_1.EventTarget {
|
|
31
|
-
constructor(options) {
|
|
32
|
-
var _a, _b, _c;
|
|
33
|
-
super();
|
|
34
|
-
if (options.baseUrl) {
|
|
35
|
-
if (!options.baseUrl.startsWith('http')) {
|
|
36
|
-
throw new Error('Base URL must start with http or https');
|
|
37
|
-
}
|
|
38
|
-
if (!options.baseUrl.endsWith('/')) {
|
|
39
|
-
throw new Error('Base URL must end with a trailing slash');
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
this.fetch = options.fetch || window.fetch.bind(window);
|
|
43
|
-
this.storage = new storage_1.ClientStorage();
|
|
44
|
-
this.schema = new Map();
|
|
45
|
-
this.resourceCache = new cache_1.LRUCache((_a = options.resourceCacheSize) !== null && _a !== void 0 ? _a : DEFAULT_RESOURCE_CACHE_SIZE);
|
|
46
|
-
this.baseUrl = options.baseUrl || DEFAULT_BASE_URL;
|
|
47
|
-
this.clientId = options.clientId || '';
|
|
48
|
-
this.authorizeUrl = options.authorizeUrl || this.baseUrl + 'oauth2/authorize';
|
|
49
|
-
this.tokenUrl = options.tokenUrl || this.baseUrl + 'oauth2/token';
|
|
50
|
-
this.logoutUrl = options.logoutUrl || this.baseUrl + 'oauth2/logout';
|
|
51
|
-
this.onUnauthenticated = options.onUnauthenticated;
|
|
52
|
-
this.activeLogin = this.storage.getObject('activeLogin');
|
|
53
|
-
if (!((_c = (_b = this.activeLogin) === null || _b === void 0 ? void 0 : _b.profile) === null || _c === void 0 ? void 0 : _c.reference)) {
|
|
54
|
-
this.clear();
|
|
55
|
-
}
|
|
56
|
-
this.loading = false;
|
|
57
|
-
this.refreshProfile().catch(console.log);
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Clears all auth state including local storage and session storage.
|
|
61
|
-
*/
|
|
62
|
-
clear() {
|
|
63
|
-
this.storage.clear();
|
|
64
|
-
this.schema.clear();
|
|
65
|
-
this.resourceCache.clear();
|
|
66
|
-
this.activeLogin = undefined;
|
|
67
|
-
this.profile = undefined;
|
|
68
|
-
this.dispatchEvent({ type: 'change' });
|
|
69
|
-
}
|
|
70
|
-
get(url) {
|
|
71
|
-
return this.request('GET', url);
|
|
72
|
-
}
|
|
73
|
-
post(url, body, contentType) {
|
|
74
|
-
return this.request('POST', url, contentType, body);
|
|
75
|
-
}
|
|
76
|
-
put(url, body, contentType) {
|
|
77
|
-
return this.request('PUT', url, contentType, body);
|
|
78
|
-
}
|
|
79
|
-
delete(url) {
|
|
80
|
-
return this.request('DELETE', url);
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Tries to register a new user.
|
|
84
|
-
* @param request The registration request.
|
|
85
|
-
* @returns Promise to the authentication response.
|
|
86
|
-
*/
|
|
87
|
-
register(request) {
|
|
88
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
89
|
-
const response = yield this.post('auth/register', request);
|
|
90
|
-
yield this.setActiveLogin(response);
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Initiates a user login flow.
|
|
95
|
-
* @param email The email address of the user.
|
|
96
|
-
* @param password The password of the user.
|
|
97
|
-
* @param remember Optional flag to remember the user.
|
|
98
|
-
* @returns Promise to the authentication response.
|
|
99
|
-
*/
|
|
100
|
-
startLogin(email, password, remember) {
|
|
101
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
102
|
-
yield this.startPkce();
|
|
103
|
-
return this.post('auth/login', {
|
|
104
|
-
clientId: this.clientId,
|
|
105
|
-
scope: DEFAULT_SCOPE,
|
|
106
|
-
codeChallengeMethod: 'S256',
|
|
107
|
-
codeChallenge: this.storage.getString('codeChallenge'),
|
|
108
|
-
email,
|
|
109
|
-
password,
|
|
110
|
-
remember: !!remember,
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Tries to sign in with Google authentication.
|
|
116
|
-
* The response parameter is the result of a Google authentication.
|
|
117
|
-
* See: https://developers.google.com/identity/gsi/web/guides/handle-credential-responses-js-functions
|
|
118
|
-
* @param googleResponse The Google credential response.
|
|
119
|
-
* @returns Promise to the authentication response.
|
|
120
|
-
*/
|
|
121
|
-
startGoogleLogin(googleResponse) {
|
|
122
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
123
|
-
yield this.startPkce();
|
|
124
|
-
return this.post('auth/google', googleResponse);
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Signs out locally.
|
|
129
|
-
* Does not invalidate tokens with the server.
|
|
130
|
-
*/
|
|
131
|
-
signOut() {
|
|
132
|
-
this.clear();
|
|
133
|
-
return Promise.resolve();
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Tries to sign in the user.
|
|
137
|
-
* Returns true if the user is signed in.
|
|
138
|
-
* This may result in navigating away to the sign in page.
|
|
139
|
-
*/
|
|
140
|
-
signInWithRedirect() {
|
|
141
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
142
|
-
const code = urlParams.get('code');
|
|
143
|
-
if (!code) {
|
|
144
|
-
this.requestAuthorization();
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
return this.processCode(code);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Tries to sign out the user.
|
|
153
|
-
* See: https://docs.aws.amazon.com/cognito/latest/developerguide/logout-endpoint.html
|
|
154
|
-
*/
|
|
155
|
-
signOutWithRedirect() {
|
|
156
|
-
window.location.assign(this.logoutUrl);
|
|
157
|
-
}
|
|
158
|
-
fhirUrl(...path) {
|
|
159
|
-
const builder = [this.baseUrl, 'fhir/R4'];
|
|
160
|
-
path.forEach((p) => builder.push('/', encodeURIComponent(p)));
|
|
161
|
-
return builder.join('');
|
|
162
|
-
}
|
|
163
|
-
search(search) {
|
|
164
|
-
if (typeof search === 'string') {
|
|
165
|
-
return this.get('fhir/R4/' + search);
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
return this.get(this.fhirUrl(search.resourceType) + (0, search_1.formatSearchQuery)(search));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Searches a ValueSet resource using the "expand" operation.
|
|
173
|
-
* See: https://www.hl7.org/fhir/operation-valueset-expand.html
|
|
174
|
-
* @param system The ValueSet system url.
|
|
175
|
-
* @param filter The search string.
|
|
176
|
-
* @returns Promise to expanded ValueSet.
|
|
177
|
-
*/
|
|
178
|
-
searchValueSet(system, filter) {
|
|
179
|
-
return this.get(this.fhirUrl('ValueSet', '$expand') +
|
|
180
|
-
`?url=${encodeURIComponent(system)}` +
|
|
181
|
-
`&filter=${encodeURIComponent(filter)}`);
|
|
182
|
-
}
|
|
183
|
-
read(resourceType, id) {
|
|
184
|
-
const cacheKey = resourceType + '/' + id;
|
|
185
|
-
const promise = this.get(this.fhirUrl(resourceType, id)).then((resource) => {
|
|
186
|
-
this.resourceCache.set(cacheKey, resource);
|
|
187
|
-
return resource;
|
|
188
|
-
});
|
|
189
|
-
this.resourceCache.set(cacheKey, promise);
|
|
190
|
-
return promise;
|
|
191
|
-
}
|
|
192
|
-
readCached(resourceType, id) {
|
|
193
|
-
const cached = this.resourceCache.get(resourceType + '/' + id);
|
|
194
|
-
return cached ? Promise.resolve(cached) : this.read(resourceType, id);
|
|
195
|
-
}
|
|
196
|
-
readReference(reference) {
|
|
197
|
-
const refString = reference === null || reference === void 0 ? void 0 : reference.reference;
|
|
198
|
-
if (!refString) {
|
|
199
|
-
return Promise.reject('Missing reference');
|
|
200
|
-
}
|
|
201
|
-
const [resourceType, id] = refString.split('/');
|
|
202
|
-
return this.read(resourceType, id);
|
|
203
|
-
}
|
|
204
|
-
readCachedReference(reference) {
|
|
205
|
-
const refString = reference === null || reference === void 0 ? void 0 : reference.reference;
|
|
206
|
-
if (!refString) {
|
|
207
|
-
return Promise.reject('Missing reference');
|
|
208
|
-
}
|
|
209
|
-
const [resourceType, id] = refString.split('/');
|
|
210
|
-
return this.readCached(resourceType, id);
|
|
211
|
-
}
|
|
212
|
-
getTypeDefinition(resourceType) {
|
|
213
|
-
if (!resourceType) {
|
|
214
|
-
return Promise.reject('Missing resourceType');
|
|
215
|
-
}
|
|
216
|
-
const cached = this.schema.get(resourceType);
|
|
217
|
-
if (cached) {
|
|
218
|
-
return Promise.resolve(cached);
|
|
219
|
-
}
|
|
220
|
-
let typeDef;
|
|
221
|
-
return this.search('StructureDefinition?name:exact=' + encodeURIComponent(resourceType))
|
|
222
|
-
.then((result) => {
|
|
223
|
-
var _a;
|
|
224
|
-
if (!((_a = result.entry) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
225
|
-
throw new Error('StructureDefinition not found');
|
|
226
|
-
}
|
|
227
|
-
const resource = result.entry[0].resource;
|
|
228
|
-
if (!resource) {
|
|
229
|
-
throw new Error('StructureDefinition not found');
|
|
230
|
-
}
|
|
231
|
-
typeDef = (0, types_1.indexStructureDefinition)(resource);
|
|
232
|
-
})
|
|
233
|
-
.then(() => this.search({
|
|
234
|
-
resourceType: 'SearchParameter',
|
|
235
|
-
count: 100,
|
|
236
|
-
filters: [
|
|
237
|
-
{
|
|
238
|
-
code: 'base',
|
|
239
|
-
operator: search_1.Operator.EQUALS,
|
|
240
|
-
value: resourceType,
|
|
241
|
-
},
|
|
242
|
-
],
|
|
243
|
-
}))
|
|
244
|
-
.then((result) => {
|
|
245
|
-
const entries = result.entry;
|
|
246
|
-
if (entries) {
|
|
247
|
-
typeDef.types[resourceType].searchParams = entries
|
|
248
|
-
.map((e) => e.resource)
|
|
249
|
-
.sort((a, b) => { var _a, _b; return (_b = (_a = a.name) === null || _a === void 0 ? void 0 : _a.localeCompare(b.name)) !== null && _b !== void 0 ? _b : 0; });
|
|
250
|
-
}
|
|
251
|
-
this.schema.set(resourceType, typeDef);
|
|
252
|
-
return typeDef;
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
readHistory(resourceType, id) {
|
|
256
|
-
return this.get(this.fhirUrl(resourceType, id, '_history'));
|
|
257
|
-
}
|
|
258
|
-
readPatientEverything(id) {
|
|
259
|
-
return this.get(this.fhirUrl('Patient', id, '$everything'));
|
|
260
|
-
}
|
|
261
|
-
create(resource) {
|
|
262
|
-
if (!resource.resourceType) {
|
|
263
|
-
throw new Error('Missing resourceType');
|
|
264
|
-
}
|
|
265
|
-
return this.post(this.fhirUrl(resource.resourceType), resource);
|
|
266
|
-
}
|
|
267
|
-
createBinary(data, contentType) {
|
|
268
|
-
return this.post(this.fhirUrl('Binary'), data, contentType);
|
|
269
|
-
}
|
|
270
|
-
update(resource) {
|
|
271
|
-
if (!resource.resourceType) {
|
|
272
|
-
throw new Error('Missing resourceType');
|
|
273
|
-
}
|
|
274
|
-
if (!resource.id) {
|
|
275
|
-
throw new Error('Missing id');
|
|
276
|
-
}
|
|
277
|
-
return this.put(this.fhirUrl(resource.resourceType, resource.id), resource);
|
|
278
|
-
}
|
|
279
|
-
patch(resourceType, id, operations) {
|
|
280
|
-
return this.request('PATCH', this.fhirUrl(resourceType, id), PATCH_CONTENT_TYPE, operations);
|
|
281
|
-
}
|
|
282
|
-
deleteResource(resourceType, id) {
|
|
283
|
-
return this.delete(this.fhirUrl(resourceType, id));
|
|
284
|
-
}
|
|
285
|
-
graphql(gql) {
|
|
286
|
-
return this.post(this.fhirUrl('$graphql'), gql, JSON_CONTENT_TYPE);
|
|
287
|
-
}
|
|
288
|
-
subscribe(criteria, handler) {
|
|
289
|
-
return this.create({
|
|
290
|
-
resourceType: 'Subscription',
|
|
291
|
-
status: 'active',
|
|
292
|
-
criteria: criteria,
|
|
293
|
-
channel: {
|
|
294
|
-
type: 'sse',
|
|
295
|
-
},
|
|
296
|
-
}).then((sub) => {
|
|
297
|
-
const eventSource = new EventSource(this.baseUrl + 'sse?subscription=' + encodeURIComponent(sub.id), {
|
|
298
|
-
withCredentials: true,
|
|
299
|
-
});
|
|
300
|
-
eventSource.onmessage = (e) => {
|
|
301
|
-
handler(JSON.parse(e.data));
|
|
302
|
-
};
|
|
303
|
-
return eventSource;
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
getActiveLogin() {
|
|
307
|
-
return this.activeLogin;
|
|
308
|
-
}
|
|
309
|
-
setActiveLogin(login) {
|
|
310
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
311
|
-
this.activeLogin = login;
|
|
312
|
-
this.storage.setObject('activeLogin', login);
|
|
313
|
-
this.addLogin(login);
|
|
314
|
-
this.resourceCache.clear();
|
|
315
|
-
this.refreshPromise = undefined;
|
|
316
|
-
yield this.refreshProfile();
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
getLogins() {
|
|
320
|
-
var _a;
|
|
321
|
-
return (_a = this.storage.getObject('logins')) !== null && _a !== void 0 ? _a : [];
|
|
322
|
-
}
|
|
323
|
-
addLogin(newLogin) {
|
|
324
|
-
const logins = this.getLogins().filter((login) => { var _a, _b; return ((_a = login.profile) === null || _a === void 0 ? void 0 : _a.reference) !== ((_b = newLogin.profile) === null || _b === void 0 ? void 0 : _b.reference); });
|
|
325
|
-
logins.push(newLogin);
|
|
326
|
-
this.storage.setObject('logins', logins);
|
|
327
|
-
}
|
|
328
|
-
refreshProfile() {
|
|
329
|
-
var _a;
|
|
330
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
331
|
-
const reference = (_a = this.getActiveLogin()) === null || _a === void 0 ? void 0 : _a.profile;
|
|
332
|
-
if (reference === null || reference === void 0 ? void 0 : reference.reference) {
|
|
333
|
-
this.loading = true;
|
|
334
|
-
this.profile = yield this.readCachedReference(reference);
|
|
335
|
-
this.loading = false;
|
|
336
|
-
this.dispatchEvent({ type: 'change' });
|
|
337
|
-
}
|
|
338
|
-
return this.profile;
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
getProfile() {
|
|
342
|
-
return this.profile;
|
|
343
|
-
}
|
|
344
|
-
isLoading() {
|
|
345
|
-
return this.loading;
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Makes an HTTP request.
|
|
349
|
-
* @param {string} method
|
|
350
|
-
* @param {string} url
|
|
351
|
-
* @param {string=} contentType
|
|
352
|
-
* @param {Object=} body
|
|
353
|
-
*/
|
|
354
|
-
request(method, url, contentType, body) {
|
|
355
|
-
var _a;
|
|
356
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
357
|
-
if (this.refreshPromise) {
|
|
358
|
-
yield this.refreshPromise;
|
|
359
|
-
}
|
|
360
|
-
if (!url.startsWith('http')) {
|
|
361
|
-
url = this.baseUrl + url;
|
|
362
|
-
}
|
|
363
|
-
const headers = {
|
|
364
|
-
'Content-Type': contentType || FHIR_CONTENT_TYPE,
|
|
365
|
-
};
|
|
366
|
-
const accessToken = (_a = this.getActiveLogin()) === null || _a === void 0 ? void 0 : _a.accessToken;
|
|
367
|
-
if (accessToken) {
|
|
368
|
-
headers['Authorization'] = 'Bearer ' + accessToken;
|
|
369
|
-
}
|
|
370
|
-
const options = {
|
|
371
|
-
method: method,
|
|
372
|
-
cache: 'no-cache',
|
|
373
|
-
credentials: 'include',
|
|
374
|
-
headers,
|
|
375
|
-
};
|
|
376
|
-
if (body) {
|
|
377
|
-
if (typeof body === 'string' || (typeof File !== 'undefined' && body instanceof File)) {
|
|
378
|
-
options.body = body;
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
options.body = (0, utils_1.stringify)(body);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
const response = yield this.fetch(url, options);
|
|
385
|
-
if (response.status === 401) {
|
|
386
|
-
// Refresh and try again
|
|
387
|
-
return this.handleUnauthenticated(method, url, contentType, body);
|
|
388
|
-
}
|
|
389
|
-
if (response.status === 204 || response.status === 304) {
|
|
390
|
-
// No content or change
|
|
391
|
-
return undefined;
|
|
392
|
-
}
|
|
393
|
-
const obj = yield response.json();
|
|
394
|
-
if (obj.resourceType === 'OperationOutcome' && !(0, outcomes_1.isOk)(obj)) {
|
|
395
|
-
return Promise.reject(new outcomes_1.OperationOutcomeError(obj));
|
|
396
|
-
}
|
|
397
|
-
return obj;
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Handles an unauthenticated response from the server.
|
|
402
|
-
* First, tries to refresh the access token and retry the request.
|
|
403
|
-
* Otherwise, calls unauthenticated callbacks and rejects.
|
|
404
|
-
* @param method The HTTP method of the original request.
|
|
405
|
-
* @param url The URL of the original request.
|
|
406
|
-
* @param contentType The content type of the original request.
|
|
407
|
-
* @param body The body of the original request.
|
|
408
|
-
*/
|
|
409
|
-
handleUnauthenticated(method, url, contentType, body) {
|
|
410
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
411
|
-
return this.refresh()
|
|
412
|
-
.then(() => this.request(method, url, contentType, body))
|
|
413
|
-
.catch((error) => {
|
|
414
|
-
this.clear();
|
|
415
|
-
if (this.onUnauthenticated) {
|
|
416
|
-
this.onUnauthenticated();
|
|
417
|
-
}
|
|
418
|
-
return Promise.reject(error);
|
|
419
|
-
});
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Starts a new PKCE flow.
|
|
424
|
-
* These PKCE values are stateful, and must survive redirects and page refreshes.
|
|
425
|
-
*/
|
|
426
|
-
startPkce() {
|
|
427
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
428
|
-
const pkceState = (0, crypto_1.getRandomString)();
|
|
429
|
-
this.storage.setString('pkceState', pkceState);
|
|
430
|
-
const codeVerifier = (0, crypto_1.getRandomString)();
|
|
431
|
-
this.storage.setString('codeVerifier', codeVerifier);
|
|
432
|
-
const arrayHash = yield (0, crypto_1.encryptSHA256)(codeVerifier);
|
|
433
|
-
const codeChallenge = (0, utils_1.arrayBufferToBase64)(arrayHash).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
434
|
-
this.storage.setString('codeChallenge', codeChallenge);
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Redirects the user to the login screen for authorization.
|
|
439
|
-
* Clears all auth state including local storage and session storage.
|
|
440
|
-
* See: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
|
|
441
|
-
*/
|
|
442
|
-
requestAuthorization() {
|
|
443
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
444
|
-
if (!this.authorizeUrl) {
|
|
445
|
-
throw new Error('Missing authorize URL');
|
|
446
|
-
}
|
|
447
|
-
this.startPkce();
|
|
448
|
-
window.location.assign(this.authorizeUrl +
|
|
449
|
-
'?response_type=code' +
|
|
450
|
-
'&state=' +
|
|
451
|
-
encodeURIComponent(this.storage.getString('pkceState')) +
|
|
452
|
-
'&client_id=' +
|
|
453
|
-
encodeURIComponent(this.clientId) +
|
|
454
|
-
'&redirect_uri=' +
|
|
455
|
-
encodeURIComponent(getBaseUrl()) +
|
|
456
|
-
'&scope=' +
|
|
457
|
-
encodeURIComponent(DEFAULT_SCOPE) +
|
|
458
|
-
'&code_challenge_method=S256' +
|
|
459
|
-
'&code_challenge=' +
|
|
460
|
-
encodeURIComponent(this.storage.getString('codeChallenge')));
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Processes an OAuth authorization code.
|
|
465
|
-
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
|
|
466
|
-
* @param code The authorization code received by URL parameter.
|
|
467
|
-
*/
|
|
468
|
-
processCode(code) {
|
|
469
|
-
const pkceState = this.storage.getString('pkceState');
|
|
470
|
-
if (!pkceState) {
|
|
471
|
-
this.clear();
|
|
472
|
-
throw new Error('Invalid PCKE state');
|
|
473
|
-
}
|
|
474
|
-
const codeVerifier = this.storage.getString('codeVerifier');
|
|
475
|
-
if (!codeVerifier) {
|
|
476
|
-
this.clear();
|
|
477
|
-
throw new Error('Invalid PCKE code verifier');
|
|
478
|
-
}
|
|
479
|
-
return this.fetchTokens('grant_type=authorization_code' +
|
|
480
|
-
(this.clientId ? '&client_id=' + encodeURIComponent(this.clientId) : '') +
|
|
481
|
-
'&code_verifier=' +
|
|
482
|
-
encodeURIComponent(codeVerifier) +
|
|
483
|
-
'&redirect_uri=' +
|
|
484
|
-
encodeURIComponent(getBaseUrl()) +
|
|
485
|
-
'&code=' +
|
|
486
|
-
encodeURIComponent(code));
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* Tries to refresh the auth tokens.
|
|
490
|
-
* See: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
|
|
491
|
-
*/
|
|
492
|
-
refresh() {
|
|
493
|
-
var _a;
|
|
494
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
495
|
-
if (this.refreshPromise) {
|
|
496
|
-
return this.refreshPromise;
|
|
497
|
-
}
|
|
498
|
-
const refreshToken = (_a = this.getActiveLogin()) === null || _a === void 0 ? void 0 : _a.refreshToken;
|
|
499
|
-
if (!refreshToken) {
|
|
500
|
-
this.clear();
|
|
501
|
-
return Promise.reject('Invalid refresh token');
|
|
502
|
-
}
|
|
503
|
-
this.refreshPromise = this.fetchTokens('grant_type=refresh_token' +
|
|
504
|
-
'&client_id=' +
|
|
505
|
-
encodeURIComponent(this.clientId) +
|
|
506
|
-
'&refresh_token=' +
|
|
507
|
-
encodeURIComponent(refreshToken));
|
|
508
|
-
yield this.refreshPromise;
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Makes a POST request to the tokens endpoint.
|
|
513
|
-
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
514
|
-
* @param formBody Token parameters in URL encoded format.
|
|
515
|
-
*/
|
|
516
|
-
fetchTokens(formBody) {
|
|
517
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
518
|
-
if (!this.tokenUrl) {
|
|
519
|
-
return Promise.reject('Missing token URL');
|
|
520
|
-
}
|
|
521
|
-
return this.fetch(this.tokenUrl, {
|
|
522
|
-
method: 'POST',
|
|
523
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
524
|
-
body: formBody,
|
|
525
|
-
})
|
|
526
|
-
.then((response) => {
|
|
527
|
-
if (!response.ok) {
|
|
528
|
-
return Promise.reject('Failed to fetch tokens');
|
|
529
|
-
}
|
|
530
|
-
return response.json();
|
|
531
|
-
})
|
|
532
|
-
.then((tokens) => this.verifyTokens(tokens))
|
|
533
|
-
.then(() => this.profile);
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* Verifies the tokens received from the auth server.
|
|
538
|
-
* Validates the JWT against the JWKS.
|
|
539
|
-
* See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
540
|
-
* @param tokens
|
|
541
|
-
*/
|
|
542
|
-
verifyTokens(tokens) {
|
|
543
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
544
|
-
const token = tokens.access_token;
|
|
545
|
-
// Verify token has not expired
|
|
546
|
-
const tokenPayload = (0, jwt_1.parseJWTPayload)(token);
|
|
547
|
-
if (Date.now() >= tokenPayload.exp * 1000) {
|
|
548
|
-
this.clear();
|
|
549
|
-
return Promise.reject('Token expired');
|
|
550
|
-
}
|
|
551
|
-
// Verify app_client_id
|
|
552
|
-
if (this.clientId && tokenPayload.client_id !== this.clientId) {
|
|
553
|
-
this.clear();
|
|
554
|
-
return Promise.reject('Token was not issued for this audience');
|
|
555
|
-
}
|
|
556
|
-
yield this.setActiveLogin({
|
|
557
|
-
accessToken: token,
|
|
558
|
-
refreshToken: tokens.refresh_token,
|
|
559
|
-
project: tokens.project,
|
|
560
|
-
profile: tokens.profile,
|
|
561
|
-
});
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
exports.MedplumClient = MedplumClient;
|
|
566
|
-
/**
|
|
567
|
-
* Returns the base URL for the current page.
|
|
568
|
-
*/
|
|
569
|
-
function getBaseUrl() {
|
|
570
|
-
return window.location.protocol + '//' + window.location.host + '/';
|
|
571
|
-
}
|
|
572
|
-
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["// PKCE auth ased on:\n// https://aws.amazon.com/blogs/security/how-to-add-authentication-single-page-web-application-with-amazon-cognito-oauth2-implementation/\n\nimport {\n Binary,\n Bundle,\n Project,\n ProjectMembership,\n Reference,\n Resource,\n SearchParameter,\n StructureDefinition,\n Subscription,\n ValueSet,\n} from '@medplum/fhirtypes';\nimport { LRUCache } from './cache';\nimport { encryptSHA256, getRandomString } from './crypto';\nimport { EventTarget } from './eventtarget';\nimport { parseJWTPayload } from './jwt';\nimport { isOk, OperationOutcomeError } from './outcomes';\nimport { formatSearchQuery, Operator, SearchRequest } from './search';\nimport { ClientStorage } from './storage';\nimport { IndexedStructureDefinition, indexStructureDefinition } from './types';\nimport { arrayBufferToBase64, ProfileResource, stringify } from './utils';\n\nconst DEFAULT_BASE_URL = 'https://api.medplum.com/';\nconst DEFAULT_SCOPE = 'launch/patient openid fhirUser offline_access user/*.*';\nconst DEFAULT_RESOURCE_CACHE_SIZE = 1000;\nconst JSON_CONTENT_TYPE = 'application/json';\nconst FHIR_CONTENT_TYPE = 'application/fhir+json';\nconst PATCH_CONTENT_TYPE = 'application/json-patch+json';\n\nexport interface MedplumClientOptions {\n /**\n * The client ID.\n * Optional. Default is to defer to the server to use the default client.\n * Use this to use a specific client for SMART-on-FHIR.\n */\n clientId?: string;\n\n /**\n * Base server URL.\n * Optional. Default value is \"https://api.medplum.com/\".\n * Use this to point to a custom Medplum deployment.\n */\n baseUrl?: string;\n\n /**\n * OAuth2 authorize URL.\n * Optional. Default value is baseUrl + \"/oauth2/authorize\".\n * Use this if you want to use a separate OAuth server.\n */\n authorizeUrl?: string;\n\n /**\n * OAuth2 token URL.\n * Optional. Default value is baseUrl + \"/oauth2/token\".\n * Use this if you want to use a separate OAuth server.\n */\n tokenUrl?: string;\n\n /**\n * OAuth2 logout URL.\n * Optional. Default value is baseUrl + \"/oauth2/logout\".\n * Use this if you want to use a separate OAuth server.\n */\n logoutUrl?: string;\n\n /**\n * Number of resources to store in the cache.\n * Optional. Default value is 1000.\n * Consider using this for performance of displaying Patient or Practitioner resources.\n */\n resourceCacheSize?: number;\n\n /**\n * Optional fetch implementation.\n * Optional. Default is window.fetch.\n * For nodejs applications, consider the 'node-fetch' package.\n */\n fetch?: FetchLike;\n\n /**\n * Optional callback for when the client is unauthenticated.\n * Default is do nothing.\n * For client side applications, consider redirecting to a sign in page.\n */\n onUnauthenticated?: () => void;\n}\n\nexport interface FetchLike {\n (url: string, options?: any): Promise<any>;\n}\n\nexport interface RegisterRequest {\n readonly firstName: string;\n readonly lastName: string;\n readonly projectName: string;\n readonly email: string;\n readonly password: string;\n readonly remember?: boolean;\n}\n\nexport interface GoogleCredentialResponse {\n readonly clientId: string;\n readonly credential: string;\n}\n\nexport interface LoginAuthenticationResponse {\n readonly login: string;\n readonly code?: string;\n readonly memberships?: ProjectMembership[];\n}\n\nexport interface LoginProfileResponse {\n readonly login: string;\n readonly scope: string;\n}\n\nexport interface LoginScopeResponse {\n readonly login: string;\n readonly code: string;\n}\n\nexport interface LoginState {\n readonly project: Reference<Project>;\n readonly profile: Reference<ProfileResource>;\n readonly accessToken: string;\n readonly refreshToken: string;\n}\n\nexport interface TokenResponse {\n readonly token_type: string;\n readonly id_token: string;\n readonly access_token: string;\n readonly refresh_token: string;\n readonly expires_in: number;\n readonly project: Reference<Project>;\n readonly profile: Reference<ProfileResource>;\n}\n\nexport class MedplumClient extends EventTarget {\n private readonly fetch: FetchLike;\n private readonly storage: ClientStorage;\n private readonly schema: Map<string, IndexedStructureDefinition>;\n private readonly resourceCache: LRUCache<Resource | Promise<Resource>>;\n private readonly baseUrl: string;\n private readonly clientId: string;\n private readonly authorizeUrl: string;\n private readonly tokenUrl: string;\n private readonly logoutUrl: string;\n private readonly onUnauthenticated?: () => void;\n private refreshPromise?: Promise<any>;\n private activeLogin?: LoginState;\n private profile?: ProfileResource;\n private loading: boolean;\n\n constructor(options: MedplumClientOptions) {\n super();\n\n if (options.baseUrl) {\n if (!options.baseUrl.startsWith('http')) {\n throw new Error('Base URL must start with http or https');\n }\n if (!options.baseUrl.endsWith('/')) {\n throw new Error('Base URL must end with a trailing slash');\n }\n }\n\n this.fetch = options.fetch || window.fetch.bind(window);\n this.storage = new ClientStorage();\n this.schema = new Map();\n this.resourceCache = new LRUCache(options.resourceCacheSize ?? DEFAULT_RESOURCE_CACHE_SIZE);\n this.baseUrl = options.baseUrl || DEFAULT_BASE_URL;\n this.clientId = options.clientId || '';\n this.authorizeUrl = options.authorizeUrl || this.baseUrl + 'oauth2/authorize';\n this.tokenUrl = options.tokenUrl || this.baseUrl + 'oauth2/token';\n this.logoutUrl = options.logoutUrl || this.baseUrl + 'oauth2/logout';\n this.onUnauthenticated = options.onUnauthenticated;\n this.activeLogin = this.storage.getObject<LoginState>('activeLogin');\n if (!this.activeLogin?.profile?.reference) {\n this.clear();\n }\n this.loading = false;\n this.refreshProfile().catch(console.log);\n }\n\n /**\n * Clears all auth state including local storage and session storage.\n */\n clear(): void {\n this.storage.clear();\n this.schema.clear();\n this.resourceCache.clear();\n this.activeLogin = undefined;\n this.profile = undefined;\n this.dispatchEvent({ type: 'change' });\n }\n\n get(url: string): Promise<any> {\n return this.request('GET', url);\n }\n\n post(url: string, body: any, contentType?: string): Promise<any> {\n return this.request('POST', url, contentType, body);\n }\n\n put(url: string, body: any, contentType?: string): Promise<any> {\n return this.request('PUT', url, contentType, body);\n }\n\n delete(url: string): Promise<any> {\n return this.request('DELETE', url);\n }\n\n /**\n * Tries to register a new user.\n * @param request The registration request.\n * @returns Promise to the authentication response.\n */\n async register(request: RegisterRequest): Promise<void> {\n const response = await this.post('auth/register', request);\n await this.setActiveLogin(response as LoginState);\n }\n\n /**\n * Initiates a user login flow.\n * @param email The email address of the user.\n * @param password The password of the user.\n * @param remember Optional flag to remember the user.\n * @returns Promise to the authentication response.\n */\n async startLogin(email: string, password: string, remember?: boolean): Promise<LoginAuthenticationResponse> {\n await this.startPkce();\n return this.post('auth/login', {\n clientId: this.clientId,\n scope: DEFAULT_SCOPE,\n codeChallengeMethod: 'S256',\n codeChallenge: this.storage.getString('codeChallenge') as string,\n email,\n password,\n remember: !!remember,\n }) as Promise<LoginAuthenticationResponse>;\n }\n\n /**\n * Tries to sign in with Google authentication.\n * The response parameter is the result of a Google authentication.\n * See: https://developers.google.com/identity/gsi/web/guides/handle-credential-responses-js-functions\n * @param googleResponse The Google credential response.\n * @returns Promise to the authentication response.\n */\n async startGoogleLogin(googleResponse: GoogleCredentialResponse): Promise<LoginAuthenticationResponse> {\n await this.startPkce();\n return this.post('auth/google', googleResponse) as Promise<LoginAuthenticationResponse>;\n }\n\n /**\n * Signs out locally.\n * Does not invalidate tokens with the server.\n */\n signOut(): Promise<void> {\n this.clear();\n return Promise.resolve();\n }\n\n /**\n * Tries to sign in the user.\n * Returns true if the user is signed in.\n * This may result in navigating away to the sign in page.\n */\n signInWithRedirect(): Promise<ProfileResource | void> | undefined {\n const urlParams = new URLSearchParams(window.location.search);\n const code = urlParams.get('code');\n if (!code) {\n this.requestAuthorization();\n return undefined;\n } else {\n return this.processCode(code);\n }\n }\n\n /**\n * Tries to sign out the user.\n * See: https://docs.aws.amazon.com/cognito/latest/developerguide/logout-endpoint.html\n */\n signOutWithRedirect(): void {\n window.location.assign(this.logoutUrl);\n }\n\n fhirUrl(...path: string[]): string {\n const builder = [this.baseUrl, 'fhir/R4'];\n path.forEach((p) => builder.push('/', encodeURIComponent(p)));\n return builder.join('');\n }\n\n search<T extends Resource>(search: string | SearchRequest): Promise<Bundle<T>> {\n if (typeof search === 'string') {\n return this.get('fhir/R4/' + search);\n } else {\n return this.get(this.fhirUrl(search.resourceType) + formatSearchQuery(search));\n }\n }\n\n /**\n * Searches a ValueSet resource using the \"expand\" operation.\n * See: https://www.hl7.org/fhir/operation-valueset-expand.html\n * @param system The ValueSet system url.\n * @param filter The search string.\n * @returns Promise to expanded ValueSet.\n */\n searchValueSet(system: string, filter: string): Promise<ValueSet> {\n return this.get(\n this.fhirUrl('ValueSet', '$expand') +\n `?url=${encodeURIComponent(system)}` +\n `&filter=${encodeURIComponent(filter)}`\n );\n }\n\n read<T extends Resource>(resourceType: string, id: string): Promise<T> {\n const cacheKey = resourceType + '/' + id;\n const promise = this.get(this.fhirUrl(resourceType, id)).then((resource: T) => {\n this.resourceCache.set(cacheKey, resource);\n return resource;\n });\n this.resourceCache.set(cacheKey, promise);\n return promise;\n }\n\n readCached<T extends Resource>(resourceType: string, id: string): Promise<T> {\n const cached = this.resourceCache.get(resourceType + '/' + id) as T | Promise<T> | undefined;\n return cached ? Promise.resolve(cached) : this.read(resourceType, id);\n }\n\n readReference<T extends Resource>(reference: Reference<T>): Promise<T> {\n const refString = reference?.reference;\n if (!refString) {\n return Promise.reject('Missing reference');\n }\n const [resourceType, id] = refString.split('/');\n return this.read(resourceType, id);\n }\n\n readCachedReference<T extends Resource>(reference: Reference<T>): Promise<T> {\n const refString = reference?.reference;\n if (!refString) {\n return Promise.reject('Missing reference');\n }\n const [resourceType, id] = refString.split('/');\n return this.readCached(resourceType, id);\n }\n\n getTypeDefinition(resourceType: string): Promise<IndexedStructureDefinition> {\n if (!resourceType) {\n return Promise.reject('Missing resourceType');\n }\n const cached = this.schema.get(resourceType);\n if (cached) {\n return Promise.resolve(cached);\n }\n let typeDef: IndexedStructureDefinition;\n return this.search<StructureDefinition>('StructureDefinition?name:exact=' + encodeURIComponent(resourceType))\n .then((result: Bundle<StructureDefinition>) => {\n if (!result.entry?.length) {\n throw new Error('StructureDefinition not found');\n }\n const resource = result.entry[0].resource;\n if (!resource) {\n throw new Error('StructureDefinition not found');\n }\n typeDef = indexStructureDefinition(resource);\n })\n .then(() =>\n this.search<SearchParameter>({\n resourceType: 'SearchParameter',\n count: 100,\n filters: [\n {\n code: 'base',\n operator: Operator.EQUALS,\n value: resourceType,\n },\n ],\n })\n )\n .then((result: Bundle<SearchParameter>) => {\n const entries = result.entry;\n if (entries) {\n typeDef.types[resourceType].searchParams = entries\n .map((e) => e.resource as SearchParameter)\n .sort((a, b) => a.name?.localeCompare(b.name as string) ?? 0);\n }\n this.schema.set(resourceType, typeDef);\n return typeDef;\n });\n }\n\n readHistory<T extends Resource>(resourceType: string, id: string): Promise<Bundle<T>> {\n return this.get(this.fhirUrl(resourceType, id, '_history'));\n }\n\n readPatientEverything(id: string): Promise<Bundle> {\n return this.get(this.fhirUrl('Patient', id, '$everything'));\n }\n\n create<T extends Resource>(resource: T): Promise<T> {\n if (!resource.resourceType) {\n throw new Error('Missing resourceType');\n }\n return this.post(this.fhirUrl(resource.resourceType), resource);\n }\n\n createBinary(data: any, contentType: string): Promise<Binary> {\n return this.post(this.fhirUrl('Binary'), data, contentType);\n }\n\n update<T extends Resource>(resource: T): Promise<T> {\n if (!resource.resourceType) {\n throw new Error('Missing resourceType');\n }\n if (!resource.id) {\n throw new Error('Missing id');\n }\n return this.put(this.fhirUrl(resource.resourceType, resource.id), resource);\n }\n\n patch(resourceType: string, id: string, operations: any): Promise<any> {\n return this.request('PATCH', this.fhirUrl(resourceType, id), PATCH_CONTENT_TYPE, operations);\n }\n\n deleteResource(resourceType: string, id: string): Promise<any> {\n return this.delete(this.fhirUrl(resourceType, id));\n }\n\n graphql(gql: any): Promise<any> {\n return this.post(this.fhirUrl('$graphql'), gql, JSON_CONTENT_TYPE);\n }\n\n subscribe(criteria: string, handler: (e: Resource) => void): Promise<EventSource> {\n return this.create({\n resourceType: 'Subscription',\n status: 'active',\n criteria: criteria,\n channel: {\n type: 'sse',\n },\n }).then((sub: Subscription) => {\n const eventSource = new EventSource(this.baseUrl + 'sse?subscription=' + encodeURIComponent(sub.id as string), {\n withCredentials: true,\n });\n\n eventSource.onmessage = (e: MessageEvent) => {\n handler(JSON.parse(e.data) as Resource);\n };\n\n return eventSource;\n });\n }\n\n getActiveLogin(): LoginState | undefined {\n return this.activeLogin;\n }\n\n async setActiveLogin(login: LoginState): Promise<void> {\n this.activeLogin = login;\n this.storage.setObject('activeLogin', login);\n this.addLogin(login);\n this.resourceCache.clear();\n this.refreshPromise = undefined;\n await this.refreshProfile();\n }\n\n getLogins(): LoginState[] {\n return this.storage.getObject<LoginState[]>('logins') ?? [];\n }\n\n private addLogin(newLogin: LoginState): void {\n const logins = this.getLogins().filter((login) => login.profile?.reference !== newLogin.profile?.reference);\n logins.push(newLogin);\n this.storage.setObject('logins', logins);\n }\n\n private async refreshProfile(): Promise<ProfileResource | undefined> {\n const reference = this.getActiveLogin()?.profile;\n if (reference?.reference) {\n this.loading = true;\n this.profile = await this.readCachedReference(reference);\n this.loading = false;\n this.dispatchEvent({ type: 'change' });\n }\n\n return this.profile;\n }\n\n getProfile(): ProfileResource | undefined {\n return this.profile;\n }\n\n isLoading(): boolean {\n return this.loading;\n }\n\n /**\n * Makes an HTTP request.\n * @param {string} method\n * @param {string} url\n * @param {string=} contentType\n * @param {Object=} body\n */\n private async request(method: string, url: string, contentType?: string, body?: any): Promise<any> {\n if (this.refreshPromise) {\n await this.refreshPromise;\n }\n\n if (!url.startsWith('http')) {\n url = this.baseUrl + url;\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': contentType || FHIR_CONTENT_TYPE,\n };\n\n const accessToken = this.getActiveLogin()?.accessToken;\n if (accessToken) {\n headers['Authorization'] = 'Bearer ' + accessToken;\n }\n\n const options: RequestInit = {\n method: method,\n cache: 'no-cache',\n credentials: 'include',\n headers,\n };\n\n if (body) {\n if (typeof body === 'string' || (typeof File !== 'undefined' && body instanceof File)) {\n options.body = body;\n } else {\n options.body = stringify(body);\n }\n }\n\n const response = await this.fetch(url, options);\n if (response.status === 401) {\n // Refresh and try again\n return this.handleUnauthenticated(method, url, contentType, body);\n }\n\n if (response.status === 204 || response.status === 304) {\n // No content or change\n return undefined;\n }\n\n const obj = await response.json();\n if (obj.resourceType === 'OperationOutcome' && !isOk(obj)) {\n return Promise.reject(new OperationOutcomeError(obj));\n }\n return obj;\n }\n\n /**\n * Handles an unauthenticated response from the server.\n * First, tries to refresh the access token and retry the request.\n * Otherwise, calls unauthenticated callbacks and rejects.\n * @param method The HTTP method of the original request.\n * @param url The URL of the original request.\n * @param contentType The content type of the original request.\n * @param body The body of the original request.\n */\n private async handleUnauthenticated(method: string, url: string, contentType?: string, body?: any): Promise<any> {\n return this.refresh()\n .then(() => this.request(method, url, contentType, body))\n .catch((error) => {\n this.clear();\n if (this.onUnauthenticated) {\n this.onUnauthenticated();\n }\n return Promise.reject(error);\n });\n }\n\n /**\n * Starts a new PKCE flow.\n * These PKCE values are stateful, and must survive redirects and page refreshes.\n */\n private async startPkce(): Promise<void> {\n const pkceState = getRandomString();\n this.storage.setString('pkceState', pkceState);\n\n const codeVerifier = getRandomString();\n this.storage.setString('codeVerifier', codeVerifier);\n\n const arrayHash = await encryptSHA256(codeVerifier);\n const codeChallenge = arrayBufferToBase64(arrayHash).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n this.storage.setString('codeChallenge', codeChallenge);\n }\n\n /**\n * Redirects the user to the login screen for authorization.\n * Clears all auth state including local storage and session storage.\n * See: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint\n */\n private async requestAuthorization() {\n if (!this.authorizeUrl) {\n throw new Error('Missing authorize URL');\n }\n\n this.startPkce();\n\n window.location.assign(\n this.authorizeUrl +\n '?response_type=code' +\n '&state=' +\n encodeURIComponent(this.storage.getString('pkceState') as string) +\n '&client_id=' +\n encodeURIComponent(this.clientId) +\n '&redirect_uri=' +\n encodeURIComponent(getBaseUrl()) +\n '&scope=' +\n encodeURIComponent(DEFAULT_SCOPE) +\n '&code_challenge_method=S256' +\n '&code_challenge=' +\n encodeURIComponent(this.storage.getString('codeChallenge') as string)\n );\n }\n\n /**\n * Processes an OAuth authorization code.\n * See: https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest\n * @param code The authorization code received by URL parameter.\n */\n processCode(code: string): Promise<ProfileResource> {\n const pkceState = this.storage.getString('pkceState');\n if (!pkceState) {\n this.clear();\n throw new Error('Invalid PCKE state');\n }\n\n const codeVerifier = this.storage.getString('codeVerifier');\n if (!codeVerifier) {\n this.clear();\n throw new Error('Invalid PCKE code verifier');\n }\n\n return this.fetchTokens(\n 'grant_type=authorization_code' +\n (this.clientId ? '&client_id=' + encodeURIComponent(this.clientId) : '') +\n '&code_verifier=' +\n encodeURIComponent(codeVerifier) +\n '&redirect_uri=' +\n encodeURIComponent(getBaseUrl()) +\n '&code=' +\n encodeURIComponent(code)\n );\n }\n\n /**\n * Tries to refresh the auth tokens.\n * See: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens\n */\n private async refresh(): Promise<void> {\n if (this.refreshPromise) {\n return this.refreshPromise;\n }\n\n const refreshToken = this.getActiveLogin()?.refreshToken;\n if (!refreshToken) {\n this.clear();\n return Promise.reject('Invalid refresh token');\n }\n\n this.refreshPromise = this.fetchTokens(\n 'grant_type=refresh_token' +\n '&client_id=' +\n encodeURIComponent(this.clientId) +\n '&refresh_token=' +\n encodeURIComponent(refreshToken)\n );\n\n await this.refreshPromise;\n }\n\n /**\n * Makes a POST request to the tokens endpoint.\n * See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint\n * @param formBody Token parameters in URL encoded format.\n */\n private async fetchTokens(formBody: string): Promise<ProfileResource> {\n if (!this.tokenUrl) {\n return Promise.reject('Missing token URL');\n }\n\n return this.fetch(this.tokenUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: formBody,\n })\n .then((response) => {\n if (!response.ok) {\n return Promise.reject('Failed to fetch tokens');\n }\n return response.json();\n })\n .then((tokens) => this.verifyTokens(tokens))\n .then(() => this.profile as ProfileResource);\n }\n\n /**\n * Verifies the tokens received from the auth server.\n * Validates the JWT against the JWKS.\n * See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint\n * @param tokens\n */\n private async verifyTokens(tokens: TokenResponse): Promise<void> {\n const token = tokens.access_token;\n\n // Verify token has not expired\n const tokenPayload = parseJWTPayload(token);\n if (Date.now() >= tokenPayload.exp * 1000) {\n this.clear();\n return Promise.reject('Token expired');\n }\n\n // Verify app_client_id\n if (this.clientId && tokenPayload.client_id !== this.clientId) {\n this.clear();\n return Promise.reject('Token was not issued for this audience');\n }\n\n await this.setActiveLogin({\n accessToken: token,\n refreshToken: tokens.refresh_token,\n project: tokens.project,\n profile: tokens.profile,\n });\n }\n}\n\n/**\n * Returns the base URL for the current page.\n */\nfunction getBaseUrl() {\n return window.location.protocol + '//' + window.location.host + '/';\n}\n"]}
|