@fhirfly-io/shl 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +42 -6
  2. package/dist/{chunk-UFFYRACT.cjs → chunk-63Q54EKN.cjs} +40 -21
  3. package/dist/chunk-63Q54EKN.cjs.map +1 -0
  4. package/dist/{chunk-5I5H3SLO.cjs → chunk-LXJB46WB.cjs} +2 -2
  5. package/dist/chunk-LXJB46WB.cjs.map +1 -0
  6. package/dist/{chunk-CNVYKA4D.cjs → chunk-QXSWM5QV.cjs} +46 -21
  7. package/dist/chunk-QXSWM5QV.cjs.map +1 -0
  8. package/dist/{chunk-AIOYPNKN.js → chunk-YBDRWUQU.js} +32 -13
  9. package/dist/chunk-YBDRWUQU.js.map +1 -0
  10. package/dist/{chunk-NEBFJSJW.js → chunk-YV7ZD6OM.js} +2 -2
  11. package/dist/chunk-YV7ZD6OM.js.map +1 -0
  12. package/dist/{chunk-7WIM2QP5.js → chunk-ZEE5RXIS.js} +46 -21
  13. package/dist/chunk-ZEE5RXIS.js.map +1 -0
  14. package/dist/cli.cjs +14 -14
  15. package/dist/cli.js +3 -3
  16. package/dist/express.cjs +2 -2
  17. package/dist/express.d.cts +2 -2
  18. package/dist/express.d.ts +2 -2
  19. package/dist/express.js +1 -1
  20. package/dist/fastify.cjs +16 -2
  21. package/dist/fastify.cjs.map +1 -1
  22. package/dist/fastify.d.cts +3 -3
  23. package/dist/fastify.d.ts +3 -3
  24. package/dist/fastify.js +15 -1
  25. package/dist/fastify.js.map +1 -1
  26. package/dist/index.cjs +3 -3
  27. package/dist/index.d.cts +28 -3
  28. package/dist/index.d.ts +28 -3
  29. package/dist/index.js +1 -1
  30. package/dist/lambda.cjs +2 -2
  31. package/dist/lambda.d.cts +2 -2
  32. package/dist/lambda.d.ts +2 -2
  33. package/dist/lambda.js +1 -1
  34. package/dist/server.cjs +7 -7
  35. package/dist/server.d.cts +13 -6
  36. package/dist/server.d.ts +13 -6
  37. package/dist/server.js +2 -2
  38. package/dist/{storage-DYEX5kiP.d.ts → storage-B3GyJD2y.d.ts} +1 -1
  39. package/dist/{storage-D1NajOTq.d.cts → storage-BwszYwFo.d.cts} +1 -1
  40. package/dist/{types-BbvJirBn.d.cts → types-BegxU0wQ.d.ts} +22 -2
  41. package/dist/{types-DpkUjBYr.d.ts → types-Doq5cGNm.d.cts} +8 -1
  42. package/dist/{types-DpkUjBYr.d.cts → types-Doq5cGNm.d.ts} +8 -1
  43. package/dist/{types-qPv1O_sF.d.ts → types-hHf-a3hH.d.cts} +22 -2
  44. package/package.json +1 -1
  45. package/dist/chunk-5I5H3SLO.cjs.map +0 -1
  46. package/dist/chunk-7WIM2QP5.js.map +0 -1
  47. package/dist/chunk-AIOYPNKN.js.map +0 -1
  48. package/dist/chunk-CNVYKA4D.cjs.map +0 -1
  49. package/dist/chunk-NEBFJSJW.js.map +0 -1
  50. package/dist/chunk-UFFYRACT.cjs.map +0 -1
package/dist/server.cjs CHANGED
@@ -1,31 +1,31 @@
1
1
  'use strict';
2
2
 
3
- var chunk5I5H3SLO_cjs = require('./chunk-5I5H3SLO.cjs');
3
+ var chunkLXJB46WB_cjs = require('./chunk-LXJB46WB.cjs');
4
4
  require('./chunk-UDS6UJAL.cjs');
5
- var chunkCNVYKA4D_cjs = require('./chunk-CNVYKA4D.cjs');
5
+ var chunkQXSWM5QV_cjs = require('./chunk-QXSWM5QV.cjs');
6
6
  require('./chunk-Q7SFCCGT.cjs');
7
7
 
8
8
 
9
9
 
10
10
  Object.defineProperty(exports, "ServerAzureStorage", {
11
11
  enumerable: true,
12
- get: function () { return chunk5I5H3SLO_cjs.ServerAzureStorage; }
12
+ get: function () { return chunkLXJB46WB_cjs.ServerAzureStorage; }
13
13
  });
