@striae-org/striae 4.1.0 → 4.2.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 (124) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +1 -1
  3. package/app/components/actions/case-export/core-export.ts +14 -8
  4. package/app/components/actions/case-export/data-processing.ts +1 -0
  5. package/app/components/actions/case-export/download-handlers.ts +7 -0
  6. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  7. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  8. package/app/components/actions/case-import/orchestrator.ts +78 -32
  9. package/app/components/actions/case-import/storage-operations.ts +97 -8
  10. package/app/components/actions/case-import/zip-processing.ts +159 -86
  11. package/app/components/actions/case-manage.ts +463 -8
  12. package/app/components/actions/confirm-export.ts +9 -2
  13. package/app/components/actions/image-manage.ts +77 -44
  14. package/app/components/audit/user-audit-viewer.tsx +19 -8
  15. package/app/components/audit/user-audit.module.css +21 -0
  16. package/app/components/audit/viewer/audit-entries-list.tsx +12 -2
  17. package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
  18. package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
  19. package/app/components/audit/viewer/use-audit-viewer-data.ts +24 -1
  20. package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
  21. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  22. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  23. package/app/components/canvas/canvas.module.css +64 -54
  24. package/app/components/canvas/canvas.tsx +14 -16
  25. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  26. package/app/components/canvas/confirmation/confirmation.tsx +12 -14
  27. package/app/components/colors/colors.module.css +4 -3
  28. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  29. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  30. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  31. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  32. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  33. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  34. package/app/components/navbar/navbar.module.css +447 -0
  35. package/app/components/navbar/navbar.tsx +402 -0
  36. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
  37. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
  38. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  39. package/app/components/sidebar/case-export/case-export.tsx +8 -46
  40. package/app/components/sidebar/case-import/case-import.module.css +23 -0
  41. package/app/components/sidebar/case-import/case-import.tsx +64 -16
  42. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  43. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  44. package/app/components/sidebar/cases/case-sidebar.tsx +68 -588
  45. package/app/components/sidebar/cases/cases-modal.module.css +1 -0
  46. package/app/components/sidebar/cases/cases-modal.tsx +82 -43
  47. package/app/components/sidebar/cases/cases.module.css +82 -21
  48. package/app/components/sidebar/files/files-modal.module.css +1 -0
  49. package/app/components/sidebar/files/files-modal.tsx +49 -52
  50. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  51. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +187 -138
  52. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  53. package/app/components/sidebar/notes/notes-editor-modal.tsx +64 -0
  54. package/app/components/sidebar/notes/notes.module.css +170 -1
  55. package/app/components/sidebar/sidebar-container.tsx +16 -28
  56. package/app/components/sidebar/sidebar.module.css +5 -69
  57. package/app/components/sidebar/sidebar.tsx +27 -125
  58. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  59. package/app/components/user/inactivity-warning.module.css +1 -0
  60. package/app/components/user/inactivity-warning.tsx +15 -2
  61. package/app/components/user/manage-profile.tsx +23 -10
  62. package/app/{tailwind.css → global.css} +1 -3
  63. package/app/hooks/useOverlayDismiss.ts +54 -4
  64. package/app/root.tsx +1 -1
  65. package/app/routes/auth/login.tsx +785 -774
  66. package/app/routes/striae/striae.module.css +10 -3
  67. package/app/routes/striae/striae.tsx +475 -30
  68. package/app/services/audit/audit.service.ts +173 -27
  69. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  70. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
  71. package/app/services/audit/builders/index.ts +1 -0
  72. package/app/types/audit.ts +4 -1
  73. package/app/types/case.ts +29 -0
  74. package/app/types/import.ts +3 -0
  75. package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
  76. package/app/utils/data/data-operations.ts +17 -861
  77. package/app/utils/data/index.ts +11 -1
  78. package/app/utils/data/operations/batch-operations.ts +113 -0
  79. package/app/utils/data/operations/case-operations.ts +168 -0
  80. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  81. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  82. package/app/utils/data/operations/index.ts +7 -0
  83. package/app/utils/data/operations/signing-operations.ts +225 -0
  84. package/app/utils/data/operations/types.ts +42 -0
  85. package/app/utils/data/operations/validation-operations.ts +48 -0
  86. package/app/utils/data/permissions.ts +16 -1
  87. package/app/utils/forensics/audit-export-signature.ts +5 -1
  88. package/app/utils/forensics/confirmation-signature.ts +3 -0
  89. package/app/utils/forensics/export-verification.ts +426 -22
  90. package/functions/api/_shared/firebase-auth.ts +2 -7
  91. package/functions/api/image/[[path]].ts +20 -23
  92. package/functions/api/pdf/[[path]].ts +27 -8
  93. package/package.json +7 -12
  94. package/scripts/deploy-primershear-emails.sh +2 -1
  95. package/worker-configuration.d.ts +3 -3
  96. package/workers/audit-worker/package.json +1 -1
  97. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  98. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  99. package/workers/data-worker/package.json +1 -1
  100. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  101. package/workers/data-worker/wrangler.jsonc.example +1 -1
  102. package/workers/image-worker/package.json +1 -1
  103. package/workers/image-worker/src/image-worker.example.ts +16 -5
  104. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  105. package/workers/image-worker/wrangler.jsonc.example +1 -1
  106. package/workers/keys-worker/package.json +1 -1
  107. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  108. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  109. package/workers/pdf-worker/package.json +1 -1
  110. package/workers/pdf-worker/src/formats/format-striae.ts +9 -14
  111. package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
  112. package/workers/pdf-worker/src/report-types.ts +3 -3
  113. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  114. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  115. package/workers/user-worker/package.json +1 -1
  116. package/workers/user-worker/src/user-worker.example.ts +17 -0
  117. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  118. package/workers/user-worker/wrangler.jsonc.example +1 -1
  119. package/wrangler.toml.example +1 -1
  120. package/NOTICE +0 -13
  121. package/app/components/sidebar/notes/notes-modal.tsx +0 -53
  122. package/postcss.config.js +0 -6
  123. package/public/.well-known/keybase.txt +0 -56
  124. package/tailwind.config.ts +0 -22
