Utilities Guide - striae-org/striae GitHub Wiki
- Overview
- Utility Organization
- API Client Transport
- Permissions and User Data
- Data Operations
- Forensic Signing and Hashing
- Annotation Utilities
- ID Generation
- Batch Operations
- Auth Flow Helpers
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.
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.
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.
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.
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';const sessionValidation = await validateUserSession(user);
if (!sessionValidation.valid) {
throw new Error(`Session validation failed: ${sessionValidation.reason}`);
}validateUserSession checks that:
-
user.uidanduser.emailare present. - The user record exists in KV.
- The KV email matches the Firebase session email.
Returns UserSessionValidation: { valid: boolean; reason?: string }.
// 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' });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}`);
}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 userswithUserDataOperation 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)
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: falsewhenisUploadingis true — cannot open while upload is in progress. -
canOpen: falsewhenisCheckingConfirmationis true — cannot open while confirmation status is being resolved. -
canOpen: falsewhenimageLoadedis false — no image is selected. -
isReadOnly: truewhenisConfirmedImage,isReadOnlyCase, orisArchivedCaseis 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
}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.tsis 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/datafor 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';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 }[];
}// 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// 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); // booleansaveFileAnnotations enforces annotation immutability: if an existing annotation record already has confirmationData set, the write is rejected with 'Cannot modify annotations for a confirmed image'.
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.
// Used during case rename operations — copies case data and all file annotations
await duplicateCaseData(user, fromCaseNumber, toCaseNumber, { skipDestinationCheck: true });const result = await validateDataAccess(user, caseNumber);
// { allowed: boolean; reason?: string }Combines validateUserSession + canAccessCase in one call.
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).
// 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.
const myOp = withDataOperation(async (user, ...args) => {
// session already validated
});
await myOp(user, arg1, arg2);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
includeConfirmationandisConfirmedat 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
removeFileConfirmationSummaryandremoveCaseConfirmationSummaryfor delete/archive/account lifecycle flows. - Supports case rename with
moveCaseConfirmationSummary(user, fromCaseNumber, toCaseNumber).
Additional confirmation summary accessors:
-
getConfirmationSummaryDocument(user)— returns the fullUserConfirmationSummaryDocumentfrom R2. -
getCaseConfirmationSummary(user, caseNumber)— returns theCaseConfirmationSummaryfor a single case, ornullif 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.
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.
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 charsThrows on null, undefined, or non-string input.
Additional helpers:
-
calculateSHA256Secure(content)- padded-processing variant used for high-sensitivity forensic hashing. -
calculateSHA256Binary(data)- hashesUint8Array,ArrayBuffer, orBlobinputs. -
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.
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 }.
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_keyskeyed bykeyId -
export_encryption_public_key+export_encryption_key_idfallback
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:
- one generated AES-256-GCM key for the package
- one generated IV stored in the manifest
- 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 toUint8Array. -
generateSharedAesKey()— generates a random AES-256-GCMCryptoKey. -
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.
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'.
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.jsonandaudit/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.
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'.
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.
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).
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; // numberProcesses 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.
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 ('/').
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 trueReturns PasswordPolicyResult. passwordsMatch is always true when confirmPassword is not provided.
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.
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.
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'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 |
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.
-
app/utils/— all source files described in this guide - Architecture Guide — service boundaries and worker communication patterns
- Security Guide — auth boundaries and access control policies
- Manifest and Confirmation Signing — signing model detail and verification behavior
- Export Encryption — encrypted package structure, manifest fields, and import-time decryption
- Audit Trail System — audit export and signing workflow
- Installation Guide
- Environment Variables Setup