{"id":51448629,"url":"https://github.com/faulkj/fhirstarter","last_synced_at":"2026-07-05T18:31:14.191Z","repository":{"id":363664862,"uuid":"1264351396","full_name":"faulkj/fhirstarter","owner":"faulkj","description":"SMART Backend Services auth lifecycle for any JavaScript FHIR client","archived":false,"fork":false,"pushed_at":"2026-06-16T22:12:15.000Z","size":29,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-17T00:13:43.699Z","etag":null,"topics":["authentication","back-end","emr","fhir","healthcare","smart-on-fhir","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/faulkj.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-09T20:01:53.000Z","updated_at":"2026-06-16T22:11:28.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/faulkj/fhirstarter","commit_stats":null,"previous_names":["faulkj/fhirstarter"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/faulkj/fhirstarter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/faulkj%2Ffhirstarter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/faulkj%2Ffhirstarter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/faulkj%2Ffhirstarter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/faulkj%2Ffhirstarter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/faulkj","download_url":"https://codeload.github.com/faulkj/fhirstarter/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/faulkj%2Ffhirstarter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35165553,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","back-end","emr","fhir","healthcare","smart-on-fhir","typescript"],"created_at":"2026-07-05T18:31:14.122Z","updated_at":"2026-07-05T18:31:14.182Z","avatar_url":"https://github.com/faulkj.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🔥 fhirStarter\n\nSMART on FHIR Backend Services auth lifecycle for any FHIR client.\n\n## Install\n\n```sh\nnpm install fhirstarterjs\n```\n\n## Usage\n\nThis example uses the official `fhirclient` package as the FHIR client; `fhirStarter` only manages auth.\n\n```ts\nimport FHIR from \"fhirclient\"\nimport fhirStarter from \"fhirstarterjs\"\n\nconst auth = new fhirStarter({\n   clientId: \"your-client-id\",\n   privateKey: \"./privatekey.pem\",\n   tokenEndpointUrl: \"https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token\",\n   scopes: [\"system/Patient.rs\", \"system/Observation.rs\"],\n})\n\nawait auth.start()\n\nconst client = FHIR.client({\n   serverUrl: \"https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4\",\n   tokenResponse: auth.tokenResponse(),\n})\n\nconst bundle = await client.request(\"Patient?family=Smith\")\n```\n\n`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.\n\n`fhirStarter` does not fetch FHIR resources and does not bundle a FHIR client. It manages the auth lifecycle; the FHIR client does the rest.\n\n`privateKey` can be PEM text, a `Buffer` from `readFileSync`, or a path to a PKCS#8 PEM file.\n\n## TypeScript types\n\nIf needed, you can import the public types directly:\n\n```ts\nimport type { AuthConfig, JwkSet, LiveTokenResponse, Provider } from \"fhirstarterjs\"\n```\n\n## Other FHIR clients\n\nFor clients with a bearer token setter (e.g. `fhir-kit-client`):\n\n```ts\nconst unsubscribe = auth.onRefresh((token) =\u003e {\n   client.bearerToken = token\n})\n```\n\nFor raw `fetch` or any other HTTP client:\n\n```ts\nconst token = await auth.getAccessToken()\nconst res = await fetch(url, {\n   headers: { Authorization: `Bearer ${token}` },\n})\n```\n\n## API\n\n`new fhirStarter(config)`\n\n| Member | Returns | Description |\n|---|---|---|\n| `start()` | `Promise\u003cvoid\u003e` | Fetch first token and begin proactive refresh loop |\n| `stop()` | `void` | Clear the refresh timer |\n| `token` | `string \\| null` | Current valid token, or null if expired |\n| `expiresIn` | `number \\| null` | Seconds until actual expiry, or null |\n| `authorizationHeader` | `string \\| null` | `Bearer \u003ctoken\u003e` or null |\n| `getAccessToken()` | `Promise\u003cstring\u003e` | Async valid token with lazy refresh |\n| `tokenResponse()` | `LiveTokenResponse` | Getter-backed token response for `fhirclient` |\n| `onRefresh(callback)` | `() =\u003e void` | Subscribe to token updates — returns unsubscribe |\n| `getJwks()` | `Promise\u003cJwkSet\u003e` | Public JWKS derived from the private key |\n| `fhirStarter.thumbprint(privateKey)` | `string` | RFC 7638 JWK Thumbprint (base64url SHA-256) |\n\n`getJwks()` strips private key material — host the output JSON at your registered JWKS URL and pass that URL as `jwksUrl` so the JWT `jku` header is set automatically.\n\n## Thumbprint\n\nDerive a deterministic `kid` from a private key without instantiating the class:\n\n```ts\nimport fhirStarter from \"fhirstarterjs\"\n\nconst kid = fhirStarter.thumbprint(\"./privatekey.pem\")\nconsole.log(kid) // base64url SHA-256 of the canonical RSA public JWK\n```\n\nThis implements RFC 7638 — the SHA-256 of the sorted canonical JWK members `{e, kty, n}`, base64url-encoded. Use it as the `keyId` when registering your JWKS.\n\n## JWKS\n\nSome SMART Backend Services registrations require a public JWKS URL when using `jku`. Generate it from the same private key you use for auth:\n\n```ts\nimport { writeFileSync } from \"node:fs\"\nimport fhirStarter from \"fhirstarterjs\"\n\nconst auth = new fhirStarter({\n   clientId: \"your-client-id\",\n   privateKey: \"./privatekey.pem\",\n   tokenEndpointUrl: \"https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token\",\n   scopes: [\"system/Patient.rs\"],\n   keyId: \"my-key-id\",\n   jwksUrl: \"https://example.com/.well-known/jwks.json\",\n})\n\n// No need to call auth.start() — getJwks() only needs the private key.\nconst jwks = await auth.getJwks()\nwriteFileSync(\"./jwks.json\", JSON.stringify(jwks, null, 3))\n```\n\nHost `jwks.json` at the exact URL you register with your authorization server, then pass that URL as `jwksUrl`. If you set `keyId`, the generated key includes `kid`, and signed JWTs use the same `kid` header.\n\n## Compatibility\n\n`tokenResponse()` is designed for `fhirclient.request()`. If a client copies the token at construction time rather than reading it per-request, use `onRefresh()` to update or recreate that client instead. If `fhirclient` clears its internal state after a 401, recreate the client instance with `auth.tokenResponse()`.\n\n## Scripts\n\n| Command | What |\n|---|---|\n| `npm run check` | `tsc --noEmit` |\n| `npm run build` | Compile to `dist/` |\n\n## Notes\n\n- Call `auth.start()` to fetch the first token and begin the proactive refresh loop — call `auth.stop()` during shutdown in long-running processes\n- Tokens are cached with separate refresh and expiry timestamps — if a refresh fails but the token is not yet expired, the old token remains usable\n- Concurrent callers share a single in-flight token refresh\n- JWT assertions are signed RS384, expire after 5 minutes\n- Requires Node 20+, a PKCS#8 RSA key, and SMART Backend Services scopes\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffaulkj%2Ffhirstarter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffaulkj%2Ffhirstarter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffaulkj%2Ffhirstarter/lists"}