@striae-org/striae 5.4.2 → 5.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/app/components/actions/case-export/download-handlers.ts +1 -1
  2. package/app/components/actions/case-export/metadata-helpers.ts +2 -4
  3. package/app/components/actions/case-import/zip-processing.ts +3 -3
  4. package/app/components/mobile-warning/mobile-warning.module.css +80 -0
  5. package/app/components/mobile-warning/mobile-warning.tsx +108 -0
  6. package/app/components/navbar/case-import/utils/file-validation.ts +1 -1
  7. package/app/config-example/config.json +2 -2
  8. package/app/root.tsx +2 -0
  9. package/app/services/audit/audit-file-type.ts +0 -1
  10. package/app/services/audit/audit.service.ts +1 -1
  11. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -6
  12. package/app/services/audit/index.ts +0 -1
  13. package/app/types/audit.ts +1 -1
  14. package/app/utils/auth/auth-action-settings.ts +1 -1
  15. package/app/utils/data/permissions.ts +17 -15
  16. package/app/utils/forensics/audit-export-signature.ts +4 -4
  17. package/app/utils/forensics/export-verification.ts +3 -11
  18. package/package.json +2 -2
  19. package/workers/audit-worker/package.json +1 -1
  20. package/workers/audit-worker/src/audit-worker.example.ts +1 -1
  21. package/workers/audit-worker/src/handlers/audit-routes.ts +1 -30
  22. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  23. package/workers/data-worker/package.json +17 -17
  24. package/workers/data-worker/src/encryption-utils.ts +1 -1
  25. package/workers/data-worker/src/signing-payload-utils.ts +4 -4
  26. package/workers/data-worker/wrangler.jsonc.example +1 -1
  27. package/workers/image-worker/package.json +1 -1
  28. package/workers/image-worker/src/auth.ts +7 -0
  29. package/workers/image-worker/src/handlers/delete-image.ts +26 -0
  30. package/workers/image-worker/src/handlers/mint-signed-url.ts +83 -0
  31. package/workers/image-worker/src/handlers/serve-image.ts +65 -0
  32. package/workers/image-worker/src/handlers/upload-image.ts +62 -0
  33. package/workers/image-worker/src/image-worker.example.ts +3 -707
  34. package/workers/image-worker/src/router.ts +53 -0
  35. package/workers/image-worker/src/security/key-registry.ts +193 -0
  36. package/workers/image-worker/src/security/signed-url.ts +163 -0
  37. package/workers/image-worker/src/types.ts +68 -0
  38. package/workers/image-worker/src/utils/content-disposition.ts +33 -0
  39. package/workers/image-worker/src/utils/path-utils.ts +50 -0
  40. package/workers/image-worker/src/utils/storage-metadata.ts +27 -0
  41. package/workers/image-worker/wrangler.jsonc.example +1 -1
  42. package/workers/pdf-worker/package.json +1 -1
  43. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  44. package/workers/user-worker/package.json +1 -1
  45. package/workers/user-worker/src/handlers/user-routes.ts +23 -34
  46. package/workers/user-worker/src/user-worker.example.ts +17 -23
  47. package/workers/user-worker/wrangler.jsonc.example +1 -1
  48. package/wrangler.toml.example +1 -1
  49. package/app/components/audit/viewer/use-audit-viewer-export.ts +0 -176
  50. package/app/services/audit/audit-export-csv.ts +0 -130
  51. package/app/services/audit/audit-export-report.ts +0 -205
  52. package/app/services/audit/audit-export.service.ts +0 -333
@@ -5,7 +5,7 @@
5
5
  "name": "DATA_WORKER_NAME",
6
6
  "account_id": "ACCOUNT_ID",
7
7
  "main": "src/data-worker.ts",
8
- "compatibility_date": "2026-03-31",
8
+ "compatibility_date": "2026-04-02",
9
9
  "compatibility_flags": [
10
10
  "nodejs_compat"
11
11
  ],
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "@cloudflare/puppeteer": "^1.0.6",
12
- "wrangler": "^4.78.0"
12
+ "wrangler": "^4.80.0"
13
13
  },
