@fhirfly-io/shl 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/{chunk-63Q54EKN.cjs → chunk-CN44QKWJ.cjs} +185 -36
- package/dist/chunk-CN44QKWJ.cjs.map +1 -0
- package/dist/{chunk-QXSWM5QV.cjs → chunk-H37YQWF2.cjs} +90 -11
- package/dist/chunk-H37YQWF2.cjs.map +1 -0
- package/dist/{chunk-ZEE5RXIS.js → chunk-IYRQRY4A.js} +90 -12
- package/dist/chunk-IYRQRY4A.js.map +1 -0
- package/dist/{chunk-YBDRWUQU.js → chunk-YUMCDN7I.js} +185 -36
- package/dist/chunk-YUMCDN7I.js.map +1 -0
- package/dist/cli.cjs +11 -11
- package/dist/cli.js +2 -2
- package/dist/express.cjs +12 -3
- package/dist/express.cjs.map +1 -1
- package/dist/express.d.cts +3 -2
- package/dist/express.d.ts +3 -2
- package/dist/express.js +11 -2
- package/dist/express.js.map +1 -1
- package/dist/fastify.cjs +22 -5
- package/dist/fastify.cjs.map +1 -1
- package/dist/fastify.d.cts +3 -2
- package/dist/fastify.d.ts +3 -2
- package/dist/fastify.js +21 -4
- package/dist/fastify.js.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +15 -5
- package/dist/index.d.ts +15 -5
- package/dist/index.js +1 -1
- package/dist/lambda.cjs +4 -3
- package/dist/lambda.cjs.map +1 -1
- package/dist/lambda.d.cts +3 -2
- package/dist/lambda.d.ts +3 -2
- package/dist/lambda.js +3 -2
- package/dist/lambda.js.map +1 -1
- package/dist/server.cjs +6 -2
- package/dist/server.d.cts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -1
- package/dist/{storage-B3GyJD2y.d.ts → storage-CvsOM1Eu.d.ts} +1 -1
- package/dist/{storage-BwszYwFo.d.cts → storage-DggeMhOI.d.cts} +1 -1
- package/dist/{types-BegxU0wQ.d.ts → types--SjcaaWT.d.ts} +26 -2
- package/dist/{types-hHf-a3hH.d.cts → types-BcfxBDTA.d.cts} +26 -2
- package/dist/{types-Doq5cGNm.d.ts → types-CmeXnyth.d.cts} +31 -3
- package/dist/{types-Doq5cGNm.d.cts → types-CmeXnyth.d.ts} +31 -3
- package/package.json +1 -1
- package/dist/chunk-63Q54EKN.cjs.map +0 -1
- package/dist/chunk-QXSWM5QV.cjs.map +0 -1
- package/dist/chunk-YBDRWUQU.js.map +0 -1
- package/dist/chunk-ZEE5RXIS.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/types.ts","../src/server/handler.ts"],"names":["createHash","timingSafeEqual"],"mappings":"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","file":"chunk-H37YQWF2.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 { SHLStorage, SHLMetadata } from \"../shl/types.js\";\n\n/**\n * Framework-agnostic incoming request.\n */\nexport interface HandlerRequest {\n /** HTTP method (uppercase) */\n method: string;\n /** Path relative to mount point, e.g., \"/{shlId}\" or \"/{shlId}/content\" */\n path: string;\n /** Parsed JSON body (for POST requests) */\n body?: unknown;\n /** Request headers (lowercase keys) */\n headers: Record<string, string | undefined>;\n /** Query parameters (e.g., { recipient: \"Dr. Smith\" }) */\n query?: Record<string, string | undefined>;\n}\n\n/**\n * Framework-agnostic outgoing response.\n */\nexport interface HandlerResponse {\n /** HTTP status code */\n status: number;\n /** Response headers */\n headers: Record<string, string>;\n /** Response body (string for JSON, Uint8Array for binary) */\n body: string | Uint8Array;\n}\n\n/**\n * Extended storage interface for server-side operations.\n *\n * Adds `read` and `updateMetadata` to the base `SHLStorage` interface.\n * Server storage needs to read files and atomically update metadata\n * (e.g., increment access counts).\n */\nexport interface SHLServerStorage extends SHLStorage {\n /** Read a file by key. Returns null if not found. */\n read(key: string): Promise<string | Uint8Array | null>;\n\n /**\n * Atomically read-modify-write metadata for an SHL.\n *\n * The `updater` function receives the current metadata and returns\n * the updated metadata (or `null` to signal no update should occur).\n *\n * @param shlId - The SHL identifier\n * @param updater - Function that transforms metadata\n * @returns The updated metadata, or null if the SHL was not found or updater returned null\n */\n updateMetadata(\n shlId: string,\n updater: (current: SHLMetadata) => SHLMetadata | null,\n ): Promise<SHLMetadata | null>;\n}\n\n/**\n * Extended storage interface that includes audit logging.\n *\n * When a storage backend implements `AuditableStorage`, the server handler\n * will automatically call `onAccess()` after each successful retrieval.\n * This is in addition to the `onAccess` callback on `SHLHandlerConfig`.\n *\n * Existing `SHLServerStorage` implementations continue to work unchanged —\n * audit logging is opt-in.\n */\nexport interface AuditableStorage extends SHLServerStorage {\n /** Called after each successful SHL retrieval. */\n onAccess(shlId: string, event: AccessEvent): Promise<void>;\n}\n\n/**\n * Type guard: checks if a storage backend implements AuditableStorage.\n */\nexport function isAuditableStorage(storage: SHLServerStorage): storage is AuditableStorage {\n return typeof (storage as AuditableStorage).onAccess === \"function\";\n}\n\n/**\n * CORS configuration for the SHL server handler.\n *\n * By default, the handler adds permissive CORS headers to all responses\n * so that browser-based SHL viewers can access self-hosted servers.\n * Set `cors: false` to disable, or provide an object to customize.\n */\nexport interface CorsConfig {\n /** Allowed origin(s). Default: `\"*\"` */\n origin?: string;\n /** Allowed methods. Default: `\"GET, POST, OPTIONS\"` */\n methods?: string;\n /** Allowed headers. Default: `\"Content-Type, Authorization\"` */\n headers?: string;\n}\n\n/**\n * Configuration for the SHL server handler.\n */\nexport interface SHLHandlerConfig {\n /** Server storage backend (must implement SHLServerStorage) */\n storage: SHLServerStorage;\n\n /**\n * Optional callback invoked on each successful manifest access.\n * Useful for logging, analytics, or custom access control.\n */\n onAccess?: (event: AccessEvent) => void | Promise<void>;\n\n /**\n * CORS configuration. Defaults to permissive headers (`Access-Control-Allow-Origin: *`).\n * Set to `false` to disable CORS headers entirely.\n */\n cors?: CorsConfig | false;\n}\n\n/**\n * Event emitted on each successful manifest access.\n */\nexport interface AccessEvent {\n /** The SHL identifier */\n shlId: string;\n /** Current access count (after increment) */\n accessCount: number;\n /** Timestamp of the access */\n timestamp: Date;\n /** Recipient identifier from query parameter (e.g., provider name) */\n recipient?: string;\n /** Retrieval mode used for this access */\n mode?: \"manifest\" | \"direct\";\n}\n","// 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 CorsConfig,\n} from \"./types.js\";\nimport { isAuditableStorage } 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 three routes:\n *\n * - `POST /{shlId}` — Manifest endpoint (validates passcode, checks access limits)\n * - `GET /{shlId}/content` — Content endpoint (serves encrypted JWE)\n * - `GET /{shlId}/attachment/{index}` — Attachment endpoint (serves encrypted attachment)\n *\n * By default, CORS headers are added to all responses so browser-based SHL\n * viewers can access self-hosted servers. Set `cors: false` to disable.\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 const corsHeaders = resolveCorsHeaders(config.cors);\n\n return async (req: HandlerRequest): Promise<HandlerResponse> => {\n // Handle CORS preflight\n if (req.method === \"OPTIONS\") {\n return {\n status: 204,\n headers: { ...corsHeaders },\n body: \"\",\n };\n }\n\n // Normalize path: strip leading slash, split into segments\n const path = req.path.replace(/^\\/+/, \"\");\n const segments = path.split(\"/\").filter(Boolean);\n\n let response: HandlerResponse;\n\n // Route: POST /{shlId} → manifest\n if (segments.length === 1 && req.method === \"POST\") {\n response = await handleManifest(segments[0]!, req, storage, onAccess);\n }\n // Route: GET /{shlId} → direct access (flag U)\n else if (segments.length === 1 && req.method === \"GET\") {\n response = await handleDirectAccess(segments[0]!, req, storage, onAccess);\n }\n // Route: GET /{shlId}/content → serve encrypted content\n else if (segments.length === 2 && segments[1] === \"content\" && req.method === \"GET\") {\n response = await handleContent(segments[0]!, storage);\n }\n // Route: GET /{shlId}/attachment/{index} → serve encrypted attachment\n else if (segments.length === 3 && segments[1] === \"attachment\" && req.method === \"GET\") {\n response = await handleAttachment(segments[0]!, segments[2]!, storage);\n }\n // Method not allowed for known paths\n else if (segments.length === 2 && segments[1] === \"content\" && req.method !== \"GET\") {\n response = jsonResponse(405, { error: \"Method not allowed. Use GET for content requests.\" });\n }\n else if (segments.length === 3 && segments[1] === \"attachment\" && req.method !== \"GET\") {\n response = jsonResponse(405, { error: \"Method not allowed. Use GET for attachment requests.\" });\n }\n else {\n response = jsonResponse(404, { error: \"Not found\" });\n }\n\n // Inject CORS headers into every response\n response.headers = { ...response.headers, ...corsHeaders };\n return response;\n };\n}\n\n/** Resolve CORS headers from config. Returns empty object if disabled. */\nfunction resolveCorsHeaders(cors: SHLHandlerConfig[\"cors\"]): Record<string, string> {\n if (cors === false) return {};\n const c: CorsConfig = cors ?? {};\n return {\n \"access-control-allow-origin\": c.origin ?? \"*\",\n \"access-control-allow-methods\": c.methods ?? \"GET, POST, OPTIONS\",\n \"access-control-allow-headers\": c.headers ?? \"Content-Type, Authorization\",\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 const recipient = typeof req.query?.[\"recipient\"] === \"string\" ? req.query[\"recipient\"] : undefined;\n const accessEvent = {\n shlId,\n accessCount: updatedMetadata.accessCount ?? 1,\n timestamp: new Date(),\n mode: \"manifest\" as const,\n ...(recipient ? { recipient } : {}),\n };\n if (onAccess) {\n // Fire and forget — don't let callback errors break the response\n Promise.resolve(onAccess(accessEvent)).catch(() => {});\n }\n // If storage is auditable, also fire the storage-level audit hook\n if (isAuditableStorage(storage)) {\n Promise.resolve(storage.onAccess(shlId, accessEvent)).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 handleDirectAccess(\n shlId: string,\n req: HandlerRequest,\n storage: SHLHandlerConfig[\"storage\"],\n onAccess?: SHLHandlerConfig[\"onAccess\"],\n): Promise<HandlerResponse> {\n // Read metadata to check if this is a direct-mode SHL\n const metadataRaw = await storage.read(`${shlId}/metadata.json`);\n if (metadataRaw === null) {\n return jsonResponse(404, { error: \"SHL not found\" });\n }\n\n const metadataStr = typeof metadataRaw === \"string\" ? metadataRaw : new TextDecoder().decode(metadataRaw);\n const metadata = JSON.parse(metadataStr) as SHLMetadata;\n\n // Only direct-mode SHLs support GET retrieval\n if (metadata.mode !== \"direct\") {\n return jsonResponse(405, { error: \"Method not allowed. Use POST for manifest requests.\" });\n }\n\n // Access control: expiration, access count (atomic)\n let accessDeniedReason: \"expired\" | \"exhausted\" | null = null;\n const updatedMetadata = await storage.updateMetadata(shlId, (current) => {\n if (current.expiresAt) {\n const expiresAt = new Date(current.expiresAt);\n if (expiresAt.getTime() <= Date.now()) {\n accessDeniedReason = \"expired\";\n return null;\n }\n }\n\n const currentCount = current.accessCount ?? 0;\n if (current.maxAccesses !== undefined && currentCount >= current.maxAccesses) {\n accessDeniedReason = \"exhausted\";\n return null;\n }\n\n return {\n ...current,\n accessCount: currentCount + 1,\n };\n });\n\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 (updatedMetadata === null) {\n return jsonResponse(404, { error: \"SHL not found\" });\n }\n\n // Fire access event (non-blocking)\n const recipient = typeof req.query?.[\"recipient\"] === \"string\" ? req.query[\"recipient\"] : undefined;\n const directAccessEvent = {\n shlId,\n accessCount: updatedMetadata.accessCount ?? 1,\n timestamp: new Date(),\n mode: \"direct\" as const,\n ...(recipient ? { recipient } : {}),\n };\n if (onAccess) {\n Promise.resolve(onAccess(directAccessEvent)).catch(() => {});\n }\n // If storage is auditable, also fire the storage-level audit hook\n if (isAuditableStorage(storage)) {\n Promise.resolve(storage.onAccess(shlId, directAccessEvent)).catch(() => {});\n }\n\n // Serve the encrypted content directly\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\" ? content : 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 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"]}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createHash, timingSafeEqual } from 'crypto';
|
|
2
2
|
|
|
3
|
-
// src/server/
|
|
3
|
+
// src/server/types.ts
|
|
4
|
+
function isAuditableStorage(storage) {
|
|
5
|
+
return typeof storage.onAccess === "function";
|
|
6
|
+
}
|
|
4
7
|
function createHandler(config) {
|
|
5
8
|
const { storage, onAccess } = config;
|
|
6
9
|
const corsHeaders = resolveCorsHeaders(config.cors);
|
|
@@ -17,12 +20,12 @@ function createHandler(config) {
|
|
|
17
20
|
let response;
|
|
18
21
|
if (segments.length === 1 && req.method === "POST") {
|
|
19
22
|
response = await handleManifest(segments[0], req, storage, onAccess);
|
|
23
|
+
} else if (segments.length === 1 && req.method === "GET") {
|
|
24
|
+
response = await handleDirectAccess(segments[0], req, storage, onAccess);
|
|
20
25
|
} else if (segments.length === 2 && segments[1] === "content" && req.method === "GET") {
|
|
21
26
|
response = await handleContent(segments[0], storage);
|
|
22
27
|
} else if (segments.length === 3 && segments[1] === "attachment" && req.method === "GET") {
|
|
23
28
|
response = await handleAttachment(segments[0], segments[2], storage);
|
|
24
|
-
} else if (segments.length === 1 && req.method !== "POST") {
|
|
25
|
-
response = jsonResponse(405, { error: "Method not allowed. Use POST for manifest requests." });
|
|
26
29
|
} else if (segments.length === 2 && segments[1] === "content" && req.method !== "GET") {
|
|
27
30
|
response = jsonResponse(405, { error: "Method not allowed. Use GET for content requests." });
|
|
28
31
|
} else if (segments.length === 3 && segments[1] === "attachment" && req.method !== "GET") {
|
|
@@ -96,19 +99,94 @@ async function handleManifest(shlId, req, storage, onAccess) {
|
|
|
96
99
|
if (updatedMetadata === null) {
|
|
97
100
|
return jsonResponse(404, { error: "SHL not found" });
|
|
98
101
|
}
|
|
102
|
+
const recipient = typeof req.query?.["recipient"] === "string" ? req.query["recipient"] : void 0;
|
|
103
|
+
const accessEvent = {
|
|
104
|
+
shlId,
|
|
105
|
+
accessCount: updatedMetadata.accessCount ?? 1,
|
|
106
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
107
|
+
mode: "manifest",
|
|
108
|
+
...recipient ? { recipient } : {}
|
|
109
|
+
};
|
|
99
110
|
if (onAccess) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Promise.resolve(onAccess(event)).catch(() => {
|
|
111
|
+
Promise.resolve(onAccess(accessEvent)).catch(() => {
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (isAuditableStorage(storage)) {
|
|
115
|
+
Promise.resolve(storage.onAccess(shlId, accessEvent)).catch(() => {
|
|
106
116
|
});
|
|
107
117
|
}
|
|
108
118
|
const manifestStr = typeof manifestRaw === "string" ? manifestRaw : new TextDecoder().decode(manifestRaw);
|
|
109
119
|
const manifest = JSON.parse(manifestStr);
|
|
110
120
|
return jsonResponse(200, manifest);
|
|
111
121
|
}
|
|
122
|
+
async function handleDirectAccess(shlId, req, storage, onAccess) {
|
|
123
|
+
const metadataRaw = await storage.read(`${shlId}/metadata.json`);
|
|
124
|
+
if (metadataRaw === null) {
|
|
125
|
+
return jsonResponse(404, { error: "SHL not found" });
|
|
126
|
+
}
|
|
127
|
+
const metadataStr = typeof metadataRaw === "string" ? metadataRaw : new TextDecoder().decode(metadataRaw);
|
|
128
|
+
const metadata = JSON.parse(metadataStr);
|
|
129
|
+
if (metadata.mode !== "direct") {
|
|
130
|
+
return jsonResponse(405, { error: "Method not allowed. Use POST for manifest requests." });
|
|
131
|
+
}
|
|
132
|
+
let accessDeniedReason = null;
|
|
133
|
+
const updatedMetadata = await storage.updateMetadata(shlId, (current) => {
|
|
134
|
+
if (current.expiresAt) {
|
|
135
|
+
const expiresAt = new Date(current.expiresAt);
|
|
136
|
+
if (expiresAt.getTime() <= Date.now()) {
|
|
137
|
+
accessDeniedReason = "expired";
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const currentCount = current.accessCount ?? 0;
|
|
142
|
+
if (current.maxAccesses !== void 0 && currentCount >= current.maxAccesses) {
|
|
143
|
+
accessDeniedReason = "exhausted";
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
...current,
|
|
148
|
+
accessCount: currentCount + 1
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
if (accessDeniedReason === "expired") {
|
|
152
|
+
return jsonResponse(410, { error: "SHL has expired" });
|
|
153
|
+
}
|
|
154
|
+
if (accessDeniedReason === "exhausted") {
|
|
155
|
+
return jsonResponse(410, { error: "SHL access limit reached" });
|
|
156
|
+
}
|
|
157
|
+
if (updatedMetadata === null) {
|
|
158
|
+
return jsonResponse(404, { error: "SHL not found" });
|
|
159
|
+
}
|
|
160
|
+
const recipient = typeof req.query?.["recipient"] === "string" ? req.query["recipient"] : void 0;
|
|
161
|
+
const directAccessEvent = {
|
|
162
|
+
shlId,
|
|
163
|
+
accessCount: updatedMetadata.accessCount ?? 1,
|
|
164
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
165
|
+
mode: "direct",
|
|
166
|
+
...recipient ? { recipient } : {}
|
|
167
|
+
};
|
|
168
|
+
if (onAccess) {
|
|
169
|
+
Promise.resolve(onAccess(directAccessEvent)).catch(() => {
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (isAuditableStorage(storage)) {
|
|
173
|
+
Promise.resolve(storage.onAccess(shlId, directAccessEvent)).catch(() => {
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const content = await storage.read(`${shlId}/content.jwe`);
|
|
177
|
+
if (content === null) {
|
|
178
|
+
return jsonResponse(404, { error: "Content not found" });
|
|
179
|
+
}
|
|
180
|
+
const body = typeof content === "string" ? content : new TextDecoder().decode(content);
|
|
181
|
+
return {
|
|
182
|
+
status: 200,
|
|
183
|
+
headers: {
|
|
184
|
+
"content-type": "application/jose",
|
|
185
|
+
"cache-control": "no-store"
|
|
186
|
+
},
|
|
187
|
+
body
|
|
188
|
+
};
|
|
189
|
+
}
|
|
112
190
|
async function handleContent(shlId, storage) {
|
|
113
191
|
const content = await storage.read(`${shlId}/content.jwe`);
|
|
114
192
|
if (content === null) {
|
|
@@ -153,6 +231,6 @@ function jsonResponse(status, body) {
|
|
|
153
231
|
};
|
|
154
232
|
}
|
|
155
233
|
|
|
156
|
-
export { createHandler };
|
|
157
|
-
//# sourceMappingURL=chunk-
|
|
158
|
-
//# sourceMappingURL=chunk-
|
|
234
|
+
export { createHandler, isAuditableStorage };
|
|
235
|
+
//# sourceMappingURL=chunk-IYRQRY4A.js.map
|
|
236
|
+
//# sourceMappingURL=chunk-IYRQRY4A.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/types.ts","../src/server/handler.ts"],"names":[],"mappings":"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","file":"chunk-IYRQRY4A.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 { SHLStorage, SHLMetadata } from \"../shl/types.js\";\n\n/**\n * Framework-agnostic incoming request.\n */\nexport interface HandlerRequest {\n /** HTTP method (uppercase) */\n method: string;\n /** Path relative to mount point, e.g., \"/{shlId}\" or \"/{shlId}/content\" */\n path: string;\n /** Parsed JSON body (for POST requests) */\n body?: unknown;\n /** Request headers (lowercase keys) */\n headers: Record<string, string | undefined>;\n /** Query parameters (e.g., { recipient: \"Dr. Smith\" }) */\n query?: Record<string, string | undefined>;\n}\n\n/**\n * Framework-agnostic outgoing response.\n */\nexport interface HandlerResponse {\n /** HTTP status code */\n status: number;\n /** Response headers */\n headers: Record<string, string>;\n /** Response body (string for JSON, Uint8Array for binary) */\n body: string | Uint8Array;\n}\n\n/**\n * Extended storage interface for server-side operations.\n *\n * Adds `read` and `updateMetadata` to the base `SHLStorage` interface.\n * Server storage needs to read files and atomically update metadata\n * (e.g., increment access counts).\n */\nexport interface SHLServerStorage extends SHLStorage {\n /** Read a file by key. Returns null if not found. */\n read(key: string): Promise<string | Uint8Array | null>;\n\n /**\n * Atomically read-modify-write metadata for an SHL.\n *\n * The `updater` function receives the current metadata and returns\n * the updated metadata (or `null` to signal no update should occur).\n *\n * @param shlId - The SHL identifier\n * @param updater - Function that transforms metadata\n * @returns The updated metadata, or null if the SHL was not found or updater returned null\n */\n updateMetadata(\n shlId: string,\n updater: (current: SHLMetadata) => SHLMetadata | null,\n ): Promise<SHLMetadata | null>;\n}\n\n/**\n * Extended storage interface that includes audit logging.\n *\n * When a storage backend implements `AuditableStorage`, the server handler\n * will automatically call `onAccess()` after each successful retrieval.\n * This is in addition to the `onAccess` callback on `SHLHandlerConfig`.\n *\n * Existing `SHLServerStorage` implementations continue to work unchanged —\n * audit logging is opt-in.\n */\nexport interface AuditableStorage extends SHLServerStorage {\n /** Called after each successful SHL retrieval. */\n onAccess(shlId: string, event: AccessEvent): Promise<void>;\n}\n\n/**\n * Type guard: checks if a storage backend implements AuditableStorage.\n */\nexport function isAuditableStorage(storage: SHLServerStorage): storage is AuditableStorage {\n return typeof (storage as AuditableStorage).onAccess === \"function\";\n}\n\n/**\n * CORS configuration for the SHL server handler.\n *\n * By default, the handler adds permissive CORS headers to all responses\n * so that browser-based SHL viewers can access self-hosted servers.\n * Set `cors: false` to disable, or provide an object to customize.\n */\nexport interface CorsConfig {\n /** Allowed origin(s). Default: `\"*\"` */\n origin?: string;\n /** Allowed methods. Default: `\"GET, POST, OPTIONS\"` */\n methods?: string;\n /** Allowed headers. Default: `\"Content-Type, Authorization\"` */\n headers?: string;\n}\n\n/**\n * Configuration for the SHL server handler.\n */\nexport interface SHLHandlerConfig {\n /** Server storage backend (must implement SHLServerStorage) */\n storage: SHLServerStorage;\n\n /**\n * Optional callback invoked on each successful manifest access.\n * Useful for logging, analytics, or custom access control.\n */\n onAccess?: (event: AccessEvent) => void | Promise<void>;\n\n /**\n * CORS configuration. Defaults to permissive headers (`Access-Control-Allow-Origin: *`).\n * Set to `false` to disable CORS headers entirely.\n */\n cors?: CorsConfig | false;\n}\n\n/**\n * Event emitted on each successful manifest access.\n */\nexport interface AccessEvent {\n /** The SHL identifier */\n shlId: string;\n /** Current access count (after increment) */\n accessCount: number;\n /** Timestamp of the access */\n timestamp: Date;\n /** Recipient identifier from query parameter (e.g., provider name) */\n recipient?: string;\n /** Retrieval mode used for this access */\n mode?: \"manifest\" | \"direct\";\n}\n","// 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 CorsConfig,\n} from \"./types.js\";\nimport { isAuditableStorage } 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 three routes:\n *\n * - `POST /{shlId}` — Manifest endpoint (validates passcode, checks access limits)\n * - `GET /{shlId}/content` — Content endpoint (serves encrypted JWE)\n * - `GET /{shlId}/attachment/{index}` — Attachment endpoint (serves encrypted attachment)\n *\n * By default, CORS headers are added to all responses so browser-based SHL\n * viewers can access self-hosted servers. Set `cors: false` to disable.\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 const corsHeaders = resolveCorsHeaders(config.cors);\n\n return async (req: HandlerRequest): Promise<HandlerResponse> => {\n // Handle CORS preflight\n if (req.method === \"OPTIONS\") {\n return {\n status: 204,\n headers: { ...corsHeaders },\n body: \"\",\n };\n }\n\n // Normalize path: strip leading slash, split into segments\n const path = req.path.replace(/^\\/+/, \"\");\n const segments = path.split(\"/\").filter(Boolean);\n\n let response: HandlerResponse;\n\n // Route: POST /{shlId} → manifest\n if (segments.length === 1 && req.method === \"POST\") {\n response = await handleManifest(segments[0]!, req, storage, onAccess);\n }\n // Route: GET /{shlId} → direct access (flag U)\n else if (segments.length === 1 && req.method === \"GET\") {\n response = await handleDirectAccess(segments[0]!, req, storage, onAccess);\n }\n // Route: GET /{shlId}/content → serve encrypted content\n else if (segments.length === 2 && segments[1] === \"content\" && req.method === \"GET\") {\n response = await handleContent(segments[0]!, storage);\n }\n // Route: GET /{shlId}/attachment/{index} → serve encrypted attachment\n else if (segments.length === 3 && segments[1] === \"attachment\" && req.method === \"GET\") {\n response = await handleAttachment(segments[0]!, segments[2]!, storage);\n }\n // Method not allowed for known paths\n else if (segments.length === 2 && segments[1] === \"content\" && req.method !== \"GET\") {\n response = jsonResponse(405, { error: \"Method not allowed. Use GET for content requests.\" });\n }\n else if (segments.length === 3 && segments[1] === \"attachment\" && req.method !== \"GET\") {\n response = jsonResponse(405, { error: \"Method not allowed. Use GET for attachment requests.\" });\n }\n else {\n response = jsonResponse(404, { error: \"Not found\" });\n }\n\n // Inject CORS headers into every response\n response.headers = { ...response.headers, ...corsHeaders };\n return response;\n };\n}\n\n/** Resolve CORS headers from config. Returns empty object if disabled. */\nfunction resolveCorsHeaders(cors: SHLHandlerConfig[\"cors\"]): Record<string, string> {\n if (cors === false) return {};\n const c: CorsConfig = cors ?? {};\n return {\n \"access-control-allow-origin\": c.origin ?? \"*\",\n \"access-control-allow-methods\": c.methods ?? \"GET, POST, OPTIONS\",\n \"access-control-allow-headers\": c.headers ?? \"Content-Type, Authorization\",\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 const recipient = typeof req.query?.[\"recipient\"] === \"string\" ? req.query[\"recipient\"] : undefined;\n const accessEvent = {\n shlId,\n accessCount: updatedMetadata.accessCount ?? 1,\n timestamp: new Date(),\n mode: \"manifest\" as const,\n ...(recipient ? { recipient } : {}),\n };\n if (onAccess) {\n // Fire and forget — don't let callback errors break the response\n Promise.resolve(onAccess(accessEvent)).catch(() => {});\n }\n // If storage is auditable, also fire the storage-level audit hook\n if (isAuditableStorage(storage)) {\n Promise.resolve(storage.onAccess(shlId, accessEvent)).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 handleDirectAccess(\n shlId: string,\n req: HandlerRequest,\n storage: SHLHandlerConfig[\"storage\"],\n onAccess?: SHLHandlerConfig[\"onAccess\"],\n): Promise<HandlerResponse> {\n // Read metadata to check if this is a direct-mode SHL\n const metadataRaw = await storage.read(`${shlId}/metadata.json`);\n if (metadataRaw === null) {\n return jsonResponse(404, { error: \"SHL not found\" });\n }\n\n const metadataStr = typeof metadataRaw === \"string\" ? metadataRaw : new TextDecoder().decode(metadataRaw);\n const metadata = JSON.parse(metadataStr) as SHLMetadata;\n\n // Only direct-mode SHLs support GET retrieval\n if (metadata.mode !== \"direct\") {\n return jsonResponse(405, { error: \"Method not allowed. Use POST for manifest requests.\" });\n }\n\n // Access control: expiration, access count (atomic)\n let accessDeniedReason: \"expired\" | \"exhausted\" | null = null;\n const updatedMetadata = await storage.updateMetadata(shlId, (current) => {\n if (current.expiresAt) {\n const expiresAt = new Date(current.expiresAt);\n if (expiresAt.getTime() <= Date.now()) {\n accessDeniedReason = \"expired\";\n return null;\n }\n }\n\n const currentCount = current.accessCount ?? 0;\n if (current.maxAccesses !== undefined && currentCount >= current.maxAccesses) {\n accessDeniedReason = \"exhausted\";\n return null;\n }\n\n return {\n ...current,\n accessCount: currentCount + 1,\n };\n });\n\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 (updatedMetadata === null) {\n return jsonResponse(404, { error: \"SHL not found\" });\n }\n\n // Fire access event (non-blocking)\n const recipient = typeof req.query?.[\"recipient\"] === \"string\" ? req.query[\"recipient\"] : undefined;\n const directAccessEvent = {\n shlId,\n accessCount: updatedMetadata.accessCount ?? 1,\n timestamp: new Date(),\n mode: \"direct\" as const,\n ...(recipient ? { recipient } : {}),\n };\n if (onAccess) {\n Promise.resolve(onAccess(directAccessEvent)).catch(() => {});\n }\n // If storage is auditable, also fire the storage-level audit hook\n if (isAuditableStorage(storage)) {\n Promise.resolve(storage.onAccess(shlId, directAccessEvent)).catch(() => {});\n }\n\n // Serve the encrypted content directly\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\" ? content : 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 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"]}
|
|
@@ -174,7 +174,11 @@ var CODE_SYSTEMS = {
|
|
|
174
174
|
CVX: "http://hl7.org/fhir/sid/cvx",
|
|
175
175
|
ICD10CM: "http://hl7.org/fhir/sid/icd-10-cm",
|
|
176
176
|
CONDITION_CLINICAL: "http://terminology.hl7.org/CodeSystem/condition-clinical",
|
|
177
|
-
ALLERGY_CLINICAL: "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical"
|
|
177
|
+
ALLERGY_CLINICAL: "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical",
|
|
178
|
+
/** CMS Patient-Shared Health Document category code system */
|
|
179
|
+
CMS_PATIENT_SHARED_CATEGORY: "https://cms.gov/fhir/CodeSystem/patient-shared-category",
|
|
180
|
+
/** V3 ActCode security label for patient-asserted data */
|
|
181
|
+
SECURITY_PATAST: "PATAST"
|
|
178
182
|
};
|
|
179
183
|
|
|
180
184
|
// src/ips/medication.ts
|
|
@@ -1100,8 +1104,9 @@ function resolveDocuments(documents, patientRef, profile, generateUuid2) {
|
|
|
1100
1104
|
contentType,
|
|
1101
1105
|
data: base64Content
|
|
1102
1106
|
};
|
|
1103
|
-
const
|
|
1104
|
-
const
|
|
1107
|
+
const isPshd = profile === "pshd";
|
|
1108
|
+
const typeCode = isPshd ? "60591-5" : doc.typeCode ?? "34133-9";
|
|
1109
|
+
const typeDisplay = isPshd ? "Patient summary Document" : doc.typeDisplay ?? "Summarization of episode note";
|
|
1105
1110
|
const docRefResource = {
|
|
1106
1111
|
resourceType: "DocumentReference",
|
|
1107
1112
|
id: docRefId,
|
|
@@ -1127,7 +1132,29 @@ function resolveDocuments(documents, patientRef, profile, generateUuid2) {
|
|
|
1127
1132
|
}
|
|
1128
1133
|
]
|
|
1129
1134
|
};
|
|
1130
|
-
if (
|
|
1135
|
+
if (isPshd) {
|
|
1136
|
+
docRefResource.category = [
|
|
1137
|
+
{
|
|
1138
|
+
coding: [
|
|
1139
|
+
{
|
|
1140
|
+
system: CODE_SYSTEMS.CMS_PATIENT_SHARED_CATEGORY,
|
|
1141
|
+
code: "patient-shared",
|
|
1142
|
+
display: "Patient Shared"
|
|
1143
|
+
}
|
|
1144
|
+
]
|
|
1145
|
+
}
|
|
1146
|
+
];
|
|
1147
|
+
docRefResource.author = [{ reference: patientRef }];
|
|
1148
|
+
docRefResource.meta = {
|
|
1149
|
+
security: [
|
|
1150
|
+
{
|
|
1151
|
+
system: "http://terminology.hl7.org/CodeSystem/v3-ActCode",
|
|
1152
|
+
code: CODE_SYSTEMS.SECURITY_PATAST,
|
|
1153
|
+
display: "patient asserted"
|
|
1154
|
+
}
|
|
1155
|
+
]
|
|
1156
|
+
};
|
|
1157
|
+
} else if (profile === "ips") {
|
|
1131
1158
|
docRefResource.meta = {
|
|
1132
1159
|
profile: ["http://hl7.org/fhir/uv/ips/StructureDefinition/DocumentReference-uv-ips"]
|
|
1133
1160
|
};
|
|
@@ -1326,11 +1353,9 @@ var Bundle = class {
|
|
|
1326
1353
|
async build(options) {
|
|
1327
1354
|
const profile = options?.profile ?? "ips";
|
|
1328
1355
|
const bundleId = options?.bundleId ?? generateUuid();
|
|
1329
|
-
const compositionId = generateUuid();
|
|
1330
1356
|
const patientId = generateUuid();
|
|
1331
|
-
const
|
|
1357
|
+
const timestamp = options?.compositionDate ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1332
1358
|
const patientFullUrl = `urn:uuid:${patientId}`;
|
|
1333
|
-
const compositionFullUrl = `urn:uuid:${compositionId}`;
|
|
1334
1359
|
const patientResource = normalizePatient(this._patient, patientId, profile);
|
|
1335
1360
|
const [medResult, condResult, allergyResult, immResult, resultResult] = await Promise.all([
|
|
1336
1361
|
resolveMedications(this._medications, patientFullUrl, profile, generateUuid),
|
|
@@ -1347,6 +1372,22 @@ var Bundle = class {
|
|
|
1347
1372
|
...immResult.warnings,
|
|
1348
1373
|
...resultResult.warnings
|
|
1349
1374
|
];
|
|
1375
|
+
if (profile === "pshd") {
|
|
1376
|
+
return this.buildPshdBundle(
|
|
1377
|
+
bundleId,
|
|
1378
|
+
timestamp,
|
|
1379
|
+
patientFullUrl,
|
|
1380
|
+
patientResource,
|
|
1381
|
+
medResult.entries,
|
|
1382
|
+
condResult.entries,
|
|
1383
|
+
allergyResult.entries,
|
|
1384
|
+
immResult.entries,
|
|
1385
|
+
resultResult.entries,
|
|
1386
|
+
docResult.entries
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const compositionId = generateUuid();
|
|
1390
|
+
const compositionFullUrl = `urn:uuid:${compositionId}`;
|
|
1350
1391
|
const medRefs = medResult.entries.map((e) => ({ reference: e.fullUrl }));
|
|
1351
1392
|
const condRefs = condResult.entries.map((e) => ({ reference: e.fullUrl }));
|
|
1352
1393
|
const allergyRefs = allergyResult.entries.map((e) => ({ reference: e.fullUrl }));
|
|
@@ -1355,7 +1396,7 @@ var Bundle = class {
|
|
|
1355
1396
|
const composition = this.buildComposition(
|
|
1356
1397
|
compositionId,
|
|
1357
1398
|
patientFullUrl,
|
|
1358
|
-
|
|
1399
|
+
timestamp,
|
|
1359
1400
|
profile,
|
|
1360
1401
|
medRefs,
|
|
1361
1402
|
allergyRefs,
|
|
@@ -1381,7 +1422,7 @@ var Bundle = class {
|
|
|
1381
1422
|
value: `urn:uuid:${bundleId}`
|
|
1382
1423
|
},
|
|
1383
1424
|
type: "document",
|
|
1384
|
-
timestamp
|
|
1425
|
+
timestamp,
|
|
1385
1426
|
entry: entries
|
|
1386
1427
|
};
|
|
1387
1428
|
return bundle;
|
|
@@ -1401,6 +1442,37 @@ var Bundle = class {
|
|
|
1401
1442
|
path: "Patient.birthDate"
|
|
1402
1443
|
});
|
|
1403
1444
|
}
|
|
1445
|
+
if (profile === "pshd") {
|
|
1446
|
+
if (this._documents.length === 0) {
|
|
1447
|
+
issues.push({
|
|
1448
|
+
severity: "error",
|
|
1449
|
+
message: "PSHD requires at least one DocumentReference (1..1)",
|
|
1450
|
+
path: "Bundle.entry:DocumentReference"
|
|
1451
|
+
});
|
|
1452
|
+
} else {
|
|
1453
|
+
const hasPdf = this._documents.some(
|
|
1454
|
+
(d) => (d.contentType ?? "application/pdf") === "application/pdf"
|
|
1455
|
+
);
|
|
1456
|
+
if (!hasPdf) {
|
|
1457
|
+
issues.push({
|
|
1458
|
+
severity: "error",
|
|
1459
|
+
message: "PSHD requires at least one PDF document (contentType application/pdf)",
|
|
1460
|
+
path: "DocumentReference.content.attachment.contentType"
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (!this._patient.gender) {
|
|
1465
|
+
issues.push({
|
|
1466
|
+
severity: "warning",
|
|
1467
|
+
message: "Patient.gender recommended for PSHD demographic matching",
|
|
1468
|
+
path: "Patient.gender"
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
return {
|
|
1472
|
+
valid: issues.filter((i) => i.severity === "error").length === 0,
|
|
1473
|
+
issues
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1404
1476
|
if (profile === "ips") {
|
|
1405
1477
|
if (!this.hasValidName()) {
|
|
1406
1478
|
issues.push({
|
|
@@ -1476,6 +1548,37 @@ var Bundle = class {
|
|
|
1476
1548
|
const s = this._patient;
|
|
1477
1549
|
return !!(s.given || s.family || s.name);
|
|
1478
1550
|
}
|
|
1551
|
+
buildPshdBundle(bundleId, timestamp, patientFullUrl, patientResource, medEntries, condEntries, allergyEntries, immEntries, resultEntries, docEntries) {
|
|
1552
|
+
const entries = [
|
|
1553
|
+
{ fullUrl: patientFullUrl, resource: patientResource },
|
|
1554
|
+
...medEntries,
|
|
1555
|
+
...condEntries,
|
|
1556
|
+
...allergyEntries,
|
|
1557
|
+
...immEntries,
|
|
1558
|
+
...resultEntries,
|
|
1559
|
+
...docEntries
|
|
1560
|
+
];
|
|
1561
|
+
for (const entry of entries) {
|
|
1562
|
+
const meta = entry.resource.meta;
|
|
1563
|
+
if (meta?.profile) {
|
|
1564
|
+
delete meta.profile;
|
|
1565
|
+
if (Object.keys(meta).length === 0) {
|
|
1566
|
+
delete entry.resource.meta;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return {
|
|
1571
|
+
resourceType: "Bundle",
|
|
1572
|
+
id: bundleId,
|
|
1573
|
+
identifier: {
|
|
1574
|
+
system: "urn:ietf:rfc:3986",
|
|
1575
|
+
value: `urn:uuid:${bundleId}`
|
|
1576
|
+
},
|
|
1577
|
+
type: "collection",
|
|
1578
|
+
timestamp,
|
|
1579
|
+
entry: entries
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1479
1582
|
buildComposition(id, patientRef, date, profile, medRefs, allergyRefs, condRefs, immRefs, resultRefs) {
|
|
1480
1583
|
const composition = {
|
|
1481
1584
|
resourceType: "Composition",
|
|
@@ -1604,6 +1707,7 @@ function generateUuid() {
|
|
|
1604
1707
|
var shl_exports = {};
|
|
1605
1708
|
__export(shl_exports, {
|
|
1606
1709
|
AzureStorage: () => AzureStorage,
|
|
1710
|
+
EXPIRATION_PRESETS: () => EXPIRATION_PRESETS,
|
|
1607
1711
|
FhirflyStorage: () => FhirflyStorage,
|
|
1608
1712
|
GCSStorage: () => GCSStorage,
|
|
1609
1713
|
LocalStorage: () => LocalStorage,
|
|
@@ -1615,6 +1719,18 @@ __export(shl_exports, {
|
|
|
1615
1719
|
getEntryContent: () => getEntryContent,
|
|
1616
1720
|
revoke: () => revoke
|
|
1617
1721
|
});
|
|
1722
|
+
|
|
1723
|
+
// src/shl/types.ts
|
|
1724
|
+
var EXPIRATION_PRESETS = {
|
|
1725
|
+
"point-of-care": 15 * 60 * 1e3,
|
|
1726
|
+
// 15 minutes
|
|
1727
|
+
"appointment": 24 * 60 * 60 * 1e3,
|
|
1728
|
+
// 24 hours
|
|
1729
|
+
"travel": 90 * 24 * 60 * 60 * 1e3,
|
|
1730
|
+
// 90 days
|
|
1731
|
+
"permanent": 0
|
|
1732
|
+
// no expiration
|
|
1733
|
+
};
|
|
1618
1734
|
function base64url(data) {
|
|
1619
1735
|
return data.toString("base64url");
|
|
1620
1736
|
}
|
|
@@ -1702,13 +1818,43 @@ async function generateQRCode(url) {
|
|
|
1702
1818
|
});
|
|
1703
1819
|
}
|
|
1704
1820
|
async function create(options) {
|
|
1705
|
-
const { bundle, passcode,
|
|
1821
|
+
const { bundle, passcode, maxAccesses, label, storage, debug } = options;
|
|
1822
|
+
let expiresAt;
|
|
1823
|
+
if (typeof options.expiresAt === "string") {
|
|
1824
|
+
const preset = options.expiresAt;
|
|
1825
|
+
const durationMs = EXPIRATION_PRESETS[preset];
|
|
1826
|
+
if (durationMs === void 0) {
|
|
1827
|
+
throw new ValidationError(`Unknown expiration preset: "${preset}"`);
|
|
1828
|
+
}
|
|
1829
|
+
expiresAt = durationMs > 0 ? new Date(Date.now() + durationMs) : void 0;
|
|
1830
|
+
} else {
|
|
1831
|
+
expiresAt = options.expiresAt;
|
|
1832
|
+
}
|
|
1706
1833
|
if (!bundle || typeof bundle !== "object") {
|
|
1707
1834
|
throw new ValidationError("bundle is required and must be an object");
|
|
1708
1835
|
}
|
|
1709
1836
|
if (!storage?.baseUrl) {
|
|
1710
1837
|
throw new ValidationError("storage with baseUrl is required");
|
|
1711
1838
|
}
|
|
1839
|
+
let mode = options.mode ?? "manifest";
|
|
1840
|
+
if (options.compliance === "pshd") {
|
|
1841
|
+
mode = "direct";
|
|
1842
|
+
if (passcode) {
|
|
1843
|
+
throw new ValidationError(
|
|
1844
|
+
"PSHD compliance forbids passcode (flag U is incompatible with flag P)"
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
if (!expiresAt && options.expiresAt !== "permanent") {
|
|
1848
|
+
throw new ValidationError(
|
|
1849
|
+
"PSHD compliance requires expiresAt (short-lived links for point-of-care)"
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (mode === "direct" && passcode) {
|
|
1854
|
+
throw new ValidationError(
|
|
1855
|
+
"Direct mode (flag U) is incompatible with passcode (flag P)"
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1712
1858
|
const key = generateKey();
|
|
1713
1859
|
const shlId = generateShlId();
|
|
1714
1860
|
let jwe;
|
|
@@ -1768,31 +1914,34 @@ async function create(options) {
|
|
|
1768
1914
|
);
|
|
1769
1915
|
}
|
|
1770
1916
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1917
|
+
if (mode === "manifest") {
|
|
1918
|
+
const manifest = {
|
|
1919
|
+
files: [
|
|
1920
|
+
{
|
|
1921
|
+
contentType: "application/fhir+json;fhirVersion=4.0.1",
|
|
1922
|
+
location: `${baseUrl}/${shlId}/content`
|
|
1923
|
+
},
|
|
1924
|
+
...attachments.map((att, i) => ({
|
|
1925
|
+
contentType: att.contentType,
|
|
1926
|
+
location: `${baseUrl}/${shlId}/attachment/${i}`
|
|
1927
|
+
}))
|
|
1928
|
+
],
|
|
1929
|
+
status: "finalized",
|
|
1930
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1931
|
+
};
|
|
1932
|
+
try {
|
|
1933
|
+
await storage.store(`${shlId}/manifest.json`, JSON.stringify(manifest));
|
|
1934
|
+
} catch (err) {
|
|
1935
|
+
throw new StorageError(
|
|
1936
|
+
`Failed to store manifest: ${err instanceof Error ? err.message : String(err)}`,
|
|
1937
|
+
"store"
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1792
1940
|
}
|
|
1793
1941
|
const metadata = {
|
|
1794
1942
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1795
1943
|
};
|
|
1944
|
+
if (mode === "direct") metadata.mode = "direct";
|
|
1796
1945
|
if (passcode) {
|
|
1797
1946
|
metadata.passcode = createHash("sha256").update(passcode).digest("hex");
|
|
1798
1947
|
}
|
|
@@ -1806,7 +1955,7 @@ async function create(options) {
|
|
|
1806
1955
|
"store"
|
|
1807
1956
|
);
|
|
1808
1957
|
}
|
|
1809
|
-
const flags = buildFlags(passcode);
|
|
1958
|
+
const flags = buildFlags(mode, passcode);
|
|
1810
1959
|
const shlPayload = {
|
|
1811
1960
|
url: `${baseUrl}/${shlId}`,
|
|
1812
1961
|
key: base64url(key),
|
|
@@ -1833,8 +1982,8 @@ async function create(options) {
|
|
|
1833
1982
|
if (debug) result.debugBundlePath = `${shlId}/bundle.json`;
|
|
1834
1983
|
return result;
|
|
1835
1984
|
}
|
|
1836
|
-
function buildFlags(passcode) {
|
|
1837
|
-
const flags = ["L"];
|
|
1985
|
+
function buildFlags(mode, passcode) {
|
|
1986
|
+
const flags = [mode === "direct" ? "U" : "L"];
|
|
1838
1987
|
if (passcode) flags.push("P");
|
|
1839
1988
|
return flags.sort().join("");
|
|
1840
1989
|
}
|
|
@@ -1941,5 +2090,5 @@ async function revoke(shlId, storage) {
|
|
|
1941
2090
|
}
|
|
1942
2091
|
|
|
1943
2092
|
export { ips_exports, shl_exports };
|
|
1944
|
-
//# sourceMappingURL=chunk-
|
|
1945
|
-
//# sourceMappingURL=chunk-
|
|
2093
|
+
//# sourceMappingURL=chunk-YUMCDN7I.js.map
|
|
2094
|
+
//# sourceMappingURL=chunk-YUMCDN7I.js.map
|