fhirstarterjs 1.0.2 → 1.0.3

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.
@@ -1,4 +1,4 @@
1
- import { importPKCS8, exportJWK, SignJWT } from "jose";
1
+ import { importPKCS8, exportJWK, SignJWT, } from "jose";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { readFileSync } from "node:fs";
4
4
  /**
@@ -25,7 +25,8 @@ export default class FHIRStarter {
25
25
  constructor(config) {
26
26
  if (!config.clientId)
27
27
  throw new Error("AuthConfig: clientId is required");
28
- if (!config.privateKey || (Buffer.isBuffer(config.privateKey) && config.privateKey.length === 0))
28
+ if (!config.privateKey ||
29
+ (Buffer.isBuffer(config.privateKey) && config.privateKey.length === 0))
29
30
  throw new Error("AuthConfig: privateKey is required");
30
31
  if (!config.tokenEndpointUrl)
31
32
  throw new Error("AuthConfig: tokenEndpointUrl is required");
@@ -44,14 +45,14 @@ export default class FHIRStarter {
44
45
  // --- Sync getters ---
45
46
  /** The current access token, or `null` if no valid token is cached. */
46
47
  get token() {
47
- return this.cache && Date.now() < this.cache.expiresAt
48
- ? this.cache.accessToken
48
+ return this.cache && Date.now() < this.cache.expiresAt ?
49
+ this.cache.accessToken
49
50
  : null;
50
51
  }
51
52
  /** Seconds until the cached token expires, or `null` if no valid token is cached. */
52
53
  get expiresIn() {
53
- return this.cache && Date.now() < this.cache.expiresAt
54
- ? Math.ceil((this.cache.expiresAt - Date.now()) / 1000)
54
+ return this.cache && Date.now() < this.cache.expiresAt ?
55
+ Math.ceil((this.cache.expiresAt - Date.now()) / 1000)
55
56
  : null;
56
57
  }
57
58
  /** Ready-to-use `Authorization` header value (e.g. `"Bearer <token>"`), or `null` if no valid token is cached. */
@@ -101,8 +102,12 @@ export default class FHIRStarter {
101
102
  const auth = this;
102
103
  return {
103
104
  token_type: "bearer",
104
- get access_token() { return auth.token ?? undefined; },
105
- get expires_in() { return auth.expiresIn ?? undefined; },
105
+ get access_token() {
106
+ return auth.token ?? undefined;
107
+ },
108
+ get expires_in() {
109
+ return auth.expiresIn ?? undefined;
110
+ },
106
111
  };
107
112
  };
108
113
  /**
@@ -118,7 +123,9 @@ export default class FHIRStarter {
118
123
  try {
119
124
  callback(current);
120
125
  }
121
- catch { /* ignore */ }
126
+ catch {
127
+ /* ignore */
128
+ }
122
129
  return () => this.refreshCallbacks.delete(callback);
123
130
  };
124
131
  // --- Public async ---