14
14
  "overrides": {
15
15
  "undici": "7.24.1",
@@ -0,0 +1,7 @@
1
+ import type { Env } from './types';
2
+
3
+ export function hasValidToken(request: Request, env: Env): boolean {
4
+ const authHeader = request.headers.get('Authorization');
5
+ const expectedToken = `Bearer ${env.IMAGES_API_TOKEN}`;
6
+ return authHeader === expectedToken;
7
+ }
@@ -0,0 +1,26 @@
1
+ import { hasValidToken } from '../auth';
2
+ import type { CreateImageWorkerResponse, Env } from '../types';
3
+ import { parseFileId } from '../utils/path-utils';
4
+
5
+ export async function handleImageDelete(
6
+ request: Request,
7
+ env: Env,
8
+ createJsonResponse: CreateImageWorkerResponse
9
+ ): Promise<Response> {
10
+ if (!hasValidToken(request, env)) {
11
+ return createJsonResponse({ error: 'Unauthorized' }, 403);
12
+ }
13
+
14
+ const fileId = parseFileId(new URL(request.url).pathname);
15
+ if (!fileId) {
16
+ return createJsonResponse({ error: 'Image ID is required' }, 400);
17
+ }
18
+
19
+ const existing = await env.STRIAE_FILES.head(fileId);
20
+ if (!existing) {
21
+ return createJsonResponse({ error: 'File not found' }, 404);
22
+ }
23
+
24
+ await env.STRIAE_FILES.delete(fileId);
25
+ return createJsonResponse({ success: true });
26
+ }
@@ -0,0 +1,83 @@
1
+ import { hasValidToken } from '../auth';
2
+ import {
3
+ normalizeSignedUrlTtlSeconds,
4
+ parseSignedUrlBaseUrl,
5
+ requireSignedUrlConfig,
6
+ signSignedAccessPayload
7
+ } from '../security/signed-url';
8
+ import type {
9
+ CreateImageWorkerResponse,
10
+ Env,
11
+ SignedAccessPayload
12
+ } from '../types';
13
+
14
+ interface SignedUrlMintRequestBody {
15
+ expiresInSeconds?: unknown;
16
+ }
17
+
18
+ export async function handleSignedUrlMinting(
19
+ request: Request,
20
+ env: Env,
21
+ fileId: string,
22
+ createJsonResponse: CreateImageWorkerResponse
23
+ ): Promise<Response> {
24
+ if (!hasValidToken(request, env)) {
25
+ return createJsonResponse({ error: 'Unauthorized' }, 403);
26
+ }
27
+
28
+ requireSignedUrlConfig(env);
29
+
30
+ const existing = await env.STRIAE_FILES.head(fileId);
31
+ if (!existing) {
32
+ return createJsonResponse({ error: 'File not found' }, 404);
33
+ }
34
+
35
+ let requestedExpiresInSeconds: number | undefined;
36
+ const contentType = request.headers.get('Content-Type') || '';
37
+ if (contentType.includes('application/json')) {
38
+ const requestBody = await request.json().catch(() => null);
39
+ if (requestBody && typeof requestBody === 'object') {
40
+ const expiresInSeconds = (requestBody as SignedUrlMintRequestBody).expiresInSeconds;
41
+ if (typeof expiresInSeconds === 'number') {
42
+ requestedExpiresInSeconds = expiresInSeconds;
43
+ }
44
+ }
45
+ }
46
+
47
+ const nowEpochSeconds = Math.floor(Date.now() / 1000);
48
+ const ttlSeconds = normalizeSignedUrlTtlSeconds(requestedExpiresInSeconds, env);
49
+ const payload: SignedAccessPayload = {
50
+ fileId,
51
+ iat: nowEpochSeconds,
52
+ exp: nowEpochSeconds + ttlSeconds,
53
+ nonce: crypto.randomUUID().replace(/-/g, '')
54
+ };
55
+
56
+ const signedToken = await signSignedAccessPayload(payload, env);
57
+
58
+ let baseUrl: string;
59
+ if (env.IMAGE_SIGNED_URL_BASE_URL) {
60
+ try {
61
+ baseUrl = parseSignedUrlBaseUrl(env.IMAGE_SIGNED_URL_BASE_URL);
62
+ } catch (error) {
63
+ console.error('Invalid IMAGE_SIGNED_URL_BASE_URL configuration', {
64
+ reason: error instanceof Error ? error.message : String(error)
65
+ });
66
+ return createJsonResponse({ error: 'Signed URL base URL is misconfigured' }, 500);
67
+ }
68
+ } else {
69
+ baseUrl = new URL(request.url).origin;
70
+ }
71
+
72
+ const signedUrl = `${baseUrl}/${encodeURIComponent(fileId)}?st=${encodeURIComponent(signedToken)}`;
73
+
74
+ return createJsonResponse({
75
+ success: true,
76
+ result: {
77
+ fileId,
78
+ url: signedUrl,
79
+ expiresAt: new Date(payload.exp * 1000).toISOString(),
80
+ expiresInSeconds: ttlSeconds
81
+ }
82
+ });
83
+ }
@@ -0,0 +1,65 @@
1
+ import { hasValidToken } from '../auth';
2
+ import {
3
+ decryptBinaryWithRegistry,
4
+ requireEncryptionRetrievalConfig
5
+ } from '../security/key-registry';
6
+ import { requireSignedUrlConfig, verifySignedAccessToken } from '../security/signed-url';
7
+ import type { CreateImageWorkerResponse, Env } from '../types';
8
+ import { buildSafeContentDisposition } from '../utils/content-disposition';
9
+ import { extractEnvelope } from '../utils/storage-metadata';
10
+
11
+ export async function handleImageServing(
12
+ request: Request,
13
+ env: Env,
14
+ fileId: string,
15
+ createJsonResponse: CreateImageWorkerResponse,
16
+ corsHeaders: Record<string, string>
17
+ ): Promise<Response> {
18
+ const requestUrl = new URL(request.url);
19
+ const hasSignedToken = requestUrl.searchParams.has('st');
20
+ const signedToken = requestUrl.searchParams.get('st');
21
+
22
+ if (hasSignedToken) {
23
+ requireSignedUrlConfig(env);
24
+
25
+ if (!signedToken || signedToken.trim().length === 0) {
26
+ return createJsonResponse({ error: 'Invalid or expired signed URL token' }, 403);
27
+ }
28
+
29
+ const tokenValid = await verifySignedAccessToken(signedToken, fileId, env);
30
+ if (!tokenValid) {
31
+ return createJsonResponse({ error: 'Invalid or expired signed URL token' }, 403);
32
+ }
33
+ } else if (!hasValidToken(request, env)) {
34
+ return createJsonResponse({ error: 'Unauthorized' }, 403);
35
+ }
36
+
37
+ requireEncryptionRetrievalConfig(env);
38
+
39
+ const file = await env.STRIAE_FILES.get(fileId);
40
+ if (!file) {
41
+ return createJsonResponse({ error: 'File not found' }, 404);
42
+ }
43
+
44
+ const envelope = extractEnvelope(file);
45
+ if (!envelope) {
46
+ return createJsonResponse({ error: 'Missing data-at-rest envelope metadata' }, 500);
47
+ }
48
+
49
+ const encryptedData = await file.arrayBuffer();
50
+ const plaintext = await decryptBinaryWithRegistry(encryptedData, envelope, env);
51
+
52
+ const contentType = file.customMetadata?.contentType || 'application/octet-stream';
53
+ const filename = file.customMetadata?.originalFilename || fileId;
54
+ const contentDisposition = buildSafeContentDisposition(filename, fileId);
55
+
56
+ return new Response(plaintext, {
57
+ status: 200,
58
+ headers: {
59
+ ...corsHeaders,
60
+ 'Cache-Control': 'no-store',
61
+ 'Content-Type': contentType,
62
+ 'Content-Disposition': contentDisposition
63
+ }
64
+ });
65
+ }
@@ -0,0 +1,62 @@
1
+ import { hasValidToken } from '../auth';
2
+ import { encryptBinaryForStorage } from '../encryption-utils';
3
+ import { requireEncryptionUploadConfig } from '../security/key-registry';
4
+ import type { CreateImageWorkerResponse, Env } from '../types';
5
+ import { deriveFileKind } from '../utils/content-disposition';
6
+
7
+ export async function handleImageUpload(
8
+ request: Request,
9
+ env: Env,
10
+ createJsonResponse: CreateImageWorkerResponse
11
+ ): Promise<Response> {
12
+ if (!hasValidToken(request, env)) {
13
+ return createJsonResponse({ error: 'Unauthorized' }, 403);
14
+ }
15
+
16
+ requireEncryptionUploadConfig(env);
17
+
18
+ const formData = await request.formData();
19
+ const fileValue = formData.get('file');
20
+ if (!(fileValue instanceof Blob)) {
21
+ return createJsonResponse({ error: 'Missing file upload payload' }, 400);
22
+ }
23
+
24
+ const fileBlob = fileValue;
25
+ const uploadedAt = new Date().toISOString();
26
+ const filename = fileValue instanceof File && fileValue.name ? fileValue.name : 'upload.bin';
27
+ const contentType = fileBlob.type || 'application/octet-stream';
28
+ const fileId = crypto.randomUUID().replace(/-/g, '');
29
+ const plaintextBytes = await fileBlob.arrayBuffer();
30
+
31
+ const encryptedPayload = await encryptBinaryForStorage(
32
+ plaintextBytes,
33
+ env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY,
34
+ env.DATA_AT_REST_ENCRYPTION_KEY_ID
35
+ );
36
+
37
+ await env.STRIAE_FILES.put(fileId, encryptedPayload.ciphertext, {
38
+ customMetadata: {
39
+ algorithm: encryptedPayload.envelope.algorithm,
40
+ encryptionVersion: encryptedPayload.envelope.encryptionVersion,
41
+ keyId: encryptedPayload.envelope.keyId,
42
+ dataIv: encryptedPayload.envelope.dataIv,
43
+ wrappedKey: encryptedPayload.envelope.wrappedKey,
44
+ contentType,
45
+ originalFilename: filename,
46
+ byteLength: String(fileBlob.size),
47
+ createdAt: uploadedAt,
48
+ fileKind: deriveFileKind(contentType)
49
+ }
50
+ });
51
+
52
+ return createJsonResponse({
53
+ success: true,
54
+ errors: [],
55
+ messages: [],
56
+ result: {
57
+ id: fileId,
58
+ filename,
59
+ uploaded: uploadedAt
60
+ }
61
+ });
62
+ }