@striae-org/striae 4.3.4 → 5.1.0

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 (61) hide show
  1. package/.env.example +9 -2
  2. package/app/components/actions/case-export/download-handlers.ts +66 -11
  3. package/app/components/actions/case-import/confirmation-import.ts +50 -7
  4. package/app/components/actions/case-import/confirmation-package.ts +99 -22
  5. package/app/components/actions/case-import/orchestrator.ts +116 -13
  6. package/app/components/actions/case-import/validation.ts +171 -7
  7. package/app/components/actions/case-import/zip-processing.ts +224 -127
  8. package/app/components/actions/case-manage.ts +74 -15
  9. package/app/components/actions/confirm-export.ts +32 -3
  10. package/app/components/actions/generate-pdf.ts +43 -1
  11. package/app/components/actions/image-manage.ts +13 -45
  12. package/app/components/navbar/navbar.module.css +0 -10
  13. package/app/components/navbar/navbar.tsx +0 -22
  14. package/app/components/sidebar/case-import/case-import.module.css +7 -131
  15. package/app/components/sidebar/case-import/case-import.tsx +7 -14
  16. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +17 -60
  17. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +23 -39
  18. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +5 -45
  19. package/app/components/sidebar/case-import/components/FileSelector.tsx +5 -6
  20. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +2 -48
  21. package/app/components/sidebar/case-import/utils/file-validation.ts +9 -21
  22. package/app/config-example/config.json +5 -0
  23. package/app/routes/auth/login.tsx +1 -1
  24. package/app/routes/striae/hooks/use-striae-reset-helpers.ts +4 -0
  25. package/app/routes/striae/striae.tsx +15 -4
  26. package/app/utils/data/operations/case-operations.ts +13 -1
  27. package/app/utils/data/operations/confirmation-summary-operations.ts +38 -1
  28. package/app/utils/data/operations/file-annotation-operations.ts +13 -1
  29. package/app/utils/data/operations/signing-operations.ts +93 -0
  30. package/app/utils/data/operations/types.ts +6 -0
  31. package/app/utils/forensics/export-encryption.ts +316 -0
  32. package/app/utils/forensics/export-verification.ts +1 -409
  33. package/app/utils/forensics/index.ts +1 -0
  34. package/app/utils/ui/case-messages.ts +5 -2
  35. package/package.json +2 -2
  36. package/scripts/deploy-config.sh +244 -7
  37. package/scripts/deploy-pages-secrets.sh +0 -6
  38. package/scripts/deploy-worker-secrets.sh +66 -5
  39. package/scripts/encrypt-r2-backfill.mjs +376 -0
  40. package/worker-configuration.d.ts +13 -7
  41. package/workers/audit-worker/package.json +1 -4
  42. package/workers/audit-worker/src/audit-worker.example.ts +522 -61
  43. package/workers/audit-worker/wrangler.jsonc.example +6 -1
  44. package/workers/data-worker/package.json +1 -4
  45. package/workers/data-worker/src/data-worker.example.ts +409 -1
  46. package/workers/data-worker/src/encryption-utils.ts +269 -0
  47. package/workers/data-worker/worker-configuration.d.ts +1 -1
  48. package/workers/data-worker/wrangler.jsonc.example +6 -2
  49. package/workers/image-worker/package.json +1 -4
  50. package/workers/image-worker/src/encryption-utils.ts +217 -0
  51. package/workers/image-worker/src/image-worker.example.ts +196 -127
  52. package/workers/image-worker/wrangler.jsonc.example +8 -1
  53. package/workers/keys-worker/package.json +1 -4
  54. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  55. package/workers/pdf-worker/package.json +1 -4
  56. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  57. package/workers/user-worker/package.json +1 -4
  58. package/workers/user-worker/wrangler.jsonc.example +1 -1
  59. package/wrangler.toml.example +1 -1
  60. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +0 -287
  61. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +0 -470