@@ -129,8 +136,13 @@ export default class FHIRStarter {
129
136
  getJwks = async () => {
130
137
  const { keyId } = this.config, privateKey = await this.getPrivateKey(), jwk = await exportJWK(privateKey);
131
138
  // Public key only — strip private components
132
- delete jwk.d, delete jwk.p, delete jwk.q, delete jwk.dp, delete jwk.dq, delete jwk.qi;
133
- jwk.alg = "RS384", jwk.use = "sig";
139
+ (delete jwk.d,
140
+ delete jwk.p,
141
+ delete jwk.q,
142
+ delete jwk.dp,
143
+ delete jwk.dq,
144
+ delete jwk.qi);
145
+ ((jwk.alg = "RS384"), (jwk.use = "sig"));
134
146
  if (keyId)
135
147
  jwk.kid = keyId;
136
148
  return { keys: [jwk] };
@@ -179,8 +191,8 @@ export default class FHIRStarter {
179
191
  return;
180
192
  if (this.refreshTimer)
181
193
  clearTimeout(this.refreshTimer);
182
- const delay = !this.cache || this.refreshFailed
183
- ? this.refreshRetryMs
194
+ const delay = !this.cache || this.refreshFailed ?
195
+ this.refreshRetryMs
184
196
  : Math.max(1_000, this.cache.refreshAt - Date.now());
185
197
  this.refreshTimer = setTimeout(async () => {
186
198
  try {
@@ -230,14 +242,20 @@ export default class FHIRStarter {
230
242
  throw new Error("Token response missing access_token");
231
243
  if (typeof data.expires_in !== "number" || data.expires_in <= 0)
232
244
  throw new Error("Token response has invalid expires_in");
233
- const ttlMs = data.expires_in * 1000, bufferMs = Math.min(60_000, ttlMs / 2), now = Date.now(), expiresAt = now + ttlMs, refreshAt = expiresAt - bufferMs, cache = { accessToken: data.access_token, refreshAt, expiresAt };
245
+ const ttlMs = data.expires_in * 1000, bufferMs = Math.min(60_000, ttlMs / 2), now = Date.now(), expiresAt = now + ttlMs, refreshAt = expiresAt - bufferMs, cache = {
246
+ accessToken: data.access_token,
247
+ refreshAt,
248
+ expiresAt,
249
+ };
234
250
  this.cache = cache;
235
251
  this.notifyRefresh(cache.accessToken);
236
252
  return cache;
237
253
  };
238
254
  getPrivateKey = async () => {
239
255
  if (!this.privateKeyObj)
240
- this.privateKeyObj = await importPKCS8(this.privateKeyText, "RS384");
256
+ this.privateKeyObj = await importPKCS8(this.privateKeyText, "RS384", {
257
+ extractable: true,
258
+ });
241
259
  return this.privateKeyObj;
242
260
  };
243
261
  buildJwt = async () => {
@@ -260,7 +278,9 @@ export default class FHIRStarter {
260
278
  };
261
279
  // --- Static helpers ---
262
280
  static normalizeScopes(scopes) {
263
- return Array.isArray(scopes) ? scopes.filter(Boolean) : scopes.split(/\s+/).filter(Boolean);
281
+ return Array.isArray(scopes) ?
282
+ scopes.filter(Boolean)
283
+ : scopes.split(/\s+/).filter(Boolean);
264
284
  }
265
285
  static resolvePrivateKey(privateKey) {
266
286
  if (Buffer.isBuffer(privateKey))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhirstarterjs",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "SMART Backend Services auth lifecycle for any FHIR client",
5
5
  "type": "module",
6
6
  "main": "./dist/fhirstarter.js",
@@ -11,7 +11,13 @@
11
11
  "import": "./dist/fhirstarter.js"
12
12
  }
13
13
  },
14
- "files": ["dist", "ts", "types", "README.md", "LICENSE.md"],
14
+ "files": [
15
+ "dist",
16
+ "ts",
17
+ "types",
18
+ "README.md",
19
+ "LICENSE.md"
20
+ ],
15
21
  "engines": {
16
22
  "node": ">=20"
17
23
  },
@@ -29,4 +35,4 @@
29
35
  "@types/node": "^25.9.2",
30
36
  "typescript": "^6.0.3"
31
37
  }
32
- }
38
+ }
package/ts/fhirstarter.ts CHANGED
@@ -1,6 +1,11 @@
1
- import { importPKCS8, exportJWK, SignJWT, type JWTHeaderParameters } from "jose"
2
- import { randomUUID } from "node:crypto"
3
- import { readFileSync } from "node:fs"
1
+ import {
2
+ importPKCS8,
3
+ exportJWK,
4
+ SignJWT,
5
+ type JWTHeaderParameters,
6
+ } from "jose";
7
+ import { randomUUID } from "node:crypto";
8
+ import { readFileSync } from "node:fs";
4
9
 
5
10
  /**
6
11
  * SMART Backend Services auth provider.
@@ -9,60 +14,67 @@ import { readFileSync } from "node:fs"
9
14
  * using a private RSA key and the JWT Bearer client-assertion flow (RFC 7523).
10
15
  */
11
16
  export default class FHIRStarter implements Provider {
12
- private readonly config: AuthConfig
13
- private readonly privateKeyText: string
14
- private readonly refreshCallbacks: Set<RefreshCallback> = new Set()
15
- private cache: TokenCache | null = null
16
- private refreshPromise: Promise<string> | null = null
17
- private refreshTimer: ReturnType<typeof setTimeout> | null = null
18
- private privateKeyObj: Awaited<ReturnType<typeof importPKCS8>> | null = null
19
- private started = false
20
- private refreshFailed = false
21
- private refreshRetryMs = 5_000
17
+ private readonly config: AuthConfig;
18
+ private readonly privateKeyText: string;
19
+ private readonly refreshCallbacks: Set<RefreshCallback> = new Set();
20
+ private cache: TokenCache | null = null;
21
+ private refreshPromise: Promise<string> | null = null;
22
+ private refreshTimer: ReturnType<typeof setTimeout> | null = null;
23
+ private privateKeyObj: Awaited<ReturnType<typeof importPKCS8>> | null = null;
24
+ private started = false;
25
+ private refreshFailed = false;
26
+ private refreshRetryMs = 5_000;
22
27
 
23
28
  /**
24
29
  * @param config - Auth configuration including client ID, private key, token endpoint, and scopes.
25
30
  * @throws If any required field is missing, blank, or invalid.
26
31
  */
27
32
  constructor(config: AuthConfig) {
28
- if (!config.clientId) throw new Error("AuthConfig: clientId is required")
29
- if (!config.privateKey || (Buffer.isBuffer(config.privateKey) && config.privateKey.length === 0))
30
- throw new Error("AuthConfig: privateKey is required")
31
- if (!config.tokenEndpointUrl) throw new Error("AuthConfig: tokenEndpointUrl is required")
32
-
33
- const scopes = FHIRStarter.normalizeScopes(config.scopes)
34
- if (scopes.length === 0) throw new Error("AuthConfig: at least one scope is required")
33
+ if (!config.clientId) throw new Error("AuthConfig: clientId is required");
34
+ if (
35
+ !config.privateKey ||
36
+ (Buffer.isBuffer(config.privateKey) && config.privateKey.length === 0)
37
+ )
38
+ throw new Error("AuthConfig: privateKey is required");
39
+ if (!config.tokenEndpointUrl)
40
+ throw new Error("AuthConfig: tokenEndpointUrl is required");
41
+
42
+ const scopes = FHIRStarter.normalizeScopes(config.scopes);
43
+ if (scopes.length === 0)
44
+ throw new Error("AuthConfig: at least one scope is required");
35
45
 
36
46
  try {
37
- new URL(config.tokenEndpointUrl)
47
+ new URL(config.tokenEndpointUrl);
38
48
  } catch {
39
- throw new Error(`AuthConfig: tokenEndpointUrl is not a valid URL: ${config.tokenEndpointUrl}`)
49
+ throw new Error(
50
+ `AuthConfig: tokenEndpointUrl is not a valid URL: ${config.tokenEndpointUrl}`,
51
+ );
40
52
  }
41
53
 
42
- this.config = config
43
- this.privateKeyText = FHIRStarter.resolvePrivateKey(config.privateKey)
54
+ this.config = config;
55
+ this.privateKeyText = FHIRStarter.resolvePrivateKey(config.privateKey);
44
56
  }
45
57
 
46
58
  // --- Sync getters ---
47
59
 
48
60
  /** The current access token, or `null` if no valid token is cached. */
49
61
  get token(): string | null {
50
- return this.cache && Date.now() < this.cache.expiresAt
51
- ? this.cache.accessToken
52
- : null
62
+ return this.cache && Date.now() < this.cache.expiresAt ?
63
+ this.cache.accessToken
64
+ : null;
53
65
  }
54
66
 
55
67
  /** Seconds until the cached token expires, or `null` if no valid token is cached. */
56
68
  get expiresIn(): number | null {
57
- return this.cache && Date.now() < this.cache.expiresAt
58
- ? Math.ceil((this.cache.expiresAt - Date.now()) / 1000)
59
- : null
69
+ return this.cache && Date.now() < this.cache.expiresAt ?
70
+ Math.ceil((this.cache.expiresAt - Date.now()) / 1000)
71
+ : null;
60
72
  }
61
73
 
62
74
  /** Ready-to-use `Authorization` header value (e.g. `"Bearer <token>"`), or `null` if no valid token is cached. */
63
75
  get authorizationHeader(): string | null {
64
- const token = this.token
65
- return token ? `Bearer ${token}` : null
76
+ const token = this.token;
77
+ return token ? `Bearer ${token}` : null;
66
78
  }
67
79
 
68
80
  // --- Lifecycle ---
@@ -73,30 +85,30 @@ export default class FHIRStarter implements Provider {
73
85
  * @throws If the initial token acquisition fails.
74
86
  */
75
87
  start = async (): Promise<void> => {
76
- if (this.started) return
77
- this.started = true
78
- this.refreshRetryMs = 5_000
79
- this.refreshFailed = false
88
+ if (this.started) return;
89
+ this.started = true;
90
+ this.refreshRetryMs = 5_000;
91
+ this.refreshFailed = false;
80
92
  try {
81
- await this.getAccessToken()
93
+ await this.getAccessToken();
82
94
  } catch (err) {
83
- this.started = false
84
- throw err
95
+ this.started = false;
96
+ throw err;
85
97
  }
86
- this.scheduleRefresh()
87
- }
98
+ this.scheduleRefresh();
99
+ };
88
100
 
89
101
  /**
90
102
  * Stops the background refresh timer.
91
103
  * Any in-flight refresh will still complete, but no new timers will be scheduled.
92
104
  */
93
105
  stop = (): void => {
94
- this.started = false
106
+ this.started = false;
95
107
  if (this.refreshTimer) {
96
- clearTimeout(this.refreshTimer)
97
- this.refreshTimer = null
108
+ clearTimeout(this.refreshTimer);
109
+ this.refreshTimer = null;
98
110
  }
99
- }
111
+ };
100
112
 
101
113
  // --- Token adapters ---
102
114
 
@@ -106,13 +118,17 @@ export default class FHIRStarter implements Provider {
106
118
  * Useful for libraries that hold a reference to a token response object.
107
119
  */
108
120
  tokenResponse = (): LiveTokenResponse => {
109
- const auth = this
121
+ const auth = this;
110
122
  return {
111
123
  token_type: "bearer",
112
- get access_token() { return auth.token ?? undefined },
113
- get expires_in() { return auth.expiresIn ?? undefined },
114
- }
115
- }
124
+ get access_token() {
125
+ return auth.token ?? undefined;
126
+ },
127
+ get expires_in() {
128
+ return auth.expiresIn ?? undefined;
129
+ },
130
+ };
131
+ };
116
132
 
117
133
  /**
118
134
  * Registers a callback that is invoked whenever a new access token is obtained.
@@ -121,11 +137,16 @@ export default class FHIRStarter implements Provider {
121
137
  * @returns An unsubscribe function; call it to stop receiving updates.
122
138
  */
123
139
  onRefresh = (callback: RefreshCallback): (() => void) => {
124
- this.refreshCallbacks.add(callback)
125
- const current = this.token
126
- if (current) try { callback(current) } catch { /* ignore */ }
127
- return () => this.refreshCallbacks.delete(callback)
128
- }
140
+ this.refreshCallbacks.add(callback);
141
+ const current = this.token;
142
+ if (current)
143
+ try {
144
+ callback(current);
145
+ } catch {
146
+ /* ignore */
147
+ }
148
+ return () => this.refreshCallbacks.delete(callback);
149
+ };
129
150
 
130
151
  // --- Public async ---
131
152
 
@@ -134,18 +155,22 @@ export default class FHIRStarter implements Provider {
134
155
  * Private key material (`d`, `p`, `q`, etc.) is stripped before returning.
135
156
  */
136
157
  getJwks = async (): Promise<JwkSet> => {
137
- const
138
- { keyId } = this.config,
158
+ const { keyId } = this.config,
139
159
  privateKey = await this.getPrivateKey(),
140
- jwk = await exportJWK(privateKey)
160
+ jwk = await exportJWK(privateKey);
141
161
 
142
162
  // Public key only — strip private components
143
- delete jwk.d, delete jwk.p, delete jwk.q, delete jwk.dp, delete jwk.dq, delete jwk.qi
144
- jwk.alg = "RS384", jwk.use = "sig"
145
- if (keyId) jwk.kid = keyId
146
-
147
- return { keys: [jwk] }
148
- }
163
+ (delete jwk.d,
164
+ delete jwk.p,
165
+ delete jwk.q,
166
+ delete jwk.dp,
167
+ delete jwk.dq,
168
+ delete jwk.qi);
169
+ ((jwk.alg = "RS384"), (jwk.use = "sig"));
170
+ if (keyId) jwk.kid = keyId;
171
+
172
+ return { keys: [jwk] };
173
+ };
149
174
 
150
175
  /**
151
176
  * Returns a valid access token, refreshing if the cached token is at or past its refresh threshold.
@@ -153,74 +178,77 @@ export default class FHIRStarter implements Provider {
153
178
  * @throws If no cached token is available and the refresh request fails.
154
179
  */
155
180
  getAccessToken = async (): Promise<string> => {
156
- if (this.cache && Date.now() < this.cache.refreshAt) return this.cache.accessToken
157
- const staleCache = this.cache
181
+ if (this.cache && Date.now() < this.cache.refreshAt)
182
+ return this.cache.accessToken;
183
+ const staleCache = this.cache;
158
184
  try {
159
- return await this.doRefresh()
185
+ return await this.doRefresh();
160
186
  } catch (err) {
161
187
  // Graceful degradation: if the token is not actually expired yet, return it
162
- if (staleCache && Date.now() < staleCache.expiresAt) return staleCache.accessToken
163
- throw err
188
+ if (staleCache && Date.now() < staleCache.expiresAt)
189
+ return staleCache.accessToken;
190
+ throw err;
164
191
  }
165
- }
192
+ };
166
193
 
