@fhirfly-io/shl 0.1.1 → 0.3.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 (62) hide show
  1. package/README.md +131 -53
  2. package/dist/chunk-5I5H3SLO.cjs +286 -0
  3. package/dist/chunk-5I5H3SLO.cjs.map +1 -0
  4. package/dist/{chunk-7WIM2QP5.js → chunk-7SVQ24UF.js} +13 -3
  5. package/dist/chunk-7SVQ24UF.js.map +1 -0
  6. package/dist/chunk-IDS43FSS.cjs +1956 -0
  7. package/dist/chunk-IDS43FSS.cjs.map +1 -0
  8. package/dist/{chunk-CNVYKA4D.cjs → chunk-M4HWBUJW.cjs} +13 -3
  9. package/dist/chunk-M4HWBUJW.cjs.map +1 -0
  10. package/dist/chunk-NCMJCDJQ.js +1949 -0
  11. package/dist/chunk-NCMJCDJQ.js.map +1 -0
  12. package/dist/chunk-NEBFJSJW.js +281 -0
  13. package/dist/chunk-NEBFJSJW.js.map +1 -0
  14. package/dist/chunk-UDS6UJAL.cjs +388 -0
  15. package/dist/chunk-UDS6UJAL.cjs.map +1 -0
  16. package/dist/chunk-VKB3ESIV.js +378 -0
  17. package/dist/chunk-VKB3ESIV.js.map +1 -0
  18. package/dist/cli.cjs +461 -0
  19. package/dist/cli.cjs.map +1 -0
  20. package/dist/cli.d.cts +1 -0
  21. package/dist/cli.d.ts +1 -0
  22. package/dist/cli.js +458 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/express.cjs +2 -2
  25. package/dist/express.d.cts +2 -2
  26. package/dist/express.d.ts +2 -2
  27. package/dist/express.js +1 -1
  28. package/dist/fastify.cjs +2 -2
  29. package/dist/fastify.d.cts +2 -2
  30. package/dist/fastify.d.ts +2 -2
  31. package/dist/fastify.js +1 -1
  32. package/dist/index.cjs +13 -1594
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +134 -4
  35. package/dist/index.d.ts +134 -4
  36. package/dist/index.js +3 -1591
  37. package/dist/index.js.map +1 -1
  38. package/dist/lambda.cjs +2 -2
  39. package/dist/lambda.d.cts +2 -2
  40. package/dist/lambda.d.ts +2 -2
  41. package/dist/lambda.js +1 -1
  42. package/dist/server.cjs +21 -156
  43. package/dist/server.cjs.map +1 -1
  44. package/dist/server.d.cts +54 -5
  45. package/dist/server.d.ts +54 -5
  46. package/dist/server.js +3 -156
  47. package/dist/server.js.map +1 -1
  48. package/dist/storage-B3GyJD2y.d.ts +205 -0
  49. package/dist/storage-BwszYwFo.d.cts +205 -0
  50. package/dist/{types--f4ITgu9.d.cts → types-B6RKj_xO.d.cts} +1 -1
  51. package/dist/{types-DKtPO4DP.d.ts → types-D_oopfOL.d.ts} +1 -1
  52. package/dist/{types-yk0mDByJ.d.cts → types-Doq5cGNm.d.cts} +8 -1
  53. package/dist/{types-yk0mDByJ.d.ts → types-Doq5cGNm.d.ts} +8 -1
  54. package/package.json +16 -2
  55. package/dist/chunk-7WIM2QP5.js.map +0 -1
  56. package/dist/chunk-CNVYKA4D.cjs.map +0 -1
  57. package/dist/chunk-KI44MYPE.cjs +0 -170
  58. package/dist/chunk-KI44MYPE.cjs.map +0 -1
  59. package/dist/chunk-XTLU6O32.js +0 -163
  60. package/dist/chunk-XTLU6O32.js.map +0 -1
  61. package/dist/storage-CHi9vLD_.d.cts +0 -82
  62. 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
 
@@ -14,104 +14,182 @@ 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
 
28
+ For FHIRfly API enrichment (optional — adds display names and SNOMED cross-mappings):
29
+
30
+ ```bash
31
+ npm install @fhirfly-io/shl @fhirfly-io/terminology
32
+ ```
33
+
23
34
  ## Quick Start
24
35
 
