@striae-org/striae 5.4.3 → 5.4.5

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.
@@ -39,7 +39,7 @@ export const getSafeContinuePath = (continueUrl: string | null | undefined): str
39
39
  }
40
40
 
41
41
  const safePath = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
42
- return safePath.startsWith('/') ? safePath : DEFAULT_CONTINUE_PATH;
42
+ return normalizeContinuePath(safePath);
43
43
  } catch {
44
44
  return DEFAULT_CONTINUE_PATH;
45
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "5.4.3",
3
+ "version": "5.4.5",
4
4
  "private": false,
5
5
  "description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
6
6
  "license": "Apache-2.0",
@@ -100,18 +100,18 @@
100
100
  "deploy-workers:user": "cd workers/user-worker && npm run deploy"
101
101
  },
102
102
  "dependencies": {
103
- "@react-router/cloudflare": "^7.13.2",
103
+ "@react-router/cloudflare": "^7.14.0",
104
104
  "firebase": "^12.11.0",
105
105
  "isbot": "^5.1.37",
106
106
  "jszip": "^3.10.1",
107
107
  "qrcode": "^1.5.4",
108
108
  "react": "^19.2.4",
109
109
  "react-dom": "^19.2.4",
110
- "react-router": "^7.13.2"
110
+ "react-router": "^7.14.0"
111
111
  },
112
112
  "devDependencies": {
113
- "@react-router/dev": "^7.13.2",
114
- "@react-router/fs-routes": "^7.13.2",
113
+ "@react-router/dev": "^7.14.0",
114
+ "@react-router/fs-routes": "^7.14.0",
115
115
  "@types/qrcode": "^1.5.6",
116
116
  "@types/react": "^19.2.14",
117
117
  "@types/react-dom": "^19.2.3",
@@ -139,4 +139,4 @@
139
139
  "node": ">=20.0.0"
140
140
  },
141
141
  "packageManager": "npm@11.11.0"
142
- }
142
+ }
@@ -41,7 +41,7 @@ try {
41
41
  {
42
42
  state: 'ENABLED',
43
43
  totpProviderConfig: {
44
- adjacentIntervals: 5,
44
+ adjacentIntervals: 0,
45
45
  },
46
46
  },
47
47
  ],
@@ -49,7 +49,7 @@ try {
49
49
  });
50
50
 
51
51
  console.log('✅ TOTP MFA provider enabled successfully.');
52
- console.log(' adjacentIntervals: 5 (allows ±2.5 minutes clock skew)');
52
+ console.log(' adjacentIntervals: 0 (accepts only the current TOTP interval; no adjacent interval tolerance)');
53
53
  } catch (err) {
54
54
  console.error('\n❌ Failed to enable TOTP MFA provider:');
55
55
  console.error(err?.message ?? err);
@@ -7,7 +7,7 @@
7
7
  "name": "AUDIT_WORKER_NAME",
8
8
  "account_id": "ACCOUNT_ID",
9
9
  "main": "src/audit-worker.ts",
10
- "compatibility_date": "2026-04-02",
10
+ "compatibility_date": "2026-04-03",
11
11
  "compatibility_flags": [
12
12
  "nodejs_compat"
13
13
  ],
@@ -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-04-02",
8
+ "compatibility_date": "2026-04-03",
9
9
  "compatibility_flags": [
10
10
  "nodejs_compat"
11
11
  ],
@@ -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
+ }