167
194
  // --- Private ---
168
195
 
169
196
  // Single-flight refresh — shared by getAccessToken and the proactive timer
170
197
  private doRefresh = (): Promise<string> => {
171
- if (this.refreshPromise) return this.refreshPromise
198
+ if (this.refreshPromise) return this.refreshPromise;
172
199
  this.refreshPromise = this.refreshAccessToken()
173
200
  .then((cache) => {
174
- this.refreshPromise = null
175
- this.refreshFailed = false
176
- this.refreshRetryMs = 5_000
177
- if (this.started) this.scheduleRefresh()
178
- return cache.accessToken
201
+ this.refreshPromise = null;
202
+ this.refreshFailed = false;
203
+ this.refreshRetryMs = 5_000;
204
+ if (this.started) this.scheduleRefresh();
205
+ return cache.accessToken;
179
206
  })
180
207
  .catch((err) => {
181
- this.refreshPromise = null
182
- throw err
183
- })
184
- return this.refreshPromise
185
- }
208
+ this.refreshPromise = null;
209
+ throw err;
210
+ });
211
+ return this.refreshPromise;
212
+ };
186
213
 
187
214
  private scheduleRefresh = (): void => {
188
- if (!this.started) return
189
- if (this.refreshTimer) clearTimeout(this.refreshTimer)
215
+ if (!this.started) return;
216
+ if (this.refreshTimer) clearTimeout(this.refreshTimer);
190
217
 
191
- const delay = !this.cache || this.refreshFailed
192
- ? this.refreshRetryMs
193
- : Math.max(1_000, this.cache.refreshAt - Date.now())
218
+ const delay =
219
+ !this.cache || this.refreshFailed ?
220
+ this.refreshRetryMs
221
+ : Math.max(1_000, this.cache.refreshAt - Date.now());
194
222
 
195
223
  this.refreshTimer = setTimeout(async () => {
196
224
  try {
197
- await this.doRefresh()
225
+ await this.doRefresh();
198
226
  } catch {
199
- this.refreshFailed = true
200
- this.refreshRetryMs = Math.min(this.refreshRetryMs * 2, 60_000)
201
- this.scheduleRefresh()
227
+ this.refreshFailed = true;
228
+ this.refreshRetryMs = Math.min(this.refreshRetryMs * 2, 60_000);
229
+ this.scheduleRefresh();
202
230
  }
203
- }, delay)
231
+ }, delay);
204
232
 
