@striae-org/striae 4.2.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 (65) hide show
  1. package/LICENSE +1 -1
  2. package/app/components/actions/case-manage.ts +50 -17
  3. package/app/components/audit/viewer/audit-entries-list.tsx +5 -2
  4. package/app/components/audit/viewer/use-audit-viewer-data.ts +6 -3
  5. package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
  6. package/app/components/canvas/confirmation/confirmation.tsx +6 -2
  7. package/app/components/colors/colors.module.css +4 -3
  8. package/app/components/navbar/navbar.tsx +34 -9
  9. package/app/components/sidebar/cases/case-sidebar.tsx +44 -70
  10. package/app/components/sidebar/cases/cases-modal.tsx +76 -35
  11. package/app/components/sidebar/cases/cases.module.css +20 -0
  12. package/app/components/sidebar/files/files-modal.tsx +37 -39
  13. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  14. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +37 -74
  15. package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
  16. package/app/components/sidebar/notes/notes.module.css +27 -11
  17. package/app/components/sidebar/sidebar-container.tsx +1 -0
  18. package/app/components/sidebar/sidebar.tsx +3 -0
  19. package/app/{tailwind.css → global.css} +1 -3
  20. package/app/hooks/useOverlayDismiss.ts +6 -4
  21. package/app/root.tsx +1 -1
  22. package/app/routes/striae/striae.tsx +6 -0
  23. package/app/services/audit/audit.service.ts +2 -2
  24. package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
  25. package/app/types/audit.ts +1 -0
  26. package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
  27. package/app/utils/data/data-operations.ts +17 -861
  28. package/app/utils/data/index.ts +11 -1
  29. package/app/utils/data/operations/batch-operations.ts +113 -0
  30. package/app/utils/data/operations/case-operations.ts +168 -0
  31. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  32. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  33. package/app/utils/data/operations/index.ts +7 -0
  34. package/app/utils/data/operations/signing-operations.ts +225 -0
  35. package/app/utils/data/operations/types.ts +42 -0
  36. package/app/utils/data/operations/validation-operations.ts +48 -0
  37. package/app/utils/forensics/export-verification.ts +40 -111
  38. package/functions/api/_shared/firebase-auth.ts +2 -7
  39. package/functions/api/image/[[path]].ts +20 -23
  40. package/functions/api/pdf/[[path]].ts +27 -8
  41. package/package.json +5 -10
  42. package/scripts/deploy-primershear-emails.sh +1 -1
  43. package/worker-configuration.d.ts +2 -2
  44. package/workers/audit-worker/package.json +1 -1
  45. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  46. package/workers/data-worker/package.json +1 -1
  47. package/workers/data-worker/wrangler.jsonc.example +1 -1
  48. package/workers/image-worker/package.json +1 -1
  49. package/workers/image-worker/src/image-worker.example.ts +16 -5
  50. package/workers/image-worker/wrangler.jsonc.example +1 -1
  51. package/workers/keys-worker/package.json +1 -1
  52. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  53. package/workers/pdf-worker/package.json +1 -1
  54. package/workers/pdf-worker/src/formats/format-striae.ts +1 -7
  55. package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
  56. package/workers/pdf-worker/src/report-types.ts +3 -3
  57. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  58. package/workers/user-worker/package.json +1 -1
  59. package/workers/user-worker/src/user-worker.example.ts +17 -0
  60. package/workers/user-worker/wrangler.jsonc.example +1 -1
  61. package/wrangler.toml.example +1 -1
  62. package/NOTICE +0 -13
  63. package/app/components/sidebar/notes/notes-modal.tsx +0 -52
  64. package/postcss.config.js +0 -6
  65. 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
