@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.
- package/README.md +42 -6
- package/dist/{chunk-UFFYRACT.cjs → chunk-63Q54EKN.cjs} +40 -21
- package/dist/chunk-63Q54EKN.cjs.map +1 -0
- package/dist/{chunk-5I5H3SLO.cjs → chunk-LXJB46WB.cjs} +2 -2
- package/dist/chunk-LXJB46WB.cjs.map +1 -0
- package/dist/{chunk-CNVYKA4D.cjs → chunk-QXSWM5QV.cjs} +46 -21
- package/dist/chunk-QXSWM5QV.cjs.map +1 -0
- package/dist/{chunk-AIOYPNKN.js → chunk-YBDRWUQU.js} +32 -13
- package/dist/chunk-YBDRWUQU.js.map +1 -0
- package/dist/{chunk-NEBFJSJW.js → chunk-YV7ZD6OM.js} +2 -2
- package/dist/chunk-YV7ZD6OM.js.map +1 -0
- package/dist/{chunk-7WIM2QP5.js → chunk-ZEE5RXIS.js} +46 -21
- package/dist/chunk-ZEE5RXIS.js.map +1 -0
- package/dist/cli.cjs +14 -14
- package/dist/cli.js +3 -3
- package/dist/express.cjs +2 -2
- package/dist/express.d.cts +2 -2
- package/dist/express.d.ts +2 -2
- package/dist/express.js +1 -1
- package/dist/fastify.cjs +16 -2
- package/dist/fastify.cjs.map +1 -1
- package/dist/fastify.d.cts +3 -3
- package/dist/fastify.d.ts +3 -3
- package/dist/fastify.js +15 -1
- package/dist/fastify.js.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +28 -3
- package/dist/index.d.ts +28 -3
- package/dist/index.js +1 -1
- package/dist/lambda.cjs +2 -2
- package/dist/lambda.d.cts +2 -2
- package/dist/lambda.d.ts +2 -2
- package/dist/lambda.js +1 -1
- package/dist/server.cjs +7 -7
- package/dist/server.d.cts +13 -6
- package/dist/server.d.ts +13 -6
- package/dist/server.js +2 -2
- package/dist/{storage-DYEX5kiP.d.ts → storage-B3GyJD2y.d.ts} +1 -1
- package/dist/{storage-D1NajOTq.d.cts → storage-BwszYwFo.d.cts} +1 -1
- package/dist/{types-BbvJirBn.d.cts → types-BegxU0wQ.d.ts} +22 -2
- package/dist/{types-DpkUjBYr.d.ts → types-Doq5cGNm.d.cts} +8 -1
- package/dist/{types-DpkUjBYr.d.cts → types-Doq5cGNm.d.ts} +8 -1
- package/dist/{types-qPv1O_sF.d.ts → types-hHf-a3hH.d.cts} +22 -2
- package/package.json +1 -1
- package/dist/chunk-5I5H3SLO.cjs.map +0 -1
- package/dist/chunk-7WIM2QP5.js.map +0 -1
- package/dist/chunk-AIOYPNKN.js.map +0 -1
- package/dist/chunk-CNVYKA4D.cjs.map +0 -1
- package/dist/chunk-NEBFJSJW.js.map +0 -1
- 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 (
|
|
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
|
-
|
|
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
|
-
//
|
|
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 {
|
|
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",
|
|
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
|
|
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)
|
|
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-
|
|
1933
|
-
//# sourceMappingURL=chunk-
|
|
1951
|
+
//# sourceMappingURL=chunk-63Q54EKN.cjs.map
|
|
1952
|
+
//# sourceMappingURL=chunk-63Q54EKN.cjs.map
|