205
- this.refreshTimer.unref?.()
206
- }
233
+ this.refreshTimer.unref?.();
234
+ };
207
235
 
208
236
  private notifyRefresh = (token: string): void => {
209
237
  for (const callback of this.refreshCallbacks)
210
238
  try {
211
- callback(token)
239
+ callback(token);
212
240
  } catch {
213
241
  // Ignore callback failures; auth lifecycle must continue.
214
242
  }
215
- }
243
+ };
216
244
 
217
245
  private refreshAccessToken = async (): Promise<TokenCache> => {
218
- const
219
- jwt = await this.buildJwt(),
246
+ const jwt = await this.buildJwt(),
220
247
  scopes = FHIRStarter.normalizeScopes(this.config.scopes),
221
248
  body = new URLSearchParams({
222
249
  grant_type: "client_credentials",
223
- client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
250
+ client_assertion_type:
251
+ "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
224
252
  client_assertion: jwt,
225
253
  scope: scopes.join(" "),
226
254
  }),
@@ -229,48 +257,57 @@ export default class FHIRStarter implements Provider {
229
257
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
230
258
  body,
231
259
  signal: AbortSignal.timeout(30_000),
232
- })
260
+ });
233
261
 
234
262
  if (!res.ok) {
235
- const text = await res.text().catch(() => "(no body)")
236
- throw new Error(`Token request failed (${res.status}): ${text}`)
263
+ const text = await res.text().catch(() => "(no body)");
264
+ throw new Error(`Token request failed (${res.status}): ${text}`);
237
265
  }