14
14
  Object.defineProperty(exports, "ServerGCSStorage", {
15
15
  enumerable: true,
16
- get: function () { return chunk5I5H3SLO_cjs.ServerGCSStorage; }
16
+ get: function () { return chunkLXJB46WB_cjs.ServerGCSStorage; }
17
17
  });
18
18
  Object.defineProperty(exports, "ServerLocalStorage", {
19
19
  enumerable: true,
20
- get: function () { return chunk5I5H3SLO_cjs.ServerLocalStorage; }
20
+ get: function () { return chunkLXJB46WB_cjs.ServerLocalStorage; }
21
21
  });
22
22
  Object.defineProperty(exports, "ServerS3Storage", {
23
23
  enumerable: true,
24
- get: function () { return chunk5I5H3SLO_cjs.ServerS3Storage; }
24
+ get: function () { return chunkLXJB46WB_cjs.ServerS3Storage; }
25
25
  });
26
26
  Object.defineProperty(exports, "createHandler", {
27
27
  enumerable: true,
28
- get: function () { return chunkCNVYKA4D_cjs.createHandler; }
28
+ get: function () { return chunkQXSWM5QV_cjs.createHandler; }
29
29
  });
30
30
  //# sourceMappingURL=server.cjs.map
31
31
  //# sourceMappingURL=server.cjs.map
package/dist/server.d.cts CHANGED
@@ -1,16 +1,20 @@
1
- import { S as SHLHandlerConfig, H as HandlerRequest, a as HandlerResponse, b as SHLServerStorage } from './types-BbvJirBn.cjs';
2
- export { A as AccessEvent } from './types-BbvJirBn.cjs';
3
- import { A as AzureStorage, a as AzureStorageConfig, G as GCSStorage, b as GCSStorageConfig, L as LocalStorage, c as LocalStorageConfig, S as S3StorageConfig } from './storage-D1NajOTq.cjs';
4
- import { a as SHLMetadata } from './types-DpkUjBYr.cjs';
1
+ import { S as SHLHandlerConfig, H as HandlerRequest, a as HandlerResponse, b as SHLServerStorage } from './types-hHf-a3hH.cjs';
2
+ export { A as AccessEvent, C as CorsConfig } from './types-hHf-a3hH.cjs';
3
+ import { A as AzureStorage, a as AzureStorageConfig, G as GCSStorage, b as GCSStorageConfig, L as LocalStorage, c as LocalStorageConfig, S as S3StorageConfig } from './storage-BwszYwFo.cjs';
4
+ import { a as SHLMetadata } from './types-Doq5cGNm.cjs';
5
5
 
