@fhirfly-io/shl 0.2.0 → 0.3.1

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.
Files changed (50) hide show
  1. package/README.md +42 -6
  2. package/dist/{chunk-UFFYRACT.cjs → chunk-63Q54EKN.cjs} +40 -21
  3. package/dist/chunk-63Q54EKN.cjs.map +1 -0
  4. package/dist/{chunk-5I5H3SLO.cjs → chunk-LXJB46WB.cjs} +2 -2
  5. package/dist/chunk-LXJB46WB.cjs.map +1 -0
  6. package/dist/{chunk-CNVYKA4D.cjs → chunk-QXSWM5QV.cjs} +46 -21
  7. package/dist/chunk-QXSWM5QV.cjs.map +1 -0
  8. package/dist/{chunk-AIOYPNKN.js → chunk-YBDRWUQU.js} +32 -13
  9. package/dist/chunk-YBDRWUQU.js.map +1 -0
  10. package/dist/{chunk-NEBFJSJW.js → chunk-YV7ZD6OM.js} +2 -2
  11. package/dist/chunk-YV7ZD6OM.js.map +1 -0
  12. package/dist/{chunk-7WIM2QP5.js → chunk-ZEE5RXIS.js} +46 -21
  13. package/dist/chunk-ZEE5RXIS.js.map +1 -0
  14. package/dist/cli.cjs +14 -14
  15. package/dist/cli.js +3 -3
  16. package/dist/express.cjs +2 -2
  17. package/dist/express.d.cts +2 -2
  18. package/dist/express.d.ts +2 -2
  19. package/dist/express.js +1 -1
  20. package/dist/fastify.cjs +16 -2
  21. package/dist/fastify.cjs.map +1 -1
  22. package/dist/fastify.d.cts +3 -3
  23. package/dist/fastify.d.ts +3 -3
  24. package/dist/fastify.js +15 -1
  25. package/dist/fastify.js.map +1 -1
  26. package/dist/index.cjs +3 -3
  27. package/dist/index.d.cts +28 -3
  28. package/dist/index.d.ts +28 -3
  29. package/dist/index.js +1 -1
  30. package/dist/lambda.cjs +2 -2
  31. package/dist/lambda.d.cts +2 -2
  32. package/dist/lambda.d.ts +2 -2
  33. package/dist/lambda.js +1 -1
  34. package/dist/server.cjs +7 -7
  35. package/dist/server.d.cts +13 -6
  36. package/dist/server.d.ts +13 -6
  37. package/dist/server.js +2 -2
  38. package/dist/{storage-DYEX5kiP.d.ts → storage-B3GyJD2y.d.ts} +1 -1
  39. package/dist/{storage-D1NajOTq.d.cts → storage-BwszYwFo.d.cts} +1 -1
  40. package/dist/{types-BbvJirBn.d.cts → types-BegxU0wQ.d.ts} +22 -2
  41. package/dist/{types-DpkUjBYr.d.ts → types-Doq5cGNm.d.cts} +8 -1
  42. package/dist/{types-DpkUjBYr.d.cts → types-Doq5cGNm.d.ts} +8 -1
  43. package/dist/{types-qPv1O_sF.d.ts → types-hHf-a3hH.d.cts} +22 -2
  44. package/package.json +1 -1
  45. package/dist/chunk-5I5H3SLO.cjs.map +0 -1
  46. package/dist/chunk-7WIM2QP5.js.map +0 -1
  47. package/dist/chunk-AIOYPNKN.js.map +0 -1
  48. package/dist/chunk-CNVYKA4D.cjs.map +0 -1
  49. package/dist/chunk-NEBFJSJW.js.map +0 -1
  50. package/dist/chunk-UFFYRACT.cjs.map +0 -1
package/README.md CHANGED
@@ -14,13 +14,18 @@ Raw codes + patient info → Enriched FHIR Bundle → Encrypted SHL → QR code
14
14
 
15
15
  PHI never leaves your server. Only terminology codes are sent to FHIRfly for enrichment — no BAA required.
16
16
 