36
+ ### Without FHIRfly API (no key needed)
37
+
25
38
  ```typescript
26
- import { IPS, SHL } from '@fhirfly-io/shl';
27
- import { Fhirfly } from '@fhirfly-io/terminology';
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" });
28
51
 
29
- const fhirfly = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY });
52
+ const fhirBundle = await bundle.build();
30
53
 
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' },
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
+
63
+ ```typescript
64
+ import { IPS, SHL } from "@fhirfly-io/shl";
65
+ import Fhirfly from "@fhirfly-io/terminology";
66
+
67
+ const client = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY });
68
+
69
+ const bundle = new IPS.Bundle({
70
+ name: "Maria Garcia",
71
+ birthDate: "1985-03-15",
72
+ gender: "female",
34
73
  });
35
74
 
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' });
75
+ // Code-based input — FHIRfly enriches with display names, SNOMED mappings, etc.
76
+ bundle.addMedication({ byNDC: "00071015523", fhirfly: client.ndc });
77
+ bundle.addCondition({ byICD10: "E11.9", fhirfly: client.icd10 });
78
+ bundle.addAllergy({ bySNOMED: "387207008" });
79
+ bundle.addImmunization({ byCVX: "208", fhirfly: client.cvx });
80
+ bundle.addResult({ byLOINC: "2339-0", fhirfly: client.loinc, value: 95, unit: "mg/dL" });
81
+ bundle.addDocument({ content: pdfBuffer, contentType: "application/pdf", title: "Visit Summary" });
41
82
 
42
- // Build with configurable output profile
43
- const bundle = await ips.build({ profile: 'ips' }); // or 'r4' for generic FHIR
83
+ const fhirBundle = await bundle.build();
44
84
 
45
- // Validatemandatory before packaging
46
- const validation = await bundle.validate();
85
+ // FhirflyStoragezero infrastructure, included free in all plans
86
+ const storage = new SHL.FhirflyStorage({ apiKey: process.env.FHIRFLY_API_KEY });
47
87
 
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',
88
+ const result = await SHL.create({
89
+ bundle: fhirBundle,
90
+ storage,
91
+ passcode: "1234",
92
+ label: "Maria's Health Summary",
54
93
  });
55
94
 
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
95
+ console.log(result.url); // shlink:/eyJ1cmwiOiJodHRwczovL...
96
+ console.log(result.qrCode); // data:image/png;base64,...
97
+ console.log(result.passcode); // "1234"
59
98
  ```
60
99
 
61
- ## Design Principles
100
+ ## Storage Adapters
101
+
102
+ ```typescript
103
+ // FHIRfly hosted (zero infrastructure, recommended)
104
+ new SHL.FhirflyStorage({ apiKey: "..." });
62
105
 
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
106
+ // AWS S3
107
+ new SHL.S3Storage({ bucket: "my-bucket", region: "us-east-1", baseUrl: "https://shl.example.com" });
69
108
 
70
- ## Output Profiles
109
+ // Azure Blob Storage
110
+ new SHL.AzureStorage({ container: "shl-data", connectionString: "...", baseUrl: "https://shl.example.com" });
71
111
 
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 |
112
+ // Google Cloud Storage
113
+ new SHL.GCSStorage({ bucket: "my-bucket", baseUrl: "https://shl.example.com" });
114
+
115
+ // Local filesystem (development)
116
+ new SHL.LocalStorage({ directory: "./shl-data", baseUrl: "http://localhost:3456/shl" });
117
+ ```
76
118
 
77
119
  ## Input Formats
78
120
 
79
121
  Each `add*` method supports multiple input formats:
80
122
 
81
123
  ```typescript
82
- // From raw codes (enriched via FHIRfly API)
83
- ips.addMedication({ byNDC: '0069-3150-83', fhirfly });
84
- ips.addMedication({ byRxNorm: '161', fhirfly });
124
+ // From codes (enriched via FHIRfly API)
125
+ bundle.addMedication({ byNDC: "00071015523", fhirfly: client.ndc });
126
+ bundle.addMedication({ byRxNorm: "161", fhirfly: client.rxnorm });
127
+ bundle.addCondition({ byICD10: "E11.9", fhirfly: client.icd10 });
128
+ bundle.addResult({ byLOINC: "2339-0", fhirfly: client.loinc, value: 95, unit: "mg/dL" });
129
+ bundle.addImmunization({ byCVX: "208", fhirfly: client.cvx });
85
130
 
86
131
  // From SNOMED (no API call needed)