6
6
  /**
7
7
  * Create a framework-agnostic SHL request handler.
8
8
  *
9
9
  * Returns an async function that processes incoming requests and returns
10
- * responses. This handler implements two routes:
10
+ * responses. This handler implements three routes:
11
11
  *
12
12
  * - `POST /{shlId}` — Manifest endpoint (validates passcode, checks access limits)
13
13
  * - `GET /{shlId}/content` — Content endpoint (serves encrypted JWE)
14
+ * - `GET /{shlId}/attachment/{index}` — Attachment endpoint (serves encrypted attachment)
15
+ *
16
+ * By default, CORS headers are added to all responses so browser-based SHL
17
+ * viewers can access self-hosted servers. Set `cors: false` to disable.
14
18
  *
15
19
  * Framework adapters (Express, Fastify, Lambda) translate their native
16
20
  * request/response types to/from `HandlerRequest`/`HandlerResponse`.
@@ -55,7 +59,10 @@ declare class ServerLocalStorage extends LocalStorage implements SHLServerStorag
55
59
  * Implements the full `SHLServerStorage` interface with `read` and
56
60
  * `updateMetadata` on top of S3.
57
61
  *
58
- * Uses conditional PutObject for optimistic concurrency on metadata updates.
62
+ * Note: `updateMetadata` uses a plain read-modify-write pattern without
63
+ * conditional writes. Under concurrent load, a few extra accesses may be
64
+ * allowed beyond `maxAccesses`. For strict enforcement, use FhirflyStorage
65
+ * or add your own atomic counter.
59
66
  *
60
67
  * @example
61
68
  * ```ts
package/dist/server.d.ts CHANGED
@@ -1,16 +1,20 @@
1
- import { S as SHLHandlerConfig, H as HandlerRequest, a as HandlerResponse, b as SHLServerStorage } from './types-qPv1O_sF.js';
2
- export { A as AccessEvent } from './types-qPv1O_sF.js';
3
- import { A as AzureStorage, a as AzureStorageConfig, G as GCSStorage, b as GCSStorageConfig, L as LocalStorage, c as LocalStorageConfig, S as S3StorageConfig } from './storage-DYEX5kiP.js';
4
- import { a as SHLMetadata } from './types-DpkUjBYr.js';
1
+ import { S as SHLHandlerConfig, H as HandlerRequest, a as HandlerResponse, b as SHLServerStorage } from './types-BegxU0wQ.js';
2
+ export { A as AccessEvent, C as CorsConfig } from './types-BegxU0wQ.js';
3
+ import { A as AzureStorage, a as AzureStorageConfig, G as GCSStorage, b as GCSStorageConfig, L as LocalStorage, c as LocalStorageConfig, S as S3StorageConfig } from './storage-B3GyJD2y.js';
4
+ import { a as SHLMetadata } from './types-Doq5cGNm.js';
5
5
 
6
6
  /**
7
7
  * Create a framework-agnostic SHL request handler.
8
8
  *
9
9
  * Returns an async function that processes incoming requests and returns
10
- * responses. This handler implements two routes:
10
+ * responses. This handler implements three routes:
11
11
  *
12
12
  * - `POST /{shlId}` — Manifest endpoint (validates passcode, checks access limits)
13
13
  * - `GET /{shlId}/content` — Content endpoint (serves encrypted JWE)
14
+ * - `GET /{shlId}/attachment/{index}` — Attachment endpoint (serves encrypted attachment)
15
+ *
16
+ * By default, CORS headers are added to all responses so browser-based SHL
17
+ * viewers can access self-hosted servers. Set `cors: false` to disable.
14
18
  *
15
19
  * Framework adapters (Express, Fastify, Lambda) translate their native
16
20
  * request/response types to/from `HandlerRequest`/`HandlerResponse`.
@@ -55,7 +59,10 @@ declare class ServerLocalStorage extends LocalStorage implements SHLServerStorag
55
59
  * Implements the full `SHLServerStorage` interface with `read` and
56
60
  * `updateMetadata` on top of S3.
57
61
  *
58
- * Uses conditional PutObject for optimistic concurrency on metadata updates.
62
+ * Note: `updateMetadata` uses a plain read-modify-write pattern without
63
+ * conditional writes. Under concurrent load, a few extra accesses may be
64
+ * allowed beyond `maxAccesses`. For strict enforcement, use FhirflyStorage
65
+ * or add your own atomic counter.
59
66
  *
60
67
  * @example
61
68
  * ```ts
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
- export { ServerAzureStorage, ServerGCSStorage, ServerLocalStorage, ServerS3Storage } from './chunk-NEBFJSJW.js';
1
+ export { ServerAzureStorage, ServerGCSStorage, ServerLocalStorage, ServerS3Storage } from './chunk-YV7ZD6OM.js';
2
2
  import './chunk-VKB3ESIV.js';
3
- export { createHandler } from './chunk-7WIM2QP5.js';
3
+ export { createHandler } from './chunk-ZEE5RXIS.js';
4
4
  import './chunk-PZ5AY32C.js';
5
5
  //# sourceMappingURL=server.js.map
6
6
  //# sourceMappingURL=server.js.map
@@ -1,4 +1,4 @@
1
- import { S as SHLStorage } from './types-DpkUjBYr.js';
1
+ import { S as SHLStorage } from './types-Doq5cGNm.js';
2
2
 
3
3
  /**
4
4
  * Configuration for local filesystem SHL storage.
@@ -1,4 +1,4 @@
1
- import { S as SHLStorage } from './types-DpkUjBYr.cjs';
1
+ import { S as SHLStorage } from './types-Doq5cGNm.cjs';
2
2
 
3
3
  /**
4
4
  * Configuration for local filesystem SHL storage.
@@ -1,4 +1,4 @@
1
- import { S as SHLStorage, a as SHLMetadata } from './types-DpkUjBYr.cjs';
1
+ import { S as SHLStorage, a as SHLMetadata } from './types-Doq5cGNm.js';
2
2
 
3
3
  /**
4
4
  * Framework-agnostic incoming request.
@@ -46,6 +46,21 @@ interface SHLServerStorage extends SHLStorage {
46
46
  */
47
47
  updateMetadata(shlId: string, updater: (current: SHLMetadata) => SHLMetadata | null): Promise<SHLMetadata | null>;
48
48
  }
