@ovok/core 0.4.22 → 0.4.23

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.
@@ -0,0 +1,10 @@
1
+ import { ProfileResource } from "@medplum/core";
2
+ import { Project, Reference } from "@medplum/fhirtypes";
3
+ /** Safe account metadata for account pickers; authentication tokens are omitted. */
4
+ export interface OvokSavedAccount {
5
+ id: string;
6
+ profile: Reference<ProfileResource>;
7
+ project: Reference<Project>;
8
+ active: boolean;
9
+ expired: boolean;
10
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,8 +1,9 @@
1
- import { IClientStorage, MedplumClient, MedplumClientOptions, SubscriptionManager } from "@medplum/core";
1
+ import { IClientStorage, LoginState, MedplumClient, MedplumClientOptions, SubscriptionManager } from "@medplum/core";
2
2
  import { Bundle, CapabilityStatement, Observation } from "@medplum/fhirtypes";
3
3
  import { OvokPublicApiEndpoint } from "../conformance/capability";
4
4
  import * as aiFhirMethods from "./ai-fhir/methods";
5
5
  import * as authMethods from "./auth/methods";
6
+ import { OvokSavedAccount } from "./auth/types/Account";
6
7
  import * as botMethods from "./bot/methods";
7
8
  import * as changeHistoryMethods from "./change-history/methods";
8
9
  import * as aiMethods from "./chat/ai/methods";
@@ -34,6 +35,7 @@ export type OvokFhirVersion = "R4" | "R5";
34
35
  export declare class OvokClient extends MedplumClient {
35
36
  protected socialLoginClientId: string;
36
37
  clientStorage: IClientStorage | undefined;
38
+ private readonly hasExplicitStorageAdapter;
37
39
  private readonly offlineQueueOptions;
38
40
  private offlineMeasurementQueue;
39
41
  /**
@@ -44,6 +46,19 @@ export declare class OvokClient extends MedplumClient {
44
46
  socialLoginClientId?: string;
45
47
  offlineQueue?: OfflineMeasurementQueueOptions;
46
48
  });
49
+ /** Lists saved accounts without exposing their access or refresh tokens. */
50
+ getSavedAccounts(): OvokSavedAccount[];
51
+ /** Activates a previously saved account without asking for its password again. */
52
+ switchAccount(accountId: string): Promise<void>;
53
+ /**
54
+ * Ends the selected account's current server session and removes its saved login.
55
+ * The account is retained locally if the server cannot confirm the logout.
56
+ */
57
+ removeAccount(accountId: string): Promise<void>;
58
+ /** Removes every saved account and expiry marker from client storage. */
59
+ clearAllAccounts(): void;
60
+ /** Reauthentication clears the expired marker for the account that signed in. */
61
+ setActiveLogin(login: LoginState): Promise<void>;
47
62
  /** Saves a measurement immediately, or durably queues it when the opt-in queue is enabled. */
48
63
  saveMeasurement(params: GenerateObservationBodyParams): Promise<SaveObservationsResult>;
49
64
  /** Reads Ovok's composed FHIR and public-route capability statement. */
@@ -57,9 +72,14 @@ export declare class OvokClient extends MedplumClient {
57
72
  private getOfflineMeasurementQueue;
58
73
  /**
59
74
  * Medplum clears its entire client storage after an unrecoverable 401. Keep
60
- * queued measurements so they can be retried only after their owner signs in.
75
+ * saved accounts (marking only the active one expired) and queued measurements.
76
+ * Use clearAllAccounts() when the caller intends to remove all saved accounts.
61
77
  */
62
78
  clear(): void;
79
+ private getLogin;
80
+ private getExpiredAccountIds;
81
+ private setAccountExpired;
82
+ private restoreActiveLogin;
63
83
  private executeConditionalObservationCreates;
64
84
  private executeObservationBundle;
65
85
  private bindMethods;
@@ -63,16 +63,20 @@ const slimMethods = __importStar(require("./slim/methods"));
63
63
  exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
64
64
  var observation_bundle_utils_2 = require("./observation/observation-bundle-utils");
65
65
  Object.defineProperty(exports, "MAX_OBSERVATION_BUNDLE_BYTES", { enumerable: true, get: function () { return observation_bundle_utils_2.MAX_OBSERVATION_BUNDLE_BYTES; } });
66
+ const LOGINS_STORAGE_KEY = "logins";
67
+ const EXPIRED_ACCOUNTS_STORAGE_KEY = "ovok.core.expired-accounts";
66
68
  class OvokClient extends core_1.MedplumClient {
67
69
  /**
68
70
  * @param config - The configuration for the client.
69
71
  * @param config.socialLoginClientId - The social login client id to use for the client.
70
72
  */
71
73
  constructor(config) {
72
- var _a, _b;
73
- super(config);
74
+ var _a, _b, _c;
75
+ const storage = (_a = config.storage) !== null && _a !== void 0 ? _a : new core_1.ClientStorage();
76
+ super({ ...config, storage });
74
77
  this.socialLoginClientId = "";
75
- this.socialLoginClientId = (_a = config.socialLoginClientId) !== null && _a !== void 0 ? _a : "";
78
+ this.hasExplicitStorageAdapter = config.storage !== undefined;
79
+ this.socialLoginClientId = (_b = config.socialLoginClientId) !== null && _b !== void 0 ? _b : "";
76
80
  this.bindMethods(authMethods);
77
81
  this.bindMethods(observationMethods);
78
82
  this.bindMethods(questionnaireResponseMethods);
@@ -96,8 +100,88 @@ class OvokClient extends core_1.MedplumClient {
96
100
  });
97
101
  return (0, errors_1.isSignalsError)(mapped) ? mapped : (0, ovok_error_1.mapOvokApiError)(mapped);
98
102
  });
99
- this.clientStorage = config.storage;
100
- this.offlineQueueOptions = (_b = config.offlineQueue) !== null && _b !== void 0 ? _b : {};
103
+ this.clientStorage = storage;
104
+ this.offlineQueueOptions = (_c = config.offlineQueue) !== null && _c !== void 0 ? _c : {};
105
+ }
106
+ /** Lists saved accounts without exposing their access or refresh tokens. */
107
+ getSavedAccounts() {
108
+ var _a, _b;
109
+ const activeAccountId = (_b = (_a = this.getActiveLogin()) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
110
+ const expiredAccountIds = new Set(this.getExpiredAccountIds());
111
+ return this.getLogins().flatMap((login) => {
112
+ var _a;
113
+ const id = (_a = login.profile) === null || _a === void 0 ? void 0 : _a.reference;
114
+ return id
115
+ ? [{
116
+ id,
117
+ profile: login.profile,
118
+ project: login.project,
119
+ active: id === activeAccountId,
120
+ expired: expiredAccountIds.has(id),
121
+ }]
122
+ : [];
123
+ });
124
+ }
125
+ /** Activates a previously saved account without asking for its password again. */
126
+ async switchAccount(accountId) {
127
+ const login = this.getLogin(accountId);
128
+ if (!login) {
129
+ throw new Error(`No saved account found for ${accountId}.`);
130
+ }
131
+ if (this.getExpiredAccountIds().includes(accountId)) {
132
+ throw new Error("This account's session has expired. Sign in again.");
133
+ }
134
+ await this.setActiveLogin(login);
135
+ }
136
+ /**
137
+ * Ends the selected account's current server session and removes its saved login.
138
+ * The account is retained locally if the server cannot confirm the logout.
139
+ */
140
+ async removeAccount(accountId) {
141
+ var _a, _b;
142
+ const login = this.getLogin(accountId);
143
+ if (!login) {
144
+ throw new Error(`No saved account found for ${accountId}.`);
145
+ }
146
+ const previousLogin = this.getActiveLogin();
147
+ try {
148
+ await this.setActiveLogin(login);
149
+ await this.delete("auth/session/current");
150
+ }
151
+ catch (error) {
152
+ if (previousLogin && previousLogin.profile.reference !== accountId) {
153
+ await this.restoreActiveLogin(previousLogin);
154
+ }
155
+ throw error;
156
+ }
157
+ this.clearActiveLogin();
158
+ const remainingLogins = this.getLogins().filter((savedLogin) => savedLogin.profile.reference !== accountId);
159
+ (_a = this.clientStorage) === null || _a === void 0 ? void 0 : _a.setObject(LOGINS_STORAGE_KEY, remainingLogins);
160
+ this.setAccountExpired(accountId, false);
161
+ const nextLogin = (_b = ((previousLogin === null || previousLogin === void 0 ? void 0 : previousLogin.profile.reference) !== accountId ? previousLogin : undefined)) !== null && _b !== void 0 ? _b : remainingLogins[0];
162
+ if (nextLogin) {
163
+ await this.restoreActiveLogin(nextLogin);
164
+ }
165
+ }
166
+ /** Removes every saved account and expiry marker from client storage. */
167
+ clearAllAccounts() {
168
+ var _a;
169
+ const storage = this.clientStorage;
170
+ const queueKey = (_a = this.offlineQueueOptions.storageKey) !== null && _a !== void 0 ? _a : offline_1.OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY;
171
+ const queuedMeasurements = storage === null || storage === void 0 ? void 0 : storage.getObject(queueKey);
172
+ super.clear();
173
+ if (Array.isArray(queuedMeasurements)) {
174
+ storage === null || storage === void 0 ? void 0 : storage.setObject(queueKey, queuedMeasurements);
175
+ }
176
+ }
177
+ /** Reauthentication clears the expired marker for the account that signed in. */
178
+ async setActiveLogin(login) {
179
+ var _a;
180
+ await super.setActiveLogin(login);
181
+ const accountId = (_a = login.profile) === null || _a === void 0 ? void 0 : _a.reference;
182
+ if (accountId) {
183
+ this.setAccountExpired(accountId, false);
184
+ }
101
185
  }
102
186
  /** Saves a measurement immediately, or durably queues it when the opt-in queue is enabled. */
103
187
  async saveMeasurement(params) {
@@ -145,7 +229,7 @@ class OvokClient extends core_1.MedplumClient {
145
229
  if (this.offlineQueueOptions.enabled !== true) {
146
230
  return undefined;
147
231
  }
148
- if (!this.clientStorage) {
232
+ if (!this.clientStorage || !this.hasExplicitStorageAdapter) {
149
233
  throw new Error("Offline measurement queue requires a durable client storage adapter.");
150
234
  }
151
235
  (_a = this.offlineMeasurementQueue) !== null && _a !== void 0 ? _a : (this.offlineMeasurementQueue = new offline_1.OfflineMeasurementQueue(this.clientStorage, (bundle) => this.executeObservationBundle(bundle), {
@@ -168,18 +252,62 @@ class OvokClient extends core_1.MedplumClient {
168
252
  }
169
253
  /**
170
254
  * Medplum clears its entire client storage after an unrecoverable 401. Keep
171
- * queued measurements so they can be retried only after their owner signs in.
255
+ * saved accounts (marking only the active one expired) and queued measurements.
256
+ * Use clearAllAccounts() when the caller intends to remove all saved accounts.
172
257
  */
173
258
  clear() {
174
- var _a;
259
+ var _a, _b, _c;
175
260
  const storage = this.clientStorage;
176
- const queueKey = (_a = this.offlineQueueOptions.storageKey) !== null && _a !== void 0 ? _a : offline_1.OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY;
261
+ const logins = this.getLogins();
262
+ const activeAccountId = (_b = (_a = this.getActiveLogin()) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
263
+ const expiredAccountIds = new Set(this.getExpiredAccountIds());
264
+ if (activeAccountId) {
265
+ expiredAccountIds.add(activeAccountId);
266
+ }
267
+ const queueKey = (_c = this.offlineQueueOptions.storageKey) !== null && _c !== void 0 ? _c : offline_1.OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY;
177
268
  const queuedMeasurements = storage === null || storage === void 0 ? void 0 : storage.getObject(queueKey);
178
269
  super.clear();
270
+ if (logins.length > 0) {
271
+ storage === null || storage === void 0 ? void 0 : storage.setObject(LOGINS_STORAGE_KEY, logins);
272
+ }
273
+ if (expiredAccountIds.size > 0) {
274
+ storage === null || storage === void 0 ? void 0 : storage.setObject(EXPIRED_ACCOUNTS_STORAGE_KEY, Array.from(expiredAccountIds));
275
+ }
179
276
  if (Array.isArray(queuedMeasurements)) {
180
277
  storage === null || storage === void 0 ? void 0 : storage.setObject(queueKey, queuedMeasurements);
181
278
  }
182
279
  }
280
+ getLogin(accountId) {
281
+ return this.getLogins().find((login) => { var _a; return ((_a = login.profile) === null || _a === void 0 ? void 0 : _a.reference) === accountId; });
282
+ }
283
+ getExpiredAccountIds() {
284
+ var _a, _b;
285
+ return ((_b = (_a = this.clientStorage) === null || _a === void 0 ? void 0 : _a.getObject(EXPIRED_ACCOUNTS_STORAGE_KEY)) !== null && _b !== void 0 ? _b : []);
286
+ }
287
+ setAccountExpired(accountId, expired) {
288
+ var _a, _b;
289
+ const expiredAccountIds = new Set(this.getExpiredAccountIds());
290
+ if (expired) {
291
+ expiredAccountIds.add(accountId);
292
+ }
293
+ else {
294
+ expiredAccountIds.delete(accountId);
295
+ }
296
+ if (expiredAccountIds.size > 0) {
297
+ (_a = this.clientStorage) === null || _a === void 0 ? void 0 : _a.setObject(EXPIRED_ACCOUNTS_STORAGE_KEY, Array.from(expiredAccountIds));
298
+ }
299
+ else {
300
+ (_b = this.clientStorage) === null || _b === void 0 ? void 0 : _b.setString(EXPIRED_ACCOUNTS_STORAGE_KEY, undefined);
301
+ }
302
+ }
303
+ async restoreActiveLogin(login) {
304
+ try {
305
+ await this.setActiveLogin(login);
306
+ }
307
+ catch (_a) {
308
+ // Keep the saved login available for a later retry if restoration is offline.
309
+ }
310
+ }
183
311
  async executeConditionalObservationCreates(observations) {
184
312
  const entries = observations.map(offline_1.entryForObservation);
185
313
  return this.executeObservationBundle({
@@ -48,7 +48,7 @@ exports.OVOK_CORE_REQUIREMENTS = {
48
48
  description: "The FHIR surface a server must provide for the @ovok/core client library's own methods to function.",
49
49
  software: {
50
50
  name: "@ovok/core",
51
- version: "0.4.22",
51
+ version: "0.4.23",
52
52
  },
53
53
  fhirVersion: "4.0.1",
54
54
  format: ["json"],
package/dist/index.d.ts CHANGED
@@ -929,6 +929,7 @@ export * from "./client/errors/ovok-error";
929
929
  export * from "./client/auth/errors";
930
930
  export type { RegisterBody } from "./client/auth/types/RegisterBody";
931
931
  export type { RegisterPractitionerBody } from "./client/auth/types/RegisterPractitionerBody";
932
+ export type { OvokSavedAccount } from "./client/auth/types/Account";
932
933
  export { MAX_CONDITIONAL_OBSERVATION_ENTRIES, MAX_OBSERVATION_BUNDLE_BYTES, } from "./client/ovok-client";
933
934
  export type { SaveObservationsResult } from "./client/ovok-client";
934
935
  export * from "./client/offline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovok/core",
3
- "version": "0.4.22",
3
+ "version": "0.4.23",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",