87
- ips.addMedication({ bySNOMED: '376988009' });
132
+ bundle.addMedication({ bySNOMED: "376988009" });
133
+ bundle.addAllergy({ bySNOMED: "387207008" });
88
134
 
89
135
  // From existing FHIR R4 resources
90
- ips.addMedication({ fromResource: existingMedicationStatement });
136
+ bundle.addMedication({ fromResource: existingMedicationStatement });
137
+ bundle.addCondition({ fromResource: existingCondition });
138
+
139
+ // Manual coding (no API dependency)
140
+ bundle.addMedication({ code: "376988009", system: "http://snomed.info/sct", display: "Levothyroxine" });
91
141
  ```
92
142
 
93
- ## Storage Adapters
143
+ ## CLI
144
+
145
+ ```bash
146
+ npx @fhirfly-io/shl validate bundle.json # Validate a FHIR Bundle
147
+ npx @fhirfly-io/shl create bundle.json # Create an SHL from a bundle
148
+ npx @fhirfly-io/shl decode shlink:/eyJ... # Decode an SHL URL
149
+ npx @fhirfly-io/shl serve # Start a local SHL server
150
+ npx @fhirfly-io/shl demo # Full round-trip demo
151
+ ```
152
+
153
+ ## Server Middleware
154
+
155
+ Host your own SHL endpoints:
94
156
 
95
157
  ```typescript
96
- // AWS S3 (pre-signed URLs)
97
- new SHL.S3Storage({ bucket: 'my-bucket', region: 'us-east-1' });
158
+ import express from "express";
159
+ import { expressMiddleware } from "@fhirfly-io/shl/express";
160
+ import { ServerLocalStorage } from "@fhirfly-io/shl/server";
98
161
 
99
- // Azure Blob Storage (SAS tokens)
100
- new SHL.AzureStorage({ container: 'my-container', connectionString: '...' });
162
+ const storage = new ServerLocalStorage({
163
+ directory: "./shl-data",
164
+ baseUrl: "http://localhost:3000/shl",
165
+ });
101
166
 
