@striae-org/striae 3.3.0 → 4.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 (68) hide show
  1. package/.env.example +1 -1
  2. package/README.md +1 -1
  3. package/app/components/actions/case-export/core-export.ts +5 -2
  4. package/app/components/actions/case-export/download-handlers.ts +1 -1
  5. package/app/components/actions/case-import/confirmation-import.ts +24 -23
  6. package/app/components/actions/case-import/image-operations.ts +20 -49
  7. package/app/components/actions/case-import/orchestrator.ts +1 -1
  8. package/app/components/actions/case-import/storage-operations.ts +54 -89
  9. package/app/components/actions/case-import/validation.ts +2 -13
  10. package/app/components/actions/case-manage.ts +15 -27
  11. package/app/components/actions/generate-pdf.ts +3 -7
  12. package/app/components/actions/image-manage.ts +64 -129
  13. package/app/components/button/button.module.css +12 -8
  14. package/app/components/sidebar/case-export/case-export.tsx +11 -6
  15. package/app/components/sidebar/cases/case-sidebar.tsx +21 -6
  16. package/app/components/sidebar/sidebar.module.css +0 -2
  17. package/app/components/user/delete-account.tsx +7 -7
  18. package/app/config-example/config.json +2 -8
  19. package/app/hooks/useInactivityTimeout.ts +2 -5
  20. package/app/root.tsx +94 -63
  21. package/app/routes/auth/emailActionHandler.tsx +1 -1
  22. package/app/routes/auth/emailVerification.tsx +1 -1
  23. package/app/routes/auth/login.tsx +7 -9
  24. package/app/routes/auth/passwordReset.tsx +1 -1
  25. package/app/routes/auth/route.ts +4 -3
  26. package/app/routes/striae/striae.tsx +4 -8
  27. package/app/services/audit/audit-api-client.ts +40 -0
  28. package/app/services/audit/audit-worker-client.ts +14 -17
  29. package/app/styles/root.module.css +13 -101
  30. package/app/tailwind.css +9 -2
  31. package/app/utils/auth.ts +5 -32
  32. package/app/utils/data-api-client.ts +43 -0
  33. package/app/utils/data-operations.ts +59 -75
  34. package/app/utils/image-api-client.ts +130 -0
  35. package/app/utils/pdf-api-client.ts +43 -0
  36. package/app/utils/permissions.ts +10 -23
  37. package/app/utils/user-api-client.ts +90 -0
  38. package/functions/api/_shared/firebase-auth.ts +255 -0
  39. package/functions/api/audit/[[path]].ts +150 -0
  40. package/functions/api/data/[[path]].ts +141 -0
  41. package/functions/api/image/[[path]].ts +143 -0
  42. package/functions/api/pdf/[[path]].ts +110 -0
  43. package/functions/api/user/[[path]].ts +196 -0
  44. package/package.json +3 -2
  45. package/public/.well-known/security.txt +3 -4
  46. package/scripts/deploy-all.sh +22 -8
  47. package/scripts/deploy-config.sh +194 -165
  48. package/scripts/deploy-pages-secrets.sh +231 -0
  49. package/scripts/deploy-worker-secrets.sh +1 -1
  50. package/worker-configuration.d.ts +7491 -11363
  51. package/workers/audit-worker/worker-configuration.d.ts +11323 -7448
  52. package/workers/audit-worker/wrangler.jsonc.example +1 -8
  53. package/workers/data-worker/worker-configuration.d.ts +11323 -7448
  54. package/workers/data-worker/wrangler.jsonc.example +1 -8
  55. package/workers/image-worker/src/image-worker.example.ts +10 -2
  56. package/workers/image-worker/worker-configuration.d.ts +11322 -7447
  57. package/workers/image-worker/wrangler.jsonc.example +1 -8
  58. package/workers/keys-worker/src/keys.ts +2 -1
  59. package/workers/keys-worker/worker-configuration.d.ts +11322 -7447
  60. package/workers/keys-worker/wrangler.jsonc.example +2 -9
  61. package/workers/pdf-worker/src/assets/icon-256.png +0 -0
  62. package/workers/pdf-worker/worker-configuration.d.ts +11323 -7448
  63. package/workers/pdf-worker/wrangler.jsonc.example +1 -8
  64. package/workers/user-worker/src/user-worker.example.ts +121 -41
  65. package/workers/user-worker/worker-configuration.d.ts +11323 -7448
  66. package/workers/user-worker/wrangler.jsonc.example +1 -8
  67. package/wrangler.toml.example +1 -1
  68. package/app/styles/legal-pages.module.css +0 -113
