Utilities Guide - striae-org/striae GitHub Wiki

Table of Contents

  1. Overview
  2. Utility Organization
  3. API Client Transport
  4. Permissions and User Data
  5. Data Operations
  6. Forensic Signing and Hashing
  7. Annotation Utilities
  8. ID Generation
  9. Batch Operations
  10. Auth Flow Helpers
  1. Style and Layout Utilities
  2. Case Messages
  3. Version Utility

Overview

Utilities are organized under app/utils/ by domain (api/, auth/, common/, data/, forensics/, ui/). They are pure TypeScript modules with no default exports. Import them through subpath aliases such as ~/utils/data, ~/utils/api, ~/utils/forensics, ~/utils/auth, ~/utils/common, and ~/utils/ui.

The highest-importance modules for case workflows are app/utils/data/*, app/utils/data/permissions.ts, and the API client wrappers in app/utils/api/. Use these modules instead of direct worker fetch calls from components or routes.


Utility Organization

Current top-level utility domains:

  • app/utils/api/ - same-origin Pages API clients
  • app/utils/auth/ - auth configuration, MFA, and password policy helpers
  • app/utils/common/ - generic helpers such as batch execution, IDs, and version access
  • app/utils/data/ - permission checks, case/file annotation operations, list filters, and confirmation summary helpers
  • app/utils/forensics/ - hashing, signature payloads, and export verification
  • app/utils/ui/ - display-oriented helpers such as timestamp preservation and style utilities

Most domains expose barrel files:

  • ~/utils/api
  • ~/utils/auth
  • ~/utils/common
  • ~/utils/data
  • ~/utils/forensics
  • ~/utils/ui

Prefer importing from those barrels for new code unless a deep import is needed for a very specific internal helper.


API Client Transport

auth.ts

The ~/utils/auth barrel re-exports shared auth-flow helpers used across the app.

import { evaluatePasswordPolicy } from '~/utils/auth';

Use this domain for auth action settings, MFA helpers, phone MFA helpers, and password policy validation rather than duplicating auth-flow logic in routes or components.

API client wrappers

Public app-to-service transport is implemented through same-origin Pages APIs. These wrappers retrieve Firebase ID tokens from the current user session and add Authorization: Bearer {idToken} automatically:

  • user-api-client.ts -> /api/user/*
  • data-api-client.ts -> /api/data/*
  • image-api-client.ts -> /api/image/*
  • pdf-api-client.ts -> /api/pdf/*

The app/utils/api/index.ts barrel re-exports all four clients, so new code can use ~/utils/api when importing multiple API helpers.

import { fetchDataApi } from '~/utils/api';

async function loadCaseJson(user: User, path: string): Promise<Response> {
  const response = await fetchDataApi(user, path, {
    method: 'GET',
    headers: {
      'Accept': 'application/json'
    }
  });

  if (!response.ok) {
    throw new Error(`Data API request failed: ${response.status}`);
  }

  return response;
}

image-api-client.ts also provides uploadImageApi with XHR progress callbacks for upload UX and createSignedImageUrlApi for generating temporary signed URLs for image access.

user-api-client.ts also provides checkUserExistsApi(user, targetUid) which verifies whether a target user account exists in KV without requiring admin privileges.


Permissions and User Data

permissions.ts

Central module for all user data reads/writes and access control decisions. Routes and actions must use these functions rather than calling the User Worker directly.

import {
  validateUserSession,
  getUserData,
  createUser,
  updateUserData,
  getUserCases,
  getUserReadOnlyCases,
  getUserLimits,
  getUserUsage,
  canCreateCase,
  canAccessCase,
  canModifyCase,
  canUploadFile,
  isUserPermitted,
  getLimitsDescription,
  getNotesViewPermission,
  getNotesButtonTooltip,
  type NotesViewPermission,
} from '~/utils/data';

Session Validation

const sessionValidation = await validateUserSession(user);
if (!sessionValidation.valid) {
  throw new Error(`Session validation failed: ${sessionValidation.reason}`);
}

validateUserSession checks that:

  • user.uid and user.email are present.
  • The user record exists in KV.
  • The KV email matches the Firebase session email.

Returns UserSessionValidation: { valid: boolean; reason?: string }.

User Data

// Read
const userData = await getUserData(user);           // UserData | null
const cases = await getUserCases(user);             // CaseMetadata[]
const readOnlyCases = await getUserReadOnlyCases(user); // ReadOnlyCaseMetadata[]
const permitted = await isUserPermitted(user);       // boolean

// Write
const newUser = await createUser(user, firstName, lastName, company, permitted, badgeId);
const updated = await updateUserData(user, { company: 'New Org' });

Access Control

Three guards are used throughout the codebase for all case operations:

Function Use for Returns
canCreateCase(user) Before creating a new case { canCreate: boolean; reason?: string }
canAccessCase(user, caseNumber) Before any read on a case PermissionResult
canModifyCase(user, caseNumber) Before any write to a case PermissionResult
canUploadFile(user, currentFileCount) Before accepting an image upload { canUpload: boolean; reason?: string }

canAccessCase grants access if the case is in userData.cases (owned) or userData.readOnlyCases (shared for review). canModifyCase additionally requires userData.permitted === true for owned cases.

const accessCheck = await canAccessCase(user, caseNumber);
if (!accessCheck.allowed) {
  return null;
}

const modifyCheck = await canModifyCase(user, caseNumber);
if (!modifyCheck.allowed) {
  throw new Error(`Modification denied: ${modifyCheck.reason}`);
}

Limits

const limits = getUserLimits(userData);
// limits.maxCases and limits.maxFilesPerCase are Infinity for permitted users
// and config-bound values for review users

const usage = await getUserUsage(user);
// usage.currentCases

const description = await getLimitsDescription(user);
// '' for permitted users; 'Read-Only Account: Case review only.' for review users

Higher-Order Wrapper

withUserDataOperation wraps an operation with session validation and standardized error handling:

const myOp = withUserDataOperation(async (userData, user) => {
  // userData is guaranteed valid here
  return doSomethingWithUserData(userData);
});
await myOp(user);

Additional user/case list mutation helpers exported from this module:

  • addUserCase(user, caseData)
  • removeUserCase(user, caseNumber)

Notes View Permissions

getNotesViewPermission determines whether the Image Notes panel can be opened for the current image and case context, and whether it should be in read-only mode. All notes-open/close permission decisions must go through this function rather than ad-hoc flag checks in components.

const permission = getNotesViewPermission({
  imageLoaded: true,
  isUploading: false,
  isCheckingConfirmation: false,
  isReadOnlyCase: false,
  isArchivedCase: false,
  isConfirmedImage: false,
});
// permission: { canOpen: boolean; isReadOnly: boolean; reason?: string }

Rules enforced:

  • canOpen: false when isUploading is true — cannot open while upload is in progress.
  • canOpen: false when isCheckingConfirmation is true — cannot open while confirmation status is being resolved.
  • canOpen: false when imageLoaded is false — no image is selected.
  • isReadOnly: true when isConfirmedImage, isReadOnlyCase, or isArchivedCase is true — notes are viewable but not editable.

getNotesButtonTooltip derives a user-facing tooltip string from a NotesViewPermission result and optional case/image context. Returns undefined when the button is in normal edit mode (no tooltip needed).

const tooltip = getNotesButtonTooltip(permission, {
  isReadOnlyCase: false,
  isArchivedCase: false,
  isConfirmedImage: true,
});
// 'Image notes: viewing only (image is confirmed)'

NotesViewPermission interface:

interface NotesViewPermission {
  canOpen: boolean;    // Can the notes panel be opened
  isReadOnly: boolean; // Are notes in read-only mode (can view but not edit)
  reason?: string;     // Reason if notes cannot be opened
}

Data Operations

data-operations.ts

Provides all case and annotation CRUD operations against the Data Worker (Cloudflare R2). Every function validates the user session and checks permissions before making network calls.

Current implementation note:

  • app/utils/data/data-operations.ts is a compatibility surface.
  • Active implementation is split into app/utils/data/operations/* (case-operations.ts, file-annotation-operations.ts, batch-operations.ts, validation-operations.ts, signing-operations.ts, confirmation-summary-operations.ts).
  • Prefer imports from ~/utils/data for new code.
import {
  getCaseData,
  updateCaseData,
  deleteCaseData,
  getFileAnnotations,
  saveFileAnnotations,
  deleteFileAnnotations,
  batchUpdateFiles,
  duplicateCaseData,
  validateDataAccess,
  caseExists,
  fileHasAnnotations,
  signForensicManifest,
  signConfirmationData,
  signAuditExportData,
  decryptExportBatch,
  getConfirmationSummaryDocument,
  getCaseConfirmationSummary,
  ensureCaseConfirmationSummary,
  upsertFileConfirmationSummary,
  removeFileConfirmationSummary,
  removeCaseConfirmationSummary,
  moveCaseConfirmationSummary,
  withDataOperation,
} from '~/utils/data';

Key Types

interface DataOperationOptions {
  includeTimestamp?: boolean;  // default true; adds updatedAt to saved data
  retryCount?: number;
  skipValidation?: boolean;    // only use when batch-level checks already ran
}

interface FileUpdate {
  fileId: string;
  annotations: AnnotationData;
}

interface BatchUpdateResult {
  successful: string[];
  failed: { fileId: string; error: string }[];
}

Case CRUD

// Read — returns null if not found or access denied
const caseData = await getCaseData(user, caseNumber);

// Write — adds updatedAt by default
await updateCaseData(user, caseNumber, caseData);

// Delete
await deleteCaseData(user, caseNumber);

// Existence check
const exists = await caseExists(user, caseNumber); // boolean

File Annotation CRUD

// Read — returns null on 404 (not throws), so callers can handle missing annotations gracefully
const annotations = await getFileAnnotations(user, caseNumber, fileId);

// Write — blocks if existing annotations already have confirmationData (immutability rule)
await saveFileAnnotations(user, caseNumber, fileId, annotationData);

// Delete
await deleteFileAnnotations(user, caseNumber, fileId);

// Check
const hasAnnotations = await fileHasAnnotations(user, caseNumber, fileId); // boolean

saveFileAnnotations enforces annotation immutability: if an existing annotation record already has confirmationData set, the write is rejected with 'Cannot modify annotations for a confirmed image'.

Batch Updates

const result = await batchUpdateFiles(user, caseNumber, [
  { fileId: 'abc', annotations: { ... } },
  { fileId: 'def', annotations: { ... } },
]);
// result.successful: string[] of fileIds
// result.failed: { fileId, error }[]

Validates session and canModifyCase once for the batch, then calls saveFileAnnotations per file with skipValidation: true.

Case Duplication

// Used during case rename operations — copies case data and all file annotations
await duplicateCaseData(user, fromCaseNumber, toCaseNumber, { skipDestinationCheck: true });

Access Validation Utility

const result = await validateDataAccess(user, caseNumber);
// { allowed: boolean; reason?: string }

Combines validateUserSession + canAccessCase in one call.

Forensic Signing

These send data to the Data Worker's signing endpoints, which sign with the server-side private key. The returned signature is stored alongside the exported data.

// Sign a forensic manifest (image hashes + data hash)
const { manifestVersion, signature } = await signForensicManifest(user, caseNumber, manifest);

// Sign a confirmation export
const { signatureVersion, signature } = await signConfirmationData(user, caseNumber, confirmationData);

// Sign audit export metadata
const { signatureVersion, signature } = await signAuditExportData(user, auditExport, { caseNumber });

All three validate the session and access before forwarding to the worker. They throw on unexpected version mismatches between the signing response and the expected constants (FORENSIC_MANIFEST_VERSION, CONFIRMATION_SIGNATURE_VERSION, AUDIT_EXPORT_SIGNATURE_VERSION).

Export Decryption

// Decrypt an encrypted export package (data + images) via the Data Worker
const { plaintext, decryptedImages } = await decryptExportBatch(
  user,
  encryptionManifest,    // EncryptionManifest from the ZIP
  encryptedDataBase64,   // base64-encoded encrypted data payload
  encryptedImageMap      // Record<filename, base64-encoded encrypted image>
);

Validates the session, sends the wrapped AES key and encrypted payloads to the Data Worker for server-side RSA-OAEP unwrapping and AES-256-GCM decryption, and returns the plaintext JSON string plus a map of decrypted image Blobs.

Higher-Order Wrapper

const myOp = withDataOperation(async (user, ...args) => {
  // session already validated
});
await myOp(user, arg1, arg2);

Confirmation Status Summary Utility

The confirmation summary utility maintains lightweight per-file and per-case confirmation state used by file list UI indicators.

const summary = await ensureCaseConfirmationSummary(user, caseNumber, files, {
  forceRefresh: false,
  maxAgeMs: 5 * 60 * 1000,
});

Behavior highlights:

  • Stores derived metadata at /{userId}/meta/confirmation-status.json.
  • Tracks includeConfirmation and isConfirmed at file and case levels.
  • Refreshes stale or missing file entries opportunistically from annotation data.
  • Removes stale entries when files disappear from a case.
  • Supports targeted cleanup with removeFileConfirmationSummary and removeCaseConfirmationSummary for delete/archive/account lifecycle flows.
  • Supports case rename with moveCaseConfirmationSummary(user, fromCaseNumber, toCaseNumber).

Additional confirmation summary accessors:

  • getConfirmationSummaryDocument(user) — returns the full UserConfirmationSummaryDocument from R2.
  • getCaseConfirmationSummary(user, caseNumber) — returns the CaseConfirmationSummary for a single case, or null if not present.

~/utils/data also re-exports summary telemetry and shared types from app/utils/data/confirmation-summary/summary-core.ts:

import {
  getConfirmationSummaryTelemetry,
  resetConfirmationSummaryTelemetry,
  type CaseConfirmationSummary,
  type ConfirmationSummaryEnsureOptions,
  type ConfirmationSummaryTelemetry,
  type FileConfirmationSummary,
  type UserConfirmationSummaryDocument,
} from '~/utils/data';

These are useful for measuring cache hit/miss behavior around confirmation status hydration in the cases and files modals.

Modal Filter Utilities

The cases and files management modals now use dedicated filtering/sorting helpers under app/utils/data/.

case-filters.ts

import {
  filterCasesForModal,
  sortCasesForModal,
  getCasesForModal,
  type CasesModalPreferences,
  type CasesModalCaseItem,
  type CaseConfirmationStatusValue,
} from '~/utils/data/case-filters';
  • filterCasesForModal(...) filters by archive state and confirmation state.
  • sortCasesForModal(...) sorts by recent creation time or natural case-number order.
  • getCasesForModal(...) composes filtering + sorting for the cases modal.

The preferences model supports:

type CasesModalSortBy = 'recent' | 'alphabetical';
type CasesModalConfirmationFilter = 'all' | 'pending' | 'confirmed' | 'none-requested';

file-filters.ts

import {
  filterFilesForModal,
  sortFilesForModal,
  getFilesForModal,
  type FilesModalPreferences,
  type FileConfirmationById,
} from '~/utils/data/file-filters';
  • filterFilesForModal(...) filters by search query, confirmation state, and item type.
  • sortFilesForModal(...) sorts by recent upload, filename, confirmation priority, or item type.
  • getFilesForModal(...) composes filtering + sorting for the files modal.

The file modal preferences support:

type FilesModalSortBy = 'recent' | 'filename' | 'confirmation' | 'itemType';
type FilesModalConfirmationFilter = 'all' | 'pending' | 'confirmed' | 'none-requested';
type FilesModalItemTypeFilter = 'all' | 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';

Legacy compatibility aliases still exist in code (FilesModalClassTypeFilter and classTypeFilter) and are migrated to itemType-named preferences at runtime.


Forensic Signing and Hashing

SHA256.ts

Core cryptographic utilities for forensic manifest integrity. Uses the Web Crypto API (crypto.subtle).

import {
  calculateSHA256,
  calculateSHA256Secure,
  calculateSHA256Binary,
  extractForensicManifestData,
  createManifestSigningPayload,
  verifyForensicManifestSignature,
  generateForensicManifestSecure,
  generateForensicManifestWithTimestampSecure,
  validateCaseIntegritySecure,
  FORENSIC_MANIFEST_VERSION,
  FORENSIC_MANIFEST_SIGNATURE_ALGORITHM,
} from '~/utils/forensics';

Types

interface ForensicManifestData {
  dataHash: string;                           // SHA-256 of case data.json
  imageHashes: { [filename: string]: string }; // SHA-256 per image file
  manifestHash: string;                       // SHA-256 of the combined hash object
  totalFiles: number;
  createdAt: string;                          // ISO 8601
}

interface ForensicManifestSignature {
  algorithm: string;  // 'RSASSA-PSS-SHA-256'
  keyId: string;
  signedAt: string;
  value: string;      // base64url-encoded signature
}

interface ManifestSignatureVerificationResult {
  isValid: boolean;
  keyId?: string;
  error?: string;
}

interface SignedForensicManifest extends ForensicManifestData {
  manifestVersion?: string;
  signature?: ForensicManifestSignature;
}

Hash calculation

const hash = await calculateSHA256(content); // lowercase hex, 64 chars

Throws on null, undefined, or non-string input.

Additional helpers:

  • calculateSHA256Secure(content) - padded-processing variant used for high-sensitivity forensic hashing.
  • calculateSHA256Binary(data) - hashes Uint8Array, ArrayBuffer, or Blob inputs.
  • generateForensicManifestSecure(dataContent, imageFiles) - builds a full manifest from case JSON and image blobs.
  • generateForensicManifestWithTimestampSecure(dataContent, imageFiles, createdAt) - same as above but preserves a supplied timestamp.
  • validateCaseIntegritySecure(dataContent, imageFiles, expectedManifest) - validates data hash, image hashes, and manifest hash together and returns a structured summary.

Manifest signature verification

const result = await verifyForensicManifestSignature(signedManifest, verificationPublicKeyPem?);
// result.isValid: boolean
// result.error: present on failure (no hash values are exposed)

Internally, extractForensicManifestData validates every hash against /^[a-f0-9]{64}$/i, normalizes imageHashes to sorted lowercase keys, then createManifestSigningPayload serializes a canonical JSON object with a deterministic key order before signature verification.


signature-utils.ts

Low-level RSA-PSS / SHA-256 verification shared by all three signing domains (manifest, confirmation, audit export).

import {
  verifySignaturePayload,
  getVerificationPublicKey,
  getCurrentPublicSigningKeyDetails,
  createPublicSigningKeyFileName,
} from '~/utils/forensics';

Additional helpers

Function Description
getCurrentPublicSigningKeyDetails() Returns the currently configured public key ID and PEM from config
createPublicSigningKeyFileName(keyId?) Builds a safe PEM filename for public key download/export

Exported types

Type Description
SignatureEnvelope { algorithm, keyId, signedAt, value } — standardized signature wrapper
SignatureVerificationResult { isValid, keyId?, error? } — verification outcome
SignatureVerificationMessages Customizable error message strings for each failure case
SignatureVerificationOptions { verificationPublicKeyPem? } — optional PEM override for verification
PublicSigningKeyDetails { keyId, publicKeyPem } — nullable key details from config

getVerificationPublicKey(keyId)

Looks up a PEM public key from config.json. Supports both a keyed map (manifest_signing_public_keys) and a legacy single-key config (manifest_signing_public_key + manifest_signing_key_id). Returns null if no key is configured for the given keyId.

verifySignaturePayload(payload, signature, expectedAlgorithm, messages?, options?)

const result = await verifySignaturePayload(
  payload,           // canonical JSON string
  signature,         // { algorithm, keyId, value }
  'RSASSA-PSS-SHA-256',
  {
    unsupportedAlgorithmPrefix: '...',
    missingKeyOrValueError: '...',
    noVerificationKeyPrefix: '...',
    invalidPublicKeyError: '...',
    verificationFailedError: '...',
  },
  {
    verificationPublicKeyPem: '...',  // optional PEM override
  }
);

Handles PEM normalization (\n replacement), base64url decoding of the signature value, and crypto.subtle.verify. Returns { isValid, keyId, error }.


export-encryption.ts

Shared helpers for encrypting export payloads in the browser before packaging them into ZIP archives.

import {
  EXPORT_ENCRYPTION_VERSION,
  EXPORT_ENCRYPTION_ALGORITHM,
  getCurrentEncryptionPublicKeyDetails,
  encryptExportDataWithAllImages,
  type EncryptionManifest,
} from '~/utils/forensics';

Key constants

  • EXPORT_ENCRYPTION_VERSION = '1.0'
  • EXPORT_ENCRYPTION_ALGORITHM = 'RSA-OAEP-AES-256-GCM'

getCurrentEncryptionPublicKeyDetails()

Resolves the active export encryption public key from config.json. Supports both:

  • export_encryption_public_keys keyed by keyId
  • export_encryption_public_key + export_encryption_key_id fallback

Returns:

interface PublicEncryptionKeyDetails {
  keyId: string | null;
  publicKeyPem: string | null;
}

encryptExportDataWithAllImages(plaintextString, imageBlobs, publicKeyPem, keyId)

Encrypts one data payload plus zero or more related blobs using:

  1. one generated AES-256-GCM key for the package
  2. one generated IV stored in the manifest
  3. RSA-OAEP wrapping of the AES key with the configured public key

Returns:

interface EncryptedImageEntry {
  filename: string;
  encryptedHash: string;  // SHA-256 of encrypted bytes (lowercase hex)
  iv: string;             // base64url — per-image nonce
}

interface EncryptionManifest {
  encryptionVersion: string;
  algorithm: string;
  keyId: string;
  wrappedKey: string;          // base64url
  dataIv: string;              // base64url — nonce for the data file
  encryptedImages: EncryptedImageEntry[];
}

interface EncryptedExportResult {
  ciphertext: Uint8Array;
  encryptedImages: Uint8Array[];
  encryptionManifest: EncryptionManifest;
}

encryptedHash is the SHA-256 hash of the encrypted bytes written into the ZIP for that file. Each image gets its own IV (iv), while the data payload uses dataIv.

This module is used by case export, confirmation export, and case archive packaging. Import-time decryption is handled through decryptExportBatch() in signing-operations.ts, which forwards encrypted payloads to the Data Worker.

Additional low-level helpers exported for advanced use:

  • base64UrlDecode(value) — decodes a base64url string to Uint8Array.
  • generateSharedAesKey() — generates a random AES-256-GCM CryptoKey.
  • encryptDataWithSharedKey(plaintextString, sharedAesKey, iv) — encrypts a string with AES-256-GCM.
  • encryptImageWithSharedKey(imageBlob, sharedAesKey, iv) — encrypts an image blob; returns { ciphertext, hash }.
  • wrapAesKeyWithPublicKey(aesKey, publicKeyPem) — RSA-OAEP wraps an AES key; returns base64url string.

confirmation-signature.ts

Signing payload construction and verification for confirmation export files.

import {
  createConfirmationSigningPayload,
  verifyConfirmationSignature,
  CONFIRMATION_SIGNATURE_VERSION,
} from '~/utils/forensics';

createConfirmationSigningPayload(confirmationData, signatureVersion?)

Produces the canonical JSON string used for both signing (server-side) and verification (client-side). Key ordering is deterministic; confirmation entries are sorted by confirmationId|confirmedAt|confirmedBy; image IDs are sorted alphabetically. Hashes are uppercased in the payload.

verifyConfirmationSignature(confirmationData, verificationPublicKeyPem?)

Validates the shape of confirmationData before verification: checks all metadata fields including the SHA-256 hash regex, validates each confirmation entry structure, then calls verifySignaturePayload. Returns ManifestSignatureVerificationResult. An optional verificationPublicKeyPem can be provided to override the configured public key.

Current version constant: CONFIRMATION_SIGNATURE_VERSION = '2.0'.


export-verification.ts

High-level verification utility for signed export artifacts. It wraps confirmation and case export checks behind a single entrypoint so workflow code does not duplicate verification branching logic.

import {
  verifyCasePackageIntegrity,
  validateConfirmationHash,
  removeForensicWarning,
  type ExportVerificationResult,
  type CasePackageIntegrityInput,
  type CasePackageIntegrityResult,
} from '~/utils/forensics/export-verification';

ExportVerificationResult

interface ExportVerificationResult {
  isValid: boolean;
  message: string;
  exportType?: 'case-zip' | 'confirmation' | 'audit-json';
}

CasePackageIntegrityInput

interface CasePackageIntegrityInput {
  cleanedContent: string;
  imageFiles: Record<string, Blob>;
  forensicManifest: SignedForensicManifest;
  verificationPublicKeyPem?: string;
  bundledAuditFiles?: {
    auditTrailContent?: string;
    auditSignatureContent?: string;
  };
}

CasePackageIntegrityResult

interface CasePackageIntegrityResult {
  isValid: boolean;
  signatureResult: ManifestSignatureVerificationResult;
  integrityResult: { isValid, dataValid, imageValidation, manifestValid, errors, summary };
  bundledAuditVerification: ExportVerificationResult | null;
}

verifyCasePackageIntegrity(input)

Validates the full case export package:

  • Extracts and validates the forensic manifest structure.
  • Verifies the manifest signature using the provided or configured public key.
  • Validates data and image integrity via validateCaseIntegritySecure.
  • Optionally verifies bundled audit export files (audit/case-audit-trail.json and audit/case-audit-signature.json) when present in the input.

validateConfirmationHash(jsonContent, expectedHash)

  • Recomputes the confirmation hash after removing signature metadata fields.
  • Returns boolean only; expected/actual hash values are intentionally not exposed.

removeForensicWarning(content)

  • Removes forensic warning wrappers from JSON/CSV case export content before hash/integrity validation.
  • Handles the currently supported warning formats for exported files.

The standalone public-key verification modal has been removed from the current UI. This utility remains the shared verification implementation for package-validation workflows and delegates cryptographic checks to SHA256.ts, confirmation-signature.ts, and audit-export-signature.ts.


audit-export-signature.ts

Signing payload construction and verification for audit export files.

import {
  createAuditExportSigningPayload,
  verifyAuditExportSignature,
  isValidAuditExportSigningPayload,
  AUDIT_EXPORT_SIGNATURE_VERSION,
} from '~/utils/forensics';

AuditExportSigningPayload

interface AuditExportSigningPayload {
  signatureVersion: string;
  exportFormat: 'csv' | 'json' | 'txt';
  exportType: 'entries' | 'trail' | 'report';
  scopeType: 'case' | 'user';
  scopeIdentifier: string;
  generatedAt: string;
  totalEntries: number;
  hash: string;  // SHA-256 hex of the export content
}

isValidAuditExportSigningPayload(payload)

Type guard that validates all fields, including format/type/scope enums and the SHA-256 hash regex. Used before signing and before verification.

createAuditExportSigningPayload(payload)

Produces a canonical JSON string with a fixed key order. The hash field is uppercased in the payload.

verifyAuditExportSignature(payload, signature?, verificationPublicKeyPem?)

Returns ManifestSignatureVerificationResult. Validates the payload shape with isValidAuditExportSigningPayload before calling verifySignaturePayload. Optional signature and verificationPublicKeyPem parameters allow overriding the embedded signature and configured public key respectively.

Current version constant: AUDIT_EXPORT_SIGNATURE_VERSION = '1.0'.


Annotation Utilities

annotation-timestamp.ts

import { resolveEarliestAnnotationTimestamp } from '~/utils/ui';

This helper is also re-exported from ~/utils/ui.

resolveEarliestAnnotationTimestamp(incoming?, existing?, fallback?)

Returns the earliest valid ISO timestamp from the provided candidates. Used to preserve the original annotation timestamp when re-saving annotation data, ensuring that re-uploads do not advance the creation time.

const timestamp = resolveEarliestAnnotationTimestamp(
  incomingTimestamp,   // from the incoming payload
  existingTimestamp,   // from the stored record
  new Date().toISOString()  // fallback (default)
);

Ignores NaN dates and returns the fallback if no valid candidates are found.


ID Generation

id-generator.ts

import {
  generateUniqueId,
  generateConfirmationId,
  generateWorkflowId,
} from '~/utils/common';
Function Output format Description
generateUniqueId(length?) 'A3XK...' (default 10 chars) Uppercase alphanumeric string
generateConfirmationId() 'CONF-XXXXXXXXXX' Prefixed ID for confirmation records
generateWorkflowId(caseNumber) 'case-<base36ts>-<random8>' Audit workflow identifier

IDs are generated using Math.random over an uppercase alphanumeric charset (A–Z, 0–9).


Batch Operations

batch-operations.ts

Generic utility for processing lists of items in bounded batches with exponential backoff retry.

import { executeBatchOperations, batchedAuditLog } from '~/utils/common';
const result = await executeBatchOperations(
  items,
  async (item) => {
    return await processItem(item);
  },
  {
    batchSize: 3,       // items per batch (default: 3)
    baseDelay: 300,     // ms between batches and base for backoff (default: 300)
    maxRetries: 2,      // retries per item (default: 2)
    retryMultiplier: 2, // exponential backoff multiplier (default: 2)
  }
);

result.successful; // Array<{ item, result }>
result.failed;     // Array<{ item, error, retryCount }>
result.totalProcessed; // number

Processes batches sequentially. After each batch, waits baseDelay ms (plus an extra baseDelay if any failures occurred). Within each batch, items are processed in parallel via Promise.allSettled.

batchedAuditLog(auditEntries, options?)

Rate-limited helper specifically for audit writes:

const result = await batchedAuditLog([
  () => auditService.logEvent(...),
  () => auditService.logEvent(...),
], {
  batchSize: 2,
  delay: 500,
});

// result: { successful: number; failed: number }

It executes audit callbacks in small batches to avoid overwhelming the audit service during bulk workflows.


Auth Flow Helpers

auth-action-settings.ts

Utilities for constructing Firebase ActionCodeSettings and validating continue URLs.

import {
  buildActionCodeSettings,
  getSafeContinuePath,
  getAuthActionRoutePath,
} from '~/utils/auth';

buildActionCodeSettings(continuePath?)

Returns a Firebase ActionCodeSettings object with a url built from config.json's url field plus the given path. Invalid or missing paths default to '/'. Paths must start with / and must not start with //.

getSafeContinuePath(continueUrl)

Parses a continueUrl and validates that its origin matches the configured app origin. Returns only pathname + search + hash if safe, or '/' if the origin doesn't match or parsing fails. Guards against open-redirect vectors.

const safePath = getSafeContinuePath(searchParams.get('continueUrl'));

getAuthActionRoutePath()

Returns the static auth action route path ('/').


password-policy.ts

import { evaluatePasswordPolicy } from '~/utils/auth';

evaluatePasswordPolicy(password, confirmPassword?)

const result = evaluatePasswordPolicy(password, confirmPassword);
// result.hasMinLength   — >= 10 characters
// result.hasUpperCase   — at least one A–Z
// result.hasNumber      — at least one 0–9
// result.hasSpecialChar — at least one of !@#$%^&*(),.?":{}|<>
// result.passwordsMatch — true if confirmPassword is omitted
// result.isStrong       — all of the above are true

Returns PasswordPolicyResult. passwordsMatch is always true when confirmPassword is not provided.


mfa.ts

Firebase MFA enrollment inspection utilities.

import {
  userHasMFA,
  getMFAFactorCount,
  getMFAFactors,
  getTotpFactors,
  hasTotpEnrolled,
  getPhoneMfaFactors,
  getMfaMethodLabel,
} from '~/utils/auth';
Function Returns Description
userHasMFA(user) boolean true if any MFA factor is enrolled
getMFAFactorCount(user) number Count of enrolled factors
getMFAFactors(user) Array<{ uid, factorId, displayName, enrollmentTime }> Enrolled factor details
getTotpFactors(user) MultiFactorInfo[] Enrolled TOTP (Authenticator App) factors only
hasTotpEnrolled(user) boolean true if any TOTP factor is enrolled
getPhoneMfaFactors(user) MultiFactorInfo[] Enrolled Phone/SMS factors only
getMfaMethodLabel(factorId) string Human-readable label: 'Authenticator App', 'Phone (SMS)', or 'Unknown Method'

These are synchronous wrappers over multiFactor(user).enrolledFactors.


mfa-phone.ts

Phone number formatting and display utilities for the MFA enrollment and sign-in flows.

import {
  formatPhoneNumberForMfa,
  maskPhoneNumber,
  validatePhoneNumber,
  getPhoneDisplayValue,
  getMaskedFactorDisplay,
} from '~/utils/auth';

formatPhoneNumberForMfa(phone)

Normalizes a phone string to E.164 format (+1xxxxxxxxxx). If already E.164, strips non-digits and reattaches the +. For US numbers starting with 1 and 11 digits, formats directly. Otherwise prepends +1.

maskPhoneNumber(phone)

Returns ***-***-XXXX showing only the last 4 digits.

validatePhoneNumber(phone)

Returns { isValid: boolean; errorMessage?: string }. Rejects empty strings and known test numbers (5551234567, 15551234567).

getPhoneDisplayValue(factor)

Extracts the raw phone number from a MultiFactorInfo display name that uses the 'phone: ...' prefix convention.

getMaskedFactorDisplay(factor)

Returns a masked display string from an enrolled MultiFactorInfo, or 'your enrolled phone' if no display value is present.


Style and Layout Utilities

style.ts

Helpers for CSS value conversions and class name composition.

import {
  media,
  pxToNum, numToPx,
  pxToRem,
  msToNum, numToMs,
  rgbToThreeColor,
  cssProps,
  classes,
} from '~/utils/ui';

Breakpoints

media = {
  desktop: 2560,  // 4K/2K
  laptop:  1920,  // FHD
  tablet:  1024,  // iPad Pro
  mobile:   768,  // Landscape phones
  mobileS:  480,  // Portrait phones
}

Value Converters

Function Example
pxToNum('16px')16 Parse px string to number
numToPx(16)'16px' Number to px string
pxToRem(16)'1rem' px to rem (divides by 16)
msToNum('300ms')300 Parse ms string to number
numToMs(300)'300ms' Number to ms string
rgbToThreeColor('255 128 0')[1, 0.5, 0] RGB space-string to Three.js Color inputs

cssProps(props, style?)

Converts a JS object into CSS custom properties (-- prefixed). Numbers are converted to px, except keys named delay (converted to ms) and opacity (converted to %):

cssProps({ width: 200, delay: 300, opacity: 0.5 })
// { '--width': '200px', '--delay': '300ms', '--opacity': '50%' }

An optional second style object is merged into the result as plain (non-prefixed) CSS properties.

classes(...classNames)

Filters falsy values and joins truthy class name strings:

classes('button', isActive && 'active', undefined)
// 'button active'

Case Messages

case-messages.ts

Centralized user-facing message strings for case import, export, and management operations. Prevents message drift across components and ensures consistent UX copy.

Archive import UX follow-up behavior uses this module to keep preview/import alerts consolidated (for example archived regular-case conflicts and self-import warnings), so archive messaging stays aligned across modal surfaces.

import {
  IMPORT_FILE_TYPE_NOT_ALLOWED,
  IMPORT_FILE_TYPE_NOT_SUPPORTED,
  ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE,
  ARCHIVED_SELF_IMPORT_NOTE,
  DATA_INTEGRITY_VALIDATION_PASSED,
  DATA_INTEGRITY_VALIDATION_FAILED,
  DATA_INTEGRITY_BLOCKED_TAMPERING,
  CONFIRM_CASE_IMPORT,
  EXPORT_FAILED,
  EXPORT_ALL_FAILED,
  ENTER_CASE_NUMBER_REQUIRED,
  DELETE_CASE_CONFIRMATION,
  DELETE_FILE_CONFIRMATION,
  DELETE_CASE_FAILED,
  DELETE_FILE_FAILED,
  RENAME_CASE_FAILED,
  CREATE_READ_ONLY_CASE_EXISTS_ERROR,
  CLEAR_READ_ONLY_CASE_SUCCESS,
  CLEAR_READ_ONLY_CASE_PARTIAL_FAILURE,
  CLEAR_READ_ONLY_CASE_GENERIC_ERROR,
  NO_READ_ONLY_CASE_LOADED,
  CANNOT_DELETE_READ_ONLY_CASE_FILES,
  READ_ONLY_CASE_CANNOT_ARCHIVE_AGAIN,
} from '~/utils/ui';

Most constants are plain strings. A few are factory functions that accept a case number or file name:

Constant Type Description
IMPORT_FILE_TYPE_NOT_ALLOWED string Rejects non-ZIP import attempts
IMPORT_FILE_TYPE_NOT_SUPPORTED string Rejects unsupported ZIP formats
ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE string Blocks archived import when regular case exists
ARCHIVED_SELF_IMPORT_NOTE string Warns original exporter about self-import rules
DATA_INTEGRITY_VALIDATION_PASSED string Integrity check success
DATA_INTEGRITY_VALIDATION_FAILED string Integrity check failure
DATA_INTEGRITY_BLOCKED_TAMPERING string Import blocked due to hash mismatch
CONFIRM_CASE_IMPORT string Confirmation prompt for review import
EXPORT_FAILED string Generic export error
EXPORT_ALL_FAILED string Bulk export error
ENTER_CASE_NUMBER_REQUIRED string Missing case number validation
DELETE_CASE_CONFIRMATION(caseNumber) (string) => string Case deletion prompt
DELETE_FILE_CONFIRMATION(fileName) (string) => string File deletion prompt
DELETE_CASE_FAILED string Case deletion error
DELETE_FILE_FAILED string File deletion error
RENAME_CASE_FAILED string Case rename error
CREATE_READ_ONLY_CASE_EXISTS_ERROR(caseNumber) (string) => string Read-only case already exists
CLEAR_READ_ONLY_CASE_SUCCESS(caseNumber) (string) => string Read-only case removal success
CLEAR_READ_ONLY_CASE_PARTIAL_FAILURE(caseNumber) (string) => string Partial failure clearing read-only case
CLEAR_READ_ONLY_CASE_GENERIC_ERROR string Generic read-only case clear error
NO_READ_ONLY_CASE_LOADED string No read-only case is loaded
CANNOT_DELETE_READ_ONLY_CASE_FILES string Blocks file deletion for read-only cases
READ_ONLY_CASE_CANNOT_ARCHIVE_AGAIN string Blocks re-archiving read-only cases

Version Utility

version.ts

import { getAppVersion } from '~/utils/common';

const version = getAppVersion(); // e.g. '0.9.10'

Reads version directly from package.json at build time. Used in PDF report headers and version display UI.


References

⚠️ **GitHub.com Fallback** ⚠️