@striae-org/striae 5.2.0 → 5.3.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 (105) hide show
  1. package/.env.example +36 -33
  2. package/README.md +5 -46
  3. package/app/components/actions/case-export/core-export.ts +2 -174
  4. package/app/components/actions/case-export/download-handlers.ts +83 -750
  5. package/app/components/actions/case-export/index.ts +6 -30
  6. package/app/components/actions/case-export/metadata-helpers.ts +0 -78
  7. package/app/components/actions/case-export/types-constants.ts +0 -43
  8. package/app/components/actions/case-import/confirmation-import.ts +13 -14
  9. package/app/components/actions/case-import/zip-processing.ts +92 -12
  10. package/app/components/actions/generate-pdf.ts +3 -2
  11. package/app/components/audit/user-audit-viewer.tsx +0 -19
  12. package/app/components/audit/viewer/audit-viewer-header.tsx +0 -33
  13. package/app/components/navbar/case-modals/archive-case-modal.tsx +1 -1
  14. package/app/components/navbar/navbar.tsx +1 -1
  15. package/app/components/sidebar/case-import/case-import.module.css +35 -0
  16. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +59 -3
  17. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +2 -4
  18. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +1 -1
  19. package/app/components/sidebar/notes/class-details-shared.ts +2 -2
  20. package/app/components/toast/toast.module.css +36 -0
  21. package/app/components/toast/toast.tsx +6 -2
  22. package/app/components/user/manage-profile.tsx +4 -3
  23. package/app/config-example/config.json +1 -2
  24. package/app/root.tsx +0 -7
  25. package/app/routes/_index.tsx +1 -1
  26. package/app/routes/auth/login.example.tsx +22 -103
  27. package/app/routes/auth/route.ts +1 -1
  28. package/app/routes/striae/striae.tsx +53 -59
  29. package/app/services/firebase/index.ts +0 -3
  30. package/app/types/export.ts +1 -2
  31. package/app/utils/auth/index.ts +0 -1
  32. package/app/utils/data/permissions.ts +3 -2
  33. package/package.json +10 -17
  34. package/public/_headers +0 -4
  35. package/public/_routes.json +0 -1
  36. package/worker-configuration.d.ts +20 -17
  37. package/workers/audit-worker/src/audit-worker.example.ts +9 -806
  38. package/workers/audit-worker/src/config.ts +7 -0
  39. package/workers/audit-worker/src/crypto/data-at-rest.ts +410 -0
  40. package/workers/audit-worker/src/handlers/audit-routes.ts +125 -0
  41. package/workers/audit-worker/src/storage/audit-storage.ts +99 -0
  42. package/workers/audit-worker/src/types.ts +56 -0
  43. package/workers/audit-worker/worker-configuration.d.ts +1 -1
  44. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  45. package/workers/data-worker/src/config.ts +11 -0
  46. package/workers/data-worker/src/data-worker.example.ts +21 -942
  47. package/workers/data-worker/src/handlers/decrypt-export.ts +118 -0
  48. package/workers/data-worker/src/handlers/signing.ts +174 -0
  49. package/workers/data-worker/src/handlers/storage-routes.ts +129 -0
  50. package/workers/data-worker/src/registry/key-registry.ts +368 -0
  51. package/workers/data-worker/src/types.ts +46 -0
  52. package/workers/data-worker/worker-configuration.d.ts +1 -1
  53. package/workers/data-worker/wrangler.jsonc.example +1 -1
  54. package/workers/image-worker/worker-configuration.d.ts +1 -1
  55. package/workers/image-worker/wrangler.jsonc.example +1 -1
  56. package/workers/pdf-worker/worker-configuration.d.ts +2 -3
  57. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  58. package/workers/user-worker/src/auth.ts +30 -0
  59. package/workers/user-worker/src/cleanup/account-deletion.ts +337 -0
  60. package/workers/user-worker/src/config.ts +4 -0
  61. package/workers/user-worker/src/encryption-utils.ts +25 -0
  62. package/workers/user-worker/src/firebase/admin.ts +152 -0
  63. package/workers/user-worker/src/handlers/user-routes.ts +242 -0
  64. package/workers/user-worker/src/registry/user-kv.ts +172 -0
  65. package/workers/user-worker/src/storage/user-records.ts +34 -0
  66. package/workers/user-worker/src/types.ts +106 -0
  67. package/workers/user-worker/src/user-worker.example.ts +18 -964
  68. package/workers/user-worker/worker-configuration.d.ts +4 -2
  69. package/workers/user-worker/wrangler.jsonc.example +12 -1
  70. package/wrangler.toml.example +1 -1
  71. package/app/components/actions/case-export/data-processing.ts +0 -223
  72. package/app/components/sidebar/case-export/case-export.module.css +0 -418
  73. package/app/components/sidebar/case-export/case-export.tsx +0 -310
  74. package/app/types/exceljs-bare.d.ts +0 -9
  75. package/app/utils/auth/auth.ts +0 -11
  76. package/public/.well-known/security.txt +0 -6
  77. package/public/favicon.ico +0 -0
  78. package/public/icon-256.png +0 -0
  79. package/public/icon-512.png +0 -0
  80. package/public/manifest.json +0 -39
  81. package/public/shortcut.png +0 -0
  82. package/public/social-image.png +0 -0
  83. package/public/vendor/exceljs.LICENSE +0 -22
  84. package/public/vendor/exceljs.bare.min.js +0 -45
  85. package/scripts/deploy-all.sh +0 -166
  86. package/scripts/deploy-config/modules/env-utils.sh +0 -322
  87. package/scripts/deploy-config/modules/keys.sh +0 -404
  88. package/scripts/deploy-config/modules/prompt.sh +0 -372
  89. package/scripts/deploy-config/modules/scaffolding.sh +0 -336
  90. package/scripts/deploy-config/modules/validation.sh +0 -365
  91. package/scripts/deploy-config.sh +0 -236
  92. package/scripts/deploy-pages-secrets.sh +0 -231
  93. package/scripts/deploy-pages.sh +0 -34
  94. package/scripts/deploy-primershear-emails.sh +0 -167
  95. package/scripts/deploy-worker-secrets.sh +0 -374
  96. package/scripts/dev.cjs +0 -23
  97. package/scripts/install-workers.sh +0 -88
  98. package/scripts/run-eslint.cjs +0 -43
  99. package/scripts/update-compatibility-dates.cjs +0 -124
  100. package/scripts/update-markdown-versions.cjs +0 -43
  101. package/workers/keys-worker/package.json +0 -18
  102. package/workers/keys-worker/src/keys.example.ts +0 -67
  103. package/workers/keys-worker/src/keys.ts +0 -67
  104. package/workers/keys-worker/worker-configuration.d.ts +0 -7447
  105. package/workers/keys-worker/wrangler.jsonc.example +0 -15
