@welshare/react 0.0.1-alpha.2 → 0.2.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 (38) hide show
  1. package/README.md +44 -0
  2. package/dist/esm/hooks/use-welshare.d.ts +5 -0
  3. package/dist/esm/hooks/use-welshare.d.ts.map +1 -1
  4. package/dist/esm/hooks/use-welshare.js +125 -7
  5. package/dist/esm/index.d.ts +3 -0
  6. package/dist/esm/index.d.ts.map +1 -1
  7. package/dist/esm/index.js +4 -1
  8. package/dist/esm/lib/encryption.d.ts +19 -0
  9. package/dist/esm/lib/encryption.d.ts.map +1 -0
  10. package/dist/esm/lib/encryption.js +61 -0
  11. package/dist/esm/lib/uploads.d.ts +3 -0
  12. package/dist/esm/lib/uploads.d.ts.map +1 -0
  13. package/dist/esm/lib/uploads.js +17 -0
  14. package/dist/esm/types.d.ts +35 -3
  15. package/dist/esm/types.d.ts.map +1 -1
  16. package/dist/node_modules/@welshare/react/.DS_Store +0 -0
  17. package/dist/node_modules/@welshare/react/README.md +44 -0
  18. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-welshare.d.ts +5 -0
  19. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-welshare.d.ts.map +1 -1
  20. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-welshare.js +125 -7
  21. package/dist/node_modules/@welshare/react/dist/esm/index.d.ts +3 -0
  22. package/dist/node_modules/@welshare/react/dist/esm/index.d.ts.map +1 -1
  23. package/dist/node_modules/@welshare/react/dist/esm/index.js +4 -1
  24. package/dist/node_modules/@welshare/react/dist/esm/lib/encryption.d.ts +19 -0
  25. package/dist/node_modules/@welshare/react/dist/esm/lib/encryption.d.ts.map +1 -0
  26. package/dist/node_modules/@welshare/react/dist/esm/lib/encryption.js +61 -0
  27. package/dist/node_modules/@welshare/react/dist/esm/lib/uploads.d.ts +3 -0
  28. package/dist/node_modules/@welshare/react/dist/esm/lib/uploads.d.ts.map +1 -0
  29. package/dist/node_modules/@welshare/react/dist/esm/lib/uploads.js +17 -0
  30. package/dist/node_modules/@welshare/react/dist/esm/types.d.ts +35 -3
  31. package/dist/node_modules/@welshare/react/dist/esm/types.d.ts.map +1 -1
  32. package/dist/node_modules/@welshare/react/package.json +2 -2
  33. package/dist/node_modules/@welshare/react/src/hooks/use-welshare.ts +167 -7
  34. package/dist/node_modules/@welshare/react/src/index.ts +13 -5
  35. package/dist/node_modules/@welshare/react/src/lib/encryption.ts +110 -0
  36. package/dist/node_modules/@welshare/react/src/lib/uploads.ts +29 -0
  37. package/dist/node_modules/@welshare/react/src/types.ts +41 -4
  38. package/package.json +2 -2
package/README.md CHANGED
@@ -81,6 +81,49 @@ export function QuestionnaireForm() {
81
81
  }