102
- // Google Cloud Storage
103
- new SHL.GCSStorage({ bucket: 'my-bucket' });
167
+ const app = express();
168
+ app.use("/shl", expressMiddleware({ storage }));
169
+ app.listen(3000);
170
+ ```
171
+
172
+ Also available for Fastify (`@fhirfly-io/shl/fastify`) and Lambda (`@fhirfly-io/shl/lambda`).
104
173
 
105
- // Local filesystem (development/testing only)
106
- new SHL.LocalStorage({ directory: './shl-data' });
174
+ ## Live Exercise
175
+
176
+ Run the comprehensive integration test against the live API to exercise every SDK path:
177
+
178
+ ```bash
179
+ npx tsx examples/live-exercise/index.ts --api-key <your-key> --verbose
107
180
  ```
108
181
 
182
+ 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.
183
+
109
184
  ## Related
110
185
 
111
- - [@fhirfly-io/terminology](https://www.npmjs.com/package/@fhirfly-io/terminology) — FHIRfly terminology API SDK (required for code enrichment)
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
188
+ - [@fhirfly-io/terminology](https://www.npmjs.com/package/@fhirfly-io/terminology) — FHIRfly terminology API SDK
112
189
  - [SMART Health Links Spec](https://docs.smarthealthit.org/smart-health-links/spec/)
113
190
  - [IPS Implementation Guide](https://build.fhir.org/ig/HL7/fhir-ips/)
114
- - [FHIRfly Documentation](https://fhirfly.io/docs)
191
+ - [FHIRfly SHL Docs](https://fhirfly.io/docs/shl/overview)
192
+ - [SHL Viewer](https://fhirfly.io/shl/viewer)
115
193
 
116
194
  ## License
117
195
 
@@ -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"]}
@@ -1,3 +1,5 @@
1
+ import { createHash, timingSafeEqual } from 'crypto';
2
+
1
3
  // src/server/handler.ts
2
4
  function createHandler(config) {
3
5
  const { storage, onAccess } = config;
@@ -49,7 +51,15 @@ async function handleManifest(shlId, req, storage, onAccess) {
49
51
  return null;
50
52
  }
51
53
  if (metadata.passcode) {
52
- if (!providedPasscode || providedPasscode !== metadata.passcode) {
54
+ if (!providedPasscode) {
55
+ accessDeniedReason = "passcode";
56
+ return null;
57
+ }
58
+ const providedHash = createHash("sha256").update(providedPasscode).digest("hex");
59
+ const storedHash = metadata.passcode;
60
+ const a = Buffer.from(providedHash);
61
+ const b = Buffer.from(storedHash);
62
+ if (a.length !== b.length || !timingSafeEqual(a, b)) {
53
63
  accessDeniedReason = "passcode";
54
64
  return null;
55
65
  }
@@ -129,5 +139,5 @@ function jsonResponse(status, body) {
129
139
  }
130
140
 
131
141
  export { createHandler };
132
- //# sourceMappingURL=chunk-7WIM2QP5.js.map
133
- //# sourceMappingURL=chunk-7WIM2QP5.js.map
142
+ //# sourceMappingURL=chunk-7SVQ24UF.js.map
143
+ //# sourceMappingURL=chunk-7SVQ24UF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/handler.ts"],"names":[],"mappings":"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","file":"chunk-7SVQ24UF.js","sourcesContent":["// Copyright 2026 FHIRfly.io LLC. All rights reserved.\n// Licensed under the MIT License. See LICENSE file in the project root.\nimport type {\n HandlerRequest,\n HandlerResponse,\n SHLHandlerConfig,\n} from \"./types.js\";\nimport type { SHLMetadata, Manifest } from \"../shl/types.js\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Create a framework-agnostic SHL request handler.\n *\n * Returns an async function that processes incoming requests and returns\n * responses. This handler implements two routes:\n *\n * - `POST /{shlId}` — Manifest endpoint (validates passcode, checks access limits)\n * - `GET /{shlId}/content` — Content endpoint (serves encrypted JWE)\n *\n * Framework adapters (Express, Fastify, Lambda) translate their native\n * request/response types to/from `HandlerRequest`/`HandlerResponse`.\n *\n * @example\n * ```ts\n * const handle = createHandler({ storage });\n * const response = await handle({\n * method: \"POST\",\n * path: \"/abc123\",\n * body: { passcode: \"1234\" },\n * headers: { \"content-type\": \"application/json\" },\n * });\n * ```\n */\nexport function createHandler(\n config: SHLHandlerConfig,\n): (req: HandlerRequest) => Promise<HandlerResponse> {\n const { storage, onAccess } = config;\n\n return async (req: HandlerRequest): Promise<HandlerResponse> => {\n // Normalize path: strip leading slash, split into segments\n const path = req.path.replace(/^\\/+/, \"\");\n const segments = path.split(\"/\").filter(Boolean);\n\n // Route: POST /{shlId} → manifest\n if (segments.length === 1 && req.method === \"POST\") {\n return handleManifest(segments[0]!, req, storage, onAccess);\n }\n\n // Route: GET /{shlId}/content → serve encrypted content\n if (segments.length === 2 && segments[1] === \"content\" && req.method === \"GET\") {\n return handleContent(segments[0]!, storage);\n }\n\n // Route: GET /{shlId}/attachment/{index} → serve encrypted attachment\n if (segments.length === 3 && segments[1] === \"attachment\" && req.method === \"GET\") {\n const index = segments[2]!;\n return handleAttachment(segments[0]!, index, storage);\n }\n\n // Method not allowed for known paths\n if (segments.length === 1 && req.method !== \"POST\") {\n return jsonResponse(405, { error: \"Method not allowed. Use POST for manifest requests.\" });\n }\n if (segments.length === 2 && segments[1] === \"content\" && req.method !== \"GET\") {\n return jsonResponse(405, { error: \"Method not allowed. Use GET for content requests.\" });\n }\n if (segments.length === 3 && segments[1] === \"attachment\" && req.method !== \"GET\") {\n return jsonResponse(405, { error: \"Method not allowed. Use GET for attachment requests.\" });\n }\n\n return jsonResponse(404, { error: \"Not found\" });\n };\n}\n\nasync function handleManifest(\n shlId: string,\n req: HandlerRequest,\n storage: SHLHandlerConfig[\"storage\"],\n onAccess?: SHLHandlerConfig[\"onAccess\"],\n): Promise<HandlerResponse> {\n // Read manifest to verify the SHL exists\n const manifestRaw = await storage.read(`${shlId}/manifest.json`);\n if (manifestRaw === null) {\n return jsonResponse(404, { error: \"SHL not found\" });\n }\n\n // Atomically check access control + increment counter\n let updatedMetadata: SHLMetadata | null = null;\n let accessDeniedReason: \"expired\" | \"exhausted\" | \"passcode\" | null = null;\n const reqBody = (req.body && typeof req.body === \"object\" ? req.body : {}) as Record<string, unknown>;\n const providedPasscode = typeof reqBody[\"passcode\"] === \"string\" ? reqBody[\"passcode\"] : undefined;\n\n updatedMetadata = await storage.updateMetadata(shlId, (metadata) => {\n // Check expiration\n if (metadata.expiresAt) {\n const expiresAt = new Date(metadata.expiresAt);\n if (expiresAt.getTime() <= Date.now()) {\n accessDeniedReason = \"expired\";\n return null;\n }\n }\n\n // Check access count\n const currentCount = metadata.accessCount ?? 0;\n if (metadata.maxAccesses !== undefined && currentCount >= metadata.maxAccesses) {\n accessDeniedReason = \"exhausted\";\n return null;\n }\n\n // Check passcode (timing-safe comparison with SHA-256 hash)\n if (metadata.passcode) {\n if (!providedPasscode) {\n accessDeniedReason = \"passcode\";\n return null;\n }\n const providedHash = createHash(\"sha256\").update(providedPasscode).digest(\"hex\");\n const storedHash = metadata.passcode;\n const a = Buffer.from(providedHash);\n const b = Buffer.from(storedHash);\n // Constant-time comparison: compare with self if lengths differ to avoid timing leak\n if (a.length !== b.length || !timingSafeEqual(a, b)) {\n accessDeniedReason = \"passcode\";\n return null;\n }\n }\n\n // Access granted — increment count\n return {\n ...metadata,\n accessCount: currentCount + 1,\n };\n });\n\n // Handle access control failures\n if (accessDeniedReason === \"expired\") {\n return jsonResponse(410, { error: \"SHL has expired\" });\n }\n if (accessDeniedReason === \"exhausted\") {\n return jsonResponse(410, { error: \"SHL access limit reached\" });\n }\n if (accessDeniedReason === \"passcode\") {\n return jsonResponse(401, { error: \"Invalid passcode\" });\n }\n\n // If updateMetadata returned null but no denied reason, metadata file is missing\n if (updatedMetadata === null) {\n return jsonResponse(404, { error: \"SHL not found\" });\n }\n\n // Fire access event (non-blocking)\n if (onAccess) {\n const event = {\n shlId,\n accessCount: updatedMetadata.accessCount ?? 1,\n timestamp: new Date(),\n };\n // Fire and forget — don't let callback errors break the response\n Promise.resolve(onAccess(event)).catch(() => {});\n }\n\n // Return manifest\n const manifestStr = typeof manifestRaw === \"string\"\n ? manifestRaw\n : new TextDecoder().decode(manifestRaw);\n const manifest = JSON.parse(manifestStr) as Manifest;\n\n return jsonResponse(200, manifest);\n}\n\nasync function handleContent(\n shlId: string,\n storage: SHLHandlerConfig[\"storage\"],\n): Promise<HandlerResponse> {\n const content = await storage.read(`${shlId}/content.jwe`);\n if (content === null) {\n return jsonResponse(404, { error: \"Content not found\" });\n }\n\n const body = typeof content === \"string\"\n ? content\n : new TextDecoder().decode(content);\n\n return {\n status: 200,\n headers: {\n \"content-type\": \"application/jose\",\n \"cache-control\": \"no-store\",\n },\n body,\n };\n}\n\nasync function handleAttachment(\n shlId: string,\n index: string,\n storage: SHLHandlerConfig[\"storage\"],\n): Promise<HandlerResponse> {\n if (!/^\\d+$/.test(index)) {\n return jsonResponse(400, { error: \"Invalid attachment index\" });\n }\n const content = await storage.read(`${shlId}/attachment-${index}.jwe`);\n if (content === null) {\n return jsonResponse(404, { error: \"Attachment not found\" });\n }\n const body = typeof content === \"string\"\n ? content\n : new TextDecoder().decode(content);\n return {\n status: 200,\n headers: {\n \"content-type\": \"application/jose\",\n \"cache-control\": \"no-store\",\n },\n body,\n };\n}\n\nfunction jsonResponse(status: number, body: unknown): HandlerResponse {\n return {\n status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n body: JSON.stringify(body),\n };\n}\n"]}