238
266
 
239
- let data: TokenResponse
240
- try { data = await res.json() }
241
- catch { throw new Error("Token response is not valid JSON") }
267
+ let data: TokenResponse;
268
+ try {
269
+ data = await res.json();
270
+ } catch {
271
+ throw new Error("Token response is not valid JSON");
272
+ }
242
273
 
243
274
  if (!data.access_token || typeof data.access_token !== "string")
244
- throw new Error("Token response missing access_token")
275
+ throw new Error("Token response missing access_token");
245
276
  if (typeof data.expires_in !== "number" || data.expires_in <= 0)
246
- throw new Error("Token response has invalid expires_in")
277
+ throw new Error("Token response has invalid expires_in");
247
278
 
248
- const
249
- ttlMs = data.expires_in * 1000,
279
+ const ttlMs = data.expires_in * 1000,
250
280
  bufferMs = Math.min(60_000, ttlMs / 2),
251
281
  now = Date.now(),
252
282
  expiresAt = now + ttlMs,
253
283
  refreshAt = expiresAt - bufferMs,
254
- cache: TokenCache = { accessToken: data.access_token, refreshAt, expiresAt }
255
-
256
- this.cache = cache
257
- this.notifyRefresh(cache.accessToken)
258
- return cache
259
- }
260
-
261
- private getPrivateKey = async (): Promise<Awaited<ReturnType<typeof importPKCS8>>> => {
284
+ cache: TokenCache = {
285
+ accessToken: data.access_token,
286
+ refreshAt,
287
+ expiresAt,
288
+ };
289
+
290
+ this.cache = cache;
291
+ this.notifyRefresh(cache.accessToken);
292
+ return cache;
293
+ };
294
+
295
+ private getPrivateKey = async (): Promise<
296
+ Awaited<ReturnType<typeof importPKCS8>>
297
+ > => {
262
298
  if (!this.privateKeyObj)
263
- this.privateKeyObj = await importPKCS8(this.privateKeyText, "RS384")
264
- return this.privateKeyObj
265
- }
299
+ this.privateKeyObj = await importPKCS8(this.privateKeyText, "RS384", {
300
+ extractable: true,
301
+ });
302
+ return this.privateKeyObj;
303
+ };
266
304
 
