@striae-org/striae 3.0.4

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 (223) hide show
  1. package/.env.example +100 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +18 -0
  4. package/README.md +133 -0
  5. package/app/components/actions/case-export/core-export.ts +328 -0
  6. package/app/components/actions/case-export/data-processing.ts +167 -0
  7. package/app/components/actions/case-export/download-handlers.ts +900 -0
  8. package/app/components/actions/case-export/index.ts +41 -0
  9. package/app/components/actions/case-export/metadata-helpers.ts +107 -0
  10. package/app/components/actions/case-export/types-constants.ts +56 -0
  11. package/app/components/actions/case-export/validation-utils.ts +25 -0
  12. package/app/components/actions/case-export.ts +4 -0
  13. package/app/components/actions/case-import/annotation-import.ts +35 -0
  14. package/app/components/actions/case-import/confirmation-import.ts +363 -0
  15. package/app/components/actions/case-import/image-operations.ts +61 -0
  16. package/app/components/actions/case-import/index.ts +39 -0
  17. package/app/components/actions/case-import/orchestrator.ts +420 -0
  18. package/app/components/actions/case-import/storage-operations.ts +270 -0
  19. package/app/components/actions/case-import/validation.ts +189 -0
  20. package/app/components/actions/case-import/zip-processing.ts +413 -0
  21. package/app/components/actions/case-manage.ts +524 -0
  22. package/app/components/actions/case-review.ts +4 -0
  23. package/app/components/actions/confirm-export.ts +351 -0
  24. package/app/components/actions/generate-pdf.ts +210 -0
  25. package/app/components/actions/image-manage.ts +385 -0
  26. package/app/components/actions/notes-manage.ts +33 -0
  27. package/app/components/actions/signout.module.css +15 -0
  28. package/app/components/actions/signout.tsx +50 -0
  29. package/app/components/audit/user-audit-viewer.tsx +975 -0
  30. package/app/components/audit/user-audit.module.css +568 -0
  31. package/app/components/auth/auth-provider.tsx +78 -0
  32. package/app/components/auth/mfa-enrollment.module.css +268 -0
  33. package/app/components/auth/mfa-enrollment.tsx +398 -0
  34. package/app/components/auth/mfa-verification.module.css +251 -0
  35. package/app/components/auth/mfa-verification.tsx +295 -0
  36. package/app/components/button/button.module.css +63 -0
  37. package/app/components/button/button.tsx +46 -0
  38. package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
  39. package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
  40. package/app/components/canvas/canvas.module.css +314 -0
  41. package/app/components/canvas/canvas.tsx +449 -0
  42. package/app/components/canvas/confirmation/confirmation.module.css +187 -0
  43. package/app/components/canvas/confirmation/confirmation.tsx +214 -0
  44. package/app/components/colors/colors.module.css +59 -0
  45. package/app/components/colors/colors.tsx +68 -0
  46. package/app/components/form/base-form.tsx +21 -0
  47. package/app/components/form/form-button.tsx +28 -0
  48. package/app/components/form/form-field.tsx +53 -0
  49. package/app/components/form/form-message.tsx +17 -0
  50. package/app/components/form/form-toggle.tsx +23 -0
  51. package/app/components/form/form.module.css +427 -0
  52. package/app/components/form/index.ts +6 -0
  53. package/app/components/icon/icon.module.css +3 -0
  54. package/app/components/icon/icon.tsx +27 -0
  55. package/app/components/icon/icons.svg +102 -0
  56. package/app/components/icon/manifest.json +110 -0
  57. package/app/components/sidebar/case-export/case-export.module.css +386 -0
  58. package/app/components/sidebar/case-export/case-export.tsx +317 -0
  59. package/app/components/sidebar/case-import/case-import.module.css +626 -0
  60. package/app/components/sidebar/case-import/case-import.tsx +404 -0
  61. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
  62. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
  63. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
  64. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
  65. package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
  66. package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
  67. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
  68. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
  69. package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
  70. package/app/components/sidebar/case-import/index.ts +18 -0
  71. package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
  72. package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
  73. package/app/components/sidebar/cases/cases-modal.module.css +166 -0
  74. package/app/components/sidebar/cases/cases-modal.tsx +201 -0
  75. package/app/components/sidebar/cases/cases.module.css +713 -0
  76. package/app/components/sidebar/files/files-modal.module.css +209 -0
  77. package/app/components/sidebar/files/files-modal.tsx +239 -0
  78. package/app/components/sidebar/hash/hash-utility.module.css +366 -0
  79. package/app/components/sidebar/hash/hash-utility.tsx +982 -0
  80. package/app/components/sidebar/notes/notes-modal.tsx +51 -0
  81. package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
  82. package/app/components/sidebar/notes/notes.module.css +360 -0
  83. package/app/components/sidebar/sidebar-container.tsx +149 -0
  84. package/app/components/sidebar/sidebar.module.css +321 -0
  85. package/app/components/sidebar/sidebar.tsx +215 -0
  86. package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
  87. package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
  88. package/app/components/theme-provider/theme-provider.tsx +131 -0
  89. package/app/components/theme-provider/theme.ts +155 -0
  90. package/app/components/toast/toast.module.css +137 -0
  91. package/app/components/toast/toast.tsx +56 -0
  92. package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
  93. package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
  94. package/app/components/toolbar/toolbar.module.css +42 -0
  95. package/app/components/toolbar/toolbar.tsx +167 -0
  96. package/app/components/user/delete-account.module.css +274 -0
  97. package/app/components/user/delete-account.tsx +471 -0
  98. package/app/components/user/inactivity-warning.module.css +145 -0
  99. package/app/components/user/inactivity-warning.tsx +84 -0
  100. package/app/components/user/manage-profile.module.css +190 -0
  101. package/app/components/user/manage-profile.tsx +253 -0
  102. package/app/components/user/mfa-phone-update.tsx +739 -0
  103. package/app/config-example/admin-service.json +13 -0
  104. package/app/config-example/config.json +17 -0
  105. package/app/config-example/firebase.ts +21 -0
  106. package/app/config-example/inactivity.ts +13 -0
  107. package/app/config-example/meta-config.json +6 -0
  108. package/app/contexts/auth.context.ts +12 -0
  109. package/app/entry.client.tsx +12 -0
  110. package/app/entry.server.tsx +44 -0
  111. package/app/hooks/useInactivityTimeout.ts +110 -0
  112. package/app/root.tsx +170 -0
  113. package/app/routes/_index.tsx +16 -0
  114. package/app/routes/auth/emailActionHandler.module.css +232 -0
  115. package/app/routes/auth/emailActionHandler.tsx +405 -0
  116. package/app/routes/auth/emailVerification.tsx +120 -0
  117. package/app/routes/auth/login.module.css +523 -0
  118. package/app/routes/auth/login.tsx +654 -0
  119. package/app/routes/auth/passwordReset.module.css +274 -0
  120. package/app/routes/auth/passwordReset.tsx +154 -0
  121. package/app/routes/auth/route.ts +16 -0
  122. package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
  123. package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
  124. package/app/routes/mobile-prevented/route.ts +14 -0
  125. package/app/routes/striae/striae.module.css +30 -0
  126. package/app/routes/striae/striae.tsx +417 -0
  127. package/app/services/audit-export.service.ts +755 -0
  128. package/app/services/audit.service.ts +1454 -0
  129. package/app/services/firebase-errors.ts +106 -0
  130. package/app/services/firebase.ts +15 -0
  131. package/app/styles/legal-pages.module.css +113 -0
  132. package/app/styles/root.module.css +146 -0
  133. package/app/tailwind.css +225 -0
  134. package/app/types/annotations.ts +45 -0
  135. package/app/types/audit.ts +301 -0
  136. package/app/types/case.ts +90 -0
  137. package/app/types/export.ts +8 -0
  138. package/app/types/file.ts +30 -0
  139. package/app/types/import.ts +107 -0
  140. package/app/types/index.ts +24 -0
  141. package/app/types/user.ts +38 -0
  142. package/app/utils/SHA256.ts +461 -0
  143. package/app/utils/annotation-timestamp.ts +25 -0
  144. package/app/utils/audit-export-signature.ts +117 -0
  145. package/app/utils/auth-action-settings.ts +48 -0
  146. package/app/utils/auth.ts +34 -0
  147. package/app/utils/batch-operations.ts +135 -0
  148. package/app/utils/confirmation-signature.ts +193 -0
  149. package/app/utils/data-operations.ts +871 -0
  150. package/app/utils/device-detection.ts +5 -0
  151. package/app/utils/html-sanitizer.ts +80 -0
  152. package/app/utils/id-generator.ts +36 -0
  153. package/app/utils/meta.ts +48 -0
  154. package/app/utils/mfa-phone.ts +97 -0
  155. package/app/utils/mfa.ts +79 -0
  156. package/app/utils/password-policy.ts +28 -0
  157. package/app/utils/permissions.ts +562 -0
  158. package/app/utils/signature-utils.ts +160 -0
  159. package/app/utils/style.ts +83 -0
  160. package/app/utils/version.ts +5 -0
  161. package/firebase.json +11 -0
  162. package/functions/[[path]].ts +10 -0
  163. package/package.json +138 -0
  164. package/postcss.config.js +6 -0
  165. package/public/.well-known/publickey.info@striae.org.asc +17 -0
  166. package/public/.well-known/security.txt +7 -0
  167. package/public/_headers +28 -0
  168. package/public/_routes.json +13 -0
  169. package/public/assets/striae.jpg +0 -0
  170. package/public/clear.jpg +0 -0
  171. package/public/favicon.ico +0 -0
  172. package/public/favicon.svg +9 -0
  173. package/public/icon-256.png +0 -0
  174. package/public/icon-512.png +0 -0
  175. package/public/logo-dark.png +0 -0
  176. package/public/manifest.json +25 -0
  177. package/public/oin-badge.png +0 -0
  178. package/public/shortcut.png +0 -0
  179. package/public/social-image.png +0 -0
  180. package/public/striae-ascii.txt +10 -0
  181. package/scripts/deploy-all.sh +100 -0
  182. package/scripts/deploy-config.sh +940 -0
  183. package/scripts/deploy-pages.sh +34 -0
  184. package/scripts/deploy-worker-secrets.sh +215 -0
  185. package/scripts/dev.cjs +23 -0
  186. package/scripts/install-workers.sh +88 -0
  187. package/scripts/run-eslint.cjs +35 -0
  188. package/scripts/update-compatibility-dates.cjs +124 -0
  189. package/scripts/update-markdown-versions.cjs +43 -0
  190. package/tailwind.config.ts +22 -0
  191. package/tsconfig.json +33 -0
  192. package/vite.config.ts +35 -0
  193. package/worker-configuration.d.ts +7490 -0
  194. package/workers/audit-worker/package.json +17 -0
  195. package/workers/audit-worker/src/audit-worker.example.ts +195 -0
  196. package/workers/audit-worker/worker-configuration.d.ts +7448 -0
  197. package/workers/audit-worker/wrangler.jsonc.example +29 -0
  198. package/workers/data-worker/package.json +17 -0
  199. package/workers/data-worker/src/data-worker.example.ts +267 -0
  200. package/workers/data-worker/src/signature-utils.ts +79 -0
  201. package/workers/data-worker/src/signing-payload-utils.ts +290 -0
  202. package/workers/data-worker/worker-configuration.d.ts +7448 -0
  203. package/workers/data-worker/wrangler.jsonc.example +30 -0
  204. package/workers/image-worker/package.json +17 -0
  205. package/workers/image-worker/src/image-worker.example.ts +180 -0
  206. package/workers/image-worker/worker-configuration.d.ts +7447 -0
  207. package/workers/image-worker/wrangler.jsonc.example +22 -0
  208. package/workers/keys-worker/package.json +17 -0
  209. package/workers/keys-worker/src/keys.example.ts +66 -0
  210. package/workers/keys-worker/src/keys.ts +66 -0
  211. package/workers/keys-worker/worker-configuration.d.ts +7447 -0
  212. package/workers/keys-worker/wrangler.jsonc.example +22 -0
  213. package/workers/pdf-worker/package.json +17 -0
  214. package/workers/pdf-worker/src/format-striae.ts +534 -0
  215. package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
  216. package/workers/pdf-worker/src/report-types.ts +69 -0
  217. package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
  218. package/workers/pdf-worker/wrangler.jsonc.example +26 -0
  219. package/workers/user-worker/package.json +17 -0
  220. package/workers/user-worker/src/user-worker.example.ts +636 -0
  221. package/workers/user-worker/worker-configuration.d.ts +7448 -0
  222. package/workers/user-worker/wrangler.jsonc.example +29 -0
  223. package/wrangler.toml.example +8 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Utility functions for batched operations with rate limiting protection