@@ -2,7 +2,7 @@
2
2
  "name": "PDF_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/pdf-worker.ts",
5
- "compatibility_date": "2026-03-14",
5
+ "compatibility_date": "2026-03-15",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -15,12 +15,5 @@
15
15
  "enabled": true
16
16
  },
17
17
 
18
- "routes": [
19
- {
20
- "pattern": "PDF_WORKER_DOMAIN",
21
- "custom_domain": true
22
- }
23
- ],
24
-
25
18
  "placement": { "mode": "smart" }
26
19
  }
@@ -3,6 +3,8 @@ interface Env {
3
3
  USER_DB: KVNamespace;
4
4
  R2_KEY_SECRET: string;
5
5
  IMAGES_API_TOKEN: string;
6
+ DATA_WORKER_DOMAIN?: string;
7
+ IMAGES_WORKER_DOMAIN?: string;
6
8
  PROJECT_ID: string;
7
9
  FIREBASE_SERVICE_ACCOUNT_EMAIL: string;
8
10
  FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY: string;
@@ -84,9 +86,8 @@ const corsHeaders: Record<string, string> = {
84
86
  };
85
87
 
86
88
  // Worker URLs - configure these for deployment
87
- const DATA_WORKER_URL = 'DATA_WORKER_DOMAIN';
88
-
89
- const IMAGE_WORKER_URL = 'IMAGES_WORKER_DOMAIN';
89
+ const DEFAULT_DATA_WORKER_BASE_URL = 'DATA_WORKER_DOMAIN';
90
+ const DEFAULT_IMAGE_WORKER_BASE_URL = 'IMAGES_WORKER_DOMAIN';
90
91
 
91
92
  const GOOGLE_OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token';
92
93
  const FIREBASE_IDENTITY_TOOLKIT_BASE_URL = 'https://identitytoolkit.googleapis.com/v1/projects';
@@ -98,6 +99,33 @@ async function authenticate(request: Request, env: Env): Promise<void> {
98
99
  if (authKey !== env.USER_DB_AUTH) throw new Error('Unauthorized');
99
100
  }
100
101
 
102
+ function normalizeWorkerBaseUrl(workerDomain: string): string {
103
+ const trimmedDomain = workerDomain.trim().replace(/\/+$/, '');
104
+ if (trimmedDomain.startsWith('http://') || trimmedDomain.startsWith('https://')) {
105
+ return trimmedDomain;
106
+ }
107
+
108
+ return `https://${trimmedDomain}`;
109
+ }
110
+
111
+ function resolveDataWorkerBaseUrl(env: Env): string {
112
+ const configuredDomain = typeof env.DATA_WORKER_DOMAIN === 'string' ? env.DATA_WORKER_DOMAIN.trim() : '';
113
+ if (configuredDomain.length > 0) {
114
+ return normalizeWorkerBaseUrl(configuredDomain);
115
+ }
116
+
117
+ return normalizeWorkerBaseUrl(DEFAULT_DATA_WORKER_BASE_URL);
118
+ }
119
+
120
+ function resolveImageWorkerBaseUrl(env: Env): string {
121
+ const configuredDomain = typeof env.IMAGES_WORKER_DOMAIN === 'string' ? env.IMAGES_WORKER_DOMAIN.trim() : '';
122
+ if (configuredDomain.length > 0) {
123
+ return normalizeWorkerBaseUrl(configuredDomain);
124
+ }
125
+
126
+ return normalizeWorkerBaseUrl(DEFAULT_IMAGE_WORKER_BASE_URL);
127
+ }
128
+
101
129
  function base64UrlEncode(value: string | Uint8Array): string {
102
130
  const bytes = typeof value === 'string' ? textEncoder.encode(value) : value;
103
131
  let binary = '';
@@ -317,49 +345,86 @@ async function deleteSingleCase(env: Env, userUid: string, caseNumber: string):
317
345
  const dataApiKey = env.R2_KEY_SECRET;
318
346
  const imageApiKey = env.IMAGES_API_TOKEN;
319
347
 
320
- try {
321
- // Get case data from data worker
322
- const caseResponse = await fetch(`${DATA_WORKER_URL}/${encodeURIComponent(userUid)}/${encodeURIComponent(caseNumber)}/data.json`, {
323
- headers: { 'X-Custom-Auth-Key': dataApiKey }
324
- });
348
+ const dataWorkerBaseUrl = resolveDataWorkerBaseUrl(env);
349
+ const imageWorkerBaseUrl = resolveImageWorkerBaseUrl(env);
350
+ const encodedUserId = encodeURIComponent(userUid);
351
+ const encodedCaseNumber = encodeURIComponent(caseNumber);
325
352
 
326
- if (!caseResponse.ok) {
327
- return;
328
- }
353
+ const caseResponse = await fetch(`${dataWorkerBaseUrl}/${encodedUserId}/${encodedCaseNumber}/data.json`, {
354
+ headers: { 'X-Custom-Auth-Key': dataApiKey }
355
+ });
329
356
 
330
- const caseData: CaseData = await caseResponse.json();
357
+ if (caseResponse.status === 404) {
358
+ return;
359
+ }
331
360
 
332
- // Delete all files associated with this case
333
- if (caseData.files && caseData.files.length > 0) {
334
- for (const file of caseData.files) {
335
- try {
336
- // Delete image file - correct endpoint
337
- await fetch(`${IMAGE_WORKER_URL}/${encodeURIComponent(file.id)}`, {
338
- method: 'DELETE',
339
- headers: {
340
- 'Authorization': `Bearer ${imageApiKey}`
341
- }
342
- });
343
-
344
- // Delete notes file if exists
345
- await fetch(`${DATA_WORKER_URL}/${encodeURIComponent(userUid)}/${encodeURIComponent(caseNumber)}/${encodeURIComponent(file.id)}/data.json`, {
361
+ if (!caseResponse.ok) {
362
+ throw new Error(`Failed to load case data for deletion (${caseNumber}): ${caseResponse.status}`);
363
+ }
364
+
365
+ const caseData = await caseResponse.json() as CaseData;
366
+ const deletionErrors: string[] = [];
367
+
368
+ // Delete all files associated with this case
369
+ if (caseData.files && caseData.files.length > 0) {
370
+ for (const file of caseData.files) {
371
+ const encodedFileId = encodeURIComponent(file.id);
372
+
373
+ try {
374
+ const imageDeleteResponse = await fetch(`${imageWorkerBaseUrl}/${encodedFileId}`, {
375
+ method: 'DELETE',
376
+ headers: {
377
+ 'Authorization': `Bearer ${imageApiKey}`
378
+ }
379
+ });
380
+
381
+ if (!imageDeleteResponse.ok && imageDeleteResponse.status !== 404) {
382
+ deletionErrors.push(`image ${file.id} delete failed (${imageDeleteResponse.status})`);
383
+ }
384
+ } catch (error) {
385
+ const message = error instanceof Error ? error.message : 'unknown image delete error';
386
+ deletionErrors.push(`image ${file.id} delete threw (${message})`);
387
+ }
388
+
389
+ try {
390
+ const notesDeleteResponse = await fetch(
391
+ `${dataWorkerBaseUrl}/${encodedUserId}/${encodedCaseNumber}/${encodedFileId}/data.json`,
392
+ {
346
393
  method: 'DELETE',
347
394
  headers: { 'X-Custom-Auth-Key': dataApiKey }
348
- });
349
- } catch {
350
- // Continue with other files
395
+ }
396
+ );
397
+
398
+ if (!notesDeleteResponse.ok && notesDeleteResponse.status !== 404) {
399
+ deletionErrors.push(`annotation ${file.id} delete failed (${notesDeleteResponse.status})`);
351
400
  }
401
+ } catch (error) {
402
+ const message = error instanceof Error ? error.message : 'unknown annotation delete error';
403
+ deletionErrors.push(`annotation ${file.id} delete threw (${message})`);
352
404
  }
353
405
  }
406
+ }
354
407
 
355
- // Delete the case data file
356
- await fetch(`${DATA_WORKER_URL}/${encodeURIComponent(userUid)}/${encodeURIComponent(caseNumber)}/data.json`, {
357
- method: 'DELETE',
358
- headers: { 'X-Custom-Auth-Key': dataApiKey }
359
- });
408
+ // Delete case data file
409
+ try {
410
+ const caseDeleteResponse = await fetch(
411
+ `${dataWorkerBaseUrl}/${encodedUserId}/${encodedCaseNumber}/data.json`,
412
+ {
413
+ method: 'DELETE',
414
+ headers: { 'X-Custom-Auth-Key': dataApiKey }
415
+ }
416
+ );
360
417
 
361
- } catch {
362
- // Continue with user deletion even if case deletion fails
418
+ if (!caseDeleteResponse.ok && caseDeleteResponse.status !== 404) {
419
+ deletionErrors.push(`case ${caseNumber} delete failed (${caseDeleteResponse.status})`);
420
+ }
421
+ } catch (error) {
422
+ const message = error instanceof Error ? error.message : 'unknown case delete error';
423
+ deletionErrors.push(`case ${caseNumber} delete threw (${message})`);
424
+ }
425
+
426
+ if (deletionErrors.length > 0) {
427
+ throw new Error(`Case cleanup incomplete for ${caseNumber}: ${deletionErrors.join('; ')}`);
363
428
  }
364
429
  }
365
430
 
@@ -376,11 +441,10 @@ async function executeUserDeletion(
376
441
  const userObject: UserData = JSON.parse(userData);
377
442
  const ownedCases = (userObject.cases || []).map((caseItem) => caseItem.caseNumber);
378
443
  const readOnlyCases = (userObject.readOnlyCases || []).map((caseItem) => caseItem.caseNumber);
379
- const allCaseNumbers = [...ownedCases, ...readOnlyCases];
444
+ const allCaseNumbers = Array.from(new Set([...ownedCases, ...readOnlyCases]));
380
445
  const totalCases = allCaseNumbers.length;
381
446
  let completedCases = 0;
382
-
383
- await deleteFirebaseAuthUser(env, userUid);
447
+ const caseCleanupErrors: string[] = [];
384
448
 
385
449
  reportProgress?.({
386
450
  event: 'start',
@@ -396,17 +460,33 @@ async function executeUserDeletion(
396
460
  currentCaseNumber: caseNumber
397
461
  });
398
462
 
399
- await deleteSingleCase(env, userUid, caseNumber);
463
+ let caseDeletionError: string | null = null;
464
+ try {
465
+ await deleteSingleCase(env, userUid, caseNumber);
466
+ } catch (error) {
467
+ caseDeletionError = error instanceof Error ? error.message : `Case cleanup failed for ${caseNumber}`;
468
+ caseCleanupErrors.push(caseDeletionError);
469
+ console.error(`Case cleanup error for ${caseNumber}:`, error);
470
+ }
471
+
400
472
  completedCases += 1;
401
473
 
402
474
  reportProgress?.({
403
475
  event: 'case-complete',
404
476
  totalCases,
405
477
  completedCases,
406
- currentCaseNumber: caseNumber
478
+ currentCaseNumber: caseNumber,
479
+ success: caseDeletionError === null,
480
+ message: caseDeletionError || undefined
407
481
  });
408
482
  }
409
483
 
484
+ if (caseCleanupErrors.length > 0) {
485
+ throw new Error(`Failed to fully delete all case data: ${caseCleanupErrors.join(' | ')}`);
486
+ }
487
+
488
+ await deleteFirebaseAuthUser(env, userUid);
489
+
410
490
  // Delete the user account from the database
411
491
  await env.USER_DB.delete(userUid);
412
492