@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.
@@ -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
- <label className={styles.label} htmlFor="badgeId">Badge/ID: *</label>
138
- <input
139
- id="badgeId"
140
- type="text"
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
  }
@@ -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.1",
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: d8f8f87d89a635e81e94aa31fb52008f)
3
- // Runtime types generated with workerd@1.20250823.0 2026-03-26 nodejs_compat
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)