fhirstarterjs 1.0.6 → 1.0.7

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 CHANGED
@@ -81,9 +81,23 @@ const res = await fetch(url, {
81
81
  | `tokenResponse()` | `LiveTokenResponse` | Getter-backed token response for `fhirclient` |
82
82
  | `onRefresh(callback)` | `() => void` | Subscribe to token updates — returns unsubscribe |
83
83
  | `getJwks()` | `Promise<JwkSet>` | Public JWKS derived from the private key |
84
+ | `fhirStarter.thumbprint(privateKey)` | `string` | RFC 7638 JWK Thumbprint (base64url SHA-256) |
84
85
 
85
86
  `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.
86
87
 
88
+ ## Thumbprint
89
+
90
+ Derive a deterministic `kid` from a private key without instantiating the class:
91
+
92
+ ```ts
93
+ import fhirStarter from "fhirstarterjs"
94
+
95
+ const kid = fhirStarter.thumbprint("./privatekey.pem")
96
+ console.log(kid) // base64url SHA-256 of the canonical RSA public JWK
97
+ ```
98
+
99
+ This 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.
100
+
87
101
  ## JWKS
88
102
 
89
103
  Some SMART Backend Services registrations require a public JWKS URL when using `jku`. Generate it from the same private key you use for auth:
@@ -1,5 +1,5 @@
1
1
  import { importPKCS8, exportJWK, SignJWT, } from "jose";
2
- import { randomUUID } from "node:crypto";
2
+ import { createHash, createPrivateKey, createPublicKey, randomUUID } from "node:crypto";
3
3
  import { readFileSync } from "node:fs";
4
4
  /**
5
5
  * SMART Backend Services auth provider.
@@ -298,4 +298,19 @@ export default class fhirStarter {
298
298
  throw new Error(`AuthConfig: privateKey must be PEM text, a Buffer, or a readable file path: ${trimmed}`);
299
299
  }
300
300
  }
301
+ /**
302
+ * Derives a deterministic key identifier from a private key using an
303
+ * RFC 7638 JWK Thumbprint (SHA-256 of canonical RSA public JWK members, base64url).
304
+ *
305
+ * @param privateKey - RSA PKCS#8 PEM text, a Buffer, or a readable file path.
306
+ * @returns A base64url-encoded SHA-256 thumbprint string.
307
+ * @throws If the key is not RSA or cannot be parsed.
308
+ */
309
+ static thumbprint(privateKey) {
310
+ const pem = fhirStarter.resolvePrivateKey(privateKey), key = createPrivateKey(pem);
311
+ if (key.asymmetricKeyType !== "rsa")
312
+ throw new Error(`thumbprint: expected RSA key, got ${key.asymmetricKeyType}`);
313
+ const pub = createPublicKey(key).export({ format: "jwk" }), canonical = JSON.stringify({ e: pub.e, kty: "RSA", n: pub.n });
314
+ return createHash("sha256").update(canonical).digest("base64url");
315
+ }
301
316
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhirstarterjs",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "SMART Backend Services auth lifecycle for any FHIR client",
5
5
  "author": "Joshua Faulkenberry <j@joshuafaulkenberry.com> (https://joshuafaulkenberry.com/)",
6
6
  "license": "Kopimi",
package/ts/fhirstarter.ts CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  SignJWT,
5
5
  type JWTHeaderParameters,
6
6
  } from "jose";
7
- import { randomUUID } from "node:crypto";
7
+ import { createHash, createPrivateKey, createPublicKey, randomUUID } from "node:crypto";
8
8
  import { readFileSync } from "node:fs";
9
9
 
10
10
  /**
@@ -348,4 +348,24 @@ export default class fhirStarter implements Provider {
348
348
  );
349
349
  }
350
350
  }
351
+
352
+ /**
353
+ * Derives a deterministic key identifier from a private key using an
354
+ * RFC 7638 JWK Thumbprint (SHA-256 of canonical RSA public JWK members, base64url).
355
+ *
356
+ * @param privateKey - RSA PKCS#8 PEM text, a Buffer, or a readable file path.
357
+ * @returns A base64url-encoded SHA-256 thumbprint string.
358
+ * @throws If the key is not RSA or cannot be parsed.
359
+ */
360
+ static thumbprint(privateKey: string | Buffer): string {
361
+ const
362
+ pem = fhirStarter.resolvePrivateKey(privateKey),
363
+ key = createPrivateKey(pem)
364
+ if (key.asymmetricKeyType !== "rsa")
365
+ throw new Error(`thumbprint: expected RSA key, got ${key.asymmetricKeyType}`)
366
+ const
367
+ pub = createPublicKey(key).export({ format: "jwk" }) as { e?: string, n?: string, kty?: string },
368
+ canonical = JSON.stringify({ e: pub.e, kty: "RSA", n: pub.n })
369
+ return createHash("sha256").update(canonical).digest("base64url")
370
+ }
351
371
  }