@striae-org/striae 6.1.8 → 7.0.1

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 (50) hide show
  1. package/.env.example +0 -26
  2. package/app/components/actions/image-manage.ts +17 -67
  3. package/app/components/canvas/canvas.module.css +12 -0
  4. package/app/components/canvas/canvas.tsx +26 -5
  5. package/functions/api/audit/[[path]].ts +9 -24
  6. package/functions/api/data/[[path]].ts +9 -24
  7. package/functions/api/image/[[path]].ts +14 -30
  8. package/functions/api/pdf/[[path]].ts +9 -24
  9. package/functions/api/user/[[path]].ts +20 -36
  10. package/package.json +9 -10
  11. package/scripts/deploy-all.sh +29 -10
  12. package/scripts/deploy-config/modules/env-utils.sh +0 -68
  13. package/scripts/deploy-config/modules/prompt.sh +4 -110
  14. package/scripts/deploy-config/modules/scaffolding.sh +5 -0
  15. package/scripts/deploy-config/modules/validation.sh +1 -19
  16. package/scripts/deploy-pages-secrets.sh +0 -9
  17. package/scripts/deploy-worker-secrets.sh +2 -8
  18. package/tsconfig.json +1 -4
  19. package/workers/audit-worker/package.json +2 -2
  20. package/workers/audit-worker/src/audit-worker.ts +0 -5
  21. package/workers/audit-worker/src/config.ts +1 -6
  22. package/workers/audit-worker/src/types.ts +0 -1
  23. package/workers/audit-worker/wrangler.jsonc.example +2 -6
  24. package/workers/data-worker/package.json +3 -3
  25. package/workers/data-worker/src/config.ts +1 -6
  26. package/workers/data-worker/src/data-worker.ts +1 -6
  27. package/workers/data-worker/src/types.ts +0 -1
  28. package/workers/data-worker/wrangler.jsonc.example +2 -4
  29. package/workers/image-worker/package.json +2 -2
  30. package/workers/image-worker/src/handlers/delete-image.ts +5 -10
  31. package/workers/image-worker/src/handlers/mint-signed-url.ts +5 -10
  32. package/workers/image-worker/src/handlers/serve-image.ts +8 -9
  33. package/workers/image-worker/src/handlers/upload-image.ts +4 -9
  34. package/workers/image-worker/src/image-worker.ts +4 -4
  35. package/workers/image-worker/src/router.ts +11 -11
  36. package/workers/image-worker/src/security/signed-url.ts +2 -2
  37. package/workers/image-worker/src/types.ts +1 -2
  38. package/workers/image-worker/wrangler.jsonc.example +2 -1
  39. package/workers/pdf-worker/package.json +2 -2
  40. package/workers/pdf-worker/src/pdf-worker.ts +0 -8
  41. package/workers/pdf-worker/wrangler.jsonc.example +2 -1
  42. package/workers/user-worker/package.json +2 -2
  43. package/workers/user-worker/src/auth.ts +0 -7
  44. package/workers/user-worker/src/handlers/user-routes.ts +26 -34
  45. package/workers/user-worker/src/types.ts +13 -2
  46. package/workers/user-worker/src/user-worker.ts +19 -27
  47. package/workers/user-worker/wrangler.jsonc.example +2 -1
  48. package/wrangler.toml.example +22 -2
  49. package/worker-configuration.d.ts +0 -7509
  50. package/workers/image-worker/src/auth.ts +0 -7
@@ -1,26 +1,21 @@
1
- import { hasValidToken } from '../auth';
2
- import type { CreateImageWorkerResponse, Env } from '../types';
1
+ import type { CreateResponse, Env } from '../types';
3
2
  import { parseFileId } from '../utils/path-utils';
4
3
 
