@fhirfly-io/shl 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +53 -0
  2. package/dist/{chunk-SK77O3SG.cjs → chunk-CN44QKWJ.cjs} +37 -4
  3. package/dist/chunk-CN44QKWJ.cjs.map +1 -0
  4. package/dist/{chunk-JOZ6XZPO.cjs → chunk-H37YQWF2.cjs} +33 -21
  5. package/dist/chunk-H37YQWF2.cjs.map +1 -0
  6. package/dist/{chunk-KGEFZQ6W.js → chunk-IYRQRY4A.js} +33 -22
  7. package/dist/chunk-IYRQRY4A.js.map +1 -0
  8. package/dist/{chunk-UU434UFQ.js → chunk-YUMCDN7I.js} +37 -4
  9. package/dist/chunk-YUMCDN7I.js.map +1 -0
  10. package/dist/cli.cjs +11 -11
  11. package/dist/cli.js +2 -2
  12. package/dist/express.cjs +2 -2
  13. package/dist/express.d.cts +2 -2
  14. package/dist/express.d.ts +2 -2
  15. package/dist/express.js +1 -1
  16. package/dist/fastify.cjs +2 -2
  17. package/dist/fastify.d.cts +2 -2
  18. package/dist/fastify.d.ts +2 -2
  19. package/dist/fastify.js +1 -1
  20. package/dist/index.cjs +3 -3
  21. package/dist/index.d.cts +5 -3
  22. package/dist/index.d.ts +5 -3
  23. package/dist/index.js +1 -1
  24. package/dist/lambda.cjs +2 -2
  25. package/dist/lambda.d.cts +2 -2
  26. package/dist/lambda.d.ts +2 -2
  27. package/dist/lambda.js +1 -1
  28. package/dist/server.cjs +6 -2
  29. package/dist/server.d.cts +4 -4
  30. package/dist/server.d.ts +4 -4
  31. package/dist/server.js +1 -1
  32. package/dist/{storage-Cx7uXUl8.d.ts → storage-CvsOM1Eu.d.ts} +1 -1
  33. package/dist/{storage-BbzK-kFf.d.cts → storage-DggeMhOI.d.cts} +1 -1
  34. package/dist/{types-6Vw5fiat.d.ts → types--SjcaaWT.d.ts} +20 -2
  35. package/dist/{types-Cdi4IkC9.d.cts → types-BcfxBDTA.d.cts} +20 -2
  36. package/dist/{types-BLLJeWe_.d.ts → types-CmeXnyth.d.cts} +17 -3
  37. package/dist/{types-BLLJeWe_.d.cts → types-CmeXnyth.d.ts} +17 -3
  38. package/package.json +3 -3
  39. package/dist/chunk-JOZ6XZPO.cjs.map +0 -1
  40. package/dist/chunk-KGEFZQ6W.js.map +0 -1
  41. package/dist/chunk-SK77O3SG.cjs.map +0 -1
  42. package/dist/chunk-UU434UFQ.js.map +0 -1
@@ -2,7 +2,10 @@
2
2
 
3
3
  var crypto = require('crypto');
4
4
 
