API Reference - striae-org/striae GitHub Wiki
- Error Handling
- Type Definitions
- SDK Examples
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.
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 viaenv.USER_WORKERservice binding -
/api/data/*-> Data Worker viaenv.DATA_WORKERservice binding -
/api/audit/*-> Audit Worker viaenv.AUDIT_WORKERservice binding -
/api/image/*-> Image Worker viaenv.IMAGE_WORKERservice 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, withstvalidated by the Image Worker -
/api/pdf/*-> PDF Worker viaenv.PDF_WORKERservice binding; Pages proxy resolvesreportFormatserver-side using verified Firebase email and theprimershearlist from the lists-worker (STRIAE_LISTSKV, key"primershear"), then worker calls Cloudflare Browser Rendering/accounts/{ACCOUNT_ID}/browser-rendering/pdfwithAuthorization: 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 directSTRIAE_DATAandSTRIAE_FILESR2 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_DATAbucket binding -
Audit Worker: Uses required data-at-rest encryption keys plus optional registry-extension vars for rotation, and
STRIAE_AUDITbucket 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, requiredIMAGE_SIGNED_URL_SECRET, andSTRIAE_FILESbucket binding -
PDF Worker: Uses
ACCOUNT_IDandBROWSER_API_TOKENenvironment variables -
Lists Worker: Uses
LISTS_ADMIN_SECRETto guard write endpoints; ownsSTRIAE_LISTSKV 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.
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 withwrangler 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.
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.
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
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 /{userUid}Description: Delete user and all associated data including cases, files, and images.
Streaming Option: Supports server-sent event progress when either:
-
stream=trueis provided as a query parameter -
Accept: text/event-streamis sent
Parameters:
-
userUid(path): Firebase user UID
Process:
- Retrieves user data
- Deletes all user's cases and associated files from Data Worker
- Deletes all associated images from Image Worker
- Deletes user's Firebase Authentication account
- 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
PUT /{userUid}/casesDescription: 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
DELETE /{userUid}/casesDescription: 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
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
sttokens withIMAGE_SIGNED_URL_SECRET. - Default signed URL TTL is 3600 seconds, capped at 86400 seconds.
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
POST /{fileId}/signed-urlDescription: 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 /{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 byPOST /{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 /{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
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}.
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, missingreportFormat, or missing requireddata.currentDatefield -
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
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 inENCRYPTION_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.
GET /{filename}.jsonDescription: 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.jsonextension
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)
PUT /{filename}.jsonDescription: 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.jsonextension
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 /{filename}.jsonDescription: Delete JSON file from R2 storage
Parameters:
-
filename(path): Full JSON filename including.jsonextension
Response:
{
"success": true
}Status Codes:
-
200: Success -
404: File not found -
403: Forbidden -
500: Server error
POST /api/forensic/sign-manifestDescription: 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
manifestor 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
POST /api/forensic/sign-confirmationDescription: 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:
-
signatureVersiondefaults to3.0when omitted. - Only
3.0is accepted. - The payload may be provided either at
confirmationDataor 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
POST /api/forensic/sign-audit-exportDescription: 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:
-
signatureVersiondefaults to2.0when omitted. - Only
2.0is accepted. - The payload may be provided either at
auditExportor 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
POST /api/forensic/decrypt-exportDescription: 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:
-
encryptedImagesis optional and may be empty for confirmation-only exports. - Each image entry requires its own
ivfield. -
keyIdis 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.
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
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, andaction -
userIdin the request body must match theuserIdquery parameter; mismatches return400 - 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 /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-30Response:
{
"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": "string"
}Notes:
- Most workers return JSON error payloads in the shape above.
-
user-workermay also return plain-text error messages for some failure paths.
-
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
-
200: Success -
201: Created -
400: Bad Request -
403: Forbidden -
404: Not Found -
405: Method Not Allowed -
500: Internal Server Error
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
}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';Result types for audit operations:
type AuditResult = 'success' | 'failure' | 'warning' | 'blocked' | 'pending';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;
}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;
}Complete audit trail for a case or workflow:
interface AuditTrail {
caseNumber: string;
workflowId: string; // Unique identifier linking related entries
entries: ValidationAuditEntry[];
summary: AuditSummary;
}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;
}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 activitiesThe 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.
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
includeConfirmationis 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
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
}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
}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
}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)
}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;
}User account limitations and quotas:
interface UserLimits {
maxCases: number;
maxFilesPerCase: number;
}Core file information structure:
interface FileData {
id: string;
originalFilename: string;
uploadedAt: string;
}Image Worker upload response structure:
interface FileUploadResponse {
success: boolean;
result: {
id: string;
filename: string;
uploaded: string;
};
errors: Array<{
code: number;
message: string;
}>;
messages: string[];
}Image upload response wrapper (extends FileUploadResponse):
interface ImageUploadResponse {
success: boolean;
result: FileUploadResponse['result'];
errors: FileUploadResponse['errors'];
messages: FileUploadResponse['messages'];
}Case structure with associated files:
interface CaseData {
createdAt: string;
caseNumber: string;
files: FileData[];
}Case operation type definition for UI state management:
type CaseActionType = 'loaded' | 'created' | 'deleted' | null;Structure for batch case deletion operations:
interface CasesToDelete {
casesToDelete: string[];
}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;
};
}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;
};
}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
designatedReviewerEmailrestricts 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
Props interface for the main case import component:
interface CaseImportProps {
isOpen: boolean;
onClose: () => void;
onImportComplete?: (result: ImportResult | ConfirmationImportResult) => void;
}Configuration options for case import operations:
interface ImportOptions {
overwriteExisting?: boolean;
validateIntegrity?: boolean;
preserveTimestamps?: boolean;
}Result structure returned from case import operations:
interface ImportResult {
success: boolean;
caseNumber: string;
isReadOnly: boolean;
filesImported: number;
annotationsImported: number;
errors?: string[];
warnings?: string[];
}Result structure returned from confirmation import operations:
interface ConfirmationImportResult {
success: boolean;
caseNumber: string;
confirmationsImported: number;
imagesUpdated: number;
errors?: string[];
warnings?: string[];
}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;
}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
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, AuditQueryParamsUsage 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)
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();
}
}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);
}
}