package/.env.example CHANGED
@@ -29,31 +29,15 @@ MEASUREMENT_ID=your_firebase_measurement_id_here
29
29
  # ===============================
30
30
  # FIREBASE SERVICE ACCOUNT CONFIGURATION
31
31
  # ===============================
32
- # **PULLED FROM ADMIN-SERVICE.JSON FILE ONLY**
33
- PROJECT_ID=your_firebase_project_id_here
32
+ # Shared PROJECT_ID above is also sourced from app/config/admin-service.json by the deploy scripts.
34
33
  FIREBASE_SERVICE_ACCOUNT_EMAIL=your_firebase_service_account_email_here
35
34
  FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY=your_firebase_service_account_private_key_here
36
35
 
37
36
  # ================================
38
- # PAGES WORKER ENVIRONMENT VARIABLES
37
+ # SIGNING & ENCRYPTION KEYS & REGISTRIES
39
38
  # ================================
40
- PAGES_PROJECT_NAME=your_pages_project_name_here
41
- PAGES_CUSTOM_DOMAIN=your_custom_domain_here
42
39
 
43
- # ================================
44
- # KEYS WORKER ENVIRONMENT VARIABLES
45
- # ================================
46
- # Worker domains can be entered manually or auto-generated as a 10-character subdomain of PAGES_CUSTOM_DOMAIN during scripts/deploy-config.sh.
47
- KEYS_WORKER_NAME=your_keys_worker_name_here
48
- KEYS_WORKER_DOMAIN=your_keys_worker_domain_here
49
- KEYS_AUTH=your_custom_keys_auth_token_here
50
-
51
- # ================================
52
- # USER WORKER ENVIRONMENT VARIABLES
53
- # ================================
54
- USER_WORKER_NAME=your_user_worker_name_here
55
- USER_WORKER_DOMAIN=your_user_worker_domain_here
56
- KV_STORE_ID=your_kv_store_id_here
40
+ # USER WORKER KV ENCRYPTION CONFIGURATION
57
41
  USER_KV_ENCRYPTION_PRIVATE_KEY=your_user_kv_encryption_private_key_here