@@ -0,0 +1,279 @@
1
+ import type { User } from 'firebase/auth';
2
+ import { type AnnotationData } from '~/types';
3
+
4
+ export interface FileConfirmationSummary {
5
+ includeConfirmation: boolean;
6
+ isConfirmed: boolean;
7
+ updatedAt: string;
8
+ }
9
+
10
+ export interface CaseConfirmationSummary {
11
+ includeConfirmation: boolean;
12
+ isConfirmed: boolean;
13
+ updatedAt: string;
14
+ filesById: Record<string, FileConfirmationSummary>;
15
+ }
16
+
17
+ export interface UserConfirmationSummaryDocument {
18
+ version: number;
19
+ updatedAt: string;
20
+ cases: Record<string, CaseConfirmationSummary>;
21
+ }
22
+
23
+ export interface ConfirmationSummaryEnsureOptions {
24
+ forceRefresh?: boolean;
25
+ maxAgeMs?: number;
26
+ }
27
+
28
+ export interface ConfirmationSummaryTelemetry {
29
+ ensureCalls: number;
30
+ caseCacheHits: number;
31
+ caseMisses: number;
32
+ forceRefreshCalls: number;
33
+ staleCaseRefreshes: number;
34
+ staleFileRefreshes: number;
35
+ missingFileRefreshes: number;
36
+ removedFileEntries: number;
37
+ refreshedFileEntries: number;
38
+ summaryWrites: number;
39
+ }
40
+
41
+ export const CONFIRMATION_SUMMARY_VERSION = 1;
42
+ export const DEFAULT_CONFIRMATION_SUMMARY_MAX_AGE_MS = 5 * 60 * 1000;
43
+
44
+ const CONFIRMATION_SUMMARY_LOG_INTERVAL = 25;
45
+
46
+ const confirmationSummaryTelemetry: ConfirmationSummaryTelemetry = {
47
+ ensureCalls: 0,
48
+ caseCacheHits: 0,
49
+ caseMisses: 0,
50
+ forceRefreshCalls: 0,
51
+ staleCaseRefreshes: 0,
52
+ staleFileRefreshes: 0,
53
+ missingFileRefreshes: 0,
54
+ removedFileEntries: 0,
55
+ refreshedFileEntries: 0,
56
+ summaryWrites: 0
57
+ };
58
+
59
+ export function getConfirmationSummaryTelemetry(): ConfirmationSummaryTelemetry {
60
+ return { ...confirmationSummaryTelemetry };
61
+ }
62
+
63
+ export function resetConfirmationSummaryTelemetry(): void {
64
+ for (const key of Object.keys(confirmationSummaryTelemetry) as Array<keyof ConfirmationSummaryTelemetry>) {
65
+ confirmationSummaryTelemetry[key] = 0;
66
+ }
67
+ }
68
+
69
+ function getGlobalDebugFlag(): boolean {
70
+ const globalScope = globalThis as unknown as {
71
+ __STRIAE_DEBUG_CONFIRMATION_CACHE__?: boolean;
72
+ };
73
+
74
+ return globalScope.__STRIAE_DEBUG_CONFIRMATION_CACHE__ === true;
75
+ }
76
+
77
+ function getLocalStorageDebugFlag(): boolean {
78
+ if (typeof window === 'undefined' || !window.localStorage) {
79
+ return false;
80
+ }
81
+
82
+ try {
83
+ return window.localStorage.getItem('striae.debug.confirmationCache') === 'true';
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ function shouldLogConfirmationSummaryTelemetry(): boolean {
90
+ return getGlobalDebugFlag() || getLocalStorageDebugFlag();
91
+ }
92
+
93
+ function maybeLogConfirmationSummaryTelemetrySnapshot(): void {
94
+ if (!shouldLogConfirmationSummaryTelemetry()) {
95
+ return;
96
+ }
97
+
98
+ if (
99
+ confirmationSummaryTelemetry.ensureCalls === 0 ||
100
+ confirmationSummaryTelemetry.ensureCalls % CONFIRMATION_SUMMARY_LOG_INTERVAL !== 0
101
+ ) {
102
+ return;
103
+ }
104
+
105
+ const totalCaseLookups =
106
+ confirmationSummaryTelemetry.caseCacheHits + confirmationSummaryTelemetry.caseMisses;
107
+ const caseCacheHitRate =
108
+ totalCaseLookups > 0
109
+ ? Math.round((confirmationSummaryTelemetry.caseCacheHits / totalCaseLookups) * 100)
110
+ : 0;
111
+
112
+ console.info('[confirmation-cache] summary', {
113
+ ensureCalls: confirmationSummaryTelemetry.ensureCalls,
114
+ caseCacheHitRate,
115
+ caseCacheHits: confirmationSummaryTelemetry.caseCacheHits,
116
+ caseMisses: confirmationSummaryTelemetry.caseMisses,
117
+ forceRefreshCalls: confirmationSummaryTelemetry.forceRefreshCalls,
118
+ staleCaseRefreshes: confirmationSummaryTelemetry.staleCaseRefreshes,
119
+ missingFileRefreshes: confirmationSummaryTelemetry.missingFileRefreshes,
120
+ staleFileRefreshes: confirmationSummaryTelemetry.staleFileRefreshes,
121
+ refreshedFileEntries: confirmationSummaryTelemetry.refreshedFileEntries,
122
+ removedFileEntries: confirmationSummaryTelemetry.removedFileEntries,
123
+ summaryWrites: confirmationSummaryTelemetry.summaryWrites
124
+ });
125
+ }
126
+
127
+ export function trackEnsureCall(): void {
128
+ confirmationSummaryTelemetry.ensureCalls += 1;
129
+ maybeLogConfirmationSummaryTelemetrySnapshot();
130
+ }
131
+
132
+ export function trackCaseMiss(): void {
133
+ confirmationSummaryTelemetry.caseMisses += 1;
134
+ }
135
+
136
+ export function trackCaseHit(): void {
137
+ confirmationSummaryTelemetry.caseCacheHits += 1;
138
+ }
139
+
140
+ export function trackForceRefreshCall(): void {
141
+ confirmationSummaryTelemetry.forceRefreshCalls += 1;
142
+ }
143
+
144
+ export function trackStaleCaseRefresh(): void {
145
+ confirmationSummaryTelemetry.staleCaseRefreshes += 1;
146
+ }
147
+
148
+ export function trackMissingFileRefresh(): void {
149
+ confirmationSummaryTelemetry.missingFileRefreshes += 1;
150
+ }
151
+
152
+ export function trackStaleFileRefresh(): void {
153
+ confirmationSummaryTelemetry.staleFileRefreshes += 1;
154
+ }
155
+
156
+ export function trackRemovedFileEntry(): void {
157
+ confirmationSummaryTelemetry.removedFileEntries += 1;
158
+ }
159
+
160
+ export function trackRefreshedFileEntry(): void {
161
+ confirmationSummaryTelemetry.refreshedFileEntries += 1;
162
+ }
163
+
164
+ export function trackSummaryWrite(): void {
165
+ confirmationSummaryTelemetry.summaryWrites += 1;
166
+ }
167
+
168
+ export function getIsoNow(): string {
169
+ return new Date().toISOString();
170
+ }
171
+
172
+ export function createEmptyConfirmationSummary(): UserConfirmationSummaryDocument {
173
+ return {
174
+ version: CONFIRMATION_SUMMARY_VERSION,
175
+ updatedAt: getIsoNow(),
176
+ cases: {}
177
+ };
178
+ }
179
+
180
+ export function buildConfirmationSummaryPath(user: User): string {
181
+ return `/${encodeURIComponent(user.uid)}/meta/confirmation-status.json`;
182
+ }
183
+
184
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
185
+ return !!value && typeof value === 'object' && !Array.isArray(value);
186
+ }
187
+
188
+ function normalizeFileConfirmationSummary(value: unknown): FileConfirmationSummary {
189
+ if (!isPlainObject(value)) {
190
+ return {
191
+ includeConfirmation: false,
192
+ isConfirmed: false,
193
+ updatedAt: getIsoNow()
194
+ };
195
+ }
196
+
197
+ return {
198
+ includeConfirmation: value.includeConfirmation === true,
199
+ isConfirmed: value.isConfirmed === true,
200
+ updatedAt: typeof value.updatedAt === 'string' && value.updatedAt.length > 0 ? value.updatedAt : getIsoNow()
201
+ };
202
+ }
203
+
204
+ export function isStaleTimestamp(timestamp: string, maxAgeMs: number): boolean {
205
+ const parsed = Date.parse(timestamp);
206
+ if (Number.isNaN(parsed)) {
207
+ return true;
208
+ }
209
+
210
+ return Date.now() - parsed > maxAgeMs;
211
+ }
212
+
213
+ export function computeCaseConfirmationAggregate(filesById: Record<string, FileConfirmationSummary>): {
214
+ includeConfirmation: boolean;
215
+ isConfirmed: boolean;
216
+ } {
217
+ const statuses = Object.values(filesById);
218
+ const filesRequiringConfirmation = statuses.filter((entry) => entry.includeConfirmation);
219
+ const includeConfirmation = filesRequiringConfirmation.length > 0;
220
+ const isConfirmed = includeConfirmation ? filesRequiringConfirmation.every((entry) => entry.isConfirmed) : false;
221
+
222
+ return {
223
+ includeConfirmation,
224
+ isConfirmed
225
+ };
226
+ }
227
+
228
+ export function toFileConfirmationSummary(annotationData: AnnotationData | null): FileConfirmationSummary {
229
+ const includeConfirmation = annotationData?.includeConfirmation === true;
230
+
231
+ return {
232
+ includeConfirmation,
233
+ isConfirmed: includeConfirmation && !!annotationData?.confirmationData,
234
+ updatedAt: getIsoNow()
235
+ };
236
+ }
237
+
238
+ export function normalizeConfirmationSummaryDocument(payload: unknown): UserConfirmationSummaryDocument {
239
+ if (!isPlainObject(payload) || !isPlainObject(payload.cases)) {
240
+ return createEmptyConfirmationSummary();
241
+ }
242
+
243
+ const normalizedCases: Record<string, CaseConfirmationSummary> = {};
244
+
245
+ for (const [caseNumber, rawCaseEntry] of Object.entries(payload.cases)) {
246
+ if (!isPlainObject(rawCaseEntry) || !isPlainObject(rawCaseEntry.filesById)) {
247
+ continue;
248
+ }
249
+
250
+ const filesById: Record<string, FileConfirmationSummary> = {};
251
+ for (const [fileId, rawFileEntry] of Object.entries(rawCaseEntry.filesById)) {
252
+ filesById[fileId] = normalizeFileConfirmationSummary(rawFileEntry);
253
+ }
254
+
255
+ const aggregate = computeCaseConfirmationAggregate(filesById);
256
+
257
+ normalizedCases[caseNumber] = {
258
+ includeConfirmation: aggregate.includeConfirmation,
259
+ isConfirmed: aggregate.isConfirmed,
260
+ updatedAt:
261
+ typeof rawCaseEntry.updatedAt === 'string' && rawCaseEntry.updatedAt.length > 0
262
+ ? rawCaseEntry.updatedAt
263
+ : getIsoNow(),
264
+ filesById
265
+ };
266
+ }
267
+
268
+ return {
269
+ version:
270
+ typeof payload.version === 'number' && Number.isFinite(payload.version)
271
+ ? payload.version
272
+ : CONFIRMATION_SUMMARY_VERSION,
273
+ updatedAt:
274
+ typeof payload.updatedAt === 'string' && payload.updatedAt.length > 0
275
+ ? payload.updatedAt
276
+ : getIsoNow(),
277
+ cases: normalizedCases
278
+ };
279
+ }