@striae-org/striae 4.1.0 → 4.2.1

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 (124) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +1 -1
  3. package/app/components/actions/case-export/core-export.ts +14 -8
  4. package/app/components/actions/case-export/data-processing.ts +1 -0
  5. package/app/components/actions/case-export/download-handlers.ts +7 -0
  6. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  7. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  8. package/app/components/actions/case-import/orchestrator.ts +78 -32
  9. package/app/components/actions/case-import/storage-operations.ts +97 -8
  10. package/app/components/actions/case-import/zip-processing.ts +159 -86
  11. package/app/components/actions/case-manage.ts +463 -8
  12. package/app/components/actions/confirm-export.ts +9 -2
  13. package/app/components/actions/image-manage.ts +77 -44
  14. package/app/components/audit/user-audit-viewer.tsx +19 -8
  15. package/app/components/audit/user-audit.module.css +21 -0
  16. package/app/components/audit/viewer/audit-entries-list.tsx +12 -2
  17. package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
  18. package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
  19. package/app/components/audit/viewer/use-audit-viewer-data.ts +24 -1
  20. package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
  21. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  22. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  23. package/app/components/canvas/canvas.module.css +64 -54
  24. package/app/components/canvas/canvas.tsx +14 -16
  25. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  26. package/app/components/canvas/confirmation/confirmation.tsx +12 -14
  27. package/app/components/colors/colors.module.css +4 -3
  28. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  29. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  30. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  31. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  32. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  33. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  34. package/app/components/navbar/navbar.module.css +447 -0
  35. package/app/components/navbar/navbar.tsx +402 -0
  36. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
  37. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
  38. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  39. package/app/components/sidebar/case-export/case-export.tsx +8 -46
  40. package/app/components/sidebar/case-import/case-import.module.css +23 -0
  41. package/app/components/sidebar/case-import/case-import.tsx +64 -16
  42. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  43. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  44. package/app/components/sidebar/cases/case-sidebar.tsx +68 -588
  45. package/app/components/sidebar/cases/cases-modal.module.css +1 -0
  46. package/app/components/sidebar/cases/cases-modal.tsx +82 -43
  47. package/app/components/sidebar/cases/cases.module.css +82 -21
  48. package/app/components/sidebar/files/files-modal.module.css +1 -0
  49. package/app/components/sidebar/files/files-modal.tsx +49 -52
  50. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  51. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +187 -138
  52. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  53. package/app/components/sidebar/notes/notes-editor-modal.tsx +64 -0
  54. package/app/components/sidebar/notes/notes.module.css +170 -1
  55. package/app/components/sidebar/sidebar-container.tsx +16 -28
  56. package/app/components/sidebar/sidebar.module.css +5 -69
  57. package/app/components/sidebar/sidebar.tsx +27 -125
  58. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  59. package/app/components/user/inactivity-warning.module.css +1 -0
  60. package/app/components/user/inactivity-warning.tsx +15 -2
  61. package/app/components/user/manage-profile.tsx +23 -10
  62. package/app/{tailwind.css → global.css} +1 -3
  63. package/app/hooks/useOverlayDismiss.ts +54 -4
  64. package/app/root.tsx +1 -1
  65. package/app/routes/auth/login.tsx +785 -774
  66. package/app/routes/striae/striae.module.css +10 -3
  67. package/app/routes/striae/striae.tsx +475 -30
  68. package/app/services/audit/audit.service.ts +173 -27
  69. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  70. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
  71. package/app/services/audit/builders/index.ts +1 -0
  72. package/app/types/audit.ts +4 -1
  73. package/app/types/case.ts +29 -0
  74. package/app/types/import.ts +3 -0
  75. package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
  76. package/app/utils/data/data-operations.ts +17 -861
  77. package/app/utils/data/index.ts +11 -1
  78. package/app/utils/data/operations/batch-operations.ts +113 -0
  79. package/app/utils/data/operations/case-operations.ts +168 -0
  80. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  81. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  82. package/app/utils/data/operations/index.ts +7 -0
  83. package/app/utils/data/operations/signing-operations.ts +225 -0
  84. package/app/utils/data/operations/types.ts +42 -0
  85. package/app/utils/data/operations/validation-operations.ts +48 -0
  86. package/app/utils/data/permissions.ts +16 -1
  87. package/app/utils/forensics/audit-export-signature.ts +5 -1
  88. package/app/utils/forensics/confirmation-signature.ts +3 -0
  89. package/app/utils/forensics/export-verification.ts +426 -22
  90. package/functions/api/_shared/firebase-auth.ts +2 -7
  91. package/functions/api/image/[[path]].ts +20 -23
  92. package/functions/api/pdf/[[path]].ts +27 -8
  93. package/package.json +7 -12
  94. package/scripts/deploy-primershear-emails.sh +2 -1
  95. package/worker-configuration.d.ts +3 -3
  96. package/workers/audit-worker/package.json +1 -1
  97. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  98. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  99. package/workers/data-worker/package.json +1 -1
  100. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  101. package/workers/data-worker/wrangler.jsonc.example +1 -1
  102. package/workers/image-worker/package.json +1 -1
  103. package/workers/image-worker/src/image-worker.example.ts +16 -5
  104. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  105. package/workers/image-worker/wrangler.jsonc.example +1 -1
  106. package/workers/keys-worker/package.json +1 -1
  107. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  108. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  109. package/workers/pdf-worker/package.json +1 -1
  110. package/workers/pdf-worker/src/formats/format-striae.ts +9 -14
  111. package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
  112. package/workers/pdf-worker/src/report-types.ts +3 -3
  113. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  114. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  115. package/workers/user-worker/package.json +1 -1
  116. package/workers/user-worker/src/user-worker.example.ts +17 -0
  117. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  118. package/workers/user-worker/wrangler.jsonc.example +1 -1
  119. package/wrangler.toml.example +1 -1
  120. package/NOTICE +0 -13
  121. package/app/components/sidebar/notes/notes-modal.tsx +0 -53
  122. package/postcss.config.js +0 -6
  123. package/public/.well-known/keybase.txt +0 -56
  124. package/tailwind.config.ts +0 -22