+ };
@@ -674,6 +674,7 @@ export async function verifyCasePackageIntegrity(
674
674
  input: CasePackageIntegrityInput
675
675
  ): Promise<CasePackageIntegrityResult> {
676
676
  const manifestData = extractForensicManifestData(input.forensicManifest);
677
+ const verificationPublicKeyPem = input.verificationPublicKeyPem;
677
678
 
678
679
  if (!manifestData) {
679
680
  return {
@@ -694,9 +695,28 @@ export async function verifyCasePackageIntegrity(
694
695
  };
695
696
  }
696
697
 
698
+ if (!verificationPublicKeyPem) {
699
+ return {
700
+ isValid: false,
701
+ signatureResult: {
702
+ isValid: false,
703
+ error: 'Missing verification public key'
704
+ },
705
+ integrityResult: {
706
+ isValid: false,
707
+ dataValid: false,
708
+ imageValidation: {},
709
+ manifestValid: false,
710
+ errors: ['Missing verification public key'],
711
+ summary: 'Manifest validation failed'
712
+ },
713
+ bundledAuditVerification: null
714
+ };
715
+ }
716
+
697
717
  const signatureResult = await verifyForensicManifestSignature(
698
718
  input.forensicManifest,
699
- input.verificationPublicKeyPem
719
+ verificationPublicKeyPem
700
720
  );
701
721
 
702
722
  const integrityResult = await validateCaseIntegritySecure(
@@ -706,117 +726,26 @@ export async function verifyCasePackageIntegrity(
706
726
  );
707
727
 
708
728
  const bundledAuditVerification = input.bundledAuditFiles
709
- ? await (async () => {
710
- const { auditTrailContent, auditSignatureContent } = input.bundledAuditFiles ?? {};
711
-
712
- if (!auditTrailContent && !auditSignatureContent) {
713
- return null;
714
- }
715
-
716
- if (!auditTrailContent || !auditSignatureContent) {
717
- return createVerificationResult(
718
- false,
719
- 'The archive ZIP contains incomplete bundled audit verification files.',
720
- 'case-zip'
721
- );
722
- }
723
-
724
- try {
725
- const auditTrailExport = JSON.parse(auditTrailContent) as BundledAuditExportFile;
726
- const auditSignatureExport = JSON.parse(auditSignatureContent) as {
727
- signatureMetadata?: Partial<AuditExportSigningPayload>;
728
- signature?: NonNullable<BundledAuditExportFile['metadata']>['signature'];
729
- };
730
-
731
- const metadata = auditTrailExport.metadata;
732
- if (!metadata?.signature || typeof metadata.hash !== 'string') {
733
- return createVerificationResult(
734
- false,
735
- 'The bundled audit export is missing required hash or signature metadata.',
736
- 'case-zip'
737
- );
738
- }
739
-
740
- const unsignedAuditExport = auditTrailExport.auditTrail !== undefined
741
- ? {
742
- metadata: {
743
- exportTimestamp: metadata.exportTimestamp,
744
- exportVersion: metadata.exportVersion,
745
- totalEntries: metadata.totalEntries,
746
- application: metadata.application,
747
- exportType: metadata.exportType,
748
- scopeType: metadata.scopeType,
749
- scopeIdentifier: metadata.scopeIdentifier,
750
- },
751
- auditTrail: auditTrailExport.auditTrail,
752
- }
753
- : {
754
- metadata: {
755
- exportTimestamp: metadata.exportTimestamp,
756
- exportVersion: metadata.exportVersion,
757
- totalEntries: metadata.totalEntries,
758
- application: metadata.application,
759
- exportType: metadata.exportType,
760
- scopeType: metadata.scopeType,
761
- scopeIdentifier: metadata.scopeIdentifier,
762
- },
763
- auditEntries: auditTrailExport.auditEntries,
764
- };
765
-
766
- const recalculatedHash = await calculateSHA256Secure(JSON.stringify(unsignedAuditExport, null, 2));
767
- if (recalculatedHash.toUpperCase() !== metadata.hash.toUpperCase()) {
768
- return createVerificationResult(
769
- false,
770
- 'The bundled audit export failed integrity verification.',
771
- 'case-zip'
772
- );
773
- }
774
-
775
- const embeddedSignaturePayload: Partial<AuditExportSigningPayload> = metadata.signatureMetadata ?? {
776
- signatureVersion: metadata.signatureVersion,
777
- exportFormat: 'json',
778
- exportType: metadata.exportType,
779
- scopeType: metadata.scopeType,
780
- scopeIdentifier: metadata.scopeIdentifier,
781
- generatedAt: metadata.exportTimestamp,
782
- totalEntries: metadata.totalEntries,
783
- hash: metadata.hash,
784
- };
785
-
786
- const signatureVerification = await verifyAuditExportSignature(
787
- embeddedSignaturePayload,
788
- metadata.signature,
789
- input.verificationPublicKeyPem
790
- );
791
-
792
- if (!signatureVerification.isValid) {
793
- return createVerificationResult(
794
- false,
795
- getSignatureFailureMessage(signatureVerification.error, 'export ZIP'),
796
- 'case-zip'
797
- );
729
+ ? await verifyBundledAuditExport(
730
+ {
731
+ file: (path: string) => {
732
+ const content = path === 'audit/case-audit-trail.json'
733
+ ? input.bundledAuditFiles?.auditTrailContent
734
+ : path === 'audit/case-audit-signature.json'
735
+ ? input.bundledAuditFiles?.auditSignatureContent
736
+ : undefined;
737
+
738
+ if (content === undefined) {
739
+ return null;
740
+ }
741
+
742
+ return {
743
+ async: async () => content,
744
+ };
798
745
  }
799
-
800
- if (
801
- JSON.stringify(auditSignatureExport.signatureMetadata ?? null) !== JSON.stringify(metadata.signatureMetadata ?? null) ||
802
- JSON.stringify(auditSignatureExport.signature ?? null) !== JSON.stringify(metadata.signature ?? null)
803
- ) {
804
- return createVerificationResult(
805
- false,
806
- 'The bundled audit signature artifact does not match the signed audit export.',
807
- 'case-zip'
808
- );
809
- }
810
-
811
- return null;
812
- } catch {
813
- return createVerificationResult(
814
- false,
815
- 'The bundled audit export could not be parsed for verification.',
816
- 'case-zip'
817
- );
818
- }
819
- })()
746
+ },
747
+ verificationPublicKeyPem
748
+ )
820
749
  : null;
821
750
 
822
751
  return {