58
42
  USER_KV_ENCRYPTION_KEY_ID=your_user_kv_encryption_key_id_here
59
43
  USER_KV_ENCRYPTION_PUBLIC_KEY=your_user_kv_encryption_public_key_here
@@ -63,35 +47,54 @@ USER_KV_ENCRYPTION_PUBLIC_KEY=your_user_kv_encryption_public_key_here
63
47
  USER_KV_WRITE_ENDPOINTS_ENABLED=true
64
48
  # Optional key registry for rotation-safe USER_DB reads.
65
49
  # JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
66
- USER_KV_ENCRYPTION_KEYS_JSON='{"activeKeyId":"your_user_kv_active_encryption_key_id_here","keys":{"your_user_kv_active_encryption_key_id_here":"your_user_kv_encryption_private_key_here"}}'
67
- USER_KV_ENCRYPTION_ACTIVE_KEY_ID=your_user_kv_active_encryption_key_id_here
50
+ USER_KV_ENCRYPTION_KEYS_JSON='{}'
51
+ USER_KV_ENCRYPTION_ACTIVE_KEY_ID=
68
52
 
69
- # ================================
70
- # DATA WORKER ENVIRONMENT VARIABLES
71
- # ================================
72
- DATA_WORKER_NAME=your_data_worker_name_here
73
- DATA_BUCKET_NAME=your_data_bucket_name_here
74
- FILES_BUCKET_NAME=your_files_bucket_name_here
75
- DATA_WORKER_DOMAIN=your_data_worker_domain_here
76
- # Auto-generated by scripts/deploy-config.sh when placeholders are detected.
53
+ # DATA WORKER MANIFEST SIGNING CONFIGURATION
77
54
  MANIFEST_SIGNING_PRIVATE_KEY=your_manifest_signing_private_key_here
78
55
  MANIFEST_SIGNING_KEY_ID=your_manifest_signing_key_id_here
79
56
  MANIFEST_SIGNING_PUBLIC_KEY=your_manifest_signing_public_key_here
57
+
58
+ # DATA EXPORT ENCRYPTION CONFIGURATION
80
59
  EXPORT_ENCRYPTION_PRIVATE_KEY=your_export_encryption_private_key_here
81
60
  EXPORT_ENCRYPTION_KEY_ID=your_export_encryption_key_id_here
82
61
  EXPORT_ENCRYPTION_PUBLIC_KEY=your_export_encryption_public_key_here
83
62
  # Optional key registry for export decrypt compatibility.
84
63
  # JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
85
- EXPORT_ENCRYPTION_KEYS_JSON='{"activeKeyId":"your_export_encryption_active_key_id_here","keys":{"your_export_encryption_active_key_id_here":"your_export_encryption_private_key_here"}}'
86
- EXPORT_ENCRYPTION_ACTIVE_KEY_ID=your_export_encryption_active_key_id_here
64
+ EXPORT_ENCRYPTION_KEYS_JSON='{}'
65
+ EXPORT_ENCRYPTION_ACTIVE_KEY_ID=
66
+
67
+ # DATA-AT-REST ENCRYPTION CONFIGURATION
87
68
  DATA_AT_REST_ENCRYPTION_ENABLED=true
88
69
  DATA_AT_REST_ENCRYPTION_PRIVATE_KEY=your_data_at_rest_encryption_private_key_here
