@fhirfly-io/shl 0.3.2 → 0.4.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/dist/{chunk-QXSWM5QV.cjs → chunk-JOZ6XZPO.cjs} +72 -5
- package/dist/chunk-JOZ6XZPO.cjs.map +1 -0
- package/dist/{chunk-ZEE5RXIS.js → chunk-KGEFZQ6W.js} +72 -5
- package/dist/chunk-KGEFZQ6W.js.map +1 -0
- package/dist/{chunk-63Q54EKN.cjs → chunk-SK77O3SG.cjs} +151 -35
- package/dist/chunk-SK77O3SG.cjs.map +1 -0
- package/dist/{chunk-YBDRWUQU.js → chunk-UU434UFQ.js} +151 -35
- package/dist/chunk-UU434UFQ.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 +12 -4
- package/dist/index.d.ts +12 -4
- 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 +2 -2
- package/dist/server.d.cts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -1
- package/dist/{storage-BwszYwFo.d.cts → storage-BbzK-kFf.d.cts} +1 -1
- package/dist/{storage-B3GyJD2y.d.ts → storage-Cx7uXUl8.d.ts} +1 -1
- package/dist/{types-BegxU0wQ.d.ts → types-6Vw5fiat.d.ts} +7 -1
- package/dist/{types-Doq5cGNm.d.ts → types-BLLJeWe_.d.cts} +14 -0
- package/dist/{types-Doq5cGNm.d.cts → types-BLLJeWe_.d.ts} +14 -0
- package/dist/{types-hHf-a3hH.d.cts → types-Cdi4IkC9.d.cts} +7 -1
- 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
|
@@ -19,12 +19,12 @@ function createHandler(config) {
|
|
|
19
19
|
let response;
|
|
20
20
|
if (segments.length === 1 && req.method === "POST") {
|
|
21
21
|
response = await handleManifest(segments[0], req, storage, onAccess);
|
|
22
|
+
} else if (segments.length === 1 && req.method === "GET") {
|
|
23
|
+
response = await handleDirectAccess(segments[0], req, storage, onAccess);
|
|
22
24
|
} else if (segments.length === 2 && segments[1] === "content" && req.method === "GET") {
|
|
23
25
|
response = await handleContent(segments[0], storage);
|
|
24
26
|
} else if (segments.length === 3 && segments[1] === "attachment" && req.method === "GET") {
|
|
25
27
|
response = await handleAttachment(segments[0], segments[2], storage);
|
|
26
|
-
} else if (segments.length === 1 && req.method !== "POST") {
|
|
27
|
-
response = jsonResponse(405, { error: "Method not allowed. Use POST for manifest requests." });
|
|
28
28
|
} else if (segments.length === 2 && segments[1] === "content" && req.method !== "GET") {
|
|
29
29
|
response = jsonResponse(405, { error: "Method not allowed. Use GET for content requests." });
|
|
30
30
|
} else if (segments.length === 3 && segments[1] === "attachment" && req.method !== "GET") {
|
|
@@ -99,10 +99,13 @@ async function handleManifest(shlId, req, storage, onAccess) {
|
|
|
99
99
|
return jsonResponse(404, { error: "SHL not found" });
|
|
100
100
|
}
|
|
101
101
|
if (onAccess) {
|
|
102
|
+
const recipient = typeof req.query?.["recipient"] === "string" ? req.query["recipient"] : void 0;
|
|
102
103
|
const event = {
|
|
103
104
|
shlId,
|
|
104
105
|
accessCount: updatedMetadata.accessCount ?? 1,
|
|
105
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
106
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
107
|
+
mode: "manifest",
|
|
108
|
+
...recipient ? { recipient } : {}
|
|
106
109
|
};
|
|
107
110
|
Promise.resolve(onAccess(event)).catch(() => {
|
|
108
111
|
});
|
|
@@ -111,6 +114,70 @@ async function handleManifest(shlId, req, storage, onAccess) {
|
|
|
111
114
|
const manifest = JSON.parse(manifestStr);
|
|
112
115
|
return jsonResponse(200, manifest);
|
|
113
116
|
}
|
|
117
|
+
async function handleDirectAccess(shlId, req, storage, onAccess) {
|
|
118
|
+
const metadataRaw = await storage.read(`${shlId}/metadata.json`);
|
|
119
|
+
if (metadataRaw === null) {
|
|
120
|
+
return jsonResponse(404, { error: "SHL not found" });
|
|
121
|
+
}
|
|
122
|
+
const metadataStr = typeof metadataRaw === "string" ? metadataRaw : new TextDecoder().decode(metadataRaw);
|
|
123
|
+
const metadata = JSON.parse(metadataStr);
|
|
124
|
+
if (metadata.mode !== "direct") {
|
|
125
|
+
return jsonResponse(405, { error: "Method not allowed. Use POST for manifest requests." });
|
|
126
|
+
}
|
|
127
|
+
let accessDeniedReason = null;
|
|
128
|
+
const updatedMetadata = await storage.updateMetadata(shlId, (current) => {
|
|
129
|
+
if (current.expiresAt) {
|
|
130
|
+
const expiresAt = new Date(current.expiresAt);
|
|
131
|
+
if (expiresAt.getTime() <= Date.now()) {
|
|
132
|
+
accessDeniedReason = "expired";
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const currentCount = current.accessCount ?? 0;
|
|
137
|
+
if (current.maxAccesses !== void 0 && currentCount >= current.maxAccesses) {
|
|
138
|
+
accessDeniedReason = "exhausted";
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
...current,
|
|
143
|
+
accessCount: currentCount + 1
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
if (accessDeniedReason === "expired") {
|
|
147
|
+
return jsonResponse(410, { error: "SHL has expired" });
|
|
148
|
+
}
|
|
149
|
+
if (accessDeniedReason === "exhausted") {
|
|
150
|
+
return jsonResponse(410, { error: "SHL access limit reached" });
|
|
151
|
+
}
|
|
152
|
+
if (updatedMetadata === null) {
|
|
153
|
+
return jsonResponse(404, { error: "SHL not found" });
|
|
154
|
+
}
|
|
155
|
+
if (onAccess) {
|
|
156
|
+
const recipient = typeof req.query?.["recipient"] === "string" ? req.query["recipient"] : void 0;
|
|
157
|
+
const event = {
|
|
158
|
+
shlId,
|
|
159
|
+
accessCount: updatedMetadata.accessCount ?? 1,
|
|
160
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
161
|
+
mode: "direct",
|
|
162
|
+
...recipient ? { recipient } : {}
|
|
163
|
+
};
|
|
164
|
+
Promise.resolve(onAccess(event)).catch(() => {
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const content = await storage.read(`${shlId}/content.jwe`);
|
|
168
|
+
if (content === null) {
|
|
169
|
+
return jsonResponse(404, { error: "Content not found" });
|
|
170
|
+
}
|
|
171
|
+
const body = typeof content === "string" ? content : new TextDecoder().decode(content);
|
|
172
|
+
return {
|
|
173
|
+
status: 200,
|
|
174
|
+
headers: {
|
|
175
|
+
"content-type": "application/jose",
|
|
176
|
+
"cache-control": "no-store"
|
|
177
|
+
},
|
|
178
|
+
body
|
|
179
|
+
};
|
|
180
|
+
}
|
|
114
181
|
async function handleContent(shlId, storage) {
|
|
115
182
|
const content = await storage.read(`${shlId}/content.jwe`);
|
|
116
183
|
if (content === null) {
|
|
@@ -156,5 +223,5 @@ function jsonResponse(status, body) {
|
|
|
156
223
|
}
|
|
157
224
|
|
|
158
225
|
exports.createHandler = createHandler;
|
|
159
|
-
//# sourceMappingURL=chunk-
|
|
160
|
-
//# sourceMappingURL=chunk-
|
|
226
|
+
//# sourceMappingURL=chunk-JOZ6XZPO.cjs.map
|
|
227
|
+
//# sourceMappingURL=chunk-JOZ6XZPO.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/handler.ts"],"names":["createHash","timingSafeEqual"],"mappings":"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","file":"chunk-JOZ6XZPO.cjs","sourcesContent":["// Copyright 2026 FHIRfly.io LLC. All rights reserved.\n// Licensed under the MIT License. See LICENSE file in the project root.\nimport type {\n HandlerRequest,\n HandlerResponse,\n SHLHandlerConfig,\n CorsConfig,\n} from \"./types.js\";\nimport type { SHLMetadata, Manifest } from \"../shl/types.js\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Create a framework-agnostic SHL request handler.\n *\n * Returns an async function that processes incoming requests and returns\n * responses. This handler implements 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 if (onAccess) {\n const recipient = typeof req.query?.[\"recipient\"] === \"string\" ? req.query[\"recipient\"] : undefined;\n const event = {\n shlId,\n accessCount: updatedMetadata.accessCount ?? 1,\n timestamp: new Date(),\n mode: \"manifest\" as const,\n ...(recipient ? { recipient } : {}),\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 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 if (onAccess) {\n const recipient = typeof req.query?.[\"recipient\"] === \"string\" ? req.query[\"recipient\"] : undefined;\n const event = {\n shlId,\n accessCount: updatedMetadata.accessCount ?? 1,\n timestamp: new Date(),\n mode: \"direct\" as const,\n ...(recipient ? { recipient } : {}),\n };\n Promise.resolve(onAccess(event)).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"]}
|
|
@@ -17,12 +17,12 @@ function createHandler(config) {
|
|
|
17
17
|
let response;
|
|
18
18
|
if (segments.length === 1 && req.method === "POST") {
|
|
19
19
|
response = await handleManifest(segments[0], req, storage, onAccess);
|
|
20
|
+
} else if (segments.length === 1 && req.method === "GET") {
|
|
21
|
+
response = await handleDirectAccess(segments[0], req, storage, onAccess);
|
|
20
22
|
} else if (segments.length === 2 && segments[1] === "content" && req.method === "GET") {
|
|
21
23
|
response = await handleContent(segments[0], storage);
|
|
22
24
|
} else if (segments.length === 3 && segments[1] === "attachment" && req.method === "GET") {
|
|
23
25
|
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
26
|
} else if (segments.length === 2 && segments[1] === "content" && req.method !== "GET") {
|
|
27
27
|
response = jsonResponse(405, { error: "Method not allowed. Use GET for content requests." });
|
|
28
28
|
} else if (segments.length === 3 && segments[1] === "attachment" && req.method !== "GET") {
|
|
@@ -97,10 +97,13 @@ async function handleManifest(shlId, req, storage, onAccess) {
|
|
|
97
97
|
return jsonResponse(404, { error: "SHL not found" });
|
|
98
98
|
}
|
|
99
99
|
if (onAccess) {
|
|
100
|
+
const recipient = typeof req.query?.["recipient"] === "string" ? req.query["recipient"] : void 0;
|
|
100
101
|
const event = {
|
|
101
102
|
shlId,
|
|
102
103
|
accessCount: updatedMetadata.accessCount ?? 1,
|
|
103
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
104
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
105
|
+
mode: "manifest",
|
|
106
|
+
...recipient ? { recipient } : {}
|
|
104
107
|
};
|
|
105
108
|
Promise.resolve(onAccess(event)).catch(() => {
|
|
106
109
|
});
|
|
@@ -109,6 +112,70 @@ async function handleManifest(shlId, req, storage, onAccess) {
|
|
|
109
112
|
const manifest = JSON.parse(manifestStr);
|
|
110
113
|
return jsonResponse(200, manifest);
|
|
111
114
|
}
|
|
115
|
+
async function handleDirectAccess(shlId, req, storage, onAccess) {
|
|
116
|
+
const metadataRaw = await storage.read(`${shlId}/metadata.json`);
|
|
117
|
+
if (metadataRaw === null) {
|
|
118
|
+
return jsonResponse(404, { error: "SHL not found" });
|
|
119
|
+
}
|
|
120
|
+
const metadataStr = typeof metadataRaw === "string" ? metadataRaw : new TextDecoder().decode(metadataRaw);
|
|
121
|
+
const metadata = JSON.parse(metadataStr);
|
|
122
|
+
if (metadata.mode !== "direct") {
|
|
123
|
+
return jsonResponse(405, { error: "Method not allowed. Use POST for manifest requests." });
|
|
124
|
+
}
|
|
125
|
+
let accessDeniedReason = null;
|
|
126
|
+
const updatedMetadata = await storage.updateMetadata(shlId, (current) => {
|
|
127
|
+
if (current.expiresAt) {
|
|
128
|
+
const expiresAt = new Date(current.expiresAt);
|
|
129
|
+
if (expiresAt.getTime() <= Date.now()) {
|
|
130
|
+
accessDeniedReason = "expired";
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const currentCount = current.accessCount ?? 0;
|
|
135
|
+
if (current.maxAccesses !== void 0 && currentCount >= current.maxAccesses) {
|
|
136
|
+
accessDeniedReason = "exhausted";
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
...current,
|
|
141
|
+
accessCount: currentCount + 1
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
if (accessDeniedReason === "expired") {
|
|
145
|
+
return jsonResponse(410, { error: "SHL has expired" });
|
|
146
|
+
}
|
|
147
|
+
if (accessDeniedReason === "exhausted") {
|
|
148
|
+
return jsonResponse(410, { error: "SHL access limit reached" });
|
|
149
|
+
}
|
|
150
|
+
if (updatedMetadata === null) {
|
|
151
|
+
return jsonResponse(404, { error: "SHL not found" });
|
|
152
|
+
}
|
|
153
|
+
if (onAccess) {
|
|
154
|
+
const recipient = typeof req.query?.["recipient"] === "string" ? req.query["recipient"] : void 0;
|
|
155
|
+
const event = {
|
|
156
|
+
shlId,
|
|
157
|
+
accessCount: updatedMetadata.accessCount ?? 1,
|
|
158
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
159
|
+
mode: "direct",
|
|
160
|
+
...recipient ? { recipient } : {}
|
|
161
|
+
};
|
|
162
|
+
Promise.resolve(onAccess(event)).catch(() => {
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const content = await storage.read(`${shlId}/content.jwe`);
|
|
166
|
+
if (content === null) {
|
|
167
|
+
return jsonResponse(404, { error: "Content not found" });
|
|
168
|
+
}
|
|
169
|
+
const body = typeof content === "string" ? content : new TextDecoder().decode(content);
|
|
170
|
+
return {
|
|
171
|
+
status: 200,
|
|
172
|
+
headers: {
|
|
173
|
+
"content-type": "application/jose",
|
|
174
|
+
"cache-control": "no-store"
|
|
175
|
+
},
|
|
176
|
+
body
|
|
177
|
+
};
|
|
178
|
+
}
|
|
112
179
|
async function handleContent(shlId, storage) {
|
|
113
180
|
const content = await storage.read(`${shlId}/content.jwe`);
|
|
114
181
|
if (content === null) {
|
|
@@ -154,5 +221,5 @@ function jsonResponse(status, body) {
|
|
|
154
221
|
}
|
|
155
222
|
|
|
156
223
|
export { createHandler };
|
|
157
|
-
//# sourceMappingURL=chunk-
|
|
158
|
-
//# sourceMappingURL=chunk-
|
|
224
|
+
//# sourceMappingURL=chunk-KGEFZQ6W.js.map
|
|
225
|
+
//# sourceMappingURL=chunk-KGEFZQ6W.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/handler.ts"],"names":[],"mappings":"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","file":"chunk-KGEFZQ6W.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 CorsConfig,\n} from \"./types.js\";\nimport type { SHLMetadata, Manifest } from \"../shl/types.js\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Create a framework-agnostic SHL request handler.\n *\n * Returns an async function that processes incoming requests and returns\n * responses. This handler implements 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 if (onAccess) {\n const recipient = typeof req.query?.[\"recipient\"] === \"string\" ? req.query[\"recipient\"] : undefined;\n const event = {\n shlId,\n accessCount: updatedMetadata.accessCount ?? 1,\n timestamp: new Date(),\n mode: \"manifest\" as const,\n ...(recipient ? { recipient } : {}),\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 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 if (onAccess) {\n const recipient = typeof req.query?.[\"recipient\"] === \"string\" ? req.query[\"recipient\"] : undefined;\n const event = {\n shlId,\n accessCount: updatedMetadata.accessCount ?? 1,\n timestamp: new Date(),\n mode: \"direct\" as const,\n ...(recipient ? { recipient } : {}),\n };\n Promise.resolve(onAccess(event)).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"]}
|
|
@@ -180,7 +180,11 @@ var CODE_SYSTEMS = {
|
|
|
180
180
|
CVX: "http://hl7.org/fhir/sid/cvx",
|
|
181
181
|
ICD10CM: "http://hl7.org/fhir/sid/icd-10-cm",
|
|
182
182
|
CONDITION_CLINICAL: "http://terminology.hl7.org/CodeSystem/condition-clinical",
|
|
183
|
-
ALLERGY_CLINICAL: "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical"
|
|
183
|
+
ALLERGY_CLINICAL: "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical",
|
|
184
|
+
/** CMS Patient-Shared Health Document category code system */
|
|
185
|
+
CMS_PATIENT_SHARED_CATEGORY: "https://cms.gov/fhir/CodeSystem/patient-shared-category",
|
|
186
|
+
/** V3 ActCode security label for patient-asserted data */
|
|
187
|
+
SECURITY_PATAST: "PATAST"
|
|
184
188
|
};
|
|
185
189
|
|
|
186
190
|
// src/ips/medication.ts
|
|
@@ -1106,8 +1110,9 @@ function resolveDocuments(documents, patientRef, profile, generateUuid2) {
|
|
|
1106
1110
|
contentType,
|
|
1107
1111
|
data: base64Content
|
|
1108
1112
|
};
|
|
1109
|
-
const
|
|
1110
|
-
const
|
|
1113
|
+
const isPshd = profile === "pshd";
|
|
1114
|
+
const typeCode = isPshd ? "60591-5" : doc.typeCode ?? "34133-9";
|
|
1115
|
+
const typeDisplay = isPshd ? "Patient summary Document" : doc.typeDisplay ?? "Summarization of episode note";
|
|
1111
1116
|
const docRefResource = {
|
|
1112
1117
|
resourceType: "DocumentReference",
|
|
1113
1118
|
id: docRefId,
|
|
@@ -1133,7 +1138,29 @@ function resolveDocuments(documents, patientRef, profile, generateUuid2) {
|
|
|
1133
1138
|
}
|
|
1134
1139
|
]
|
|
1135
1140
|
};
|
|
1136
|
-
if (
|
|
1141
|
+
if (isPshd) {
|
|
1142
|
+
docRefResource.category = [
|
|
1143
|
+
{
|
|
1144
|
+
coding: [
|
|
1145
|
+
{
|
|
1146
|
+
system: CODE_SYSTEMS.CMS_PATIENT_SHARED_CATEGORY,
|
|
1147
|
+
code: "patient-shared",
|
|
1148
|
+
display: "Patient Shared"
|
|
1149
|
+
}
|
|
1150
|
+
]
|
|
1151
|
+
}
|
|
1152
|
+
];
|
|
1153
|
+
docRefResource.author = [{ reference: patientRef }];
|
|
1154
|
+
docRefResource.meta = {
|
|
1155
|
+
security: [
|
|
1156
|
+
{
|
|
1157
|
+
system: "http://terminology.hl7.org/CodeSystem/v3-ActCode",
|
|
1158
|
+
code: CODE_SYSTEMS.SECURITY_PATAST,
|
|
1159
|
+
display: "patient asserted"
|
|
1160
|
+
}
|
|
1161
|
+
]
|
|
1162
|
+
};
|
|
1163
|
+
} else if (profile === "ips") {
|
|
1137
1164
|
docRefResource.meta = {
|
|
1138
1165
|
profile: ["http://hl7.org/fhir/uv/ips/StructureDefinition/DocumentReference-uv-ips"]
|
|
1139
1166
|
};
|
|
@@ -1332,11 +1359,9 @@ var Bundle = class {
|
|
|
1332
1359
|
async build(options) {
|
|
1333
1360
|
const profile = options?.profile ?? "ips";
|
|
1334
1361
|
const bundleId = options?.bundleId ?? generateUuid();
|
|
1335
|
-
const compositionId = generateUuid();
|
|
1336
1362
|
const patientId = generateUuid();
|
|
1337
|
-
const
|
|
1363
|
+
const timestamp = options?.compositionDate ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1338
1364
|
const patientFullUrl = `urn:uuid:${patientId}`;
|
|
1339
|
-
const compositionFullUrl = `urn:uuid:${compositionId}`;
|
|
1340
1365
|
const patientResource = normalizePatient(this._patient, patientId, profile);
|
|
1341
1366
|
const [medResult, condResult, allergyResult, immResult, resultResult] = await Promise.all([
|
|
1342
1367
|
resolveMedications(this._medications, patientFullUrl, profile, generateUuid),
|
|
@@ -1353,6 +1378,22 @@ var Bundle = class {
|
|
|
1353
1378
|
...immResult.warnings,
|
|
1354
1379
|
...resultResult.warnings
|
|
1355
1380
|
];
|
|
1381
|
+
if (profile === "pshd") {
|
|
1382
|
+
return this.buildPshdBundle(
|
|
1383
|
+
bundleId,
|
|
1384
|
+
timestamp,
|
|
1385
|
+
patientFullUrl,
|
|
1386
|
+
patientResource,
|
|
1387
|
+
medResult.entries,
|
|
1388
|
+
condResult.entries,
|
|
1389
|
+
allergyResult.entries,
|
|
1390
|
+
immResult.entries,
|
|
1391
|
+
resultResult.entries,
|
|
1392
|
+
docResult.entries
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
const compositionId = generateUuid();
|
|
1396
|
+
const compositionFullUrl = `urn:uuid:${compositionId}`;
|
|
1356
1397
|
const medRefs = medResult.entries.map((e) => ({ reference: e.fullUrl }));
|
|
1357
1398
|
const condRefs = condResult.entries.map((e) => ({ reference: e.fullUrl }));
|
|
1358
1399
|
const allergyRefs = allergyResult.entries.map((e) => ({ reference: e.fullUrl }));
|
|
@@ -1361,7 +1402,7 @@ var Bundle = class {
|
|
|
1361
1402
|
const composition = this.buildComposition(
|
|
1362
1403
|
compositionId,
|
|
1363
1404
|
patientFullUrl,
|
|
1364
|
-
|
|
1405
|
+
timestamp,
|
|
1365
1406
|
profile,
|
|
1366
1407
|
medRefs,
|
|
1367
1408
|
allergyRefs,
|
|
@@ -1387,7 +1428,7 @@ var Bundle = class {
|
|
|
1387
1428
|
value: `urn:uuid:${bundleId}`
|
|
1388
1429
|
},
|
|
1389
1430
|
type: "document",
|
|
1390
|
-
timestamp
|
|
1431
|
+
timestamp,
|
|
1391
1432
|
entry: entries
|
|
1392
1433
|
};
|
|
1393
1434
|
return bundle;
|
|
@@ -1407,6 +1448,37 @@ var Bundle = class {
|
|
|
1407
1448
|
path: "Patient.birthDate"
|
|
1408
1449
|
});
|
|
1409
1450
|
}
|
|
1451
|
+
if (profile === "pshd") {
|
|
1452
|
+
if (this._documents.length === 0) {
|
|
1453
|
+
issues.push({
|
|
1454
|
+
severity: "error",
|
|
1455
|
+
message: "PSHD requires at least one DocumentReference (1..1)",
|
|
1456
|
+
path: "Bundle.entry:DocumentReference"
|
|
1457
|
+
});
|
|
1458
|
+
} else {
|
|
1459
|
+
const hasPdf = this._documents.some(
|
|
1460
|
+
(d) => (d.contentType ?? "application/pdf") === "application/pdf"
|
|
1461
|
+
);
|
|
1462
|
+
if (!hasPdf) {
|
|
1463
|
+
issues.push({
|
|
1464
|
+
severity: "error",
|
|
1465
|
+
message: "PSHD requires at least one PDF document (contentType application/pdf)",
|
|
1466
|
+
path: "DocumentReference.content.attachment.contentType"
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (!this._patient.gender) {
|
|
1471
|
+
issues.push({
|
|
1472
|
+
severity: "warning",
|
|
1473
|
+
message: "Patient.gender recommended for PSHD demographic matching",
|
|
1474
|
+
path: "Patient.gender"
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
return {
|
|
1478
|
+
valid: issues.filter((i) => i.severity === "error").length === 0,
|
|
1479
|
+
issues
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1410
1482
|
if (profile === "ips") {
|
|
1411
1483
|
if (!this.hasValidName()) {
|
|
1412
1484
|
issues.push({
|
|
@@ -1482,6 +1554,28 @@ var Bundle = class {
|
|
|
1482
1554
|
const s = this._patient;
|
|
1483
1555
|
return !!(s.given || s.family || s.name);
|
|
1484
1556
|
}
|
|
1557
|
+
buildPshdBundle(bundleId, timestamp, patientFullUrl, patientResource, medEntries, condEntries, allergyEntries, immEntries, resultEntries, docEntries) {
|
|
1558
|
+
const entries = [
|
|
1559
|
+
{ fullUrl: patientFullUrl, resource: patientResource },
|
|
1560
|
+
...medEntries,
|
|
1561
|
+
...condEntries,
|
|
1562
|
+
...allergyEntries,
|
|
1563
|
+
...immEntries,
|
|
1564
|
+
...resultEntries,
|
|
1565
|
+
...docEntries
|
|
1566
|
+
];
|
|
1567
|
+
return {
|
|
1568
|
+
resourceType: "Bundle",
|
|
1569
|
+
id: bundleId,
|
|
1570
|
+
identifier: {
|
|
1571
|
+
system: "urn:ietf:rfc:3986",
|
|
1572
|
+
value: `urn:uuid:${bundleId}`
|
|
1573
|
+
},
|
|
1574
|
+
type: "collection",
|
|
1575
|
+
timestamp,
|
|
1576
|
+
entry: entries
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1485
1579
|
buildComposition(id, patientRef, date, profile, medRefs, allergyRefs, condRefs, immRefs, resultRefs) {
|
|
1486
1580
|
const composition = {
|
|
1487
1581
|
resourceType: "Composition",
|
|
@@ -1715,6 +1809,25 @@ async function create(options) {
|
|
|
1715
1809
|
if (!storage?.baseUrl) {
|
|
1716
1810
|
throw new chunkUDS6UJAL_cjs.ValidationError("storage with baseUrl is required");
|
|
1717
1811
|
}
|
|
1812
|
+
let mode = options.mode ?? "manifest";
|
|
1813
|
+
if (options.compliance === "pshd") {
|
|
1814
|
+
mode = "direct";
|
|
1815
|
+
if (passcode) {
|
|
1816
|
+
throw new chunkUDS6UJAL_cjs.ValidationError(
|
|
1817
|
+
"PSHD compliance forbids passcode (flag U is incompatible with flag P)"
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
if (!expiresAt) {
|
|
1821
|
+
throw new chunkUDS6UJAL_cjs.ValidationError(
|
|
1822
|
+
"PSHD compliance requires expiresAt (short-lived links for point-of-care)"
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (mode === "direct" && passcode) {
|
|
1827
|
+
throw new chunkUDS6UJAL_cjs.ValidationError(
|
|
1828
|
+
"Direct mode (flag U) is incompatible with passcode (flag P)"
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1718
1831
|
const key = generateKey();
|
|
1719
1832
|
const shlId = generateShlId();
|
|
1720
1833
|
let jwe;
|
|
@@ -1774,31 +1887,34 @@ async function create(options) {
|
|
|
1774
1887
|
);
|
|
1775
1888
|
}
|
|
1776
1889
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1890
|
+
if (mode === "manifest") {
|
|
1891
|
+
const manifest = {
|
|
1892
|
+
files: [
|
|
1893
|
+
{
|
|
1894
|
+
contentType: "application/fhir+json;fhirVersion=4.0.1",
|
|
1895
|
+
location: `${baseUrl}/${shlId}/content`
|
|
1896
|
+
},
|
|
1897
|
+
...attachments.map((att, i) => ({
|
|
1898
|
+
contentType: att.contentType,
|
|
1899
|
+
location: `${baseUrl}/${shlId}/attachment/${i}`
|
|
1900
|
+
}))
|
|
1901
|
+
],
|
|
1902
|
+
status: "finalized",
|
|
1903
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1904
|
+
};
|
|
1905
|
+
try {
|
|
1906
|
+
await storage.store(`${shlId}/manifest.json`, JSON.stringify(manifest));
|
|
1907
|
+
} catch (err) {
|
|
1908
|
+
throw new chunkUDS6UJAL_cjs.StorageError(
|
|
1909
|
+
`Failed to store manifest: ${err instanceof Error ? err.message : String(err)}`,
|
|
1910
|
+
"store"
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1798
1913
|
}
|
|
1799
1914
|
const metadata = {
|
|
1800
1915
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1801
1916
|
};
|
|
1917
|
+
if (mode === "direct") metadata.mode = "direct";
|
|
1802
1918
|
if (passcode) {
|
|
1803
1919
|
metadata.passcode = crypto$1.createHash("sha256").update(passcode).digest("hex");
|
|
1804
1920
|
}
|
|
@@ -1812,7 +1928,7 @@ async function create(options) {
|
|
|
1812
1928
|
"store"
|
|
1813
1929
|
);
|
|
1814
1930
|
}
|
|
1815
|
-
const flags = buildFlags(passcode);
|
|
1931
|
+
const flags = buildFlags(mode, passcode);
|
|
1816
1932
|
const shlPayload = {
|
|
1817
1933
|
url: `${baseUrl}/${shlId}`,
|
|
1818
1934
|
key: base64url(key),
|
|
@@ -1839,8 +1955,8 @@ async function create(options) {
|
|
|
1839
1955
|
if (debug) result.debugBundlePath = `${shlId}/bundle.json`;
|
|
1840
1956
|
return result;
|
|
1841
1957
|
}
|
|
1842
|
-
function buildFlags(passcode) {
|
|
1843
|
-
const flags = ["L"];
|
|
1958
|
+
function buildFlags(mode, passcode) {
|
|
1959
|
+
const flags = [mode === "direct" ? "U" : "L"];
|
|
1844
1960
|
if (passcode) flags.push("P");
|
|
1845
1961
|
return flags.sort().join("");
|
|
1846
1962
|
}
|
|
@@ -1948,5 +2064,5 @@ async function revoke(shlId, storage) {
|
|
|
1948
2064
|
|
|
1949
2065
|
exports.ips_exports = ips_exports;
|
|
1950
2066
|
exports.shl_exports = shl_exports;
|
|
1951
|
-
//# sourceMappingURL=chunk-
|
|
1952
|
-
//# sourceMappingURL=chunk-
|
|
2067
|
+
//# sourceMappingURL=chunk-SK77O3SG.cjs.map
|
|
2068
|
+
//# sourceMappingURL=chunk-SK77O3SG.cjs.map
|