17
+ ## Prerequisites
18
+
19
+ - **Node.js** 18 or later
20
+ - **FHIRfly API key** — optional, for code enrichment (display names, SNOMED mappings). [Get a free key](https://fhirfly.io/register). Without it, use manual `code`/`system`/`display` input or `bySNOMED`/`fromResource` (no API call needed).
21
+
17
22
  ## Installation
18
23
 
19
24
  ```bash
20
25
  npm install @fhirfly-io/shl
21
26
  ```
22
27
 
23
- For FHIRfly API enrichment (recommended):
28
+ For FHIRfly API enrichment (optional — adds display names and SNOMED cross-mappings):
24
29
 
25
30
  ```bash
26
31
  npm install @fhirfly-io/shl @fhirfly-io/terminology
@@ -28,17 +33,46 @@ npm install @fhirfly-io/shl @fhirfly-io/terminology
28
33
 
29
34
  ## Quick Start
30
35
 
36
+ ### Without FHIRfly API (no key needed)
37
+
38
+ ```typescript
39
+ import { IPS, SHL } from "@fhirfly-io/shl";
40
+
41
+ const bundle = new IPS.Bundle({
42
+ name: "Maria Garcia",
43
+ birthDate: "1985-03-15",
44
+ gender: "female",
45
+ });
46
+
47
+ // Manual coding — no API dependency
48
+ bundle.addMedication({ code: "376988009", system: "http://snomed.info/sct", display: "Levothyroxine" });
49
+ bundle.addCondition({ code: "44054006", system: "http://snomed.info/sct", display: "Type 2 diabetes" });
50
+ bundle.addAllergy({ bySNOMED: "387207008" });
51
+
52
+ const fhirBundle = await bundle.build();
53
+
54
+ const storage = new SHL.LocalStorage({ directory: "./shl-data", baseUrl: "http://localhost:3456/shl" });
55
+ const result = await SHL.create({ bundle: fhirBundle, storage, passcode: "1234" });
56
+
57
+ console.log(result.url); // shlink:/eyJ...
58
+ console.log(result.qrCode); // data:image/png;base64,...
59
+ ```
60
+
61
+ ### With FHIRfly API (enriched)
62
+
31
63
  ```typescript
32
64
  import { IPS, SHL } from "@fhirfly-io/shl";
33
65
  import Fhirfly from "@fhirfly-io/terminology";
34
66
 
35
67
  const client = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY });
36
68
 
37
- // Build the IPS Bundle
38
69
  const bundle = new IPS.Bundle({
39
- patient: { name: "Maria Garcia", birthDate: "1985-03-15", gender: "female" },
70
+ name: "Maria Garcia",
71
+ birthDate: "1985-03-15",
72
+ gender: "female",
40
73
  });
41
74
 
75
+ // Code-based input — FHIRfly enriches with display names, SNOMED mappings, etc.
42
76
  bundle.addMedication({ byNDC: "00071015523", fhirfly: client.ndc });
43
77
  bundle.addCondition({ byICD10: "E11.9", fhirfly: client.icd10 });
44
78
  bundle.addAllergy({ bySNOMED: "387207008" });
