API Reference - striae-org/striae GitHub Wiki

Table of Contents

  1. Overview
  2. Authentication
  3. User Worker API
  4. Image Worker API
  1. PDF Worker API
  2. Data Worker API
  3. Audit Worker API
  1. Error Handling
  2. Type Definitions
  3. SDK Examples

Overview

Striae uses Cloudflare Pages Functions as the public API surface, which proxies to five specialized Cloudflare Workers. Public API calls use same-origin /api/* routes and return JSON unless otherwise noted by endpoint.

Current worker implementations are modularized under each worker's src/ tree with focused handler, storage, registry, and format modules where applicable. The public HTTP surface remains the same even though the internals were refactored for maintainability.

Authentication

Authentication Methods

Striae uses a two-hop authentication model:

  • Browser/client -> Pages API: Authorization: Bearer {firebase_id_token}
  • Pages API -> Worker upstream: via Cloudflare Service Bindings (no shared-secret auth headers)

Current route-to-worker binding mapping:

  • /api/user/* -> User Worker via env.USER_WORKER service binding
  • /api/data/* -> Data Worker via env.DATA_WORKER service binding
  • /api/audit/* -> Audit Worker via env.AUDIT_WORKER service binding
  • /api/image/* -> Image Worker via env.IMAGE_WORKER service binding; Pages enforces Firebase bearer auth for browser requests; GET requests carrying a ?st= query parameter bypass Firebase verification at the Pages layer and are forwarded directly, with st validated by the Image Worker
  • /api/pdf/* -> PDF Worker via env.PDF_WORKER service binding; Pages proxy resolves reportFormat server-side using verified Firebase email and the primershear list from the lists-worker (STRIAE_LISTS KV, key "primershear"), then worker calls Cloudflare Browser Rendering /accounts/{ACCOUNT_ID}/browser-rendering/pdf with Authorization: Bearer {BROWSER_API_TOKEN}

There is no public or internal Keys Worker API in the current architecture. Browser-visible key material is limited to public signing and export-encryption keys delivered through runtime config; private keys remain in Pages and worker secrets.

Authentication Environment Variables:

  • User Worker: Uses PROJECT_ID, FIREBASE_SERVICE_ACCOUNT_EMAIL, FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY, required user-KV encryption keys (USER_KV_ENCRYPTION_PRIVATE_KEY, USER_KV_ENCRYPTION_PUBLIC_KEY, USER_KV_ENCRYPTION_KEY_ID) plus optional registry-extension vars (USER_KV_ENCRYPTION_KEYS_JSON, USER_KV_ENCRYPTION_ACTIVE_KEY_ID) for rotation; has direct STRIAE_DATA and STRIAE_FILES R2 bucket bindings for account deletion cleanup only
  • Data Worker: Uses required signing/export-encryption keys plus optional registry-extension vars for rotation, required data-at-rest encryption keys plus optional registry-extension vars for rotation, and STRIAE_DATA bucket binding
  • Audit Worker: Uses required data-at-rest encryption keys plus optional registry-extension vars for rotation, and STRIAE_AUDIT bucket binding
  • Image Worker: Uses required data-at-rest encryption keys (DATA_AT_REST_ENCRYPTION_PRIVATE_KEY, DATA_AT_REST_ENCRYPTION_PUBLIC_KEY, DATA_AT_REST_ENCRYPTION_KEY_ID) plus optional registry-extension vars (DATA_AT_REST_ENCRYPTION_KEYS_JSON, DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID) for rotation, required IMAGE_SIGNED_URL_SECRET, and STRIAE_FILES bucket binding
  • PDF Worker: Uses ACCOUNT_ID and BROWSER_API_TOKEN environment variables
  • Lists Worker: Uses LISTS_ADMIN_SECRET to guard write endpoints; owns STRIAE_LISTS KV namespace (keys "allow" and "primershear")

For full data-at-rest and user-KV encryption implementation details, key registry patterns, and rotation behavior, see Data-at-Rest Encryption.

Pages Function PDF proxy (functions/api/pdf/[[path]].ts) resolves report format server-side using the primershear list fetched from the lists-worker (env.LISTS_WORKER service binding, GET /primershear).

Pages Function registration gateway (functions/api/auth/can-register.ts) gates new account creation using the allow list fetched from the lists-worker (env.LISTS_WORKER service binding, GET /members). See Registration Gateway API.

Registration Gateway API

Route: GET /api/auth/can-register

Authentication: None — this endpoint is intentionally unauthenticated (called before Firebase account creation).

Implementation: functions/api/auth/can-register.ts

Purpose: Called by the registration form before attempting createUserWithEmailAndPassword. Returns whether the supplied email is permitted to register based on the allow list stored in the lists-worker KV (STRIAE_LISTS namespace, key "allow"). If the list is empty, all registrations are allowed (fail-open, backward-compatible).

Query Parameters:

Parameter Type Required Description
email string Yes The email address to check

Responses:

Status Body Meaning
200 { "allowed": true } Email is on the allowlist (or no allowlist is configured)
400 Missing required parameter: email email query parameter absent or empty
403 { "allowed": false } Email is not on the allowlist
405 Method not allowed Non-GET/OPTIONS request

Allowlist format (from lists-worker KV key "allow"):

  • Comma-separated string of entries, managed via the lists-worker HTTP API (POST /members, DELETE /members) or directly with wrangler kv key put --remote.
  • Each entry is either an exact email ([email protected]) or a domain wildcard (@example.com, which matches any email from that domain).
  • Matching is case-insensitive.

Defense-in-depth: In addition to the client-side check, the user Pages proxy (functions/api/user/[[path]].ts) enforces the same allowlist for PUT requests where the user record does not yet exist (i.e., the initial registration PUT). Existing user profile updates (where the record already exists) are always allowed through. Both layers fail open when the lists-worker is unreachable.

Shared helper: functions/api/_shared/registration-allowlist.ts exports isEmailAllowed(email, registrationEmails): boolean used by both the can-register endpoint and the user proxy.

User Worker API

Public Base URL: /api/user

Service Binding: env.USER_WORKER

Authentication: browser calls require Firebase bearer auth; Pages proxy routes to the worker via service binding.

Environment Variables Required:

  • PROJECT_ID - Firebase project ID for admin account deletion
  • FIREBASE_SERVICE_ACCOUNT_EMAIL - Service account client email for admin deletion token exchange
  • FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY - PKCS#8 private key for signing admin deletion JWT assertions
  • USER_KV_ENCRYPTION_PRIVATE_KEY - PKCS#8 private key used to decrypt user-profile KV envelopes
  • USER_KV_ENCRYPTION_PUBLIC_KEY - SPKI public key used to encrypt user-profile KV envelopes
  • USER_KV_ENCRYPTION_KEY_ID - Key identifier required by encrypted user-profile KV records

User profile records are stored in USER_DB as encrypted envelopes (RSA-OAEP-AES-256-GCM). Account deletion cleanup accesses STRIAE_DATA and STRIAE_FILES via direct R2 bucket bindings. The worker fails closed if encryption configuration is missing, if key ids do not match, or if a stored record is plaintext/unencrypted. See Data-at-Rest Encryption — User Worker for envelope structure and key registry details.

Endpoints

Get User Data

GET /{userUid}

Description: Retrieve user profile data from KV storage

Parameters:

  • userUid (path): Firebase user UID

Response:

{
  "uid": "string",
  "email": "string",
  "firstName": "string",
  "lastName": "string",
  "company": "string",
  "badgeId": "string",
  "permitted": boolean,
  "cases": [
    {
      "caseNumber": "string",
      "createdAt": "ISO 8601 date"
    }
  ],
  "readOnlyCases": [
    {
      "caseNumber": "string",
      "importedAt": "ISO 8601 date",
      "originalExportDate": "ISO 8601 date",
      "originalExportedBy": "string",
      "isReadOnly": true
    }
  ],
  "createdAt": "ISO 8601 date",
  "updatedAt": "ISO 8601 date"
}

readOnlyCases is present only when the user has imported read-only case packages.

Status Codes:

  • 200: Success
  • 404: User not found
  • 403: Forbidden (invalid auth header)
  • 500: Server error

Create/Update User

PUT /{userUid}

Description: Create new user or update existing user data. Preserves existing cases when updating.

Parameters:

  • userUid (path): Firebase user UID

Request Body:

{
  "email": "string",
  "firstName": "string", 
  "lastName": "string",
  "company": "string",
  "badgeId": "string",
  "permitted": boolean,
  "readOnlyCases": []
}

readOnlyCases is optional. When provided, replaces the user's current read-only case list. badgeId is trimmed on write.

Response: User object (same as GET response)

Status Codes:

  • 200: User updated
  • 201: User created
  • 403: Forbidden
  • 500: Server error

Delete User

DELETE /{userUid}

Description: Delete user and all associated data including cases, files, and images.

Streaming Option: Supports server-sent event progress when either:

  • stream=true is provided as a query parameter
  • Accept: text/event-stream is sent

Parameters:

  • userUid (path): Firebase user UID

Process:

  1. Retrieves user data
  2. Deletes all user's cases and associated files from Data Worker
  3. Deletes all associated images from Image Worker
  4. Deletes user's Firebase Authentication account
  5. Deletes user record from KV storage

Security Notes:

  • Demo accounts (permitted: false) should be protected from deletion at the UI level
  • Deletion includes all user data, cases, files, and annotations
  • Action is irreversible and permanent

Response:

{
  "success": true,
  "message": "Account successfully deleted"
}

Error Response:

{
  "success": false,
  "message": "Account deletion failed"
}

Status Codes:

  • 200: Success
  • 404: User not found
  • 403: Forbidden
  • 500: Server error

Add Cases to User

PUT /{userUid}/cases

Description: Add case assignments to user. Filters out duplicate case numbers.

Parameters:

  • userUid (path): Firebase user UID

Request Body:

{
  "cases": [
    {
      "caseNumber": "string"
    }
  ]
}

Response: Updated user object

Status Codes:

  • 200: Success
  • 404: User not found
  • 403: Forbidden
  • 500: Server error

Remove Cases from User

DELETE /{userUid}/cases

Description: Remove case assignments from user. Does not delete the actual case data.

Parameters:

  • userUid (path): Firebase user UID

Request Body:

{
  "casesToDelete": ["caseNumber1", "caseNumber2"]
}

Response: Updated user object

Status Codes:

  • 200: Success
  • 404: User not found
  • 403: Forbidden
  • 500: Server error

Image Worker API

Public Base URL: /api/image

Service Binding: env.IMAGE_WORKER

Authentication: browser calls require Firebase bearer auth for most requests. Exception: GET requests carrying a ?st= query parameter bypass Firebase identity verification at the Pages layer and are forwarded directly as signed token requests. Signed st tokens are validated by the Image Worker against IMAGE_SIGNED_URL_SECRET.

Signed URL notes:

  • Public callers mint same-origin signed URLs via POST /{fileId}/signed-url.
  • The worker signs and verifies st tokens with IMAGE_SIGNED_URL_SECRET.
  • Default signed URL TTL is 3600 seconds, capped at 86400 seconds.

Endpoints

Upload Image

POST /

Description: Upload image/file bytes to the Image Worker, which encrypts the payload and stores it in the STRIAE_FILES R2 bucket.

Request: Multipart form data with image file

Response:

{
  "result": {
    "id": "string",
    "filename": "string",
    "uploaded": "ISO 8601 date"
  },
  "success": boolean,
  "errors": [],
  "messages": []
}

Status Codes:

  • 200: Success
  • 403: Forbidden (invalid token)
  • 400: Invalid file or missing file
  • 500: Server error

Mint Signed Image URL

POST /{fileId}/signed-url

Description: Create a signed same-origin image URL that can later be fetched with GET /{fileId}?st={token}. Pages still requires a Firebase bearer token; the st token is validated by the Image Worker as an additional signed-read control.

Parameters:

  • fileId (path): Stored file/image ID

Request Body:

{
  "expiresInSeconds": 3600
}

expiresInSeconds is optional. When omitted, the worker uses its default TTL.

Response:

{
  "success": true,
  "result": {
    "fileId": "string",
    "url": "https://app.example.com/api/image/{fileId}?st=...",
    "expiresAt": "ISO 8601 date",
    "expiresInSeconds": 3600
  }
}

Status Codes:

  • 200: Success
  • 403: Forbidden (invalid token)
  • 404: File not found
  • 500: Signed URL configuration or worker error

Get File/Image Bytes

GET /{fileId}

Description: Retrieve and decrypt stored file/image bytes. Calls must be Firebase-authenticated at the Pages layer; st may also be supplied for signed-read validation in the Image Worker.

Parameters:

  • fileId (path): Stored file/image ID

  • st (query, optional): Opaque signed access token minted by POST /{fileId}/signed-url

Response: Binary response body with original content type and inline Content-Disposition. Cache-Control: no-store is set on all image responses.

Status Codes:

  • 200: Success
  • 400: Invalid image path encoding
  • 403: Forbidden (invalid bearer token or invalid/expired signed URL token)
  • 404: File not found
  • 500: Server error

Delete File/Image

DELETE /{imageId}

Description: Delete stored file/image object from encrypted files bucket

Parameters:

  • imageId (path): Stored file/image ID

Response:

{
  "success": true
}

Status Codes:

  • 200: Success
  • 400: Missing image ID
  • 403: Forbidden (invalid token)
  • 404: Image not found
  • 500: Server error

PDF Worker API

Public Base URL: /api/pdf

Service Binding: env.PDF_WORKER

Authentication: browser calls require Firebase bearer auth; Pages proxy routes to the worker via service binding. The PDF worker then calls Cloudflare Browser Rendering REST /accounts/{ACCOUNT_ID}/browser-rendering/pdf with Authorization: Bearer {BROWSER_API_TOKEN}.

Endpoints

Generate PDF

POST /

Description: Generate PDF report with annotations

Modular Report Formats: For architecture details and custom format module implementation, see PDF Report System.

Request Body (public browser callers via /api/pdf/*):

{
  "data": {
    "reportMode": "audit-trail (optional)",
    "imageUrl": "string",
    "caseNumber": "string",
    "annotationData": {
      "leftCase": "string",
      "rightCase": "string",
      "leftItem": "string",
      "rightItem": "string",
      "caseFontColor": "string",
      "leftItemType": "Bullet | Cartridge Case | Shotshell | Other",
      "leftCustomClass": "string",
      "leftClassNote": "string",
      "rightItemType": "Bullet | Cartridge Case | Shotshell | Other",
      "rightCustomClass": "string",
      "rightClassNote": "string",
      "indexType": "number | color",
      "indexNumber": "string",
      "indexColor": "string",
      "supportLevel": "ID | Exclusion | Inconclusive",
      "leftBulletData": {
        "caliber": "string",
        "mass": "string",
        "lgNumber": "number",
        "lgDirection": "string",
        "barrelType": "string",
        "jacketMetal": "string",
        "coreMetal": "string",
        "bulletType": "string"
      },
      "leftCartridgeCaseData": {
        "caliber": "string",
        "brand": "string",
        "metal": "string",
        "primerType": "string",
        "fpiShape": "string",
        "hasFpDrag": "boolean",
        "hasExtractorMarks": "boolean"
      },
      "leftShotshellData": {
        "gauge": "string",
        "shotSize": "string",
        "metal": "string",
        "brand": "string",
        "fpiShape": "string"
      },
      "rightBulletData": "BulletAnnotationData (optional)",
      "rightCartridgeCaseData": "CartridgeCaseAnnotationData (optional)",
      "rightShotshellData": "ShotshellAnnotationData (optional)",
      "leftHasSubclass": boolean,
      "rightHasSubclass": boolean,
      "includeConfirmation": boolean,
      "leftAdditionalNotes": "string",
      "rightAdditionalNotes": "string",
      "additionalNotes": "string",
      "boxAnnotations": [
        {
          "id": "string",
          "x": "number (percentage 0-100)",
          "y": "number (percentage 0-100)",
          "width": "number (percentage 0-100)",
          "height": "number (percentage 0-100)",
          "color": "string (hex color)"
        }
      ]
    },
    "activeAnnotations": ["string"],
    "auditTrailReport": {
      "caseNumber": "string",
      "exportedAt": "ISO timestamp",
      "exportRangeStart": "ISO timestamp",
      "exportRangeEnd": "ISO timestamp",
      "chunkIndex": "number",
      "totalChunks": "number",
      "totalEntries": "number",
      "includeRawJsonAppendix": "boolean",
      "entries": ["ValidationAuditEntry objects"]
    },
    "currentDate": "string",
    "notesUpdatedFormatted": "string",
    "userCompany": "string",
    "userFirstName": "string",
    "userLastName": "string",
    "userBadgeId": "string"
  }
}

Note: Side-specific item fields (leftItemType, rightItemType, side-specific detail objects, and leftAdditionalNotes/rightAdditionalNotes) are the primary contract for current clients. Legacy fields (classType, itemType, bulletData, cartridgeCaseData, shotshellData, hasSubclass, customClass, classNote, additionalNotes) are still accepted for backward compatibility. userBadgeId is the authenticated analyst's Badge/ID and is rendered in the PDF report footer alongside the analyst's name.

Audit trail mode note: when data.reportMode = "audit-trail", auditTrailReport is required and the PDF worker renders the comprehensive audit report template (including raw JSON appendix entries) instead of evidence-image annotation layout.

Request Body (direct/internal worker callers):

{
  "reportFormat": "striae",
  "data": {
    "reportMode": "audit-trail (optional)",
    "imageUrl": "string",
    "caseNumber": "string",
    "annotationData": {
      "leftCase": "string",
      "rightCase": "string",
      "leftItem": "string",
      "rightItem": "string",
      "caseFontColor": "string",
      "leftItemType": "Bullet | Cartridge Case | Shotshell | Other",
      "leftCustomClass": "string",
      "leftClassNote": "string",
      "rightItemType": "Bullet | Cartridge Case | Shotshell | Other",
      "rightCustomClass": "string",
      "rightClassNote": "string",
      "indexType": "number | color",
      "indexNumber": "string",
      "indexColor": "string",
      "supportLevel": "ID | Exclusion | Inconclusive",
      "leftHasSubclass": boolean,
      "rightHasSubclass": boolean,
      "includeConfirmation": boolean,
      "leftAdditionalNotes": "string",
      "rightAdditionalNotes": "string",
      "additionalNotes": "string",
      "boxAnnotations": [
        {
          "id": "string",
          "x": "number (percentage 0-100)",
          "y": "number (percentage 0-100)",
          "width": "number (percentage 0-100)",
          "height": "number (percentage 0-100)",
          "color": "string (hex color)"
        }
      ]
    },
    "activeAnnotations": ["string"],
    "auditTrailReport": {
      "caseNumber": "string",
      "exportedAt": "ISO timestamp",
      "exportRangeStart": "ISO timestamp",
      "exportRangeEnd": "ISO timestamp",
      "chunkIndex": "number",
      "totalChunks": "number",
      "totalEntries": "number",
      "includeRawJsonAppendix": "boolean",
      "entries": ["ValidationAuditEntry objects"]
    },
    "currentDate": "string",
    "notesUpdatedFormatted": "string",
    "userCompany": "string",
    "userFirstName": "string",
    "userLastName": "string",
    "userBadgeId": "string"
  }
}

Format resolution note: For browser calls through /api/pdf/*, Pages proxy injects/overrides reportFormat using verified Firebase email and optional PRIMERSHEAR_EMAILS.

Supported format names: striae, primershear

Legacy compatibility: The worker also accepts legacy top-level payloads (without reportFormat and data) for backward compatibility.

Case audit export behavior summary:

  • Triggered by case audit viewer export flow in app layer.
  • Exports full case history regardless of active viewer filters.
  • Splits large exports into multiple requests/PDF parts (chunkIndex / totalChunks).
  • For imported archived read-only cases with bundled audit data, export includes all bundled entries.

Response: PDF file (binary data)

Headers:

  • Content-Type: application/pdf

Status Codes:

  • 200: Success
  • 400: Invalid request body, missing reportFormat, or missing required data.currentDate field
  • 403: Forbidden (invalid worker auth)
  • 405: Method not allowed
  • 500: Server error
  • 502: Browser Rendering endpoint error or missing Browser Rendering credentials
  • 504: Browser Rendering timeout

Data Worker API

Public Base URL: /api/data

Service Binding: env.DATA_WORKER

Authentication: browser calls require Firebase bearer auth; Pages proxy routes to the worker via service binding.

Environment Variables Required:

  • MANIFEST_SIGNING_PRIVATE_KEY - PKCS#8 private key used for signing
  • MANIFEST_SIGNING_KEY_ID - Signing key identifier embedded in signature metadata
  • STRIAE_DATA - R2 bucket binding that stores case and annotation JSON payloads
  • EXPORT_ENCRYPTION_PRIVATE_KEY - PKCS#8 private key used to decrypt encrypted export packages
  • EXPORT_ENCRYPTION_KEY_ID - Decryption key identifier expected in ENCRYPTION_MANIFEST.json
  • EXPORT_ENCRYPTION_KEYS_JSON (optional) - Export decryption key registry JSON for multi-key decryption fallback
  • EXPORT_ENCRYPTION_ACTIVE_KEY_ID (optional) - Active export registry key ID used for preferred key selection
  • DATA_AT_REST_ENCRYPTION_ENABLED (optional) - Feature flag for storage encryption. Accepted: 1, true, yes, on
  • DATA_AT_REST_ENCRYPTION_PUBLIC_KEY (required when enabled) - SPKI PEM public key for storage encryption
  • DATA_AT_REST_ENCRYPTION_KEY_ID (required when enabled) - Key identifier embedded in encrypted payloads
  • DATA_AT_REST_ENCRYPTION_PRIVATE_KEY (optional) - PKCS#8 PEM private key for storage decryption (legacy mode)
  • DATA_AT_REST_ENCRYPTION_KEYS_JSON (optional) - Data-at-rest decryption key registry JSON
  • DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID (optional) - Active data-at-rest registry key ID

Export package encryption for case, confirmation, and archive workflows is mandatory. These decryption variables are therefore required for trusted import workflows.

Endpoints

Get File Data

GET /{filename}.json

Description: Retrieve JSON file data from R2 storage. Automatically decrypts data-at-rest encrypted payloads when encryption metadata is present.

Parameters:

  • filename (path): Full JSON filename including .json extension

Response: JSON data from file or empty array if file doesn't exist

Status Codes:

  • 200: Success
  • 400: Invalid file type (non-JSON)
  • 403: Forbidden
  • 500: Server error (including decryption failures)

Save File Data

PUT /{filename}.json

Description: Save JSON data to R2 storage file. Encrypts data at rest when DATA_AT_REST_ENCRYPTION_ENABLED is set.

Parameters:

  • filename (path): Full JSON filename including .json extension

Request Body: Any valid JSON data

Response:

{
  "success": true
}

Status Codes:

  • 200: Success
  • 400: Invalid file type (non-JSON)
  • 403: Forbidden
  • 500: Server error

Delete File

DELETE /{filename}.json

Description: Delete JSON file from R2 storage

Parameters:

  • filename (path): Full JSON filename including .json extension

Response:

{
  "success": true
}

Status Codes:

  • 200: Success
  • 404: File not found
  • 403: Forbidden
  • 500: Server error

Sign Forensic Manifest

POST /api/forensic/sign-manifest

Description: Sign canonical forensic manifest payload with the server-side private key.

Request Body:

{
  "manifest": {
    "dataHash": "64-char sha256 hex",
    "imageHashes": {
      "image-1.jpg": "64-char sha256 hex"
    },
    "manifestHash": "64-char sha256 hex",
    "totalFiles": 2,
    "createdAt": "2026-03-12T00:00:00.000Z"
  }
}

Notes:

  • The payload may be provided either at manifest or at the JSON root.
  • Returns manifestVersion: "3.0" when successful.
  • No client-supplied seed or salt field is required; RSA-PSS randomness is handled by the worker.

Response:

{
  "success": true,
  "manifestVersion": "3.0",
  "signature": {
    "algorithm": "RSASSA-PSS-SHA-256",
    "keyId": "forensic-signing-key-v1",
    "signedAt": "2026-03-12T00:00:00.000Z",
    "value": "base64url-signature"
  }
}

Status Codes:

  • 200: Signature created
  • 400: Invalid manifest payload
  • 403: Forbidden
  • 500: Server error

Sign Confirmation Export

POST /api/forensic/sign-confirmation

Description: Sign canonical confirmation export payload with the server-side private key.

Request Body:

{
  "confirmationData": {
    "metadata": {
      "caseNumber": "CASE-2026-001",
      "exportDate": "2026-03-12T00:00:00.000Z",
      "exportedBy": "[email protected]",
      "exportedByUid": "uid-123",
      "exportedByName": "Reviewer Name",
      "exportedByCompany": "Agency",
      "totalConfirmations": 1,
      "version": "2.0",
      "hash": "64-char sha256 hex"
    },
    "confirmations": {
      "original-image-id": [
        {
          "fullName": "Reviewer Name",
          "badgeId": "ABC123",
          "timestamp": "03/12/2026 12:00:00",
          "confirmationId": "CONF-123",
          "confirmedBy": "uid-123",
          "confirmedByEmail": "[email protected]",
          "confirmedByCompany": "Agency",
          "confirmedAt": "2026-03-12T00:00:00.000Z"
        }
      ]
    }
  },
  "signatureVersion": "3.0"
}

Notes:

  • signatureVersion defaults to 3.0 when omitted.
  • Only 3.0 is accepted.
  • The payload may be provided either at confirmationData or at the JSON root.

Response:

{
  "success": true,
  "signatureVersion": "3.0",
  "signature": {
    "algorithm": "RSASSA-PSS-SHA-256",
    "keyId": "forensic-signing-key-v1",
    "signedAt": "2026-03-12T00:00:00.000Z",
    "value": "base64url-signature"
  }
}

Status Codes:

  • 200: Signature created
  • 400: Invalid confirmation payload or unsupported signature version
  • 403: Forbidden
  • 500: Server error

Sign Audit Export Metadata

POST /api/forensic/sign-audit-export

Description: Sign canonical audit export metadata with the server-side private key.

Request Body:

{
  "auditExport": {
    "signatureVersion": "2.0",
    "exportFormat": "json",
    "exportType": "trail",
    "scopeType": "case",
    "scopeIdentifier": "CASE-2026-001",
    "generatedAt": "2026-03-12T00:00:00.000Z",
    "totalEntries": 42,
    "hash": "64-char sha256 hex"
  },
  "signatureVersion": "2.0"
}

Notes:

  • signatureVersion defaults to 2.0 when omitted.
  • Only 2.0 is accepted.
  • The payload may be provided either at auditExport or at the JSON root.

Response:

{
  "success": true,
  "signatureVersion": "2.0",
  "signature": {
    "algorithm": "RSASSA-PSS-SHA-256",
    "keyId": "forensic-signing-key-v1",
    "signedAt": "2026-03-12T00:00:00.000Z",
    "value": "base64url-signature"
  }
}

Status Codes:

  • 200: Signature created
  • 400: Invalid audit export payload or unsupported signature version
  • 403: Forbidden
  • 500: Server error

Decrypt Encrypted Export Package

POST /api/forensic/decrypt-export

Description: Decrypt encrypted case, confirmation, or archive package payloads for trusted import workflows.

Request Body:

{
  "wrappedKey": "base64url-wrapped-aes-key",
  "dataIv": "base64url-iv",
  "encryptedData": "base64url-ciphertext",
  "encryptedImages": [
    {
      "filename": "image-1.jpg",
      "encryptedData": "base64url-ciphertext",
      "iv": "base64url-iv"
    }
  ],
  "keyId": "export-encryption-key-v1"
}

Notes:

  • encryptedImages is optional and may be empty for confirmation-only exports.
  • Each image entry requires its own iv field.
  • keyId is used as the first decryption candidate, then worker-side registry fallback keys are attempted.
  • Missing server key configuration or registry parsing failures return a decryption error response.

Response:

{
  "success": true,
  "plaintext": "decrypted json string",
  "decryptedImages": [
    {
      "filename": "image-1.jpg",
      "data": "base64-image-bytes"
    }
  ]
}

Status Codes:

  • 200: Decryption succeeded
  • 400: Missing required fields
  • 403: Forbidden
  • 500: Decryption failed (including key-registry configuration or key-selection failures)

Note: Audit entry APIs are documented under the Audit Worker section below.

Audit Worker API

Public Base URL: /api/audit

Service Binding: env.AUDIT_WORKER

Authentication: browser calls require Firebase bearer auth; Pages proxy routes to the worker via service binding.

Storage: Cloudflare R2 bucket (STRIAE_AUDIT - Dedicated audit bucket)

Environment Variables Required:

  • STRIAE_AUDIT - R2 bucket binding for audit trail storage
  • DATA_AT_REST_ENCRYPTION_ENABLED (optional) - Feature flag for audit encryption. Accepted: 1, true, yes, on
  • DATA_AT_REST_ENCRYPTION_PUBLIC_KEY (required when enabled) - SPKI PEM public key for audit encryption
  • DATA_AT_REST_ENCRYPTION_KEY_ID (required when enabled) - Key identifier for audit encryption
  • DATA_AT_REST_ENCRYPTION_PRIVATE_KEY (optional) - PKCS#8 PEM private key for decryption (legacy mode)
  • DATA_AT_REST_ENCRYPTION_KEYS_JSON (optional) - Data-at-rest decryption key registry JSON
  • DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID (optional) - Active data-at-rest registry key ID

Endpoints

Store Audit Entry

POST /audit/?userId={userId}

Description: Create a new audit trail entry for forensic accountability and compliance tracking

Parameters:

  • userId (query, required): User identifier for audit trail organization

Request Body:

{
  "timestamp": "2025-09-23T14:30:15.123Z",
  "userId": "aDzwq3G6IBVRJVCEFijdg7B0fwq2",
  "userEmail": "[email protected]",
  "action": "export",
  "result": "success",
  "fileName": "CASE-2025-001-audit.json",
  "details": {
    "caseNumber": "CASE-2025-001",
    "workflowPhase": "case-export",
    "validationErrors": []
  }
}

Validation Notes:

  • Required fields validated by the worker: timestamp, userId, and action
  • userId in the request body must match the userId query parameter; mismatches return 400
  • Additional fields are accepted and persisted as part of the audit payload

Response:

{
  "success": true,
  "entryCount": 42,
  "filename": "audit-trails/aDzwq3G6IBVRJVCEFijdg7B0fwq2/2025-09-23.json"
}

Status Codes:

  • 200: Audit entry stored successfully
  • 400: Invalid request data or missing required fields
  • 403: Forbidden
  • 500: Server error

Audit Entry Fields:

Field Type Required Description
timestamp string (ISO 8601) Yes When the action occurred
userId string Yes User identifier for trail grouping
action string Yes Action name (for example case-create, export, import)
result string No Result status such as success, failure, warning, or blocked
details object No Action-specific metadata and workflow context
userEmail string No User email for analyst attribution
fileName string No Associated file name
caseNumber string No Case identifier when action is case-scoped

Get User Audit Entries

GET /audit/?userId={userId}

Description: Retrieve audit trail entries for a specific user with optional filtering

Query Parameters:

  • userId (required): User identifier
  • startDate (optional): Start date filter (YYYY-MM-DD or ISO 8601 format)
  • endDate (optional): End date filter (YYYY-MM-DD or ISO 8601 format)

Example:

GET /audit/?userId=aDzwq3G6IBVRJVCEFijdg7B0fwq2&startDate=2025-09-01&endDate=2025-09-30

Response:

{
  "entries": [
    {
      "timestamp": "2025-09-23T14:30:15.123Z",
      "userId": "aDzwq3G6IBVRJVCEFijdg7B0fwq2",
      "userEmail": "[email protected]",
      "action": "case-export",
      "result": "success",
      "details": {
        "caseNumber": "CASE-2025-001",
        "workflowPhase": "case-export",
        "validationErrors": []
      }
    }
  ],
  "total": 42
}

Status Codes:

  • 200: Success
  • 400: Invalid parameters or missing userId
  • 403: Forbidden
  • 500: Server error

Query Behavior:

  • If no date range is specified, returns entries for the current date only
  • Date range queries read daily audit files and aggregate results
  • Results are sorted by timestamp in descending order (most recent first)
  • Additional filtering (action, result, case number, pagination) is applied client-side in app/services/audit/audit.service.ts

Storage Architecture:

  • Audit entries are stored in R2 with daily file organization: audit-trails/{userId}/{YYYY-MM-DD}.json
  • Individual entries within a daily file are append-only; the file is re-encrypted on each append
  • All entries include tamper-proof timestamps from server time

Error Handling

Standard Error Response Format

{
  "error": "string"
}

Notes:

  • Most workers return JSON error payloads in the shape above.
  • user-worker may also return plain-text error messages for some failure paths.

Common Error Codes

  • Forbidden: Invalid authentication header or token
  • Method not allowed: Unsupported HTTP verb for the endpoint
  • User not found: User UID does not exist in KV
  • Invalid manifest payload: Signing payload failed schema validation
  • userId parameter is required: Audit endpoint called without required query parameter

HTTP Status Codes

  • 200: Success
  • 201: Created
  • 400: Bad Request
  • 403: Forbidden
  • 404: Not Found
  • 405: Method Not Allowed
  • 500: Internal Server Error

Type Definitions

Audit Trail Types

ValidationAuditEntry Interface

Core audit entry structure for all validation events:

interface ValidationAuditEntry {
  timestamp: string;           // ISO 8601 timestamp
  userId: string;             // User identifier  
  userEmail: string;          // User email for identification
  action: AuditAction;        // What action was performed
  result: AuditResult;        // Success/failure/warning/blocked
  details: AuditDetails;      // Action-specific details
}

AuditAction Type

All supported audit actions:

type AuditAction = 
  // Case Management Actions
  | 'case-create' | 'case-rename' | 'case-delete'
  // Confirmation Workflow Actions  
  | 'case-export' | 'case-import' | 'confirmation-create' 
  | 'confirmation-export' | 'confirmation-import'
  // File Operations
  | 'file-upload' | 'file-delete' | 'file-access'
  // Annotation Operations
  | 'annotation-create' | 'annotation-edit' | 'annotation-delete'
  // User & Session Management
  | 'user-login' | 'user-logout' | 'user-profile-update' 
  | 'user-password-reset' | 'user-account-delete'
  // Registration & MFA
  | 'user-registration' | 'email-verification' | 'mfa-enrollment' | 'mfa-authentication'
  // Document Generation
  | 'pdf-generate'
  // Security & Monitoring
  | 'security-violation'
  // Legacy actions (for backward compatibility)
  | 'import' | 'export' | 'confirm' | 'validate';

AuditResult Type

Result types for audit operations:

type AuditResult = 'success' | 'failure' | 'warning' | 'blocked' | 'pending';

AuditDetails Interface

Detailed information for each audit entry:

interface AuditDetails {
  // Core identification
  fileName?: string;
  fileType?: AuditFileType;
  caseNumber?: string;
  confirmationId?: string;

  // Badge/ID — populated for confirmation-import entries to attribute
  // the reviewing examiner's badge number to the audit record
  reviewerBadgeId?: string;
  
  // Validation & Security
  hashValid?: boolean;
  validationErrors: string[];
  securityChecks?: SecurityCheckResults;
  
  // Context & Workflow
  originalExaminerUid?: string;
  reviewingExaminerUid?: string;
  workflowPhase?: WorkflowPhase;
  
  // Performance & Metrics
  performanceMetrics?: PerformanceMetrics;
  
  // Specialized details
  caseDetails?: CaseAuditDetails;
  fileDetails?: FileAuditDetails;
  annotationDetails?: AnnotationAuditDetails;
  sessionDetails?: SessionAuditDetails;
  securityDetails?: SecurityAuditDetails;
  userProfileDetails?: UserProfileAuditDetails;
}

UserProfileAuditDetails Interface

Captures user identity and profile-change context in audit entries. The badgeId field is propagated to every audit entry via the AuditService in-memory cache so that Badge/ID filtering in the viewer remains consistent across a session:

interface UserProfileAuditDetails {
  // Which profile field was changed (for user-profile-update entries)
  profileField?: 'displayName' | 'email' | 'organization' | 'role'
               | 'preferences' | 'avatar' | 'badgeId';
  oldValue?: string;
  newValue?: string;

  // Badge/ID of the acting user — cached by AuditService across logEvent calls
  badgeId?: string;

  // Auth-specific fields
  resetMethod?: 'email' | 'sms' | 'security-questions' | 'admin-reset';
  verificationMethod?: 'email-link' | 'sms-code' | 'totp' | 'backup-codes' | 'admin-verification';
  verificationAttempts?: number;
  passwordComplexityMet?: boolean;
  previousPasswordReused?: boolean;

  // Account deletion fields
  deletionReason?: 'user-requested' | 'admin-initiated' | 'policy-violation' | 'inactive-account';
  casesCount?: number;
  filesCount?: number;

  // Registration fields
  registrationMethod?: 'email-password' | 'sso' | 'admin-created' | 'api';
  firstName?: string;
  lastName?: string;
  company?: string;
  emailVerificationRequired?: boolean;
  mfaEnrollmentRequired?: boolean;
}

AuditTrail Interface

Complete audit trail for a case or workflow:

interface AuditTrail {
  caseNumber: string;
  workflowId: string;           // Unique identifier linking related entries
  entries: ValidationAuditEntry[];
  summary: AuditSummary;
}

AuditSummary Interface

Summary of audit trail for reporting and compliance:

interface AuditSummary {
  totalEvents: number;
  successfulEvents: number;
  failedEvents: number;
  warningEvents: number;
  workflowPhases: WorkflowPhase[];
  participatingUsers: string[];     // User IDs
  startTimestamp: string;
  endTimestamp: string;
  complianceStatus: 'compliant' | 'non-compliant' | 'pending';
  securityIncidents: number;
}

WorkflowPhase Type

Workflow phases for tracking different types of forensic activities:

type WorkflowPhase = 
  | 'casework'           // Case, notes, image, and pdf related actions
  | 'case-export'        // Only case exporting
  | 'case-import'        // Only case importing  
  | 'confirmation'       // Only confirmation-related activity
  | 'user-management';   // User login, logout, profile management, account activities

Core Annotation Types

AnnotationData Interface

The comprehensive data structure for all annotation information:

interface AnnotationData {
  leftCase: string;
  rightCase: string;
  leftItem: string;
  rightItem: string;
  caseFontColor?: string;
  leftItemType?: 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
  leftCustomClass?: string;
  leftClassNote?: string;
  leftBulletData?: BulletAnnotationData;
  leftCartridgeCaseData?: CartridgeCaseAnnotationData;
  leftShotshellData?: ShotshellAnnotationData;
  leftHasSubclass?: boolean;
  rightItemType?: 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
  rightCustomClass?: string;
  rightClassNote?: string;
  rightBulletData?: BulletAnnotationData;
  rightCartridgeCaseData?: CartridgeCaseAnnotationData;
  rightShotshellData?: ShotshellAnnotationData;
  rightHasSubclass?: boolean;
  itemType?: 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other'; // legacy
  classType?: string; // legacy
  customClass?: string; // legacy
  classNote?: string; // legacy
  bulletData?: BulletAnnotationData; // legacy
  cartridgeCaseData?: CartridgeCaseAnnotationData; // legacy
  shotshellData?: ShotshellAnnotationData; // legacy
  hasSubclass?: boolean; // legacy
  indexType?: 'number' | 'color';
  indexNumber?: string;
  indexColor?: string;
  supportLevel?: 'ID' | 'Exclusion' | 'Inconclusive';
  includeConfirmation: boolean;
  confirmationData?: ConfirmationData;
  leftAdditionalNotes?: string;
  rightAdditionalNotes?: string;
  additionalNotes?: string;
  boxAnnotations?: BoxAnnotation[];
  updatedAt: string;
  earliestAnnotationTimestamp?: string; // ISO timestamp of first annotation created (notes or box)
}

Item characteristic sub-objects (leftBulletData, leftCartridgeCaseData, leftShotshellData, and right-side equivalents) are populated by the Item Details modal and are absent unless the examiner records side-specific measurements or marks. Legacy single-side fields remain available for backward compatibility.

Notes fields: leftAdditionalNotes and rightAdditionalNotes capture side-specific notes in the split item model. additionalNotes remains as a compatibility field used by legacy flows and box-annotation note integration.

ConfirmationData Interface

Authenticated confirmation data structure for forensic verification workflow (defined in app/types/annotations.ts):

interface ConfirmationData {
  fullName: string;           // Confirming examiner's full name
  badgeId: string;            // Badge/ID number of confirming examiner  
  timestamp: string;          // Human-readable confirmation timestamp
  confirmationId: string;     // Unique ID generated at confirmation time
  confirmedBy: string;        // User UID of the confirming examiner
  confirmedByEmail: string;   // Email of the confirming examiner
  confirmedByCompany: string; // Company/Lab of the confirming examiner
  confirmedAt: string;        // ISO timestamp of confirmation
}

Purpose: Captures authenticated digital confirmation from reviewing examiners for independent verification of forensic conclusions.

Workflow Context:

  • Created when reviewing examiner confirms findings using the Confirmation modal
  • Stored within AnnotationData when includeConfirmation is true
  • Exported via case export and imported by original examiner
  • Includes unique confirmation ID for audit trails and traceability
  • Appears in PDF reports with full examiner attribution

BulletAnnotationData Interface

Class characteristic data for Bullet examinations (defined in app/types/annotations.ts):

interface BulletAnnotationData {
  caliber?: string;
  mass?: string;
  diameter?: string;
  calcDiameter?: string;      // Calculated diameter
  lgNumber?: number;          // Number of lands and grooves
  lgDirection?: string;       // Rifling direction (right/left twist)
  barrelType?: string;        // Barrel type (conventional, polygonal, etc.)
  lWidths?: string[];         // Land widths L1..Ln (aligned with lgNumber)
  gWidths?: string[];         // Groove widths G1..Gn (aligned with lgNumber)
  jacketMetal?: string;       // Jacket metal composition
  coreMetal?: string;         // Core metal composition
  bulletType?: string;        // Bullet construction type
}

CartridgeCaseAnnotationData Interface

Class characteristic data for Cartridge Case examinations (defined in app/types/annotations.ts):

interface CartridgeCaseAnnotationData {
  caliber?: string;
  brand?: string;
  metal?: string;
  primerType?: string;          // Primer type (Boxer, Berdan, etc.)
  fpiShape?: string;            // Firing pin impression shape
  apertureShape?: string;       // Breech face aperture shape
  hasFpDrag?: boolean;          // Firing pin drag present
  hasExtractorMarks?: boolean;  // Extractor marks present
  hasEjectorMarks?: boolean;    // Ejector marks present
  hasChamberMarks?: boolean;    // Chamber marks present
  hasMagazineLipMarks?: boolean; // Magazine lip marks present
  hasPrimerShear?: boolean;     // Primer shear present
  hasEjectionPortMarks?: boolean; // Ejection port marks present
}

ShotshellAnnotationData Interface

Class characteristic data for Shotshell examinations (defined in app/types/annotations.ts):

interface ShotshellAnnotationData {
  gauge?: string;
  shotSize?: string;
  metal?: string;
  brand?: string;
  fpiShape?: string;            // Firing pin impression shape
  hasExtractorMarks?: boolean;  // Extractor marks present
  hasEjectorMarks?: boolean;    // Ejector marks present
  hasChamberMarks?: boolean;    // Chamber marks present
}

BoxAnnotation Interface

Individual box annotation structure with percentage-based coordinates (defined in app/types/annotations.ts):

interface BoxAnnotation {
  id: string;
  x: number;         // Percentage 0-100
  y: number;         // Percentage 0-100
  width: number;     // Percentage 0-100
  height: number;    // Percentage 0-100
  color: string;     // Hex color code
  label?: string;    // Optional label text
  timestamp: string; // Creation timestamp (ISO 8601 format)
}

User Management Types

UserData Interface

Core user profile and permissions structure:

interface UserData {
  uid: string;
  email: string | null;
  firstName: string;
  lastName: string;
  company: string;
  badgeId?: string;
  permitted: boolean;
  cases: Array<{
    caseNumber: string;
    createdAt: string;
  }>;
  readOnlyCases?: Array<{
    caseNumber: string;
    importedAt: string;
    originalExportDate: string;
    originalExportedBy: string;
    sourceHash?: string;
    sourceManifestVersion?: string;
    sourceSignatureKeyId?: string;
    sourceSignatureValid?: boolean;
    isReadOnly: true;
  }>;
  createdAt: string;
  updatedAt?: string;
}

UserLimits Interface

User account limitations and quotas:

interface UserLimits {
  maxCases: number;
  maxFilesPerCase: number;
}

File Management Types

FileData Interface

Core file information structure:

interface FileData {
  id: string;
  originalFilename: string;
  uploadedAt: string;
}

FileUploadResponse Interface

Image Worker upload response structure:

interface FileUploadResponse {
  success: boolean;
  result: {
    id: string;
    filename: string;
    uploaded: string;
  };
  errors: Array<{
    code: number;
    message: string;
  }>;
  messages: string[];
}

ImageUploadResponse Interface

Image upload response wrapper (extends FileUploadResponse):

interface ImageUploadResponse {
  success: boolean;
  result: FileUploadResponse['result'];
  errors: FileUploadResponse['errors'];
  messages: FileUploadResponse['messages'];
}

Case Management Types

CaseData Interface

Case structure with associated files:

interface CaseData {
  createdAt: string;
  caseNumber: string;
  files: FileData[];
}

CaseActionType

Case operation type definition for UI state management:

type CaseActionType = 'loaded' | 'created' | 'deleted' | null;

CasesToDelete Interface

Structure for batch case deletion operations:

interface CasesToDelete {
  casesToDelete: string[];
}

Case Export Types

CaseExportData Interface

Complete case export data structure for single case exports:

interface CaseExportData {
  metadata: {
    caseNumber: string;
    caseCreatedDate: string;
    exportDate: string;
    exportedBy: string | null;
    exportedByUid: string;
    exportedByName: string;
    exportedByCompany: string;
    striaeExportSchemaVersion: string;
    totalFiles: number;
  };
  files: Array<{
    fileData: FileData;
    annotations?: AnnotationData;
    hasAnnotations: boolean;
  }>;
  summary?: {
    filesWithAnnotations: number;
    filesWithoutAnnotations: number;
    totalBoxAnnotations: number;
    filesWithConfirmations?: number;
    filesWithConfirmationsRequested?: number;
    lastModified?: string;
    earliestAnnotationDate?: string;
    latestAnnotationDate?: string;
    exportError?: string;
  };
}

AllCasesExportData Interface

Comprehensive export data structure for bulk case exports:

interface AllCasesExportData {
  metadata: {
    exportDate: string;
    exportedBy: string | null;
    exportedByUid: string;
    exportedByName: string;
    exportedByCompany: string;
    striaeExportSchemaVersion: string;
    totalCases: number;
    totalFiles: number;
    totalAnnotations: number;
    totalConfirmations: number;
    totalConfirmationsRequested: number;
  };
  cases: CaseExportData[];
  summary?: {
    casesWithFiles: number;
    casesWithAnnotations: number;
    casesWithoutFiles: number;
    lastModified?: string;
    earliestAnnotationDate?: string;
    latestAnnotationDate?: string;
  };
}

ExportOptions Interface

Configuration options for case export operations:

interface ExportOptions {
  includeMetadata?: boolean;
  includeUserInfo?: boolean;
  protectForensicData?: boolean;
  designatedReviewerEmail?: string;
}

Export Features:

  • Encrypted ZIP package: exports produce an encrypted ZIP containing case data, encrypted images, FORENSIC_MANIFEST.json, ENCRYPTION_MANIFEST.json, and the public signing-key PEM
  • Designated Reviewer: optional designatedReviewerEmail restricts the exported package to a specific reviewer
  • Comprehensive Data: all annotation fields including case identifiers, colors, classifications, and box annotations
  • Manifest Signing: server-signed forensic manifest included before encryption metadata is written
  • Metadata Rich: complete export metadata including timestamps, user info, and summary statistics
  • Error Handling: graceful handling of failed case exports with detailed error information
  • Progress Tracking: real-time progress updates for ZIP generation and image packaging

Case Import Types

CaseImportProps Interface

Props interface for the main case import component:

interface CaseImportProps {
  isOpen: boolean;
  onClose: () => void;
  onImportComplete?: (result: ImportResult | ConfirmationImportResult) => void;
}

ImportOptions Interface

Configuration options for case import operations:

interface ImportOptions {
  overwriteExisting?: boolean;
  validateIntegrity?: boolean;
  preserveTimestamps?: boolean;
}

ImportResult Interface

Result structure returned from case import operations:

interface ImportResult {
  success: boolean;
  caseNumber: string;
  isReadOnly: boolean;
  filesImported: number;
  annotationsImported: number;
  errors?: string[];
  warnings?: string[];
}

ConfirmationImportResult Interface

Result structure returned from confirmation import operations:

interface ConfirmationImportResult {
  success: boolean;
  caseNumber: string;
  confirmationsImported: number;
  imagesUpdated: number;
  errors?: string[];
  warnings?: string[];
}

ReadOnlyCaseMetadata Interface

Metadata structure for imported read-only cases:

interface ReadOnlyCaseMetadata {
  caseNumber: string;
  importedAt: string;
  originalExportDate: string;
  originalExportedBy: string;
  sourceHash?: string;
  sourceManifestVersion?: string;
  sourceSignatureKeyId?: string;
  sourceSignatureValid?: boolean;
  isReadOnly: true;
}

CaseImportPreview Interface

Preview information generated during ZIP parsing before import:

interface CaseImportPreview {
  caseNumber: string;
  exportedBy: string | null;
  exportedByName: string | null;
  exportedByCompany: string | null;
  exportDate: string;
  totalFiles: number;
  caseCreatedDate?: string;
  hashValid?: boolean;
  hashError?: string;
  hasAnnotations: boolean;
  validationSummary: string;
  errors?: string[];
  validationDetails?: {
    hasForensicManifest: boolean;
    dataValid?: boolean;
    imageValidation?: { [filename: string]: boolean };
    manifestValid?: boolean;
    signatureValid?: boolean;
    signatureKeyId?: string;
    signatureError?: string;
    validationSummary?: string;
    integrityErrors?: string[];
  };
}

Import Features:

  • Complete ZIP Package Import: Full case data and image import from exported ZIP packages
  • Read-Only Protection: Imported cases are automatically set to read-only mode for secure review
  • Duplicate Prevention: Prevents import if user was the original case analyst
  • Progress Tracking: Multi-stage progress reporting with detailed status updates
  • Image Integration: Automatic upload and association of all case images
  • Metadata Preservation: Complete preservation of original export metadata and timestamps
  • Data Integrity: Comprehensive validation of ZIP contents and case data structure

Encrypted ZIP Package Details:

  • Single Case: encrypted ZIP packages are produced per-case
  • Encrypted Images: all associated image files are individually encrypted before packaging
  • Encrypted Data File: case data is encrypted and stored as ciphertext inside the ZIP
  • FORENSIC_MANIFEST.json: server-signed manifest covering data and image hashes
  • ENCRYPTION_MANIFEST.json: required envelope describing encryption parameters; import is fail-closed without it
  • Public Key PEM: signing public key bundled for offline verification
  • Progress Feedback: real-time progress updates during packaging and image encryption
  • JSZip Integration: browser-based ZIP generation using JSZip library

Centralized Type Management

Type Definition Location: All interfaces are centrally defined in app/types/ with barrel exports from app/types/index.ts:

// Barrel export structure
export * from './annotations';  // AnnotationData, BoxAnnotation
export * from './user';        // UserData, UserLimits
export * from './file';        // FileData, FileUploadResponse, ImageUploadResponse
export * from './case';        // CaseData, CaseActionType, CaseExportData, AllCasesExportData
export * from './import';      // ImportResult, ConfirmationImportResult, CaseImportPreview
export * from './export';      // ExportOptions
export * from './audit';       // ValidationAuditEntry, AuditTrail, AuditQueryParams

Usage Patterns:

  • Centralized Imports: import { UserData, FileData, AnnotationData } from '~/types';
  • Component Props: All component interfaces extend or use centralized types
  • Worker APIs: Type validation based on shared interface definitions
  • PDF Generation: Type-safe data flow from frontend through workers
  • Data Storage: Structured data persistence with comprehensive type validation

Benefits:

  • Single source of truth for all data structures
  • Type safety across entire application stack
  • Consistent API contracts between frontend and workers
  • Easier refactoring and maintenance with centralized type management
  • Compile-time error detection for data structure changes
  • Comprehensive coverage of all application domains (user, file, case, annotation)
  • Utility types for common patterns (loading states, API responses, pagination)

SDK Examples

JavaScript/TypeScript Client

class StriaeAPI {
  constructor(private apiKey: string, private baseUrl: string) {}

  async getUser(userUid: string): Promise<UserData> {
    const response = await fetch(`${this.baseUrl}/${userUid}`, {
      headers: {
        'Authorization': `Bearer ${this.firebaseToken}`,
        'Content-Type': 'application/json'
      }
    });
    
    if (!response.ok) {
      throw new Error(`API Error: ${response.status}`);
    }
    
    return response.json();
  }

  async uploadImage(file: File): Promise<UploadResult> {
    const formData = new FormData();
    formData.append('file', file);

    const response = await fetch(`${this.baseUrl}`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`
      },
      body: formData
    });

    return response.json();
  }
}

Error Handling Example

try {
  const userData = await api.getUser(userUid);
  console.log('User data:', userData);
} catch (error) {
  if (error.status === 404) {
    console.log('User not found');
  } else if (error.status === 403) {
    console.log('Authentication failed (forbidden)');
  } else {
    console.error('API error:', error);
  }
}

Related Documentation

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