@welshare/react 0.2.3 → 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 (55) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +85 -44
  3. package/dist/esm/components/connect-button.d.ts.map +1 -1
  4. package/dist/esm/components/connect-button.js +1 -4
  5. package/dist/esm/components/welshare-logo.d.ts +1 -1
  6. package/dist/esm/hooks/use-binary-uploads.d.ts +59 -0
  7. package/dist/esm/hooks/use-binary-uploads.d.ts.map +1 -0
  8. package/dist/esm/hooks/use-binary-uploads.js +93 -0
  9. package/dist/esm/hooks/use-welshare.d.ts.map +1 -1
  10. package/dist/esm/hooks/use-welshare.js +16 -8
  11. package/dist/esm/index.d.ts +3 -1
  12. package/dist/esm/index.d.ts.map +1 -1
  13. package/dist/esm/index.js +8 -5
  14. package/dist/esm/lib/encryption.d.ts.map +1 -1
  15. package/dist/esm/lib/encryption.js +3 -13
  16. package/dist/esm/lib/uploads.d.ts.map +1 -1
  17. package/dist/esm/lib/uploads.js +1 -3
  18. package/dist/esm/types.d.ts +11 -0
  19. package/dist/esm/types.d.ts.map +1 -1
  20. package/dist/esm/utils.d.ts.map +1 -1
  21. package/dist/node_modules/@welshare/react/.turbo/turbo-build.log +5 -0
  22. package/dist/node_modules/@welshare/react/.turbo/turbo-lint.log +14 -0
  23. package/dist/node_modules/@welshare/react/LICENSE +1 -1
  24. package/dist/node_modules/@welshare/react/README.md +85 -44
  25. package/dist/node_modules/@welshare/react/dist/esm/components/connect-button.d.ts.map +1 -1
  26. package/dist/node_modules/@welshare/react/dist/esm/components/connect-button.js +1 -4
  27. package/dist/node_modules/@welshare/react/dist/esm/components/welshare-logo.d.ts +1 -1
  28. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-binary-uploads.d.ts +59 -0
  29. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-binary-uploads.d.ts.map +1 -0
  30. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-binary-uploads.js +93 -0
  31. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-welshare.d.ts.map +1 -1
  32. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-welshare.js +16 -8
  33. package/dist/node_modules/@welshare/react/dist/esm/index.d.ts +3 -1
  34. package/dist/node_modules/@welshare/react/dist/esm/index.d.ts.map +1 -1
  35. package/dist/node_modules/@welshare/react/dist/esm/index.js +8 -5
  36. package/dist/node_modules/@welshare/react/dist/esm/lib/encryption.d.ts.map +1 -1
  37. package/dist/node_modules/@welshare/react/dist/esm/lib/encryption.js +3 -13
  38. package/dist/node_modules/@welshare/react/dist/esm/lib/uploads.d.ts.map +1 -1
  39. package/dist/node_modules/@welshare/react/dist/esm/lib/uploads.js +1 -3
  40. package/dist/node_modules/@welshare/react/dist/esm/types.d.ts +11 -0
  41. package/dist/node_modules/@welshare/react/dist/esm/types.d.ts.map +1 -1
  42. package/dist/node_modules/@welshare/react/dist/esm/utils.d.ts.map +1 -1
  43. package/dist/node_modules/@welshare/react/eslint.config.mjs +2 -2
  44. package/dist/node_modules/@welshare/react/package.json +5 -2
  45. package/dist/node_modules/@welshare/react/src/components/connect-button.tsx +1 -4
  46. package/dist/node_modules/@welshare/react/src/components/welshare-logo.tsx +1 -1
  47. package/dist/node_modules/@welshare/react/src/hooks/use-binary-uploads.ts +181 -0
  48. package/dist/node_modules/@welshare/react/src/hooks/use-welshare.ts +24 -10
  49. package/dist/node_modules/@welshare/react/src/index.ts +22 -5
  50. package/dist/node_modules/@welshare/react/src/lib/encryption.ts +3 -13
  51. package/dist/node_modules/@welshare/react/src/lib/uploads.ts +2 -4
  52. package/dist/node_modules/@welshare/react/src/types.ts +21 -10
  53. package/dist/node_modules/@welshare/react/src/utils.ts +1 -5
  54. package/dist/node_modules/@welshare/react/tsconfig.json +3 -12
  55. package/package.json +5 -2