@@ -48,7 +82,7 @@ bundle.addDocument({ content: pdfBuffer, contentType: "application/pdf", title:
48
82
 
49
83
  const fhirBundle = await bundle.build();
50
84
 
51
- // Create the SHL (zero-infra with FhirflyStorage)
85
+ // FhirflyStorage zero infrastructure, included free in all plans
52
86
  const storage = new SHL.FhirflyStorage({ apiKey: process.env.FHIRFLY_API_KEY });
53
87
 
54
88
  const result = await SHL.create({
@@ -122,7 +156,7 @@ Host your own SHL endpoints:
122
156
 
123
157
  ```typescript
124
158
  import express from "express";
125
- import { createShlMiddleware } from "@fhirfly-io/shl/express";
159
+ import { expressMiddleware } from "@fhirfly-io/shl/express";
126
160
  import { ServerLocalStorage } from "@fhirfly-io/shl/server";
127
161
 
128
162
  const storage = new ServerLocalStorage({
@@ -131,7 +165,7 @@ const storage = new ServerLocalStorage({
131
165
  });
132
166
 
133
167
  const app = express();
134
- app.use("/shl", createShlMiddleware({ storage }));
168
+ app.use("/shl", expressMiddleware({ storage }));
135
169
  app.listen(3000);
136
170
  ```
137
171
 
@@ -149,6 +183,8 @@ Covers bundle building, FhirflyStorage, LocalStorage + Express, SHL consumption,
149
183
 
150
184
  ## Related
151
185
 
186
+ - [EHR Integration Guide](https://fhirfly.io/docs/shl/ehr-integration) — Map HL7v2, CCDA, FHIR R4, and flat database records to IPS bundles
187
+ - [Security & Compliance](https://fhirfly.io/docs/shl/security) — Zero-knowledge architecture, HIPAA, compliance checklist
152
188
  - [@fhirfly-io/terminology](https://www.npmjs.com/package/@fhirfly-io/terminology) — FHIRfly terminology API SDK
153
189
  - [SMART Health Links Spec](https://docs.smarthealthit.org/smart-health-links/spec/)
154
190
  - [IPS Implementation Guide](https://build.fhir.org/ig/HL7/fhir-ips/)
@@ -2,7 +2,7 @@
2
2
 
3
3
  var chunkUDS6UJAL_cjs = require('./chunk-UDS6UJAL.cjs');
4
4
  var chunkQ7SFCCGT_cjs = require('./chunk-Q7SFCCGT.cjs');
5
- var crypto = require('crypto');
5
+ var crypto$1 = require('crypto');
6
6
  var zlib = require('zlib');
7
7
  var QRCode = require('qrcode');
8
8
 
@@ -1603,11 +1603,7 @@ var Bundle = class {
1603
1603
  }
1604
1604
  };
1605
1605
  function generateUuid() {
1606
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1607
- const r = Math.random() * 16 | 0;
1608
- const v = c === "x" ? r : r & 3 | 8;
1609
- return v.toString(16);
1610
- });
1606
+ return crypto.randomUUID();
1611
1607
  }
1612
1608
 
1613
1609
  // src/shl/index.ts
@@ -1622,6 +1618,7 @@ chunkQ7SFCCGT_cjs.__export(shl_exports, {
1622
1618
  decode: () => decode,
1623
1619
  decrypt: () => decrypt,
1624
1620
  decryptContent: () => decryptContent2,
1621
+ getEntryContent: () => getEntryContent,
1625
1622
  revoke: () => revoke
1626
1623
  });
1627
1624
  function base64url(data) {
@@ -1631,10 +1628,10 @@ function base64urlDecode(str) {
1631
1628
  return Buffer.from(str, "base64url");
1632
1629
  }
1633
1630
  function generateKey() {
1634
- return crypto.randomBytes(32);
1631
+ return crypto$1.randomBytes(32);
1635
1632
  }
1636
1633
  function generateShlId() {
1637
- return base64url(crypto.randomBytes(32));
1634
+ return base64url(crypto$1.randomBytes(32));
1638
1635
  }
1639
1636
  function encryptBundle(bundle, key) {
1640
1637
  const json = JSON.stringify(bundle);
@@ -1646,8 +1643,8 @@ function encryptBundle(bundle, key) {
1646
1643
  zip: "DEF"
1647
1644
  };
1648
1645
  const headerB64 = base64url(Buffer.from(JSON.stringify(header), "utf8"));
1649
- const iv = crypto.randomBytes(12);
1650
- const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
1646
+ const iv = crypto$1.randomBytes(12);
1647
+ const cipher = crypto$1.createCipheriv("aes-256-gcm", key, iv);
1651
1648
  cipher.setAAD(Buffer.from(headerB64, "ascii"));
1652
1649
  const ciphertext = Buffer.concat([cipher.update(compressed), cipher.final()]);
1653
1650
  const tag = cipher.getAuthTag();
@@ -1657,8 +1654,8 @@ function encryptContent(data, key, contentType) {
1657
1654
  const compressed = zlib.deflateRawSync(data);
1658
1655
  const header = { alg: "dir", enc: "A256GCM", cty: contentType, zip: "DEF" };
1659
1656
  const headerB64 = base64url(Buffer.from(JSON.stringify(header), "utf8"));
1660
- const iv = crypto.randomBytes(12);
1661
- const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
1657
+ const iv = crypto$1.randomBytes(12);
1658
+ const cipher = crypto$1.createCipheriv("aes-256-gcm", key, iv);
1662
1659
  cipher.setAAD(Buffer.from(headerB64, "ascii"));
1663
1660
  const ciphertext = Buffer.concat([cipher.update(compressed), cipher.final()]);
1664
1661
  const tag = cipher.getAuthTag();
@@ -1677,7 +1674,7 @@ function decryptContent(jwe, key) {
1677
1674
  const iv = base64urlDecode(ivB64);
1678
1675
  const ciphertext = base64urlDecode(ciphertextB64);
1679
1676
  const tag = base64urlDecode(tagB64);
1680
- const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
1677
+ const decipher = crypto$1.createDecipheriv("aes-256-gcm", key, iv);
1681
1678
  decipher.setAAD(Buffer.from(headerB64, "ascii"));
1682
1679
  decipher.setAuthTag(tag);
1683
1680
  const compressed = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
@@ -1697,7 +1694,7 @@ function decryptBundle(jwe, key) {
1697
1694
  const iv = base64urlDecode(ivB64);
1698
1695
  const ciphertext = base64urlDecode(ciphertextB64);
1699
1696
  const tag = base64urlDecode(tagB64);
1700
- const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
1697
+ const decipher = crypto$1.createDecipheriv("aes-256-gcm", key, iv);
1701
1698
  decipher.setAAD(Buffer.from(headerB64, "ascii"));
1702
1699
  decipher.setAuthTag(tag);
1703
1700
  const compressed = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
@@ -1710,8 +1707,6 @@ async function generateQRCode(url) {
1710
1707
  margin: 2
1711
1708
  });
1712
1709
  }
1713
-
1714
- // src/shl/create.ts
1715
1710
  async function create(options) {
1716
1711
  const { bundle, passcode, expiresAt, maxAccesses, label, storage, debug } = options;
1717
1712
  if (!bundle || typeof bundle !== "object") {
@@ -1740,6 +1735,11 @@ async function create(options) {
1740
1735
  );
1741
1736
  }
1742
1737
  if (debug) {
1738
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") {
1739
+ throw new chunkUDS6UJAL_cjs.ValidationError(
1740
+ "debug mode is not allowed when NODE_ENV=production \u2014 it stores unencrypted PHI"
1741
+ );
1742
+ }
1743
1743
  const debugPath = `${shlId}/bundle.json`;
1744
1744
  try {
1745
1745
  await storage.store(debugPath, JSON.stringify(bundle, null, 2));
@@ -1777,14 +1777,16 @@ async function create(options) {
1777
1777
  const manifest = {
1778
1778
  files: [
1779
1779
  {
1780
- contentType: "application/fhir+json",
1780
+ contentType: "application/fhir+json;fhirVersion=4.0.1",
1781
1781
  location: `${baseUrl}/${shlId}/content`
1782
1782
  },
1783
1783
  ...attachments.map((att, i) => ({
1784
1784
  contentType: att.contentType,
1785
1785
  location: `${baseUrl}/${shlId}/attachment/${i}`
1786
1786
  }))
1787
- ]
1787
+ ],
1788
+ status: "finalized",
1789
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
1788
1790
  };
1789
1791
  try {
1790
1792
  await storage.store(`${shlId}/manifest.json`, JSON.stringify(manifest));
@@ -1797,7 +1799,9 @@ async function create(options) {
1797
1799
  const metadata = {
1798
1800
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1799
1801
  };
1800
- if (passcode) metadata.passcode = passcode;
1802
+ if (passcode) {
1803
+ metadata.passcode = crypto$1.createHash("sha256").update(passcode).digest("hex");
1804
+ }
1801
1805
  if (maxAccesses !== void 0) metadata.maxAccesses = maxAccesses;
1802
1806
  if (expiresAt) metadata.expiresAt = expiresAt.toISOString();
1803
1807
  try {
@@ -1918,6 +1922,21 @@ function decryptContent2(jwe, key) {
1918
1922
  );
1919
1923
  }
1920
1924
  }
1925
+ async function getEntryContent(entry) {
1926
+ if (entry.embedded) {
1927
+ return entry.embedded;
1928
+ }
1929
+ if (!entry.location) {
1930
+ throw new chunkUDS6UJAL_cjs.ValidationError(
1931
+ "Manifest entry has neither location nor embedded content"
1932
+ );
1933
+ }
1934
+ const res = await globalThis.fetch(entry.location);
1935
+ if (!res.ok) {
1936
+ throw new chunkUDS6UJAL_cjs.EncryptionError(`Failed to fetch content: HTTP ${res.status}`);
1937
+ }
1938
+ return res.text();
1939
+ }
1921
1940
 
1922
1941
  // src/shl/revoke.ts
1923
1942
  async function revoke(shlId, storage) {
@@ -1929,5 +1948,5 @@ async function revoke(shlId, storage) {
1929
1948
 
1930
1949
  exports.ips_exports = ips_exports;
1931
1950
  exports.shl_exports = shl_exports;
1932
- //# sourceMappingURL=chunk-UFFYRACT.cjs.map
1933
- //# sourceMappingURL=chunk-UFFYRACT.cjs.map
1951
+ //# sourceMappingURL=chunk-63Q54EKN.cjs.map
1952
+ //# sourceMappingURL=chunk-63Q54EKN.cjs.map