@fhirfly-io/shl 0.1.1 → 0.2.0
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 +95 -53
- package/dist/chunk-5I5H3SLO.cjs +286 -0
- package/dist/chunk-5I5H3SLO.cjs.map +1 -0
- package/dist/chunk-AIOYPNKN.js +1926 -0
- package/dist/chunk-AIOYPNKN.js.map +1 -0
- package/dist/chunk-NEBFJSJW.js +281 -0
- package/dist/chunk-NEBFJSJW.js.map +1 -0
- package/dist/chunk-UDS6UJAL.cjs +388 -0
- package/dist/chunk-UDS6UJAL.cjs.map +1 -0
- package/dist/chunk-UFFYRACT.cjs +1933 -0
- package/dist/chunk-UFFYRACT.cjs.map +1 -0
- package/dist/chunk-VKB3ESIV.js +378 -0
- package/dist/chunk-VKB3ESIV.js.map +1 -0
- package/dist/cli.cjs +461 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +458 -0
- package/dist/cli.js.map +1 -0
- package/dist/express.d.cts +2 -2
- package/dist/express.d.ts +2 -2
- package/dist/fastify.d.cts +2 -2
- package/dist/fastify.d.ts +2 -2
- package/dist/index.cjs +13 -1594
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -4
- package/dist/index.d.ts +109 -4
- package/dist/index.js +3 -1591
- package/dist/index.js.map +1 -1
- package/dist/lambda.d.cts +2 -2
- package/dist/lambda.d.ts +2 -2
- package/dist/server.cjs +19 -154
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +54 -5
- package/dist/server.d.ts +54 -5
- package/dist/server.js +2 -155
- package/dist/server.js.map +1 -1
- package/dist/storage-D1NajOTq.d.cts +205 -0
- package/dist/storage-DYEX5kiP.d.ts +205 -0
- package/dist/{types--f4ITgu9.d.cts → types-BbvJirBn.d.cts} +1 -1
- package/dist/{types-yk0mDByJ.d.cts → types-DpkUjBYr.d.cts} +1 -1
- package/dist/{types-yk0mDByJ.d.ts → types-DpkUjBYr.d.ts} +1 -1
- package/dist/{types-DKtPO4DP.d.ts → types-qPv1O_sF.d.ts} +1 -1
- package/package.json +16 -2
- package/dist/chunk-KI44MYPE.cjs +0 -170
- package/dist/chunk-KI44MYPE.cjs.map +0 -1
- package/dist/chunk-XTLU6O32.js +0 -163
- package/dist/chunk-XTLU6O32.js.map +0 -1
- package/dist/storage-CHi9vLD_.d.cts +0 -82
- package/dist/storage-iI_tyHcX.d.ts +0 -82
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @fhirfly-io/shl
|
|
2
2
|
|
|
3
|
-
SMART Health Links SDK for Node.js — build FHIR Bundles from clinical codes,
|
|
3
|
+
SMART Health Links SDK for Node.js — build IPS FHIR Bundles from clinical codes, encrypt, and share via SHL/QR code.
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
@@ -20,98 +20,140 @@ PHI never leaves your server. Only terminology codes are sent to FHIRfly for enr
|
|
|
20
20
|
npm install @fhirfly-io/shl
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
For FHIRfly API enrichment (recommended):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @fhirfly-io/shl @fhirfly-io/terminology
|
|
27
|
+
```
|
|
28
|
+
|
|
23
29
|
## Quick Start
|
|
24
30
|
|
|
25
31
|
```typescript
|
|
26
|
-
import { IPS, SHL } from
|
|
27
|
-
import
|
|
32
|
+
import { IPS, SHL } from "@fhirfly-io/shl";
|
|
33
|
+
import Fhirfly from "@fhirfly-io/terminology";
|
|
28
34
|
|
|
29
|
-
const
|
|
35
|
+
const client = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY });
|
|
30
36
|
|
|
31
|
-
// Build
|
|
32
|
-
const
|
|
33
|
-
patient: { name:
|
|
37
|
+
// Build the IPS Bundle
|
|
38
|
+
const bundle = new IPS.Bundle({
|
|
39
|
+
patient: { name: "Maria Garcia", birthDate: "1985-03-15", gender: "female" },
|
|
34
40
|
});
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
bundle.addMedication({ byNDC: "00071015523", fhirfly: client.ndc });
|
|
43
|
+
bundle.addCondition({ byICD10: "E11.9", fhirfly: client.icd10 });
|
|
44
|
+
bundle.addAllergy({ bySNOMED: "387207008" });
|
|
45
|
+
bundle.addImmunization({ byCVX: "208", fhirfly: client.cvx });
|
|
46
|
+
bundle.addResult({ byLOINC: "2339-0", fhirfly: client.loinc, value: 95, unit: "mg/dL" });
|
|
47
|
+
bundle.addDocument({ content: pdfBuffer, contentType: "application/pdf", title: "Visit Summary" });
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
const bundle = await ips.build({ profile: 'ips' }); // or 'r4' for generic FHIR
|
|
49
|
+
const fhirBundle = await bundle.build();
|
|
44
50
|
|
|
45
|
-
//
|
|
46
|
-
const
|
|
51
|
+
// Create the SHL (zero-infra with FhirflyStorage)
|
|
52
|
+
const storage = new SHL.FhirflyStorage({ apiKey: process.env.FHIRFLY_API_KEY });
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
storage
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
label: 'Medical Summary for Oliver Brown',
|
|
54
|
+
const result = await SHL.create({
|
|
55
|
+
bundle: fhirBundle,
|
|
56
|
+
storage,
|
|
57
|
+
passcode: "1234",
|
|
58
|
+
label: "Maria's Health Summary",
|
|
54
59
|
});
|
|
55
60
|
|
|
56
|
-
console.log(
|
|
57
|
-
console.log(
|
|
58
|
-
console.log(
|
|
61
|
+
console.log(result.url); // shlink:/eyJ1cmwiOiJodHRwczovL...
|
|
62
|
+
console.log(result.qrCode); // data:image/png;base64,...
|
|
63
|
+
console.log(result.passcode); // "1234"
|
|
59
64
|
```
|
|
60
65
|
|
|
61
|
-
##
|
|
66
|
+
## Storage Adapters
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- **Bring Your Own PDF** — SDK wraps pre-rendered PDFs as FHIR DocumentReference; you control rendering
|
|
67
|
-
- **Validation as a gate** — `SHL.create()` refuses to package invalid Bundles
|
|
68
|
-
- **Create-only for v1** — SHL creation; receiving/decoding is out of scope
|
|
68
|
+
```typescript
|
|
69
|
+
// FHIRfly hosted (zero infrastructure, recommended)
|
|
70
|
+
new SHL.FhirflyStorage({ apiKey: "..." });
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
// AWS S3
|
|
73
|
+
new SHL.S3Storage({ bucket: "my-bucket", region: "us-east-1", baseUrl: "https://shl.example.com" });
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
// Azure Blob Storage
|
|
76
|
+
new SHL.AzureStorage({ container: "shl-data", connectionString: "...", baseUrl: "https://shl.example.com" });
|
|
77
|
+
|
|
78
|
+
// Google Cloud Storage
|
|
79
|
+
new SHL.GCSStorage({ bucket: "my-bucket", baseUrl: "https://shl.example.com" });
|
|
80
|
+
|
|
81
|
+
// Local filesystem (development)
|
|
82
|
+
new SHL.LocalStorage({ directory: "./shl-data", baseUrl: "http://localhost:3456/shl" });
|
|
83
|
+
```
|
|
76
84
|
|
|
77
85
|
## Input Formats
|
|
78
86
|
|
|
79
87
|
Each `add*` method supports multiple input formats:
|
|
80
88
|
|
|
81
89
|
```typescript
|
|
82
|
-
// From
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
// From codes (enriched via FHIRfly API)
|
|
91
|
+
bundle.addMedication({ byNDC: "00071015523", fhirfly: client.ndc });
|
|
92
|
+
bundle.addMedication({ byRxNorm: "161", fhirfly: client.rxnorm });
|
|
93
|
+
bundle.addCondition({ byICD10: "E11.9", fhirfly: client.icd10 });
|
|
94
|
+
bundle.addResult({ byLOINC: "2339-0", fhirfly: client.loinc, value: 95, unit: "mg/dL" });
|
|
95
|
+
bundle.addImmunization({ byCVX: "208", fhirfly: client.cvx });
|
|
85
96
|
|
|
86
97
|
// From SNOMED (no API call needed)
|
|
87
|
-
|
|
98
|
+
bundle.addMedication({ bySNOMED: "376988009" });
|
|
99
|
+
bundle.addAllergy({ bySNOMED: "387207008" });
|
|
88
100
|
|
|
89
101
|
// From existing FHIR R4 resources
|
|
90
|
-
|
|
102
|
+
bundle.addMedication({ fromResource: existingMedicationStatement });
|
|
103
|
+
bundle.addCondition({ fromResource: existingCondition });
|
|
104
|
+
|
|
105
|
+
// Manual coding (no API dependency)
|
|
106
|
+
bundle.addMedication({ code: "376988009", system: "http://snomed.info/sct", display: "Levothyroxine" });
|
|
91
107
|
```
|
|
92
108
|
|
|
93
|
-
##
|
|
109
|
+
## CLI
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx @fhirfly-io/shl validate bundle.json # Validate a FHIR Bundle
|
|
113
|
+
npx @fhirfly-io/shl create bundle.json # Create an SHL from a bundle
|
|
114
|
+
npx @fhirfly-io/shl decode shlink:/eyJ... # Decode an SHL URL
|
|
115
|
+
npx @fhirfly-io/shl serve # Start a local SHL server
|
|
116
|
+
npx @fhirfly-io/shl demo # Full round-trip demo
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Server Middleware
|
|
120
|
+
|
|
121
|
+
Host your own SHL endpoints:
|
|
94
122
|
|
|
95
123
|
```typescript
|
|
96
|
-
|
|
97
|
-
|
|
124
|
+
import express from "express";
|
|
125
|
+
import { createShlMiddleware } from "@fhirfly-io/shl/express";
|
|
126
|
+
import { ServerLocalStorage } from "@fhirfly-io/shl/server";
|
|
98
127
|
|
|
99
|
-
|
|
100
|
-
|
|
128
|
+
const storage = new ServerLocalStorage({
|
|
129
|
+
directory: "./shl-data",
|
|
130
|
+
baseUrl: "http://localhost:3000/shl",
|
|
131
|
+
});
|
|
101
132
|
|
|
102
|
-
|
|
103
|
-
|
|
133
|
+
const app = express();
|
|
134
|
+
app.use("/shl", createShlMiddleware({ storage }));
|
|
135
|
+
app.listen(3000);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Also available for Fastify (`@fhirfly-io/shl/fastify`) and Lambda (`@fhirfly-io/shl/lambda`).
|
|
139
|
+
|
|
140
|
+
## Live Exercise
|
|
104
141
|
|
|
105
|
-
|
|
106
|
-
|
|
142
|
+
Run the comprehensive integration test against the live API to exercise every SDK path:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npx tsx examples/live-exercise/index.ts --api-key <your-key> --verbose
|
|
107
146
|
```
|
|
108
147
|
|
|
148
|
+
Covers bundle building, FhirflyStorage, LocalStorage + Express, SHL consumption, access control, and edge cases. See [`examples/live-exercise/README.md`](examples/live-exercise/README.md) for details.
|
|
149
|
+
|
|
109
150
|
## Related
|
|
110
151
|
|
|
111
|
-
- [@fhirfly-io/terminology](https://www.npmjs.com/package/@fhirfly-io/terminology) — FHIRfly terminology API SDK
|
|
152
|
+
- [@fhirfly-io/terminology](https://www.npmjs.com/package/@fhirfly-io/terminology) — FHIRfly terminology API SDK
|
|
112
153
|
- [SMART Health Links Spec](https://docs.smarthealthit.org/smart-health-links/spec/)
|
|
113
154
|
- [IPS Implementation Guide](https://build.fhir.org/ig/HL7/fhir-ips/)
|
|
114
|
-
- [FHIRfly
|
|
155
|
+
- [FHIRfly SHL Docs](https://fhirfly.io/docs/shl/overview)
|
|
156
|
+
- [SHL Viewer](https://fhirfly.io/shl/viewer)
|
|
115
157
|
|
|
116
158
|
## License
|
|
117
159
|
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkUDS6UJAL_cjs = require('./chunk-UDS6UJAL.cjs');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
|
|
7
|
+
var ServerLocalStorage = class extends chunkUDS6UJAL_cjs.LocalStorage {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
super(config);
|
|
10
|
+
}
|
|
11
|
+
async read(key) {
|
|
12
|
+
const filePath = path.join(this.config.directory, key);
|
|
13
|
+
if (!fs.existsSync(filePath)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return fs.readFileSync(filePath, "utf8");
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async updateMetadata(shlId, updater) {
|
|
23
|
+
const key = `${shlId}/metadata.json`;
|
|
24
|
+
const raw = await this.read(key);
|
|
25
|
+
if (raw === null) return null;
|
|
26
|
+
const current = JSON.parse(raw);
|
|
27
|
+
const updated = updater(current);
|
|
28
|
+
if (updated === null) return null;
|
|
29
|
+
await this.store(key, JSON.stringify(updated));
|
|
30
|
+
return updated;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var _s3Module;
|
|
34
|
+
async function getS3Module() {
|
|
35
|
+
if (_s3Module) return _s3Module;
|
|
36
|
+
try {
|
|
37
|
+
_s3Module = await import('@aws-sdk/client-s3');
|
|
38
|
+
return _s3Module;
|
|
39
|
+
} catch {
|
|
40
|
+
throw new chunkUDS6UJAL_cjs.StorageError(
|
|
41
|
+
"@aws-sdk/client-s3 is required for ServerS3Storage. Install it: npm install @aws-sdk/client-s3",
|
|
42
|
+
"import"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
var ServerS3Storage = class {
|
|
47
|
+
_config;
|
|
48
|
+
_client;
|
|
49
|
+
constructor(config) {
|
|
50
|
+
this._config = config;
|
|
51
|
+
}
|
|
52
|
+
get baseUrl() {
|
|
53
|
+
return this._config.baseUrl.replace(/\/+$/, "");
|
|
54
|
+
}
|
|
55
|
+
async store(key, content) {
|
|
56
|
+
try {
|
|
57
|
+
const s3 = await getS3Module();
|
|
58
|
+
const client = this._getClient(s3);
|
|
59
|
+
const body = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
60
|
+
const command = new s3.PutObjectCommand({
|
|
61
|
+
Bucket: this._config.bucket,
|
|
62
|
+
Key: this._s3Key(key),
|
|
63
|
+
Body: body,
|
|
64
|
+
ContentType: this._contentType(key)
|
|
65
|
+
});
|
|
66
|
+
await client.send(command);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof chunkUDS6UJAL_cjs.StorageError) throw err;
|
|
69
|
+
throw new chunkUDS6UJAL_cjs.StorageError(
|
|
70
|
+
`Failed to store ${key}: ${err instanceof Error ? err.message : String(err)}`,
|
|
71
|
+
"store"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async delete(prefix) {
|
|
76
|
+
try {
|
|
77
|
+
const s3 = await getS3Module();
|
|
78
|
+
const client = this._getClient(s3);
|
|
79
|
+
const s3Prefix = this._s3Key(prefix);
|
|
80
|
+
let continuationToken;
|
|
81
|
+
do {
|
|
82
|
+
const listInput = {
|
|
83
|
+
Bucket: this._config.bucket,
|
|
84
|
+
Prefix: s3Prefix
|
|
85
|
+
};
|
|
86
|
+
if (continuationToken) listInput["ContinuationToken"] = continuationToken;
|
|
87
|
+
const listCommand = new s3.ListObjectsV2Command(listInput);
|
|
88
|
+
const response = await client.send(listCommand);
|
|
89
|
+
const objects = response.Contents;
|
|
90
|
+
if (!objects || objects.length === 0) break;
|
|
91
|
+
const deleteCommand = new s3.DeleteObjectsCommand({
|
|
92
|
+
Bucket: this._config.bucket,
|
|
93
|
+
Delete: {
|
|
94
|
+
Objects: objects.map((obj) => ({ Key: obj.Key })),
|
|
95
|
+
Quiet: true
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
await client.send(deleteCommand);
|
|
99
|
+
continuationToken = response.IsTruncated ? response.NextContinuationToken : void 0;
|
|
100
|
+
} while (continuationToken);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err instanceof chunkUDS6UJAL_cjs.StorageError) throw err;
|
|
103
|
+
throw new chunkUDS6UJAL_cjs.StorageError(
|
|
104
|
+
`Failed to delete ${prefix}: ${err instanceof Error ? err.message : String(err)}`,
|
|
105
|
+
"delete"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async read(key) {
|
|
110
|
+
try {
|
|
111
|
+
const s3 = await getS3Module();
|
|
112
|
+
const client = this._getClient(s3);
|
|
113
|
+
const command = new s3.GetObjectCommand({
|
|
114
|
+
Bucket: this._config.bucket,
|
|
115
|
+
Key: this._s3Key(key)
|
|
116
|
+
});
|
|
117
|
+
const response = await client.send(command);
|
|
118
|
+
if (!response.Body) return null;
|
|
119
|
+
return response.Body.transformToString();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const code = err.name;
|
|
122
|
+
if (code === "NoSuchKey") return null;
|
|
123
|
+
if (err instanceof chunkUDS6UJAL_cjs.StorageError) throw err;
|
|
124
|
+
throw new chunkUDS6UJAL_cjs.StorageError(
|
|
125
|
+
`Failed to read ${key}: ${err instanceof Error ? err.message : String(err)}`,
|
|
126
|
+
"read"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async updateMetadata(shlId, updater) {
|
|
131
|
+
const key = `${shlId}/metadata.json`;
|
|
132
|
+
const raw = await this.read(key);
|
|
133
|
+
if (raw === null) return null;
|
|
134
|
+
const current = JSON.parse(raw);
|
|
135
|
+
const updated = updater(current);
|
|
136
|
+
if (updated === null) return null;
|
|
137
|
+
await this.store(key, JSON.stringify(updated));
|
|
138
|
+
return updated;
|
|
139
|
+
}
|
|
140
|
+
_getClient(s3) {
|
|
141
|
+
if (!this._client) {
|
|
142
|
+
this._client = new s3.S3Client({ region: this._config.region });
|
|
143
|
+
}
|
|
144
|
+
return this._client;
|
|
145
|
+
}
|
|
146
|
+
_s3Key(key) {
|
|
147
|
+
const prefix = this._config.prefix?.replace(/\/+$/, "");
|
|
148
|
+
return prefix ? `${prefix}/${key}` : key;
|
|
149
|
+
}
|
|
150
|
+
_contentType(key) {
|
|
151
|
+
if (key.endsWith(".jwe")) return "application/jose";
|
|
152
|
+
if (key.endsWith(".json")) return "application/json";
|
|
153
|
+
return "application/octet-stream";
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
var _azureModule;
|
|
157
|
+
async function getAzureModule() {
|
|
158
|
+
if (_azureModule) return _azureModule;
|
|
159
|
+
try {
|
|
160
|
+
_azureModule = await import('@azure/storage-blob');
|
|
161
|
+
return _azureModule;
|
|
162
|
+
} catch {
|
|
163
|
+
throw new chunkUDS6UJAL_cjs.StorageError(
|
|
164
|
+
"@azure/storage-blob is required for ServerAzureStorage. Install it: npm install @azure/storage-blob",
|
|
165
|
+
"import"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
var ServerAzureStorage = class extends chunkUDS6UJAL_cjs.AzureStorage {
|
|
170
|
+
_serverContainerClient;
|
|
171
|
+
constructor(config) {
|
|
172
|
+
super(config);
|
|
173
|
+
}
|
|
174
|
+
async read(key) {
|
|
175
|
+
try {
|
|
176
|
+
const container = await this._getServerContainer();
|
|
177
|
+
const blobName = this._serverBlobName(key);
|
|
178
|
+
const client = container.getBlockBlobClient(blobName);
|
|
179
|
+
const response = await client.download();
|
|
180
|
+
if (!response.readableStreamBody) return null;
|
|
181
|
+
return await streamToString(response.readableStreamBody);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const code = err.statusCode;
|
|
184
|
+
if (code === 404) return null;
|
|
185
|
+
if (err instanceof chunkUDS6UJAL_cjs.StorageError) throw err;
|
|
186
|
+
throw new chunkUDS6UJAL_cjs.StorageError(
|
|
187
|
+
`Failed to read ${key}: ${err instanceof Error ? err.message : String(err)}`,
|
|
188
|
+
"read"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async updateMetadata(shlId, updater) {
|
|
193
|
+
const key = `${shlId}/metadata.json`;
|
|
194
|
+
const raw = await this.read(key);
|
|
195
|
+
if (raw === null) return null;
|
|
196
|
+
const current = JSON.parse(raw);
|
|
197
|
+
const updated = updater(current);
|
|
198
|
+
if (updated === null) return null;
|
|
199
|
+
await this.store(key, JSON.stringify(updated));
|
|
200
|
+
return updated;
|
|
201
|
+
}
|
|
202
|
+
async _getServerContainer() {
|
|
203
|
+
if (!this._serverContainerClient) {
|
|
204
|
+
const azure = await getAzureModule();
|
|
205
|
+
const serviceClient = azure.BlobServiceClient.fromConnectionString(this.config.connectionString);
|
|
206
|
+
this._serverContainerClient = serviceClient.getContainerClient(this.config.container);
|
|
207
|
+
}
|
|
208
|
+
return this._serverContainerClient;
|
|
209
|
+
}
|
|
210
|
+
_serverBlobName(key) {
|
|
211
|
+
const prefix = this.config.prefix?.replace(/\/+$/, "");
|
|
212
|
+
return prefix ? `${prefix}/${key}` : key;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
var _gcsModule;
|
|
216
|
+
async function getGCSModule() {
|
|
217
|
+
if (_gcsModule) return _gcsModule;
|
|
218
|
+
try {
|
|
219
|
+
_gcsModule = await import('@google-cloud/storage');
|
|
220
|
+
return _gcsModule;
|
|
221
|
+
} catch {
|
|
222
|
+
throw new chunkUDS6UJAL_cjs.StorageError(
|
|
223
|
+
"@google-cloud/storage is required for ServerGCSStorage. Install it: npm install @google-cloud/storage",
|
|
224
|
+
"import"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
var ServerGCSStorage = class extends chunkUDS6UJAL_cjs.GCSStorage {
|
|
229
|
+
_serverBucket;
|
|
230
|
+
constructor(config) {
|
|
231
|
+
super(config);
|
|
232
|
+
}
|
|
233
|
+
async read(key) {
|
|
234
|
+
try {
|
|
235
|
+
const bucket = await this._getServerBucket();
|
|
236
|
+
const fileName = this._serverFileName(key);
|
|
237
|
+
const file = bucket.file(fileName);
|
|
238
|
+
const [content] = await file.download();
|
|
239
|
+
return content.toString("utf8");
|
|
240
|
+
} catch (err) {
|
|
241
|
+
const code = err.code;
|
|
242
|
+
if (code === 404) return null;
|
|
243
|
+
if (err instanceof chunkUDS6UJAL_cjs.StorageError) throw err;
|
|
244
|
+
throw new chunkUDS6UJAL_cjs.StorageError(
|
|
245
|
+
`Failed to read ${key}: ${err instanceof Error ? err.message : String(err)}`,
|
|
246
|
+
"read"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async updateMetadata(shlId, updater) {
|
|
251
|
+
const key = `${shlId}/metadata.json`;
|
|
252
|
+
const raw = await this.read(key);
|
|
253
|
+
if (raw === null) return null;
|
|
254
|
+
const current = JSON.parse(raw);
|
|
255
|
+
const updated = updater(current);
|
|
256
|
+
if (updated === null) return null;
|
|
257
|
+
await this.store(key, JSON.stringify(updated));
|
|
258
|
+
return updated;
|
|
259
|
+
}
|
|
260
|
+
async _getServerBucket() {
|
|
261
|
+
if (!this._serverBucket) {
|
|
262
|
+
const gcs = await getGCSModule();
|
|
263
|
+
const storage = new gcs.Storage();
|
|
264
|
+
this._serverBucket = storage.bucket(this.config.bucket);
|
|
265
|
+
}
|
|
266
|
+
return this._serverBucket;
|
|
267
|
+
}
|
|
268
|
+
_serverFileName(key) {
|
|
269
|
+
const prefix = this.config.prefix?.replace(/\/+$/, "");
|
|
270
|
+
return prefix ? `${prefix}/${key}` : key;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
async function streamToString(stream) {
|
|
274
|
+
const chunks = [];
|
|
275
|
+
for await (const chunk of stream) {
|
|
276
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
277
|
+
}
|
|
278
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
exports.ServerAzureStorage = ServerAzureStorage;
|
|
282
|
+
exports.ServerGCSStorage = ServerGCSStorage;
|
|
283
|
+
exports.ServerLocalStorage = ServerLocalStorage;
|
|
284
|
+
exports.ServerS3Storage = ServerS3Storage;
|
|
285
|
+
//# sourceMappingURL=chunk-5I5H3SLO.cjs.map
|
|
286
|
+
//# sourceMappingURL=chunk-5I5H3SLO.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/storage.ts"],"names":["LocalStorage","join","existsSync","readFileSync","StorageError","AzureStorage","GCSStorage"],"mappings":"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","file":"chunk-5I5H3SLO.cjs","sourcesContent":["// Copyright 2026 FHIRfly.io LLC. All rights reserved.\n// Licensed under the MIT License. See LICENSE file in the project root.\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { LocalStorage, AzureStorage, GCSStorage } from \"../shl/storage.js\";\nimport type { LocalStorageConfig, S3StorageConfig, AzureStorageConfig, GCSStorageConfig } from \"../shl/storage.js\";\nimport type { SHLServerStorage } from \"./types.js\";\nimport type { SHLMetadata } from \"../shl/types.js\";\nimport { StorageError } from \"../errors.js\";\n\n/**\n * Local filesystem server storage for SMART Health Links.\n *\n * Extends the base `LocalStorage` (write-only) with `read` and\n * `updateMetadata` methods needed for serving SHLs.\n *\n * @example\n * ```ts\n * import { ServerLocalStorage } from \"@fhirfly-io/shl/server\";\n *\n * const storage = new ServerLocalStorage({\n * directory: \"./shl-data\",\n * baseUrl: \"https://shl.example.com\",\n * });\n * ```\n */\nexport class ServerLocalStorage extends LocalStorage implements SHLServerStorage {\n constructor(config: LocalStorageConfig) {\n super(config);\n }\n\n async read(key: string): Promise<string | Uint8Array | null> {\n const filePath = join(this.config.directory, key);\n if (!existsSync(filePath)) {\n return null;\n }\n try {\n // Read as UTF-8 for JSON/JWE files, binary for others\n return readFileSync(filePath, \"utf8\");\n } catch {\n return null;\n }\n }\n\n async updateMetadata(\n shlId: string,\n updater: (current: SHLMetadata) => SHLMetadata | null,\n ): Promise<SHLMetadata | null> {\n const key = `${shlId}/metadata.json`;\n const raw = await this.read(key);\n if (raw === null) return null;\n\n const current = JSON.parse(raw as string) as SHLMetadata;\n const updated = updater(current);\n if (updated === null) return null;\n\n await this.store(key, JSON.stringify(updated));\n return updated;\n }\n}\n\n// Minimal S3 interfaces (same pattern as shl/storage.ts)\ninterface S3ClientInstance {\n send(command: unknown): Promise<unknown>;\n}\ninterface S3Module {\n S3Client: new (config: { region: string }) => S3ClientInstance;\n PutObjectCommand: new (input: Record<string, unknown>) => unknown;\n GetObjectCommand: new (input: Record<string, unknown>) => unknown;\n ListObjectsV2Command: new (input: Record<string, unknown>) => unknown;\n DeleteObjectsCommand: new (input: Record<string, unknown>) => unknown;\n}\n\nlet _s3Module: S3Module | undefined;\nasync function getS3Module(): Promise<S3Module> {\n if (_s3Module) return _s3Module;\n try {\n _s3Module = (await import(\"@aws-sdk/client-s3\")) as unknown as S3Module;\n return _s3Module;\n } catch {\n throw new StorageError(\n \"@aws-sdk/client-s3 is required for ServerS3Storage. Install it: npm install @aws-sdk/client-s3\",\n \"import\",\n );\n }\n}\n\n/**\n * S3-backed server storage for SMART Health Links.\n *\n * Implements the full `SHLServerStorage` interface with `read` and\n * `updateMetadata` on top of S3.\n *\n * Uses conditional PutObject for optimistic concurrency on metadata updates.\n *\n * @example\n * ```ts\n * import { ServerS3Storage } from \"@fhirfly-io/shl/server\";\n *\n * const storage = new ServerS3Storage({\n * bucket: \"my-shl-bucket\",\n * region: \"us-east-1\",\n * baseUrl: \"https://shl.example.com\",\n * });\n * ```\n */\nexport class ServerS3Storage implements SHLServerStorage {\n private readonly _config: S3StorageConfig;\n private _client?: S3ClientInstance;\n\n constructor(config: S3StorageConfig) {\n this._config = config;\n }\n\n get baseUrl(): string {\n return this._config.baseUrl.replace(/\\/+$/, \"\");\n }\n\n async store(key: string, content: string | Uint8Array): Promise<void> {\n try {\n const s3 = await getS3Module();\n const client = this._getClient(s3);\n const body = typeof content === \"string\" ? Buffer.from(content, \"utf8\") : content;\n\n const command = new s3.PutObjectCommand({\n Bucket: this._config.bucket,\n Key: this._s3Key(key),\n Body: body,\n ContentType: this._contentType(key),\n });\n\n await client.send(command);\n } catch (err) {\n if (err instanceof StorageError) throw err;\n throw new StorageError(\n `Failed to store ${key}: ${err instanceof Error ? err.message : String(err)}`,\n \"store\",\n );\n }\n }\n\n async delete(prefix: string): Promise<void> {\n try {\n const s3 = await getS3Module();\n const client = this._getClient(s3);\n const s3Prefix = this._s3Key(prefix);\n\n let continuationToken: string | undefined;\n do {\n const listInput: Record<string, unknown> = {\n Bucket: this._config.bucket,\n Prefix: s3Prefix,\n };\n if (continuationToken) listInput[\"ContinuationToken\"] = continuationToken;\n\n const listCommand = new s3.ListObjectsV2Command(listInput);\n const response = (await client.send(listCommand)) as {\n Contents?: Array<{ Key?: string }>;\n IsTruncated?: boolean;\n NextContinuationToken?: string;\n };\n\n const objects = response.Contents;\n if (!objects || objects.length === 0) break;\n\n const deleteCommand = new s3.DeleteObjectsCommand({\n Bucket: this._config.bucket,\n Delete: {\n Objects: objects.map((obj) => ({ Key: obj.Key })),\n Quiet: true,\n },\n });\n await client.send(deleteCommand);\n\n continuationToken = response.IsTruncated\n ? response.NextContinuationToken\n : undefined;\n } while (continuationToken);\n } catch (err) {\n if (err instanceof StorageError) throw err;\n throw new StorageError(\n `Failed to delete ${prefix}: ${err instanceof Error ? err.message : String(err)}`,\n \"delete\",\n );\n }\n }\n\n async read(key: string): Promise<string | Uint8Array | null> {\n try {\n const s3 = await getS3Module();\n const client = this._getClient(s3);\n\n const command = new s3.GetObjectCommand({\n Bucket: this._config.bucket,\n Key: this._s3Key(key),\n });\n\n const response = (await client.send(command)) as {\n Body?: { transformToString(): Promise<string> };\n };\n\n if (!response.Body) return null;\n return response.Body.transformToString();\n } catch (err) {\n // S3 returns NoSuchKey for missing objects\n const code = (err as { name?: string }).name;\n if (code === \"NoSuchKey\") return null;\n if (err instanceof StorageError) throw err;\n throw new StorageError(\n `Failed to read ${key}: ${err instanceof Error ? err.message : String(err)}`,\n \"read\",\n );\n }\n }\n\n async updateMetadata(\n shlId: string,\n updater: (current: SHLMetadata) => SHLMetadata | null,\n ): Promise<SHLMetadata | null> {\n const key = `${shlId}/metadata.json`;\n const raw = await this.read(key);\n if (raw === null) return null;\n\n const current = JSON.parse(raw as string) as SHLMetadata;\n const updated = updater(current);\n if (updated === null) return null;\n\n await this.store(key, JSON.stringify(updated));\n return updated;\n }\n\n private _getClient(s3: S3Module): S3ClientInstance {\n if (!this._client) {\n this._client = new s3.S3Client({ region: this._config.region });\n }\n return this._client;\n }\n\n private _s3Key(key: string): string {\n const prefix = this._config.prefix?.replace(/\\/+$/, \"\");\n return prefix ? `${prefix}/${key}` : key;\n }\n\n private _contentType(key: string): string {\n if (key.endsWith(\".jwe\")) return \"application/jose\";\n if (key.endsWith(\".json\")) return \"application/json\";\n return \"application/octet-stream\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Azure Blob Storage (Server)\n// ---------------------------------------------------------------------------\n\n// Minimal Azure interfaces for server-side operations\ninterface AzureBlobModule {\n BlobServiceClient: {\n fromConnectionString(connectionString: string): AzureBlobServiceClient;\n };\n}\ninterface AzureBlobServiceClient {\n getContainerClient(container: string): AzureContainerClient;\n}\ninterface AzureContainerClient {\n getBlockBlobClient(blobName: string): AzureBlockBlobClient;\n listBlobsFlat(options?: { prefix?: string }): AsyncIterable<{ name: string }>;\n}\ninterface AzureBlockBlobClient {\n upload(content: Uint8Array | Buffer, contentLength: number, options?: Record<string, unknown>): Promise<unknown>;\n deleteIfExists(): Promise<unknown>;\n download(): Promise<{ readableStreamBody?: NodeJS.ReadableStream }>;\n}\n\nlet _azureModule: AzureBlobModule | undefined;\nasync function getAzureModule(): Promise<AzureBlobModule> {\n if (_azureModule) return _azureModule;\n try {\n _azureModule = (await import(\"@azure/storage-blob\")) as unknown as AzureBlobModule;\n return _azureModule;\n } catch {\n throw new StorageError(\n \"@azure/storage-blob is required for ServerAzureStorage. Install it: npm install @azure/storage-blob\",\n \"import\",\n );\n }\n}\n\n/**\n * Azure Blob Storage server storage for SMART Health Links.\n *\n * Extends the base `AzureStorage` with `read` and `updateMetadata`\n * methods needed for serving SHLs.\n *\n * @example\n * ```ts\n * import { ServerAzureStorage } from \"@fhirfly-io/shl/server\";\n *\n * const storage = new ServerAzureStorage({\n * container: \"shl-data\",\n * connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING!,\n * baseUrl: \"https://shl.example.com\",\n * });\n * ```\n */\nexport class ServerAzureStorage extends AzureStorage implements SHLServerStorage {\n private _serverContainerClient?: AzureContainerClient;\n\n constructor(config: AzureStorageConfig) {\n super(config);\n }\n\n async read(key: string): Promise<string | Uint8Array | null> {\n try {\n const container = await this._getServerContainer();\n const blobName = this._serverBlobName(key);\n const client = container.getBlockBlobClient(blobName);\n\n const response = await client.download();\n if (!response.readableStreamBody) return null;\n\n return await streamToString(response.readableStreamBody);\n } catch (err) {\n const code = (err as { statusCode?: number }).statusCode;\n if (code === 404) return null;\n if (err instanceof StorageError) throw err;\n throw new StorageError(\n `Failed to read ${key}: ${err instanceof Error ? err.message : String(err)}`,\n \"read\",\n );\n }\n }\n\n async updateMetadata(\n shlId: string,\n updater: (current: SHLMetadata) => SHLMetadata | null,\n ): Promise<SHLMetadata | null> {\n const key = `${shlId}/metadata.json`;\n const raw = await this.read(key);\n if (raw === null) return null;\n\n const current = JSON.parse(raw as string) as SHLMetadata;\n const updated = updater(current);\n if (updated === null) return null;\n\n await this.store(key, JSON.stringify(updated));\n return updated;\n }\n\n private async _getServerContainer(): Promise<AzureContainerClient> {\n if (!this._serverContainerClient) {\n const azure = await getAzureModule();\n const serviceClient = azure.BlobServiceClient.fromConnectionString(this.config.connectionString);\n this._serverContainerClient = serviceClient.getContainerClient(this.config.container);\n }\n return this._serverContainerClient;\n }\n\n private _serverBlobName(key: string): string {\n const prefix = this.config.prefix?.replace(/\\/+$/, \"\");\n return prefix ? `${prefix}/${key}` : key;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Google Cloud Storage (Server)\n// ---------------------------------------------------------------------------\n\n// Minimal GCS interfaces for server-side operations\ninterface GCSModule {\n Storage: new () => GCSStorageClient;\n}\ninterface GCSStorageClient {\n bucket(name: string): GCSBucket;\n}\ninterface GCSBucket {\n file(name: string): GCSFile;\n getFiles(options?: { prefix?: string }): Promise<[GCSFile[]]>;\n}\ninterface GCSFile {\n save(content: Buffer, options?: Record<string, unknown>): Promise<void>;\n delete(options?: Record<string, unknown>): Promise<unknown>;\n download(): Promise<[Buffer]>;\n name: string;\n}\n\nlet _gcsModule: GCSModule | undefined;\nasync function getGCSModule(): Promise<GCSModule> {\n if (_gcsModule) return _gcsModule;\n try {\n _gcsModule = (await import(\"@google-cloud/storage\")) as unknown as GCSModule;\n return _gcsModule;\n } catch {\n throw new StorageError(\n \"@google-cloud/storage is required for ServerGCSStorage. Install it: npm install @google-cloud/storage\",\n \"import\",\n );\n }\n}\n\n/**\n * Google Cloud Storage server storage for SMART Health Links.\n *\n * Extends the base `GCSStorage` with `read` and `updateMetadata`\n * methods needed for serving SHLs.\n *\n * @example\n * ```ts\n * import { ServerGCSStorage } from \"@fhirfly-io/shl/server\";\n *\n * const storage = new ServerGCSStorage({\n * bucket: \"my-shl-bucket\",\n * baseUrl: \"https://shl.example.com\",\n * });\n * ```\n */\nexport class ServerGCSStorage extends GCSStorage implements SHLServerStorage {\n private _serverBucket?: GCSBucket;\n\n constructor(config: GCSStorageConfig) {\n super(config);\n }\n\n async read(key: string): Promise<string | Uint8Array | null> {\n try {\n const bucket = await this._getServerBucket();\n const fileName = this._serverFileName(key);\n const file = bucket.file(fileName);\n\n const [content] = await file.download();\n return content.toString(\"utf8\");\n } catch (err) {\n const code = (err as { code?: number }).code;\n if (code === 404) return null;\n if (err instanceof StorageError) throw err;\n throw new StorageError(\n `Failed to read ${key}: ${err instanceof Error ? err.message : String(err)}`,\n \"read\",\n );\n }\n }\n\n async updateMetadata(\n shlId: string,\n updater: (current: SHLMetadata) => SHLMetadata | null,\n ): Promise<SHLMetadata | null> {\n const key = `${shlId}/metadata.json`;\n const raw = await this.read(key);\n if (raw === null) return null;\n\n const current = JSON.parse(raw as string) as SHLMetadata;\n const updated = updater(current);\n if (updated === null) return null;\n\n await this.store(key, JSON.stringify(updated));\n return updated;\n }\n\n private async _getServerBucket(): Promise<GCSBucket> {\n if (!this._serverBucket) {\n const gcs = await getGCSModule();\n const storage = new gcs.Storage();\n this._serverBucket = storage.bucket(this.config.bucket);\n }\n return this._serverBucket;\n }\n\n private _serverFileName(key: string): string {\n const prefix = this.config.prefix?.replace(/\\/+$/, \"\");\n return prefix ? `${prefix}/${key}` : key;\n }\n}\n\n/** Helper: convert a Node.js ReadableStream to a string. */\nasync function streamToString(stream: NodeJS.ReadableStream): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string));\n }\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n"]}
|