49
+ /**
50
+ * CORS configuration for the SHL server handler.
51
+ *
52
+ * By default, the handler adds permissive CORS headers to all responses
53
+ * so that browser-based SHL viewers can access self-hosted servers.
54
+ * Set `cors: false` to disable, or provide an object to customize.
55
+ */
56
+ interface CorsConfig {
57
+ /** Allowed origin(s). Default: `"*"` */
58
+ origin?: string;
59
+ /** Allowed methods. Default: `"GET, POST, OPTIONS"` */
60
+ methods?: string;
61
+ /** Allowed headers. Default: `"Content-Type, Authorization"` */
62
+ headers?: string;
63
+ }
49
64
  /**
50
65
  * Configuration for the SHL server handler.
51
66
  */
@@ -57,6 +72,11 @@ interface SHLHandlerConfig {
57
72
  * Useful for logging, analytics, or custom access control.
58
73
  */
59
74
  onAccess?: (event: AccessEvent) => void | Promise<void>;
75
+ /**
76
+ * CORS configuration. Defaults to permissive headers (`Access-Control-Allow-Origin: *`).
77
+ * Set to `false` to disable CORS headers entirely.
78
+ */
79
+ cors?: CorsConfig | false;
60
80
  }
61
81
  /**
62
82
  * Event emitted on each successful manifest access.
@@ -70,4 +90,4 @@ interface AccessEvent {
70
90
  timestamp: Date;
71
91
  }
72
92
 
73
- export type { AccessEvent as A, HandlerRequest as H, SHLHandlerConfig as S, HandlerResponse as a, SHLServerStorage as b };
93
+ export type { AccessEvent as A, CorsConfig as C, HandlerRequest as H, SHLHandlerConfig as S, HandlerResponse as a, SHLServerStorage as b };
@@ -47,6 +47,9 @@ interface SHLResult {
47
47
  }
48
48
  /**
49
49
  * SHL manifest file entry.
50
+ *
51
+ * Entries may use `location` (URL to fetch content) or `embedded` (inline JWE).
52
+ * Use `SHL.getEntryContent()` to handle both patterns transparently.
50
53
  */