@@ -0,0 +1,181 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { decrypt } from "../lib/encryption.js";
3
+ import { type EncryptionKey } from "../utils.js";
4
+ import type {
5
+ RequestUploadCredentialsPayload,
6
+ UploadCredentials,
7
+ } from "../types.js";
8
+ import {
9
+ Nillion,
10
+ WelshareApi,
11
+ resolveEnvironment,
12
+ type WelshareApiEnvironment,
13
+ type WelshareEnvironmentName,
14
+ } from "@welshare/sdk";
15
+
16
+ export interface UseBinaryUploadsOptions {
17
+ /**
18
+ * The user's session keypair for authentication.
19
+ */
20
+ keypair: Nillion.Keypair | null | undefined;
21
+ /**
22
+ * The Welshare environment to use for API calls.
23
+ */
24
+ environment: WelshareApiEnvironment | WelshareEnvironmentName;
25
+ }
26
+
27
+ export interface UseBinaryUploadsResult {
28
+ /**
29
+ * Request upload credentials (presigned URL) for uploading an encrypted file.
30
+ */
31
+ createUploadCredentials: (
32
+ payload: RequestUploadCredentialsPayload
33
+ ) => Promise<UploadCredentials>;
34
+
35
+ /**
36
+ * Download and decrypt a file by its document ID.
37
+ * Only works for files owned by the current user.
38
+ */
39
+ downloadAndDecryptFile: (documentId: string) => Promise<File | undefined>;
40
+ /**
41
+ * Whether an operation is currently running.
42
+ */
43
+ isRunning: boolean;
44
+ /**
45
+ * Error message if the last operation failed.
46
+ */
47
+ error: string | null;
48
+ }
49
+
50
+ /**
51
+ * Hook for managing binary file uploads and downloads with Welshare.
52
+ *
53
+ * This hook provides functionality to:
54
+ * - Request presigned URLs for uploading encrypted files to S3
55
+ * - Download and decrypt files from Nillion storage
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * const { createUploadCredentials, downloadAndDecryptFile, isRunning, error } = useBinaryUploads({
60
+ * keypair: sessionKeyPair,
61
+ * environment: 'production',
62
+ * });
63
+ *
64
+ * // Upload flow: get credentials, encrypt, upload to S3, then submit metadata via submitBinaryData
65
+ * const credentials = await createUploadCredentials({
66
+ * applicationId: 'my-app',
67
+ * reference: 'user-photo',
68
+ * fileName: 'photo.jpg',
69
+ * fileType: 'image/jpeg',
70
+ * });
71
+ *
72
+ * // Download and decrypt
73
+ * const file = await downloadAndDecryptFile(documentId);
74
+ * ```
75
+ */
76
+ export const useBinaryUploads = (
77
+ options: UseBinaryUploadsOptions
78
+ ): UseBinaryUploadsResult => {
79
+ const [isRunning, setIsRunning] = useState(false);
80
+ const [error, setError] = useState<string | null>(null);
81
+ const mountedRef = useRef(true);
82
+
83
+ useEffect(() => {
84
+ mountedRef.current = true;
85
+ return () => {
86
+ mountedRef.current = false;
87
+ };
88
+ }, []);
89
+
90
+ const resolvedEnvironment = useMemo(
91
+ () => resolveEnvironment(options.environment),
92
+ [options.environment]
93
+ );
94
+ const { keypair } = options;
95
+
96
+ const createUploadCredentials = useCallback(
97
+ async (
98
+ payload: RequestUploadCredentialsPayload
99
+ ): Promise<UploadCredentials> => {
100
+ if (!keypair) {
101
+ throw new Error("No keypair available");
102
+ }
103
+
104
+ const { reference, fileName, fileType } = payload;
105
+
106
+ const { presignedUrl, uploadKey } =
107
+ await WelshareApi.fetchS3WriteDelegation(
108
+ keypair,
109
+ { reference, fileName, fileType },
110
+ resolvedEnvironment
111
+ );
112
+
113
+ return { presignedUrl, uploadKey };
114
+ },
115
+ [keypair, resolvedEnvironment]
116
+ );
117
+
118
+ /**
119
+ * Downloads and decrypts a file by its document ID from Nillion.
120
+ */
121
+ const downloadAndDecryptFile = useCallback(
122
+ async (documentId: string): Promise<File | undefined> => {
123
+ if (!keypair) {
124
+ throw new Error("No keypair available");
125
+ }
126
+
127
+ try {
128
+ setIsRunning(true);
129
+ setError(null);
130
+
131
+ const { binaryFile, data: downloadResponse } =
132
+ await WelshareApi.fetchBinaryData(
133
+ keypair,
134
+ resolvedEnvironment,
135
+ documentId
136
+ );
137
+
138
+ const encodedEncryptionKey: EncryptionKey = JSON.parse(
139
+ binaryFile.encryption_key
140
+ );
141
+
142
+ const decryptedData = await decrypt(
143
+ await downloadResponse,
144
+ encodedEncryptionKey
145
+ );
146
+
147
+ if (!decryptedData) {
148
+ throw new Error("Failed to decrypt file (received null)");
149
+ }
150
+
151
+ const decryptedFile = new File([decryptedData], binaryFile.file_name, {
152
+ type: binaryFile.file_type,
153
+ });
154
+
155
+ return decryptedFile;
156
+ } catch (err) {
157
+ console.error("Error during file download/decryption:", err);
158
+ const errorMessage =
159
+ err instanceof Error
160
+ ? err.message
161
+ : "Failed to download/decrypt file";
162
+ if (mountedRef.current) {
163
+ setError(errorMessage);
164
+ }
165
+ return undefined;
166
+ } finally {
167
+ if (mountedRef.current) {
168
+ setIsRunning(false);
169
+ }
170
+ }
171
+ },
172
+ [keypair, resolvedEnvironment]
173
+ );
174
+
175
+ return {
176
+ createUploadCredentials,
177
+ downloadAndDecryptFile,
178
+ isRunning,
179
+ error,
180
+ };
181
+ };
@@ -8,6 +8,10 @@ import {
8
8
  UploadCredentials,
9
9
  WelshareConnectionOptions,
10
10
  } from "@/types.js";
