@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.
Files changed (50) hide show
  1. package/README.md +95 -53
  2. package/dist/chunk-5I5H3SLO.cjs +286 -0
  3. package/dist/chunk-5I5H3SLO.cjs.map +1 -0
  4. package/dist/chunk-AIOYPNKN.js +1926 -0
  5. package/dist/chunk-AIOYPNKN.js.map +1 -0
  6. package/dist/chunk-NEBFJSJW.js +281 -0
  7. package/dist/chunk-NEBFJSJW.js.map +1 -0
  8. package/dist/chunk-UDS6UJAL.cjs +388 -0
  9. package/dist/chunk-UDS6UJAL.cjs.map +1 -0
  10. package/dist/chunk-UFFYRACT.cjs +1933 -0
  11. package/dist/chunk-UFFYRACT.cjs.map +1 -0
  12. package/dist/chunk-VKB3ESIV.js +378 -0
  13. package/dist/chunk-VKB3ESIV.js.map +1 -0
  14. package/dist/cli.cjs +461 -0
  15. package/dist/cli.cjs.map +1 -0
  16. package/dist/cli.d.cts +1 -0
  17. package/dist/cli.d.ts +1 -0
  18. package/dist/cli.js +458 -0
  19. package/dist/cli.js.map +1 -0
  20. package/dist/express.d.cts +2 -2
  21. package/dist/express.d.ts +2 -2
  22. package/dist/fastify.d.cts +2 -2
  23. package/dist/fastify.d.ts +2 -2
  24. package/dist/index.cjs +13 -1594
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +109 -4
  27. package/dist/index.d.ts +109 -4
  28. package/dist/index.js +3 -1591
  29. package/dist/index.js.map +1 -1
  30. package/dist/lambda.d.cts +2 -2
  31. package/dist/lambda.d.ts +2 -2
  32. package/dist/server.cjs +19 -154
  33. package/dist/server.cjs.map +1 -1
  34. package/dist/server.d.cts +54 -5
  35. package/dist/server.d.ts +54 -5
  36. package/dist/server.js +2 -155
  37. package/dist/server.js.map +1 -1
  38. package/dist/storage-D1NajOTq.d.cts +205 -0
  39. package/dist/storage-DYEX5kiP.d.ts +205 -0
  40. package/dist/{types--f4ITgu9.d.cts → types-BbvJirBn.d.cts} +1 -1
  41. package/dist/{types-yk0mDByJ.d.cts → types-DpkUjBYr.d.cts} +1 -1
  42. package/dist/{types-yk0mDByJ.d.ts → types-DpkUjBYr.d.ts} +1 -1
  43. package/dist/{types-DKtPO4DP.d.ts → types-qPv1O_sF.d.ts} +1 -1
  44. package/package.json +16 -2
  45. package/dist/chunk-KI44MYPE.cjs +0 -170
  46. package/dist/chunk-KI44MYPE.cjs.map +0 -1
  47. package/dist/chunk-XTLU6O32.js +0 -163
  48. package/dist/chunk-XTLU6O32.js.map +0 -1
  49. package/dist/storage-CHi9vLD_.d.cts +0 -82
  50. 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, enrich with terminology, encrypt, and share via SHL/QR code.
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
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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 '@fhirfly-io/shl';
27
- import { Fhirfly } from '@fhirfly-io/terminology';
32
+ import { IPS, SHL } from "@fhirfly-io/shl";
33
+ import Fhirfly from "@fhirfly-io/terminology";
28
34
 
29
- const fhirfly = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY });
35
+ const client = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY });
30
36
 
