@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
@@ -1,10 +1,15 @@
1
1
  import {
2
+ BinaryFileSubmissionPayload,
2
3
  DialogMessage,
4
+ RequestUploadCredentialsPayload,
5
+ RunningFileUpload,
3
6
  SubmissionPayload,
4
7
  SubmissionSchemaId,
8
+ UploadCredentials,
5
9
  WelshareConnectionOptions,
6
10
  } from "@/types.js";
7
- import { useEffect, useState } from "react";
11
+ import { useEffect, useRef, useState } from "react";
12
+ import { encryptAndUploadFile } from "../lib/uploads.js";
8
13
 
9
14
  export const useWelshare = (props: WelshareConnectionOptions) => {
10
15
  const [storageKey, setStorageKey] = useState<string>();
@@ -12,9 +17,14 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
12
17
  const [dialogWindow, setDialogWindow] = useState<Window | null>(null);
13
18
  const [messageIdCounter, setMessageIdCounter] = useState(0);
14
19
  const [isSubmitting, setIsSubmitting] = useState(false);
20
+ const [uploadState, setUploadState] = useState<
21
+ "preparing" | "uploading" | "anchoring" | "finished" | "error"
22
+ >();
23
+
24
+ // (claude opus) Use ref to control current upload in effect without triggering re-renders
25
+ const currentUploadRef = useRef<RunningFileUpload>(null);
15
26
 
16
27
  const options: WelshareConnectionOptions = {
17
- environment: "development",
18
28
  apiBaseUrl: "https://wallet.welshare.app",
19
29
  ...props,
20
30
  };
@@ -22,7 +32,7 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
22
32
  const WELSHARE_WALLET_URL = `${options.apiBaseUrl}/wallet-external`;
23
33
 
24
34
  useEffect(() => {
25
- const handleMessage = (event: MessageEvent<DialogMessage>) => {
35
+ const handleMessage = async (event: MessageEvent<DialogMessage>) => {
26
36
  // Verify origin for security
27
37
  if (event.origin !== new URL(WELSHARE_WALLET_URL).origin) {
28
38
  return;
@@ -35,6 +45,14 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
35
45
  errorMessage = message.payload.error || "An unknown error occurred";
36
46
  console.error("Welshare Wallet sent an error:", errorMessage);
37
47
  setIsSubmitting(false);
48
+
49
+ // Reject the promise if there's an ongoing upload
50
+ if (currentUploadRef.current?.uploadPromise) {
51
+ currentUploadRef.current.uploadPromise.reject(
52
+ new Error(errorMessage)
53
+ );
54
+ }
55
+
38
56
  options.callbacks.onError?.(errorMessage);
39
57
  break;
40
58
  case "DIALOG_READY":
@@ -61,6 +79,14 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
61
79
  console.debug("Welshare Wallet Dialog is closing");
62
80
  setStorageKey(undefined);
63
81
  setIsDialogOpen(false);
82
+
83
+ // Reject any pending upload promises
84
+ if (currentUploadRef.current?.uploadPromise) {
85
+ currentUploadRef.current.uploadPromise.reject(
86
+ new Error("Dialog closed before upload completed")
87
+ );
88
+ }
89
+
64
90
  options.callbacks.onDialogClosing?.();
65
91
  break;
66
92
 
@@ -69,6 +95,79 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
69
95
  setIsSubmitting(false);
70
96
  options.callbacks.onUploaded?.(message.payload);
71
97
  break;
98
+
99
+ //todo: make this work for batches, too
100
+ case "UPLOAD_CREDENTIALS_CREATED":
101
+ const credentials: UploadCredentials = message.payload;
102
+ console.debug("upload credentials created", credentials);
103
+
104
+ if (!currentUploadRef.current) {
105
+ throw new Error("No upload in progress");
106
+ }
107
+
108
+ if (!dialogWindow) {
109
+ currentUploadRef.current.uploadPromise.reject(
110
+ new Error("(UPLOAD_CREDENTIALS_CREATED) Dialog window not open")
111
+ );
112
+ return;
113
+ }
114
+ try {
115
+ setUploadState("uploading");
116
+ const encodedEncryptionKey = await encryptAndUploadFile(
117
+ currentUploadRef.current.file,
118
+ credentials.presignedUrl
119
+ );
120
+
121
+ setUploadState("anchoring");
122
+ const binarySubmissionPayload: BinaryFileSubmissionPayload = {
123
+ timestamp: currentUploadRef.current.timestamp,
124
+ applicationId: options.applicationId,
125
+ reference: currentUploadRef.current.reference,
126
+ fileName: currentUploadRef.current.fileName,
127
+ fileType: currentUploadRef.current.fileType,
128
+ encryptionKey: encodedEncryptionKey,
129
+ fileSize: currentUploadRef.current.file.size,
130
+ url: `welshare://${credentials.uploadKey}`,
131
+ };
132
+
133
+ //write to Nillion
134
+ const message: DialogMessage = {
135
+ type: "SUBMIT_BINARY_DATA",
136
+ id: String(messageIdCounter),
137
+ payload: binarySubmissionPayload,
138
+ };
139
+
140
+ dialogWindow.postMessage(message, WELSHARE_WALLET_URL);
141
+ setMessageIdCounter((prev) => prev + 1);
142
+ } catch (error) {
143
+ console.error("error uploading file", error);
144
+ // Update status to error
145
+ currentUploadRef.current.uploadPromise.reject(error as Error);
146
+ setUploadState("error");
147
+
148
+ // Call error callback
149
+ const errorMsg =
150
+ error instanceof Error ? error.message : "Upload failed";
151
+ options.callbacks.onError?.(errorMsg);
152
+ }
153
+ break;
154
+ //{insertetUid, errors}
155
+ case "BINARY_DATA_SUBMITTED":
156
+ if (!currentUploadRef.current) {
157
+ throw new Error("No upload in progress");
158
+ }
159
+ currentUploadRef.current.uploadPromise.resolve({
160
+ url: message.payload.url,
161
+ binaryFileUid: message.payload.insertedUid,
162
+ });
163
+
164
+ setUploadState("finished");
165
+ options.callbacks.onFileUploaded?.(
166
+ message.payload.insertedUid,
167
+ message.payload.url
168
+ );
169
+ break;
170
+
72
171
  default:
73
172
  console.log(
74
173
  "Received unexpected message from Welshare Wallet:",
@@ -82,7 +181,54 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
82
181
  return () => {
83
182
  window.removeEventListener("message", handleMessage);
84
183
  };
85
- }, [WELSHARE_WALLET_URL, options.callbacks]);
184
+ }, [WELSHARE_WALLET_URL, dialogWindow, messageIdCounter, options.applicationId, options.callbacks]);
185
+
186
+ /**
187
+ * Starts a file upload and returns a promise that resolves with the uploaded file URL
188
+ * @param file The file to upload
189
+ * @param reference string A reference identifier for the upload
190
+ * @returns Promise<{ url: string; binaryFileUid: string }> url usually starts with welshare:// and resolves to an encrypted storage location
191
+ */
192
+ const uploadFile = (
193
+ file: File,
194
+ reference: string
195
+ ): Promise<{ url: string; binaryFileUid: string }> => {
196
+ return new Promise((resolve, reject) => {
197
+ if (!dialogWindow) {
198
+ reject(new Error("(uploadFile) Dialog window not open"));
199
+ return;
200
+ }
201
+
202
+ const payload: RequestUploadCredentialsPayload = {
203
+ timestamp: new Date(),
204
+ applicationId: options.applicationId,
205
+ reference: reference,
206
+ fileName: file.name,
207
+ fileType: file.type,
208
+ };
209
+
210
+ setUploadState("preparing");
211
+ // (claude opus) Store the upload with the file and promise handlers on a ref
212
+ currentUploadRef.current = {
213
+ ...payload,
214
+ file,
215
+ reference,
216
+ uploadPromise: { resolve, reject },
217
+ };
218
+
219
+ const message: DialogMessage = {
220
+ type: "REQUEST_UPLOAD_CREDENTIALS",
221
+ id: String(messageIdCounter),
222
+ payload: {
223
+ ...payload,
224
+ applicationId: options.applicationId,
225
+ },
226
+ };
227
+
228
+ dialogWindow.postMessage(message, WELSHARE_WALLET_URL);
229
+ setMessageIdCounter((prev) => prev + 1);
230
+ });
231
+ };
86
232
 
87
233
  /**
88
234
  * @param schemaType a welshare schema type uid
@@ -93,7 +239,7 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
93
239
  submission: SubmissionPayload<T>
94
240
  ) => {
95
241
  if (!dialogWindow) {
96
- throw new Error("Dialog window not open");
242
+ throw new Error("(submitData) Dialog window not open");
97
243
  }
98
244
 
99
245
  const submissionPayload = {
@@ -122,12 +268,24 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
122
268
 
123
269
  const openWallet = () => {
124
270
  const width = 800;
125
- const height = 600;
271
+ const height = 700;
126
272
  const left = window.screenX + (window.outerWidth - width) / 2;
127
273
  const top = window.screenY + (window.outerHeight - height) / 2;
128
274
 
275
+ let socialParams = "";
276
+ if (options.interpolateSocials) {
277
+ const socialEntries = Object.entries(options.interpolateSocials)
278
+ .filter(([_, value]) => value !== undefined && value !== null)
279
+ .map(([key, value]) => `social.${key}=${encodeURIComponent(String(value))}`);
280
+
281
+ if (socialEntries.length > 0) {
282
+ socialParams = `&${socialEntries.join('&')}`;
283
+ }
284
+ }
285
+
286
+ const walletUrl = `${WELSHARE_WALLET_URL}?applicationId=${options.applicationId}${socialParams}`;
129
287
  const newWindow = window.open(
130
- WELSHARE_WALLET_URL,
288
+ walletUrl,
131
289
  "Welshare Wallet",
132
290
  `width=${width},height=${height},left=${left},top=${top}`
133
291
  );
@@ -142,7 +300,9 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
142
300
  storageKey,
143
301
  openWallet,
144
302
  isDialogOpen,
303
+ uploadFile,
145
304
  submitData,
305
+ uploadState,
146
306
  isSubmitting,
147
307
  };
148
308
  };
@@ -2,12 +2,20 @@
2
2
  //import { QuestionnaireResponseSchema, ReflexSubmissionSchema } from "@welshare/sdk";
3
3
  export { ConnectWelshareButton } from "./components/connect-button.js";
4
4
  // ---- hooks ----
5
- export {
6
- useWelshare
7
- } from "./hooks/use-welshare.js";
5
+ export { useWelshare } from "./hooks/use-welshare.js";
8
6
 
7
+ export {
8
+ decodeEncryptionKey,
9
+ decrypt,
10
+ encodeEncryptionKey,
11
+ encryptFile,
12
+ generateRandomAESKey,
13
+ type EncryptionKey
14
+ } from "./lib/encryption.js";
15
+ export { encryptAndUploadFile } from "./lib/uploads.js";
9
16
  //todo: import them from the SDK or a dedicated SDK constants export
10
17
  export const Schemas = {
11
18
  QuestionnaireResponse: "b14b538f-7de3-4767-ad77-464d755d78bd", //QuestionnaireResponseSchema.schemaUid,
12
- ReflexSubmission: "f5cf2d8a-1f78-4f21-b4bd-082e983b830c" //ReflexSubmissionSchema.schemaUid,
13
- };
19
+ ReflexSubmission: "f5cf2d8a-1f78-4f21-b4bd-082e983b830c", //ReflexSubmissionSchema.schemaUid,
20
+ BinaryFile: "9d696baf-483f-4cc0-b748-23a22c1705f5" //BinaryFilesSchema.schemaUid,
21
+ };
@@ -0,0 +1,110 @@
1
+ export const ALGORITHM = "AES-GCM";
2
+ export type Algorithm = "AES-GCM";
3
+
4
+ export const generateRandomAESKey = async (): Promise<CryptoKey> => {
5
+ // Generate a 256-bit AES-GCM key for file encryption
6
+ return window.crypto.subtle.generateKey(
7
+ {
8
+ name: ALGORITHM,
9
+ length: 256, // 256-bit key
10
+ },
11
+ true, // Key is extractable (needed for storage/transmission)
12
+ ["encrypt", "decrypt"] // Key usage
13
+ );
14
+ };
15
+
16
+ /// also Generates random IV (12 bytes for AES-GCM)
17
+ /// @return {arraybuffer ciphertext, uint8array iv}
18
+ export const encryptFile = async (
19
+ file: File,
20
+ key: CryptoKey
21
+ ): Promise<{ encryptedData: ArrayBuffer; iv: Uint8Array }> => {
22
+ // Read file as ArrayBuffer
23
+ const fileData = await file.arrayBuffer();
24
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
25
+
26
+ // Encrypt the file data
27
+ const encryptedData = await window.crypto.subtle.encrypt(
28
+ {
29
+ name: ALGORITHM,
30
+ iv: iv,
31
+ },
32
+ key,
33
+ fileData
34
+ );
35
+
36
+ return { encryptedData, iv };
37
+ };
38
+
39
+ export type EncryptionKey = {
40
+ algorithm: Algorithm;
41
+ key: string;
42
+ iv: string;
43
+ };
44
+
45
+ export const encodeEncryptionKey = async (
46
+ key: CryptoKey,
47
+ iv: Uint8Array
48
+ ): Promise<EncryptionKey> => {
49
+ // Export the key as raw bytes
50
+ const exportedKey = await window.crypto.subtle.exportKey("raw", key);
51
+ const keyHex = Array.from(new Uint8Array(exportedKey))
52
+ .map((b) => b.toString(16).padStart(2, "0"))
53
+ .join("");
54
+
55
+ const ivHex = Array.from(iv)
56
+ .map((b) => b.toString(16).padStart(2, "0"))
57
+ .join("");
58
+
59
+ return {
60
+ algorithm: ALGORITHM,
61
+ key: keyHex,
62
+ iv: ivHex,
63
+ };
64
+ };
65
+
66
+ export const decodeEncryptionKey = (
67
+ encryptionKey: EncryptionKey
68
+ ): { key: Uint8Array<ArrayBuffer>; iv: Uint8Array<ArrayBuffer> } => {
69
+ const keyBytes = new Uint8Array(
70
+ encryptionKey.key
71
+ .match(/.{1,2}/g)!
72
+ .map((byte: string) => parseInt(byte, 16))
73
+ );
74
+ const ivBytes = new Uint8Array(
75
+ encryptionKey.iv.match(/.{1,2}/g)!.map((byte: string) => parseInt(byte, 16))
76
+ );
77
+ return { key: keyBytes, iv: ivBytes };
78
+ };
79
+
80
+ // Helper function to decrypt a file using encoded encryption key
81
+ export const decrypt = async (
82
+ encryptedData: ArrayBuffer,
83
+ encryptionKey: EncryptionKey
84
+ ): Promise<ArrayBuffer | null> => {
85
+ try {
86
+ const { key: keyBytes, iv } = decodeEncryptionKey(encryptionKey);
87
+
88
+ const key = await window.crypto.subtle.importKey(
89
+ "raw",
90
+ keyBytes,
91
+ { name: ALGORITHM },
92
+ false,
93
+ ["decrypt"]
94
+ );
95
+
96
+ const decryptedData = await window.crypto.subtle.decrypt(
97
+ {
98
+ name: ALGORITHM,
99
+ iv,
100
+ },
101
+ key,
102
+ encryptedData
103
+ );
104
+
105
+ return decryptedData;
106
+ } catch (error) {
107
+ console.error("Failed to decrypt file:", error);
108
+ return null;
109
+ }
110
+ };
@@ -0,0 +1,29 @@
1
+ import {
2
+ encodeEncryptionKey,
3
+ encryptFile,
4
+ EncryptionKey,
5
+ generateRandomAESKey,
6
+ } from "./encryption.js";
7
+
8
+ export const encryptAndUploadFile = async (
9
+ file: File,
10
+ presignedUrl: string
11
+ ): Promise<EncryptionKey> => {
12
+ const encryptionKey = await generateRandomAESKey();
13
+ const { encryptedData, iv } = await encryptFile(file, encryptionKey);
14
+
15
+ // Upload encrypted file to S3
16
+ const uploadResponse = await fetch(presignedUrl, {
17
+ method: "PUT",
18
+ body: encryptedData,
19
+ headers: {
20
+ "Content-Type": file.type,
21
+ },
22
+ });
23
+
24
+ if (!uploadResponse.ok) {
25
+ throw new Error(`Failed to upload file ${uploadResponse.status}`);
26
+ }
27
+
28
+ return encodeEncryptionKey(encryptionKey, iv);
29
+ };
@@ -1,11 +1,11 @@
1
+ import { EncryptionKey } from "./lib/encryption.js";
2
+
1
3
  export interface DialogMessage {
2
4
  type: string;
3
5
  payload?: any;
4
6
  id?: string;
5
7
  }
6
8
 
7
- export type WelshareEnvironment = "development" | "production";
8
-
9
9
  /**
10
10
  * a welshare schema type uid
11
11
  */
@@ -18,16 +18,53 @@ export interface SubmissionPayload<T> {
18
18
  submission: T;
19
19
  }
20
20
 
21
+ export type UploadCredentials = {
22
+ presignedUrl: string;
23
+ uploadKey: string;
24
+ }
25
+
26
+ export interface RequestUploadCredentialsPayload {
27
+ timestamp?: Date;
28
+ applicationId: string;
29
+ reference: string;
30
+ fileName: string;
31
+ fileType: string;
32
+ }
33
+
34
+ export interface BinaryFileSubmissionPayload extends RequestUploadCredentialsPayload {
35
+ encryptionKey: EncryptionKey;
36
+ /// in bytes
37
+ fileSize: number;
38
+ url: string;
39
+ }
40
+
41
+ export interface RunningFileUpload extends RequestUploadCredentialsPayload {
42
+ credentials?: UploadCredentials;
43
+ file: File;
44
+ reference: string;
45
+ uploadPromise: {
46
+ resolve: (result: { url: string; binaryFileUid: string }) => void;
47
+ reject: (error: Error) => void;
48
+ };
49
+ }
50
+
21
51
  export interface DataSubmissionDialogMessage extends DialogMessage {
22
- payload: SubmissionPayload<unknown>;
52
+ payload:
53
+ | SubmissionPayload<unknown>
54
+ | BinaryFileSubmissionPayload
55
+ | RequestUploadCredentialsPayload;
23
56
  }
24
57
 
25
58
  export interface WelshareConnectionOptions {
26
59
  applicationId: string;
60
+ interpolateSocials?: {
61
+ emailAddress?: string;
62
+ privy?: string;
63
+ }
27
64
  //todo: must go into build config, not supposed to be used by users
28
65
  apiBaseUrl?: string;
29
- environment?: WelshareEnvironment;
30
66
  callbacks: {
67
+ onFileUploaded?: (insertedUid: string, url: string) => void;
31
68
  onUploaded?: (payload: SubmissionPayload<unknown>) => void;
32
69
  onError?: (error: string) => void;
33
70
  onSessionReady?: (sessionPubKey: string) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@welshare/react",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "0.2.0",
4
4
  "description": "React library for integrating with Welshare's sovereign data sharing platform",
5
5
  "keywords": [
6
6
  "react",
@@ -77,7 +77,7 @@
77
77
  "node": ">=20.0.0"
78
78
  },
79
79
  "scripts": {
80
- "lint": "eslint . --max-warnings 10",
80
+ "lint": "eslint . --max-warnings 25",
81
81
  "build": "tshy",
82
82
  "build:clean": "rm -rf ./dist",
83
83
  "test": "vitest",