11
+ import {
12
+ getBaseUrl,
13
+ WELSHARE_API_ENVIRONMENT,
14
+ } from "@welshare/sdk/environment";
11
15
  import { useEffect, useRef, useState } from "react";
12
16
  import { encryptAndUploadFile } from "../lib/uploads.js";
13
17
 
@@ -24,12 +28,17 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
24
28
  // (claude opus) Use ref to control current upload in effect without triggering re-renders
25
29
  const currentUploadRef = useRef<RunningFileUpload>(null);
26
30
 
31
+ // Resolve the base URL from environment or apiBaseUrl
32
+ const resolvedBaseUrl = props.environment
33
+ ? getBaseUrl(props.environment)
34
+ : (props.apiBaseUrl ?? getBaseUrl(WELSHARE_API_ENVIRONMENT.production));
35
+
27
36
  const options: WelshareConnectionOptions = {
28
- apiBaseUrl: "https://wallet.welshare.app",
29
37
  ...props,
38
+ apiBaseUrl: resolvedBaseUrl,
30
39
  };
31
40
 
32
- const WELSHARE_WALLET_URL = `${options.apiBaseUrl}/wallet-external`;
41
+ const WELSHARE_WALLET_URL = `${resolvedBaseUrl}/wallet-external`;
33
42
 