51
54
  interface ManifestEntry {
52
55
  /** MIME type of the content (e.g., "application/fhir+json", "application/pdf") */
@@ -62,6 +65,10 @@ interface ManifestEntry {
62
65
  interface Manifest {
63
66
  /** Array of files available via this SHL */
64
67
  files: ManifestEntry[];
68
+ /** Manifest status per SHL spec: "finalized", "can-change", or "no-longer-valid" */
69
+ status?: "finalized" | "can-change" | "no-longer-valid";
70
+ /** ISO 8601 timestamp of when the manifest was last updated */
71
+ lastUpdated?: string;
65
72
  }
66
73
  /**
67
74
  * Metadata stored alongside an SHL for access control.
@@ -93,4 +100,4 @@ interface SHLStorage {
93
100
  delete(prefix: string): Promise<void>;
94
101
  }
95
102
 
96
- export type { Manifest as M, SHLStorage as S, SHLMetadata as a, SHLOptions as b, SHLResult as c, ManifestEntry as d, SHLAttachment as e };
103
+ export type { ManifestEntry as M, SHLStorage as S, SHLMetadata as a, SHLOptions as b, SHLResult as c, Manifest as d, SHLAttachment as e };
@@ -47,6 +47,9 @@ interface SHLResult {
47
47
  }
48
48
  /**
49
49
  * SHL manifest file entry.
50
+ *
51
+ * Entries may use `location` (URL to fetch content) or `embedded` (inline JWE).
52
+ * Use `SHL.getEntryContent()` to handle both patterns transparently.
50
53
  */
51
54
  interface ManifestEntry {
52
55
  /** MIME type of the content (e.g., "application/fhir+json", "application/pdf") */
@@ -62,6 +65,10 @@ interface ManifestEntry {
62
65
  interface Manifest {
63
66
  /** Array of files available via this SHL */
64
67
  files: ManifestEntry[];
68
+ /** Manifest status per SHL spec: "finalized", "can-change", or "no-longer-valid" */
69
+ status?: "finalized" | "can-change" | "no-longer-valid";
70
+ /** ISO 8601 timestamp of when the manifest was last updated */
71
+ lastUpdated?: string;
65
72
  }
66
73
  /**
67
74
  * Metadata stored alongside an SHL for access control.
@@ -93,4 +100,4 @@ interface SHLStorage {
93
100
  delete(prefix: string): Promise<void>;
94
101
  }
95
102
 
96
- export type { Manifest as M, SHLStorage as S, SHLMetadata as a, SHLOptions as b, SHLResult as c, ManifestEntry as d, SHLAttachment as e };
103
+ export type { ManifestEntry as M, SHLStorage as S, SHLMetadata as a, SHLOptions as b, SHLResult as c, Manifest as d, SHLAttachment as e };
@@ -1,4 +1,4 @@
1
- import { S as SHLStorage, a as SHLMetadata } from './types-DpkUjBYr.js';
1
+ import { S as SHLStorage, a as SHLMetadata } from './types-Doq5cGNm.cjs';
2
2
 
3
3
  /**
4
4
  * Framework-agnostic incoming request.
@@ -46,6 +46,21 @@ interface SHLServerStorage extends SHLStorage {
46
46
  */
47
47
  updateMetadata(shlId: string, updater: (current: SHLMetadata) => SHLMetadata | null): Promise<SHLMetadata | null>;
48
48
  }
49
+ /**
50
+ * CORS configuration for the SHL server handler.
51
+ *
52
+ * By default, the handler adds permissive CORS headers to all responses
53
+ * so that browser-based SHL viewers can access self-hosted servers.
54
+ * Set `cors: false` to disable, or provide an object to customize.
55
+ */
56
+ interface CorsConfig {
57
+ /** Allowed origin(s). Default: `"*"` */
58
+ origin?: string;
59
+ /** Allowed methods. Default: `"GET, POST, OPTIONS"` */
60
+ methods?: string;
61
+ /** Allowed headers. Default: `"Content-Type, Authorization"` */
62
+ headers?: string;
63
+ }
49
64
  /**
50
65
  * Configuration for the SHL server handler.
51
66
  */
@@ -57,6 +72,11 @@ interface SHLHandlerConfig {
57
72
  * Useful for logging, analytics, or custom access control.
58
73
  */
59
74
  onAccess?: (event: AccessEvent) => void | Promise<void>;
75
+ /**
76
+ * CORS configuration. Defaults to permissive headers (`Access-Control-Allow-Origin: *`).
77
+ * Set to `false` to disable CORS headers entirely.
78
+ */
79
+ cors?: CorsConfig | false;
60
80
  }
61
81
  /**
62
82
  * Event emitted on each successful manifest access.
@@ -70,4 +90,4 @@ interface AccessEvent {
70
90
  timestamp: Date;
71
91
  }
72
92
 
73
- export type { AccessEvent as A, HandlerRequest as H, SHLHandlerConfig as S, HandlerResponse as a, SHLServerStorage as b };
93
+ export type { AccessEvent as A, CorsConfig as C, HandlerRequest as H, SHLHandlerConfig as S, HandlerResponse as a, SHLServerStorage as b };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fhirfly-io/shl",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Official FHIRfly SDK for SMART Health Links - IPS bundle creation and SHL sharing",
5
5
  "author": "FHIRfly.io LLC <admin@fhirfly.io>",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/server/storage.ts"],"names":["LocalStorage","join","existsSync","readFileSync","StorageError","AzureStorage","GCSStorage"],"mappings":"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","file":"chunk-5I5H3SLO.cjs","sourcesContent":["// Copyright 2026 FHIRfly.io LLC. All rights reserved.\n// Licensed under the MIT License. See LICENSE file in the project root.\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { LocalStorage, AzureStorage, GCSStorage } from \"../shl/storage.js\";\nimport type { LocalStorageConfig, S3StorageConfig, AzureStorageConfig, GCSStorageConfig } from \"../shl/storage.js\";\nimport type { SHLServerStorage } from \"./types.js\";\nimport type { SHLMetadata } from \"../shl/types.js\";\nimport { StorageError } from \"../errors.js\";\n\n/**\n * Local filesystem server storage for SMART Health Links.\n *\n * Extends the base `LocalStorage` (write-only) with `read` and\n * `updateMetadata` methods needed for serving SHLs.\n *\n * @example\n * ```ts\n * import { ServerLocalStorage } from \"@fhirfly-io/shl/server\";\n *\n * const storage = new ServerLocalStorage({\n * directory: \"./shl-data\",\n * baseUrl: \"https://shl.example.com\",\n * });\n * ```\n */\nexport class ServerLocalStorage extends LocalStorage implements SHLServerStorage {\n constructor(config: LocalStorageConfig) {\n super(config);\n }\n\n async read(key: string): Promise<string | Uint8Array | null> {\n const filePath = join(this.config.directory, key);\n if (!existsSync(filePath)) {\n return null;\n }\n try {\n // Read as UTF-8 for JSON/JWE files, binary for others\n return readFileSync(filePath, \"utf8\");\n } catch {\n return null;\n }\n }\n\n async updateMetadata(\n shlId: string,\n updater: (current: SHLMetadata) => SHLMetadata | null,\n ): Promise<SHLMetadata | null> {\n const key = `${shlId}/metadata.json`;\n const raw = await this.read(key);\n if (raw === null) return null;\n\n const current = JSON.parse(raw as string) as SHLMetadata;\n const updated = updater(current);\n if (updated === null) return null;\n\n await this.store(key, JSON.stringify(updated));\n return updated;\n }\n}\n\n// Minimal S3 interfaces (same pattern as shl/storage.ts)\ninterface S3ClientInstance {\n send(command: unknown): Promise<unknown>;\n}\ninterface S3Module {\n S3Client: new (config: { region: string }) => S3ClientInstance;\n PutObjectCommand: new (input: Record<string, unknown>) => unknown;\n GetObjectCommand: new (input: Record<string, unknown>) => unknown;\n ListObjectsV2Command: new (input: Record<string, unknown>) => unknown;\n DeleteObjectsCommand: new (input: Record<string, unknown>) => unknown;\n}\n\nlet _s3Module: S3Module | undefined;\nasync function getS3Module(): Promise<S3Module> {\n if (_s3Module) return _s3Module;\n try {\n _s3Module = (await import(\"@aws-sdk/client-s3\")) as unknown as S3Module;\n return _s3Module;\n } catch {\n throw new StorageError(\n \"@aws-sdk/client-s3 is required for ServerS3Storage. Install it: npm install @aws-sdk/client-s3\",\n \"import\",\n );\n }\n}\n\n/**\n * S3-backed server storage for SMART Health Links.\n *\n * Implements the full `SHLServerStorage` interface with `read` and\n * `updateMetadata` on top of S3.\n *\n * Uses conditional PutObject for optimistic concurrency on metadata updates.\n *\n * @example\n * ```ts\n * import { ServerS3Storage } from \"@fhirfly-io/shl/server\";\n *\n * const storage = new ServerS3Storage({\n * bucket: \"my-shl-bucket\",\n * region: \"us-east-1\",\n * baseUrl: \"https://shl.example.com\",\n * });\n * ```\n */\nexport class ServerS3Storage implements SHLServerStorage {\n private readonly _config: S3StorageConfig;\n private _client?: S3ClientInstance;\n\n constructor(config: S3StorageConfig) {\n this._config = config;\n }\n\n get baseUrl(): string {\n return this._config.baseUrl.replace(/\\/+$/, \"\");\n }\n\n async store(key: string, content: string | Uint8Array): Promise<void> {\n try {\n const s3 = await getS3Module();\n const client = this._getClient(s3);\n const body = typeof content === \"string\" ? Buffer.from(content, \"utf8\") : content;\n\n const command = new s3.PutObjectCommand({\n Bucket: this._config.bucket,\n Key: this._s3Key(key),\n Body: body,\n ContentType: this._contentType(key),\n });\n\n await client.send(command);\n } catch (err) {\n if (err instanceof StorageError) throw err;\n throw new StorageError(\n `Failed to store ${key}: ${err instanceof Error ? err.message : String(err)}`,\n \"store\",\n );\n }\n }\n\n async delete(prefix: string): Promise<void> {\n try {\n const s3 = await getS3Module();\n const client = this._getClient(s3);\n const s3Prefix = this._s3Key(prefix);\n\n let continuationToken: string | undefined;\n do {\n const listInput: Record<string, unknown> = {\n Bucket: this._config.bucket,\n Prefix: s3Prefix,\n };\n if (continuationToken) listInput[\"ContinuationToken\"] = continuationToken;\n\n const listCommand = new s3.ListObjectsV2Command(listInput);\n const response = (await client.send(listCommand)) as {\n Contents?: Array<{ Key?: string }>;\n IsTruncated?: boolean;\n NextContinuationToken?: string;\n };\n\n const objects = response.Contents;\n if (!objects || objects.length === 0) break;\n\n const deleteCommand = new s3.DeleteObjectsCommand({\n Bucket: this._config.bucket,\n Delete: {\n Objects: objects.map((obj) => ({ Key: obj.Key })),\n Quiet: true,\n },\n });\n await client.send(deleteCommand);\n\n continuationToken = response.IsTruncated\n ? response.NextContinuationToken\n : undefined;\n } while (continuationToken);\n } catch (err) {\n if (err instanceof StorageError) throw err;\n throw new StorageError(\n `Failed to delete ${prefix}: ${err instanceof Error ? err.message : String(err)}`,\n \"delete\",\n );\n }\n }\n\n async read(key: string): Promise<string | Uint8Array | null> {\n try {\n const s3 = await getS3Module();\n const client = this._getClient(s3);\n\n const command = new s3.GetObjectCommand({\n Bucket: this._config.bucket,\n Key: this._s3Key(key),\n });\n\n const response = (await client.send(command)) as {\n Body?: { transformToString(): Promise<string> };\n };\n\n if (!response.Body) return null;\n return response.Body.transformToString();\n } catch (err) {\n // S3 returns NoSuchKey for missing objects\n const code = (err as { name?: string }).name;\n if (code === \"NoSuchKey\") return null;\n if (err instanceof StorageError) throw err;\n throw new StorageError(\n `Failed to read ${key}: ${err instanceof Error ? err.message : String(err)}`,\n \"read\",\n );\n }\n }\n\n async updateMetadata(\n shlId: string,\n updater: (current: SHLMetadata) => SHLMetadata | null,\n ): Promise<SHLMetadata | null> {\n const key = `${shlId}/metadata.json`;\n const raw = await this.read(key);\n if (raw === null) return null;\n\n const current = JSON.parse(raw as string) as SHLMetadata;\n const updated = updater(current);\n if (updated === null) return null;\n\n await this.store(key, JSON.stringify(updated));\n return updated;\n }\n\n private _getClient(s3: S3Module): S3ClientInstance {\n if (!this._client) {\n this._client = new s3.S3Client({ region: this._config.region });\n }\n return this._client;\n }\n\n private _s3Key(key: string): string {\n const prefix = this._config.prefix?.replace(/\\/+$/, \"\");\n return prefix ? `${prefix}/${key}` : key;\n }\n\n private _contentType(key: string): string {\n if (key.endsWith(\".jwe\")) return \"application/jose\";\n if (key.endsWith(\".json\")) return \"application/json\";\n return \"application/octet-stream\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Azure Blob Storage (Server)\n// ---------------------------------------------------------------------------\n\n// Minimal Azure interfaces for server-side operations\ninterface AzureBlobModule {\n BlobServiceClient: {\n fromConnectionString(connectionString: string): AzureBlobServiceClient;\n };\n}\ninterface AzureBlobServiceClient {\n getContainerClient(container: string): AzureContainerClient;\n}\ninterface AzureContainerClient {\n getBlockBlobClient(blobName: string): AzureBlockBlobClient;\n listBlobsFlat(options?: { prefix?: string }): AsyncIterable<{ name: string }>;\n}\ninterface AzureBlockBlobClient {\n upload(content: Uint8Array | Buffer, contentLength: number, options?: Record<string, unknown>): Promise<unknown>;\n deleteIfExists(): Promise<unknown>;\n download(): Promise<{ readableStreamBody?: NodeJS.ReadableStream }>;\n}\n\nlet _azureModule: AzureBlobModule | undefined;\nasync function getAzureModule(): Promise<AzureBlobModule> {\n if (_azureModule) return _azureModule;\n try {\n _azureModule = (await import(\"@azure/storage-blob\")) as unknown as AzureBlobModule;\n return _azureModule;\n } catch {\n throw new StorageError(\n \"@azure/storage-blob is required for ServerAzureStorage. Install it: npm install @azure/storage-blob\",\n \"import\",\n );\n }\n}\n\n/**\n * Azure Blob Storage server storage for SMART Health Links.\n *\n * Extends the base `AzureStorage` with `read` and `updateMetadata`\n * methods needed for serving SHLs.\n *\n * @example\n * ```ts\n * import { ServerAzureStorage } from \"@fhirfly-io/shl/server\";\n *\n * const storage = new ServerAzureStorage({\n * container: \"shl-data\",\n * connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING!,\n * baseUrl: \"https://shl.example.com\",\n * });\n * ```\n */\nexport class ServerAzureStorage extends AzureStorage implements SHLServerStorage {\n private _serverContainerClient?: AzureContainerClient;\n\n constructor(config: AzureStorageConfig) {\n super(config);\n }\n\n async read(key: string): Promise<string | Uint8Array | null> {\n try {\n const container = await this._getServerContainer();\n const blobName = this._serverBlobName(key);\n const client = container.getBlockBlobClient(blobName);\n\n const response = await client.download();\n if (!response.readableStreamBody) return null;\n\n return await streamToString(response.readableStreamBody);\n } catch (err) {\n const code = (err as { statusCode?: number }).statusCode;\n if (code === 404) return null;\n if (err instanceof StorageError) throw err;\n throw new StorageError(\n `Failed to read ${key}: ${err instanceof Error ? err.message : String(err)}`,\n \"read\",\n );\n }\n }\n\n async updateMetadata(\n shlId: string,\n updater: (current: SHLMetadata) => SHLMetadata | null,\n ): Promise<SHLMetadata | null> {\n const key = `${shlId}/metadata.json`;\n const raw = await this.read(key);\n if (raw === null) return null;\n\n const current = JSON.parse(raw as string) as SHLMetadata;\n const updated = updater(current);\n if (updated === null) return null;\n\n await this.store(key, JSON.stringify(updated));\n return updated;\n }\n\n private async _getServerContainer(): Promise<AzureContainerClient> {\n if (!this._serverContainerClient) {\n const azure = await getAzureModule();\n const serviceClient = azure.BlobServiceClient.fromConnectionString(this.config.connectionString);\n this._serverContainerClient = serviceClient.getContainerClient(this.config.container);\n }\n return this._serverContainerClient;\n }\n\n private _serverBlobName(key: string): string {\n const prefix = this.config.prefix?.replace(/\\/+$/, \"\");\n return prefix ? `${prefix}/${key}` : key;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Google Cloud Storage (Server)\n// ---------------------------------------------------------------------------\n\n// Minimal GCS interfaces for server-side operations\ninterface GCSModule {\n Storage: new () => GCSStorageClient;\n}\ninterface GCSStorageClient {\n bucket(name: string): GCSBucket;\n}\ninterface GCSBucket {\n file(name: string): GCSFile;\n getFiles(options?: { prefix?: string }): Promise<[GCSFile[]]>;\n}\ninterface GCSFile {\n save(content: Buffer, options?: Record<string, unknown>): Promise<void>;\n delete(options?: Record<string, unknown>): Promise<unknown>;\n download(): Promise<[Buffer]>;\n name: string;\n}\n\nlet _gcsModule: GCSModule | undefined;\nasync function getGCSModule(): Promise<GCSModule> {\n if (_gcsModule) return _gcsModule;\n try {\n _gcsModule = (await import(\"@google-cloud/storage\")) as unknown as GCSModule;\n return _gcsModule;\n } catch {\n throw new StorageError(\n \"@google-cloud/storage is required for ServerGCSStorage. Install it: npm install @google-cloud/storage\",\n \"import\",\n );\n }\n}\n\n/**\n * Google Cloud Storage server storage for SMART Health Links.\n *\n * Extends the base `GCSStorage` with `read` and `updateMetadata`\n * methods needed for serving SHLs.\n *\n * @example\n * ```ts\n * import { ServerGCSStorage } from \"@fhirfly-io/shl/server\";\n *\n * const storage = new ServerGCSStorage({\n * bucket: \"my-shl-bucket\",\n * baseUrl: \"https://shl.example.com\",\n * });\n * ```\n */\nexport class ServerGCSStorage extends GCSStorage implements SHLServerStorage {\n private _serverBucket?: GCSBucket;\n\n constructor(config: GCSStorageConfig) {\n super(config);\n }\n\n async read(key: string): Promise<string | Uint8Array | null> {\n try {\n const bucket = await this._getServerBucket();\n const fileName = this._serverFileName(key);\n const file = bucket.file(fileName);\n\n const [content] = await file.download();\n return content.toString(\"utf8\");\n } catch (err) {\n const code = (err as { code?: number }).code;\n if (code === 404) return null;\n if (err instanceof StorageError) throw err;\n throw new StorageError(\n `Failed to read ${key}: ${err instanceof Error ? err.message : String(err)}`,\n \"read\",\n );\n }\n }\n\n async updateMetadata(\n shlId: string,\n updater: (current: SHLMetadata) => SHLMetadata | null,\n ): Promise<SHLMetadata | null> {\n const key = `${shlId}/metadata.json`;\n const raw = await this.read(key);\n if (raw === null) return null;\n\n const current = JSON.parse(raw as string) as SHLMetadata;\n const updated = updater(current);\n if (updated === null) return null;\n\n await this.store(key, JSON.stringify(updated));\n return updated;\n }\n\n private async _getServerBucket(): Promise<GCSBucket> {\n if (!this._serverBucket) {\n const gcs = await getGCSModule();\n const storage = new gcs.Storage();\n this._serverBucket = storage.bucket(this.config.bucket);\n }\n return this._serverBucket;\n }\n\n private _serverFileName(key: string): string {\n const prefix = this.config.prefix?.replace(/\\/+$/, \"\");\n return prefix ? `${prefix}/${key}` : key;\n }\n}\n\n/** Helper: convert a Node.js ReadableStream to a string. */\nasync function streamToString(stream: NodeJS.ReadableStream): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string));\n }\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n"]}
@@ -1 +0,0 @@
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"]}