@@ -0,0 +1,196 @@
1
+ import type { User } from 'firebase/auth';
2
+ import type { AnnotationData } from '~/types';
3
+
4
+ import { fetchDataApi } from '../../api';
5
+ import { canAccessCase, canModifyCase, validateUserSession } from '../permissions';
6
+ import { removeFileConfirmationSummary, upsertFileConfirmationSummary } from './confirmation-summary-operations';
7
+ import type { DataOperationOptions } from './types';
8
+
9
+ /**
10
+ * Get file annotation data from R2 storage.
11
+ */
12
+ export const getFileAnnotations = async (
13
+ user: User,
14
+ caseNumber: string,
15
+ fileId: string
16
+ ): Promise<AnnotationData | null> => {
17
+ try {
18
+ const sessionValidation = await validateUserSession(user);
19
+ if (!sessionValidation.valid) {
20
+ throw new Error(`Session validation failed: ${sessionValidation.reason}`);
21
+ }
22
+
23
+ const accessCheck = await canAccessCase(user, caseNumber);
24
+ if (!accessCheck.allowed) {
25
+ throw new Error(`Access denied: ${accessCheck.reason}`);
26
+ }
27
+
28
+ if (!fileId || typeof fileId !== 'string') {
29
+ throw new Error('Invalid file ID provided');
30
+ }
31
+
32
+ const response = await fetchDataApi(
33
+ user,
34
+ `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/${encodeURIComponent(fileId)}/data.json`,
35
+ {
36
+ method: 'GET'
37
+ }
38
+ );
39
+
40
+ if (response.status === 404) {
41
+ return null;
42
+ }
43
+
44
+ if (!response.ok) {
45
+ throw new Error(`Failed to fetch file annotations: ${response.status} ${response.statusText}`);
46
+ }
47
+
48
+ return await response.json() as AnnotationData;
49
+ } catch (error) {
50
+ console.error(`Error fetching annotations for ${caseNumber}/${fileId}:`, error);
51
+ return null;
52
+ }
53
+ };
54
+
55
+ /**
56
+ * Save file annotation data to R2 storage.
57
+ */
58
+ export const saveFileAnnotations = async (
59
+ user: User,
60
+ caseNumber: string,
61
+ fileId: string,
62
+ annotationData: AnnotationData,
63
+ options: DataOperationOptions = {}
64
+ ): Promise<void> => {
65
+ try {
66
+ const sessionValidation = await validateUserSession(user);
67
+ if (!sessionValidation.valid) {
68
+ throw new Error(`Session validation failed: ${sessionValidation.reason}`);
69
+ }
70
+
71
+ if (options.skipValidation !== true) {
72
+ const modifyCheck = await canModifyCase(user, caseNumber);
73
+ if (!modifyCheck.allowed) {
74
+ throw new Error(`Modification denied: ${modifyCheck.reason}`);
75
+ }
76
+ }
77
+
78
+ if (!fileId || typeof fileId !== 'string') {
79
+ throw new Error('Invalid file ID provided');
80
+ }
81
+
82
+ if (!annotationData || typeof annotationData !== 'object') {
83
+ throw new Error('Invalid annotation data provided');
84
+ }
85
+
86
+ // Enforce immutability once confirmation data exists on an image.
87
+ const existingResponse = await fetchDataApi(
88
+ user,
89
+ `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/${encodeURIComponent(fileId)}/data.json`,
90
+ {
91
+ method: 'GET'
92
+ }
93
+ );
94
+
95
+ if (existingResponse.ok) {
96
+ const existingAnnotations = await existingResponse.json() as AnnotationData;
97
+ if (existingAnnotations?.confirmationData) {
98
+ throw new Error('Cannot modify annotations for a confirmed image');
99
+ }
100
+ } else if (existingResponse.status !== 404) {
101
+ throw new Error(`Failed to verify existing annotations: ${existingResponse.status} ${existingResponse.statusText}`);
102
+ }
103
+
104
+ const dataToSave = {
105
+ ...annotationData,
106
+ updatedAt: new Date().toISOString()
107
+ };
108
+
109
+ const response = await fetchDataApi(
110
+ user,
111
+ `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/${encodeURIComponent(fileId)}/data.json`,
112
+ {
113
+ method: 'PUT',
114
+ headers: {
115
+ 'Content-Type': 'application/json'
116
+ },
117
+ body: JSON.stringify(dataToSave)
118
+ }
119
+ );
120
+
121
+ if (!response.ok) {
122
+ throw new Error(`Failed to save file annotations: ${response.status} ${response.statusText}`);
123
+ }
124
+
125
+ try {
126
+ await upsertFileConfirmationSummary(user, caseNumber, fileId, dataToSave);
127
+ } catch (summaryError) {
128
+ console.warn(`Failed to update confirmation summary for ${caseNumber}/${fileId}:`, summaryError);
129
+ }
130
+ } catch (error) {
131
+ console.error(`Error saving annotations for ${caseNumber}/${fileId}:`, error);
132
+ throw error;
133
+ }
134
+ };
135
+
136
+ /**
137
+ * Delete file annotation data from R2 storage.
138
+ */
139
+ export const deleteFileAnnotations = async (
140
+ user: User,
141
+ caseNumber: string,
142
+ fileId: string,
143
+ options: { skipValidation?: boolean } = {}
144
+ ): Promise<void> => {
145
+ try {
146
+ const sessionValidation = await validateUserSession(user);
147
+ if (!sessionValidation.valid) {
148
+ throw new Error(`Session validation failed: ${sessionValidation.reason}`);
149
+ }
150
+
151
+ if (options.skipValidation !== true) {
152
+ const modifyCheck = await canModifyCase(user, caseNumber);
153
+ if (!modifyCheck.allowed) {
154
+ throw new Error(`Delete denied: ${modifyCheck.reason}`);
155
+ }
156
+ }
157
+
158
+ const response = await fetchDataApi(
159
+ user,
160
+ `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/${encodeURIComponent(fileId)}/data.json`,
161
+ {
162
+ method: 'DELETE'
163
+ }
164
+ );
165
+
166
+ if (!response.ok && response.status !== 404) {
167
+ throw new Error(`Failed to delete file annotations: ${response.status} ${response.statusText}`);
168
+ }
169
+
170
+ try {
171
+ await removeFileConfirmationSummary(user, caseNumber, fileId);
172
+ } catch (summaryError) {
173
+ console.warn(`Failed to update confirmation summary after delete for ${caseNumber}/${fileId}:`, summaryError);
174
+ }
175
+ } catch (error) {
176
+ console.error(`Error deleting annotations for ${caseNumber}/${fileId}:`, error);
177
+ throw error;
178
+ }
179
+ };
180
+
181
+ /**
182
+ * Check if a file has annotations.
183
+ */
184
+ export const fileHasAnnotations = async (
185
+ user: User,
186
+ caseNumber: string,
187
+ fileId: string
188
+ ): Promise<boolean> => {
189
+ try {
190
+ const annotations = await getFileAnnotations(user, caseNumber, fileId);
191
+ return annotations !== null;
192
+ } catch (error) {
193
+ console.error(`Error checking annotations for ${caseNumber}/${fileId}:`, error);
194
+ return false;
195
+ }
196
+ };
@@ -0,0 +1,7 @@
1
+ export * from './types';
2
+ export * from './confirmation-summary-operations';
3
+ export * from './case-operations';
4
+ export * from './file-annotation-operations';
5
+ export * from './batch-operations';
6
+ export * from './validation-operations';
7
+ export * from './signing-operations';
@@ -0,0 +1,225 @@
1
+ import type { User } from 'firebase/auth';
2
+ import type { ConfirmationImportData } from '~/types';
3
+
4
+ import { fetchDataApi } from '../../api';
5
+ import {
6
+ AUDIT_EXPORT_SIGNATURE_VERSION,
7
+ type AuditExportSigningPayload,
8
+ isValidAuditExportSigningPayload
9
+ } from '../../forensics/audit-export-signature';
10
+ import { CONFIRMATION_SIGNATURE_VERSION } from '../../forensics/confirmation-signature';
11
+ import {
12
+ type ForensicManifestData,
13
+ type ForensicManifestSignature,
14
+ FORENSIC_MANIFEST_VERSION
15
+ } from '../../forensics/SHA256';
16
+ import { canAccessCase, validateUserSession } from '../permissions';
17
+ import type {
18
+ AuditExportSigningResponse,
19
+ ConfirmationSigningResponse,
20
+ ManifestSigningResponse
21
+ } from './types';
22
+
23
+ /**
24
+ * Request a server-side signature for a forensic manifest.
25
+ */
26
+ export const signForensicManifest = async (
27
+ user: User,
28
+ caseNumber: string,
29
+ manifest: ForensicManifestData
30
+ ): Promise<ManifestSigningResponse> => {
31
+ try {
32
+ const sessionValidation = await validateUserSession(user);
33
+ if (!sessionValidation.valid) {
34
+ throw new Error(`Session validation failed: ${sessionValidation.reason}`);
35
+ }
36
+
37
+ const accessCheck = await canAccessCase(user, caseNumber);
38
+ if (!accessCheck.allowed) {
39
+ throw new Error(`Manifest signing denied: ${accessCheck.reason}`);
40
+ }
41
+
42
+ const response = await fetchDataApi(user, '/api/forensic/sign-manifest', {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json'
46
+ },
47
+ body: JSON.stringify({
48
+ userId: user.uid,
49
+ caseNumber,
50
+ manifest
51
+ })
52
+ });
53
+
54
+ const responseData = await response.json().catch(() => null) as {
55
+ success?: boolean;
56
+ error?: string;
57
+ manifestVersion?: string;
58
+ signature?: ForensicManifestSignature;
59
+ } | null;
60
+
61
+ if (!response.ok) {
62
+ throw new Error(
63
+ responseData?.error ||
64
+ `Failed to sign forensic manifest: ${response.status} ${response.statusText}`
65
+ );
66
+ }
67
+
68
+ if (!responseData?.success || !responseData.signature || !responseData.manifestVersion) {
69
+ throw new Error('Invalid manifest signing response from data worker');
70
+ }
71
+
72
+ if (responseData.manifestVersion !== FORENSIC_MANIFEST_VERSION) {
73
+ throw new Error(
74
+ `Unexpected manifest version from signer: ${responseData.manifestVersion}`
75
+ );
76
+ }
77
+
78
+ return {
79
+ manifestVersion: responseData.manifestVersion,
80
+ signature: responseData.signature
81
+ };
82
+ } catch (error) {
83
+ console.error(`Error signing forensic manifest for ${caseNumber}:`, error);
84
+ throw error;
85
+ }
86
+ };
87
+
88
+ /**
89
+ * Request a server-side signature for confirmation export data.
90
+ */
91
+ export const signConfirmationData = async (
92
+ user: User,
93
+ caseNumber: string,
94
+ confirmationData: ConfirmationImportData
95
+ ): Promise<ConfirmationSigningResponse> => {
96
+ try {
97
+ const sessionValidation = await validateUserSession(user);
98
+ if (!sessionValidation.valid) {
99
+ throw new Error(`Session validation failed: ${sessionValidation.reason}`);
100
+ }
101
+
102
+ const accessCheck = await canAccessCase(user, caseNumber);
103
+ if (!accessCheck.allowed) {
104
+ throw new Error(`Confirmation signing denied: ${accessCheck.reason}`);
105
+ }
106
+
107
+ const response = await fetchDataApi(user, '/api/forensic/sign-confirmation', {
108
+ method: 'POST',
109
+ headers: {
110
+ 'Content-Type': 'application/json'
111
+ },
112
+ body: JSON.stringify({
113
+ userId: user.uid,
114
+ caseNumber,
115
+ confirmationData,
116
+ signatureVersion: CONFIRMATION_SIGNATURE_VERSION
117
+ })
118
+ });
119
+
120
+ const responseData = await response.json().catch(() => null) as {
121
+ success?: boolean;
122
+ error?: string;
123
+ signatureVersion?: string;
124
+ signature?: ForensicManifestSignature;
125
+ } | null;
126
+
127
+ if (!response.ok) {
128
+ throw new Error(
129
+ responseData?.error ||
130
+ `Failed to sign confirmation data: ${response.status} ${response.statusText}`
131
+ );
132
+ }
133
+
134
+ if (!responseData?.success || !responseData.signature || !responseData.signatureVersion) {
135
+ throw new Error('Invalid confirmation signing response from data worker');
136
+ }
137
+
138
+ if (responseData.signatureVersion !== CONFIRMATION_SIGNATURE_VERSION) {
139
+ throw new Error(
140
+ `Unexpected confirmation signature version from signer: ${responseData.signatureVersion}`
141
+ );
142
+ }
143
+
144
+ return {
145
+ signatureVersion: responseData.signatureVersion,
146
+ signature: responseData.signature
147
+ };
148
+ } catch (error) {
149
+ console.error(`Error signing confirmation data for ${caseNumber}:`, error);
150
+ throw error;
151
+ }
152
+ };
153
+
154
+ /**
155
+ * Request a server-side signature for audit export metadata.
156
+ */
157
+ export const signAuditExportData = async (
158
+ user: User,
159
+ auditExport: AuditExportSigningPayload,
160
+ options: { caseNumber?: string } = {}
161
+ ): Promise<AuditExportSigningResponse> => {
162
+ try {
163
+ const sessionValidation = await validateUserSession(user);
164
+ if (!sessionValidation.valid) {
165
+ throw new Error(`Session validation failed: ${sessionValidation.reason}`);
166
+ }
167
+
168
+ if (!isValidAuditExportSigningPayload(auditExport)) {
169
+ throw new Error('Invalid audit export payload for signing');
170
+ }
171
+
172
+ const caseNumber = options.caseNumber;
173
+ if (caseNumber) {
174
+ const accessCheck = await canAccessCase(user, caseNumber);
175
+ if (!accessCheck.allowed) {
176
+ throw new Error(`Audit export signing denied: ${accessCheck.reason}`);
177
+ }
178
+ }
179
+
180
+ const response = await fetchDataApi(user, '/api/forensic/sign-audit-export', {
181
+ method: 'POST',
182
+ headers: {
183
+ 'Content-Type': 'application/json'
184
+ },
185
+ body: JSON.stringify({
186
+ userId: user.uid,
187
+ caseNumber,
188
+ auditExport,
189
+ signatureVersion: AUDIT_EXPORT_SIGNATURE_VERSION
190
+ })
191
+ });
192
+
193
+ const responseData = await response.json().catch(() => null) as {
194
+ success?: boolean;
195
+ error?: string;
196
+ signatureVersion?: string;
197
+ signature?: ForensicManifestSignature;
198
+ } | null;
199
+
200
+ if (!response.ok) {
201
+ throw new Error(
202
+ responseData?.error ||
203
+ `Failed to sign audit export data: ${response.status} ${response.statusText}`
204
+ );
205
+ }
206
+
207
+ if (!responseData?.success || !responseData.signature || !responseData.signatureVersion) {
208
+ throw new Error('Invalid audit export signing response from data worker');
209
+ }
210
+
211
+ if (responseData.signatureVersion !== AUDIT_EXPORT_SIGNATURE_VERSION) {
212
+ throw new Error(
213
+ `Unexpected audit export signature version from signer: ${responseData.signatureVersion}`
214
+ );
215
+ }
216
+
217
+ return {
218
+ signatureVersion: responseData.signatureVersion,
219
+ signature: responseData.signature
220
+ };
221
+ } catch (error) {
222
+ console.error('Error signing audit export data:', error);
223
+ throw error;
224
+ }
225
+ };
@@ -0,0 +1,42 @@
1
+ import type { User } from 'firebase/auth';
2
+ import type { ForensicManifestSignature } from '~/utils/forensics/SHA256';
3
+
4
+ import type { AnnotationData } from '~/types';
5
+
6
+ export interface DataAccessResult {
7
+ allowed: boolean;
8
+ reason?: string;
9
+ }
10
+
11
+ export interface FileUpdate {
12
+ fileId: string;
13
+ annotations: AnnotationData;
14
+ }
15
+
16
+ export interface BatchUpdateResult {
17
+ successful: string[];
18
+ failed: { fileId: string; error: string }[];
19
+ }
20
+
21
+ export interface DataOperationOptions {
22
+ includeTimestamp?: boolean;
23
+ retryCount?: number;
24
+ skipValidation?: boolean;
25
+ }
26
+
27
+ export interface ManifestSigningResponse {
28
+ manifestVersion: string;
29
+ signature: ForensicManifestSignature;
30
+ }
31
+
32
+ export interface ConfirmationSigningResponse {
33
+ signatureVersion: string;
34
+ signature: ForensicManifestSignature;
35
+ }
36
+
37
+ export interface AuditExportSigningResponse {
38
+ signatureVersion: string;
39
+ signature: ForensicManifestSignature;
40
+ }
41
+
42
+ export type DataOperation<T> = (user: User, ...args: unknown[]) => Promise<T>;
@@ -0,0 +1,48 @@
1
+ import type { User } from 'firebase/auth';
2
+
3
+ import { canAccessCase, validateUserSession } from '../permissions';
4
+ import type { DataAccessResult, DataOperation } from './types';
5
+
6
+ /**
7
+ * Validate data access permissions for a user and case.
8
+ */
9
+ export const validateDataAccess = async (
10
+ user: User,
11
+ caseNumber: string
12
+ ): Promise<DataAccessResult> => {
13
+ try {
14
+ const sessionValidation = await validateUserSession(user);
15
+ if (!sessionValidation.valid) {
16
+ return { allowed: false, reason: sessionValidation.reason };
17
+ }
18
+
19
+ const accessCheck = await canAccessCase(user, caseNumber);
20
+ if (!accessCheck.allowed) {
21
+ return { allowed: false, reason: accessCheck.reason };
22
+ }
23
+
24
+ return { allowed: true };
25
+ } catch (error) {
26
+ console.error('Error validating data access:', error);
27
+ return { allowed: false, reason: 'Access validation failed' };
28
+ }
29
+ };
30
+
31
+ /**
32
+ * Higher-order function for consistent data operation patterns.
33
+ */
34
+ export const withDataOperation = <T>(
35
+ operation: DataOperation<T>
36
+ ) => async (user: User, ...args: unknown[]): Promise<T> => {
37
+ try {
38
+ const sessionValidation = await validateUserSession(user);
39
+ if (!sessionValidation.valid) {
40
+ throw new Error(`Operation failed: ${sessionValidation.reason}`);
41
+ }
42
+
43
+ return await operation(user, ...args);
44
+ } catch (error) {
45
+ console.error('Data operation failed:', error);
46
+ throw error;
47
+ }
48
+ };
@@ -1,7 +1,7 @@
1
1
  import type { User } from 'firebase/auth';