89
70
  DATA_AT_REST_ENCRYPTION_KEY_ID=your_data_at_rest_encryption_key_id_here
90
71
  DATA_AT_REST_ENCRYPTION_PUBLIC_KEY=your_data_at_rest_encryption_public_key_here
91
72
  # Optional key registry for data/files/audit decryption compatibility.
92
73
  # JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
93
- DATA_AT_REST_ENCRYPTION_KEYS_JSON='{"activeKeyId":"your_data_at_rest_active_encryption_key_id_here","keys":{"your_data_at_rest_active_encryption_key_id_here":"your_data_at_rest_encryption_private_key_here"}}'
94
- DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID=your_data_at_rest_active_encryption_key_id_here
74
+ DATA_AT_REST_ENCRYPTION_KEYS_JSON='{}'
75
+ DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID=
76
+
77
+ # ================================
78
+ # PAGES WORKER ENVIRONMENT VARIABLES
79
+ # ================================
80
+ PAGES_PROJECT_NAME=your_pages_project_name_here
81
+ PAGES_CUSTOM_DOMAIN=your_custom_domain_here
82
+
83
+ # ================================
84
+ # USER WORKER ENVIRONMENT VARIABLES
85
+ # ================================
86
+ # Worker domains can be entered manually or auto-generated as a shared subdomain during scripts/deploy-config.sh.
87
+ USER_WORKER_NAME=your_user_worker_name_here
88
+ USER_WORKER_DOMAIN=your_user_worker_domain_here
89
+ KV_STORE_ID=your_kv_store_id_here
90
+
91
+ # ================================
92
+ # DATA WORKER ENVIRONMENT VARIABLES
93
+ # ================================
94
+ DATA_WORKER_NAME=your_data_worker_name_here
95
+ DATA_BUCKET_NAME=your_data_bucket_name_here
96
+ FILES_BUCKET_NAME=your_files_bucket_name_here
97
+ DATA_WORKER_DOMAIN=your_data_worker_domain_here
95
98
 
96
99
  # ================================
97
100
  # AUDIT WORKER ENVIRONMENT VARIABLES
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Striae is a cloud-native forensic annotation application for firearms identification, built with React Router and Cloudflare Workers.
4
4
 
5
- This npm package publishes the Striae application source and deployment scaffolding for teams that run their own Striae environment.
5
+ This npm package publishes the Striae application source for teams that run/develop their own Striae environment.
6
6
 
7
7
  ## Live Project
8
8
 
@@ -21,53 +21,11 @@ This npm package publishes the Striae application source and deployment scaffold
21
21
  - Not a small client SDK.
22
22
  - Not a zero-config, ready-to-run desktop app.
23
23
 
24
- ## npm Package, Installation, and Full Deployment
25
-
26
- **Striae Package Links**
24
+ ## Striae Package Links
27
25
 
28
26
  - npmjs: [https://www.npmjs.com/package/@striae-org/striae](https://www.npmjs.com/package/@striae-org/striae)
29
27
  - GitHub Packages: [https://github.com/orgs/striae-org/packages/npm/package/striae](https://github.com/orgs/striae-org/packages/npm/package/striae)
30
28
 
31
- 1) Install the latest package:
32
-
33
- ```bash
34
- npm i @striae-org/striae
35
- ```
36
-
37
- 2) Copy the package scaffold into the project root
38
-
39
- ```bash
40
- cp -R node_modules/@striae-org/striae/. .
41
- ```
42
-
43
- 3) Reinstall using Striae's own package.json (includes dev deps like wrangler/react-router)
44
-
45
- ```bash
46
- rm -rf node_modules package-lock.json
47
- npm install
48
- ```
49
-
50
- 4) Prepare Firebase admin credentials (required before deploy-config can pass)
51
-
52
- ```bash
53
- mkdir -p app/config
54
- cp -f app/config-example/admin-service.json app/config/admin-service.json
55
- ```
56
-
57
- 5) Replace `app/config/admin-service.json` with your actual Firebase service account JSON
58
-
59
- 6) Authenticate Cloudflare CLI
60
-
61
- ```bash
62
- npx wrangler login
63
- ```
64
-
65
- 7) Run guided config + full deployment
66
-
67
- ```bash
68
- npm run deploy:all
69
- ```
70
-
71
29
  ## NPM Package Content Policy