@@ -1,470 +0,0 @@
1
- import {
2
- useEffect,
3
- useId,
4
- useRef,
5
- useState,
6
- type ChangeEvent,
7
- type DragEvent,
8
- } from 'react';
9
- import styles from './public-signing-key-modal.module.css';
10
- import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
11
- import { verifyExportFile } from '~/utils/forensics';
12
-
13
- const NO_PUBLIC_KEY_MESSAGE = 'No public signing key is configured for this environment.';
14
-
15
- interface SelectedPublicKeyFile {
16
- name: string;
17
- content: string;
18
- source: 'download' | 'upload';
19
- }
20
-
21
- interface VerificationOutcome {
22
- state: 'pass' | 'fail';
23
- message: string;
24
- }
25
-
26
- interface VerificationDropZoneProps {
27
- inputId: string;
28
- label: string;
29
- accept: string;
30
- emptyText: string;
31
- helperText: string;
32
- selectedFileName?: string | null;
33
- selectedDescription?: string;
34
- errorMessage?: string;
35
- isDisabled?: boolean;
36
- onFileSelected: (file: File) => void | Promise<void>;
37
- onClear?: () => void;
38
- actionButton?: {
39
- label: string;
40
- onClick: () => void;
41
- disabled?: boolean;
42
- };
43
- }
44
-
45
- const VerificationDropZone = ({
46
- inputId,
47
- label,
48
- accept,
49
- emptyText,
50
- helperText,
51
- selectedFileName,
52
- selectedDescription,
53
- errorMessage,
54
- isDisabled = false,
55
- onFileSelected,
56
- onClear,
57
- actionButton
58
- }: VerificationDropZoneProps) => {
59
- const [isDragOver, setIsDragOver] = useState(false);
60
- const inputRef = useRef<HTMLInputElement>(null);
61
-
62
- const openFilePicker = () => {
63
- if (!isDisabled) {
64
- inputRef.current?.click();
65
- }
66
- };
67
-
68
- const handleSelectedFile = (file?: File) => {
69
- if (!file || isDisabled) {
70
- return;
71
- }
72
-
73
- void onFileSelected(file);
74
-
75
- if (inputRef.current) {
76
- inputRef.current.value = '';
77
- }
78
- };
79
-
80
- const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
81
- handleSelectedFile(event.target.files?.[0]);
82
- };
83
-
84
- const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
85
- event.preventDefault();
86
- if (!isDisabled) {
87
- setIsDragOver(true);
88
- }
89
- };
90
-
91
- const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
92
- event.preventDefault();
93
- const relatedTarget = event.relatedTarget as HTMLElement | null;
94
-
95
- if (!relatedTarget || !event.currentTarget.contains(relatedTarget)) {
96
- setIsDragOver(false);
97
- }
98
- };
99
-
100
- const handleDrop = (event: DragEvent<HTMLDivElement>) => {
101
- event.preventDefault();
102
- setIsDragOver(false);
103
- handleSelectedFile(event.dataTransfer.files?.[0]);
104
- };
105
-
106
- return (
107
- <div className={styles.verificationField}>
108
- <div className={styles.fieldHeader}>
109
- <label htmlFor={inputId} className={styles.fieldLabel}>
110
- {label}
111
- </label>
112
- {selectedFileName && onClear && (
113
- <button type="button" className={styles.clearButton} onClick={onClear}>
114
- Clear
115
- </button>
116
- )}
117
- </div>
118
-
119
- <input
120
- id={inputId}
121
- ref={inputRef}
122
- type="file"
123
- accept={accept}
124
- onChange={handleInputChange}
125
- disabled={isDisabled}
126
- className={styles.hiddenFileInput}
127
- />
128
-
129
- <div
130
- className={`${styles.dropZone} ${isDragOver ? styles.dropZoneActive : ''} ${isDisabled ? styles.dropZoneDisabled : ''}`}
131
- onClick={openFilePicker}
132
- onDragOver={handleDragOver}
133
- onDragLeave={handleDragLeave}
134
- onDrop={handleDrop}
135
- role="button"
136
- tabIndex={isDisabled ? -1 : 0}
137
- aria-disabled={isDisabled}
138
- aria-label={label}
139
- onKeyDown={(event) => {
140
- if ((event.key === 'Enter' || event.key === ' ') && !isDisabled) {
141
- if (event.key === ' ') {
142
- event.preventDefault();
143
- }
144
- openFilePicker();
145
- }
146
- }}
147
- >
148
- <p className={styles.dropZonePrimary}>
149
- {isDragOver ? 'Drop file to continue' : selectedFileName || emptyText}
150
- </p>
151
- <p className={styles.dropZoneSecondary}>{selectedFileName ? selectedDescription : helperText}</p>
152
- </div>
153
-
154
- <div className={styles.fieldActions}>
155
- <button type="button" className={styles.secondaryButton} onClick={openFilePicker} disabled={isDisabled}>
156
- Choose File
157
- </button>
158
- {actionButton && (
159
- <button
160
- type="button"
161
- className={styles.secondaryButton}
162
- onClick={actionButton.onClick}
163
- disabled={isDisabled || actionButton.disabled}
164
- >
165
- {actionButton.label}
166
- </button>
167
- )}
168
- </div>
169
-
170
- {errorMessage && <p className={styles.fieldError}>{errorMessage}</p>}
171
- </div>
172
- );
173
- };
174
-
175
- function createPublicKeyDownloadFileName(publicSigningKeyId?: string | null): string {
176
- const normalizedKeyId =
177
- typeof publicSigningKeyId === 'string' && publicSigningKeyId.trim().length > 0
178
- ? `-${publicSigningKeyId.trim().replace(/[^a-z0-9_-]+/gi, '-')}`
179
- : '';
180
-
181
- return `striae-public-signing-key${normalizedKeyId}.pem`;
182
- }
183
-
184
- function downloadTextFile(fileName: string, content: string, mimeType: string): void {
185
- const blob = new Blob([content], { type: mimeType });
186
- const objectUrl = URL.createObjectURL(blob);
187
- const linkElement = document.createElement('a');
188
-
189
- linkElement.href = objectUrl;
190
- linkElement.download = fileName;
191
- linkElement.style.display = 'none';
192
-
193
- document.body.appendChild(linkElement);
194
- linkElement.click();
195
- document.body.removeChild(linkElement);
196
-
197
- window.setTimeout(() => {
198
- URL.revokeObjectURL(objectUrl);
199
- }, 0);
200
- }
201
-
202
- function formatFileSize(bytes: number): string {
203
- if (bytes < 1024 * 1024) {
204
- return `${(bytes / 1024).toFixed(1)} KB`;
205
- }
206
-
207
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
208
- }
209
-
210
- interface PublicSigningKeyModalProps {
211
- isOpen: boolean;
212
- onClose: () => void;
213
- publicSigningKeyId?: string | null;
214
- publicKeyPem?: string | null;
215
- }
216
-
217
- export const PublicSigningKeyModal = ({
218
- isOpen,
219
- onClose,
220
- publicSigningKeyId,
221
- publicKeyPem
222
- }: PublicSigningKeyModalProps) => {
223
- const [selectedPublicKey, setSelectedPublicKey] = useState<SelectedPublicKeyFile | null>(null);
224
- const [selectedExportFile, setSelectedExportFile] = useState<File | null>(null);
225
- const [keyError, setKeyError] = useState('');
226
- const [exportFileError, setExportFileError] = useState('');
227
- const [verificationOutcome, setVerificationOutcome] = useState<VerificationOutcome | null>(null);
228
- const [isVerifying, setIsVerifying] = useState(false);
229
- const publicSigningKeyTitleId = useId();
230
- const publicKeyInputId = useId();
231
- const exportFileInputId = useId();
232
- const {
233
- overlayProps,
234
- getCloseButtonProps
235
- } = useOverlayDismiss({
236
- isOpen,
237
- onClose
238
- });
239
-
240
- useEffect(() => {
241
- if (!isOpen) {
242
- setSelectedPublicKey(null);
243
- setSelectedExportFile(null);
244
- setKeyError('');
245
- setExportFileError('');
246
- setVerificationOutcome(null);
247
- setIsVerifying(false);
248
- }
249
- }, [isOpen]);
250
-
251
- if (!isOpen) {
252
- return null;
253
- }
254
-
255
- const resetVerificationState = () => {
256
- setVerificationOutcome(null);
257
- };
258
-
259
- const handlePublicKeySelected = async (file: File) => {
260
- try {
261
- const lowerName = file.name.toLowerCase();
262
- if (!lowerName.endsWith('.pem')) {
263
- setKeyError('Select a PEM public key file.');
264
- return;
265
- }
266
-
267
- const content = await file.text();
268
- if (!content.includes('-----BEGIN PUBLIC KEY-----') || !content.includes('-----END PUBLIC KEY-----')) {
269
- setKeyError('The selected file is not a valid PEM public key file.');
270
- return;
271
- }
272
-
273
- setSelectedPublicKey({
274
- name: file.name,
275
- content,
276
- source: 'upload'
277
- });
278
- setKeyError('');
279
- resetVerificationState();
280
- } catch {
281
- setKeyError('The public key file could not be read.');
282
- }
283
- };
284
-
285
- const handleDownloadCurrentPublicKey = () => {
286
- if (!publicKeyPem) {
287
- setKeyError(NO_PUBLIC_KEY_MESSAGE);
288
- return;
289
- }
290
-
291
- const fileName = createPublicKeyDownloadFileName(publicSigningKeyId);
292
- downloadTextFile(fileName, publicKeyPem, 'application/x-pem-file');
293
- setSelectedPublicKey({
294
- name: fileName,
295
- content: publicKeyPem,
296
- source: 'download'
297
- });
298
- setKeyError('');
299
- resetVerificationState();
300
- };
301
-
302
- const handleExportFileSelected = async (file: File) => {
303
- const lowerName = file.name.toLowerCase();
304
-
305
- if (!lowerName.endsWith('.zip') && !lowerName.endsWith('.json')) {
306
- setExportFileError('Select a confirmation JSON/ZIP file, standalone audit JSON export, or a case export ZIP file.');
307
- return;
308
- }
309
-
310
- setSelectedExportFile(file);
311
- setExportFileError('');
312
- resetVerificationState();
313
- };
314
-
315
- const handleVerify = async () => {
316
- const hasPublicKey = !!selectedPublicKey?.content;
317
- const hasExportFile = !!selectedExportFile;
318
-
319
- setKeyError(hasPublicKey ? '' : 'Select or download a public key PEM file first.');
320
- setExportFileError(hasExportFile ? '' : 'Select a confirmation JSON/ZIP file, standalone audit JSON export, or a case export ZIP file.');
321
-
322
- if (!hasPublicKey || !hasExportFile || !selectedPublicKey || !selectedExportFile) {
323
- return;
324
- }
325
-
326
- setIsVerifying(true);
327
- setVerificationOutcome(null);
328
-
329
- try {
330
- const result = await verifyExportFile(selectedExportFile, selectedPublicKey.content);
331
- setVerificationOutcome({
332
- state: result.isValid ? 'pass' : 'fail',
333
- message: result.message
334
- });
335
- } finally {
336
- setIsVerifying(false);
337
- }
338
- };
339
-
340
- const selectedKeyDescription = selectedPublicKey
341
- ? selectedPublicKey.source === 'download'
342
- ? 'Downloaded from this Striae environment and ready to use.'
343
- : 'Loaded from your device and ready to use.'
344
- : undefined;
345
-
346
- const selectedExportDescription = selectedExportFile
347
- ? `${(() => {
348
- const lowerName = selectedExportFile.name.toLowerCase();
349
- if (lowerName.endsWith('.zip')) {
350
- return lowerName.includes('confirmation-data-') ? 'Confirmation ZIP' : 'Case export ZIP';
351
- }
352
-
353
- if (lowerName.includes('audit')) {
354
- return 'Audit JSON';
355
- }
356
-
357
- return 'JSON export';
358
- })()} • ${formatFileSize(selectedExportFile.size)}`
359
- : undefined;
360
-
361
- return (
362
- <div
363
- className={styles.overlay}
364
- aria-label="Close public signing key dialog"
365
- {...overlayProps}
366
- >
367
- <div
368
- className={styles.modal}
369
- role="dialog"
370
- aria-modal="true"
371
- aria-labelledby={publicSigningKeyTitleId}
372
- >
373
- <div className={styles.header}>
374
- <h3 id={publicSigningKeyTitleId} className={styles.title}>
375
- Striae Verification Utility
376
- </h3>
377
- <button
378
- className={styles.closeButton}
379
- {...getCloseButtonProps({ ariaLabel: 'Close public signing key dialog' })}
380
- >
381
- &times;
382
- </button>
383
- </div>
384
-
385
- <div className={styles.content}>
386
- <p className={styles.description}>
387
- Drop a public key PEM file and a Striae confirmation JSON/ZIP, standalone audit JSON export, or case export ZIP, then run
388
- verification directly in the browser.
389
- </p>
390
-
391
- {publicSigningKeyId && (
392
- <p className={styles.meta}>
393
- Current key ID: <span>{publicSigningKeyId}</span>
394
- </p>
395
- )}
396
-
397
- <div className={styles.verifierLayout}>
398
- <VerificationDropZone
399
- inputId={publicKeyInputId}
400
- label="1. Public Key PEM"
401
- accept=".pem"
402
- emptyText="Drop a public key PEM file here"
403
- helperText="Use a .pem file containing the Striae public signing key."
404
- selectedFileName={selectedPublicKey?.name}
405
- selectedDescription={selectedKeyDescription}
406
- errorMessage={keyError}
407
- onFileSelected={handlePublicKeySelected}
408
- onClear={() => {
409
- setSelectedPublicKey(null);
410
- setKeyError('');
411
- resetVerificationState();
412
- }}
413
- actionButton={{
414
- label: 'Download Current Public Key',
415
- onClick: handleDownloadCurrentPublicKey,
416
- disabled: !publicKeyPem
417
- }}
418
- />
419
-
420
- <VerificationDropZone
421
- inputId={exportFileInputId}
422
- label="2. Confirmation File or Export ZIP"
423
- accept=".json,.zip"
424
- emptyText="Drop a confirmation JSON/ZIP, audit JSON, or case export ZIP here"
425
- helperText="Case exports use .zip. Confirmation exports can be .json or .zip. Audit exports are supported as standalone .json files."
426
- selectedFileName={selectedExportFile?.name}
427
- selectedDescription={selectedExportDescription}
428
- errorMessage={exportFileError}
429
- onFileSelected={handleExportFileSelected}
430
- onClear={() => {
431
- setSelectedExportFile(null);
432
- setExportFileError('');
433
- resetVerificationState();
434
- }}
435
- />
436
- </div>
437
-
438
- {verificationOutcome && (
439
- <div
440
- className={`${styles.resultCard} ${verificationOutcome.state === 'pass' ? styles.resultPass : styles.resultFail}`}
441
- role="status"
442
- aria-live="polite"
443
- >
444
- <p className={styles.resultTitle}>{verificationOutcome.state === 'pass' ? 'PASS' : 'FAIL'}</p>
445
- <p className={styles.resultMessage}>{verificationOutcome.message}</p>
446
- </div>
447
- )}
448
-
449
- <div className={styles.actions}>
450
- <button
451
- type="button"
452
- className={styles.primaryButton}
453
- onClick={handleVerify}
454
- disabled={isVerifying || !selectedPublicKey || !selectedExportFile}
455
- >
456
- {isVerifying ? 'Verifying...' : 'Verify File'}
457
- </button>
458
- <button
459
- type="button"
460
- className={styles.secondaryButton}
461
- onClick={onClose}
462
- >
463
- Close
464
- </button>
465
- </div>
466
- </div>
467
- </div>
468
- </div>
469
- );
470
- };