5
- // src/server/handler.ts
5
+ // src/server/types.ts
6
+ function isAuditableStorage(storage) {
7
+ return typeof storage.onAccess === "function";
8
+ }
6
9
  function createHandler(config) {
7
10
  const { storage, onAccess } = config;
8
11
  const corsHeaders = resolveCorsHeaders(config.cors);
@@ -98,16 +101,20 @@ async function handleManifest(shlId, req, storage, onAccess) {
98
101
  if (updatedMetadata === null) {
99
102
  return jsonResponse(404, { error: "SHL not found" });
100
103
  }
104
+ const recipient = typeof req.query?.["recipient"] === "string" ? req.query["recipient"] : void 0;
105
+ const accessEvent = {
106
+ shlId,
107
+ accessCount: updatedMetadata.accessCount ?? 1,
108
+ timestamp: /* @__PURE__ */ new Date(),
109
+ mode: "manifest",
110
+ ...recipient ? { recipient } : {}
111
+ };
101
112
  if (onAccess) {
102
- const recipient = typeof req.query?.["recipient"] === "string" ? req.query["recipient"] : void 0;
103
- const event = {
104
- shlId,
105
- accessCount: updatedMetadata.accessCount ?? 1,
106
- timestamp: /* @__PURE__ */ new Date(),
107
- mode: "manifest",
108
- ...recipient ? { recipient } : {}
109
- };
110
- Promise.resolve(onAccess(event)).catch(() => {
113
+ Promise.resolve(onAccess(accessEvent)).catch(() => {
114
+ });
115
+ }
116
+ if (isAuditableStorage(storage)) {
117
+ Promise.resolve(storage.onAccess(shlId, accessEvent)).catch(() => {
111
118
  });
112
119
  }
113
120
  const manifestStr = typeof manifestRaw === "string" ? manifestRaw : new TextDecoder().decode(manifestRaw);
@@ -152,16 +159,20 @@ async function handleDirectAccess(shlId, req, storage, onAccess) {
152
159
  if (updatedMetadata === null) {
153
160
  return jsonResponse(404, { error: "SHL not found" });
154
161
  }
162
+ const recipient = typeof req.query?.["recipient"] === "string" ? req.query["recipient"] : void 0;
163
+ const directAccessEvent = {
164
+ shlId,
165
+ accessCount: updatedMetadata.accessCount ?? 1,
166
+ timestamp: /* @__PURE__ */ new Date(),
167
+ mode: "direct",
168
+ ...recipient ? { recipient } : {}
169
+ };
155
170
  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(() => {
171
+ Promise.resolve(onAccess(directAccessEvent)).catch(() => {
172
+ });
173
+ }
174
+ if (isAuditableStorage(storage)) {
175
+ Promise.resolve(storage.onAccess(shlId, directAccessEvent)).catch(() => {
165
176
  });
166
177
  }
167
178
  const content = await storage.read(`${shlId}/content.jwe`);
@@ -223,5 +234,6 @@ function jsonResponse(status, body) {
223
234
  }
224
235
 
225
236
  exports.createHandler = createHandler;
226
- //# sourceMappingURL=chunk-JOZ6XZPO.cjs.map
227
- //# sourceMappingURL=chunk-JOZ6XZPO.cjs.map
237
+ exports.isAuditableStorage = isAuditableStorage;
238
+ //# sourceMappingURL=chunk-H37YQWF2.cjs.map
239
+ //# sourceMappingURL=chunk-H37YQWF2.cjs.map
@@ -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/handler.ts
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);
@@ -96,16 +99,20 @@ 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
- const recipient = typeof req.query?.["recipient"] === "string" ? req.query["recipient"] : void 0;
101
- const event = {
102
- shlId,
103
- accessCount: updatedMetadata.accessCount ?? 1,
104
- timestamp: /* @__PURE__ */ new Date(),
105
- mode: "manifest",
106
- ...recipient ? { recipient } : {}
107
- };
108
- 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(() => {
109
116
  });
110
117
  }
111
118
  const manifestStr = typeof manifestRaw === "string" ? manifestRaw : new TextDecoder().decode(manifestRaw);
@@ -150,16 +157,20 @@ async function handleDirectAccess(shlId, req, storage, onAccess) {
150
157
  if (updatedMetadata === null) {
151
158
  return jsonResponse(404, { error: "SHL not found" });
152
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
+ };
153
168
  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(() => {
169
+ Promise.resolve(onAccess(directAccessEvent)).catch(() => {
170
+ });
171
+ }
172
+ if (isAuditableStorage(storage)) {
173
+ Promise.resolve(storage.onAccess(shlId, directAccessEvent)).catch(() => {
163
174
  });
164
175
  }
165
176
  const content = await storage.read(`${shlId}/content.jwe`);
@@ -220,6 +231,6 @@ function jsonResponse(status, body) {
220
231
  };
221
232
  }
222
233
 
223
- export { createHandler };
224
- //# sourceMappingURL=chunk-KGEFZQ6W.js.map
225
- //# sourceMappingURL=chunk-KGEFZQ6W.js.map
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":";;;AA6EO,SAAS,mBAAmB,OAAA,EAAwD;AACzF,EAAA,OAAO,OAAQ,QAA6B,QAAA,KAAa,UAAA;AAC3D;ACxCO,SAAS,cACd,MAAA,EACmD;AACnD,EAAA,MAAM,EAAE,OAAA,EAAS,QAAA,EAAS,GAAI,MAAA;AAC9B,EAAA,MAAM,WAAA,GAAc,kBAAA,CAAmB,MAAA,CAAO,IAAI,CAAA;AAElD,EAAA,OAAO,OAAO,GAAA,KAAkD;AAE9D,IAAA,IAAI,GAAA,CAAI,WAAW,SAAA,EAAW;AAC5B,MAAA,OAAO;AAAA,QACL,MAAA,EAAQ,GAAA;AAAA,QACR,OAAA,EAAS,EAAE,GAAG,WAAA,EAAY;AAAA,QAC1B,IAAA,EAAM;AAAA,OACR;AAAA,IACF;AAGA,IAAA,MAAM,IAAA,GAAO,GAAA,CAAI,IAAA,CAAK,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACxC,IAAA,MAAM,WAAW,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,OAAO,OAAO,CAAA;AAE/C,IAAA,IAAI,QAAA;AAGJ,IAAA,IAAI,QAAA,CAAS,MAAA,KAAW,CAAA,IAAK,GAAA,CAAI,WAAW,MAAA,EAAQ;AAClD,MAAA,QAAA,GAAW,MAAM,cAAA,CAAe,QAAA,CAAS,CAAC,CAAA,EAAI,GAAA,EAAK,SAAS,QAAQ,CAAA;AAAA,IACtE,WAES,QAAA,CAAS,MAAA,KAAW,CAAA,IAAK,GAAA,CAAI,WAAW,KAAA,EAAO;AACtD,MAAA,QAAA,GAAW,MAAM,kBAAA,CAAmB,QAAA,CAAS,CAAC,CAAA,EAAI,GAAA,EAAK,SAAS,QAAQ,CAAA;AAAA,IAC1E,CAAA,MAAA,IAES,QAAA,CAAS,MAAA,KAAW,CAAA,IAAK,QAAA,CAAS,CAAC,CAAA,KAAM,SAAA,IAAa,GAAA,CAAI,MAAA,KAAW,KAAA,EAAO;AACnF,MAAA,QAAA,GAAW,MAAM,aAAA,CAAc,QAAA,CAAS,CAAC,GAAI,OAAO,CAAA;AAAA,IACtD,CAAA,MAAA,IAES,QAAA,CAAS,MAAA,KAAW,CAAA,IAAK,QAAA,CAAS,CAAC,CAAA,KAAM,YAAA,IAAgB,GAAA,CAAI,MAAA,KAAW,KAAA,EAAO;AACtF,MAAA,QAAA,GAAW,MAAM,iBAAiB,QAAA,CAAS,CAAC,GAAI,QAAA,CAAS,CAAC,GAAI,OAAO,CAAA;AAAA,IACvE,CAAA,MAAA,IAES,QAAA,CAAS,MAAA,KAAW,CAAA,IAAK,QAAA,CAAS,CAAC,CAAA,KAAM,SAAA,IAAa,GAAA,CAAI,MAAA,KAAW,KAAA,EAAO;AACnF,MAAA,QAAA,GAAW,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,qDAAqD,CAAA;AAAA,IAC7F,CAAA,MAAA,IACS,QAAA,CAAS,MAAA,KAAW,CAAA,IAAK,QAAA,CAAS,CAAC,CAAA,KAAM,YAAA,IAAgB,GAAA,CAAI,MAAA,KAAW,KAAA,EAAO;AACtF,MAAA,QAAA,GAAW,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,wDAAwD,CAAA;AAAA,IAChG,CAAA,MACK;AACH,MAAA,QAAA,GAAW,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,aAAa,CAAA;AAAA,IACrD;AAGA,IAAA,QAAA,CAAS,UAAU,EAAE,GAAG,QAAA,CAAS,OAAA,EAAS,GAAG,WAAA,EAAY;AACzD,IAAA,OAAO,QAAA;AAAA,EACT,CAAA;AACF;AAGA,SAAS,mBAAmB,IAAA,EAAwD;AAClF,EAAA,IAAI,IAAA,KAAS,KAAA,EAAO,OAAO,EAAC;AAC5B,EAAA,MAAM,CAAA,GAAgB,QAAQ,EAAC;AAC/B,EAAA,OAAO;AAAA,IACL,6BAAA,EAA+B,EAAE,MAAA,IAAU,GAAA;AAAA,IAC3C,8BAAA,EAAgC,EAAE,OAAA,IAAW,oBAAA;AAAA,IAC7C,8BAAA,EAAgC,EAAE,OAAA,IAAW;AAAA,GAC/C;AACF;AAEA,eAAe,cAAA,CACb,KAAA,EACA,GAAA,EACA,OAAA,EACA,QAAA,EAC0B;AAE1B,EAAA,MAAM,cAAc,MAAM,OAAA,CAAQ,IAAA,CAAK,CAAA,EAAG,KAAK,CAAA,cAAA,CAAgB,CAAA;AAC/D,EAAA,IAAI,gBAAgB,IAAA,EAAM;AACxB,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,iBAAiB,CAAA;AAAA,EACrD;AAGA,EAAA,IAAI,eAAA,GAAsC,IAAA;AAC1C,EAAA,IAAI,kBAAA,GAAkE,IAAA;AACtE,EAAA,MAAM,OAAA,GAAW,IAAI,IAAA,IAAQ,OAAO,IAAI,IAAA,KAAS,QAAA,GAAW,GAAA,CAAI,IAAA,GAAO,EAAC;AACxE,EAAA,MAAM,gBAAA,GAAmB,OAAO,OAAA,CAAQ,UAAU,MAAM,QAAA,GAAW,OAAA,CAAQ,UAAU,CAAA,GAAI,MAAA;AAEzF,EAAA,eAAA,GAAkB,MAAM,OAAA,CAAQ,cAAA,CAAe,KAAA,EAAO,CAAC,QAAA,KAAa;AAElE,IAAA,IAAI,SAAS,SAAA,EAAW;AACtB,MAAA,MAAM,SAAA,GAAY,IAAI,IAAA,CAAK,QAAA,CAAS,SAAS,CAAA;AAC7C,MAAA,IAAI,SAAA,CAAU,OAAA,EAAQ,IAAK,IAAA,CAAK,KAAI,EAAG;AACrC,QAAA,kBAAA,GAAqB,SAAA;AACrB,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF;AAGA,IAAA,MAAM,YAAA,GAAe,SAAS,WAAA,IAAe,CAAA;AAC7C,IAAA,IAAI,QAAA,CAAS,WAAA,KAAgB,MAAA,IAAa,YAAA,IAAgB,SAAS,WAAA,EAAa;AAC9E,MAAA,kBAAA,GAAqB,WAAA;AACrB,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,IAAI,SAAS,QAAA,EAAU;AACrB,MAAA,IAAI,CAAC,gBAAA,EAAkB;AACrB,QAAA,kBAAA,GAAqB,UAAA;AACrB,QAAA,OAAO,IAAA;AAAA,MACT;AACA,MAAA,MAAM,YAAA,GAAe,WAAW,QAAQ,CAAA,CAAE,OAAO,gBAAgB,CAAA,CAAE,OAAO,KAAK,CAAA;AAC/E,MAAA,MAAM,aAAa,QAAA,CAAS,QAAA;AAC5B,MAAA,MAAM,CAAA,GAAI,MAAA,CAAO,IAAA,CAAK,YAAY,CAAA;AAClC,MAAA,MAAM,CAAA,GAAI,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA;AAEhC,MAAA,IAAI,CAAA,CAAE,WAAW,CAAA,CAAE,MAAA,IAAU,CAAC,eAAA,CAAgB,CAAA,EAAG,CAAC,CAAA,EAAG;AACnD,QAAA,kBAAA,GAAqB,UAAA;AACrB,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF;AAGA,IAAA,OAAO;AAAA,MACL,GAAG,QAAA;AAAA,MACH,aAAa,YAAA,GAAe;AAAA,KAC9B;AAAA,EACF,CAAC,CAAA;AAGD,EAAA,IAAI,uBAAuB,SAAA,EAAW;AACpC,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,mBAAmB,CAAA;AAAA,EACvD;AACA,EAAA,IAAI,uBAAuB,WAAA,EAAa;AACtC,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,4BAA4B,CAAA;AAAA,EAChE;AACA,EAAA,IAAI,uBAAuB,UAAA,EAAY;AACrC,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,oBAAoB,CAAA;AAAA,EACxD;AAGA,EAAA,IAAI,oBAAoB,IAAA,EAAM;AAC5B,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,iBAAiB,CAAA;AAAA,EACrD;AAGA,EAAA,MAAM,SAAA,GAAY,OAAO,GAAA,CAAI,KAAA,GAAQ,WAAW,MAAM,QAAA,GAAW,GAAA,CAAI,KAAA,CAAM,WAAW,CAAA,GAAI,MAAA;AAC1F,EAAA,MAAM,WAAA,GAAc;AAAA,IAClB,KAAA;AAAA,IACA,WAAA,EAAa,gBAAgB,WAAA,IAAe,CAAA;AAAA,IAC5C,SAAA,sBAAe,IAAA,EAAK;AAAA,IACpB,IAAA,EAAM,UAAA;AAAA,IACN,GAAI,SAAA,GAAY,EAAE,SAAA,KAAc;AAAC,GACnC;AACA,EAAA,IAAI,QAAA,EAAU;AAEZ,IAAA,OAAA,CAAQ,QAAQ,QAAA,CAAS,WAAW,CAAC,CAAA,CAAE,MAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AAAA,EACvD;AAEA,EAAA,IAAI,kBAAA,CAAmB,OAAO,CAAA,EAAG;AAC/B,IAAA,OAAA,CAAQ,OAAA,CAAQ,QAAQ,QAAA,CAAS,KAAA,EAAO,WAAW,CAAC,CAAA,CAAE,MAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AAAA,EACtE;AAGA,EAAA,MAAM,WAAA,GAAc,OAAO,WAAA,KAAgB,QAAA,GACvC,cACA,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,WAAW,CAAA;AACxC,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,WAAW,CAAA;AAEvC,EAAA,OAAO,YAAA,CAAa,KAAK,QAAQ,CAAA;AACnC;AAEA,eAAe,kBAAA,CACb,KAAA,EACA,GAAA,EACA,OAAA,EACA,QAAA,EAC0B;AAE1B,EAAA,MAAM,cAAc,MAAM,OAAA,CAAQ,IAAA,CAAK,CAAA,EAAG,KAAK,CAAA,cAAA,CAAgB,CAAA;AAC/D,EAAA,IAAI,gBAAgB,IAAA,EAAM;AACxB,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,iBAAiB,CAAA;AAAA,EACrD;AAEA,EAAA,MAAM,WAAA,GAAc,OAAO,WAAA,KAAgB,QAAA,GAAW,cAAc,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,WAAW,CAAA;AACxG,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,WAAW,CAAA;AAGvC,EAAA,IAAI,QAAA,CAAS,SAAS,QAAA,EAAU;AAC9B,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,uDAAuD,CAAA;AAAA,EAC3F;AAGA,EAAA,IAAI,kBAAA,GAAqD,IAAA;AACzD,EAAA,MAAM,kBAAkB,MAAM,OAAA,CAAQ,cAAA,CAAe,KAAA,EAAO,CAAC,OAAA,KAAY;AACvE,IAAA,IAAI,QAAQ,SAAA,EAAW;AACrB,MAAA,MAAM,SAAA,GAAY,IAAI,IAAA,CAAK,OAAA,CAAQ,SAAS,CAAA;AAC5C,MAAA,IAAI,SAAA,CAAU,OAAA,EAAQ,IAAK,IAAA,CAAK,KAAI,EAAG;AACrC,QAAA,kBAAA,GAAqB,SAAA;AACrB,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF;AAEA,IAAA,MAAM,YAAA,GAAe,QAAQ,WAAA,IAAe,CAAA;AAC5C,IAAA,IAAI,OAAA,CAAQ,WAAA,KAAgB,MAAA,IAAa,YAAA,IAAgB,QAAQ,WAAA,EAAa;AAC5E,MAAA,kBAAA,GAAqB,WAAA;AACrB,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,OAAO;AAAA,MACL,GAAG,OAAA;AAAA,MACH,aAAa,YAAA,GAAe;AAAA,KAC9B;AAAA,EACF,CAAC,CAAA;AAED,EAAA,IAAI,uBAAuB,SAAA,EAAW;AACpC,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,mBAAmB,CAAA;AAAA,EACvD;AACA,EAAA,IAAI,uBAAuB,WAAA,EAAa;AACtC,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,4BAA4B,CAAA;AAAA,EAChE;AACA,EAAA,IAAI,oBAAoB,IAAA,EAAM;AAC5B,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,iBAAiB,CAAA;AAAA,EACrD;AAGA,EAAA,MAAM,SAAA,GAAY,OAAO,GAAA,CAAI,KAAA,GAAQ,WAAW,MAAM,QAAA,GAAW,GAAA,CAAI,KAAA,CAAM,WAAW,CAAA,GAAI,MAAA;AAC1F,EAAA,MAAM,iBAAA,GAAoB;AAAA,IACxB,KAAA;AAAA,IACA,WAAA,EAAa,gBAAgB,WAAA,IAAe,CAAA;AAAA,IAC5C,SAAA,sBAAe,IAAA,EAAK;AAAA,IACpB,IAAA,EAAM,QAAA;AAAA,IACN,GAAI,SAAA,GAAY,EAAE,SAAA,KAAc;AAAC,GACnC;AACA,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,OAAA,CAAQ,QAAQ,QAAA,CAAS,iBAAiB,CAAC,CAAA,CAAE,MAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AAAA,EAC7D;AAEA,EAAA,IAAI,kBAAA,CAAmB,OAAO,CAAA,EAAG;AAC/B,IAAA,OAAA,CAAQ,OAAA,CAAQ,QAAQ,QAAA,CAAS,KAAA,EAAO,iBAAiB,CAAC,CAAA,CAAE,MAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AAAA,EAC5E;AAGA,EAAA,MAAM,UAAU,MAAM,OAAA,CAAQ,IAAA,CAAK,CAAA,EAAG,KAAK,CAAA,YAAA,CAAc,CAAA;AACzD,EAAA,IAAI,YAAY,IAAA,EAAM;AACpB,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,qBAAqB,CAAA;AAAA,EACzD;AAEA,EAAA,MAAM,IAAA,GAAO,OAAO,OAAA,KAAY,QAAA,GAAW,UAAU,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,OAAO,CAAA;AAErF,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,GAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,kBAAA;AAAA,MAChB,eAAA,EAAiB;AAAA,KACnB;AAAA,IACA;AAAA,GACF;AACF;AAEA,eAAe,aAAA,CACb,OACA,OAAA,EAC0B;AAC1B,EAAA,MAAM,UAAU,MAAM,OAAA,CAAQ,IAAA,CAAK,CAAA,EAAG,KAAK,CAAA,YAAA,CAAc,CAAA;AACzD,EAAA,IAAI,YAAY,IAAA,EAAM;AACpB,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,qBAAqB,CAAA;AAAA,EACzD;AAEA,EAAA,MAAM,IAAA,GAAO,OAAO,OAAA,KAAY,QAAA,GAC5B,UACA,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,OAAO,CAAA;AAEpC,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,GAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,kBAAA;AAAA,MAChB,eAAA,EAAiB;AAAA,KACnB;AAAA,IACA;AAAA,GACF;AACF;AAEA,eAAe,gBAAA,CACb,KAAA,EACA,KAAA,EACA,OAAA,EAC0B;AAC1B,EAAA,IAAI,CAAC,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAA,EAAG;AACxB,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,4BAA4B,CAAA;AAAA,EAChE;AACA,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,IAAA,CAAK,GAAG,KAAK,CAAA,YAAA,EAAe,KAAK,CAAA,IAAA,CAAM,CAAA;AACrE,EAAA,IAAI,YAAY,IAAA,EAAM;AACpB,IAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,wBAAwB,CAAA;AAAA,EAC5D;AACA,EAAA,MAAM,IAAA,GAAO,OAAO,OAAA,KAAY,QAAA,GAC5B,UACA,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,OAAO,CAAA;AACpC,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,GAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,kBAAA;AAAA,MAChB,eAAA,EAAiB;AAAA,KACnB;AAAA,IACA;AAAA,GACF;AACF;AAEA,SAAS,YAAA,CAAa,QAAgB,IAAA,EAAgC;AACpE,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,kBAAA;AAAA,MAChB,eAAA,EAAiB;AAAA,KACnB;AAAA,IACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI;AAAA,GAC3B;AACF","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"]}
@@ -1558,6 +1558,15 @@ var Bundle = class {
1558
1558
  ...resultEntries,
1559
1559
  ...docEntries
1560
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
+ }
1561
1570
  return {
1562
1571
  resourceType: "Bundle",
1563
1572
  id: bundleId,
@@ -1698,6 +1707,7 @@ function generateUuid() {
1698
1707
  var shl_exports = {};
1699
1708
  __export(shl_exports, {
1700
1709
  AzureStorage: () => AzureStorage,
1710
+ EXPIRATION_PRESETS: () => EXPIRATION_PRESETS,
1701
1711
  FhirflyStorage: () => FhirflyStorage,
1702
1712
  GCSStorage: () => GCSStorage,
1703
1713
  LocalStorage: () => LocalStorage,
@@ -1709,6 +1719,18 @@ __export(shl_exports, {
1709
1719
  getEntryContent: () => getEntryContent,
1710
1720
  revoke: () => revoke
1711
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
+ };
1712
1734
  function base64url(data) {
1713
1735
  return data.toString("base64url");
1714
1736
  }
@@ -1796,7 +1818,18 @@ async function generateQRCode(url) {
1796
1818
  });
1797
1819
  }
1798
1820
  async function create(options) {
1799
- const { bundle, passcode, expiresAt, maxAccesses, label, storage, debug } = options;
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
+ }
1800
1833
  if (!bundle || typeof bundle !== "object") {
1801
1834
  throw new ValidationError("bundle is required and must be an object");
1802
1835
  }
@@ -1811,7 +1844,7 @@ async function create(options) {
1811
1844
  "PSHD compliance forbids passcode (flag U is incompatible with flag P)"
1812
1845
  );
1813
1846
  }
1814
- if (!expiresAt) {
1847
+ if (!expiresAt && options.expiresAt !== "permanent") {
1815
1848
  throw new ValidationError(
1816
1849
  "PSHD compliance requires expiresAt (short-lived links for point-of-care)"
1817
1850
  );
@@ -2057,5 +2090,5 @@ async function revoke(shlId, storage) {
2057
2090
  }
2058
2091
 
2059
2092
  export { ips_exports, shl_exports };
2060
- //# sourceMappingURL=chunk-UU434UFQ.js.map
2061
- //# sourceMappingURL=chunk-UU434UFQ.js.map
2093
+ //# sourceMappingURL=chunk-YUMCDN7I.js.map
2094
+ //# sourceMappingURL=chunk-YUMCDN7I.js.map