@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.
- package/app/components/actions/case-export/download-handlers.ts +1 -1
- package/app/components/actions/case-export/metadata-helpers.ts +2 -4
- package/app/components/actions/case-import/zip-processing.ts +3 -3
- package/app/components/mobile-warning/mobile-warning.module.css +80 -0
- package/app/components/mobile-warning/mobile-warning.tsx +108 -0
- package/app/components/navbar/case-import/utils/file-validation.ts +1 -1
- package/app/config-example/config.json +2 -2
- package/app/root.tsx +2 -0
- package/app/services/audit/audit-file-type.ts +0 -1
- package/app/services/audit/audit.service.ts +1 -1
- package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -6
- package/app/services/audit/index.ts +0 -1
- package/app/types/audit.ts +1 -1
- package/app/utils/auth/auth-action-settings.ts +1 -1
- package/app/utils/data/permissions.ts +17 -15
- package/app/utils/forensics/audit-export-signature.ts +4 -4
- package/app/utils/forensics/export-verification.ts +3 -11
- package/package.json +2 -2
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/src/audit-worker.example.ts +1 -1
- package/workers/audit-worker/src/handlers/audit-routes.ts +1 -30
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +17 -17
- package/workers/data-worker/src/encryption-utils.ts +1 -1
- package/workers/data-worker/src/signing-payload-utils.ts +4 -4
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/auth.ts +7 -0
- package/workers/image-worker/src/handlers/delete-image.ts +26 -0
- package/workers/image-worker/src/handlers/mint-signed-url.ts +83 -0
- package/workers/image-worker/src/handlers/serve-image.ts +65 -0
- package/workers/image-worker/src/handlers/upload-image.ts +62 -0
- package/workers/image-worker/src/image-worker.example.ts +3 -707
- package/workers/image-worker/src/router.ts +53 -0
- package/workers/image-worker/src/security/key-registry.ts +193 -0
- package/workers/image-worker/src/security/signed-url.ts +163 -0
- package/workers/image-worker/src/types.ts +68 -0
- package/workers/image-worker/src/utils/content-disposition.ts +33 -0
- package/workers/image-worker/src/utils/path-utils.ts +50 -0
- package/workers/image-worker/src/utils/storage-metadata.ts +27 -0
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/src/handlers/user-routes.ts +23 -34
- package/workers/user-worker/src/user-worker.example.ts +17 -23
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/audit/viewer/use-audit-viewer-export.ts +0 -176
- package/app/services/audit/audit-export-csv.ts +0 -130
- package/app/services/audit/audit-export-report.ts +0 -205
- package/app/services/audit/audit-export.service.ts +0 -333
|
@@ -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
|
+
}
|