82
82
  ```
83
83
 
84
+ ### Binary file uploads (e.g. images)
85
+
86
+ binary file uploads require a lot of back and forth with the wallet dialog that we wrapped into one convenient upload API. If you want to include binary uploads into your questionnaires, you would typically hook into your own form, upload the file using the `uploadFile` function exposed by the `useWelshare` hook and use the response information to in the respective questionnaire form answer item.
87
+
88
+ Each download should contain a reference to the resource that initiated its upload. As Welshare right now is mostly about questionnaires, you should use a combination of the resource type (questionnaire), the questionnaire id and the answer item's id
89
+
90
+ ```ts
91
+ const reference = `questionnaire/${questionnaireId}/${answerItemId}`;
92
+ ```
93
+
94
+ Binary files are addressed as items of type `valueAttachment` in Fhir. See https://www.hl7.org/fhir/questionnaireresponse.html
95
+
96
+ Before uploading, welshare encrypts all files with a new random symmetric AES (GCM / 256 bits) key. Users request a presigned upload url and post the encrypted file to an S3 compatible API of ours. Finally, they encrypt the encryption key on a user controlled Nillion *owned* collection for binary data and grant respective access rights for the application. The application a user used to upload the file is by default able to download the file again (Technically, that application is always welshare right now. This will change to the "builder" address of the respective app and the hpmp enclave keys, which allow AI access to the files)
97
+
98
+ Here's an example how to use it:
99
+
100
+ ```ts
101
+ const { isConnected, openWallet, uploadFile, submitData } = useWelshare({
102
+ applicationId: process.env.NEXT_PUBLIC_WELSHARE_APP_ID || ""
103
+ })
104
+ //... let users select a file on their box
105
+
106
+ const { url: uploadedFileUrl, binaryFileUid } = await uploadFile(
107
+ userFile,
108
+ reference: `questionnaire/${questionnaireId}/<linkId>`
109
+ );
110
+
111
+ const responseItem = {
112
+ answer = [
113
+ {
114
+ valueAttachment: {
115
+ id: binaryFileUid,
116
+ contentType: userFile.type,
117
+ size: userFile.size,
118
+ title: userFile.name,
119
+ url: uploadedFileUrl,
120
+ },
121
+ },
122
+ ];
123
+ }
124
+ // insert the responseItem into your QuestionnaireResponse
125
+ ```
126
+
84
127
  ## API
85
128
 
86
129
  ### supported callbacks
@@ -94,6 +137,7 @@ those are configured in the `useWelshare` options parameter and called back duri
94
137
  onSessionReady?: (sessionPubKey: string) => void;
95
138
  onDialogClosing?: () => void;
96
139
  onDialogReady?: () => void;
140
+ onFileUploaded?: (insertedUid: string, url: string) => void
97
141
  }
98
142
  ```
99
143
 
@@ -4,7 +4,12 @@ export declare const useWelshare: (props: WelshareConnectionOptions) => {
4
4
  storageKey: string | undefined;
5
5
  openWallet: () => void;
6
6
  isDialogOpen: boolean;
7
+ uploadFile: (file: File, reference: string) => Promise<{
8
+ url: string;
9
+ binaryFileUid: string;
10
+ }>;
7
11
  submitData: <T>(schemaId: SubmissionSchemaId, submission: SubmissionPayload<T>) => void;
12
+ uploadState: "preparing" | "uploading" | "anchoring" | "finished" | "error" | undefined;
8
13
  isSubmitting: boolean;
9
14
  };
10
15
  //# sourceMappingURL=use-welshare.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-welshare.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-welshare.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAGpB,eAAO,MAAM,WAAW,UAAW,yBAAyB;;;;;iBAkFtC,CAAC,YACT,kBAAkB,cAChB,iBAAiB,CAAC,CAAC,CAAC;;CAuDnC,CAAC"}