31
- // Build incrementally each method is independently testable
32
- const ips = new IPS.Bundle({
33
- patient: { name: 'Oliver Brown', dob: '1990-03-15', gender: 'male' },
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
- ips.addMedication({ byNDC: '0069-3150-83', fhirfly });
37
- ips.addCondition({ byICD10: 'E11.9', fhirfly });
38
- ips.addImmunization({ byCVX: '207', fhirfly });
39
- ips.addResult({ byLOINC: '2339-0', value: 95, unit: 'mg/dL' });
40
- ips.addDocument({ pdf: pdfBuffer, title: 'Visit Summary' });
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
- // Build with configurable output profile
43
- const bundle = await ips.build({ profile: 'ips' }); // or 'r4' for generic FHIR
49
+ const fhirBundle = await bundle.build();
44
50
 
45
- // Validate mandatory before packaging
46
- const validation = await bundle.validate();
51
+ // Create the SHL (zero-infra with FhirflyStorage)
52
+ const storage = new SHL.FhirflyStorage({ apiKey: process.env.FHIRFLY_API_KEY });
47
53
 
48
- // Package as SHL
49
- const shl = await SHL.create(bundle, {
50
- storage: new SHL.S3Storage({ bucket: 'my-hipaa-bucket', region: 'us-east-1' }),
51
- expiration: '30d',
52
- passcode: { generate: true },
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(shl.url); // shlink:/eyJ1cmwiOiJodHRwczovL...
57
- console.log(shl.qrCode); // Base64 PNG
58
- console.log(shl.passcode); // Communicate out-of-band to patient
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
- ## Design Principles
66
+ ## Storage Adapters
62
67
 
63
- - **Composable methods** — small, testable `add*` methods that build up a Bundle incrementally
64
- - **Configurable output** — `build({ profile: "ips" })` for IPS document Bundles, `build({ profile: "r4" })` for generic FHIR collections
65
- - **Bring Your Own Storage** — developer controls where encrypted PHI lives (S3, Azure, GCS, local)
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
- ## Output Profiles
72
+ // AWS S3
73
+ new SHL.S3Storage({ bucket: "my-bucket", region: "us-east-1", baseUrl: "https://shl.example.com" });
71
74
 
72
- | Profile | Bundle.type | Composition | Use Case |
73
- |---------|-------------|-------------|----------|
74
- | `"ips"` | `document` | Yes (with IPS sections) | Kill the Clipboard, patient portals |
75
- | `"r4"` | `collection` | No | Apps needing FHIR Bundles without IPS compliance |
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 raw codes (enriched via FHIRfly API)
83
- ips.addMedication({ byNDC: '0069-3150-83', fhirfly });
84
- ips.addMedication({ byRxNorm: '161', fhirfly });
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
- ips.addMedication({ bySNOMED: '376988009' });
98
+ bundle.addMedication({ bySNOMED: "376988009" });
99
+ bundle.addAllergy({ bySNOMED: "387207008" });
88
100
 
89
101
  // From existing FHIR R4 resources
90
- ips.addMedication({ fromResource: existingMedicationStatement });
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
- ## Storage Adapters
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
- // AWS S3 (pre-signed URLs)
97
- new SHL.S3Storage({ bucket: 'my-bucket', region: 'us-east-1' });
124
+ import express from "express";
125
+ import { createShlMiddleware } from "@fhirfly-io/shl/express";
126
+ import { ServerLocalStorage } from "@fhirfly-io/shl/server";
98
127
 
99
- // Azure Blob Storage (SAS tokens)
100
- new SHL.AzureStorage({ container: 'my-container', connectionString: '...' });
128
+ const storage = new ServerLocalStorage({
129
+ directory: "./shl-data",
130
+ baseUrl: "http://localhost:3000/shl",
131
+ });
101
132
 
102
- // Google Cloud Storage
103
- new SHL.GCSStorage({ bucket: 'my-bucket' });
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
- // Local filesystem (development/testing only)
106
- new SHL.LocalStorage({ directory: './shl-data' });
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 (required for code enrichment)
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 Documentation](https://fhirfly.io/docs)
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":";;;;;;AA0BO,IAAM,kBAAA,GAAN,cAAiCA,8BAAA,CAAyC;AAAA,EAC/E,YAAY,MAAA,EAA4B;AACtC,IAAA,KAAA,CAAM,MAAM,CAAA;AAAA,EACd;AAAA,EAEA,MAAM,KAAK,GAAA,EAAkD;AAC3D,IAAA,MAAM,QAAA,GAAWC,SAAA,CAAK,IAAA,CAAK,MAAA,CAAO,WAAW,GAAG,CAAA;AAChD,IAAA,IAAI,CAACC,aAAA,CAAW,QAAQ,CAAA,EAAG;AACzB,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,IAAI;AAEF,MAAA,OAAOC,eAAA,CAAa,UAAU,MAAM,CAAA;AAAA,IACtC,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,cAAA,CACJ,KAAA,EACA,OAAA,EAC6B;AAC7B,IAAA,MAAM,GAAA,GAAM,GAAG,KAAK,CAAA,cAAA,CAAA;AACpB,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AAC/B,IAAA,IAAI,GAAA,KAAQ,MAAM,OAAO,IAAA;AAEzB,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,GAAa,CAAA;AACxC,IAAA,MAAM,OAAA,GAAU,QAAQ,OAAO,CAAA;AAC/B,IAAA,IAAI,OAAA,KAAY,MAAM,OAAO,IAAA;AAE7B,IAAA,MAAM,KAAK,KAAA,CAAM,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAC7C,IAAA,OAAO,OAAA;AAAA,EACT;AACF;AAcA,IAAI,SAAA;AACJ,eAAe,WAAA,GAAiC;AAC9C,EAAA,IAAI,WAAW,OAAO,SAAA;AACtB,EAAA,IAAI;AACF,IAAA,SAAA,GAAa,MAAM,OAAO,oBAAoB,CAAA;AAC9C,IAAA,OAAO,SAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAIC,8BAAA;AAAA,MACR,gGAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AACF;AAqBO,IAAM,kBAAN,MAAkD;AAAA,EACtC,OAAA;AAAA,EACT,OAAA;AAAA,EAER,YAAY,MAAA,EAAyB;AACnC,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AAAA,EACjB;AAAA,EAEA,IAAI,OAAA,GAAkB;AACpB,IAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,OAAA,CAAQ,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAAA,EAChD;AAAA,EAEA,MAAM,KAAA,CAAM,GAAA,EAAa,OAAA,EAA6C;AACpE,IAAA,IAAI;AACF,MAAA,MAAM,EAAA,GAAK,MAAM,WAAA,EAAY;AAC7B,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,UAAA,CAAW,EAAE,CAAA;AACjC,MAAA,MAAM,IAAA,GAAO,OAAO,OAAA,KAAY,QAAA,GAAW,OAAO,IAAA,CAAK,OAAA,EAAS,MAAM,CAAA,GAAI,OAAA;AAE1E,MAAA,MAAM,OAAA,GAAU,IAAI,EAAA,CAAG,gBAAA,CAAiB;AAAA,QACtC,MAAA,EAAQ,KAAK,OAAA,CAAQ,MAAA;AAAA,QACrB,GAAA,EAAK,IAAA,CAAK,MAAA,CAAO,GAAG,CAAA;AAAA,QACpB,IAAA,EAAM,IAAA;AAAA,QACN,WAAA,EAAa,IAAA,CAAK,YAAA,CAAa,GAAG;AAAA,OACnC,CAAA;AAED,MAAA,MAAM,MAAA,CAAO,KAAK,OAAO,CAAA;AAAA,IAC3B,SAAS,GAAA,EAAK;AACZ,MAAA,IAAI,GAAA,YAAeA,gCAAc,MAAM,GAAA;AACvC,MAAA,MAAM,IAAIA,8BAAA;AAAA,QACR,CAAA,gBAAA,EAAmB,GAAG,CAAA,EAAA,EAAK,GAAA,YAAe,QAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA,CAAA;AAAA,QAC3E;AAAA,OACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,MAAA,EAA+B;AAC1C,IAAA,IAAI;AACF,MAAA,MAAM,EAAA,GAAK,MAAM,WAAA,EAAY;AAC7B,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,UAAA,CAAW,EAAE,CAAA;AACjC,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,MAAA,CAAO,MAAM,CAAA;AAEnC,MAAA,IAAI,iBAAA;AACJ,MAAA,GAAG;AACD,QAAA,MAAM,SAAA,GAAqC;AAAA,UACzC,MAAA,EAAQ,KAAK,OAAA,CAAQ,MAAA;AAAA,UACrB,MAAA,EAAQ;AAAA,SACV;AACA,QAAA,IAAI,iBAAA,EAAmB,SAAA,CAAU,mBAAmB,CAAA,GAAI,iBAAA;AAExD,QAAA,MAAM,WAAA,GAAc,IAAI,EAAA,CAAG,oBAAA,CAAqB,SAAS,CAAA;AACzD,QAAA,MAAM,QAAA,GAAY,MAAM,MAAA,CAAO,IAAA,CAAK,WAAW,CAAA;AAM/C,QAAA,MAAM,UAAU,QAAA,CAAS,QAAA;AACzB,QAAA,IAAI,CAAC,OAAA,IAAW,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG;AAEtC,QAAA,MAAM,aAAA,GAAgB,IAAI,EAAA,CAAG,oBAAA,CAAqB;AAAA,UAChD,MAAA,EAAQ,KAAK,OAAA,CAAQ,MAAA;AAAA,UACrB,MAAA,EAAQ;AAAA,YACN,OAAA,EAAS,QAAQ,GAAA,CAAI,CAAC,SAAS,EAAE,GAAA,EAAK,GAAA,CAAI,GAAA,EAAI,CAAE,CAAA;AAAA,YAChD,KAAA,EAAO;AAAA;AACT,SACD,CAAA;AACD,QAAA,MAAM,MAAA,CAAO,KAAK,aAAa,CAAA;AAE/B,QAAA,iBAAA,GAAoB,QAAA,CAAS,WAAA,GACzB,QAAA,CAAS,qBAAA,GACT,KAAA,CAAA;AAAA,MACN,CAAA,QAAS,iBAAA;AAAA,IACX,SAAS,GAAA,EAAK;AACZ,MAAA,IAAI,GAAA,YAAeA,gCAAc,MAAM,GAAA;AACvC,MAAA,MAAM,IAAIA,8BAAA;AAAA,QACR,CAAA,iBAAA,EAAoB,MAAM,CAAA,EAAA,EAAK,GAAA,YAAe,QAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA,CAAA;AAAA,QAC/E;AAAA,OACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,GAAA,EAAkD;AAC3D,IAAA,IAAI;AACF,MAAA,MAAM,EAAA,GAAK,MAAM,WAAA,EAAY;AAC7B,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,UAAA,CAAW,EAAE,CAAA;AAEjC,MAAA,MAAM,OAAA,GAAU,IAAI,EAAA,CAAG,gBAAA,CAAiB;AAAA,QACtC,MAAA,EAAQ,KAAK,OAAA,CAAQ,MAAA;AAAA,QACrB,GAAA,EAAK,IAAA,CAAK,MAAA,CAAO,GAAG;AAAA,OACrB,CAAA;AAED,MAAA,MAAM,QAAA,GAAY,MAAM,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA;AAI3C,MAAA,IAAI,CAAC,QAAA,CAAS,IAAA,EAAM,OAAO,IAAA;AAC3B,MAAA,OAAO,QAAA,CAAS,KAAK,iBAAA,EAAkB;AAAA,IACzC,SAAS,GAAA,EAAK;AAEZ,MAAA,MAAM,OAAQ,GAAA,CAA0B,IAAA;AACxC,MAAA,IAAI,IAAA,KAAS,aAAa,OAAO,IAAA;AACjC,MAAA,IAAI,GAAA,YAAeA,gCAAc,MAAM,GAAA;AACvC,MAAA,MAAM,IAAIA,8BAAA;AAAA,QACR,CAAA,eAAA,EAAkB,GAAG,CAAA,EAAA,EAAK,GAAA,YAAe,QAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA,CAAA;AAAA,QAC1E;AAAA,OACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,cAAA,CACJ,KAAA,EACA,OAAA,EAC6B;AAC7B,IAAA,MAAM,GAAA,GAAM,GAAG,KAAK,CAAA,cAAA,CAAA;AACpB,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AAC/B,IAAA,IAAI,GAAA,KAAQ,MAAM,OAAO,IAAA;AAEzB,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,GAAa,CAAA;AACxC,IAAA,MAAM,OAAA,GAAU,QAAQ,OAAO,CAAA;AAC/B,IAAA,IAAI,OAAA,KAAY,MAAM,OAAO,IAAA;AAE7B,IAAA,MAAM,KAAK,KAAA,CAAM,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAC7C,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAEQ,WAAW,EAAA,EAAgC;AACjD,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AACjB,MAAA,IAAA,CAAK,OAAA,GAAU,IAAI,EAAA,CAAG,QAAA,CAAS,EAAE,MAAA,EAAQ,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,CAAA;AAAA,IAChE;AACA,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA,EAEQ,OAAO,GAAA,EAAqB;AAClC,IAAA,MAAM,SAAS,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACtD,IAAA,OAAO,MAAA,GAAS,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,GAAK,GAAA;AAAA,EACvC;AAAA,EAEQ,aAAa,GAAA,EAAqB;AACxC,IAAA,IAAI,GAAA,CAAI,QAAA,CAAS,MAAM,CAAA,EAAG,OAAO,kBAAA;AACjC,IAAA,IAAI,GAAA,CAAI,QAAA,CAAS,OAAO,CAAA,EAAG,OAAO,kBAAA;AAClC,IAAA,OAAO,0BAAA;AAAA,EACT;AACF;AAyBA,IAAI,YAAA;AACJ,eAAe,cAAA,GAA2C;AACxD,EAAA,IAAI,cAAc,OAAO,YAAA;AACzB,EAAA,IAAI;AACF,IAAA,YAAA,GAAgB,MAAM,OAAO,qBAAqB,CAAA;AAClD,IAAA,OAAO,YAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAIA,8BAAA;AAAA,MACR,qGAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AACF;AAmBO,IAAM,kBAAA,GAAN,cAAiCC,8BAAA,CAAyC;AAAA,EACvE,sBAAA;AAAA,EAER,YAAY,MAAA,EAA4B;AACtC,IAAA,KAAA,CAAM,MAAM,CAAA;AAAA,EACd;AAAA,EAEA,MAAM,KAAK,GAAA,EAAkD;AAC3D,IAAA,IAAI;AACF,MAAA,MAAM,SAAA,GAAY,MAAM,IAAA,CAAK,mBAAA,EAAoB;AACjD,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,eAAA,CAAgB,GAAG,CAAA;AACzC,MAAA,MAAM,MAAA,GAAS,SAAA,CAAU,kBAAA,CAAmB,QAAQ,CAAA;AAEpD,MAAA,MAAM,QAAA,GAAW,MAAM,MAAA,CAAO,QAAA,EAAS;AACvC,MAAA,IAAI,CAAC,QAAA,CAAS,kBAAA,EAAoB,OAAO,IAAA;AAEzC,MAAA,OAAO,MAAM,cAAA,CAAe,QAAA,CAAS,kBAAkB,CAAA;AAAA,IACzD,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,OAAQ,GAAA,CAAgC,UAAA;AAC9C,MAAA,IAAI,IAAA,KAAS,KAAK,OAAO,IAAA;AACzB,MAAA,IAAI,GAAA,YAAeD,gCAAc,MAAM,GAAA;AACvC,MAAA,MAAM,IAAIA,8BAAA;AAAA,QACR,CAAA,eAAA,EAAkB,GAAG,CAAA,EAAA,EAAK,GAAA,YAAe,QAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA,CAAA;AAAA,QAC1E;AAAA,OACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,cAAA,CACJ,KAAA,EACA,OAAA,EAC6B;AAC7B,IAAA,MAAM,GAAA,GAAM,GAAG,KAAK,CAAA,cAAA,CAAA;AACpB,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AAC/B,IAAA,IAAI,GAAA,KAAQ,MAAM,OAAO,IAAA;AAEzB,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,GAAa,CAAA;AACxC,IAAA,MAAM,OAAA,GAAU,QAAQ,OAAO,CAAA;AAC/B,IAAA,IAAI,OAAA,KAAY,MAAM,OAAO,IAAA;AAE7B,IAAA,MAAM,KAAK,KAAA,CAAM,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAC7C,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAEA,MAAc,mBAAA,GAAqD;AACjE,IAAA,IAAI,CAAC,KAAK,sBAAA,EAAwB;AAChC,MAAA,MAAM,KAAA,GAAQ,MAAM,cAAA,EAAe;AACnC,MAAA,MAAM,gBAAgB,KAAA,CAAM,iBAAA,CAAkB,oBAAA,CAAqB,IAAA,CAAK,OAAO,gBAAgB,CAAA;AAC/F,MAAA,IAAA,CAAK,sBAAA,GAAyB,aAAA,CAAc,kBAAA,CAAmB,IAAA,CAAK,OAAO,SAAS,CAAA;AAAA,IACtF;AACA,IAAA,OAAO,IAAA,CAAK,sBAAA;AAAA,EACd;AAAA,EAEQ,gBAAgB,GAAA,EAAqB;AAC3C,IAAA,MAAM,SAAS,IAAA,CAAK,MAAA,CAAO,MAAA,EAAQ,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACrD,IAAA,OAAO,MAAA,GAAS,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,GAAK,GAAA;AAAA,EACvC;AACF;AAwBA,IAAI,UAAA;AACJ,eAAe,YAAA,GAAmC;AAChD,EAAA,IAAI,YAAY,OAAO,UAAA;AACvB,EAAA,IAAI;AACF,IAAA,UAAA,GAAc,MAAM,OAAO,uBAAuB,CAAA;AAClD,IAAA,OAAO,UAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAIA,8BAAA;AAAA,MACR,uGAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AACF;AAkBO,IAAM,gBAAA,GAAN,cAA+BE,4BAAA,CAAuC;AAAA,EACnE,aAAA;AAAA,EAER,YAAY,MAAA,EAA0B;AACpC,IAAA,KAAA,CAAM,MAAM,CAAA;AAAA,EACd;AAAA,EAEA,MAAM,KAAK,GAAA,EAAkD;AAC3D,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,gBAAA,EAAiB;AAC3C,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,eAAA,CAAgB,GAAG,CAAA;AACzC,MAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA;AAEjC,MAAA,MAAM,CAAC,OAAO,CAAA,GAAI,MAAM,KAAK,QAAA,EAAS;AACtC,MAAA,OAAO,OAAA,CAAQ,SAAS,MAAM,CAAA;AAAA,IAChC,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,OAAQ,GAAA,CAA0B,IAAA;AACxC,MAAA,IAAI,IAAA,KAAS,KAAK,OAAO,IAAA;AACzB,MAAA,IAAI,GAAA,YAAeF,gCAAc,MAAM,GAAA;AACvC,MAAA,MAAM,IAAIA,8BAAA;AAAA,QACR,CAAA,eAAA,EAAkB,GAAG,CAAA,EAAA,EAAK,GAAA,YAAe,QAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA,CAAA;AAAA,QAC1E;AAAA,OACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,cAAA,CACJ,KAAA,EACA,OAAA,EAC6B;AAC7B,IAAA,MAAM,GAAA,GAAM,GAAG,KAAK,CAAA,cAAA,CAAA;AACpB,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AAC/B,IAAA,IAAI,GAAA,KAAQ,MAAM,OAAO,IAAA;AAEzB,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,GAAa,CAAA;AACxC,IAAA,MAAM,OAAA,GAAU,QAAQ,OAAO,CAAA;AAC/B,IAAA,IAAI,OAAA,KAAY,MAAM,OAAO,IAAA;AAE7B,IAAA,MAAM,KAAK,KAAA,CAAM,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAC7C,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAEA,MAAc,gBAAA,GAAuC;AACnD,IAAA,IAAI,CAAC,KAAK,aAAA,EAAe;AACvB,MAAA,MAAM,GAAA,GAAM,MAAM,YAAA,EAAa;AAC/B,MAAA,MAAM,OAAA,GAAU,IAAI,GAAA,CAAI,OAAA,EAAQ;AAChC,MAAA,IAAA,CAAK,aAAA,GAAgB,OAAA,CAAQ,MAAA,CAAO,IAAA,CAAK,OAAO,MAAM,CAAA;AAAA,IACxD;AACA,IAAA,OAAO,IAAA,CAAK,aAAA;AAAA,EACd;AAAA,EAEQ,gBAAgB,GAAA,EAAqB;AAC3C,IAAA,MAAM,SAAS,IAAA,CAAK,MAAA,CAAO,MAAA,EAAQ,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACrD,IAAA,OAAO,MAAA,GAAS,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,GAAK,GAAA;AAAA,EACvC;AACF;AAGA,eAAe,eAAe,MAAA,EAAgD;AAC5E,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,WAAA,MAAiB,SAAS,MAAA,EAAQ;AAChC,IAAA,MAAA,CAAO,IAAA,CAAK,OAAO,QAAA,CAAS,KAAK,IAAI,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,KAAe,CAAC,CAAA;AAAA,EAC3E;AACA,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA,CAAE,SAAS,MAAM,CAAA;AAC9C","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"]}