2
2
  import type { UserData, ExtendedUserData, UserLimits, ReadOnlyCaseMetadata } from '~/types';
3
3
  import paths from '~/config/config.json';
4
- import { fetchUserApi } from '../api';
4
+ import { fetchDataApi, fetchUserApi } from '../api';
5
5
 
6
6
  const MAX_CASES_REVIEW = paths.max_cases_review;
7
7
  const MAX_FILES_PER_CASE_REVIEW = paths.max_files_per_case_review;
@@ -403,6 +403,21 @@ export const canModifyCase = async (user: User, caseNumber: string): Promise<Per
403
403
  return { allowed: false, reason: 'User data not found' };
404
404
  }
405
405
 
406
+ const archiveCheckResponse = await fetchDataApi(
407
+ user,
408
+ `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`,
409
+ {
410
+ method: 'GET'
411
+ }
412
+ );
413
+
414
+ if (archiveCheckResponse.ok) {
415
+ const caseData = await archiveCheckResponse.json() as { archived?: boolean };
416
+ if (caseData.archived) {
417
+ return { allowed: false, reason: 'Archived cases are immutable and read-only' };
418
+ }
419
+ }
420
+
406
421
  // Check if user owns the case (regular cases)
407
422
  if (userData.cases && userData.cases.some(c => c.caseNumber === caseNumber)) {
408
423
  // For owned cases, user must be permitted
@@ -83,7 +83,8 @@ export function createAuditExportSigningPayload(payload: AuditExportSigningPaylo
83
83
 
84
84
  export async function verifyAuditExportSignature(
85
85
  payload: Partial<AuditExportSigningPayload>,
86
- signature?: ForensicManifestSignature
86
+ signature?: ForensicManifestSignature,
87
+ verificationPublicKeyPem?: string
87
88
  ): Promise<ManifestSignatureVerificationResult> {
88
89
  if (!signature) {
89
90
  return {
@@ -112,6 +113,9 @@ export async function verifyAuditExportSignature(
112
113
  noVerificationKeyPrefix: 'No verification key configured for key ID',
113
114
  invalidPublicKeyError: 'Audit export signature verification failed: invalid public key',
114
115
  verificationFailedError: 'Audit export signature verification failed'
116
+ },
117
+ {
118
+ verificationPublicKeyPem
115
119
  }
116
120
  );
117
121
  }
@@ -134,6 +134,9 @@ export function createConfirmationSigningPayload(
134
134
  exportedByUid: confirmationData.metadata.exportedByUid,
135
135
  exportedByName: confirmationData.metadata.exportedByName,
136
136
  exportedByCompany: confirmationData.metadata.exportedByCompany,
137
+ ...(confirmationData.metadata.exportedByBadgeId
138
+ ? { exportedByBadgeId: confirmationData.metadata.exportedByBadgeId }
139
+ : {}),
137
140
  totalConfirmations: confirmationData.metadata.totalConfirmations,
138
141
  version: confirmationData.metadata.version,
139
142
  hash: confirmationData.metadata.hash.toUpperCase(),