@fhirfly-io/shl 0.1.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/dist/chunk-7WIM2QP5.js +133 -0
  4. package/dist/chunk-7WIM2QP5.js.map +1 -0
  5. package/dist/chunk-CNVYKA4D.cjs +135 -0
  6. package/dist/chunk-CNVYKA4D.cjs.map +1 -0
  7. package/dist/chunk-KI44MYPE.cjs +170 -0
  8. package/dist/chunk-KI44MYPE.cjs.map +1 -0
  9. package/dist/chunk-PZ5AY32C.js +9 -0
  10. package/dist/chunk-PZ5AY32C.js.map +1 -0
  11. package/dist/chunk-Q7SFCCGT.cjs +11 -0
  12. package/dist/chunk-Q7SFCCGT.cjs.map +1 -0
  13. package/dist/chunk-XTLU6O32.js +163 -0
  14. package/dist/chunk-XTLU6O32.js.map +1 -0
  15. package/dist/express.cjs +37 -0
  16. package/dist/express.cjs.map +1 -0
  17. package/dist/express.d.cts +39 -0
  18. package/dist/express.d.ts +39 -0
  19. package/dist/express.js +35 -0
  20. package/dist/express.js.map +1 -0
  21. package/dist/fastify.cjs +49 -0
  22. package/dist/fastify.cjs.map +1 -0
  23. package/dist/fastify.d.cts +43 -0
  24. package/dist/fastify.d.ts +43 -0
  25. package/dist/fastify.js +47 -0
  26. package/dist/fastify.js.map +1 -0
  27. package/dist/index.cjs +1615 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +765 -0
  30. package/dist/index.d.ts +765 -0
  31. package/dist/index.js +1593 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/lambda.cjs +64 -0
  34. package/dist/lambda.cjs.map +1 -0
  35. package/dist/lambda.d.cts +53 -0
  36. package/dist/lambda.d.ts +53 -0
  37. package/dist/lambda.js +62 -0
  38. package/dist/lambda.js.map +1 -0
  39. package/dist/server.cjs +166 -0
  40. package/dist/server.cjs.map +1 -0
  41. package/dist/server.d.cts +85 -0
  42. package/dist/server.d.ts +85 -0
  43. package/dist/server.js +159 -0
  44. package/dist/server.js.map +1 -0
  45. package/dist/storage-CHi9vLD_.d.cts +82 -0
  46. package/dist/storage-iI_tyHcX.d.ts +82 -0
  47. package/dist/types--f4ITgu9.d.cts +73 -0
  48. package/dist/types-DKtPO4DP.d.ts +73 -0
  49. package/dist/types-yk0mDByJ.d.cts +96 -0
  50. package/dist/types-yk0mDByJ.d.ts +96 -0
  51. package/package.json +137 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FHIRfly.io LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # @fhirfly-io/shl
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.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ ## What It Does
8
+
9
+ Takes raw clinical data (NDC codes, ICD-10 codes, RxNorm, LOINC, CVX) and produces a shareable SMART Health Link:
10
+
11
+ ```
12
+ Raw codes + patient info → Enriched FHIR Bundle → Encrypted SHL → QR code
13
+ ```
14
+
15
+ PHI never leaves your server. Only terminology codes are sent to FHIRfly for enrichment — no BAA required.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @fhirfly-io/shl
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```typescript
26
+ import { IPS, SHL } from '@fhirfly-io/shl';
27
+ import { Fhirfly } from '@fhirfly-io/terminology';
28
+
29
+ const fhirfly = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY });
30
+
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' },
34
+ });
35
+
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' });
41
+
42
+ // Build with configurable output profile
43
+ const bundle = await ips.build({ profile: 'ips' }); // or 'r4' for generic FHIR
44
+
45
+ // Validate — mandatory before packaging
46
+ const validation = await bundle.validate();
47
+
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
+ });
55
+
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
59
+ ```
60
+
61
+ ## Design Principles
62
+
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
69
+
70
+ ## Output Profiles
71
+
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 |
76
+
77
+ ## Input Formats
78
+
79
+ Each `add*` method supports multiple input formats:
80
+
81
+ ```typescript
82
+ // From raw codes (enriched via FHIRfly API)
83
+ ips.addMedication({ byNDC: '0069-3150-83', fhirfly });
84
+ ips.addMedication({ byRxNorm: '161', fhirfly });
85
+
86
+ // From SNOMED (no API call needed)
87
+ ips.addMedication({ bySNOMED: '376988009' });
88
+
89
+ // From existing FHIR R4 resources
90
+ ips.addMedication({ fromResource: existingMedicationStatement });
91
+ ```
92
+
93
+ ## Storage Adapters
94
+
95
+ ```typescript
96
+ // AWS S3 (pre-signed URLs)
97
+ new SHL.S3Storage({ bucket: 'my-bucket', region: 'us-east-1' });
98
+
99
+ // Azure Blob Storage (SAS tokens)
100
+ new SHL.AzureStorage({ container: 'my-container', connectionString: '...' });
101
+
102
+ // Google Cloud Storage
103
+ new SHL.GCSStorage({ bucket: 'my-bucket' });
104
+
105
+ // Local filesystem (development/testing only)
106
+ new SHL.LocalStorage({ directory: './shl-data' });
107
+ ```
108
+
109
+ ## Related
110
+
111
+ - [@fhirfly-io/terminology](https://www.npmjs.com/package/@fhirfly-io/terminology) — FHIRfly terminology API SDK (required for code enrichment)
112
+ - [SMART Health Links Spec](https://docs.smarthealthit.org/smart-health-links/spec/)
113
+ - [IPS Implementation Guide](https://build.fhir.org/ig/HL7/fhir-ips/)
114
+ - [FHIRfly Documentation](https://fhirfly.io/docs)
115
+
116
+ ## License
117
+
118
+ MIT
@@ -0,0 +1,133 @@
1
+ // src/server/handler.ts
2
+ function createHandler(config) {
3
+ const { storage, onAccess } = config;
4
+ return async (req) => {
5
+ const path = req.path.replace(/^\/+/, "");
6
+ const segments = path.split("/").filter(Boolean);
7
+ if (segments.length === 1 && req.method === "POST") {
8
+ return handleManifest(segments[0], req, storage, onAccess);
9
+ }
10
+ if (segments.length === 2 && segments[1] === "content" && req.method === "GET") {
11
+ return handleContent(segments[0], storage);
12
+ }
13
+ if (segments.length === 3 && segments[1] === "attachment" && req.method === "GET") {
14
+ const index = segments[2];
15
+ return handleAttachment(segments[0], index, storage);
16
+ }
17
+ if (segments.length === 1 && req.method !== "POST") {
18
+ return jsonResponse(405, { error: "Method not allowed. Use POST for manifest requests." });
19
+ }
20
+ if (segments.length === 2 && segments[1] === "content" && req.method !== "GET") {
21
+ return jsonResponse(405, { error: "Method not allowed. Use GET for content requests." });
22
+ }
23
+ if (segments.length === 3 && segments[1] === "attachment" && req.method !== "GET") {
24
+ return jsonResponse(405, { error: "Method not allowed. Use GET for attachment requests." });
25
+ }
26
+ return jsonResponse(404, { error: "Not found" });
27
+ };
28
+ }
29
+ async function handleManifest(shlId, req, storage, onAccess) {
30
+ const manifestRaw = await storage.read(`${shlId}/manifest.json`);
31
+ if (manifestRaw === null) {
32
+ return jsonResponse(404, { error: "SHL not found" });
33
+ }
34
+ let updatedMetadata = null;
35
+ let accessDeniedReason = null;
36
+ const reqBody = req.body && typeof req.body === "object" ? req.body : {};
37
+ const providedPasscode = typeof reqBody["passcode"] === "string" ? reqBody["passcode"] : void 0;
38
+ updatedMetadata = await storage.updateMetadata(shlId, (metadata) => {
39
+ if (metadata.expiresAt) {
40
+ const expiresAt = new Date(metadata.expiresAt);
41
+ if (expiresAt.getTime() <= Date.now()) {
42
+ accessDeniedReason = "expired";
43
+ return null;
44
+ }
45
+ }
46
+ const currentCount = metadata.accessCount ?? 0;
47
+ if (metadata.maxAccesses !== void 0 && currentCount >= metadata.maxAccesses) {
48
+ accessDeniedReason = "exhausted";
49
+ return null;
50
+ }
51
+ if (metadata.passcode) {
52
+ if (!providedPasscode || providedPasscode !== metadata.passcode) {
53
+ accessDeniedReason = "passcode";
54
+ return null;
55
+ }
56
+ }
57
+ return {
58
+ ...metadata,
59
+ accessCount: currentCount + 1
60
+ };
61
+ });
62
+ if (accessDeniedReason === "expired") {
63
+ return jsonResponse(410, { error: "SHL has expired" });
64
+ }
65
+ if (accessDeniedReason === "exhausted") {
66
+ return jsonResponse(410, { error: "SHL access limit reached" });
67
+ }
68
+ if (accessDeniedReason === "passcode") {
69
+ return jsonResponse(401, { error: "Invalid passcode" });
70
+ }
71
+ if (updatedMetadata === null) {
72
+ return jsonResponse(404, { error: "SHL not found" });
73
+ }
74
+ if (onAccess) {
75
+ const event = {
76
+ shlId,
77
+ accessCount: updatedMetadata.accessCount ?? 1,
78
+ timestamp: /* @__PURE__ */ new Date()
79
+ };
80
+ Promise.resolve(onAccess(event)).catch(() => {
81
+ });
82
+ }
83
+ const manifestStr = typeof manifestRaw === "string" ? manifestRaw : new TextDecoder().decode(manifestRaw);
84
+ const manifest = JSON.parse(manifestStr);
85
+ return jsonResponse(200, manifest);
86
+ }
87
+ async function handleContent(shlId, storage) {
88
+ const content = await storage.read(`${shlId}/content.jwe`);
89
+ if (content === null) {
90
+ return jsonResponse(404, { error: "Content not found" });
91
+ }
92
+ const body = typeof content === "string" ? content : new TextDecoder().decode(content);
93
+ return {
94
+ status: 200,
95
+ headers: {
96
+ "content-type": "application/jose",
97
+ "cache-control": "no-store"
98
+ },
99
+ body
100
+ };
101
+ }
102
+ async function handleAttachment(shlId, index, storage) {
103
+ if (!/^\d+$/.test(index)) {
104
+ return jsonResponse(400, { error: "Invalid attachment index" });
105
+ }
106
+ const content = await storage.read(`${shlId}/attachment-${index}.jwe`);
107
+ if (content === null) {
108
+ return jsonResponse(404, { error: "Attachment not found" });
109
+ }
110
+ const body = typeof content === "string" ? content : new TextDecoder().decode(content);
111
+ return {
112
+ status: 200,
113
+ headers: {
114
+ "content-type": "application/jose",
115
+ "cache-control": "no-store"
116
+ },
117
+ body
118
+ };
119
+ }
120
+ function jsonResponse(status, body) {
121
+ return {
122
+ status,
123
+ headers: {
124
+ "content-type": "application/json",
125
+ "cache-control": "no-store"
126
+ },
127
+ body: JSON.stringify(body)
128
+ };
129
+ }
130
+
131
+ export { createHandler };
132
+ //# sourceMappingURL=chunk-7WIM2QP5.js.map
133
+ //# sourceMappingURL=chunk-7WIM2QP5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/handler.ts"],"names":[],"mappings":"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","file":"chunk-7WIM2QP5.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\";\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\n if (metadata.passcode) {\n if (!providedPasscode || providedPasscode !== metadata.passcode) {\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"]}
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ // src/server/handler.ts
4
+ function createHandler(config) {
5
+ const { storage, onAccess } = config;
6
+ return async (req) => {
7
+ const path = req.path.replace(/^\/+/, "");
8
+ const segments = path.split("/").filter(Boolean);
9
+ if (segments.length === 1 && req.method === "POST") {
10
+ return handleManifest(segments[0], req, storage, onAccess);
11
+ }
12
+ if (segments.length === 2 && segments[1] === "content" && req.method === "GET") {
13
+ return handleContent(segments[0], storage);
14
+ }
15
+ if (segments.length === 3 && segments[1] === "attachment" && req.method === "GET") {
16
+ const index = segments[2];
17
+ return handleAttachment(segments[0], index, storage);
18
+ }
19
+ if (segments.length === 1 && req.method !== "POST") {
20
+ return jsonResponse(405, { error: "Method not allowed. Use POST for manifest requests." });
21
+ }
22
+ if (segments.length === 2 && segments[1] === "content" && req.method !== "GET") {
23
+ return jsonResponse(405, { error: "Method not allowed. Use GET for content requests." });
24
+ }
25
+ if (segments.length === 3 && segments[1] === "attachment" && req.method !== "GET") {
26
+ return jsonResponse(405, { error: "Method not allowed. Use GET for attachment requests." });
27
+ }
28
+ return jsonResponse(404, { error: "Not found" });
29
+ };
30
+ }
31
+ async function handleManifest(shlId, req, storage, onAccess) {
32
+ const manifestRaw = await storage.read(`${shlId}/manifest.json`);
33
+ if (manifestRaw === null) {
34
+ return jsonResponse(404, { error: "SHL not found" });
35
+ }
36
+ let updatedMetadata = null;
37
+ let accessDeniedReason = null;
38
+ const reqBody = req.body && typeof req.body === "object" ? req.body : {};
39
+ const providedPasscode = typeof reqBody["passcode"] === "string" ? reqBody["passcode"] : void 0;
40
+ updatedMetadata = await storage.updateMetadata(shlId, (metadata) => {
41
+ if (metadata.expiresAt) {
42
+ const expiresAt = new Date(metadata.expiresAt);
43
+ if (expiresAt.getTime() <= Date.now()) {
44
+ accessDeniedReason = "expired";
45
+ return null;
46
+ }
47
+ }
48
+ const currentCount = metadata.accessCount ?? 0;
49
+ if (metadata.maxAccesses !== void 0 && currentCount >= metadata.maxAccesses) {
50
+ accessDeniedReason = "exhausted";
51
+ return null;
52
+ }
53
+ if (metadata.passcode) {
54
+ if (!providedPasscode || providedPasscode !== metadata.passcode) {
55
+ accessDeniedReason = "passcode";
56
+ return null;
57
+ }
58
+ }
59
+ return {
60
+ ...metadata,
61
+ accessCount: currentCount + 1
62
+ };
63
+ });
64
+ if (accessDeniedReason === "expired") {
65
+ return jsonResponse(410, { error: "SHL has expired" });
66
+ }
67
+ if (accessDeniedReason === "exhausted") {
68
+ return jsonResponse(410, { error: "SHL access limit reached" });
69
+ }
70
+ if (accessDeniedReason === "passcode") {
71
+ return jsonResponse(401, { error: "Invalid passcode" });
72
+ }
73
+ if (updatedMetadata === null) {
74
+ return jsonResponse(404, { error: "SHL not found" });
75
+ }
76
+ if (onAccess) {
77
+ const event = {
78
+ shlId,
79
+ accessCount: updatedMetadata.accessCount ?? 1,
80
+ timestamp: /* @__PURE__ */ new Date()
81
+ };
82
+ Promise.resolve(onAccess(event)).catch(() => {
83
+ });
84
+ }
85
+ const manifestStr = typeof manifestRaw === "string" ? manifestRaw : new TextDecoder().decode(manifestRaw);
86
+ const manifest = JSON.parse(manifestStr);
87
+ return jsonResponse(200, manifest);
88
+ }
89
+ async function handleContent(shlId, storage) {
90
+ const content = await storage.read(`${shlId}/content.jwe`);
91
+ if (content === null) {
92
+ return jsonResponse(404, { error: "Content not found" });
93
+ }
94
+ const body = typeof content === "string" ? content : new TextDecoder().decode(content);
95
+ return {
96
+ status: 200,
97
+ headers: {
98
+ "content-type": "application/jose",
99
+ "cache-control": "no-store"
100
+ },
101
+ body
102
+ };
103
+ }
104
+ async function handleAttachment(shlId, index, storage) {
105
+ if (!/^\d+$/.test(index)) {
106
+ return jsonResponse(400, { error: "Invalid attachment index" });
107
+ }
108
+ const content = await storage.read(`${shlId}/attachment-${index}.jwe`);
109
+ if (content === null) {
110
+ return jsonResponse(404, { error: "Attachment not found" });
111
+ }
112
+ const body = typeof content === "string" ? content : new TextDecoder().decode(content);
113
+ return {
114
+ status: 200,
115
+ headers: {
116
+ "content-type": "application/jose",
117
+ "cache-control": "no-store"
118
+ },
119
+ body
120
+ };
121
+ }
122
+ function jsonResponse(status, body) {
123
+ return {
124
+ status,
125
+ headers: {
126
+ "content-type": "application/json",
127
+ "cache-control": "no-store"
128
+ },
129
+ body: JSON.stringify(body)
130
+ };
131
+ }
132
+
133
+ exports.createHandler = createHandler;
134
+ //# sourceMappingURL=chunk-CNVYKA4D.cjs.map
135
+ //# sourceMappingURL=chunk-CNVYKA4D.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/handler.ts"],"names":[],"mappings":"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","file":"chunk-CNVYKA4D.cjs","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\";\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\n if (metadata.passcode) {\n if (!providedPasscode || providedPasscode !== metadata.passcode) {\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"]}
@@ -0,0 +1,170 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+
6
+ // src/errors.ts
7
+ var ShlError = class _ShlError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "ShlError";
11
+ Object.setPrototypeOf(this, _ShlError.prototype);
12
+ }
13
+ };
14
+ var ValidationError = class _ValidationError extends ShlError {
15
+ issues;
16
+ constructor(message, issues = []) {
17
+ super(message);
18
+ this.name = "ValidationError";
19
+ this.issues = issues;
20
+ Object.setPrototypeOf(this, _ValidationError.prototype);
21
+ }
22
+ };
23
+ var StorageError = class _StorageError extends ShlError {
24
+ operation;
25
+ constructor(message, operation) {
26
+ super(message);
27
+ this.name = "StorageError";
28
+ this.operation = operation;
29
+ Object.setPrototypeOf(this, _StorageError.prototype);
30
+ }
31
+ };
32
+ var EncryptionError = class _EncryptionError extends ShlError {
33
+ constructor(message) {
34
+ super(message);
35
+ this.name = "EncryptionError";
36
+ Object.setPrototypeOf(this, _EncryptionError.prototype);
37
+ }
38
+ };
39
+ var LocalStorage = class {
40
+ _config;
41
+ constructor(config) {
42
+ this._config = config;
43
+ }
44
+ /** Returns the storage configuration. */
45
+ get config() {
46
+ return this._config;
47
+ }
48
+ /** Base URL with trailing slashes stripped. */
49
+ get baseUrl() {
50
+ return this._config.baseUrl.replace(/\/+$/, "");
51
+ }
52
+ async store(key, content) {
53
+ const filePath = path.join(this._config.directory, key);
54
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
55
+ fs.writeFileSync(filePath, content);
56
+ }
57
+ async delete(prefix) {
58
+ const dirPath = path.join(this._config.directory, prefix);
59
+ fs.rmSync(dirPath, { recursive: true, force: true });
60
+ }
61
+ };
62
+ var _s3Module;
63
+ async function getS3Module() {
64
+ if (_s3Module) return _s3Module;
65
+ try {
66
+ _s3Module = await import('@aws-sdk/client-s3');
67
+ return _s3Module;
68
+ } catch {
69
+ throw new StorageError(
70
+ "@aws-sdk/client-s3 is required for S3Storage. Install it: npm install @aws-sdk/client-s3",
71
+ "import"
72
+ );
73
+ }
74
+ }
75
+ var S3Storage = class {
76
+ _config;
77
+ _client;
78
+ constructor(config) {
79
+ this._config = config;
80
+ }
81
+ /** Returns the storage configuration. */
82
+ get config() {
83
+ return this._config;
84
+ }
85
+ /** Base URL with trailing slashes stripped. */
86
+ get baseUrl() {
87
+ return this._config.baseUrl.replace(/\/+$/, "");
88
+ }
89
+ async store(key, content) {
90
+ try {
91
+ const s3 = await getS3Module();
92
+ const client = this._getClient(s3);
93
+ const body = typeof content === "string" ? Buffer.from(content, "utf8") : content;
94
+ const command = new s3.PutObjectCommand({
95
+ Bucket: this._config.bucket,
96
+ Key: this._s3Key(key),
97
+ Body: body,
98
+ ContentType: this._contentType(key)
99
+ });
100
+ await client.send(command);
101
+ } catch (err) {
102
+ if (err instanceof StorageError) throw err;
103
+ throw new StorageError(
104
+ `Failed to store ${key}: ${err instanceof Error ? err.message : String(err)}`,
105
+ "store"
106
+ );
107
+ }
108
+ }
109
+ async delete(prefix) {
110
+ try {
111
+ const s3 = await getS3Module();
112
+ const client = this._getClient(s3);
113
+ const s3Prefix = this._s3Key(prefix);
114
+ let continuationToken;
115
+ do {
116
+ const listInput = {
117
+ Bucket: this._config.bucket,
118
+ Prefix: s3Prefix
119
+ };
120
+ if (continuationToken) {
121
+ listInput["ContinuationToken"] = continuationToken;
122
+ }
123
+ const listCommand = new s3.ListObjectsV2Command(listInput);
124
+ const response = await client.send(listCommand);
125
+ const objects = response.Contents;
126
+ if (!objects || objects.length === 0) break;
127
+ const deleteInput = {
128
+ Bucket: this._config.bucket,
129
+ Delete: {
130
+ Objects: objects.map((obj) => ({ Key: obj.Key })),
131
+ Quiet: true
132
+ }
133
+ };
134
+ const deleteCommand = new s3.DeleteObjectsCommand(deleteInput);
135
+ await client.send(deleteCommand);
136
+ continuationToken = response.IsTruncated ? response.NextContinuationToken : void 0;
137
+ } while (continuationToken);
138
+ } catch (err) {
139
+ if (err instanceof StorageError) throw err;
140
+ throw new StorageError(
141
+ `Failed to delete ${prefix}: ${err instanceof Error ? err.message : String(err)}`,
142
+ "delete"
143
+ );
144
+ }
145
+ }
146
+ _getClient(s3) {
147
+ if (!this._client) {
148
+ this._client = new s3.S3Client({ region: this._config.region });
149
+ }
150
+ return this._client;
151
+ }
152
+ _s3Key(key) {
153
+ const prefix = this._config.prefix?.replace(/\/+$/, "");
154
+ return prefix ? `${prefix}/${key}` : key;
155
+ }
156
+ _contentType(key) {
157
+ if (key.endsWith(".jwe")) return "application/jose";
158
+ if (key.endsWith(".json")) return "application/json";
159
+ return "application/octet-stream";
160
+ }
161
+ };
162
+
163
+ exports.EncryptionError = EncryptionError;
164
+ exports.LocalStorage = LocalStorage;
165
+ exports.S3Storage = S3Storage;
166
+ exports.ShlError = ShlError;
167
+ exports.StorageError = StorageError;
168
+ exports.ValidationError = ValidationError;
169
+ //# sourceMappingURL=chunk-KI44MYPE.cjs.map
170
+ //# sourceMappingURL=chunk-KI44MYPE.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/shl/storage.ts"],"names":["join","mkdirSync","dirname","writeFileSync","rmSync"],"mappings":"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","file":"chunk-KI44MYPE.cjs","sourcesContent":["// Copyright 2026 FHIRfly.io LLC. All rights reserved.\n// Licensed under the MIT License. See LICENSE file in the project root.\n/**\n * Base error class for all SHL SDK errors.\n */\nexport class ShlError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ShlError\";\n Object.setPrototypeOf(this, ShlError.prototype);\n }\n}\n\n/**\n * Error thrown when IPS bundle validation fails.\n */\nexport class ValidationError extends ShlError {\n readonly issues: string[];\n\n constructor(message: string, issues: string[] = []) {\n super(message);\n this.name = \"ValidationError\";\n this.issues = issues;\n Object.setPrototypeOf(this, ValidationError.prototype);\n }\n}\n\n/**\n * Error thrown when a storage operation fails.\n */\nexport class StorageError extends ShlError {\n readonly operation: string;\n\n constructor(message: string, operation: string) {\n super(message);\n this.name = \"StorageError\";\n this.operation = operation;\n Object.setPrototypeOf(this, StorageError.prototype);\n }\n}\n\n/**\n * Error thrown when encryption or decryption fails.\n */\nexport class EncryptionError extends ShlError {\n constructor(message: string) {\n super(message);\n this.name = \"EncryptionError\";\n Object.setPrototypeOf(this, EncryptionError.prototype);\n }\n}\n","// Copyright 2026 FHIRfly.io LLC. All rights reserved.\n// Licensed under the MIT License. See LICENSE file in the project root.\nimport { mkdirSync, writeFileSync, rmSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { SHLStorage } from \"./types.js\";\nimport { StorageError } from \"../errors.js\";\n\n/**\n * Configuration for local filesystem SHL storage.\n */\nexport interface LocalStorageConfig {\n /** Directory path for storing SHL files */\n directory: string;\n /** Base URL for serving the files (trailing slashes are stripped) */\n baseUrl: string;\n}\n\n/**\n * Local filesystem storage for SMART Health Links.\n * Useful for development and testing.\n *\n * Files are written to `{directory}/{key}`. The user's server\n * maps `{baseUrl}/{shlId}` to reads from this directory.\n *\n * @example\n * ```ts\n * const storage = new SHL.LocalStorage({\n * directory: \"./shl-data\",\n * baseUrl: \"http://localhost:3000/shl\",\n * });\n * ```\n */\nexport class LocalStorage implements SHLStorage {\n private readonly _config: LocalStorageConfig;\n\n constructor(config: LocalStorageConfig) {\n this._config = config;\n }\n\n /** Returns the storage configuration. */\n get config(): LocalStorageConfig {\n return this._config;\n }\n\n /** Base URL with trailing slashes stripped. */\n get baseUrl(): string {\n return this._config.baseUrl.replace(/\\/+$/, \"\");\n }\n\n async store(key: string, content: string | Uint8Array): Promise<void> {\n const filePath = join(this._config.directory, key);\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, content);\n }\n\n async delete(prefix: string): Promise<void> {\n const dirPath = join(this._config.directory, prefix);\n rmSync(dirPath, { recursive: true, force: true });\n }\n}\n\n/**\n * Configuration for S3-based SHL storage.\n */\nexport interface S3StorageConfig {\n /** S3 bucket name */\n bucket: string;\n /** AWS region */\n region: string;\n /** Optional key prefix */\n prefix?: string;\n /** Base URL for serving the files */\n baseUrl: string;\n}\n\n// Minimal interfaces for @aws-sdk/client-s3 (peer dependency)\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 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 S3Storage. Install it: npm install @aws-sdk/client-s3\",\n \"import\",\n );\n }\n}\n\n/**\n * S3-backed storage for SMART Health Links.\n *\n * Requires `@aws-sdk/client-s3` as a peer dependency — install it separately:\n * ```\n * npm install @aws-sdk/client-s3\n * ```\n *\n * @example\n * ```ts\n * const storage = new SHL.S3Storage({\n * bucket: \"my-shl-bucket\",\n * region: \"us-east-1\",\n * baseUrl: \"https://shl.example.com\",\n * });\n * ```\n */\nexport class S3Storage implements SHLStorage {\n private readonly _config: S3StorageConfig;\n private _client?: S3ClientInstance;\n\n constructor(config: S3StorageConfig) {\n this._config = config;\n }\n\n /** Returns the storage configuration. */\n get config(): S3StorageConfig {\n return this._config;\n }\n\n /** Base URL with trailing slashes stripped. */\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 =\n 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\n do {\n const listInput: Record<string, unknown> = {\n Bucket: this._config.bucket,\n Prefix: s3Prefix,\n };\n if (continuationToken) {\n listInput[\"ContinuationToken\"] = continuationToken;\n }\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 deleteInput = {\n Bucket: this._config.bucket,\n Delete: {\n Objects: objects.map((obj) => ({ Key: obj.Key })),\n Quiet: true,\n },\n };\n\n const deleteCommand = new s3.DeleteObjectsCommand(deleteInput);\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 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"]}