3
+ */
4
+
5
+ interface BatchOperationOptions {
6
+ batchSize: number;
7
+ baseDelay: number;
8
+ maxRetries: number;
9
+ retryMultiplier: number;
10
+ }
11
+
12
+ interface BatchResult<T> {
13
+ successful: Array<{ item: T; result: any }>;
14
+ failed: Array<{ item: T; error: string; retryCount: number }>;
15
+ totalProcessed: number;
16
+ }
17
+
18
+ /**
19
+ * Execute operations in batches with exponential backoff on failures
20
+ */
21
+ export const executeBatchOperations = async <T>(
22
+ items: T[],
23
+ operation: (item: T) => Promise<any>,
24
+ options: Partial<BatchOperationOptions> = {}
25
+ ): Promise<BatchResult<T>> => {
26
+ const config = {
27
+ batchSize: 3,
28
+ baseDelay: 300,
29
+ maxRetries: 2,
30
+ retryMultiplier: 2,
31
+ ...options
32
+ };
33
+
34
+ const successful: Array<{ item: T; result: any }> = [];
35
+ const failed: Array<{ item: T; error: string; retryCount: number }> = [];
36
+ let totalProcessed = 0;
37
+
38
+ // Process items in batches
39
+ for (let i = 0; i < items.length; i += config.batchSize) {
40
+ const batch = items.slice(i, i + config.batchSize);
41
+ const batchNumber = Math.floor(i / config.batchSize) + 1;
42
+ const totalBatches = Math.ceil(items.length / config.batchSize);
43
+
44
+ console.log(`📦 Processing batch ${batchNumber}/${totalBatches} (${batch.length} items)...`);
45
+
46
+ // Process batch with retry logic
47
+ const batchResults = await Promise.allSettled(
48
+ batch.map(async item => {
49
+ let lastError: Error | null = null;
50
+
51
+ for (let retryCount = 0; retryCount <= config.maxRetries; retryCount++) {
52
+ try {
53
+ const result = await operation(item);
54
+ return { item, result };
55
+ } catch (error) {
56
+ lastError = error instanceof Error ? error : new Error('Unknown error');
57
+
58
+ if (retryCount < config.maxRetries) {
59
+ // Wait before retry with exponential backoff
60
+ const retryDelay = config.baseDelay * Math.pow(config.retryMultiplier, retryCount);
61
+ console.warn(`⚠️ Operation failed, retrying in ${retryDelay}ms (attempt ${retryCount + 2}/${config.maxRetries + 1})`);
62
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
63
+ }
64
+ }
65
+ }
66
+
67
+ // All retries exhausted
68
+ throw lastError;
69
+ })
70
+ );
71
+
72
+ // Process batch results
73
+ batchResults.forEach((result, index) => {
74
+ totalProcessed++;
75
+ if (result.status === 'fulfilled') {
76
+ successful.push(result.value);
77
+ } else {
78
+ failed.push({
79
+ item: batch[index],
80
+ error: result.reason?.message || 'Unknown error',
81
+ retryCount: config.maxRetries
82
+ });
83
+ }
84
+ });
85
+
86
+ // Add delay between batches if not the last batch
87
+ if (i + config.batchSize < items.length) {
88
+ const delay = config.baseDelay + (failed.length > 0 ? config.baseDelay : 0); // Extra delay if failures occurred
89
+ console.log(`⏱️ Waiting ${delay}ms before next batch...`);
90
+ await new Promise(resolve => setTimeout(resolve, delay));
91
+ }
92
+ }
93
+
94
+ return {
95
+ successful,
96
+ failed,
97
+ totalProcessed
98
+ };
99
+ };
100
+
101
+ /**
102
+ * Rate-limited audit logging to prevent overwhelming the audit service
103
+ */
104
+ export const batchedAuditLog = async (
105
+ auditEntries: Array<() => Promise<void>>,
106
+ options: { batchSize?: number; delay?: number } = {}
107
+ ): Promise<{ successful: number; failed: number }> => {
108
+ const { batchSize = 2, delay = 500 } = options;
109
+ let successful = 0;
110
+ let failed = 0;
111
+
112
+ for (let i = 0; i < auditEntries.length; i += batchSize) {
113
+ const batch = auditEntries.slice(i, i + batchSize);
114
+
115
+ const results = await Promise.allSettled(
116
+ batch.map(auditFn => auditFn())
117
+ );
118
+
119
+ results.forEach(result => {
120
+ if (result.status === 'fulfilled') {
121
+ successful++;
122
+ } else {
123
+ failed++;
124
+ console.error('Audit logging failed:', result.reason);
125
+ }
126
+ });
127
+
128
+ // Add delay between audit batches
129
+ if (i + batchSize < auditEntries.length) {
130
+ await new Promise(resolve => setTimeout(resolve, delay));
131
+ }
132
+ }
133
+
134
+ return { successful, failed };
135
+ };
@@ -0,0 +1,193 @@
1
+ import { ConfirmationImportData } from '~/types';
2
+ import {
3
+ ForensicManifestSignature,
4
+ FORENSIC_MANIFEST_SIGNATURE_ALGORITHM,
5
+ ManifestSignatureVerificationResult
6
+ } from './SHA256';
7
+ import { verifySignaturePayload } from './signature-utils';
8
+
9
+ export const CONFIRMATION_SIGNATURE_VERSION = '2.0';
10
+
11
+ const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/i;
12
+
13
+ type ConfirmationEntry = {
14
+ fullName: string;
15
+ badgeId: string;
16
+ timestamp: string;
17
+ confirmationId: string;
18
+ confirmedBy: string;
19
+ confirmedByEmail: string;
20
+ confirmedByCompany: string;
21
+ confirmedAt: string;
22
+ };
23
+
24
+ type ConfirmationMap = Record<string, ConfirmationEntry[]>;
25
+
26
+ function hasValidConfirmationEntry(entry: Partial<ConfirmationEntry>): entry is ConfirmationEntry {
27
+ return (
28
+ typeof entry.fullName === 'string' &&
29
+ typeof entry.badgeId === 'string' &&
30
+ typeof entry.timestamp === 'string' &&
31
+ typeof entry.confirmationId === 'string' &&
32
+ typeof entry.confirmedBy === 'string' &&
33
+ typeof entry.confirmedByEmail === 'string' &&
34
+ typeof entry.confirmedByCompany === 'string' &&
35
+ typeof entry.confirmedAt === 'string' &&
36
+ !Number.isNaN(Date.parse(entry.confirmedAt))
37
+ );
38
+ }
39
+
40
+ function isValidConfirmationData(candidate: Partial<ConfirmationImportData>): candidate is ConfirmationImportData {
41
+ if (!candidate || !candidate.metadata || !candidate.confirmations) {
42
+ return false;
43
+ }
44
+
45
+ const { metadata } = candidate;
46
+
47
+ if (
48
+ typeof metadata.caseNumber !== 'string' ||
49
+ typeof metadata.exportDate !== 'string' ||
50
+ typeof metadata.exportedBy !== 'string' ||
51
+ typeof metadata.exportedByUid !== 'string' ||
52
+ typeof metadata.exportedByName !== 'string' ||
53
+ typeof metadata.exportedByCompany !== 'string' ||
54
+ typeof metadata.totalConfirmations !== 'number' ||
55
+ metadata.totalConfirmations < 0 ||
56
+ typeof metadata.version !== 'string' ||
57
+ typeof metadata.hash !== 'string' ||
58
+ !SHA256_HEX_REGEX.test(metadata.hash)
59
+ ) {
60
+ return false;
61
+ }
62
+
63
+ if (Number.isNaN(Date.parse(metadata.exportDate))) {
64
+ return false;
65
+ }
66
+
67
+ if (
68
+ typeof metadata.originalExportCreatedAt === 'string' &&
69
+ Number.isNaN(Date.parse(metadata.originalExportCreatedAt))
70
+ ) {
71
+ return false;
72
+ }
73
+
74
+ const confirmations = candidate.confirmations as Record<string, unknown>;
75
+ for (const [imageId, confirmationList] of Object.entries(confirmations)) {
76
+ if (!imageId || !Array.isArray(confirmationList)) {
77
+ return false;
78
+ }
79
+
80
+ for (const confirmationEntry of confirmationList) {
81
+ if (
82
+ typeof confirmationEntry !== 'object' ||
83
+ confirmationEntry === null ||
84
+ !hasValidConfirmationEntry(confirmationEntry as Partial<ConfirmationEntry>)
85
+ ) {
86
+ return false;
87
+ }
88
+ }
89
+ }
90
+
91
+ return true;
92
+ }
93
+
94
+ function normalizeConfirmationEntries(entries: ConfirmationEntry[]): ConfirmationEntry[] {
95
+ return [...entries]
96
+ .map((entry) => ({
97
+ fullName: entry.fullName,
98
+ badgeId: entry.badgeId,
99
+ timestamp: entry.timestamp,
100
+ confirmationId: entry.confirmationId,
101
+ confirmedBy: entry.confirmedBy,
102
+ confirmedByEmail: entry.confirmedByEmail,
103
+ confirmedByCompany: entry.confirmedByCompany,
104
+ confirmedAt: entry.confirmedAt
105
+ }))
106
+ .sort((left, right) => {
107
+ const leftKey = `${left.confirmationId}|${left.confirmedAt}|${left.confirmedBy}`;
108
+ const rightKey = `${right.confirmationId}|${right.confirmedAt}|${right.confirmedBy}`;
109
+ return leftKey.localeCompare(rightKey);
110
+ });
111
+ }
112
+
113
+ function normalizeConfirmations(confirmations: ConfirmationMap): ConfirmationMap {
114
+ const normalized: ConfirmationMap = {};
115
+ const sortedImageIds = Object.keys(confirmations).sort();
116
+
117
+ for (const imageId of sortedImageIds) {
118
+ normalized[imageId] = normalizeConfirmationEntries(confirmations[imageId] || []);
119
+ }
120
+
121
+ return normalized;
122
+ }
123
+
124
+ export function createConfirmationSigningPayload(
125
+ confirmationData: ConfirmationImportData,
126
+ signatureVersion: string = CONFIRMATION_SIGNATURE_VERSION
127
+ ): string {
128
+ const canonicalPayload = {
129
+ signatureVersion,
130
+ metadata: {
131
+ caseNumber: confirmationData.metadata.caseNumber,
132
+ exportDate: confirmationData.metadata.exportDate,
133
+ exportedBy: confirmationData.metadata.exportedBy,
134
+ exportedByUid: confirmationData.metadata.exportedByUid,
135
+ exportedByName: confirmationData.metadata.exportedByName,
136
+ exportedByCompany: confirmationData.metadata.exportedByCompany,
137
+ totalConfirmations: confirmationData.metadata.totalConfirmations,
138
+ version: confirmationData.metadata.version,
139
+ hash: confirmationData.metadata.hash.toUpperCase(),
140
+ ...(confirmationData.metadata.originalExportCreatedAt
141
+ ? { originalExportCreatedAt: confirmationData.metadata.originalExportCreatedAt }
142
+ : {})
143
+ },
144
+ confirmations: normalizeConfirmations(confirmationData.confirmations)
145
+ };
146
+
147
+ return JSON.stringify(canonicalPayload);
148
+ }
149
+
150
+ export async function verifyConfirmationSignature(
151
+ confirmationData: Partial<ConfirmationImportData>
152
+ ): Promise<ManifestSignatureVerificationResult> {
153
+ const signature = confirmationData.metadata?.signature as ForensicManifestSignature | undefined;
154
+ const signatureVersion = confirmationData.metadata?.signatureVersion;
155
+
156
+ if (!signature) {
157
+ return {
158
+ isValid: false,
159
+ error: 'Missing confirmation signature'
160
+ };
161
+ }
162
+
163
+ if (signatureVersion !== CONFIRMATION_SIGNATURE_VERSION) {
164
+ return {
165
+ isValid: false,
166
+ keyId: signature.keyId,
167
+ error: `Unsupported confirmation signature version: ${signatureVersion || 'unknown'}`
168
+ };
169
+ }
170
+
171
+ if (!isValidConfirmationData(confirmationData)) {
172
+ return {
173
+ isValid: false,
174
+ keyId: signature.keyId,
175
+ error: 'Confirmation content is malformed'
176
+ };
177
+ }
178
+
179
+ const payload = createConfirmationSigningPayload(confirmationData, signatureVersion);
180
+
181
+ return verifySignaturePayload(
182
+ payload,
183
+ signature,
184
+ FORENSIC_MANIFEST_SIGNATURE_ALGORITHM,
185
+ {
186
+ unsupportedAlgorithmPrefix: 'Unsupported confirmation signature algorithm',
187
+ missingKeyOrValueError: 'Missing confirmation signature key ID or value',
188
+ noVerificationKeyPrefix: 'No verification key configured for key ID',
189
+ invalidPublicKeyError: 'Confirmation signature verification failed: invalid public key',
190
+ verificationFailedError: 'Confirmation signature verification failed'
191
+ }
192
+ );
193
+ }