34
43
  useEffect(() => {
35
44
  const handleMessage = async (event: MessageEvent<DialogMessage>) => {
@@ -181,7 +190,13 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
181
190
  return () => {
182
191
  window.removeEventListener("message", handleMessage);
183
192
  };
184
- }, [WELSHARE_WALLET_URL, dialogWindow, messageIdCounter, options.applicationId, options.callbacks]);
193
+ }, [
194
+ WELSHARE_WALLET_URL,
195
+ dialogWindow,
196
+ messageIdCounter,
197
+ options.applicationId,
198
+ options.callbacks,
199
+ ]);
185
200
 
186
201
  /**
187
202
  * Starts a file upload and returns a promise that resolves with the uploaded file URL
@@ -219,10 +234,7 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
219
234
  const message: DialogMessage = {
220
235
  type: "REQUEST_UPLOAD_CREDENTIALS",
221
236
  id: String(messageIdCounter),
222
- payload: {
223
- ...payload,
224
- applicationId: options.applicationId,
225
- },
237
+ payload: { ...payload, applicationId: options.applicationId },
226
238
  };
227
239
 
228
240
  dialogWindow.postMessage(message, WELSHARE_WALLET_URL);
@@ -276,10 +288,12 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
276
288
  if (options.interpolateSocials) {
277
289
  const socialEntries = Object.entries(options.interpolateSocials)
278
290
  .filter(([_, value]) => value !== undefined && value !== null)
279
- .map(([key, value]) => `social.${key}=${encodeURIComponent(String(value))}`);
280
-
291
+ .map(
292
+ ([key, value]) => `social.${key}=${encodeURIComponent(String(value))}`
293
+ );
294
+
281
295
  if (socialEntries.length > 0) {
282
- socialParams = `&${socialEntries.join('&')}`;
296
+ socialParams = `&${socialEntries.join("&")}`;
283
297
  }
284
298
  }
285
299
 
@@ -1,20 +1,37 @@
1
- //todo: this is not tree shaken and leaks nillion dependencies transitively. Consider pulling it up
2
- //import { QuestionnaireResponseSchema, ReflexSubmissionSchema } from "@welshare/sdk";
1
+ // ---- Components ----
3
2
  export { ConnectWelshareButton } from "./components/connect-button.js";
4
3
  export { WelshareLogo } from "./components/welshare-logo.js";
5
- // ---- hooks ----
4
+
5
+ // ---- Hooks ----
6
6
  export { useWelshare } from "./hooks/use-welshare.js";
7
+ export {
8
+ useBinaryUploads,
9
+ type UseBinaryUploadsOptions,
10
+ type UseBinaryUploadsResult,
11
+ } from "./hooks/use-binary-uploads.js";
7
12
 
13
+ // ---- Environment (re-exported from @welshare/sdk) ----
14
+ export {
15
+ WELSHARE_API_ENVIRONMENT,
16
+ resolveEnvironment,
17
+ getBaseUrl,
18
+ type WelshareApiEnvironment,
19
+ type WelshareEnvironmentName,
20
+ type NillionClusterConfig,
21
+ } from "@welshare/sdk/environment";
22
+
23
+ // ---- Utils ----
8
24
  export {
9
25
  decrypt,
10
26
  encodeEncryptionKey,
11
27
  encryptFile,
12
- generateRandomAESKey
28
+ generateRandomAESKey,
13
29
  } from "./lib/encryption.js";
14
30
  export { decodeEncryptionKey, type EncryptionKey } from "./utils.js";
15
31
 
16
32
  export { browserDownload, encryptAndUploadFile } from "./lib/uploads.js";
17
- //todo: import them from the SDK or a dedicated SDK constants export
33
+
34
+ //todo: import them from the SDK
18
35
  export const Schemas = {
19
36
  QuestionnaireResponse: "b14b538f-7de3-4767-ad77-464d755d78bd", //QuestionnaireResponseSchema.schemaUid,
20
37
  ReflexSubmission: "f5cf2d8a-1f78-4f21-b4bd-082e983b830c", //ReflexSubmissionSchema.schemaUid,
@@ -24,10 +24,7 @@ export const encryptFile = async (
24
24
 
25
25
  // Encrypt the file data
26
26
  const encryptedData = await window.crypto.subtle.encrypt(
27
- {
28
- name: ALGORITHM,
29
- iv: iv,
30
- },
27
+ { name: ALGORITHM, iv: iv },
31
28
  key,
32
29
  fileData
33
30
  );
@@ -49,11 +46,7 @@ export const encodeEncryptionKey = async (
49
46
  .map((b) => b.toString(16).padStart(2, "0"))
50
47
  .join("");
51
48
 
52
- return {
53
- algorithm: ALGORITHM,
54
- key: keyHex,
55
- iv: ivHex,
56
- };
49
+ return { algorithm: ALGORITHM, key: keyHex, iv: ivHex };
57
50
  };
58
51
 
59
52
  // Helper function to decrypt a file using encoded encryption key
@@ -73,10 +66,7 @@ export const decrypt = async (
73
66
  );
74
67
 
75
68
  const decryptedData = await window.crypto.subtle.decrypt(
76
- {
77
- name: ALGORITHM,
78
- iv: iv as BufferSource,
79
- },
69
+ { name: ALGORITHM, iv: iv as BufferSource },
80
70
  key,
81
71
  encryptedData
82
72
  );
@@ -16,9 +16,7 @@ export const encryptAndUploadFile = async (
16
16
  const uploadResponse = await fetch(presignedUrl, {
17
17
  method: "PUT",
18
18
  body: encryptedData,
19
- headers: {
20
- "Content-Type": file.type,
21
- },
19
+ headers: { "Content-Type": file.type },
22
20
  });
23
21
 
24
22
  if (!uploadResponse.ok) {
@@ -37,4 +35,4 @@ export const browserDownload = (decryptedFile: File) => {
37
35
  a.click();
38
36
  document.body.removeChild(a);
39
37
  window.URL.revokeObjectURL(downloadUrl);
40
- }
38
+ };
@@ -1,4 +1,8 @@
1
1
  import { EncryptionKey } from "./utils.js";
2
+ import type {
3
+ WelshareApiEnvironment,
4
+ WelshareEnvironmentName,
5
+ } from "@welshare/sdk/environment";
2
6
 
3
7
  export interface DialogMessage {
4
8
  type: string;
@@ -9,7 +13,7 @@ export interface DialogMessage {
9
13
  /**
10
14
  * a welshare schema type uid
11
15
  */