5
4
  export async function handleImageDelete(
6
5
  request: Request,
7
6
  env: Env,
8
- createJsonResponse: CreateImageWorkerResponse
7
+ respond: CreateResponse
9
8
  ): Promise<Response> {
10
- if (!hasValidToken(request, env)) {
11
- return createJsonResponse({ error: 'Unauthorized' }, 403);
12
- }
13
-
14
9
  const fileId = parseFileId(new URL(request.url).pathname);
15
10
  if (!fileId) {
16
- return createJsonResponse({ error: 'Image ID is required' }, 400);
11
+ return respond({ error: 'Image ID is required' }, 400);
17
12
  }
18
13
 
19
14
  const existing = await env.STRIAE_FILES.head(fileId);
20
15
  if (!existing) {
21
- return createJsonResponse({ error: 'File not found' }, 404);
16
+ return respond({ error: 'File not found' }, 404);
22
17
  }
23
18
 
24
19
  await env.STRIAE_FILES.delete(fileId);
25
- return createJsonResponse({ success: true });
20
+ return respond({ success: true });
26
21
  }
@@ -1,4 +1,3 @@
1
- import { hasValidToken } from '../auth';
2
1
  import {
3
2
  normalizeSignedUrlTtlSeconds,
4
3
  parseSignedUrlBaseUrl,
@@ -6,7 +5,7 @@ import {
6
5
  signSignedAccessPayload
7
6
  } from '../security/signed-url';
8
7
  import type {
9
- CreateImageWorkerResponse,
8
+ CreateResponse,
10
9
  Env,
11
10
  SignedAccessPayload
12
11
  } from '../types';
@@ -19,17 +18,13 @@ export async function handleSignedUrlMinting(
19
18
  request: Request,
20
19
  env: Env,
21
20
  fileId: string,
22
- createJsonResponse: CreateImageWorkerResponse
21
+ respond: CreateResponse
23
22
  ): Promise<Response> {
24
- if (!hasValidToken(request, env)) {
25
- return createJsonResponse({ error: 'Unauthorized' }, 403);
26
- }
27
-
28
23
  requireSignedUrlConfig(env);
29
24
 
30
25
  const existing = await env.STRIAE_FILES.head(fileId);
31
26
  if (!existing) {
32
- return createJsonResponse({ error: 'File not found' }, 404);
27
+ return respond({ error: 'File not found' }, 404);
33
28
  }
34
29
 
35
30
  let requestedExpiresInSeconds: number | undefined;
@@ -63,7 +58,7 @@ export async function handleSignedUrlMinting(
63
58
  console.error('Invalid IMAGE_SIGNED_URL_BASE_URL configuration', {
64
59
  reason: error instanceof Error ? error.message : String(error)
65
60
  });
66
- return createJsonResponse({ error: 'Signed URL base URL is misconfigured' }, 500);
61
+ return respond({ error: 'Signed URL base URL is misconfigured' }, 500);
67
62
  }
68
63
  } else {
69
64
  baseUrl = new URL(request.url).origin;
@@ -71,7 +66,7 @@ export async function handleSignedUrlMinting(
71
66
 
72
67
  const signedUrl = `${baseUrl}/${encodeURIComponent(fileId)}?st=${encodeURIComponent(signedToken)}`;
73
68
 
74
- return createJsonResponse({
69
+ return respond({
75
70
  success: true,
76
71
  result: {
77
72
  fileId,
@@ -1,10 +1,9 @@
1
- import { hasValidToken } from '../auth';
2
1
  import {
3
2
  decryptBinaryWithRegistry,
4
3
  requireEncryptionRetrievalConfig
5
4
  } from '../security/key-registry';
6
5
  import { requireSignedUrlConfig, verifySignedAccessToken } from '../security/signed-url';
7
- import type { CreateImageWorkerResponse, Env } from '../types';
6
+ import type { CreateResponse, Env } from '../types';
8
7
  import { buildSafeContentDisposition } from '../utils/content-disposition';
9
8
  import { extractEnvelope } from '../utils/storage-metadata';
10
9
 
@@ -12,7 +11,7 @@ export async function handleImageServing(
12
11
  request: Request,
13
12
  env: Env,
14
13
  fileId: string,
15
- createJsonResponse: CreateImageWorkerResponse
14
+ respond: CreateResponse
16
15
  ): Promise<Response> {
17
16
  const requestUrl = new URL(request.url);
18
17
  const hasSignedToken = requestUrl.searchParams.has('st');
@@ -22,27 +21,27 @@ export async function handleImageServing(
22
21
  requireSignedUrlConfig(env);
23
22
 
24
23
  if (!signedToken || signedToken.trim().length === 0) {
25
- return createJsonResponse({ error: 'Invalid or expired signed URL token' }, 403);
24
+ return respond({ error: 'Invalid or expired signed URL token' }, 403);
26
25
  }
27
26
 
28
27
  const tokenValid = await verifySignedAccessToken(signedToken, fileId, env);
29
28
  if (!tokenValid) {
30
- return createJsonResponse({ error: 'Invalid or expired signed URL token' }, 403);
29
+ return respond({ error: 'Invalid or expired signed URL token' }, 403);
31
30
  }
32
- } else if (!hasValidToken(request, env)) {
33
- return createJsonResponse({ error: 'Unauthorized' }, 403);
31
+ } else {
32
+ return respond({ error: 'Unauthorized' }, 403);
34
33
  }
35
34
 
36
35
  requireEncryptionRetrievalConfig(env);
37
36
 
38
37
  const file = await env.STRIAE_FILES.get(fileId);
39
38
  if (!file) {
40
- return createJsonResponse({ error: 'File not found' }, 404);
39
+ return respond({ error: 'File not found' }, 404);
41
40
  }
42
41
 
43
42
  const envelope = extractEnvelope(file);
44
43
  if (!envelope) {
45
- return createJsonResponse({ error: 'Missing data-at-rest envelope metadata' }, 500);
44
+ return respond({ error: 'Missing data-at-rest envelope metadata' }, 500);
46
45
  }
47
46
 
48
47
  const encryptedData = await file.arrayBuffer();
@@ -1,24 +1,19 @@
1
- import { hasValidToken } from '../auth';
2
1
  import { encryptBinaryForStorage } from '../encryption-utils';
3
2
  import { requireEncryptionUploadConfig } from '../security/key-registry';
4
- import type { CreateImageWorkerResponse, Env } from '../types';
3
+ import type { CreateResponse, Env } from '../types';
5
4
  import { deriveFileKind } from '../utils/content-disposition';
6
5
 
7
6
  export async function handleImageUpload(
8
7
  request: Request,
9
8
  env: Env,
10
- createJsonResponse: CreateImageWorkerResponse
9
+ respond: CreateResponse
11
10
  ): Promise<Response> {
12
- if (!hasValidToken(request, env)) {
13
- return createJsonResponse({ error: 'Unauthorized' }, 403);
14
- }
15
-
16
11
  requireEncryptionUploadConfig(env);
17
12
 
18
13
  const formData = await request.formData();
19
14
  const fileValue = formData.get('file');
20
15
  if (!(fileValue instanceof Blob)) {
21
- return createJsonResponse({ error: 'Missing file upload payload' }, 400);
16
+ return respond({ error: 'Missing file upload payload' }, 400);
22
17
  }
23
18
 
24
19
  const fileBlob = fileValue;
@@ -49,7 +44,7 @@ export async function handleImageUpload(
49
44
  }
50
45
  });
51
46
 
52
- return createJsonResponse({
47
+ return respond({
53
48
  success: true,
54
49
  errors: [],
55
50
  messages: [],
@@ -1,7 +1,7 @@
1
1
  import { routeImageWorkerRequest } from './router';
2
- import type { APIResponse, Env } from './types';
2
+ import type { APIResponse, CreateResponse, Env } from './types';
3
3
 
4
- const createJsonResponse = (data: APIResponse, status: number = 200): Response => new Response(
4
+ const createWorkerResponse: CreateResponse = (data: APIResponse, status: number = 200): Response => new Response(
5
5
  JSON.stringify(data),
6
6
  {
7
7
  status,
@@ -12,10 +12,10 @@ const createJsonResponse = (data: APIResponse, status: number = 200): Response =
12
12
  export default {
13
13
  async fetch(request: Request, env: Env): Promise<Response> {
14
14
  try {
15
- return await routeImageWorkerRequest(request, env, createJsonResponse);
15
+ return await routeImageWorkerRequest(request, env, createWorkerResponse);
16
16
  } catch (error) {
17
17
  const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
18
- return createJsonResponse({ error: errorMessage }, 500);
18
+ return createWorkerResponse({ error: errorMessage }, 500);
19
19
  }
20
20
  }
21
21
  };
@@ -2,51 +2,51 @@ import { handleImageDelete } from './handlers/delete-image';
2
2
  import { handleSignedUrlMinting } from './handlers/mint-signed-url';
3
3
  import { handleImageServing } from './handlers/serve-image';
4
4
  import { handleImageUpload } from './handlers/upload-image';
5
- import type { CreateImageWorkerResponse, Env } from './types';
5
+ import type { CreateResponse, Env } from './types';
6
6
  import { parsePathSegments } from './utils/path-utils';
7
7
 
8
8
  export async function routeImageWorkerRequest(
9
9
  request: Request,
10
10
  env: Env,
11
- createJsonResponse: CreateImageWorkerResponse
11
+ respond: CreateResponse
12
12
  ): Promise<Response> {
13
13
  const requestUrl = new URL(request.url);
14
14
  const pathSegments = parsePathSegments(requestUrl.pathname);
15
15
  if (!pathSegments) {
16
- return createJsonResponse({ error: 'Invalid image path encoding' }, 400);
16
+ return respond({ error: 'Invalid image path encoding' }, 400);
17
17
  }
18
18
 
19
19
  switch (request.method) {
20
20
  case 'POST': {
21
21
  if (pathSegments.length === 0) {
22
- return handleImageUpload(request, env, createJsonResponse);
22
+ return handleImageUpload(request, env, respond);
23
23
  }
24
24
 
25
25
  if (pathSegments.length === 2 && pathSegments[1] === 'signed-url') {
26
- return handleSignedUrlMinting(request, env, pathSegments[0], createJsonResponse);
26
+ return handleSignedUrlMinting(request, env, pathSegments[0], respond);
27
27
  }
28
28
 
29
- return createJsonResponse({ error: 'Not found' }, 404);
29
+ return respond({ error: 'Not found' }, 404);
30
30
  }
31
31
 
32
32
  case 'GET': {
33
33
  const fileId = pathSegments.length === 1 ? pathSegments[0] : null;
34
34
  if (!fileId) {
35
- return createJsonResponse({ error: 'Image ID is required' }, 400);
35
+ return respond({ error: 'Image ID is required' }, 400);
36
36
  }
37
37
 
38
- return handleImageServing(request, env, fileId, createJsonResponse);
38
+ return handleImageServing(request, env, fileId, respond);
39
39
  }
40
40
 
41
41
  case 'DELETE': {
42
42
  if (pathSegments.length !== 1) {
43
- return createJsonResponse({ error: 'Not found' }, 404);
43
+ return respond({ error: 'Not found' }, 404);
44
44
  }
45
45
 
46
- return handleImageDelete(request, env, createJsonResponse);
46
+ return handleImageDelete(request, env, respond);
47
47
  }
48
48
 
49
49
  default:
50
- return createJsonResponse({ error: 'Method not allowed' }, 405);
50
+ return respond({ error: 'Method not allowed' }, 405);
51
51
  }
52
52
  }
@@ -51,7 +51,7 @@ export function normalizeSignedUrlTtlSeconds(requestedTtlSeconds: unknown, env:
51
51
  }
52
52
 
53
53
  export function requireSignedUrlConfig(env: Env): void {
54
- const resolvedSecret = (env.IMAGE_SIGNED_URL_SECRET || env.IMAGES_API_TOKEN || '').trim();
54
+ const resolvedSecret = (env.IMAGE_SIGNED_URL_SECRET || '').trim();
55
55
  if (resolvedSecret.length === 0) {
56
56
  throw new Error('Signed URL configuration is missing');
57
57
  }
@@ -77,7 +77,7 @@ export function parseSignedUrlBaseUrl(raw: string): string {
77
77
  }
78
78
 
79
79
  async function getSignedUrlHmacKey(env: Env): Promise<CryptoKey> {
80
- const resolvedSecret = (env.IMAGE_SIGNED_URL_SECRET || env.IMAGES_API_TOKEN || '').trim();
80
+ const resolvedSecret = (env.IMAGE_SIGNED_URL_SECRET || '').trim();
81
81
  const keyBytes = new TextEncoder().encode(resolvedSecret);
82
82
 
83
83
  return crypto.subtle.importKey(
@@ -1,5 +1,4 @@
1
1
  export interface Env {
2
- IMAGES_API_TOKEN: string;
3
2
  STRIAE_FILES: R2Bucket;
4
3
  DATA_AT_REST_ENCRYPTION_PRIVATE_KEY?: string;
5
4
  DATA_AT_REST_ENCRYPTION_PUBLIC_KEY: string;
@@ -65,4 +64,4 @@ export interface SignedAccessPayload {
65
64
  nonce: string;
66
65
  }
67
66
 
68
- export type CreateImageWorkerResponse = (data: APIResponse, status?: number) => Response;
67
+ export type CreateResponse = (data: APIResponse, status?: number) => Response;
@@ -2,7 +2,8 @@
2
2
  "name": "IMAGES_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/image-worker.ts",
5
- "compatibility_date": "2026-04-20",
5
+ "workers_dev": false,
6
+ "compatibility_date": "2026-04-22",
6
7
  "compatibility_flags": [
7
8
  "nodejs_compat"
8
9
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-worker",
3
- "version": "6.1.8",
3
+ "version": "7.0.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "generate:assets": "node scripts/generate-assets.js",
@@ -9,6 +9,6 @@
9
9
  "start": "wrangler dev"
10
10
  },
11
11
  "devDependencies": {
12
- "wrangler": "^4.84.0"
12
+ "wrangler": "^4.84.1"
13
13
  }
14
14
  }
@@ -2,7 +2,6 @@ import type { PDFGenerationData, PDFGenerationRequest, ReportModule, ReportPdfOp
2
2
  import { getAuditTrailPdfOptions, isAuditTrailReportMode, renderAuditTrailReport } from './audit-trail-report';
3
3
 
4
4
  interface Env {
5
- PDF_WORKER_AUTH: string;
6
5
  ACCOUNT_ID?: string;
7
6
  BROWSER_API_TOKEN?: string;
8
7
  }
@@ -40,9 +39,6 @@ const reportModuleLoaders: Record<string, () => Promise<ReportModule>> = {
40
39
 
41
40
  };
42
41
 
43
- const hasValidHeader = (request: Request, env: Env): boolean =>
44
- request.headers.get('X-Custom-Auth-Key') === env.PDF_WORKER_AUTH;
45
-
46
42
  function isTimeoutError(error: unknown): boolean {
47
43
  return error instanceof Error && (
48
44
  error.name === 'AbortError' ||
@@ -193,10 +189,6 @@ async function renderPdfViaRestEndpoint(env: Env, html: string, pdfOptions: Repo
193
189
 
194
190
  export default {
195
191
  async fetch(request: Request, env: Env): Promise<Response> {
196
- if (!hasValidHeader(request, env)) {
197
- return jsonResponse({ error: 'Forbidden' }, 403);
198
- }
199
-
200
192
  if (request.method === 'POST') {
201
193
  try {
202
194
  const payload = await request.json() as unknown;
@@ -2,7 +2,8 @@
2
2
  "name": "PDF_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/pdf-worker.ts",
5
- "compatibility_date": "2026-04-20",
5
+ "workers_dev": false,
6
+ "compatibility_date": "2026-04-22",
6
7
  "compatibility_flags": [
7
8
  "nodejs_compat"
8
9
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "user-worker",
3
- "version": "6.1.8",
3
+ "version": "7.0.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -8,6 +8,6 @@
8
8
  "start": "wrangler dev"
9
9
  },
10
10
  "devDependencies": {
11
- "wrangler": "^4.84.0"
11
+ "wrangler": "^4.84.1"
12
12
  }
13
13
  }
@@ -1,12 +1,5 @@
1
1
  import type { Env } from './types';
2
2
 
3
- export async function authenticate(request: Request, env: Env): Promise<void> {
4
- const authKey = request.headers.get('X-Custom-Auth-Key');
5
- if (authKey !== env.USER_DB_AUTH) {
6
- throw new Error('Unauthorized');
7
- }
8
- }
9
-
10
3
  export function requireUserKvReadConfig(env: Env): void {
11
4
  const hasLegacyPrivateKey = typeof env.USER_KV_ENCRYPTION_PRIVATE_KEY === 'string' && env.USER_KV_ENCRYPTION_PRIVATE_KEY.trim().length > 0;
12
5
  const hasRegistryPrivateKeys = typeof env.USER_KV_ENCRYPTION_KEYS_JSON === 'string' && env.USER_KV_ENCRYPTION_KEYS_JSON.trim().length > 0;
@@ -3,49 +3,38 @@ import { readUserRecord, writeUserRecord } from '../storage/user-records';
3
3
  import type {
4
4
  AddCasesRequest,
5
5
  AccountDeletionProgressEvent,
6
+ CreateResponse,
6
7
  DeleteCasesRequest,
7
8
  Env,
8
9
  UserData,
9
10
  UserRequestData
10
11
  } from '../types';
11
12
 
12
- function createJsonResponse(data: unknown, status: number = 200): Response {
13
- return new Response(JSON.stringify(data), {
14
- status,
15
- headers: { 'Content-Type': 'application/json; charset=utf-8' }
16
- });
17
- }
18
-
19
- function createTextResponse(message: string, status: number): Response {
20
- return new Response(message, {
21
- status,
22
- headers: { 'Content-Type': 'text/plain; charset=utf-8' }
23
- });
24
- }
25
-
26
13
  export async function handleGetUser(
27
14
  env: Env,
28
- userUid: string
15
+ userUid: string,
16
+ respond: CreateResponse
29
17
  ): Promise<Response> {
30
18
  try {
31
19
  const userData = await readUserRecord(env, userUid);
32
20
  if (userData === null) {
33
- return createTextResponse('User not found', 404);
21
+ return respond({ error: 'User not found' }, 404);
34
22
  }
35
23
 
36
- return createJsonResponse(userData);
24
+ return respond(userData);
37
25
  } catch (error) {
38
26
  const errorMessage = error instanceof Error ? error.message : 'Unknown user data read error';
39
27
  console.error('Failed to get user data:', { uid: userUid, reason: errorMessage });
40
28
 
41
- return createTextResponse('Failed to get user data', 500);
29
+ return respond({ error: 'Failed to get user data' }, 500);
42
30
  }
43
31
  }
44
32
 
45
33
  export async function handleAddUser(
46
34
  request: Request,
47
35
  env: Env,
48
- userUid: string
36
+ userUid: string,
37
+ respond: CreateResponse
49
38
  ): Promise<Response> {
50
39
  try {
51
40
  const requestData: UserRequestData = await request.json();
@@ -87,20 +76,21 @@ export async function handleAddUser(
87
76
 
88
77
  await writeUserRecord(env, userUid, userData);
89
78
 
90
- return createJsonResponse(userData, existingUser !== null ? 200 : 201);
79
+ return respond(userData, existingUser !== null ? 200 : 201);
91
80
  } catch {
92
- return createTextResponse('Failed to save user data', 500);
81
+ return respond({ error: 'Failed to save user data' }, 500);
93
82
  }
94
83
  }
95
84
 
96
85
  export async function handleDeleteUser(
97
86
  env: Env,
98
- userUid: string
87
+ userUid: string,
88
+ respond: CreateResponse
99
89
  ): Promise<Response> {
100
90
  try {
101
91
  const result = await executeUserDeletion(env, userUid);
102
92
 
103
- return createJsonResponse({
93
+ return respond({
104
94
  success: result.success,
105
95
  message: result.message
106
96
  });
@@ -109,10 +99,10 @@ export async function handleDeleteUser(
109
99
  const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
110
100
 
111
101
  if (errorMessage === 'User not found') {
112
- return createTextResponse('User not found', 404);
102
+ return respond({ error: 'User not found' }, 404);
113
103
  }
114
104
 
115
- return createJsonResponse({
105
+ return respond({
116
106
  success: false,
117
107
  message: 'Failed to delete user account'
118
108
  }, 500);
@@ -170,13 +160,14 @@ export function handleDeleteUserWithProgress(
170
160
  export async function handleAddCases(
171
161
  request: Request,
172
162
  env: Env,
173
- userUid: string
163
+ userUid: string,
164
+ respond: CreateResponse
174
165
  ): Promise<Response> {
175
166
  try {
176
167
  const { cases = [] }: AddCasesRequest = await request.json();
177
168
  const userData = await readUserRecord(env, userUid);
178
169
  if (!userData) {
179
- return createTextResponse('User not found', 404);
170
+ return respond({ error: 'User not found' }, 404);
180
171
  }
181
172
 
182
173
  const existingCases = userData.cases || [];
@@ -188,30 +179,31 @@ export async function handleAddCases(
188
179
  userData.updatedAt = new Date().toISOString();
189
180
  await writeUserRecord(env, userUid, userData);
190
181
 
191
- return createJsonResponse(userData);
182
+ return respond(userData);
192
183
  } catch {
193
- return createTextResponse('Failed to add cases', 500);
184
+ return respond({ error: 'Failed to add cases' }, 500);
194
185
  }
195
186
  }
196
187
 
197
188
  export async function handleDeleteCases(
198
189
  request: Request,
199
190
  env: Env,
200
- userUid: string
191
+ userUid: string,
192
+ respond: CreateResponse
201
193
  ): Promise<Response> {
202
194
  try {
203
195
  const { casesToDelete }: DeleteCasesRequest = await request.json();
204
196
  const userData = await readUserRecord(env, userUid);
205
197
  if (!userData) {
206
- return createTextResponse('User not found', 404);
198
+ return respond({ error: 'User not found' }, 404);
207
199
  }
208
200
 
209
- userData.cases = userData.cases.filter((caseItem) => !casesToDelete.includes(caseItem.caseNumber));
201
+ userData.cases = (userData.cases || []).filter((caseItem) => !casesToDelete.includes(caseItem.caseNumber));
210
202
  userData.updatedAt = new Date().toISOString();
211
203
  await writeUserRecord(env, userUid, userData);
212
204
 
213
- return createJsonResponse(userData);
205
+ return respond(userData);
214
206
  } catch {
215
- return createTextResponse('Failed to delete cases', 500);
207
+ return respond({ error: 'Failed to delete cases' }, 500);
216
208
  }
217
209
  }
@@ -1,9 +1,7 @@
1
1
  export interface Env {
2
- USER_DB_AUTH: string;
3
2
  USER_DB: KVNamespace;
4
3
  STRIAE_DATA: R2Bucket;
5
4
  STRIAE_FILES: R2Bucket;
6
- R2_KEY_SECRET: string;
7
5
  DATA_AT_REST_ENCRYPTION_PRIVATE_KEY?: string;
8
6
  DATA_AT_REST_ENCRYPTION_KEY_ID?: string;
9
7
  DATA_AT_REST_ENCRYPTION_KEYS_JSON?: string;
@@ -30,6 +28,19 @@ export interface PrivateKeyRegistry {
30
28
 
31
29
  export type DecryptionTelemetryOutcome = 'primary-hit' | 'fallback-hit' | 'all-failed';
32
30
 
31
+ export interface SuccessResponse {
32
+ success: boolean;
33
+ message?: string;
34
+ }
35
+
36
+ export interface ErrorResponse {
37
+ error: string;
38
+ }
39
+
40
+ export type APIResponse = SuccessResponse | ErrorResponse | UserData;
41
+
42
+ export type CreateResponse = (data: APIResponse, status?: number) => Response;
43
+
33
44
  export interface CaseItem {
34
45
  caseNumber: string;
35
46
  caseName?: string;
@@ -1,4 +1,4 @@
1
- import { authenticate, requireUserKvReadConfig, requireUserKvWriteConfig } from './auth';
1
+ import { requireUserKvReadConfig, requireUserKvWriteConfig } from './auth';
2
2
  import { USER_CASES_SEGMENT } from './config';
3
3
  import {
4
4
  handleAddCases,
@@ -8,42 +8,38 @@ import {
8
8
  handleDeleteUserWithProgress,
9
9
  handleGetUser
10
10
  } from './handlers/user-routes';
11
- import type { Env } from './types';
11
+ import type { CreateResponse, Env } from './types';
12
12
 
13
- function createTextResponse(message: string, status: number): Response {
14
- return new Response(message, {
15
- status,
16
- headers: { 'Content-Type': 'text/plain; charset=utf-8' }
17
- });
18
- }
13
+ const createWorkerResponse: CreateResponse = (data, status: number = 200): Response => new Response(
14
+ JSON.stringify(data),
15
+ { status, headers: { 'Content-Type': 'application/json' } }
16
+ );
19
17
 
20
18
  export default {
21
19
  async fetch(request: Request, env: Env): Promise<Response> {
22
20
  try {
23
- await authenticate(request, env);
24
-
25
21
  // DELETE can mutate user KV data (for example /:uid/cases), so non-GET methods require write config.
26
22
  if (request.method === 'GET') {
27
23
  requireUserKvReadConfig(env);
28
24
  } else {
29
25
  requireUserKvWriteConfig(env);
30
26
  }
31
-
27
+
32
28
  const url = new URL(request.url);
33
29
  const parts = url.pathname.split('/');
34
30
  const userUid = parts[1];
35
31
  const isCasesEndpoint = parts[2] === USER_CASES_SEGMENT;
36
-
32
+
37
33
  if (!userUid) {
38
- return createTextResponse('Not Found', 404);
34
+ return createWorkerResponse({ error: 'Not Found' }, 404);
39
35
  }
40
36
 
41
37
  // Handle regular cases endpoint
42
38
  if (isCasesEndpoint) {
43
39
  switch (request.method) {
44
- case 'PUT': return handleAddCases(request, env, userUid);
45
- case 'DELETE': return handleDeleteCases(request, env, userUid);
46
- default: return createTextResponse('Method not allowed', 405);
40
+ case 'PUT': return handleAddCases(request, env, userUid, createWorkerResponse);
41
+ case 'DELETE': return handleDeleteCases(request, env, userUid, createWorkerResponse);
42
+ default: return createWorkerResponse({ error: 'Method not allowed' }, 405);
47
43
  }
48
44
  }
49
45
 
@@ -52,24 +48,20 @@ export default {
52
48
  const streamProgress = url.searchParams.get('stream') === 'true' || acceptsEventStream;
53
49
 
54
50
  switch (request.method) {
55
- case 'GET': return handleGetUser(env, userUid);
56
- case 'PUT': return handleAddUser(request, env, userUid);
51
+ case 'GET': return handleGetUser(env, userUid, createWorkerResponse);
52
+ case 'PUT': return handleAddUser(request, env, userUid, createWorkerResponse);
57
53
  case 'DELETE': return streamProgress
58
54
  ? handleDeleteUserWithProgress(env, userUid)
59
- : handleDeleteUser(env, userUid);
60
- default: return createTextResponse('Method not allowed', 405);
55
+ : handleDeleteUser(env, userUid, createWorkerResponse);
56
+ default: return createWorkerResponse({ error: 'Method not allowed' }, 405);
61
57
  }
62
58
  } catch (error) {
63
59
  const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
64
- if (errorMessage === 'Unauthorized') {
65
- return createTextResponse('Forbidden', 403);
66
- }
67
-
68
60
  if (errorMessage === 'User KV encryption is not fully configured') {
69
- return createTextResponse(errorMessage, 500);
61
+ return createWorkerResponse({ error: errorMessage }, 500);
70
62
  }
71
-
72
- return createTextResponse('Internal Server Error', 500);
63
+
64
+ return createWorkerResponse({ error: 'Internal Server Error' }, 500);
73
65
  }
74
66
  }
75
67
  };