72
30
 
73
31
  This package intentionally includes only non-sensitive defaults and runtime source needed for setup.
@@ -75,9 +33,10 @@ This package intentionally includes only non-sensitive defaults and runtime sour
75
33
  Included:
76
34
 
77
35
  - `app/` source (with `app/config-example/`)
78
- - `functions/`, `public/`, `scripts/`
36
+ - `functions/`, `public/`
79
37
  - Worker package manifests
80
- - Worker source files except runtime entry files (`workers/*/src/*.ts` and excluding `workers/*/src/*worker.ts`)
38
+ - Worker source files needed by the example workers, including nested helper modules, while excluding production worker entry files (`workers/*/src/**/*.ts` excluding `workers/*/src/**/*worker.ts`)
39
+ - PDF worker example support files limited to `workers/pdf-worker/src/assets/generated-assets.example.ts` and `workers/pdf-worker/src/formats/format-striae.ts` (no extra PDF image assets or custom formats)
81
40
  - Worker example Wrangler configs (`workers/*/wrangler.jsonc.example`)
82
41
  - Project-level example and build config (`.env.example`, `wrangler.toml.example`, `tsconfig.json`, etc.)
83
42
 
@@ -1,183 +1,11 @@
1
1
  import type { User } from 'firebase/auth';
2
- import { type AnnotationData, type CaseExportData, type AllCasesExportData, type ExportOptions } from '~/types';
2
+ import { type AnnotationData, type CaseExportData, type ExportOptions } from '~/types';
3
3
  import { getCaseData } from '~/utils/data';
4
4
  import { fetchFiles } from '../image-manage';
5
5
  import { getNotes } from '../notes-manage';
6
- import { validateCaseNumber, listCases } from '../case-manage';
6
+ import { validateCaseNumber } from '../case-manage';
7
7
  import { getUserExportMetadata } from './metadata-helpers';
8
8
 