12
- export type SubmissionSchemaId = string
16
+ export type SubmissionSchemaId = string;
13
17
 
14
18
  export interface SubmissionPayload<T> {
15
19
  applicationId: string;
@@ -18,10 +22,7 @@ export interface SubmissionPayload<T> {
18
22
  submission: T;
19
23
  }
20
24
 
21
- export type UploadCredentials = {
22
- presignedUrl: string;
23
- uploadKey: string;
24
- }
25
+ export type UploadCredentials = { presignedUrl: string; uploadKey: string };
25
26
 
26
27
  export interface RequestUploadCredentialsPayload {
27
28
  timestamp?: Date;
@@ -31,7 +32,8 @@ export interface RequestUploadCredentialsPayload {
31
32
  fileType: string;
32
33
  }
33
34
 
34
- export interface BinaryFileSubmissionPayload extends RequestUploadCredentialsPayload {
35
+ export interface BinaryFileSubmissionPayload
36
+ extends RequestUploadCredentialsPayload {
35
37
  encryptionKey: EncryptionKey;
36
38
  /// in bytes
37
39
  fileSize: number;
@@ -49,8 +51,8 @@ export interface RunningFileUpload extends RequestUploadCredentialsPayload {
49
51
  }
50
52
 
51
53
  export interface DataSubmissionDialogMessage extends DialogMessage {
52
- payload:
53
- | SubmissionPayload<unknown>
54
+ payload:
55
+ | SubmissionPayload<unknown>
54
56
  | BinaryFileSubmissionPayload
55
57
  | RequestUploadCredentialsPayload;
56
58
  }
@@ -61,8 +63,17 @@ export interface WelshareConnectionOptions {
61
63
  emailAddress?: string;
62
64
  privy?: string;
63
65
  twitter?: string;
64
- }
65
- //todo: must go into build config, not supposed to be used by users
66
+ };
67
+ /**
68
+ * The Welshare environment to connect to.
69
+ * Can be an environment name ('production', 'staging', etc.) or a full environment object.
70
+ * Takes precedence over apiBaseUrl if both are provided.
71
+ */
72
+ environment?: WelshareApiEnvironment | WelshareEnvironmentName;
73
+ /**
74
+ * @deprecated Use `environment` instead for type-safe configuration.
75
+ * The base URL of the Welshare API. Defaults to production.
76
+ */
66
77
  apiBaseUrl?: string;
67
78
  callbacks: {
68
79
  onFileUploaded?: (insertedUid: string, url: string) => void;
@@ -1,11 +1,7 @@
1
1
  export const ALGORITHM = "AES-GCM";
2
2
  export type Algorithm = "AES-GCM";
3
3
 
4
- export type EncryptionKey = {
5
- algorithm: Algorithm;
6
- key: string;
7
- iv: string;
8
- };
4
+ export type EncryptionKey = { algorithm: Algorithm; key: string; iv: string };
9
5
 
10
6
  export const decodeEncryptionKey = (
11
7
  encryptionKey: EncryptionKey
@@ -2,20 +2,11 @@
2
2
  "extends": "@workspace/typescript-config/react-library.json",
3
3
  "compilerOptions": {
4
4
  "baseUrl": ".",
5
- "paths": {
6
- "@/*": ["./src/*"],
7
- "@/types/*": ["./types/*"],
8
- },
9
- "plugins": [
10
- ],
5
+ "paths": { "@/*": ["./src/*"], "@/types/*": ["./types/*"] },
6
+ "plugins": [],
11
7
  "allowJs": true,
12
8
  "jsx": "react-jsx"
13
9
  },
14
- "include": [
15
- "next-env.d.ts",
16
- "next.config.ts",
17
- "**/*.ts",
18
- "**/*.tsx",
19
- ],
10
+ "include": ["next-env.d.ts", "next.config.ts", "**/*.ts", "**/*.tsx"],
20
11
  "exclude": ["node_modules"]
21
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@welshare/react",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
4
4
  "description": "React library for integrating with Welshare's sovereign data sharing platform",
5
5
  "keywords": [
6
6
  "react",
@@ -22,6 +22,9 @@
22
22
  },
23
23
  "homepage": "https://welshare.health",
24
24
  "type": "module",
25
+ "dependencies": {
26
+ "@welshare/sdk": "0.2.0"
27
+ },
25
28
  "peerDependencies": {
26
29
  "react": "^19",
27
30
  "react-dom": "^19"
@@ -81,7 +84,7 @@
81
84
  "LICENSE"
82
85
  ],
83
86
  "engines": {
84
- "node": ">=20.0.0"
87
+ "node": "^22.0.0"
85
88
  },
86
89
  "scripts": {
87
90
  "lint": "eslint . --max-warnings 25",