@fhirfly-io/shl 0.3.1 → 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.
Files changed (47) hide show
  1. package/dist/{chunk-QXSWM5QV.cjs → chunk-JOZ6XZPO.cjs} +72 -5
  2. package/dist/chunk-JOZ6XZPO.cjs.map +1 -0
  3. package/dist/{chunk-ZEE5RXIS.js → chunk-KGEFZQ6W.js} +72 -5
  4. package/dist/chunk-KGEFZQ6W.js.map +1 -0
  5. package/dist/{chunk-63Q54EKN.cjs → chunk-SK77O3SG.cjs} +151 -35
  6. package/dist/chunk-SK77O3SG.cjs.map +1 -0
  7. package/dist/{chunk-YBDRWUQU.js → chunk-UU434UFQ.js} +151 -35
  8. package/dist/chunk-UU434UFQ.js.map +1 -0
  9. package/dist/cli.cjs +11 -11
  10. package/dist/cli.js +2 -2
  11. package/dist/express.cjs +12 -3
  12. package/dist/express.cjs.map +1 -1
  13. package/dist/express.d.cts +3 -2
  14. package/dist/express.d.ts +3 -2
  15. package/dist/express.js +11 -2
  16. package/dist/express.js.map +1 -1
  17. package/dist/fastify.cjs +22 -5
  18. package/dist/fastify.cjs.map +1 -1
  19. package/dist/fastify.d.cts +3 -2
  20. package/dist/fastify.d.ts +3 -2
  21. package/dist/fastify.js +21 -4
  22. package/dist/fastify.js.map +1 -1
  23. package/dist/index.cjs +3 -3
  24. package/dist/index.d.cts +12 -4
  25. package/dist/index.d.ts +12 -4
  26. package/dist/index.js +1 -1
  27. package/dist/lambda.cjs +4 -3
  28. package/dist/lambda.cjs.map +1 -1
  29. package/dist/lambda.d.cts +3 -2
  30. package/dist/lambda.d.ts +3 -2
  31. package/dist/lambda.js +3 -2
  32. package/dist/lambda.js.map +1 -1
  33. package/dist/server.cjs +2 -2
  34. package/dist/server.d.cts +4 -4
  35. package/dist/server.d.ts +4 -4
  36. package/dist/server.js +1 -1
  37. package/dist/{storage-BwszYwFo.d.cts → storage-BbzK-kFf.d.cts} +1 -1
  38. package/dist/{storage-B3GyJD2y.d.ts → storage-Cx7uXUl8.d.ts} +1 -1
  39. package/dist/{types-BegxU0wQ.d.ts → types-6Vw5fiat.d.ts} +7 -1
  40. package/dist/{types-Doq5cGNm.d.ts → types-BLLJeWe_.d.cts} +14 -0
  41. package/dist/{types-Doq5cGNm.d.cts → types-BLLJeWe_.d.ts} +14 -0
  42. package/dist/{types-hHf-a3hH.d.cts → types-Cdi4IkC9.d.cts} +7 -1
  43. package/package.json +3 -3
  44. package/dist/chunk-63Q54EKN.cjs.map +0 -1
  45. package/dist/chunk-QXSWM5QV.cjs.map +0 -1
  46. package/dist/chunk-YBDRWUQU.js.map +0 -1
  47. 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-QXSWM5QV.cjs.map
160
- //# sourceMappingURL=chunk-QXSWM5QV.cjs.map
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-ZEE5RXIS.js.map
158
- //# sourceMappingURL=chunk-ZEE5RXIS.js.map
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 typeCode = doc.typeCode ?? "34133-9";
1110
- const typeDisplay = doc.typeDisplay ?? "Summarization of episode note";
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 (profile === "ips") {
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 compositionDate = options?.compositionDate ?? (/* @__PURE__ */ new Date()).toISOString();
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
- compositionDate,
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: compositionDate,
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
- const manifest = {
1778
- files: [
1779
- {
1780
- contentType: "application/fhir+json;fhirVersion=4.0.1",
1781
- location: `${baseUrl}/${shlId}/content`
1782
- },
1783
- ...attachments.map((att, i) => ({
1784
- contentType: att.contentType,
1785
- location: `${baseUrl}/${shlId}/attachment/${i}`
1786
- }))
1787
- ],
1788
- status: "finalized",
1789
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
1790
- };
1791
- try {
1792
- await storage.store(`${shlId}/manifest.json`, JSON.stringify(manifest));
1793
- } catch (err) {
1794
- throw new chunkUDS6UJAL_cjs.StorageError(
1795
- `Failed to store manifest: ${err instanceof Error ? err.message : String(err)}`,
1796
- "store"
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-63Q54EKN.cjs.map
1952
- //# sourceMappingURL=chunk-63Q54EKN.cjs.map
2067
+ //# sourceMappingURL=chunk-SK77O3SG.cjs.map
2068
+ //# sourceMappingURL=chunk-SK77O3SG.cjs.map