@striae-org/striae 5.4.1 → 5.4.2
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/app/components/actions/case-import/confirmation-import.ts +11 -0
- package/app/components/actions/case-import/orchestrator.ts +1 -0
- package/app/components/actions/case-import/storage-operations.ts +2 -0
- package/app/components/actions/confirm-export.ts +6 -4
- package/app/components/canvas/confirmation/confirmation.tsx +4 -18
- package/app/types/case.ts +1 -0
- package/app/types/import.ts +2 -0
- package/app/utils/forensics/confirmation-signature.ts +10 -0
- package/package.json +1 -1
- package/worker-configuration.d.ts +4 -3
- package/workers/data-worker/src/signing-payload-utils.ts +11 -0
|
@@ -185,6 +185,17 @@ export async function importConfirmationData(
|
|
|
185
185
|
throw new Error('You cannot import confirmation data that you exported yourself.');
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// Validate that this confirmation package was intended for the current user.
|
|
189
|
+
// originalCaseOwnerUid is embedded at export time and covered by the package signature.
|
|
190
|
+
if (
|
|
191
|
+
confirmationData.metadata.originalCaseOwnerUid &&
|
|
192
|
+
confirmationData.metadata.originalCaseOwnerUid !== user.uid
|
|
193
|
+
) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
'This confirmation package was not exported for your case. It can only be imported by the original case owner.'
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
188
199
|
onProgress?.('Validating case', 50, 'Checking case exists...');
|
|
189
200
|
|
|
190
201
|
// Check if case exists in user's regular cases
|
|
@@ -512,6 +512,7 @@ export async function importCaseForReview(
|
|
|
512
512
|
importedAt: new Date().toISOString(),
|
|
513
513
|
originalExportDate: caseData.metadata.exportDate,
|
|
514
514
|
originalExportedBy: caseData.metadata.exportedBy || 'Unknown',
|
|
515
|
+
originalExportedByUid: caseData.metadata.exportedByUid,
|
|
515
516
|
sourceHash: parsedForensicManifest?.manifestHash,
|
|
516
517
|
sourceManifestVersion: parsedForensicManifest?.manifestVersion,
|
|
517
518
|
sourceSignatureKeyId: parsedForensicManifest?.signature?.keyId,
|
|
@@ -118,6 +118,8 @@ export async function storeCaseDataInR2(
|
|
|
118
118
|
...(bundledAuditTrail && { bundledAuditTrail }),
|
|
119
119
|
// Add original image ID mapping for confirmation linking
|
|
120
120
|
originalImageIds: originalImageIds,
|
|
121
|
+
// Store original case owner UID so confirmation exports can embed the intended recipient
|
|
122
|
+
...(caseData.metadata.exportedByUid && { originalCaseOwnerUid: caseData.metadata.exportedByUid }),
|
|
121
123
|
// Add forensic manifest timestamp if available for confirmation exports
|
|
122
124
|
...(forensicManifest?.createdAt && { forensicManifestCreatedAt: forensicManifest.createdAt }),
|
|
123
125
|
// Store full forensic manifest metadata for chain-of-custody validation
|
|
@@ -153,7 +153,7 @@ export async function getCaseConfirmations(
|
|
|
153
153
|
export async function getCaseDataWithManifest(
|
|
154
154
|
user: User,
|
|
155
155
|
caseNumber: string
|
|
156
|
-
): Promise<{ confirmations: CaseConfirmations | null; forensicManifestCreatedAt?: string }> {
|
|
156
|
+
): Promise<{ confirmations: CaseConfirmations | null; forensicManifestCreatedAt?: string; originalCaseOwnerUid?: string }> {
|
|
157
157
|
try {
|
|
158
158
|
const caseData = await getCaseData(user, caseNumber) as CaseDataWithConfirmations & { forensicManifestCreatedAt?: string };
|
|
159
159
|
if (!caseData) {
|
|
@@ -163,7 +163,8 @@ export async function getCaseDataWithManifest(
|
|
|
163
163
|
|
|
164
164
|
return {
|
|
165
165
|
confirmations: caseData.confirmations || null,
|
|
166
|
-
forensicManifestCreatedAt: caseData.forensicManifestCreatedAt
|
|
166
|
+
forensicManifestCreatedAt: caseData.forensicManifestCreatedAt,
|
|
167
|
+
originalCaseOwnerUid: caseData.originalCaseOwnerUid
|
|
167
168
|
};
|
|
168
169
|
|
|
169
170
|
} catch (error) {
|
|
@@ -206,7 +207,7 @@ export async function exportConfirmationData(
|
|
|
206
207
|
auditService.startWorkflow(caseNumber);
|
|
207
208
|
|
|
208
209
|
// Get all confirmation data and forensic manifest info for the case
|
|
209
|
-
const { confirmations: caseConfirmations, forensicManifestCreatedAt } = await getCaseDataWithManifest(user, caseNumber);
|
|
210
|
+
const { confirmations: caseConfirmations, forensicManifestCreatedAt, originalCaseOwnerUid } = await getCaseDataWithManifest(user, caseNumber);
|
|
210
211
|
|
|
211
212
|
if (!caseConfirmations || Object.keys(caseConfirmations).length === 0) {
|
|
212
213
|
throw new Error('No confirmation data found for this case');
|
|
@@ -256,7 +257,8 @@ export async function exportConfirmationData(
|
|
|
256
257
|
...userMetadata,
|
|
257
258
|
totalConfirmations: Object.keys(caseConfirmations).length,
|
|
258
259
|
version: '2.0',
|
|
259
|
-
...(originalExportCreatedAt && { originalExportCreatedAt })
|
|
260
|
+
...(originalExportCreatedAt && { originalExportCreatedAt }),
|
|
261
|
+
...(originalCaseOwnerUid && { originalCaseOwnerUid })
|
|
260
262
|
},
|
|
261
263
|
confirmations: caseConfirmations
|
|
262
264
|
};
|
|
@@ -72,11 +72,6 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
72
72
|
if (!isOpen) return null;
|
|
73
73
|
|
|
74
74
|
const handleConfirm = async () => {
|
|
75
|
-
if (!badgeId.trim()) {
|
|
76
|
-
setError('Badge/ID is required');
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
75
|
setIsConfirming(true);
|
|
81
76
|
setError('');
|
|
82
77
|
|
|
@@ -134,19 +129,10 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
134
129
|
</div>
|
|
135
130
|
|
|
136
131
|
<div className={styles.field}>
|
|
137
|
-
<
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
className={styles.input}
|
|
142
|
-
value={badgeId}
|
|
143
|
-
onChange={(e) => {
|
|
144
|
-
setBadgeId(e.target.value);
|
|
145
|
-
if (error) setError('');
|
|
146
|
-
}}
|
|
147
|
-
placeholder="Enter your badge or ID number"
|
|
148
|
-
disabled={isConfirming || hasExistingConfirmation}
|
|
149
|
-
/>
|
|
132
|
+
<span className={styles.label}>Badge/ID:</span>
|
|
133
|
+
<div className={styles.readOnlyValue}>
|
|
134
|
+
{badgeId || 'Not set'}
|
|
135
|
+
</div>
|
|
150
136
|
</div>
|
|
151
137
|
|
|
152
138
|
<div className={styles.field}>
|
package/app/types/case.ts
CHANGED
|
@@ -115,6 +115,7 @@ export interface CaseDataWithConfirmations {
|
|
|
115
115
|
archiveReason?: string;
|
|
116
116
|
importedAt?: string;
|
|
117
117
|
originalImageIds?: { [originalId: string]: string };
|
|
118
|
+
originalCaseOwnerUid?: string;
|
|
118
119
|
confirmations?: CaseConfirmations;
|
|
119
120
|
bundledAuditTrail?: BundledAuditTrailData;
|
|
120
121
|
}
|
package/app/types/import.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface ReadOnlyCaseMetadata {
|
|
|
21
21
|
importedAt: string;
|
|
22
22
|
originalExportDate: string;
|
|
23
23
|
originalExportedBy: string;
|
|
24
|
+
originalExportedByUid?: string;
|
|
24
25
|
sourceHash?: string;
|
|
25
26
|
sourceManifestVersion?: string;
|
|
26
27
|
sourceSignatureKeyId?: string;
|
|
@@ -63,6 +64,7 @@ export interface ConfirmationImportData {
|
|
|
63
64
|
value: string;
|
|
64
65
|
};
|
|
65
66
|
originalExportCreatedAt?: string;
|
|
67
|
+
originalCaseOwnerUid?: string;
|
|
66
68
|
};
|
|
67
69
|
confirmations: {
|
|
68
70
|
[originalImageId: string]: Array<{
|
|
@@ -71,6 +71,13 @@ function isValidConfirmationData(candidate: Partial<ConfirmationImportData>): ca
|
|
|
71
71
|
return false;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
if (
|
|
75
|
+
typeof metadata.originalCaseOwnerUid !== 'undefined' &&
|
|
76
|
+
(typeof metadata.originalCaseOwnerUid !== 'string' || metadata.originalCaseOwnerUid.trim().length === 0)
|
|
77
|
+
) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
const confirmations = candidate.confirmations as Record<string, unknown>;
|
|
75
82
|
for (const [imageId, confirmationList] of Object.entries(confirmations)) {
|
|
76
83
|
if (!imageId || !Array.isArray(confirmationList)) {
|
|
@@ -146,6 +153,9 @@ export function createConfirmationSigningPayload(
|
|
|
146
153
|
hash: confirmationData.metadata.hash.toUpperCase(),
|
|
147
154
|
...(confirmationData.metadata.originalExportCreatedAt
|
|
148
155
|
? { originalExportCreatedAt: confirmationData.metadata.originalExportCreatedAt }
|
|
156
|
+
: {}),
|
|
157
|
+
...(confirmationData.metadata.originalCaseOwnerUid
|
|
158
|
+
? { originalCaseOwnerUid: confirmationData.metadata.originalCaseOwnerUid }
|
|
149
159
|
: {})
|
|
150
160
|
},
|
|
151
161
|
confirmations: normalizeConfirmations(confirmationData.confirmations)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
-
// Generated by Wrangler by running `wrangler types` (hash:
|
|
3
|
-
// Runtime types generated with workerd@1.20250823.0 2026-03-
|
|
2
|
+
// Generated by Wrangler by running `wrangler types` (hash: df3e4db815fe1724ed2abbb4f6b2065f)
|
|
3
|
+
// Runtime types generated with workerd@1.20250823.0 2026-03-31 nodejs_compat
|
|
4
4
|
declare namespace Cloudflare {
|
|
5
5
|
interface Env {
|
|
6
6
|
ACCOUNT_ID: string;
|
|
@@ -52,6 +52,7 @@ declare namespace Cloudflare {
|
|
|
52
52
|
IMAGES_WORKER_DOMAIN: string;
|
|
53
53
|
IMAGE_SIGNED_URL_SECRET: string;
|
|
54
54
|
IMAGE_SIGNED_URL_TTL_SECONDS: string;
|
|
55
|
+
IMAGE_SIGNED_URL_BASE_URL: string;
|
|
55
56
|
PDF_WORKER_NAME: string;
|
|
56
57
|
PDF_WORKER_DOMAIN: string;
|
|
57
58
|
PDF_WORKER_AUTH: string;
|
|
@@ -64,7 +65,7 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
|
|
|
64
65
|
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
|
|
65
66
|
};
|
|
66
67
|
declare namespace NodeJS {
|
|
67
|
-
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "ACCOUNT_ID" | "USER_DB_AUTH" | "R2_KEY_SECRET" | "IMAGES_API_TOKEN" | "API_KEY" | "AUTH_DOMAIN" | "PROJECT_ID" | "STORAGE_BUCKET" | "MESSAGING_SENDER_ID" | "APP_ID" | "MEASUREMENT_ID" | "FIREBASE_SERVICE_ACCOUNT_EMAIL" | "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" | "USER_KV_ENCRYPTION_PRIVATE_KEY" | "USER_KV_ENCRYPTION_KEY_ID" | "USER_KV_ENCRYPTION_PUBLIC_KEY" | "USER_KV_WRITE_ENDPOINTS_ENABLED" | "USER_KV_ENCRYPTION_KEYS_JSON" | "USER_KV_ENCRYPTION_ACTIVE_KEY_ID" | "MANIFEST_SIGNING_PRIVATE_KEY" | "MANIFEST_SIGNING_KEY_ID" | "MANIFEST_SIGNING_PUBLIC_KEY" | "EXPORT_ENCRYPTION_PRIVATE_KEY" | "EXPORT_ENCRYPTION_KEY_ID" | "EXPORT_ENCRYPTION_PUBLIC_KEY" | "EXPORT_ENCRYPTION_KEYS_JSON" | "EXPORT_ENCRYPTION_ACTIVE_KEY_ID" | "DATA_AT_REST_ENCRYPTION_ENABLED" | "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" | "DATA_AT_REST_ENCRYPTION_KEY_ID" | "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" | "DATA_AT_REST_ENCRYPTION_KEYS_JSON" | "DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID" | "PAGES_PROJECT_NAME" | "PAGES_CUSTOM_DOMAIN" | "USER_WORKER_NAME" | "USER_WORKER_DOMAIN" | "KV_STORE_ID" | "DATA_WORKER_NAME" | "DATA_BUCKET_NAME" | "FILES_BUCKET_NAME" | "DATA_WORKER_DOMAIN" | "AUDIT_WORKER_NAME" | "AUDIT_BUCKET_NAME" | "AUDIT_WORKER_DOMAIN" | "IMAGES_WORKER_NAME" | "IMAGES_WORKER_DOMAIN" | "IMAGE_SIGNED_URL_SECRET" | "IMAGE_SIGNED_URL_TTL_SECONDS" | "PDF_WORKER_NAME" | "PDF_WORKER_DOMAIN" | "PDF_WORKER_AUTH" | "BROWSER_API_TOKEN" | "PRIMERSHEAR_EMAILS">> {}
|
|
68
|
+
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "ACCOUNT_ID" | "USER_DB_AUTH" | "R2_KEY_SECRET" | "IMAGES_API_TOKEN" | "API_KEY" | "AUTH_DOMAIN" | "PROJECT_ID" | "STORAGE_BUCKET" | "MESSAGING_SENDER_ID" | "APP_ID" | "MEASUREMENT_ID" | "FIREBASE_SERVICE_ACCOUNT_EMAIL" | "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" | "USER_KV_ENCRYPTION_PRIVATE_KEY" | "USER_KV_ENCRYPTION_KEY_ID" | "USER_KV_ENCRYPTION_PUBLIC_KEY" | "USER_KV_WRITE_ENDPOINTS_ENABLED" | "USER_KV_ENCRYPTION_KEYS_JSON" | "USER_KV_ENCRYPTION_ACTIVE_KEY_ID" | "MANIFEST_SIGNING_PRIVATE_KEY" | "MANIFEST_SIGNING_KEY_ID" | "MANIFEST_SIGNING_PUBLIC_KEY" | "EXPORT_ENCRYPTION_PRIVATE_KEY" | "EXPORT_ENCRYPTION_KEY_ID" | "EXPORT_ENCRYPTION_PUBLIC_KEY" | "EXPORT_ENCRYPTION_KEYS_JSON" | "EXPORT_ENCRYPTION_ACTIVE_KEY_ID" | "DATA_AT_REST_ENCRYPTION_ENABLED" | "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" | "DATA_AT_REST_ENCRYPTION_KEY_ID" | "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" | "DATA_AT_REST_ENCRYPTION_KEYS_JSON" | "DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID" | "PAGES_PROJECT_NAME" | "PAGES_CUSTOM_DOMAIN" | "USER_WORKER_NAME" | "USER_WORKER_DOMAIN" | "KV_STORE_ID" | "DATA_WORKER_NAME" | "DATA_BUCKET_NAME" | "FILES_BUCKET_NAME" | "DATA_WORKER_DOMAIN" | "AUDIT_WORKER_NAME" | "AUDIT_BUCKET_NAME" | "AUDIT_WORKER_DOMAIN" | "IMAGES_WORKER_NAME" | "IMAGES_WORKER_DOMAIN" | "IMAGE_SIGNED_URL_SECRET" | "IMAGE_SIGNED_URL_TTL_SECONDS" | "IMAGE_SIGNED_URL_BASE_URL" | "PDF_WORKER_NAME" | "PDF_WORKER_DOMAIN" | "PDF_WORKER_AUTH" | "BROWSER_API_TOKEN" | "PRIMERSHEAR_EMAILS">> {}
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// Begin runtime types
|
|
@@ -18,6 +18,7 @@ export interface ConfirmationSignatureMetadata {
|
|
|
18
18
|
version: string;
|
|
19
19
|
hash: string;
|
|
20
20
|
originalExportCreatedAt?: string;
|
|
21
|
+
originalCaseOwnerUid?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export interface ConfirmationRecord {
|
|
@@ -153,6 +154,13 @@ export function isValidConfirmationPayload(
|
|
|
153
154
|
return false;
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
if (
|
|
158
|
+
typeof metadata.originalCaseOwnerUid !== 'undefined' &&
|
|
159
|
+
(typeof metadata.originalCaseOwnerUid !== 'string' || metadata.originalCaseOwnerUid.trim().length === 0)
|
|
160
|
+
) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
156
164
|
for (const [imageId, confirmationList] of Object.entries(candidate.confirmations)) {
|
|
157
165
|
if (!imageId || !Array.isArray(confirmationList)) {
|
|
158
166
|
return false;
|
|
@@ -271,6 +279,9 @@ export function createConfirmationSigningPayload(confirmationData: ConfirmationS
|
|
|
271
279
|
hash: confirmationData.metadata.hash.toUpperCase(),
|
|
272
280
|
...(confirmationData.metadata.originalExportCreatedAt
|
|
273
281
|
? { originalExportCreatedAt: confirmationData.metadata.originalExportCreatedAt }
|
|
282
|
+
: {}),
|
|
283
|
+
...(confirmationData.metadata.originalCaseOwnerUid
|
|
284
|
+
? { originalCaseOwnerUid: confirmationData.metadata.originalCaseOwnerUid }
|
|
274
285
|
: {})
|
|
275
286
|
},
|
|
276
287
|
confirmations: normalizeConfirmations(confirmationData.confirmations)
|