9
- /**
10
- * Export all cases for a user
11
- */
12
- export async function exportAllCases(
13
- user: User,
14
- options: ExportOptions = {},
15
- onProgress?: (current: number, total: number, caseName: string) => void
16
- ): Promise<AllCasesExportData> {
17
- // NOTE: startTime tracking moved to download handlers
18
-
19
- try {
20
- // NOTE: Audit workflow management moved to download handlers
21
-
22
- const {
23
- includeMetadata = true
24
- } = options;
25
-
26
- // Get user export metadata
27
- const userMetadata = await getUserExportMetadata(user);
28
-
29
- // Get list of all cases for the user
30
- const caseNumbers = await listCases(user);
31
-
32
- if (!caseNumbers || caseNumbers.length === 0) {
33
- throw new Error('No cases found for user');
34
- }
35
-
36
- const exportedCases: CaseExportData[] = [];
37
- let totalFiles = 0;
38
- let totalAnnotations = 0;
39
- let totalConfirmations = 0;
40
- let totalConfirmationsRequested = 0;
41
- let casesWithFiles = 0;
42
- let casesWithAnnotations = 0;
43
- let casesWithoutFiles = 0;
44
- let lastModified: string | undefined;
45
- let earliestAnnotationDate: string | undefined;
46
- let latestAnnotationDate: string | undefined;
47
-
48
- // Export each case
49
- for (let i = 0; i < caseNumbers.length; i++) {
50
- const caseNumber = caseNumbers[i];
51
-
52
- // Report progress
53
- if (onProgress) {
54
- onProgress(i + 1, caseNumbers.length, caseNumber);
55
- }
56
-
57
- try {
58
- const caseExport = await exportCaseData(user, caseNumber, options);
59
- exportedCases.push(caseExport);
60
-
61
- // Update totals
62
- totalFiles += caseExport.metadata.totalFiles;
63
-
64
- if (caseExport.metadata.totalFiles > 0) {
65
- casesWithFiles++;
66
- } else {
67
- casesWithoutFiles++;
68
- }
69
-
70
- // Count annotations and confirmations
71
- const caseAnnotations = caseExport.files.filter(f => f.hasAnnotations).length;
72
- if (caseAnnotations > 0) {
73
- casesWithAnnotations++;
74
- totalAnnotations += caseAnnotations;
75
- }
76
-
77
- // Count confirmations
78
- if (caseExport.summary?.filesWithConfirmations) {
79
- totalConfirmations += caseExport.summary.filesWithConfirmations;
80
- }
81
- if (caseExport.summary?.filesWithConfirmationsRequested) {
82
- totalConfirmationsRequested += caseExport.summary.filesWithConfirmationsRequested;
83
- }
84
-
85
- // Track latest modification
86
- if (caseExport.summary?.lastModified) {
87
- if (!lastModified || caseExport.summary.lastModified > lastModified) {
88
- lastModified = caseExport.summary.lastModified;
89
- }
90
- }
91
-
92
- // Track annotation date range across all cases
93
- if (caseExport.summary?.earliestAnnotationDate) {
94
- if (!earliestAnnotationDate || caseExport.summary.earliestAnnotationDate < earliestAnnotationDate) {
95
- earliestAnnotationDate = caseExport.summary.earliestAnnotationDate;
96
- }
97
- }
98
- if (caseExport.summary?.latestAnnotationDate) {
99
- if (!latestAnnotationDate || caseExport.summary.latestAnnotationDate > latestAnnotationDate) {
100
- latestAnnotationDate = caseExport.summary.latestAnnotationDate;
101
- }
102
- }
103
-
104
- } catch (error) {
105
- // Get case creation date even for failed exports
106
- let caseCreatedDate = new Date().toISOString(); // fallback
107
- try {
108
- const caseData = await getCaseData(user, caseNumber);
109
- if (caseData?.createdAt) {
110
- caseCreatedDate = caseData.createdAt;
111
- }
112
- } catch {
113
- // Use fallback date if case lookup fails
114
- }
115
-
116
- // Create a placeholder entry for failed exports
117
- exportedCases.push({
118
- metadata: {
119
- caseNumber,
120
- caseCreatedDate,
121
- exportDate: new Date().toISOString(),
122
- ...userMetadata,
123
- striaeExportSchemaVersion: '1.0',
124
- totalFiles: 0
125
- },
126
- files: [],
127
- summary: {
128
- filesWithAnnotations: 0,
129
- filesWithoutAnnotations: 0,
130
- totalBoxAnnotations: 0,
131
- exportError: error instanceof Error ? error.message : 'Unknown error'
132
- }
133
- });
134
- casesWithoutFiles++;
135
- }
136
- }
137
-
138
- const allCasesExport: AllCasesExportData = {
139
- metadata: {
140
- exportDate: new Date().toISOString(),
141
- ...userMetadata,
142
- striaeExportSchemaVersion: '1.0',
143
- totalCases: caseNumbers.length,
144
- totalFiles,
145
- totalAnnotations,
146
- totalConfirmations,
147
- totalConfirmationsRequested
148
- },
149
- cases: exportedCases
150
- };
151
-
152
- if (includeMetadata) {
153
- allCasesExport.summary = {
154
- casesWithFiles,
155
- casesWithAnnotations,
156
- casesWithoutFiles,
157
- lastModified,
158
- earliestAnnotationDate,
159
- latestAnnotationDate
160
- };
161
- }
162
-
163
- // Report completion
164
- if (onProgress) {
165
- onProgress(caseNumbers.length, caseNumbers.length, 'Export completed!');
166
- }
167
-
168
- // NOTE: Audit logging moved to download handlers where actual filename and format are known
169
-
170
- return allCasesExport;
171
-
172
- } catch (error) {
173
- console.error('Export all cases failed:', error);
174
-
175
- // NOTE: Audit logging for failures moved to download handlers
176
-
177
- throw error;
178
- }
179
- }
180
-
181
9
  /**
182
10
  * Export case data with files and annotations
183
11
  */