@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.
- package/.env.example +100 -0
- package/LICENSE +190 -0
- package/NOTICE +18 -0
- package/README.md +133 -0
- package/app/components/actions/case-export/core-export.ts +328 -0
- package/app/components/actions/case-export/data-processing.ts +167 -0
- package/app/components/actions/case-export/download-handlers.ts +900 -0
- package/app/components/actions/case-export/index.ts +41 -0
- package/app/components/actions/case-export/metadata-helpers.ts +107 -0
- package/app/components/actions/case-export/types-constants.ts +56 -0
- package/app/components/actions/case-export/validation-utils.ts +25 -0
- package/app/components/actions/case-export.ts +4 -0
- package/app/components/actions/case-import/annotation-import.ts +35 -0
- package/app/components/actions/case-import/confirmation-import.ts +363 -0
- package/app/components/actions/case-import/image-operations.ts +61 -0
- package/app/components/actions/case-import/index.ts +39 -0
- package/app/components/actions/case-import/orchestrator.ts +420 -0
- package/app/components/actions/case-import/storage-operations.ts +270 -0
- package/app/components/actions/case-import/validation.ts +189 -0
- package/app/components/actions/case-import/zip-processing.ts +413 -0
- package/app/components/actions/case-manage.ts +524 -0
- package/app/components/actions/case-review.ts +4 -0
- package/app/components/actions/confirm-export.ts +351 -0
- package/app/components/actions/generate-pdf.ts +210 -0
- package/app/components/actions/image-manage.ts +385 -0
- package/app/components/actions/notes-manage.ts +33 -0
- package/app/components/actions/signout.module.css +15 -0
- package/app/components/actions/signout.tsx +50 -0
- package/app/components/audit/user-audit-viewer.tsx +975 -0
- package/app/components/audit/user-audit.module.css +568 -0
- package/app/components/auth/auth-provider.tsx +78 -0
- package/app/components/auth/mfa-enrollment.module.css +268 -0
- package/app/components/auth/mfa-enrollment.tsx +398 -0
- package/app/components/auth/mfa-verification.module.css +251 -0
- package/app/components/auth/mfa-verification.tsx +295 -0
- package/app/components/button/button.module.css +63 -0
- package/app/components/button/button.tsx +46 -0
- package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
- package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
- package/app/components/canvas/canvas.module.css +314 -0
- package/app/components/canvas/canvas.tsx +449 -0
- package/app/components/canvas/confirmation/confirmation.module.css +187 -0
- package/app/components/canvas/confirmation/confirmation.tsx +214 -0
- package/app/components/colors/colors.module.css +59 -0
- package/app/components/colors/colors.tsx +68 -0
- package/app/components/form/base-form.tsx +21 -0
- package/app/components/form/form-button.tsx +28 -0
- package/app/components/form/form-field.tsx +53 -0
- package/app/components/form/form-message.tsx +17 -0
- package/app/components/form/form-toggle.tsx +23 -0
- package/app/components/form/form.module.css +427 -0
- package/app/components/form/index.ts +6 -0
- package/app/components/icon/icon.module.css +3 -0
- package/app/components/icon/icon.tsx +27 -0
- package/app/components/icon/icons.svg +102 -0
- package/app/components/icon/manifest.json +110 -0
- package/app/components/sidebar/case-export/case-export.module.css +386 -0
- package/app/components/sidebar/case-export/case-export.tsx +317 -0
- package/app/components/sidebar/case-import/case-import.module.css +626 -0
- package/app/components/sidebar/case-import/case-import.tsx +404 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
- package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
- package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
- package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
- package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
- package/app/components/sidebar/case-import/index.ts +18 -0
- package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
- package/app/components/sidebar/cases/cases-modal.module.css +166 -0
- package/app/components/sidebar/cases/cases-modal.tsx +201 -0
- package/app/components/sidebar/cases/cases.module.css +713 -0
- package/app/components/sidebar/files/files-modal.module.css +209 -0
- package/app/components/sidebar/files/files-modal.tsx +239 -0
- package/app/components/sidebar/hash/hash-utility.module.css +366 -0
- package/app/components/sidebar/hash/hash-utility.tsx +982 -0
- package/app/components/sidebar/notes/notes-modal.tsx +51 -0
- package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
- package/app/components/sidebar/notes/notes.module.css +360 -0
- package/app/components/sidebar/sidebar-container.tsx +149 -0
- package/app/components/sidebar/sidebar.module.css +321 -0
- package/app/components/sidebar/sidebar.tsx +215 -0
- package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
- package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
- package/app/components/theme-provider/theme-provider.tsx +131 -0
- package/app/components/theme-provider/theme.ts +155 -0
- package/app/components/toast/toast.module.css +137 -0
- package/app/components/toast/toast.tsx +56 -0
- package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
- package/app/components/toolbar/toolbar.module.css +42 -0
- package/app/components/toolbar/toolbar.tsx +167 -0
- package/app/components/user/delete-account.module.css +274 -0
- package/app/components/user/delete-account.tsx +471 -0
- package/app/components/user/inactivity-warning.module.css +145 -0
- package/app/components/user/inactivity-warning.tsx +84 -0
- package/app/components/user/manage-profile.module.css +190 -0
- package/app/components/user/manage-profile.tsx +253 -0
- package/app/components/user/mfa-phone-update.tsx +739 -0
- package/app/config-example/admin-service.json +13 -0
- package/app/config-example/config.json +17 -0
- package/app/config-example/firebase.ts +21 -0
- package/app/config-example/inactivity.ts +13 -0
- package/app/config-example/meta-config.json +6 -0
- package/app/contexts/auth.context.ts +12 -0
- package/app/entry.client.tsx +12 -0
- package/app/entry.server.tsx +44 -0
- package/app/hooks/useInactivityTimeout.ts +110 -0
- package/app/root.tsx +170 -0
- package/app/routes/_index.tsx +16 -0
- package/app/routes/auth/emailActionHandler.module.css +232 -0
- package/app/routes/auth/emailActionHandler.tsx +405 -0
- package/app/routes/auth/emailVerification.tsx +120 -0
- package/app/routes/auth/login.module.css +523 -0
- package/app/routes/auth/login.tsx +654 -0
- package/app/routes/auth/passwordReset.module.css +274 -0
- package/app/routes/auth/passwordReset.tsx +154 -0
- package/app/routes/auth/route.ts +16 -0
- package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
- package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
- package/app/routes/mobile-prevented/route.ts +14 -0
- package/app/routes/striae/striae.module.css +30 -0
- package/app/routes/striae/striae.tsx +417 -0
- package/app/services/audit-export.service.ts +755 -0
- package/app/services/audit.service.ts +1454 -0
- package/app/services/firebase-errors.ts +106 -0
- package/app/services/firebase.ts +15 -0
- package/app/styles/legal-pages.module.css +113 -0
- package/app/styles/root.module.css +146 -0
- package/app/tailwind.css +225 -0
- package/app/types/annotations.ts +45 -0
- package/app/types/audit.ts +301 -0
- package/app/types/case.ts +90 -0
- package/app/types/export.ts +8 -0
- package/app/types/file.ts +30 -0
- package/app/types/import.ts +107 -0
- package/app/types/index.ts +24 -0
- package/app/types/user.ts +38 -0
- package/app/utils/SHA256.ts +461 -0
- package/app/utils/annotation-timestamp.ts +25 -0
- package/app/utils/audit-export-signature.ts +117 -0
- package/app/utils/auth-action-settings.ts +48 -0
- package/app/utils/auth.ts +34 -0
- package/app/utils/batch-operations.ts +135 -0
- package/app/utils/confirmation-signature.ts +193 -0
- package/app/utils/data-operations.ts +871 -0
- package/app/utils/device-detection.ts +5 -0
- package/app/utils/html-sanitizer.ts +80 -0
- package/app/utils/id-generator.ts +36 -0
- package/app/utils/meta.ts +48 -0
- package/app/utils/mfa-phone.ts +97 -0
- package/app/utils/mfa.ts +79 -0
- package/app/utils/password-policy.ts +28 -0
- package/app/utils/permissions.ts +562 -0
- package/app/utils/signature-utils.ts +160 -0
- package/app/utils/style.ts +83 -0
- package/app/utils/version.ts +5 -0
- package/firebase.json +11 -0
- package/functions/[[path]].ts +10 -0
- package/package.json +138 -0
- package/postcss.config.js +6 -0
- package/public/.well-known/publickey.info@striae.org.asc +17 -0
- package/public/.well-known/security.txt +7 -0
- package/public/_headers +28 -0
- package/public/_routes.json +13 -0
- package/public/assets/striae.jpg +0 -0
- package/public/clear.jpg +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-dark.png +0 -0
- package/public/manifest.json +25 -0
- package/public/oin-badge.png +0 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/striae-ascii.txt +10 -0
- package/scripts/deploy-all.sh +100 -0
- package/scripts/deploy-config.sh +940 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-worker-secrets.sh +215 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/install-workers.sh +88 -0
- package/scripts/run-eslint.cjs +35 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/tailwind.config.ts +22 -0
- package/tsconfig.json +33 -0
- package/vite.config.ts +35 -0
- package/worker-configuration.d.ts +7490 -0
- package/workers/audit-worker/package.json +17 -0
- package/workers/audit-worker/src/audit-worker.example.ts +195 -0
- package/workers/audit-worker/worker-configuration.d.ts +7448 -0
- package/workers/audit-worker/wrangler.jsonc.example +29 -0
- package/workers/data-worker/package.json +17 -0
- package/workers/data-worker/src/data-worker.example.ts +267 -0
- package/workers/data-worker/src/signature-utils.ts +79 -0
- package/workers/data-worker/src/signing-payload-utils.ts +290 -0
- package/workers/data-worker/worker-configuration.d.ts +7448 -0
- package/workers/data-worker/wrangler.jsonc.example +30 -0
- package/workers/image-worker/package.json +17 -0
- package/workers/image-worker/src/image-worker.example.ts +180 -0
- package/workers/image-worker/worker-configuration.d.ts +7447 -0
- package/workers/image-worker/wrangler.jsonc.example +22 -0
- package/workers/keys-worker/package.json +17 -0
- package/workers/keys-worker/src/keys.example.ts +66 -0
- package/workers/keys-worker/src/keys.ts +66 -0
- package/workers/keys-worker/worker-configuration.d.ts +7447 -0
- package/workers/keys-worker/wrangler.jsonc.example +22 -0
- package/workers/pdf-worker/package.json +17 -0
- package/workers/pdf-worker/src/format-striae.ts +534 -0
- package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
- package/workers/pdf-worker/src/report-types.ts +69 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
- package/workers/pdf-worker/wrangler.jsonc.example +26 -0
- package/workers/user-worker/package.json +17 -0
- package/workers/user-worker/src/user-worker.example.ts +636 -0
- package/workers/user-worker/worker-configuration.d.ts +7448 -0
- package/workers/user-worker/wrangler.jsonc.example +29 -0
- 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
|
+
}
|