267
305
  private buildJwt = async (): Promise<string> => {
268
- const
269
- { clientId, tokenEndpointUrl, keyId, jwksUrl } = this.config,
306
+ const { clientId, tokenEndpointUrl, keyId, jwksUrl } = this.config,
270
307
  privateKey = await this.getPrivateKey(),
271
- header: JWTHeaderParameters = { alg: "RS384", typ: "JWT" }
272
- if (keyId) header.kid = keyId
273
- if (jwksUrl) header.jku = jwksUrl
308
+ header: JWTHeaderParameters = { alg: "RS384", typ: "JWT" };
309
+ if (keyId) header.kid = keyId;
310
+ if (jwksUrl) header.jku = jwksUrl;
274
311
 
275
312
  return new SignJWT({
276
313
  iss: clientId,
@@ -282,25 +319,30 @@ export default class FHIRStarter implements Provider {
282
319
  .setIssuedAt()
283
320
  .setNotBefore("now")
284
321
  .setExpirationTime("5m")
285
- .sign(privateKey)
286
- }
322
+ .sign(privateKey);
323
+ };
287
324
 
288
325
  // --- Static helpers ---
289
326
 
290
327
  private static normalizeScopes(scopes: string | string[]): string[] {
291
- return Array.isArray(scopes) ? scopes.filter(Boolean) : scopes.split(/\s+/).filter(Boolean)
328
+ return Array.isArray(scopes) ?
329
+ scopes.filter(Boolean)
330
+ : scopes.split(/\s+/).filter(Boolean);
292
331
  }
293
332
 
294
333
  private static resolvePrivateKey(privateKey: string | Buffer): string {
295
- if (Buffer.isBuffer(privateKey)) return privateKey.toString("utf-8").trim()
334
+ if (Buffer.isBuffer(privateKey))
335
+ return privateKey.toString("utf-8").trim();
296
336
 
297
- const trimmed = privateKey.trim()
298
- if (trimmed.includes("-----BEGIN")) return trimmed
337
+ const trimmed = privateKey.trim();
338
+ if (trimmed.includes("-----BEGIN")) return trimmed;
299
339
 
300
340
  try {
301
- return readFileSync(trimmed, "utf-8").trim()
341
+ return readFileSync(trimmed, "utf-8").trim();
302
342
  } catch {
303
- throw new Error(`AuthConfig: privateKey must be PEM text, a Buffer, or a readable file path: ${trimmed}`)
343
+ throw new Error(
344
+ `AuthConfig: privateKey must be PEM text, a Buffer, or a readable file path: ${trimmed}`,
345
+ );
304
346
  }
305
347
  }
306
348
  }