fhirstarterjs 1.0.4 → 1.0.6
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/LICENSE.md +44 -24
- package/README.md +8 -8
- package/dist/fhirstarter.js +8 -5
- package/package.json +29 -5
- package/ts/fhirstarter.ts +8 -5
- package/types/api.d.ts +2 -1
- package/types/internal.d.ts +2 -0
package/LICENSE.md
CHANGED
|
@@ -1,24 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
# Kopimi License
|
|
2
|
+
|
|
3
|
+
This software is released under the Kopimi License.
|
|
4
|
+
|
|
5
|
+
## Permission
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
8
|
+
this software and associated documentation files (the "Software"), to copy,
|
|
9
|
+
modify, merge, publish, distribute, sublicense, sell, use, study,
|
|
10
|
+
reverse-engineer, decompile, and otherwise deal in the Software without
|
|
11
|
+
restriction, including without limitation for commercial or non-commercial
|
|
12
|
+
purposes.
|
|
13
|
+
|
|
14
|
+
Commercial use, sale, resale, or distribution for profit must include clear
|
|
15
|
+
attribution to the original author(s) in the distributed copy and in any
|
|
16
|
+
customary public-facing product notices, documentation, credits, or legal
|
|
17
|
+
notices associated with that use.
|
|
18
|
+
|
|
19
|
+
## Kopimi Propagation
|
|
20
|
+
|
|
21
|
+
The Software is Kopimi. Copies, substantial portions, and derivative works must
|
|
22
|
+
preserve the freedom to copy, share, modify, remix, and redistribute the
|
|
23
|
+
Software. No copy, substantial portion, or derivative work may be distributed
|
|
24
|
+
under terms that prohibit those freedoms.
|
|
25
|
+
|
|
26
|
+
License notices, author notices, and Kopimi notices included with the Software
|
|
27
|
+
may not be removed from redistributed copies, substantial portions, or
|
|
28
|
+
derivative works.
|
|
29
|
+
|
|
30
|
+
Attribution to the original author(s) is appreciated for non-commercial use and
|
|
31
|
+
required for commercial use as described above.
|
|
32
|
+
|
|
33
|
+
## Disclaimer
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
36
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
37
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE
|
|
38
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
39
|
+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
40
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
41
|
+
|
|
42
|
+
This license is itself Kopimi. Copy it, use it, and remix it.
|
|
43
|
+
|
|
44
|
+
For more information, please refer to <https://kopimi.com>
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 🔥
|
|
1
|
+
# 🔥 fhirStarter
|
|
2
2
|
|
|
3
3
|
SMART on FHIR Backend Services auth lifecycle for any FHIR client.
|
|
4
4
|
|
|
@@ -10,13 +10,13 @@ npm install fhirstarterjs
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
This example uses the official `fhirclient` package as the FHIR client; `
|
|
13
|
+
This example uses the official `fhirclient` package as the FHIR client; `fhirStarter` only manages auth.
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
import FHIR from "fhirclient"
|
|
17
|
-
import
|
|
17
|
+
import fhirStarter from "fhirstarterjs"
|
|
18
18
|
|
|
19
|
-
const auth = new
|
|
19
|
+
const auth = new fhirStarter({
|
|
20
20
|
clientId: "your-client-id",
|
|
21
21
|
privateKey: "./privatekey.pem",
|
|
22
22
|
tokenEndpointUrl: "https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token",
|
|
@@ -35,7 +35,7 @@ const bundle = await client.request("Patient?family=Smith")
|
|
|
35
35
|
|
|
36
36
|
`auth.start()` fetches the first token and starts the proactive refresh loop. `auth.tokenResponse()` returns a live getter-backed object — `fhirclient` reads `access_token` dynamically per request, so it always picks up the latest token.
|
|
37
37
|
|
|
38
|
-
`
|
|
38
|
+
`fhirStarter` does not fetch FHIR resources and does not bundle a FHIR client. It manages the auth lifecycle; the FHIR client does the rest.
|
|
39
39
|
|
|
40
40
|
`privateKey` can be PEM text, a `Buffer` from `readFileSync`, or a path to a PKCS#8 PEM file.
|
|
41
41
|
|
|
@@ -68,7 +68,7 @@ const res = await fetch(url, {
|
|
|
68
68
|
|
|
69
69
|
## API
|
|
70
70
|
|
|
71
|
-
`new
|
|
71
|
+
`new fhirStarter(config)`
|
|
72
72
|
|
|
73
73
|
| Member | Returns | Description |
|
|
74
74
|
|---|---|---|
|
|
@@ -90,9 +90,9 @@ Some SMART Backend Services registrations require a public JWKS URL when using `
|
|
|
90
90
|
|
|
91
91
|
```ts
|
|
92
92
|
import { writeFileSync } from "node:fs"
|
|
93
|
-
import
|
|
93
|
+
import fhirStarter from "fhirstarterjs"
|
|
94
94
|
|
|
95
|
-
const auth = new
|
|
95
|
+
const auth = new fhirStarter({
|
|
96
96
|
clientId: "your-client-id",
|
|
97
97
|
privateKey: "./privatekey.pem",
|
|
98
98
|
tokenEndpointUrl: "https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token",
|
package/dist/fhirstarter.js
CHANGED
|
@@ -7,7 +7,7 @@ import { readFileSync } from "node:fs";
|
|
|
7
7
|
* Manages client-credentials token acquisition and proactive refresh
|
|
8
8
|
* using a private RSA key and the JWT Bearer client-assertion flow (RFC 7523).
|
|
9
9
|
*/
|
|
10
|
-
export default class
|
|
10
|
+
export default class fhirStarter {
|
|
11
11
|
config;
|
|
12
12
|
privateKeyText;
|
|
13
13
|
refreshCallbacks = new Set();
|
|
@@ -30,7 +30,7 @@ export default class FHIRStarter {
|
|
|
30
30
|
throw new Error("AuthConfig: privateKey is required");
|
|
31
31
|
if (!config.tokenEndpointUrl)
|
|
32
32
|
throw new Error("AuthConfig: tokenEndpointUrl is required");
|
|
33
|
-
const scopes =
|
|
33
|
+
const scopes = fhirStarter.normalizeScopes(config.scopes);
|
|
34
34
|
if (scopes.length === 0)
|
|
35
35
|
throw new Error("AuthConfig: at least one scope is required");
|
|
36
36
|
try {
|
|
@@ -40,7 +40,7 @@ export default class FHIRStarter {
|
|
|
40
40
|
throw new Error(`AuthConfig: tokenEndpointUrl is not a valid URL: ${config.tokenEndpointUrl}`);
|
|
41
41
|
}
|
|
42
42
|
this.config = config;
|
|
43
|
-
this.privateKeyText =
|
|
43
|
+
this.privateKeyText = fhirStarter.resolvePrivateKey(config.privateKey);
|
|
44
44
|
}
|
|
45
45
|
// --- Sync getters ---
|
|
46
46
|
/** The current access token, or `null` if no valid token is cached. */
|
|
@@ -108,6 +108,9 @@ export default class FHIRStarter {
|
|
|
108
108
|
get expires_in() {
|
|
109
109
|
return auth.expiresIn ?? undefined;
|
|
110
110
|
},
|
|
111
|
+
get scope() {
|
|
112
|
+
return auth.cache?.scope;
|
|
113
|
+
},
|
|
111
114
|
};
|
|
112
115
|
};
|
|
113
116
|
/**
|
|
@@ -216,7 +219,7 @@ export default class FHIRStarter {
|
|
|
216
219
|
}
|
|
217
220
|
};
|
|
218
221
|
refreshAccessToken = async () => {
|
|
219
|
-
const jwt = await this.buildJwt(), scopes =
|
|
222
|
+
const jwt = await this.buildJwt(), scopes = fhirStarter.normalizeScopes(this.config.scopes), body = new URLSearchParams({
|
|
220
223
|
grant_type: "client_credentials",
|
|
221
224
|
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
|
222
225
|
client_assertion: jwt,
|
|
@@ -246,6 +249,7 @@ export default class FHIRStarter {
|
|
|
246
249
|
accessToken: data.access_token,
|
|
247
250
|
refreshAt,
|
|
248
251
|
expiresAt,
|
|
252
|
+
...(typeof data.scope === "string" && { scope: data.scope }),
|
|
249
253
|
};
|
|
250
254
|
this.cache = cache;
|
|
251
255
|
this.notifyRefresh(cache.accessToken);
|
|
@@ -272,7 +276,6 @@ export default class FHIRStarter {
|
|
|
272
276
|
})
|
|
273
277
|
.setProtectedHeader(header)
|
|
274
278
|
.setIssuedAt()
|
|
275
|
-
.setNotBefore(0)
|
|
276
279
|
.setExpirationTime("5m")
|
|
277
280
|
.sign(privateKey);
|
|
278
281
|
};
|
package/package.json
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fhirstarterjs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "SMART Backend Services auth lifecycle for any FHIR client",
|
|
5
|
+
"author": "Joshua Faulkenberry <j@joshuafaulkenberry.com> (https://joshuafaulkenberry.com/)",
|
|
6
|
+
"license": "Kopimi",
|
|
7
|
+
"homepage": "https://github.com/FaulkJ/fhirstarter#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/FaulkJ/fhirstarter.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/FaulkJ/fhirstarter/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"fhir",
|
|
17
|
+
"smart",
|
|
18
|
+
"smart-on-fhir",
|
|
19
|
+
"backend-services",
|
|
20
|
+
"oauth",
|
|
21
|
+
"jwt",
|
|
22
|
+
"hl7",
|
|
23
|
+
"healthcare",
|
|
24
|
+
"ehr",
|
|
25
|
+
"epic",
|
|
26
|
+
"fhirclient",
|
|
27
|
+
"token",
|
|
28
|
+
"jwks"
|
|
29
|
+
],
|
|
5
30
|
"type": "module",
|
|
6
31
|
"main": "./dist/fhirstarter.js",
|
|
7
|
-
"types": "./
|
|
32
|
+
"types": "./types/index.d.ts",
|
|
8
33
|
"exports": {
|
|
9
34
|
".": {
|
|
10
35
|
"types": "./types/index.d.ts",
|
|
11
36
|
"import": "./dist/fhirstarter.js"
|
|
12
37
|
}
|
|
13
38
|
},
|
|
39
|
+
"sideEffects": false,
|
|
14
40
|
"files": [
|
|
15
41
|
"dist",
|
|
16
42
|
"ts",
|
|
@@ -21,8 +47,6 @@
|
|
|
21
47
|
"engines": {
|
|
22
48
|
"node": ">=20"
|
|
23
49
|
},
|
|
24
|
-
"author": "Joshua Faulkenberry <j@joshuafaulkenberry.com> (https://joshuafaulkenberry.com/)",
|
|
25
|
-
"license": "Kopimi",
|
|
26
50
|
"scripts": {
|
|
27
51
|
"check": "tsc --noEmit",
|
|
28
52
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -32,7 +56,7 @@
|
|
|
32
56
|
"jose": "^6.2.3"
|
|
33
57
|
},
|
|
34
58
|
"devDependencies": {
|
|
35
|
-
"@types/node": "^25.9.
|
|
59
|
+
"@types/node": "^25.9.3",
|
|
36
60
|
"typescript": "^6.0.3"
|
|
37
61
|
}
|
|
38
62
|
}
|
package/ts/fhirstarter.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { readFileSync } from "node:fs";
|
|
|
13
13
|
* Manages client-credentials token acquisition and proactive refresh
|
|
14
14
|
* using a private RSA key and the JWT Bearer client-assertion flow (RFC 7523).
|
|
15
15
|
*/
|
|
16
|
-
export default class
|
|
16
|
+
export default class fhirStarter implements Provider {
|
|
17
17
|
private readonly config: AuthConfig;
|
|
18
18
|
private readonly privateKeyText: string;
|
|
19
19
|
private readonly refreshCallbacks: Set<RefreshCallback> = new Set();
|
|
@@ -39,7 +39,7 @@ export default class FHIRStarter implements Provider {
|
|
|
39
39
|
if (!config.tokenEndpointUrl)
|
|
40
40
|
throw new Error("AuthConfig: tokenEndpointUrl is required");
|
|
41
41
|
|
|
42
|
-
const scopes =
|
|
42
|
+
const scopes = fhirStarter.normalizeScopes(config.scopes);
|
|
43
43
|
if (scopes.length === 0)
|
|
44
44
|
throw new Error("AuthConfig: at least one scope is required");
|
|
45
45
|
|
|
@@ -52,7 +52,7 @@ export default class FHIRStarter implements Provider {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
this.config = config;
|
|
55
|
-
this.privateKeyText =
|
|
55
|
+
this.privateKeyText = fhirStarter.resolvePrivateKey(config.privateKey);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// --- Sync getters ---
|
|
@@ -127,6 +127,9 @@ export default class FHIRStarter implements Provider {
|
|
|
127
127
|
get expires_in() {
|
|
128
128
|
return auth.expiresIn ?? undefined;
|
|
129
129
|
},
|
|
130
|
+
get scope() {
|
|
131
|
+
return auth.cache?.scope;
|
|
132
|
+
},
|
|
130
133
|
};
|
|
131
134
|
};
|
|
132
135
|
|
|
@@ -244,7 +247,7 @@ export default class FHIRStarter implements Provider {
|
|
|
244
247
|
|
|
245
248
|
private refreshAccessToken = async (): Promise<TokenCache> => {
|
|
246
249
|
const jwt = await this.buildJwt(),
|
|
247
|
-
scopes =
|
|
250
|
+
scopes = fhirStarter.normalizeScopes(this.config.scopes),
|
|
248
251
|
body = new URLSearchParams({
|
|
249
252
|
grant_type: "client_credentials",
|
|
250
253
|
client_assertion_type:
|
|
@@ -285,6 +288,7 @@ export default class FHIRStarter implements Provider {
|
|
|
285
288
|
accessToken: data.access_token,
|
|
286
289
|
refreshAt,
|
|
287
290
|
expiresAt,
|
|
291
|
+
...(typeof data.scope === "string" && { scope: data.scope }),
|
|
288
292
|
};
|
|
289
293
|
|
|
290
294
|
this.cache = cache;
|
|
@@ -317,7 +321,6 @@ export default class FHIRStarter implements Provider {
|
|
|
317
321
|
})
|
|
318
322
|
.setProtectedHeader(header)
|
|
319
323
|
.setIssuedAt()
|
|
320
|
-
.setNotBefore(0)
|
|
321
324
|
.setExpirationTime("5m")
|
|
322
325
|
.sign(privateKey);
|
|
323
326
|
};
|
package/types/api.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ declare global {
|
|
|
21
21
|
keys: import("jose").JWK[]
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/** Auth lifecycle provider implemented by {@link
|
|
24
|
+
/** Auth lifecycle provider implemented by {@link fhirStarter}. */
|
|
25
25
|
interface Provider {
|
|
26
26
|
/** Current valid token, or null if no unexpired token is cached. */
|
|
27
27
|
readonly token: string | null
|
|
@@ -48,6 +48,7 @@ declare global {
|
|
|
48
48
|
token_type: "bearer"
|
|
49
49
|
readonly access_token: string | undefined
|
|
50
50
|
readonly expires_in: number | undefined
|
|
51
|
+
readonly scope: string | undefined
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
package/types/internal.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ declare global {
|
|
|
14
14
|
refreshAt: number
|
|
15
15
|
/** Epoch ms — actual token expiry. Never treat as expired before this. */
|
|
16
16
|
expiresAt: number
|
|
17
|
+
/** Granted scopes returned by the token endpoint, if any. */
|
|
18
|
+
scope?: string
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
type RefreshCallback = (token: string) => void
|