1
+ {"version":3,"file":"use-welshare.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-welshare.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,iBAAiB,EACjB,kBAAkB,EAElB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,WAAW,UAAW,yBAAyB;;;;;uBAmLlD,IAAI,aACC,MAAM,KAChB,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;iBA0C9B,CAAC,YACT,kBAAkB,cAChB,iBAAiB,CAAC,CAAC,CAAC;;;CAqEnC,CAAC"}
@@ -1,18 +1,21 @@
1
- import { useEffect, useState } from "react";
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { encryptAndUploadFile } from "../lib/uploads.js";
2
3
  export const useWelshare = (props) => {
3
4
  const [storageKey, setStorageKey] = useState();
4
5
  const [isDialogOpen, setIsDialogOpen] = useState(false);
5
6
  const [dialogWindow, setDialogWindow] = useState(null);
6
7
  const [messageIdCounter, setMessageIdCounter] = useState(0);
7
8
  const [isSubmitting, setIsSubmitting] = useState(false);
9
+ const [uploadState, setUploadState] = useState();
10
+ // (claude opus) Use ref to control current upload in effect without triggering re-renders
11
+ const currentUploadRef = useRef(null);
8
12
  const options = {
9
- environment: "development",
10
13
  apiBaseUrl: "https://wallet.welshare.app",
11
14
  ...props,
12
15
  };
13
16
  const WELSHARE_WALLET_URL = `${options.apiBaseUrl}/wallet-external`;
14
17
  useEffect(() => {
15
- const handleMessage = (event) => {
18
+ const handleMessage = async (event) => {
16
19
  // Verify origin for security
17
20
  if (event.origin !== new URL(WELSHARE_WALLET_URL).origin) {
18
21
  return;
@@ -24,6 +27,10 @@ export const useWelshare = (props) => {
24
27
  errorMessage = message.payload.error || "An unknown error occurred";
25
28
  console.error("Welshare Wallet sent an error:", errorMessage);
26
29
  setIsSubmitting(false);
30
+ // Reject the promise if there's an ongoing upload
31
+ if (currentUploadRef.current?.uploadPromise) {
32
+ currentUploadRef.current.uploadPromise.reject(new Error(errorMessage));
33
+ }
27
34
  options.callbacks.onError?.(errorMessage);
28
35
  break;
29
36
  case "DIALOG_READY":
@@ -46,6 +53,10 @@ export const useWelshare = (props) => {
46
53
  console.debug("Welshare Wallet Dialog is closing");
47
54
  setStorageKey(undefined);
48
55
  setIsDialogOpen(false);
56
+ // Reject any pending upload promises
57
+ if (currentUploadRef.current?.uploadPromise) {
58
+ currentUploadRef.current.uploadPromise.reject(new Error("Dialog closed before upload completed"));
59
+ }
49
60
  options.callbacks.onDialogClosing?.();
50
61
  break;
51
62
  case "DATA_UPLOADED":
@@ -53,6 +64,62 @@ export const useWelshare = (props) => {
53
64
  setIsSubmitting(false);
54
65
  options.callbacks.onUploaded?.(message.payload);
55
66
  break;
67
+ //todo: make this work for batches, too
68
+ case "UPLOAD_CREDENTIALS_CREATED":
69
+ const credentials = message.payload;
70
+ console.debug("upload credentials created", credentials);
71
+ if (!currentUploadRef.current) {
72
+ throw new Error("No upload in progress");
73
+ }
74
+ if (!dialogWindow) {
75
+ currentUploadRef.current.uploadPromise.reject(new Error("(UPLOAD_CREDENTIALS_CREATED) Dialog window not open"));
76
+ return;
77
+ }
78
+ try {
79
+ setUploadState("uploading");
80
+ const encodedEncryptionKey = await encryptAndUploadFile(currentUploadRef.current.file, credentials.presignedUrl);
81
+ setUploadState("anchoring");
82
+ const binarySubmissionPayload = {
83
+ timestamp: currentUploadRef.current.timestamp,
84
+ applicationId: options.applicationId,
85
+ reference: currentUploadRef.current.reference,
86
+ fileName: currentUploadRef.current.fileName,
87
+ fileType: currentUploadRef.current.fileType,
88
+ encryptionKey: encodedEncryptionKey,
89
+ fileSize: currentUploadRef.current.file.size,
90
+ url: `welshare://${credentials.uploadKey}`,
91
+ };
92
+ //write to Nillion
93
+ const message = {
94
+ type: "SUBMIT_BINARY_DATA",
95
+ id: String(messageIdCounter),
96
+ payload: binarySubmissionPayload,
97
+ };
98
+ dialogWindow.postMessage(message, WELSHARE_WALLET_URL);
99
+ setMessageIdCounter((prev) => prev + 1);
100
+ }
101
+ catch (error) {
102
+ console.error("error uploading file", error);
103
+ // Update status to error
104
+ currentUploadRef.current.uploadPromise.reject(error);
105
+ setUploadState("error");
106
+ // Call error callback
107
+ const errorMsg = error instanceof Error ? error.message : "Upload failed";
108
+ options.callbacks.onError?.(errorMsg);
109
+ }
110
+ break;
111
+ //{insertetUid, errors}
112
+ case "BINARY_DATA_SUBMITTED":
113
+ if (!currentUploadRef.current) {
114
+ throw new Error("No upload in progress");
115
+ }
116
+ currentUploadRef.current.uploadPromise.resolve({
117
+ url: message.payload.url,
118
+ binaryFileUid: message.payload.insertedUid,
119
+ });
120
+ setUploadState("finished");
121
+ options.callbacks.onFileUploaded?.(message.payload.insertedUid, message.payload.url);
122
+ break;
56
123
  default:
57
124
  console.log("Received unexpected message from Welshare Wallet:", message);
58
125
  }
@@ -61,14 +128,53 @@ export const useWelshare = (props) => {
61
128
  return () => {
62
129
  window.removeEventListener("message", handleMessage);
63
130
  };
64
- }, [WELSHARE_WALLET_URL, options.callbacks]);
131
+ }, [WELSHARE_WALLET_URL, dialogWindow, messageIdCounter, options.applicationId, options.callbacks]);
132
+ /**
133
+ * Starts a file upload and returns a promise that resolves with the uploaded file URL
134
+ * @param file The file to upload
135
+ * @param reference string A reference identifier for the upload
136
+ * @returns Promise<{ url: string; binaryFileUid: string }> url usually starts with welshare:// and resolves to an encrypted storage location
137
+ */
138
+ const uploadFile = (file, reference) => {
139
+ return new Promise((resolve, reject) => {
140
+ if (!dialogWindow) {
141
+ reject(new Error("(uploadFile) Dialog window not open"));
142
+ return;
143
+ }
144
+ const payload = {
145
+ timestamp: new Date(),
146
+ applicationId: options.applicationId,
147
+ reference: reference,
148
+ fileName: file.name,
149
+ fileType: file.type,
150
+ };
151
+ setUploadState("preparing");
152
+ // (claude opus) Store the upload with the file and promise handlers on a ref
153
+ currentUploadRef.current = {
154
+ ...payload,
155
+ file,
156
+ reference,
157
+ uploadPromise: { resolve, reject },
158
+ };
159
+ const message = {
160
+ type: "REQUEST_UPLOAD_CREDENTIALS",
161
+ id: String(messageIdCounter),
162
+ payload: {
163
+ ...payload,
164
+ applicationId: options.applicationId,
165
+ },
166
+ };
167
+ dialogWindow.postMessage(message, WELSHARE_WALLET_URL);
168
+ setMessageIdCounter((prev) => prev + 1);
169
+ });
170
+ };
65
171
  /**
66
172
  * @param schemaType a welshare schema type uid
67
173
  * @param submission your submission that validates against the schema type
68
174
  */
69
175
  const submitData = (schemaId, submission) => {
70
176
  if (!dialogWindow) {
71
- throw new Error("Dialog window not open");
177
+ throw new Error("(submitData) Dialog window not open");
72
178
  }
73
179
  const submissionPayload = {
74
180
  applicationId: options.applicationId,
@@ -92,10 +198,20 @@ export const useWelshare = (props) => {
92
198
  };
93
199
  const openWallet = () => {
94
200
  const width = 800;
95
- const height = 600;
201
+ const height = 700;
96
202
  const left = window.screenX + (window.outerWidth - width) / 2;
97
203
  const top = window.screenY + (window.outerHeight - height) / 2;
98
- const newWindow = window.open(WELSHARE_WALLET_URL, "Welshare Wallet", `width=${width},height=${height},left=${left},top=${top}`);
204
+ let socialParams = "";
205
+ if (options.interpolateSocials) {
206
+ const socialEntries = Object.entries(options.interpolateSocials)
207
+ .filter(([_, value]) => value !== undefined && value !== null)
208
+ .map(([key, value]) => `social.${key}=${encodeURIComponent(String(value))}`);
209
+ if (socialEntries.length > 0) {
210
+ socialParams = `&${socialEntries.join('&')}`;
211
+ }
212
+ }
213
+ const walletUrl = `${WELSHARE_WALLET_URL}?applicationId=${options.applicationId}${socialParams}`;
214
+ const newWindow = window.open(walletUrl, "Welshare Wallet", `width=${width},height=${height},left=${left},top=${top}`);
99
215
  if (newWindow) {
100
216
  setDialogWindow(newWindow);
101
217
  }
@@ -105,7 +221,9 @@ export const useWelshare = (props) => {
105
221
  storageKey,
106
222
  openWallet,
107
223
  isDialogOpen,
224
+ uploadFile,
108
225
  submitData,
226
+ uploadState,
109
227
  isSubmitting,
110
228
  };
111
229
  };
@@ -1,7 +1,10 @@
1
1
  export { ConnectWelshareButton } from "./components/connect-button.js";
2
2
  export { useWelshare } from "./hooks/use-welshare.js";
3
+ export { decodeEncryptionKey, decrypt, encodeEncryptionKey, encryptFile, generateRandomAESKey, type EncryptionKey } from "./lib/encryption.js";
4
+ export { encryptAndUploadFile } from "./lib/uploads.js";
3
5
  export declare const Schemas: {
4
6
  QuestionnaireResponse: string;
5
7
  ReflexSubmission: string;
8
+ BinaryFile: string;
6
9
  };
7
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAEvE,OAAO,EACL,WAAW,EACZ,MAAM,yBAAyB,CAAC;AAGjC,eAAO,MAAM,OAAO;;;CAGnB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAEvE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EACL,mBAAmB,EACnB,OAAO,EACP,mBAAmB,EACnB,WAAW,EACX,oBAAoB,EACpB,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,eAAO,MAAM,OAAO;;;;CAInB,CAAC"}
package/dist/esm/index.js CHANGED
@@ -3,8 +3,11 @@
3
3
  export { ConnectWelshareButton } from "./components/connect-button.js";
4
4
  // ---- hooks ----
5
5
  export { useWelshare } from "./hooks/use-welshare.js";
6
+ export { decodeEncryptionKey, decrypt, encodeEncryptionKey, encryptFile, generateRandomAESKey } from "./lib/encryption.js";
7
+ export { encryptAndUploadFile } from "./lib/uploads.js";
6
8
  //todo: import them from the SDK or a dedicated SDK constants export
7
9
  export const Schemas = {
8
10
  QuestionnaireResponse: "b14b538f-7de3-4767-ad77-464d755d78bd", //QuestionnaireResponseSchema.schemaUid,
9
- ReflexSubmission: "f5cf2d8a-1f78-4f21-b4bd-082e983b830c" //ReflexSubmissionSchema.schemaUid,
11
+ ReflexSubmission: "f5cf2d8a-1f78-4f21-b4bd-082e983b830c", //ReflexSubmissionSchema.schemaUid,
12
+ BinaryFile: "9d696baf-483f-4cc0-b748-23a22c1705f5" //BinaryFilesSchema.schemaUid,
10
13
  };
@@ -0,0 +1,19 @@
1
+ export declare const ALGORITHM = "AES-GCM";
2
+ export type Algorithm = "AES-GCM";
3
+ export declare const generateRandomAESKey: () => Promise<CryptoKey>;
4
+ export declare const encryptFile: (file: File, key: CryptoKey) => Promise<{
5
+ encryptedData: ArrayBuffer;
6
+ iv: Uint8Array;
7
+ }>;
8
+ export type EncryptionKey = {
9
+ algorithm: Algorithm;
10
+ key: string;
11
+ iv: string;
12
+ };
13
+ export declare const encodeEncryptionKey: (key: CryptoKey, iv: Uint8Array) => Promise<EncryptionKey>;
14
+ export declare const decodeEncryptionKey: (encryptionKey: EncryptionKey) => {
15
+ key: Uint8Array<ArrayBuffer>;
16
+ iv: Uint8Array<ArrayBuffer>;
17
+ };
18
+ export declare const decrypt: (encryptedData: ArrayBuffer, encryptionKey: EncryptionKey) => Promise<ArrayBuffer | null>;
19
+ //# sourceMappingURL=encryption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../../../src/lib/encryption.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,YAAY,CAAC;AACnC,MAAM,MAAM,SAAS,GAAG,SAAS,CAAC;AAElC,eAAO,MAAM,oBAAoB,QAAa,OAAO,CAAC,SAAS,CAU9D,CAAC;AAIF,eAAO,MAAM,WAAW,SAChB,IAAI,OACL,SAAS,KACb,OAAO,CAAC;IAAE,aAAa,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,UAAU,CAAA;CAAE,CAgBxD,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,eAAO,MAAM,mBAAmB,QACzB,SAAS,MACV,UAAU,KACb,OAAO,CAAC,aAAa,CAgBvB,CAAC;AAEF,eAAO,MAAM,mBAAmB,kBACf,aAAa,KAC3B;IAAE,GAAG,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC;IAAC,EAAE,EAAE,UAAU,CAAC,WAAW,CAAC,CAAA;CAU7D,CAAC;AAGF,eAAO,MAAM,OAAO,kBACH,WAAW,iBACX,aAAa,KAC3B,OAAO,CAAC,WAAW,GAAG,IAAI,CA0B5B,CAAC"}
@@ -0,0 +1,61 @@
1
+ export const ALGORITHM = "AES-GCM";
2
+ export const generateRandomAESKey = async () => {
3
+ // Generate a 256-bit AES-GCM key for file encryption
4
+ return window.crypto.subtle.generateKey({
5
+ name: ALGORITHM,
6
+ length: 256, // 256-bit key
7
+ }, true, // Key is extractable (needed for storage/transmission)
8
+ ["encrypt", "decrypt"] // Key usage
9
+ );
10
+ };
11
+ /// also Generates random IV (12 bytes for AES-GCM)
12
+ /// @return {arraybuffer ciphertext, uint8array iv}
13
+ export const encryptFile = async (file, key) => {
14
+ // Read file as ArrayBuffer
15
+ const fileData = await file.arrayBuffer();
16
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
17
+ // Encrypt the file data
18
+ const encryptedData = await window.crypto.subtle.encrypt({
19
+ name: ALGORITHM,
20
+ iv: iv,
21
+ }, key, fileData);
22
+ return { encryptedData, iv };
23
+ };
24
+ export const encodeEncryptionKey = async (key, iv) => {
25
+ // Export the key as raw bytes
26
+ const exportedKey = await window.crypto.subtle.exportKey("raw", key);
27
+ const keyHex = Array.from(new Uint8Array(exportedKey))
28
+ .map((b) => b.toString(16).padStart(2, "0"))
29
+ .join("");
30
+ const ivHex = Array.from(iv)
31
+ .map((b) => b.toString(16).padStart(2, "0"))
32
+ .join("");
33
+ return {
34
+ algorithm: ALGORITHM,
35
+ key: keyHex,
36
+ iv: ivHex,
37
+ };
38
+ };
39
+ export const decodeEncryptionKey = (encryptionKey) => {
40
+ const keyBytes = new Uint8Array(encryptionKey.key
41
+ .match(/.{1,2}/g)
42
+ .map((byte) => parseInt(byte, 16)));
43
+ const ivBytes = new Uint8Array(encryptionKey.iv.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
44
+ return { key: keyBytes, iv: ivBytes };
45
+ };
46
+ // Helper function to decrypt a file using encoded encryption key
47
+ export const decrypt = async (encryptedData, encryptionKey) => {
48
+ try {
49
+ const { key: keyBytes, iv } = decodeEncryptionKey(encryptionKey);
50
+ const key = await window.crypto.subtle.importKey("raw", keyBytes, { name: ALGORITHM }, false, ["decrypt"]);
51
+ const decryptedData = await window.crypto.subtle.decrypt({
52
+ name: ALGORITHM,
53
+ iv,
54
+ }, key, encryptedData);
55
+ return decryptedData;
56
+ }
57
+ catch (error) {
58
+ console.error("Failed to decrypt file:", error);
59
+ return null;
60
+ }
61
+ };
@@ -0,0 +1,3 @@
1
+ import { EncryptionKey } from "./encryption.js";
2
+ export declare const encryptAndUploadFile: (file: File, presignedUrl: string) => Promise<EncryptionKey>;
3
+ //# sourceMappingURL=uploads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploads.d.ts","sourceRoot":"","sources":["../../../src/lib/uploads.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAEd,MAAM,iBAAiB,CAAC;AAEzB,eAAO,MAAM,oBAAoB,SACzB,IAAI,gBACI,MAAM,KACnB,OAAO,CAAC,aAAa,CAkBvB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { encodeEncryptionKey, encryptFile, generateRandomAESKey, } from "./encryption.js";
2
+ export const encryptAndUploadFile = async (file, presignedUrl) => {
3
+ const encryptionKey = await generateRandomAESKey();
4
+ const { encryptedData, iv } = await encryptFile(file, encryptionKey);
5
+ // Upload encrypted file to S3
6
+ const uploadResponse = await fetch(presignedUrl, {
7
+ method: "PUT",
8
+ body: encryptedData,
9
+ headers: {
10
+ "Content-Type": file.type,
11
+ },
12
+ });
13
+ if (!uploadResponse.ok) {
14
+ throw new Error(`Failed to upload file ${uploadResponse.status}`);
15
+ }
16
+ return encodeEncryptionKey(encryptionKey, iv);
17
+ };
@@ -1,9 +1,9 @@
1
+ import { EncryptionKey } from "./lib/encryption.js";
1
2
  export interface DialogMessage {
2
3
  type: string;
3
4
  payload?: any;
4
5
  id?: string;
5
6
  }
6
- export type WelshareEnvironment = "development" | "production";
7
7
  /**
8
8
  * a welshare schema type uid
9
9
  */
@@ -14,14 +14,46 @@ export interface SubmissionPayload<T> {
14
14
  schemaId: SubmissionSchemaId;
15
15
  submission: T;
16
16
  }
17
+ export type UploadCredentials = {
18
+ presignedUrl: string;
19
+ uploadKey: string;
20
+ };
21
+ export interface RequestUploadCredentialsPayload {
22
+ timestamp?: Date;
23
+ applicationId: string;
24
+ reference: string;
25
+ fileName: string;
26
+ fileType: string;
27
+ }
28
+ export interface BinaryFileSubmissionPayload extends RequestUploadCredentialsPayload {
29
+ encryptionKey: EncryptionKey;
30
+ fileSize: number;
31
+ url: string;
32
+ }
33
+ export interface RunningFileUpload extends RequestUploadCredentialsPayload {
34
+ credentials?: UploadCredentials;
35
+ file: File;
36
+ reference: string;
37
+ uploadPromise: {
38
+ resolve: (result: {
39
+ url: string;
40
+ binaryFileUid: string;
41
+ }) => void;
42
+ reject: (error: Error) => void;
43
+ };
44
+ }
17
45
  export interface DataSubmissionDialogMessage extends DialogMessage {
18
- payload: SubmissionPayload<unknown>;
46
+ payload: SubmissionPayload<unknown> | BinaryFileSubmissionPayload | RequestUploadCredentialsPayload;
19
47
  }
20
48
  export interface WelshareConnectionOptions {
21
49
  applicationId: string;
50
+ interpolateSocials?: {
51
+ emailAddress?: string;
52
+ privy?: string;
53
+ };
22
54
  apiBaseUrl?: string;
23
- environment?: WelshareEnvironment;
24
55
  callbacks: {
56
+ onFileUploaded?: (insertedUid: string, url: string) => void;
25
57
  onUploaded?: (payload: SubmissionPayload<unknown>) => void;
26
58
  onError?: (error: string) => void;
27
59
  onSessionReady?: (sessionPubKey: string) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,YAAY,CAAC;AAE/D;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAA;AAEvC,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,EAAE,CAAC,CAAC;CACf;AAED,MAAM,WAAW,2BAA4B,SAAQ,aAAa;IAChE,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,MAAM,CAAC;IAEtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,SAAS,EAAE;QACT,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;QAC3D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;QAClC,cAAc,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;QACjD,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;QAC7B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;KAC5B,CAAC;CACH"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAA;AAEvC,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,EAAE,CAAC,CAAC;CACf;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAA;AAED,MAAM,WAAW,+BAA+B;IAC9C,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,2BAA4B,SAAQ,+BAA+B;IAClF,aAAa,EAAE,aAAa,CAAC;IAE7B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAkB,SAAQ,+BAA+B;IACxE,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE;QACb,OAAO,EAAE,CAAC,MAAM,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,aAAa,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAC;QAClE,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;KAChC,CAAC;CACH;AAED,MAAM,WAAW,2BAA4B,SAAQ,aAAa;IAChE,OAAO,EACH,iBAAiB,CAAC,OAAO,CAAC,GAC1B,2BAA2B,GAC3B,+BAA+B,CAAC;CACrC;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE;QACnB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAA;IAED,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE;QACT,cAAc,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5D,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;QAC3D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;QAClC,cAAc,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;QACjD,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;QAC7B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;KAC5B,CAAC;CACH"}
@@ -81,6 +81,49 @@ export function QuestionnaireForm() {
81
81
  }
82
82
  ```
83
83
 
84
+ ### Binary file uploads (e.g. images)
85
+
86
+ binary file uploads require a lot of back and forth with the wallet dialog that we wrapped into one convenient upload API. If you want to include binary uploads into your questionnaires, you would typically hook into your own form, upload the file using the `uploadFile` function exposed by the `useWelshare` hook and use the response information to in the respective questionnaire form answer item.
87
+
88
+ Each download should contain a reference to the resource that initiated its upload. As Welshare right now is mostly about questionnaires, you should use a combination of the resource type (questionnaire), the questionnaire id and the answer item's id
89
+
90
+ ```ts
91
+ const reference = `questionnaire/${questionnaireId}/${answerItemId}`;
92
+ ```
93
+
94
+ Binary files are addressed as items of type `valueAttachment` in Fhir. See https://www.hl7.org/fhir/questionnaireresponse.html
95
+
96
+ Before uploading, welshare encrypts all files with a new random symmetric AES (GCM / 256 bits) key. Users request a presigned upload url and post the encrypted file to an S3 compatible API of ours. Finally, they encrypt the encryption key on a user controlled Nillion *owned* collection for binary data and grant respective access rights for the application. The application a user used to upload the file is by default able to download the file again (Technically, that application is always welshare right now. This will change to the "builder" address of the respective app and the hpmp enclave keys, which allow AI access to the files)
97
+
98
+ Here's an example how to use it:
99
+
100
+ ```ts
101
+ const { isConnected, openWallet, uploadFile, submitData } = useWelshare({
102
+ applicationId: process.env.NEXT_PUBLIC_WELSHARE_APP_ID || ""
103
+ })
104
+ //... let users select a file on their box
105
+
106
+ const { url: uploadedFileUrl, binaryFileUid } = await uploadFile(
107
+ userFile,
108
+ reference: `questionnaire/${questionnaireId}/<linkId>`
109
+ );
110
+
111
+ const responseItem = {
112
+ answer = [
113
+ {
114
+ valueAttachment: {
115
+ id: binaryFileUid,
116
+ contentType: userFile.type,
117
+ size: userFile.size,
118
+ title: userFile.name,
119
+ url: uploadedFileUrl,
120
+ },
121
+ },
122
+ ];
123
+ }
124
+ // insert the responseItem into your QuestionnaireResponse
125
+ ```
126
+
84
127
  ## API
85
128
 
86
129
  ### supported callbacks
@@ -94,6 +137,7 @@ those are configured in the `useWelshare` options parameter and called back duri
94
137
  onSessionReady?: (sessionPubKey: string) => void;
95
138
  onDialogClosing?: () => void;
96
139
  onDialogReady?: () => void;
140
+ onFileUploaded?: (insertedUid: string, url: string) => void
97
141
  }
98
142
  ```
99
143
 
@@ -4,7 +4,12 @@ export declare const useWelshare: (props: WelshareConnectionOptions) => {
4
4
  storageKey: string | undefined;
5
5
  openWallet: () => void;
6
6
  isDialogOpen: boolean;
7
+ uploadFile: (file: File, reference: string) => Promise<{
8
+ url: string;
9
+ binaryFileUid: string;
10
+ }>;
7
11
  submitData: <T>(schemaId: SubmissionSchemaId, submission: SubmissionPayload<T>) => void;
12
+ uploadState: "preparing" | "uploading" | "anchoring" | "finished" | "error" | undefined;
8
13
  isSubmitting: boolean;
9
14
  };
10
15
  //# sourceMappingURL=use-welshare.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-welshare.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-welshare.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAGpB,eAAO,MAAM,WAAW,UAAW,yBAAyB;;;;;iBAkFtC,CAAC,YACT,kBAAkB,cAChB,iBAAiB,CAAC,CAAC,CAAC;;CAuDnC,CAAC"}
1
+ {"version":3,"file":"use-welshare.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-welshare.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,iBAAiB,EACjB,kBAAkB,EAElB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,WAAW,UAAW,yBAAyB;;;;;uBAmLlD,IAAI,aACC,MAAM,KAChB,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;iBA0C9B,CAAC,YACT,kBAAkB,cAChB,iBAAiB,CAAC,CAAC,CAAC;;;CAqEnC,CAAC"}